LLM生成代码的兜底方案:契约式设计与副作用管理
1. 先想清楚LLM 生成的代码到底缺了什么大模型生成代码这件事现在已经不是“能不能跑”的问题而是“跑起来之后敢不敢长期用”的问题。很多开发者让 LLM 写一段排序函数、一个接口调用、一个配置文件都能很快拿到结果看起来也像模像样。但一旦进入真实业务代码被 review、被测试、被部署、被多个人维护问题就开始暴露。这个主题的核心观点很直接LLM 生成的代码必须靠 Design by Contract契约式设计和 effects副作用/效果管理来兜底否则只能停留在 Demo 阶段。先说人话解释这两个概念。Design by Contract 的意思是代码里的每个函数、模块、接口都应该有明确的“契约”。调用方负责满足前置条件被调用方保证输出结果双方都遵守约定。这个思想不是新东西写传统代码时很多人已经在用只是没有系统化表达。到了 LLM 生成代码的场景它变得格外重要因为模型不会天然知道你业务里的边界条件是什么它只会根据训练数据里的统计规律去“猜”。如果不在生成时把契约写清楚模型就会默认一切正常然后在边界条件上翻车。effects 指的是代码中会改变外部世界状态的副作用比如读写文件、调用 API、修改数据库、发消息、操作 GPU 显存、访问网络等。LLM 写代码时特别容易忽略这类操作的失败分支和恢复逻辑。模型会写“调用接口拿到数据”但可能不写接口超时怎么办会写“把结果保存到文件”但可能不写磁盘满了怎么办会写“并发执行任务”但可能不写资源竞争和死锁。理解这两个词之后再看 LLM 生成代码的现状会发现很多问题不是模型智力不够而是缺少约束和边界意识。这篇文章就围绕这个问题拆解为什么契约和副作用管理是 LLM 生成代码的核心基础设施以及在实践里怎么落地。适合谁看正在用 LLM 辅助开发的工程师、做 AI 编程工具的产品和技术同学、以及对代码质量和可维护性有要求的团队。如果你只是偶尔让模型写一个脚本自己跑可能感受不深但一旦进入团队协作和生产环境这些问题就会变成事故。2. 为什么 LLM 生成的代码容易“能跑但不敢用”我见过太多这样的流程开发者把需求描述给 LLM模型返回一段代码复制到编辑器运行通过提交。这个过程看起来效率很高但问题出在“通过”的定义上。大多数时候所谓通过只是“当前输入下没有报错”而不是“所有合法输入下都符合预期”。2.1 模型的统计本质决定了它只会“猜”边界LLM 生成代码的本质是概率预测。它看到你的 prompt根据训练数据里学到的模式逐字生成最可能的代码序列。这个过程中它没有真正理解你的业务逻辑也不知道你的数据分布更不了解你的系统里有哪些隐藏约束。举个例子。你让模型写一个函数把用户输入的字符串转成整数。模型可能很快写出int(value)这在 Python 里确实能跑但如果输入是12.5、abc、空字符串、None函数就会直接抛异常。一个合格的程序员会先想清楚这个函数的输入范围是什么非法输入应该返回什么是抛出特定异常还是返回默认值这些就是契约的一部分。LLM 不会主动问这些问题除非你在 prompt 里明确要求。大多数情况下模型只会按最常见的路径生成代码把边界情况留给运行时去暴露。2.2 副作用是 LLM 代码里最容易被忽略的雷区如果只是纯计算函数模型出错的影响范围还比较可控。真正的风险来自副作用操作。我让 LLM 写一个批量文件处理脚本它会自然写出遍历目录、打开文件、处理内容、保存结果这样的流程。但以下问题模型通常不会主动处理文件不存在或路径权限不足时怎么办处理过程中磁盘空间耗尽怎么办某个文件格式不符合预期时是跳过还是终止并发处理时多个任务同时写同一个文件怎么办任务中断后能否断点续跑还是从头再来这些都属于 effects 管理。LLM 生成的代码如果不显式处理这些问题就会在真实数据上随机爆炸。你可能跑 100 个文件没问题跑到第 101 个文件时遇到一个异常整个任务挂掉前面处理的结果也白费了。2.3 “能跑”和“正确”之间隔着大量隐性判断做技术的人都知道一段代码能运行只说明语法正确、运行时没有触发致命错误但完全不代表逻辑正确。对 LLM 生成的代码来说这个差距更大。判断一段代码是否正确需要检查前置条件是否清楚非法输入是否有明确定义返回值是否符合调用方预期副作用是否在失败时正确回滚或清理边界条件下资源占用是否可控并发情况下数据是否一致这些判断如果只靠肉眼 review效率很低如果不 review直接上生产风险极高。Design by Contract 和 effects 管理就是要把这些隐形判断变成代码里可验证、可执行、可检查的显式部分。3. 把契约写进 prompt让 LLM 生成代码前先定义边界既然 LLM 天生不会主动考虑边界那就得在设计阶段强制它考虑。实践上最有效的方式是把契约要求直接写进生成代码的 prompt 里。3.1 一套可复用的代码生成 Prompt 结构我自己在让 LLM 写代码时一般会采用以下 prompt 结构确保模型在生成之前就明确契约请编写一个函数完成以下需求 功能描述 【这里写清楚要做什么】 输入定义 - 参数名、类型、取值范围 - 哪些输入是合法的哪些属于非法输入 - 空值、极值、异常字符如何处理 输出定义 - 成功时的返回值类型和内容 - 失败时抛出什么异常或返回什么错误 副作用说明 - 是否会读写文件、访问数据库、调用 API - 涉及外部资源时的超时设置、失败重试策略 - 资源释放和清理逻辑 边界条件 - 并发调用时是否有状态冲突 - 数据量很大时的性能要求 - 环境依赖Python 版本、系统、第三方库版本 请先列出可能出现的失败场景再编写代码。这个结构不是让模型输出一堆理论而是逼它把一个函数的完整契约说出来。如果模型生成代码之前就能列出失败场景那么代码里大概率会包含对应的处理逻辑。3.2 用契约描述驱动单测生成LLM 生成代码之后另一个常见问题是模型自己生成的代码让它自己写测试用例往往容易“配合演出”——测试覆盖的都是正常路径边界情况被无视。解决办法是把契约描述单独拿出来让 LLM 基于契约生成测试用例而不是基于代码生成测试用例。这样做的好处是测试用例针对的是“契约”而不是“实现”。契约要求输入为空时返回特定错误测试用例就会写这一条契约要求超时必须重试测试用例就会模拟超时场景。这种测试才能真正验证生成代码的健壮性。我在实际项目里的做法是三步先让 LLM 根据需求写出契约描述再让 LLM 根据契约生成代码最后让 LLM 根据契约生成测试用例这三步不要跳过也不要把它们合并在一个 prompt 里因为合并之后模型容易在前面的输出里“暗示”后面的输出测试和代码变成自证失去了独立验证的意义。注意分开生成不是形式主义而是为了让测试用例独立于实现。独立性是测试有效性的前提。4. 运行时副作用管理LLM 代码上生产前的必修课契约主要解决“函数级别”的行为约定副作用管理则要解决“系统级别”的资源安全。一个 LLM 生成的函数即使单体测试全部通过放到有真实 I/O、真实并发、真实资源限制的环境里仍然可能出问题。4.1 先识别代码里的副作用类型LLM 生成代码时最常见的副作用包括文件系统操作读写文件、创建目录、删除文件网络请求调用 HTTP API、访问数据库、连接消息队列进程操作启动子进程、执行系统命令资源占用内存分配、GPU 显存使用、磁盘空间状态修改修改全局变量、更新缓存、写入日志外部依赖使用操作系统命令、需要特定的服务和端口把副作用列出来之后才可以逐个讨论处理策略。实际排查时我发现很多问题不是开发时没意识到副作用而是没有提前把副作用的失败模式写清楚。4.2 给 LLM 代码包一层副作用安全壳与其要求 LLM 生成完美无缺的代码不如在架构层面给 LLM 代码包一层“安全壳”。这层壳负责处理副作用相关的通用问题让业务代码专注于逻辑本身。以 Python 为例一个常见的做法是定义统一的执行函数处理超时、重试、资源释放import time import traceback from typing import Callable, Any class EffectSafety: 为 LLM 生成的代码提供副作用保护壳。 def __init__(self, timeout: int 30, retry: int 3): self.timeout timeout self.retry retry def run_with_safety(self, task: Callable[..., Any], *args, **kwargs) - dict: 带超时和重试地执行任务。 返回统一结构便于调用方判断结果。 result {success: False, data: None, error: , attempts: 0} for attempt in range(1, self.retry 1): result[attempts] attempt try: data task(*args, **kwargs) result[success] True result[data] data return result except Exception as e: result[error] traceback.format_exc() print(f第 {attempt} 次执行失败: {e}) if attempt self.retry: time.sleep(2 * attempt) return result这段代码解决的问题是LLM 生成的函数可能考虑不到网络超时、重试策略、错误日志格式。把这些通用机制放在外层业务函数只负责核心逻辑一次不成功就重试重试还不行就把完整错误信息记录下来方便排查。4.3 用统一的输入输出结构约束异常行为LLM 生成的代码最常见的丑陋行为是不按约定抛异常有时候返回 None有时候抛 KeyError有时候直接系统退出。对调用方来说这种不确定性比逻辑 bug 更棘手。解决思路是统一返回值结构。# 成功结果 { success: True, data: {processed: 10, skipped: 2}, error: None } # 失败结果 { success: False, data: None, error: 文件不存在: /tmp/data.csv }只要 LLM 生成的代码被包在这个结构里调用方就不需要判断“返回值是 None 还是 False 还是抛异常”只需要检查success字段。这个模式在批量任务里特别有用它可以实现单条失败不中断整体任务失败的记录单独标记方便后续重跑。经验批量任务里“单条失败不阻断整体”比“单条一定会成功”更重要。前者保证任务能跑完后者只是一个理想状态。5. 让 LLM 自己识别 effects从一个真实排查案例说起讲一个我最近让 LLM 写文档处理脚本时遇到的例子。需求是读取一批 Markdown 文件把里面包含特定关键词的段落提取出来输出到一个新文件。表面上看这个需求很简单LLM 很快就写好了代码。但真正跑起来的时候遇到三个问题第一部分文件编码不是 UTF-8直接读取报错。模型没有在读取时指定编码也没有做异常处理。第二输出文件最终只有一个但如果线程或多次运行之间没有清空旧内容结果会重复追加。模型没有考虑“输出文件已经存在”的情况。第三输入目录里有些文件不是 Markdown是图片和压缩包模型没有做扩展名过滤。这些问题都不是逻辑难度高的问题但确实会让脚本现场翻车。我把这三个问题整理进 prompt重新跑了一次模型新增了编码检测、文件类型过滤、输出文件覆盖策略。这个过程中我意识到一件事LLM 不是不能处理 effects而是需要你明确告诉它“这里有 effects请处理”。5.1 让 LLM 列出 effects 清单再写码现在我在 prompt 里会加一个固定步骤在正式写代码之前请先完成两件事 1. 列出代码中涉及的所有外部副作用文件、网络、数据库、进程、资源占用等 2. 列出每个副作用可能出现的失败场景 3. 针对每个失败场景给出你的处理方案 4. 确认上述列表完整后再输出完整代码不要小看这一步。它能让模型在生成代码时从“被动词库匹配”转换成“先规划再编码”生成结果的鲁棒性明显提升。当然不是说让模型列了清单就能完全规避问题但它至少能覆盖大多数常见失败模式。5.2 给 LLM 定义一个“错误即数据”的代码风格LLM 生成代码时如果 prompt 里没有明确要求它倾向于用异常处理错误。异常在大多数编程语言里是合理的但在批量任务、接口服务、长时间运行的进程中用返回值表达错误往往更稳定原因有三调用方不需要记住每个函数可能抛什么异常错误信息可以统一收集和记录异常不会中断整个任务流程所以我会在 prompt 里限制错误处理风格。示例请不要使用异常作为流程控制手段。所有可能失败的操作必须返回统一的结果对象。 结果对象至少包含 success、data、error 三个字段。这个风格约束其实就是在把 effects 的处理方式“契约化”。6. LLM 应用开发里的 effects比代码生成更广的一个话题说到这值得把 effects 的话题从“代码生成”扩展到“LLM 应用开发”。因为现在很多团队已经在做 LLM 相关产品比如聊天机器人、内容生成工具、知识库问答、Agent 工作流。这些系统里LLM 不是只生成一段代码而是参与运行时的决策。这时候effects 管理的复杂度更高。6.1 LLM 应用里的典型副作用在一个普通的 LLM 应用中一次请求可能涉及以下副作用调用 LLM API 获取生成结果读取向量数据库或知识库内容调用外部工具 API搜索、天气、日历等写入会话历史或日志根据 LLM 输出触发后续流程这些副作用如果管理不好LLM 应用会表现得极不稳定。我见过不少基于 Agent 框架的 Demo功能看起来很强但实际部署后经常遇到模型输出格式偶尔不匹配导致工具调用失败、外部 API 超时导致整个请求挂起、上下文积累太多导致费用飙升、工具权限过大导致误操作。6.2 给 LLM 的工具调用也定义契约很多 LLM 应用里模型会调用外部工具。这类调用比起纯代码生成更容易产生不可控的副作用。比如一个助手被赋予了“可执行系统命令”的能力模型可能因为理解偏差执行了删除操作一个知识库问答系统被赋予了“可读取任意文件”的权限模型可能访问到不该暴露的数据。这类问题不能只靠提示词解决需要从权限、范围和失败处理三个层面做约束。权限上给 LLM 的工具调用设置最小权限原则只暴露任务必需的工具不暴露所有工具。范围上对工具的输入参数做白名单校验允许访问哪些路径、哪些接口、哪些字段在工具调用入口统一限制。失败处理上工具调用必须返回结构化错误LLM 才能根据错误信息重新规划。如果工具失败只是抛一个乱码异常模型的后续处理完全不可预测。这里还需要提醒一个容易被忽略的点LLM 生成代码和 LLM 运行时调用工具两者对 effects 的处理思路有差别。代码生成场景里effects 是“模型写出来的代码要处理副作用”运行时工具调用场景里effects 是“模型本身发起的外部操作要被系统管理”。前者考的是代码质量后者考的是系统架构。很多项目把这两件事混为一谈导致问题排查时方向错误。6.3 关于编排框架和 Agent 的边界提醒在相关热词里能看到很多关于 LLM 编排框架、Agent、MCP 的讨论。这些框架确实在解决一部分 effects 管理问题比如工具注册、调用流程、上下文传递。但我个人的观点是框架只是提供了“管理 effects 的基础设施”不等同于“已经管理好了 effects”。使用编排框架时至少还要自己确认每个工具的输入是否有校验每次调用的超时时间是否合理失败后的重试策略是否会造成重复副作用连续多次调用时上下文是否正确不会串号并发请求时状态是否正确隔离举个例子一个 Agent 需要调用“发送邮件”工具。如果网络超时了Agent 重试一次结果第一次调用其实已经发送成功第二次重试就会导致用户收到两封邮件。这类问题在 LLM Agent 里很常见本质就是 effects 的幂等性没有设计好。模型再强大也无法弥补这类基础设计缺陷。7. 落地建议不同阶段做不同的事针对不同团队和不同阶段Design by Contract 和 effects 管理的落地重点不太一样。下面按三个阶段拆解。7.1 个人开发者和学习阶段如果你还在学习阶段或者只是在个人项目里用 LLM 写代码首要任务是建立“契约意识”。不需要买工具也不需要上复杂框架先把习惯养成每次让 LLM 写函数先让它列出输入输出定义和失败场景生成代码后自己 review 一遍重点检查边界条件和异常处理手动补充两个最可能被忽略的测试用例空输入和异常输入涉及文件、网络、数据库操作时强制要求模型写异常处理和资源释放这个阶段的目标不是写出生产级代码而是让自己和模型之间形成稳定的协作方式。你会发现模型对你的需求理解越清晰生成的代码质量越接近你的预期。7.2 团队协作和小规模项目阶段到了团队协作阶段契约必须文档化副作用必须统一管理。建议做四件事第一定义统一的代码生成规范。把 prompt 里的契约结构沉淀成团队模板避免每个成员用不同的方式跟模型沟通。第二代码 review 时增加一个“副作用清单检查”。每次 review LLM 生成的代码先问这个代码涉及哪些外部操作失败时会怎样资源能释放吗并发安全吗第三统一错误处理风格。强制使用结果对象模式或统一异常类型避免每个模块的错误风格不一致。第四给模型生成的代码配置基础的运行沙箱。至少要有超时控制、文件访问范围控制、网络访问限制。如果条件允许容器化运行最稳妥。这一步最核心的转变是把 LLM 生成的代码当成“外部贡献者提交的 PR”来对待而不是“自己写的代码”。外部贡献者不了解你的系统约束你需要通过 review、测试和沙箱来兜底。7.3 生产环境和大型系统阶段生产环境里LLM 生成代码可能只是整个系统的一个环节。这时候除了代码本身的契约和副作用管理还要考虑可观测性每次 LLM 辅助生成的代码运行时的关键操作都要有日志灰度发布LLM 生成的新逻辑先小流量跑一段时间观察错误率和资源占用回滚能力生成代码出问题时能够快速回退到上一版本资源隔离LLM 生成的高频任务不要与核心服务抢占资源数据一致性涉及数据库操作时事务边界必须由人工确认不能让模型决定提交时机生产环境最忌讳的是“模型说能跑就部署”。模型说能跑通常只代表它自己觉得逻辑完整了不代表它在你的流量、数据、依赖条件下没问题。把 LLM 生成代码的生产流程拆成生成、契约检查、静态分析、测试、沙箱、灰度、发布每一步都有人或工具把关。8. 常见误区和排查思路最后说几个我在实践中经常看到的误区和对应的排查思路。8.1 误区一报错一定是我 prompt 写得不好这个说法不准确。prompt 可以优化但很多报错不是 prompt 造成的而是运行时环境造成的。你的依赖版本、Python 版本、系统权限、文件编码、网络环境都会影响 LLM 生成代码能否正常运行。排查顺序应该是先看报错信息是语法错误、类型错误、文件错误还是网络错误再确认运行环境依赖是否完整、版本是否匹配、路径是否存在再看输入数据格式是否正确、编码是否符合预期、文件是否为空然后看 prompt契约是否清楚、边界是否定义最后才考虑是重新生成代码还是手动修复不要一上来就重新让模型生成一遍那样大概率还会遇到相同问题因为问题根本不在模型理解而在环境或数据。8.2 误区二加了契约 prompt 之后代码就绝对安全Design by Contract 只是约束模型在生成时“考虑”边界但生成的代码是否真正实现了契约还需要验证。模型给出的契约描述可能和实现代码不一致。比如 prompt 里写了“输入为空时返回空列表”但生成的代码里可能根本没有这个分支。验证方法是让模型基于契约生成测试用例然后真正运行测试看是否全部通过。不要只看模型自己说“测试已通过”要实际跑一遍。8.3 误区三效果管理只需要关注运行时不报错不报错只是最低标准。副作用管理还要考虑幂等性同一个操作执行两次结果是否一致可重复性任务中断后重跑是否会重复制造副作用可清理性失败后临时文件、进程、连接是否能正确释放可观察性操作是否留下了足够的日志方便定位问题这些指标光靠运行不报错是看不出来的需要自己设计验证场景。在我实际操作中最有效的方式是“故意制造失败”手动让文件不存在、让 API 超时、让磁盘写入受限看代码能否稳定处理。这种故障注入测试对 LLM 生成的代码尤其重要因为模型很少主动写好这些场景。8.4 组件和参数建议表针对 LLM 生成代码的契约和 effects 管理我整理了一个常用的检查表可以直接复制到项目模板里检查维度核心问题通过标准输入契约函数接收什么类型、范围、格式的输入非法输入有明确处理方案输出契约成功返回什么失败返回什么调用方不需要猜测返回含义错误处理出错时是抛异常还是返回错误对象错误能被调用方捕获并处理幂等性同一操作执行两次结果是否一致重复执行不会产生重复副作用资源释放文件、网络、数据库资源是否关闭长时间运行无资源泄漏超时控制外部调用是否有超时限制外部服务无响应时不会永久阻塞重试策略失败后是否重试重试次数多少重试不会放大副作用并发安全多个任务同时执行时状态是否隔离无共享状态或共享状态有锁日志记录关键操作是否有日志出现问题可以通过日志定位权限边界代码是否有最小权限不会访问无关资源这张表可以直接作为 LLM 生成代码的验收清单。未通过的项目要么让模型修改要么人工补上。9. 我的建议把契约思维从代码生成扩展到整个 LLM 开发流程说到最后我想把视角再拉高一点。Design by Contract 和 effects 管理看起来是写代码时的具体技巧但实际上是一种思维模式。这种思维模式适用于整个 LLM 开发生命周期设计 prompt 时你要定义模型输入输出的边界设计 Agent 时你要定义工具调用的权限和失败策略设计评测集时你要定义哪些行为算成功哪些算失败设计部署方案时你要定义资源限制、日志规范、回滚条件这些本质上都是契约。只是有些契约约束的是函数有些契约约束的是模型行为有些契约约束的是整个系统。LLM 的不可预测性不会消失但可以通过契约和副作用管理把不可预测性限制在可控范围内。我自己踩过很多坑之后最大的感受是不要指望模型自己具备工程素养。模型能快速生成大量代码但它不会主动关心你的业务边界、资源约束和运维需求。这些必须由人来定义再通过工具、框架和纪律变成系统的一部分。如果只能给一条建议我会说在让 LLM 生成任何代码之前先花五分钟把输入、输出、失败场景和副作用写清楚。这五分钟省下的排查时间通常会远远超过五分钟本身。这背后的逻辑不复杂。LLM 生成代码的价值在于速度快、覆盖广但如果缺少契约和副作用管理这个速度快是建立在高返工率之上的。真正的效率不是第一次生成出多少代码而是这些代码最终有多少能稳定运行在生产环境里。10. 从实战里长出来的几个习惯按惯例最后分享几个我在实践里养成的习惯不一定适合所有团队但应该能给正在踩坑的人一些参考。第一新任务第一次跑永远用最小样例。不管 LLM 生成的代码看起来多完善先拿一个只有几行数据的小文件跑通确认输出格式和日志正常再上完整数据。第二不要让 LLM 在一个 prompt 里既写契约又写代码又写测试。分开做前后独立验证效果远好于一次生成。虽然多花了一点时间但代码质量和测试有效性会提升很多。第三所有涉及外部资源的代码统一走封装好的执行函数。不要允许 LLM 直接裸写open()、requests.get()、os.system()。封装可以统一超时、重试、日志和错误结构。第四给 LLM 生成代码设定“禁止事项”。比如禁止删除文件、禁止覆盖生产数据库、禁止无条件执行 shell 命令。这些限制写进 prompt比事后检查成本低得多。第五留一个专门的“副作用复盘文档”。每个项目上线一段时间后把实际遇到的所有外部资源相关问题记下来。这些记录是后续 LLM prompt 优化的最佳资料比网上任何教程都贴近你的真实场景。第六如果你的项目已经进入 Agent 或多工具编排阶段一定要提前设计好工具调用的权限边界和幂等策略。模型能力越强能触达的系统资源越多effects 失控时的破坏力也越大。这不是说不要用 Agent而是要在使用之前就把刹车系统装好。这段内容写到这里还是那句话真正的问题从来不是 LLM 能不能生成可运行的代码而是这些代码在你的系统里能不能稳定、安全、可维护地运行。Design by Contract 和 effects 管理就是把这句口号变成工程实践的两根支柱。