ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

用契约式设计和效果系统约束LLM生成代码

用契约式设计和效果系统约束LLM生成代码 最近在项目里用大模型辅助生成业务代码经历了几个很“典型”的翻车现场接口调通了、单测也过了上线之后却出现重复扣款、订单状态错乱、日志打爆磁盘。排查到最后问题几乎都指向同一个根因——大模型生成的代码“看起来正确”但缺少边界约束副作用完全不可控。后来我逐渐意识到让大模型生成可靠代码不能只靠提示词里写一句“请生成高质量的代码”而是要把软件工程里沉淀已久的两个原则重新捡起来——Design by Contract契约式设计和Effects副作用显式化。本文会从概念讲起然后用一套轻量级 Python 框架演示如何在实际项目中约束 LLM 生成的代码。读完你会理解为什么 LLM 生成代码必须设置“输入输出边界”怎么用前置条件、后置条件、不变量约束生成结果怎么把“副作用”从隐藏变成显式声明并通过静态扫描拦截危险调用如何把上述规则写进 Prompt让大模型从生成阶段就开始守规矩。文章核心是思路 完整可运行代码不依赖任何重型框架适合在后端项目里逐步落地。1. 背景LLM 生成代码的“不可靠”来自哪里在讨论怎么约束之前先看清楚问题本身。大模型本质上是概率生成器。它根据输入上下文预测下一个 token而不是像编译器那样理解程序的完整状态。这意味着它生成的每一行代码都是“统计上合理”但未必“逻辑上正确”。具体到工程落地风险集中在三个方面1.1 边界缺失只看到函数内部看不到调用方LLM 在生成一个函数时经常只盯着函数体本身很少主动思考参数从哪来可能是用户输入、上游接口、数据库查询结果参数是否一定合法会不会是负数、空字符串、超出范围的 ID函数返回后调用方期望什么必须是非空对象还是允许None比如下面这段生成代码单看逻辑似乎没问题def create_order(user_id, amount): order_id insert_order(user_id, amount) return order_id但如果上游传入了amount0甚至负数insert_order会往数据库写入一条脏数据。这就是典型的前置条件缺失。1.2 副作用隐藏函数偷偷做了“额外的事”大模型生成的代码很容易加入隐藏副作用在业务函数里打印日志在循环里发 HTTP 请求在查询接口中隐式写入缓存调用random.random()引入不确定性修改全局状态或配置文件。这些副作用在开发和测试阶段很难发现因为单测环境往往没有外部依赖或者日志不会立即引发问题。但一旦进入生产就可能造成性能下降、脏数据、甚至安全事故。1.3 验证困难单元测试覆盖不了所有隐藏分支很多团队的 LLM 代码验证方式仍然停留在“跑几个用例 人工 review”。然而生成代码的路径组合非常多靠用例覆盖很难穷尽。尤其是副作用单元测试只能证明“当前输入下没发生意外”不能证明“所有输入下行为都正确”。所以我们需要一种机制让代码自己“声明边界”并且让边界可以被自动校验。这正是 Design by Contract 和 Effects 能发挥作用的地方。2. Design by Contract先定义“能做什么”的边界2.1 从 Eiffel 语言说起Design by Contract契约式设计最早由 Bertrand Meyer 在《Object-Oriented Software Construction》中系统提出并在 Eiffel 语言中落地。它的核心思想非常朴素把函数调用看作一份合同调用方和实现方各自有权利和义务。一份合同通常包含三个部分前置条件precondition函数运行之前必须为真否则实现方有权拒绝执行后置条件postcondition函数运行之后必须为真否则实现方没有履行合同不变量invariant在整个对象生命周期或流程中始终为真任何操作前后都不能破坏。用租房的例子来类比前置条件租客入住前必须支付押金后置条件退租时房东必须退还押金扣除合理损耗不变量出租期间房屋所有权始终属于房东。2.2 前置条件拦住非法输入前置条件是 LLM 生成代码时最容易遗漏的部分。大模型通常默认“调用方会传合法参数”但现实世界的数据往往不合法。一个带前置条件的函数应该是这样def withdraw(account, amount): # 前置条件金额必须为正数 if amount 0: raise ValueError(amount must be positive) # 前置条件账户余额必须充足 if account.balance amount: raise ValueError(insufficient balance) account.balance - amount return account.balance注意这里的前置条件不是简单的“防御性编程”。它明确告诉调用方如果你传入非法金额后果由你负责。设计良好的前置条件可以尽早暴露调用方的错误而不是让脏数据一路传递到数据库层。2.3 后置条件验证输出结果后置条件检查的是“函数答应你的事情有没有做到”。def parse_config(text: str) - dict: # 后置条件返回结果必须是 dict且不能为空 result _do_parse(text) if not isinstance(result, dict): raise RuntimeError(parse result must be dict) return result对于 LLM 生成代码后置条件尤其重要。大模型经常在一个返回语句里写出return None、return {}之类的可疑结果。加上后置条件后一旦生成代码没有履行承诺系统会立刻报错而不是等到下一层调用的空指针异常。2.4 不变量守住核心业务规则不变量是比前置/后置条件更全局的约束。它描述的是整个对象或流程中“永远不能破坏的规则”。以订单系统为例订单总额必须等于“商品单价 × 数量 运费 - 优惠”订单状态必须在PENDING - PAID - SHIPPED - COMPLETED的合法状态机内流转用户余额不能小于 0。不变量通常放在类的方法调用前后进行校验。LLM 生成的代码如果破坏了不变量比如在订单处理过程中把用户余额改成负数即使函数本身“返回值正确”业务上也已经铸成大错。2.5 对 LLM 生成代码的意义有了契约之后代码的正确性边界变得可验证。我们可以要求 LLM 在生成代码时同步产出“契约声明”然后在运行前或运行时自动校验。这样非法参数在入口处被拦截不会污染后续逻辑返回值在出口处被校验异常行为立刻暴露核心业务不变量持续生效防止生成代码破坏关键规则。换句话说Contract 把“代码应该怎么正确”从人的期望变成了机器可以检查的约束。3. Effects把“副作用”从隐藏变为显式3.1 什么是副作用函数式编程中如果一个函数只依赖参数计算返回值不修改外部状态、不做 I/O那么它是纯函数。反之只要函数做了以下任何一件事就是有副作用的修改全局变量或外部对象的状态打印日志、写入文件、发起网络请求读取用户输入产生随机数抛出未在声明中体现的异常。副作用本身不是坏事业务代码不可避免要读写数据库、调用外部 API。但隐藏的副作用是灾难——它们让代码难以测试、难以推理、难以并发安全。3.2 为什么 LLM 生成代码时副作用容易失控大模型训练语料里混入了大量“示例代码”而这些示例往往省略了环境依赖和函数契约。于是模型学到了很多坏习惯在业务函数里顺手print()调试在循环里调用time.sleep()在查询函数里偷偷写入统计表用random.random()做业务判断让结果不可复现。这些行为在模型看来是“合理的”因为语料里确实大量存在。但在真实的工程体系里它们会造成不可预知的后果。3.3 Effect System让副作用成为类型签名的一部分为了根治隐藏副作用学术界和工业界提出了效果系统effect system。简单说就是让函数类型不仅描述接收什么、返回什么还描述它会产生什么效果。例如Java 的 checked exception 就是一种受限的效果声明方法签名声明它可能抛出IOException调用方必须处理Koka、Eff 这类语言支持代数效应副作用被显式建模TypeScript 通过 JSDoc 的throws标注来声明异常Rust 的所有权模型从可变性角度限制了副作用的发生范围。在 LLM 生成代码的场景我们不一定需要一套完整的语言级效果系统但完全可以借鉴它的思路默认所有函数都是纯函数PURE如果函数有副作用必须显式声明通过静态分析检测声明与实际行为是否一致。这样一段 LLM 生成的代码如果想偷偷print、发网络请求、修改数据库就会被静态检查拦截。3.4 Effects 与 Design by Contract 的关系两者是互补的Contract 管正确性边界什么参数能进、什么结果能出、什么不变量不能破坏Effects 管影响力边界函数可以触碰哪些外部资源、允许产生哪些效果。一个函数可以同时携带两者前置条件user_id是正整数后置条件返回order_id是非空字符串允许效果READ,WRITE禁止效果IO,RANDOM。这样一来我们既能知道代码“做什么”也知道它“碰什么”LLM 生成代码的可控性自然大幅提升。4. 环境准备与项目结构为了让思路落地我们用一个轻量级 Python 框架来演示。不需要安装第三方库核心功能用标准库functools、typing、ast实现便于你理解原理并改造到自己的项目。环境说明Python 3.9示例代码在 3.10 环境下编写兼容 3.9 及以上版本操作系统不限Linux、macOS、Windows 均可不需要额外依赖只需要标准库。项目结构如下llm_contract_demo/ ├── contract.py # 契约装饰器前置条件、后置条件 ├── effect_checker.py # 效果声明与 AST 静态检查 ├── order_service.py # 被约束的示例业务代码 └── main.py # 演示运行与检查结果下面我们从零开始实现这个迷你框架。5. 手写一个轻量“契约 效果”检查器为了不依赖第三方库我们直接实现一个小型契约装饰器。它的能力会弱于 icontract、deal 这类成熟库但足够演示核心思想而且代码量小你可以按需扩展。5.1 实现 contract.py在项目根目录创建contract.py# contract.py from functools import wraps from typing import Callable class ContractError(Exception): 契约校验失败的基础异常 class PreconditionError(ContractError): 前置条件失败 class PostconditionError(ContractError): 后置条件失败 def pre(condition: Callable, message: str ): 前置条件装饰器。 condition 是接受与原始函数相同参数的函数返回 True/False。 在调用真实函数之前执行失败时抛出 PreconditionError。 def decorator(func): wraps(func) def wrapper(*args, **kwargs): if not condition(*args, **kwargs): msg message or fPrecondition failed for {func.__name__} raise PreconditionError(msg) return func(*args, **kwargs) return wrapper return decorator def post(condition: Callable, message: str ): 后置条件装饰器。 condition 接收 (result, args, kwargs) 三个参数返回 True/False。 在原始函数返回之后执行失败时抛出 PostconditionError。 def decorator(func): wraps(func) def wrapper(*args, **kwargs): result func(*args, **kwargs) if not condition(result, args, kwargs): msg message or ( fPostcondition failed for {func.__name__}, result{result!r} ) raise PostconditionError(msg) return result return wrapper return decorator这段代码的作用pre装饰器在原函数执行前检查condition(*args, **kwargs)是否为真。如果不满足直接抛异常原函数不会被调用post装饰器在函数返回后执行condition接收三个参数返回值、原始参数元组、原始关键字参数字典方便编写更多场景的后置校验。5.2 实现 effect_checker.py接下来实现效果声明和 AST 静态检查。# effect_checker.py import ast from enum import Enum, auto class Effect(Enum): PURE auto() # 纯函数无副作用 READ auto() # 读取外部状态 WRITE auto() # 修改外部状态 IO auto() # 文件、网络、打印、输入输出 RANDOM auto() # 随机数 / 不确定性 # 从源码调用名映射到效果类型 DANGEROUS_CALLS { print: Effect.IO, open: Effect.IO, input: Effect.IO, time.sleep: Effect.IO, os.system: Effect.IO, requests.get: Effect.IO, requests.post: Effect.IO, requests.put: Effect.IO, requests.delete: Effect.IO, db.execute: Effect.WRITE, session.commit: Effect.WRITE, random.random: Effect.RANDOM, random.choice: Effect.RANDOM, } def _get_call_name(node: ast.Call) - str: 把 ast.Call 节点解析为字符串形式的调用名。 if isinstance(node.func, ast.Name): return node.func.id if isinstance(node.func, ast.Attribute): parts [] current node.func while isinstance(current, ast.Attribute): parts.append(current.attr) current current.value if isinstance(current, ast.Name): parts.append(current.id) return ..join(reversed(parts)) return def analyze_source_code(source: str) - set: 解析 Python 源码返回其中出现的所有副作用类型。 这是一个演示级静态检查器生产环境可以扩展为更完善的 AST 规则。 tree ast.parse(source) detected set() for node in ast.walk(tree): if isinstance(node, ast.Call): name _get_call_name(node) if name in DANGEROUS_CALLS: detected.add(DANGEROUS_CALLS[name]) return detected def assert_allowed(source: str, allowed_effects: set): 检查源码中检测到的副作用是否都在 allowed_effects 允许的范围内。 detected analyze_source_code(source) not_allowed detected - allowed_effects if not_allowed: effect_names , .join(e.name for e in sorted(not_allowed, keylambda e: e.name)) raise ValueError(f检测到未声明副作用: {effect_names}) return detected说明默认策略是“没有声明就是 PURE”这对约束 LLM 生成代码非常有效analyze_source_code用ast遍历源码中的函数调用匹配print、open、requests.*、db.execute等危险调用assert_allowed把检测到的效果与允许集合比对如果有超出范围的效果直接报错。注意这个检查器是演示级的。它把整个源码文件里的所有调用都检测了一遍包括定义在函数内部但从未执行的分支。生产环境通常需要结合具体入口函数做更精确的控制流分析。不过作为 CI 中的一个“守门员”它的思路已经足够实用。5.3 使用契约装饰器组合业务逻辑现在创建order_service.py演示如何用契约约束一个订单创建函数。# order_service.py from contract import pre, post # 模拟数据库 _db {} _total_orders 0 def _insert_order(user_id: int, amount: float) - str: 写入订单表返回订单号。 global _total_orders _total_orders 1 order_id fORD-{1000 _total_orders} _db[order_id] {user_id: user_id, amount: amount} return order_id pre(lambda user_id, amount: isinstance(user_id, int) and user_id 0, user_id must be positive int) pre(lambda user_id, amount: isinstance(amount, (int, float)) and amount 0, amount must be positive number) post(lambda result, args, kwargs: isinstance(result, str) and result.startswith(ORD-), result must be order id string) def create_order(user_id: int, amount: float) - str: 创建订单只允许读取和写入订单表不允许打印、随机数、网络请求。 return _insert_order(user_id, amount)这里用pre声明了两个前置条件user_id必须是正整数amount必须是正数。用post声明后置条件返回值必须是字符串且以ORD-开头。这些约束一旦被违反调用方会立刻收到PreconditionError或PostconditionError。5.4 运行与验证创建main.py# main.py from pathlib import Path from contract import PreconditionError, PostconditionError from effect_checker import Effect, assert_allowed import order_service def demo_contract(): print( 契约检查演示 ) order_id order_service.create_order(1001, 299.0) print(f正常调用成功: {order_id}) try: order_service.create_order(0, 299.0) except PreconditionError as e: print(f前置条件拦截: {e}) try: order_service.create_order(1001, 0) except PreconditionError as e: print(f前置条件拦截: {e}) try: # 模拟一个返回值不符合后置条件的函数 post(lambda result, args, kwargs: result 0, result must be positive) def bad_function(): return -1 bad_function() except PostconditionError as e: print(f后置条件拦截: {e}) def demo_effect_check(): print(\n 效果检查演示 ) source_path Path(__file__).parent / order_service.py source source_path.read_text(encodingutf-8) # 允许 READ 和 WRITE不允许 IO / RANDOM allowed {Effect.READ, Effect.WRITE} detected assert_allowed(source, allowed) print(f检测到的效果: {[e.name for e in sorted(detected, keylambda e: e.name)]}) print(效果检查通过符合声明的副作用范围) if __name__ __main__: demo_contract() demo_effect_check()运行方式python main.py预期输出 契约检查演示 正常调用成功: ORD-1001 前置条件拦截: user_id must be positive int 前置条件拦截: amount must be positive number 后置条件拦截: result must be positive 效果检查演示 检测到的效果: [WRITE] 效果检查通过符合声明的副作用范围这里的效果检查会发现order_service.py中只有_insert_order内部的_db[order_id] ...会被映射为WRITE因为我们把db.execute列入了映射表但这里并没有真正的db.execute实际上这个简单示例会检测不到 WRITE因为赋值操作不在DANGEROUS_CALLS中。为了让演示更贴近真实可以把_insert_order里的写入改成模拟调用一个叫db.execute的函数或者直接把_db[order_id] ...这种赋值操作也纳入检测规则。我们在下一节的完整实战中会用一个更完整的示例。6. 实战把一个“危险”的 LLM 生成函数改造成受控代码理论和迷你框架都有了下面用一个完整的实战案例串起来假设我们让 LLM 生成一个订单创建函数然后逐步加入契约和效果约束。6.1 需求描述业务需求只有一句话编写函数create_order(user_id, amount)创建订单并返回订单号。这个需求非常开放LLM 有大量自由发挥的空间。6.2 第一版LLM 无约束生成危险代码假设这是大模型直接返回的代码# llm_generated_first.py import random import time def create_order(user_id, amount): print(creating order for user, user_id) # 模拟网络延迟实际上是隐藏副作用 time.sleep(0.1) if amount 0: return None # 用随机数生成订单号结果不可复现 order_id ORD- str(random.randint(1000, 9999)) # 模拟写入数据库 db.insert(orders, user_iduser_id, amountamount) # 顺手更新一下余额但函数契约里完全没体现 db.update(users, user_iduser_id, amount-amount) return order_id这段代码存在很多问题前置条件缺失user_id可能为负数或字符串amount可能为 0 或负数后置条件缺失amount 0时返回None调用方可能收到None后继续使用导致空指针隐藏副作用print、time.sleep、random都是未被声明的效果业务不变量破坏更新用户余额的操作写在订单函数里一旦订单创建失败余额已经被扣了。如果开发时不仔细 review这段代码很可能进入生产环境。6.3 第二版用契约约束边界在第一版基础上加入前置条件和后置条件# order_service_v2.py from contract import pre, post pre(lambda user_id, amount: isinstance(user_id, int) and user_id 0, user_id must be positive int) pre(lambda user_id, amount: isinstance(amount, (int, float)) and amount 0, amount must be positive number) post(lambda result, args, kwargs: isinstance(result, str) and result.startswith(ORD-), result must be non-empty order id) def create_order(user_id: int, amount: float) - str: # 这里还保留了副作用但至少非法输入会被拦截 order_id fORD-{user_id}-{amount} return order_id加入契约后非法user_id和amount在入口处被拦截返回值受到后置条件约束不会出现None。但副作用仍然没有被显式管理。函数内部如果偷偷加了print检查器应该能发现。6.4 第三版用效果声明 静态检查管理副作用为了让效果检查真正落地我们扩展effect_checker.py让它能检测更多副作用调用同时把create_order声明为只允许READ和WRITE。先扩展 DANGEROUS_CALLS# effect_checker.py 扩展部分 DANGEROUS_CALLS { print: Effect.IO, open: Effect.IO, input: Effect.IO, time.sleep: Effect.IO, os.system: Effect.IO, requests.get: Effect.IO, requests.post: Effect.IO, requests.put: Effect.IO, requests.delete: Effect.IO, db.execute: Effect.WRITE, db.update: Effect.WRITE, session.commit: Effect.WRITE, random.random: Effect.RANDOM, random.randint: Effect.RANDOM, random.choice: Effect.RANDOM, }然后创建一个更真实的业务模块order_service_final.py# order_service_final.py from contract import pre, post # 模拟数据库 _db_orders {} _db_users { 1001: {balance: 500.0}, 1002: {balance: 99.0}, } def db_execute(sql: str, **kwargs): 模拟数据库执行。实际项目对应数据库驱动调用。 # 这里只做演示不真正连接数据库 return fexecuted: {sql} {kwargs} def _get_user_balance(user_id: int) - float: 读取用户余额。效果READ return _db_users[user_id][balance] def _decrease_balance(user_id: int, amount: float) - None: 扣减用户余额。效果WRITE _db_users[user_id][balance] - amount pre(lambda user_id, amount: isinstance(user_id, int) and user_id 0, user_id must be positive int) pre(lambda user_id, amount: isinstance(amount, (int, float)) and amount 0, amount must be positive number) post(lambda result, args, kwargs: isinstance(result, str) and result.startswith(ORD-), result must be non-empty order id) def create_order(user_id: int, amount: float) - str: 创建订单并扣减用户余额。 允许的副作用 - READ读取用户余额 - WRITE写入订单表、扣减余额 禁止的副作用 - IOprint、网络请求、文件操作 - RANDOM随机数 # 读取用户余额属于 READ 效果 balance _get_user_balance(user_id) # 如果余额不足直接抛出业务异常而不是返回 None if balance amount: raise ValueError(insufficient balance) # 写入订单表属于 WRITE 效果 order_id fORD-{user_id}-{int(amount)} db_execute(INSERT INTO orders (user_id, amount) VALUES (?, ?), user_iduser_id, amountamount) # 扣减余额属于 WRITE 效果 _decrease_balance(user_id, amount) return order_id注意这里把“更新用户余额”这个操作保留在了订单函数里。从业务设计上看更合理的做法通常是让订单服务和用户服务分离通过事务或最终一致性保证两个操作同时成功或同时失败。但在本文场景中我们先把“效果声明”做出来让所有副作用都写清楚。然后扩展main.py对最终版代码做效果检查# main.py 扩展 def demo_effect_check_final(): print(\n 最终效果检查演示 ) source_path Path(__file__).parent / order_service_final.py source source_path.read_text(encodingutf-8) allowed {Effect.READ, Effect.WRITE} detected assert_allowed(source, allowed) print(f检测到的效果: {[e.name for e in sorted(detected, keylambda e: e.name)]}) print(效果检查通过create_order 的副作用符合声明范围)运行python main.py后最终输出类似 契约检查演示 正常调用成功: ORD-1001 前置条件拦截: user_id must be positive int 前置条件拦截: amount must be positive number 后置条件拦截: result must be positive 效果检查演示 检测到的效果: [WRITE] 效果检查通过符合声明的副作用范围 最终效果检查演示 检测到的效果: [WRITE] 效果检查通过create_order 的副作用符合声明范围如果有人在create_order里偷偷加一行print(hello)静态检查会立刻报错ValueError: 检测到未声明副作用: IO6.5 怎样把约束“教”给 LLM框架只是“裁判”真正的生成源头是 LLM。所以我们需要把契约和效果规则直接写进 Prompt让模型从生成阶段就开始遵守。下面是一份可直接套用的 Prompt 模板请生成一个 Python 函数 create_order(user_id, amount)并严格满足以下契约 【前置条件】 1. user_id 必须是正整数。 2. amount 必须是正数。 【后置条件】 1. 返回值必须是非空字符串以 ORD- 开头。 【副作用约束】 1. 允许的副作用READ读取用户余额、WRITE写入订单表、扣减余额。 2. 禁止使用 print、open、input、time.sleep、random、requests 等 I/O 或随机操作。 3. 禁止修改全局配置。 【额外要求】 1. 请先用三行注释说明你的契约设计再写实现。 2. 请在实现中标注每一处 READ / WRITE 效果对应的代码行。把这段 Prompt 输入给 LLM得到的代码通常会更“规矩”因为模型的注意力被明确引导到了边界和副作用上。7. 常见问题与排查思路在实际落地过程中你可能会遇到下面这些问题问题现象常见原因解决思路LLM 生成的代码在测试集上通过但上线后出现脏数据契约只覆盖了部分入口调用方绕过了前置校验所有外部入口函数都必须加契约不能只加核心业务函数契约装饰器抛出异常导致服务不可用把契约校验和生产数据流耦合在一起校验失败时直接中断请求区分强校验和弱校验生产环境先记录日志确认无误后再启用强校验函数声明 PURE但实际仍然打印日志副作用声明靠自觉缺少静态检查在 CI 阶段运行 AST 检查器任何未声明副作用都阻断合并前置条件写得太严格导致正常业务被拦截把内部错误和外部非法输入混为一谈前置条件只约束“调用方必须保证”的边界内部异常用 try-except 处理LLM 不遵循 Prompt 中的约束Prompt 只写了“要规范”没有给出具体契约语法和效果清单给模型提供代码骨架要求它先填空式写契约再写实现静态检查误报把合法调用判断为副作用检查器规则过于简单比如把所有db.execute都视为写操作扩展 AST 规则结合函数上下文做更精确的判断必要时人工 review 豁免下面补充两个比较典型的排查场景。7.1 场景一契约校验偶发失败现象线上偶发出现PreconditionError导致部分请求失败。排查步骤先看日志中的失败入参确认是哪个前置条件不满足顺着调用链找上游判断是上游传入数据本身有问题还是上游已经处理过但遗漏了边界如果是上游数据问题在前置条件不满足时抛出语义明确的业务异常而不是直接落库。解决思路把前置条件从“静默失败”改成“显式失败 友好提示”同时在上游补一层数据校验。7.2 场景二效果检查器没有拦截到隐藏副作用现象assert_allowed通过了但运行时代码仍然访问了外部资源。排查步骤确认DANGEROUS_CALLS是否覆盖了实际调用的 API确认ast.walk是否能遍历到被调用的函数确认是否通过getattr、__import__、eval 等动态方式绕过了静态检查检查是否使用了第三方库的别名比如from requests import get后调用get(...)。解决思路静态检查不是银弹。对于高风险模块还需要在运行时结合沙箱或权限控制兜底。LLM 生成的代码尤其如此——静态检查负责“拦住明显的违规”运行时沙箱负责“拦住绕过静态检查的行为”。8. 工程化落地建议8.1 契约先行再谈生成不要等 LLM 生成完代码再事后补契约。更高效的做法是先在项目中定义好核心接口的“契约模板”然后把模板作为 Prompt 的输入让 LLM 在给定契约空间内生成实现。例如定义一个接口模板# template.py def process_payment(user_id: int, amount: float) - str: 契约 - 前置条件user_id 0, amount 0 - 后置条件返回非空字符串 payment_id - 允许效果READ, WRITE - 禁止效果IO, RANDOM ...然后在 Prompt 中要求 LLM 只补全...部分不允许修改契约注释。这样生成代码的边界从一开始就是确定的。8.2 把效果检查集成到 CI/CDeffect_checker.py可以封装成一个简单的命令行脚本然后接入 CI 流水线。例如python effect_checker.py --check order_service_final.py --allowed READ,WRITE在 Git 提交前或合并请求前运行这个命令任何代码只要检测到未声明副作用就阻断合并。这是约束 LLM 生成代码的最强手段之一——它不依赖模型自律而是靠构建系统强制校验。8.3 默认 PURE例外声明团队内部可以约定一个默认原则所有函数默认是纯函数任何副作用都必须显式声明。这个原则听起来严格但能带来两个好处LLM 生成代码时如果被要求“默认 PURE”它会倾向于写出更简洁、更可测的纯函数人工 review 时只需要关注那些显式声明了declares或有副作用注释的函数缩短审查范围。8.4 契约校验失败要记录上下文当契约校验失败时不要只抛出一个异常。把入参、调用方、契约类型前置/后置都记录到日志中。这样无论是人工排查还是让 LLM 根据失败日志修复代码都会更容易。在装饰器内部增加日志钩子时可以采用类似下面的思路def pre(condition, message, on_errorNone): def decorator(func): wraps(func) def wrapper(*args, **kwargs): if not condition(*args, **kwargs): if on_error: on_error(func, args, kwargs, message) raise PreconditionError(message) return func(*args, **kwargs) return wrapper return decorator这样可以在on_error回调中接入监控告警而不是简单抛异常。8.5 对 LLM 生成代码做针对性测试除了测试正常路径和异常路径还要专门测试“契约是否被遵守”。具体来说分别测试前置条件满足和不满足的场景测试返回值是否符合后置条件用effect_checker静态扫描代码而不是只运行测试用例对包含副作用的代码做幂等性测试确保重复调用不会产生重复效果。8.6 生产环境数据库操作的安全底线涉及数据库写入、删除、更新时无论代码是人类写的还是 LLM 生成的都必须遵守同样的安全规则变更前在测试环境验证生产库操作前先备份使用事务保证原子性权限遵循最小原则LLM 生成的代码运行账户不应该拥有管理员权限。这些规则与契约、效果检查同样重要它们决定了一段代码能造成的“实际损失上限”。8.7 结合 LLM Agent 场景当前很多项目已经不只是让 LLM 生成一段代码而是让 LLM Agent 直接调用工具、执行命令。这时“效果系统”的意义更加重大。如果 Agent 的工具调用没有效果约束它可能在查询接口中误触发写入操作调用危险 Shell 命令循环调用外部 API 造成资损。把Effect作为工具注册表的一部分明确每个工具允许的效果类型Agent 的每一次工具调用都要经过效果校验可以极大地降低失控风险。9. 总结与下一步这篇文章从 LLM 生成代码的“不可靠”出发介绍了两个关键原则Design by Contract 和 Effects。我们亲手实现了一个轻量框架contract.py提供前置条件和后置条件装饰器effect_checker.py用 AST 静态扫描检测副作用声明是否与实际相符完整的订单服务案例展示了如何从“危险代码”改造成“受控代码”一套 Prompt 模板可以帮助 LLM 从生成阶段就遵守契约边界。核心思想一句话总结契约负责告诉代码“什么能进、什么能出”效果负责告诉代码“哪里能碰、哪里不能碰”。这两个原则并不需要复杂的编译器支持用 Python 标准库就能实现一个可用的版本。下一步你可以继续探索把这里的手写装饰器替换为icontract或deal等成熟契约库并研究它们的性能开销研究 TypeScript/Java 等语言中的契约与效果表达方式把效果检查扩展为 mypy 插件或自定义 linter 规则集成到更完整的静态检查体系在 LLM Agent 场景中把 Effect 作为工具注册表的元数据限制 Agent 的行为范围。如果你在项目里遇到过 LLM 生成代码导致的奇怪问题欢迎在评论区分享。后续我还会整理更多关于 LLM 应用开发、Agent 工具约束的实战案例可以顺手收藏备用。
返回列表