用契约式设计与Effects显式化提升LLM生成代码的可验证性

📅 2026/8/27 2:06:00
用契约式设计与Effects显式化提升LLM生成代码的可验证性
这次我们不看新的本地部署工具也不聊某个模型的显存占用而是讨论一个直接影响 LLM 生成代码质量的方法论问题为什么 Design by Contract 和 effects 对 LLM 生成的代码来说是必需品。如果你已经用 Claude Code、Cursor 或各种大模型接口生成过业务代码大概率遇到过这种场景代码能跑通但边界条件处理不对函数返回了错误类型副作用被藏在某个角落测试一跑就随机失败。这几个问题恰恰是契约式设计和 effects 系统能解决的。先说结论LLM 生成代码最大的问题不是跑不通而是跑起来之后你不知道它什么时候会坏。传统的类型系统和单元测试能兜住一部分问题但真正做到可验证、可回归、可审计需要在生成代码之前就把“契约”写清楚把“副作用”显式暴露出来。本文会从工程实践角度拆解这两件事给出可落地的提示词模板、代码示例和批量验证方法适合正在用 LLM 写业务代码、做代码审查或搭自动化测试的开发团队参考。1. 核心能力速览这篇文章不是推荐某个具体开源项目而是一套可以嵌入现有开发流程的方法论组合。先把核心信息整理成表格方便快速判断是否需要继续读下去。能力项说明主题类型LLM 代码生成质量与工程方法论核心方法Design by Contract契约式设计、effects 副作用显式化解决的核心问题LLM 生成代码的边界错误、副作用不可控、测试不稳定、回归困难适用场景业务逻辑生成、API 编排、数据处理、自动化测试、代码审查依赖工具LLM API 或本地模型、类型系统、断言库、测试框架、静态检查工具关键收益生成代码可验证、可回归、边界清晰、副作用可追踪落地成本需要投入提示词设计、契约测试模板和批量评估脚本中期收益明显是否支持批量任务支持可通过脚本对多个生成结果批量跑契约测试是否支持 API 集成支持模型服务本身提供接口契约测试也可做成独立服务显存与硬件要求走模型 API 时本机无显存压力本地模型需按参数规模评估文章后面会展开这套方法真正的价值在于它不依赖某个特定模型也不要求你换掉现有的 LLM 工具链。无论你用的是 Claude Code、Kimi Code、还是公司内部部署的开源模型都可以在提示词、代码结构和验证流程三个层面把它嵌进去。2. 为什么 LLM 生成的代码需要 Design by Contract2.1 生成代码的典型问题先看几个真实开发中非常常见的现象。第一次用 LLM 生成代码时大多数人关注的是“能不能跑通”验证方式通常是直接执行一遍主流程。主流程一旦通了就默认这个函数是可信的。但问题恰恰出在这里。我最近在一个数据清洗项目里让 LLM 生成一个日期解析函数输入是字符串输出是标准化的YYYY-MM-DD。主流程测试完全通过但把空字符串、None、2024-13-40、带时区的 ISO 字符串丢进去函数的行为就完全不可预测了。有的分支返回原始字符串有的分支抛异常有的分支静默返回None。调用方拿到返回值之后继续往下算最后在完全无关的位置爆出一个很难追踪的错误。这不是偶然现象。LLM 在生成实现代码时对“输入到底长什么样、输出必须满足什么约束、内部状态不能违反什么规则”这三件事的把握取决于提示词里有没有把这些约束写清楚。你不写模型就按照训练数据里的平均风格发挥。而训练数据里的函数边界处理往往是参差不齐的。另一个典型问题是返回值设计随意。同一个业务模块里LLM 生成的函数有的用异常表达失败有的返回None有的返回false有的返回一个带error字段的对象。调用方代码是另一个 LLM 生成的它并不知道每个函数到底怎么表达失败于是只能逐个试。最后的结果是代码能跑但整个模块的错误处理逻辑混乱出问题的时候很难定位。2.2 契约式设计解决什么问题Design by Contract 的核心思想其实很简单一个函数不是孤立的实现而是一个“契约”的履行方。契约由三部分组成前置条件、后置条件、不变式。前置条件规定调用方必须满足什么。后置条件规定函数返回时世界应该处于什么状态。不变式规定在某个对象或系统运行过程中始终保持的属性。把这三件事写清楚之后LLM 生成的实现就有了明确的约束框架。它不再“自由发挥”而是在一个边界清晰的盒子里选择实现路径。对我的实际体验来说最有价值的改变是契约让“验证”这件事变简单了。以前验证 LLM 生成的代码要人工读一遍逻辑思考它哪里可能出错。有了契约之后只需要写一个测试先乱造输入去违反前置条件再检查返回值是否满足后置条件。只要后置条件断言能通过实现内部怎么写风险都可控。同时契约也保护了调用方。前置条件一旦写在代码里调用方必须遵守调用方 Layer 生成时就能通过静态检查或运行时断言发现问题。LLM 生成调用方代码时前置条件就是它需要处理的输入约束。这让两个独立生成的代码块更容易对齐。3. Design by Contract 的落地方式3.1 前置条件、后置条件与不变式的代码表达先说具体怎么写。用 Python 举例我们定义一个格式化数字字符串的函数。契约如下前置条件输入必须是字符串且内容是非负十进制数允许小数。后置条件输出是字符串包含千分位逗号小数部分不被截断。不变式函数不修改外部状态不写日志不访问网络。代码可以这样表达def format_number(value: str) - str: # 前置条件 if not isinstance(value, str): raise TypeError(value must be str) if not value.strip(): raise ValueError(value must not be empty) # 解析十进制数允许小数 try: decimal_value Decimal(value) except: raise ValueError(fcannot parse decimal: {value!r}) if decimal_value 0: raise ValueError(value must be non-negative) # 实现 parts format(decimal_value, ,).split(.) integer_part parts[0] if len(parts) 1: fraction_part parts[1] else: fraction_part # 后置条件 if not isinstance(result, str): raise AssertionError(postcondition failed: result must be str) if result ! expected_format: raise AssertionError(postcondition failed: unexpected format) return result注意上面这段代码我刻意没有写完整个实现因为重点不是这个函数本身而是结构。真正交给 LLM 生成时提示词应该把前置条件和后置条件作为硬性要求写进去让模型在生成实现之前先列出契约然后再写代码。3.2 用类型系统表达契约运行时断言是一种兜底但更早的检查发生在类型层面。TypeScript、Java、Kotlin、Rust 这类静态类型语言可以把一部分契约直接放进类型签名里。比如把失败情况建模成类型而不是依赖异常或nulltype ParseResult | { ok: true; value: string } | { ok: false; error: string }; function formatNumber(value: string): ParseResult { if (!/^\d(\.\d)?$/.test(value)) { return { ok: false, error: invalid number format }; } // 实现格式化逻辑 const formatted new Intl.NumberFormat(en-US).format(Number(value)); return { ok: true, value: formatted }; }这样调用方必须处理error分支不可能默认拿到一个string直接往下传。类型系统就把“可能失败”这个后置条件变成了编译期可见的契约。LLM 生成调用方代码时看到ParseResult这个联合类型自然知道要分情况处理。3.3 用运行时断言与测试固化契约类型系统能表达一部分约束但表达不了“字符串必须是非负十进制数”“数组必须非空”“返回结果必须满足某种格式”这类内容。这时候需要把契约写进测试。把契约测试单独拆一个文件是关键实践。例如下面的test_contract.py专门验证函数的前置条件和后置条件与业务实现解耦import pytest from decimal import Decimal from your_module import format_number def test_precondition_rejects_negative(): with pytest.raises(ValueError): format_number(-1) def test_postcondition_returns_thousands_separator(): result format_number(1234567.89) assert result 1,234,567.89 def test_postcondition_keeps_fraction(): result format_number(0.00001) assert result 0.00001这份测试文件就是契约的机器可读版本。LLM 生成的实现如果违反了契约测试会直接失败不需要人工去读实现逻辑。批量评估时契约测试就是那个统一尺子。4. Effects 系统让副作用显式化4.1 什么是 effects以及副作用为什么危险函数式编程语境里的 effects指的是一个操作除了返回纯值之外对外部世界产生的所有影响。常见的包括I/O 读写、HTTP 请求、数据库写入、日志输出、随机数生成、获取当前时间、修改全局状态。LLM 生成代码时副作用的危险在于它通常不做显式管理。模型从训练数据里学到的函数经常随手写个print()、随手操作全局变量、随手在函数内部发一个网络请求。这些函数单独看没问题组合在一起就非常难测试。因为测试结果取决于外部环境比如当前时间、网络状态、临时目录内容。你跑一次测试通过了再跑一次可能就失败而且失败的原因跟你的代码逻辑完全无关。我在一个小型自动化脚本里遇到过这种情况LLM 生成的处理函数在内部偷偷调用了某个外部 API每次执行都会产生网络时延和潜在限流。单元测试变成集成测试一条本来应该 10 毫秒跑完的用例实际要等 2 秒还不稳定。4.2 把 effects 从实现细节提升为接口设计解决思路是反过来的不要禁止副作用而是让副作用成为接口设计中必须被看见的部分。具体落地有几个层次。最低的层次是依赖注入函数需要的文件路径、HTTP 客户端、数据库连接都通过参数传入而不是在函数内部自己创建。中间一个层次是返回值建模把“可能失败、可能产生副作用”用类型表达出来比如TaskResultT、IOError, T、EitherError, T。最高的层次是 effect 系统由运行环境统一调度副作用应用代码不直接执行 I/O。对于 LLM 生成代码这个场景不需要一上来就上完整的 effect 库先做到依赖注入和返回值建模就已经能解决大部分问题。type NotifyResult | { ok: true; messageId: string } | { ok: false; error: rate_limited | network_error }; async function processPayment( userId: string, amount: number, deps: { loadUser: (id: string) PromiseUser; deductBalance: (id: string, amount: number) Promisevoid; sendNotification: (userId: string, title: string) PromiseNotifyResult; } ): Promisevoid { const user await deps.loadUser(userId); await deps.deductBalance(user.id, amount); const notify await deps.sendNotification(user.id, 扣款成功); if (!notify.ok notify.error rate_limited) { // 通知失败不阻塞主流程但必须记录 console.warn(notification rate limited, userId); } }这个函数里loadUser、deductBalance、sendNotification全部通过deps参数注入。LLM 生成测试时只需要 mock 这三个函数不需要真的连数据库、不需要真的发通知。而且NotifyResult把“通知可能失败”这件事显式暴露出来了生成代码时就必须处理失败分支。4.3 副作用显式化对 LLM 生成的直接影响副作用显式化之后LLM 生成实现时会发现提示词里的约束更多了。它不能随手创建一个fetch请求因为依赖没有注入它不能默默吞掉异常因为返回值类型要求它返回一个明确的失败分支。这些约束看起来增加了生成负担实际反而降低了出错率。原因很直接LLM 在一个约束明确的盒子里生成代码比让它自由发挥更稳定。约束限制了搜索空间模型不需要决定“错误处理用什么风格”“外部依赖从哪里来”这些在接口设计阶段已经定死了。模型只需要专注实现业务逻辑本身。从多次生成的结果看带显式 effect 接口的代码第一次生成通过契约测试的概率明显高于没有约束的版本。这个结论在不同模型上有差异但方向是一致的。5. 适用场景与使用边界契约式设计和 effects 显式化不是银弹必须选对场景才能发挥价值。最适合的是业务逻辑、数据转换、API 编排、流程控制这类方向。这些代码的正确性可以用输入输出关系描述契约测试写起来直接副作用可以集中到边界层处理。举几个例子订单金额计算、用户输入校验、数据格式标准化、文件批处理脚本、定时任务。用 LLM 生成这类代码时花时间写清楚契约收益非常明显。不太适合的场景是硬件驱动、嵌入式实时系统、极端性能敏感的热点路径。这类代码的核心约束往往不是输入输出关系而是时序、内存占用、功耗、中断频率。把时间花在契约设计上不如直接手写并用专用工具验证。另一个边界是项目周期非常短的临时脚本。如果一段代码写完就跑一次之后不会再改那契约测试的成本确实可以省掉前提是你对正确性要求也不高。合规和安全边界同样需要注意。LLM 生成的代码如果涉及用户数据、支付信息、密钥管理必须人工审查不能只依赖契约测试。契约能保证函数输入输出满足定义但无法保证你的业务逻辑本身合法。尤其不要直接把 API Key 或密钥写进生成代码里要用环境变量或密钥管理服务注入。使用人脸、声音、版权素材相关的代码生成时要确认授权链条完整。这些内容不是技术细节是使用任何 LLM 生成代码方案时都不能跳过的红线。6. 验证环境与前置条件这套方法的验证环境比部署一个模型简单得多。如果使用云端模型 API本机只需要准备语言运行时、依赖管理工具和一个测试框架。如果使用本地开源模型则需要考虑显存和磁盘空间。为了满足不同需求我给一个通用检查清单。操作系统Windows / Linux / macOS 均可本文示例无平台依赖。Python 环境Python 3.10建议使用venv或uv管理虚拟环境。Node.js 环境Node 18用于运行 TypeScript 示例和测试。包管理器Python 使用pip或uvNode 使用npm或pnpm。LLM 访问方式推荐先从 API 服务开始成本低、稳定、不需要考虑显存如果必须本地部署再根据模型参数量确认显卡配置。磁盘空间API 模式只需几十 MB 依赖本地模型根据参数量从几 GB 到几十 GB 不等。端口占用如果要把契约测试做成服务指定一个空闲端口比如 8000如果只跑命令行测试不涉及端口。本地模型部署的显存占用需要按实际模型版本测试。以常见的开源对话模型为例7B 量化模型通常需要 6GB 到 8GB 显存13B 需要 10GB 以上70B 基本需要多卡或大显存。这些数值会随量化方式、上下文长度、并发数变化。读文章时不要把它当成固定标准先跑一个最小测试用nvidia-smi观察实际占用再决定要不要上更大参数量的模型。如果只是编写和验证业务代码推荐直接用 API把显存留给其他更重的任务。安装依赖时核心是测试框架和类型检查工具# Python 示例 python -m venv .venv source .venv/bin/activate # Windows 用 .venv\Scripts\activate pip install pytest pytest-cov hypothesis # Node.js 示例 npm init -y npm install -D typescript vitest tsx如果你实际项目里已经有测试框架不必重复安装。这套方法论不绑定特定工具pytest、JUnit、Vitest都可以充当契约验证的执行器。7. 代码生成测试与效果验证7.1 基础契约生成测试先跑一个最基础的功能测试让 LLM 在提示词里先写契约再写实现。以“金额格式化函数”为例可以这样写提示词请实现一个 Python 函数 format_number(value: str) - str。 要求 1. 先写前置条件输入必须是字符串内容为非负十进制数允许小数。 2. 再写后置条件输出包含千分位逗号小数部分不截断。 3. 实现必须满足上述契约所有前置条件不满足时抛出 ValueError。 4. 不要使用外部依赖不要访问网络不要修改全局状态。把这段提示词发给 LLM生成完成后把实现代码保存到项目里并在test_contract.py中补充契约测试。然后运行pytest test_contract.py -v预期结果是所有测试通过。这只是一个基线测试真正的验证维度在下面几个方向正常输入1234567.89返回1,234,567.89。边界输入0、0.00001、999。非法输入-1、、abc、None。随机输入用hypothesis生成大量合法和非法的字符串检查会不会出现未捕获异常。7.2 带副作用代码的生成测试第二步是测试 effects 显式化是否有效。继续用前面的processPayment示例写一份测试文件import { describe, it, expect, vi } from vitest; import { processPayment } from ./payment; describe(processPayment contract, () { it(should deduct balance after loading user, async () { const loadUser vi.fn().mockResolvedValue({ id: u1 }); const deductBalance vi.fn().mockResolvedValue(undefined); const sendNotification vi.fn().mockResolvedValue({ ok: true, messageId: m1 }); await processPayment(u1, 100, { loadUser, deductBalance, sendNotification, }); expect(deductBalance).toHaveBeenCalledWith(u1, 100); expect(sendNotification).toHaveBeenCalled(); }); it(should not crash when notification rate limited, async () { const loadUser vi.fn().mockResolvedValue({ id: u1 }); const deductBalance vi.fn().mockResolvedValue(undefined); const sendNotification vi.fn().mockResolvedValue({ ok: false, error: rate_limited }); await expect( processPayment(u1, 100, { loadUser, deductBalance, sendNotification, }) ).resolves.toBeUndefined(); }); });通过注入 mock 依赖测试不会真正发起通知请求也不会依赖数据库。每次运行都稳定。如果生成的实现直接在里面写死了网络调用这个测试会失败因为sendNotification根本不会被调用。这其实就是用测试验证“副作用是否保持在边界层”。7.3 判断成功的标准一个函数生成得是否成功不要只看主流程有没有输出。更可靠的判断标准是前置条件测试全部通过非法输入按契约被拒绝没有静默吞错。后置条件测试全部通过合法输入的结果格式正确。副作用隔离测试通过通过依赖注入的 mock 能完全控制外部行为。随机回归测试通过大量随机输入没有触发未捕获异常。如果这四条全过可以认为这段 LLM 生成代码达到了“可合入”的底线。之后再考虑代码风格、模块划分、性能优化这类更进一步的问题。如果测试没过不要直接让 LLM 重写整个函数把失败的测试用例作为反馈发给模型让它针对性修效率会高很多。8. 接口 API 与批量评估实践8.1 模型 API 调用示例契约测试和批量脚本需要调用模型接口。不同厂商的接口格式不同但结构上高度相似构建提示词、发送请求、解析结果。下面给一个通用 Python 示例实际使用时按模型服务商提供的接口文档调整路径和参数import requests import os api_key os.environ.get(LLM_API_KEY) url https://your-llm-service.example/v1/chat/completions prompt 请实现一个 Python 函数format_number(value: str) - str。 先列出前置条件和后置条件再写实现。 payload { model: your-model-name, messages: [ {role: system, content: 你是一个严格遵循契约的代码生成助手。}, {role: user, content: prompt}, ], temperature: 0.2, max_tokens: 800, } response requests.post( url, headers{ Authorization: fBearer {api_key}, Content-Type: application/json, }, jsonpayload, timeout120, ) print(response.json()[choices][0][message][content])不要把 API Key 硬编码在代码里。用环境变量或密钥管理服务避免密钥泄露。这是最基本的工程习惯LLM 生成代码时尤其容易踩这个坑因为模型会倾向于把“看起来像配置”的内容直接写进源码。8.2 批量评估脚本批量评估是接口能力最有价值的部分。单独的代码生成演示说服力有限批量跑五十次、一百次统计契约通过率才有参考意义。核心逻辑很简单对同一个契约任务每次调用模型生成一段实现写入临时文件再运行测试框架统计通过率。import subprocess import tempfile from pathlib import Path import requests import os def generate_implementation(session, prompt: str, idx: int) - str: url https://your-llm-service.example/v1/chat/completions api_key os.environ[LLM_API_KEY] payload { model: your-model-name, messages: [ {role: system, content: 你是一个严格遵循契约的代码生成助手。}, {role: user, content: prompt}, ], temperature: 0.4, max_tokens: 800, } resp session.post( url, headers{Authorization: fBearer {api_key}}, jsonpayload, timeout120, ) content resp.json()[choices][0][message][content] return content session requests.Session() result_counts {passed: 0, failed: 0} with tempfile.TemporaryDirectory() as tmpdir: for idx in range(30): # 生成实现 code generate_implementation(session, CONTRACT_PROMPT, idx) impl_file Path(tmpdir) / fgenerated_{idx}.py impl_file.write_text(code, encodingutf-8) # 运行契约测试 test_file Path(tmpdir) / ftest_{idx}.py test_file.write_text(CONTRACT_TEST_TEMPLATE, encodingutf-8) proc subprocess.run( [pytest, str(test_file), -q], capture_outputTrue, textTrue, cwdtmpdir, ) if proc.returncode 0: result_counts[passed] 1 print(fcase {idx}: PASS) else: result_counts[failed] 1 print(fcase {idx}: FAIL) pass_rate result_counts[passed] / 30 * 100 print(fpass rate: {pass_rate:.1f}%)这个脚本的价值在于它把“LLM 到底能不能稳定生成符合契约的代码”这个问题量化了。第一次跑的时候可能会发现通过率不到一半。这个结果比任何主观判断都有效它能逼你改进提示词比如增加示例、细化前置条件描述、把返回类型写进系统提示。通过率可以作为提示词版本的指标来追踪。今天版本通过率 40%改完提示词之后 70%说明方向是对的。8.3 批量任务的失败重试批量调用接口时网络超时、限流、返回格式异常都是常见问题。脚本里要加上失败重试和错误隔离。单个请求失败了不中断整个批次重试两次还不够的把输入记录下来最后统一处理。import time def generate_with_retry(session, payload, max_retries3): for attempt in range(max_retries): try: resp session.post(payload_url, jsonpayload, timeout60) resp.raise_for_status() return resp.json() except (requests.Timeout, requests.HTTPError) as exc: if attempt max_retries - 1: raise time.sleep(2 * (attempt 1))同时要用指数退避控制重试间隔避免打爆接口。批量任务建议一次跑 20 到 30 个样例先观察稳定性再扩大到上百个。并发数也不要一开始就拉满按接口限流要求来。9. 资源占用与性能观察前面提到的资源占用核心可以从两个维度观察。第一是生成阶段调用模型接口时本机几乎不消耗显存消耗的是 token 配额和 API 调用成本如果本地部署模型显存消耗由模型参数和上下文长度决定。第二是验证阶段跑契约测试时主要消耗 CPU 和少量内存瓶颈通常在测试框架和子进程启动上。如果你本地部署开源模型来生成代码我用一个保守的建议先用量化版本跑最小测试不要一开始就追求最大参数模型。观察nvidia-smi里的显存占用、推理速度和首 token 延迟。一次完整的代码生成输入提示词可能 500 到 1000 token输出实现可能 300 到 800 token。按这个量级估算上下文窗口 8K 的模型基本够用更大的上下文对代码生成语法类任务提升有限反而增加显存压力。验证阶段的性能优化主要是减少子进程启动开销。批量评估脚本里每个样例启动一个 pytest 进程30 个样例可能要跑几十秒如果扩大到 100 个时间会明显增加。另一种做法是只做模块导入和函数级断言不启动完整测试框架速度更快但覆盖的测试能力也弱一些。实际情况里建议分两层快速规则检查跑全量完整契约测试只跑抽检。如果发现批量任务卡住优先检查请求超时设置、子进程等待时间和临时文件路径权限。接口调用失败时先看返回状态码确认是网络问题、鉴权问题还是限流再决定要不要重试。契约测试本身失败时要把实现代码和测试输出同时留存下来方便复盘 LLM 到底在哪类契约上栽跟头。10. 常见问题与排查方法把实际操作中最容易遇到的问题整理成一个排查表按现象定位原因比逐行读日志更快。问题现象可能原因排查方式解决方案生成的函数不检查前置条件提示词没有明确要求契约检查生成代码开头是否有输入校验在提示词中增加“前置条件不满足时必须抛出异常”的规则契约测试通过但线上数据出错后置条件写得太弱检查后置条件是否覆盖所有业务规则增加更严格的断言并补充随机数据测试测试运行结果不稳定实现里隐藏了网络请求或全局状态用 mock 注入依赖检查函数是否直接调用外部服务把外部依赖改造成参数注入禁止在函数内部自行创建返回结果类型不统一提示词没有规定失败表达方式查看不同调用返回的是 null、false 还是异常用联合类型或 Result 类型统一失败表达批量脚本部分请求超时并发数过高或网络不稳定查看接口日志和超时设置降低并发数加入重试和指数退避API 返回 401API Key 缺失或权限不足检查环境变量和调用头确认密钥有效不要硬编码在代码中本地模型显存不足模型参数过大或上下文过长运行 nvidia-smi 查看显存占用换量化版本、缩短上下文、降低 max_tokens生成的代码风格混乱提示词约束不足对比多次生成结果在系统提示中给出代码风格规范比如禁止 print、禁止全局变量端口冲突导致 API 测试失败本地服务占用了相同端口查看进程监听状态更换端口或关闭冲突进程最值得重视的其实是第三条测试不稳定。如果把问题归因于“环境问题”而跳过副作用就会继续藏在代码里。正确做法是让测试通过 mock 完全隔离外部依赖保证契约测试在任何机器、任何时间运行都得到相同结果。做到这一点LLM 生成代码的可信度才会真正提升。11. 最佳实践与工程建议到目前为止方法已经讲完了最后落到工程实践层面有几条建议对实际团队最有用。第一提示词里强制要求“先生成契约再写实现”。不要把实现作为唯一输出。要求模型先列前置条件、后置条件、不变式然后再给出代码。这样即使实现有问题你也有一份可审查的契约文档。实际测试中这个简单的提示词改动往往就能显著提高代码质量。第二把契约测试和业务测试分开。契约测试只验证输入输出关系、错误处理和副作用边界业务测试验证具体业务流程。分开之后生成代码的评估指标更清晰。契约测试通过代表函数“行为正确”业务测试通过代表调用方“使用正确”两者混在一起很难定位问题。第三模块边界上显式处理副作用。文件读写、网络请求、数据库操作、时间获取都集中到专门的服务层或仓库层。业务逻辑层只接受注入的依赖。LLM 生成业务逻辑时就不会随手在中间加一个外部调用。这个实践几乎适用于所有业务项目无论你用不用 LLM。第四批量评估要留存样本。跑 30 个样例统计通过率时要把每个样例的输入、输出、测试结果保存在独立目录里。后续改提示词、换模型时可以拿同一批历史样例做对比避免只靠印象判断“新提示词更好”。第五不要把 API Key、数据库密码、内部服务地址写进生成代码。LLM 生成的内容可能来自训练数据也有可能无意间包含敏感信息。代码审查阶段必须把密钥扫描纳入检查清单。第六合入之前做人工复核。契约测试能验证“实现是否满足契约”但不能验证“契约本身是否满足业务真实需求”。真实业务里有人写错了前置条件导致线上数据异常这种情况测试是查不出来的。LLM 生成的代码越稳定越容易让人忽略契约定义阶段的人工投入这一步不能省。12. 总结与下一步这套方法论最值得尝试的点是它能把 LLM 生成代码这件事从“跑通万事大吉”推向“可验证、可回归、可评估”。你不需要立刻全面铺开可以先挑一个数据清洗函数、一个 API 编排模块把一个契约写完整跑一轮批量测试看看通过率是多少。建议优先验证的功能是提示词要求 LLM 先生成契约后用契约测试做批量评估。这是投入最小、反馈最直接的切入点。最容易踩的坑有两个一个是前置条件写得太宽松导致非法输入直接穿透另一个是副作用没有隔离测试在本地过、在 CI 上挂。这两个坑一旦踩过你就能真正理解为什么这篇内容要反复强调契约和 effects。下一步可以扩展的方向包括把契约测试接到 CI 流程里让每次 LLM 生成的 PR 自动跑契约测试并报告通过率把提示词模板做成团队共享配置统一风格或者更进一步把契约定义本身交给 LLM 生成再由专家审查形成一个“契约生成 - 实现生成 - 契约验证”的半自动闭环。这样LLM 在项目里承担的就不只是“写代码的助手”而是一个“在约束盒子里稳定交付的工程角色”。