从去年开始,越来越多的团队把 LLM 接进了日常开发流水线:让模型写 CRUD 接口、补单元测试、生成数据解析脚本、甚至充当 Agent 自动调用工具。但我发现一个共性现象:LLM 生成的代码“第一眼很正确”,一旦进入边界条件、异常分支、副作用场景,就很容易暴露出问题。尤其是自动生成的工具函数被 Agent 调用时,一个“看起来没问题”的函数可能悄悄修改了数据库状态、发起了网络请求,或者在没有做任何校验的情况下把脏数据继续往后传。
这篇文章想讨论两个被很多人忽略、但正在成为 LLM 生成代码核心工程问题的概念:Design by Contract(契约式设计)和Effects(效果/副作用机制)。我会先讲清楚这两个概念是什么,再围绕一个真实的订单解析场景,演示如何把契约和 effect 标记嵌入 LLM 生成的代码,最后给出能够落地的工程建议。
如果你正在用 LLM 辅助开发,或者正在搭建 LLM Agent 工具调用链路,这篇内容应该能帮你在“代码质量”和“模型输出可控性”之间找到一个更可靠的中间层。
1. 为什么设计契约和 effects 是 LLM 生成代码的关键工程问题
1.1 LLM 生成代码的价值与风险管理
先看一个很常见的开发场景。业务方给了一段需求:解析订单文本,计算订单总额,并支持折扣。开发者把这几个函数丢给 LLM,几秒钟之后模型就输出了完整代码。代码风格统一、命名规范、甚至附带了注释。复制进项目,跑一下正常路径,结果完全正确。
问题出在哪里?
- 如果传入的
order_text是空字符串,函数会怎样? - 如果折扣率是负数,函数会怎样?
- 如果订单里有一个商品金额是负数,函数会怎样?
- 如果函数内部调用了日志系统、缓存系统或发送了网络请求,调用方是否知道?
LLM 的优势在于生成“分布上最常见”的代码。它不知道你当前函数的上游数据是否经过了清洗,也不知道这个函数未来会被哪些模块调用。它更像是一种“概率性代码补全”,而不是一种“语义正确性证明”。所以,当我们把 LLM 生成代码作为生产代码引入时,真正需要的不是它看起来多规范,而是它在任意输入下是否还能守住约定的行为边界。
1.2 两个经常被忽略的工程概念
传统软件工程里,我们有一套成熟的方法来约束函数行为:
- Design by Contract:把调用者和实现者之间的约定显式表达出来。调用者负责满足前置条件,实现者承诺给出满足后置条件的结果。
- Effects:描述函数在执行过程中可能产生的副作用,比如读写外部存储、网络调用、修改全局状态、抛出异常等。
这两个概念在手工编写代码的时代就已经很强大,但在 LLM 生成代码的场景下,它们的价值会被放大很多倍。因为手工开发者可以在写代码时下意识地考虑调用上下文,而 LLM 没有这种上下文感知能力,它只能依赖明确写出来的约束。契约和 effect 正好是把“上下文约束”从人脑搬到代码里的工具。
1.3 本文适合谁读、能解决什么问题
本文适合以下读者:
- 正在使用 ChatGPT、Copilot、国产 LLM 等工具生成业务代码的开发者;
- 正在搭建 LLM Agent 工具调用体系,需要控制工具函数副作用的工程师;
- 关注代码可维护性,希望把模型输出纳入测试和评审体系的技术负责人。
读完本文,你会理解:
- 为什么“编译通过”不等于“行为正确”;
- 如何用前置条件、后置条件、不变量描述 LLM 生成代码的契约;
- 如何用 effect 标记约束 LLM 生成代码的副作用;
- 如何把契约和 effect 写进 Prompt,让 LLM 生成的代码天然更可靠;
- 遇到“契约违反”“副作用失控”时,应该按什么思路排查。
2. Design by Contract:把“正确”写进代码
2.1 契约的本质
Design by Contract 是由 Bertrand Meyer 在 Eiffel 语言中提出的一种软件设计方法。它的核心隐喻是:函数调用就像一份法律合同。
- 调用者有责任满足某些前置条件(precondition);
- 实现者有责任在满足前置条件的前提下,返回满足后置条件(postcondition)的结果;
- 在整个对象生命周期内,需要始终保持某些不变量(invariant)。
在 LLM 生成代码的语境下,契约的作用更加直接:它不给模型“自由发挥”的空间。比如你要求模型实现parse_order(text: str) -> Order,如果契约里明确写了“当text为空时抛出ValueError”,模型生成的代码就有更高的概率真的处理空字符串,而不是假设调用方不会传空值。
2.2 三个核心要素
前置条件(Precondition)
前置条件描述“调用这个函数必须满足什么条件”。如果调用者不满足条件,函数可以拒绝执行,而不是返回一个奇怪的、无意义的默认值。
# 前置条件:text 非空,且包含 "order_id=" 前缀 def parse_order(text: str) -> Order: if not text or not text.strip(): raise ValueError("text must not be empty") if not text.startswith("order_id="): raise ValueError("text must start with order_id=") # ... 后续解析逻辑后置条件(Postcondition)
后置条件描述“函数执行完后,返回值或对象状态必须满足什么条件”。如果函数的计算结果不满足后置条件,说明实现内部出现了 bug。
def calculate_total(order: Order) -> float: total = sum(item.price * item.quantity for item in order.items) # 后置条件:总额必须非负 if total < 0: raise ArithmeticError("total must not be negative") return total不变量(Invariant)
不变量描述“在整个对象生命周期内,从始至终都成立的性质”。对于订单对象来说,一个常见不变量是:订单中不能存在数量小于等于 0 的商品。
@dataclass(frozen=True) class Order: order_id: str items: tuple[OrderItem, ...] def __post_init__(self): # 不变量:订单 ID 非空 if not self.order_id: raise ValueError("order_id must not be empty") # 不变量:商品项不可为空 if len(self.items) == 0: raise ValueError("order must contain at least one item")2.3 为什么 LLM 更需要契约
传统开发中,契约的主要收益是让团队协作更清晰、错误定位更早。但在 LLM 生成代码的过程中,契约还有一层额外价值:它是可验证的约束,也是 Prompt 设计的一部分。
当你要求 LLM 生成代码时,如果 Prompt 里只有“实现一个解析订单的函数”,模型输出什么取决于训练数据里“大多数解析函数”长什么样子。但如果你在 Prompt 中明确写出前置条件、后置条件和不变量,并进一步要求“这些规则必须用代码断言实现”,模型就有了一个可对齐的规格说明书。它不再凭记忆编造行为,而是在一个相对封闭的规则空间里填充实现细节。
对于 LLM Agent 场景,契约的作用更加重要。Agent 在决定是否调用某个工具前,通常会读取工具的描述信息。如果每个工具的描述里都有明确的前置条件和后置条件,Agent 做出错误调用的概率会明显下降。
3. Effects:给副作用划定边界
3.1 什么是 Effect
Effect 可以翻译为“效果”或“副作用”,用来描述一段代码在完成主功能之外,对外部世界产生的影响。
纯函数是理想的:相同输入永远得到相同输出,不修改外部状态,不执行 I/O。但真实业务代码不可能全是纯函数。我们总需要写数据库操作、发消息、调第三方 API。问题不在于有副作用,而在于副作用没有被声明。
当代码是人工编写时,调用方可以通过阅读函数体来了解副作用。但当代码由 LLM 生成,或者被 LLM Agent 动态调用时,调用方(可能是另一个程序,也可能是 Agent 模型)没有能力可靠地“读完函数体再决策”。这时候,副作用必须被显式标记出来。
3.2 常见副作用类型
对于 LLM 生成代码和 LLM Agent 工具链路来说,我习惯把这些副作用分成四类:
| Effect 类型 | 说明 | 典型例子 |
|---|---|---|
| pure | 纯计算,无外部影响 | 字符串拼接、数学运算 |
| read_only | 只读取外部数据,不修改 | 查询数据库、读文件、GET 请求 |
| mutating | 修改外部状态 | 写文件、更新数据库、发送 POST 请求 |
| io | 与外部世界交互,但结果不可完全控制 | 用户输入、网络超时、随机数 |
这四类之间不是完全互斥的。一个函数可能既发起网络请求又修改本地缓存。但有了分类之后,我们至少可以给每个函数一个“副作用签名”,让调用方知道这个函数能不能被放到并行环境、能不能被重复执行、是否需要特别授权。
3.3 副作用失控的后果
在 LLM 生成代码的场景中,副作用失控通常出现在两个层面。
第一个层面是“代码内部的隐藏副作用”。LLM 生成的函数可能悄悄修改传入的可变对象、写日志文件、调用远程接口。代码评审时如果只关注主流程,这些隐藏副作用很容易被漏掉。
第二个层面是“Agent 工具调用的过度授权”。当 LLM Agent 可以调用一组工具时,如果工具描述里没有 effect 信息,模型就可能在一个“只读查询”场景下调用了一个“写入型工具”。这个问题在安全领域经常被讨论。要从根本上缓解,需要两个措施:一是工具接口的设计要遵循最小权限原则,二是在工具描述里明确声明 effect 类型,让 Agent 在调用前就知道这个工具会做什么。
这里需要特别强调:无论是让 LLM 生成调用代码,还是让 Agent 选择工具,都要确保在合法授权范围内、在测试环境中验证后再操作生产数据。任何涉及数据库变更、文件修改或外部请求的操作,都应该遵循最小权限和审计留痕原则。
4. 环境准备与示例场景说明
4.1 演示环境
本文的代码示例使用 Python 3 编写,只依赖标准库,不需要额外安装第三方包。示例中用到了dataclasses、functools、typing,这些都是 Python 3.7 之后常见的标准库模块。
版本需要根据你的实际项目调整。如果你使用的是 Java 项目,可以用 JSR 380 的 Bean Validation 来表达契约;如果使用 TypeScript,可以用 zod 做运行时校验,用函数式效果库管理副作用。本文的重点是“思路和结构”,语言本身不是限制。
4.2 业务场景假设
我们假设一个电商模块收到了如下需求:
- 从一段文本中解析订单信息,文本格式为
order_id=订单号; sku=商品编码; price=商品单价; quantity=商品数量; - 计算订单总金额;
- 应用折扣,返回新的订单对象,不影响原订单。
这个场景非常适合演示契约和 effect,因为它同时涉及字符串解析、数值计算和对象不可变性。
4.3 项目结构规划
为了便于演示,我们把代码拆成两个文件:
demos/ ├── order_without_contract.py # 第一版:无契约版本 ├── order_with_contract.py # 第二版:引入契约 └── order_with_effect.py # 第三版:引入 effect 标记实际项目中不需要刻意拆成三个文件,这里是为了让三个版本之间的差异更加清晰。下面开始逐步编码。
5. 实战:从无契约代码升级为带契约与 Effect 标记的代码
5.1 第一版:只有“看起来对”的代码
先模拟一个典型的 LLM 直接输出结果。它没有错误处理,也没有任何约束。
# 文件路径:demos/order_without_contract.py from dataclasses import dataclass @dataclass(frozen=True) class OrderItem: sku: str price: float quantity: int @dataclass(frozen=True) class Order: order_id: str items: tuple[OrderItem, ...] def parse_order(text: str) -> Order: parts = text.split(";") order_id = parts[0].split("=")[1] items = [] for part in parts[1:]: kv = part.split("=") if kv[0] == "sku": sku = kv[1] elif kv[0] == "price": price = float(kv[1]) elif kv[0] == "quantity": quantity = int(kv[1]) items.append(OrderItem(sku=sku, price=price, quantity=quantity)) return Order(order_id=order_id, items=tuple(items)) def calculate_total(order: Order) -> float: return sum(item.price * item.quantity for item in order.items) def apply_discount(order: Order, discount_rate: float) -> Order: discounted_items = tuple( OrderItem(item.sku, item.price * (1 - discount_rate), item.quantity) for item in order.items ) return Order(order_id=order.order_id, items=discounted_items)如果你把这版代码交给测试同学,很快会收到一堆 bug 反馈:
- 传空字符串会直接抛
IndexError,错误信息不友好; - 传负数折扣率不会拦截,会返回一个价格更高(甚至为负)的订单;
- 如果商品缺少
quantity字段,会直接NameError; - 整个函数没有声明任何行为约束,调用方只能靠看源码猜测。
这就是典型的“编译通过但行为不可控”。LLM 生成这类代码很快,但它把概率分布中“最常见的写法”复现了出来,没有回答任何关于边界条件的问题。
5.2 第二版:引入前置条件与后置条件
现在我们用显式的契约,把函数的行为边界圈出来。这里定义两个简单的装饰器,用来声明前置条件和后置条件。
# 文件路径:demos/order_with_contract.py import functools from dataclasses import dataclass def require(condition_func): """前置条件装饰器:函数执行前检查条件""" def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): if not condition_func(*args, **kwargs): raise ValueError(f"Precondition failed for {func.__name__}") return func(*args, **kwargs) return wrapper return decorator def ensure(condition_func): """后置条件装饰器:函数执行后检查返回值""" def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): result = func(*args, **kwargs) if not condition_func(result): raise ArithmeticError(f"Postcondition failed for {func.__name__}") return result return wrapper return decorator然后定义数据结构和业务函数。注意Order在__post_init__中维护不变量:
@dataclass(frozen=True) class OrderItem: sku: str price: float quantity: int def __post_init__(self): if not self.sku: raise ValueError("sku must not be empty") if self.price <= 0: raise ValueError("price must be positive") if self.quantity <= 0: raise ValueError("quantity must be positive") @dataclass(frozen=True) class Order: order_id: str items: tuple[OrderItem, ...] def __post_init__(self): if not self.order_id: raise ValueError("order_id must not be empty") if len(self.items) == 0: raise ValueError("order must contain at least one item") @require(lambda text: text is not None and text.strip() != "") def parse_order(text: str) -> Order: parts = text.split(";") order_id = parts[0].split("=")[1] if not order_id: raise ValueError("order_id must not be empty") items = [] for part in parts[1:]: kv = part.split("=") key = kv[0] value = kv[1] if key == "sku": sku = value elif key == "price": price = float(value) elif key == "quantity": quantity = int(value) items.append(OrderItem(sku=sku, price=price, quantity=quantity)) if not items: raise ValueError("order must contain at least one item") return Order(order_id=order_id, items=tuple(items)) @ensure(lambda total: total >= 0) def calculate_total(order: Order) -> float: return sum(item.price * item.quantity for item in order.items) @require(lambda order, discount_rate: 0 < discount_rate < 1) def apply_discount(order: Order, discount_rate: float) -> Order: discounted_items = tuple( OrderItem(item.sku, item.price * (1 - discount_rate), item.quantity) for item in order.items ) return Order(order_id=order.order_id, items=discounted_items)现在再看之前的几个边界问题:
- 传空字符串给
parse_order,前置条件直接拦截; - 传负数折扣率给
apply_discount,前置条件直接拦截; - 商品数量为正、价格为正,这些数据不变量在
OrderItem创建时强制检查; - 返回总额不可能为负,算出来负值说明实现有 bug,后置条件兜底。
这就是契约式设计的价值:它不是靠开发者“记得检查”,而是把行为约束提升到代码结构层面,让错误尽早暴露。
5.3 第三版:用 Effect 标记约束副作用
有了契约之后,函数的行为边界清晰了很多,但还有一个问题:调用方(尤其是 LLM Agent)怎么知道这个函数是否安全、是否会产生外部效果?
下面用装饰器给函数挂上 effect 标记。这里的实现只是一个简单示意,你可以换成 Java 自定义注解、TypeScript 装饰器,或者直接写在工具的 JSON Schema 描述里。
# 文件路径:demos/order_with_effect.py import functools from dataclasses import dataclass from enum import Enum, auto class EffectKind(Enum): PURE = auto() # 纯计算,无副作用 READ_ONLY = auto() # 只读外部数据 MUTATING = auto() # 修改外部状态 IO = auto() # 与外部世界交互 def effect(*kinds: EffectKind): """给函数标记副作用类型""" def decorator(func): setattr(func, "__effects__", frozenset(kinds)) return func return decorator然后给上一版代码中的函数加上标记:
@effect(EffectKind.PURE) @require(lambda text: text is not None and text.strip() != "") def parse_order(text: str) -> Order: # 函数体与第二版相同 ... @effect(EffectKind.PURE) @ensure(lambda total: total >= 0) def calculate_total(order: Order) -> float: ... @effect(EffectKind.PURE) @require(lambda order, discount_rate: 0 < discount_rate < 1) def apply_discount(order: Order, discount_rate: float) -> Order: ...这三个函数都不访问外部系统,所以标记为PURE。如果其中一个函数需要记录日志,我们可以把它标记为IO。如果它需要把订单写入数据库,就标记为MUTATING。
有了标记之后,Agent 工具路由可以读取这些元信息,在调用前做决策:
def describe_tool(func) -> dict: """把函数的 effect 信息暴露给外部调用方""" kinds = getattr(func, "__effects__", {}) return { "name": func.__name__, "effects": [kind.name for kind in kinds], "precondition": func.__doc__ or "no documented precondition", }这样,LLM Agent 在决定调用工具前,看到的是一份带“副作用签名”的接口说明。它知道这个函数是只读的、可重入的、还是需要特别授权的。这比让模型通过函数名猜测要可靠得多。
5.4 运行与验证
保存第二版代码后,可以在命令行里执行验证。为了看到契约的效果,我加一个简单的测试脚本:
# 文件路径:demos/test_contract.py from order_with_contract import parse_order, calculate_total, apply_discount # 正常路径 order = parse_order("order_id=1001; sku=apple; price=3.5; quantity=2; sku=banana; price=2.0; quantity=3") print("total:", calculate_total(order)) # 边界路径:空文本 try: parse_order("") except ValueError as e: print("parse_order error:", e) # 边界路径:折扣率不合法 try: apply_discount(order, -0.2) except ValueError as e: print("apply_discount error:", e)预期输出类似:
total: 13.0 parse_order error: Precondition failed for parse_order apply_discount error: Precondition failed for apply_discount在实际项目中,观察点有两个:
- 契约是否在第一时间拦截了非法输入;
- 非法输入被拦截后,错误信息是否足够定位问题。
这两个观察点也是后续排查和评审的重点。
6. 常见问题与排查思路
6.1 LLM 生成的代码不满足契约怎么办
现象:模型生成的代码即使声明了前置条件,函数内部仍然没有检查,或者检查了但异常类型不一致。
可能原因:LLM 生成的代码只把“契约描述”当作注释,没有转换成真实的运行时断言;或者模型没有理解“前置条件必须在函数入口处执行”。
排查步骤:
- 检查函数入口是否有对应的
if判断; - 检查异常类型是否与设计一致(比如统一抛
ValueError而不是抛IndexError); - 检查契约条件是否与 Prompt 中描述的完全一致。
解决方案:不要让 LLM “自觉”实现契约。在代码生成后的审查阶段,用静态检查脚本扫描关键函数,确认存在必要的防御性判断。另外,可以把契约检查提取到装饰器或配置文件中,减少模型自由发挥的空间。
6.2 契约检查太多影响性能怎么办
现象:接口层增加了大量前置条件和后置条件检查,每次调用都有额外开销。
可能原因:把高频调用链路中所有函数都加上了复杂契约检查,而且条件本身涉及数据库查询或网络调用。
排查步骤:
- 用 profile 定位哪些契约检查耗时最大;
- 区分“核心业务不变量”和“防御式检查”;
- 考虑把开销大的检查放到测试阶段和生产环境的采样日志中。
解决方案:契约检查并不是越多越好。优先级应该是:数据不变量 > 涉及安全的副作用控制 > 业务规则校验。生产环境中可以把后置条件检查降级为日志告警,而不是直接阻断。
这里也提醒一下:如果 LLM 生成的代码被直接接入生产环境的高频接口,务必做好流量回放和灰度验证,不要只依赖模型自测。
6.3 工具函数副作用声明错误如何发现
现象:一个函数被标记为PURE,实际运行时却修改了全局变量或写入了日志。
可能原因:LLM 生成代码时把外部调用隐藏在辅助函数中,主函数表面上是纯函数;或者模型对 effect 的理解有偏差。
排查步骤:
- 代码评审时重点检查函数体内部是否调用了外部系统;
- 利用 Python 的 AST 分析或 Java 的字节码分析,扫描函数内是否出现
print、open、requests、sqlalchemy等关键字; - 在测试环境中对标记为
PURE的函数做幂等性和无状态验证。
解决方案:将 effect 标记纳入工具描述的规范字段,并在 CI 中增加“副作用声明一致性检查”。如果发现声明为PURE的函数包含 I/O 调用,构建直接失败。
6.4 常见问题汇总表
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
LLM 生成的函数不检查空值,直接抛IndexError | 没有前置条件约束 | 在 Prompt 中显式声明前置条件,要求使用异常类型返回 |
| 函数算出了负数总额 | 缺少后置条件,或内部数值逻辑错误 | 在函数出口检查返回值,不满足即抛异常 |
| 折扣函数改了原订单对象 | 可变对象被直接修改 | 使用不可变数据结构,或在不变量中声明不得修改原对象 |
| Agent 调用了不该写的工具 | 工具描述缺少 effect 信息 | 为工具补充 effect 标记,设置权限校验 |
| 契约检查过多导致接口变慢 | 把业务校验与契约检查混在一起 | 分级管理:数据不变量必须检查,业务校验可采样 |
| 测试环境正常但生产环境失败 | 生产数据不符合前置条件 | 记录违约样例,回访数据质量,补充数据清洗 |
7. 最佳实践与工程落地建议
7.1 把契约写进 Prompt
Prompt 不只是给模型的自然语言任务描述,它本身就是一份“接口说明书”。建议在 Prompt 中按固定模板描述函数契约:
请实现函数 parse_order(text: str) -> Order: 前置条件: - text 非空; - text 以 "order_id=" 开头。 后置条件: - 返回的 Order.order_id 非空; - Order.items 长度大于 0; - 每个 OrderItem 的 price 和 quantity 均为正数。 不变量: - Order 对象不可变。 要求:所有前置条件必须在函数入口处显示检查,违反时抛出 ValueError。这样做的本质是:把模型的可疑自由度压缩到最小,让它的任务从“凭空实现一个解析函数”变成“在给定边界内填充逻辑”。当 Llama、GPT、Claude 一类模型看到完整约束时,输出质量通常会比只给一句“解析订单文本”稳定得多。
7.2 在 CI 阶段运行契约验证
契约的价值只有在“自动化执行”时才能体现出来。建议在 CI 流程中加入一个专门阶段:对 LLM 生成的代码运行契约测试。
具体做法:
- 为每个工具函数维护一份契约清单,包含前置条件、后置条件和不变量;
- 在 CI 中执行契约测试集,输入包括正常数据、边界数据和非法数据;
- 对 LLM Agent 的工具函数,额外检查 “effect 声明一致”,确保标记为
PURE的函数不会出现 I/O 调用。
这些不需要昂贵的框架。用pytest加参数化用例就能覆盖大多数场景。
7.3 为 Agent 工具接口补充 effect 说明
在 LLM 应用开发中,Agent 工具调用的可靠性比单个函数的正确性更影响整体体验。如果 Agent 无法判断某个工具是否会产生副作用,它就会在错误的场景做出错误的决策。
建议做法:在工具描述中提供一个标准字段effects,取值可以是pure、read_only、mutating、io。同时配合权限系统,对mutating和io类工具增加二次确认或者专用凭证,避免 Agent 在过度授权条件下执行危险操作。
这也能提升 LLM 对工具链路的理解能力。训练模型虽然可以在一定程度上记住常见工具的行为,但显式声明仍然是工程上最稳妥的约束手段。
7.4 代码评审时重点检查哪些位置
代码评审不可能取代自动验证,但可以补齐自动化的盲区。对于 LLM 生成的代码,我建议评审者重点看这几个位置:
- 函数入口是否立刻检查输入;
- 数据对象是否在创建时就满足不变量;
- 返回值是否经过后置条件校验;
- 函数体内部有没有隐藏 I/O 或状态修改;
- 异常类型和错误信息是否有区分度;
- 对 LLM Agent 暴露的工具,是否有明确的 effect 描述。
这些检查点不需要全部靠人眼完成,可以写成一份 checklist,加入团队的 PR 模板。这样时间长了之后,LLM 生成代码的质量会逐渐逼近受控工程代码。
8. 总结与进阶方向
回到开头的问题:为什么 Design by Contract 和 effects 是 LLM 生成代码的关键工程问题?
因为 LLM 不是形式化验证工具,它更擅长根据统计规律生成看起来合理的代码。要让这种代码真正可用,我们需要给它一个明确的行为边界,并且在边界被突破时有机制能够尽早发现。Design by Contract 负责定义“输入-输出-状态”的约束,effects 负责定义“函数对外部世界的影响范围”。两者组合起来,就构成了一层独立于模型能力的质量防线。
下一步可以继续学习的方向包括:
- 类型系统与运行时校验:Python 里的
dataclass、pydantic,TypeScript 里的zod,Java 里的 Bean Validation; - 形式化方法与自动证明:想要更严格地验证 LLM 生成的行为契约,可以了解 TLA+、Dafny 这类工具;
- Agent 工具编排:了解 MCP、工具路由和权限模型,掌握如何把 effect 声明落地到 Agent 调用链中;
- 契约测试实践:在现有项目里把核心业务函数逐步加上前置条件和后置条件,积累一套可复用的契约测试用例。
LLM 生成代码不是洪水猛兽,但它也不应该是“黑盒魔法”。给它补上契约和 effect 这两个工程钩子之后,模型输出就能从“看起来对”变成“可验证地对”。希望这篇笔记能把这两个偏工程底层的概念,变成你日常 Prompt 设计和代码评审中的实际检查项。