过去大半年我一直在折腾 AI Agent 写真实业务代码。最头疼的不是它写不出来而是它写出来的东西你不敢信代码结构完整、命名也规范、注释还齐结果一跑测试边界条件炸了一地。后来听到一位在开发者工具领域深耕多年的同行聊起这个话题他说了一句让我印象很深的话让 Agent 写出更正确代码真正的起点不在更强的模型也不在更长的上下文而在于让它在交付之前先学会自我验证。这个说法刚听的时候有点抽象。什么叫自我验证不就是让 Agent 自己检查自己的代码吗后来我在实际工作里反复实验才慢慢体会到这句话的分量。我们很多人一直在琢磨怎么让 Agent 写得更好却很少想过怎么让 Agent 自己判断写得到底对不对。没有这一步模型再大也会该错照样错。这篇文章我想把“自我验证”这件事从头到尾讲透它为什么重要、验证信号到底怎么设计、怎么把它落到一个实际可运行的 Agent 工作流里以及我踩过的那些坑。如果你正在搭 AI 编程助手或者手上有团队要靠 Agent 提效这篇文章应该能帮你省下不少试错成本。1. 为什么“自我验证”是 Agent 写出正确代码的起点1.1 先看清楚 Agent 为什么会“一本正经地犯错”先说一个底层事实LLM 写代码本质上是在做概率预测。你给它一个任务它根据训练数据里见过的模式逐 token 地生成“看起来最合理”的代码。注意是“看起来合理”不是“经过验证正确”。这两个概念之间的距离就是所有问题的来源。我在另外一个地方打过一个比方这就像一位非常自信的新同事。你问他一个问题他会立刻给出一个结构完整、语气笃定的答案但他没有意识到他的答案可能来自对类似场景的模糊记忆而不是对当前场景的实际推演。你如果直接认可他他就会带着这套答案继续往下做直到在某个边角撞出问题。Agent 也一样。代码生成完它自己看不到编译错误也看不到测试失败因为它没有运行过。它只能继续推测“这样写应该没问题”。这并不代表模型偷懒而是它的训练目标里本来就没有“我写完代码要执行一下看看结果再交付”这个环节。所以在谈自我验证之前得先建立这层认知不要指望模型天然知道自己做错了。它写出错误代码不是态度问题而是机制问题。要让它输出更可靠的代码就得在流程上给它补上它缺失的那道“现实检验”。1.2 自我验证解决的核心问题让 Agent 不再凭感觉交付人类写代码为什么相对靠谱不是因为人不出错而是因为人的工作流里有反馈回路写完代码要编译、要跑测试、要 review出错会被拦住然后修正。这一整套反馈回路才是正确性的保障。Agent 写代码如果只有生成、没有反馈就等于把人类工作流里最关键的一截抽掉了。自我验证做的事情本质上就是把这套反馈回路还给 Agent代码生成之后让它在执行环境里跑起来用编译结果、测试结果、运行时的输出来审视自己的产物发现问题再修复直到验证通过。我自己的感受是这一步比很多花里胡哨的技术优化都重要。曾经我花了很多时间调提示词、换更大的模型、加各种 few-shot 示例生成质量的提升都有限。但一旦我把“先验证再交付”写进工作流同样的模型代码合格率直接上了一个台阶。原因很简单——你不再要求它一次就能蒙对而是允许它失败、看结果、再修。1.3 什么才算真正的验证信号既然要自我验证首先得搞清楚“拿什么来验证”。我一般把信号分成两类。硬信号指的是外部环境给回来的客观结果编译器的报错、类型检查失败、lint 规则告警、测试用例通过或失败、接口返回的 HTTP 状态码、运行时抛出的异常。这些信号来自真实执行和模型自己的脑补没有关系不会因为模型“觉得很自信”而发生改变。软信号则是模型自己给出的判断。比如“我觉得这段代码没问题”“从逻辑上讲应该能通过”“按我的理解这样做是对的”。这类信号基本不值钱。它的来源大概率是训练数据里的相似文本并不代表代码真的在跑。我在实际项目里见过很多次 Agent 在没有任何测试输出的情况下直接自信地写一句“All tests passed”这就是典型的软信号在冒充硬信号。自我验证的骨架就是尽可能多地把系统的判定依据建立在硬信号上。一个验证回路里硬信号越多Agent 的自我修正就越可靠软信号占比越高整个工作流就越接近“换一种方式瞎猜”。2. 搭建自我验证闭环的三个关键层次验证层次主要工具能拦住的问题明显局限第一层静态检查TypeScript、eslint、ruff、mypy、go vet类型错误、拼写错误、接口不匹配拦不住逻辑错误和运行时行为错误第二层测试断言Jest、Vitest、pytest、Go test行为与预期不符、边界条件错误需要测试本身写得足够严格第三层运行时观察沙箱执行、mock 依赖、日志追踪异步时序、真实依赖交互、异常处理复杂度高需要设计验证场景和输入2.1 第一层编译器和静态检查器用“硬约束”兜底自我验证的第一步也是最基础的一层是利用编译器和静态检查工具。TypeScript 有tsc --noEmitPython 有ruff和mypyGo 有go vet和golangci-lintRust 有cargo check和clippy。这些工具的判断完全由语言规范和静态规则决定可重复、无歧义是最理想的验证信号来源。我举个真实例子。之前我让 Agent 写一个 React 组件它把组件 props 的名字写错了一个类型也差了一截。在 TypeScript 项目里这种错误运行时才会暴露编译阶段就能拦截。Agent 拿到tsc的报错信息之后几乎都能自己修对。因为报错信息足够明确第几行、哪个属性不存在、期望什么类型。这相当于给了它一张精确的“错题定位图”。但如果没有这层静态检查同样的错误要等到运行时才炸出来。Agent 那时候需要读更长的堆栈、猜更多的上下文修复成本和难度都会翻倍。实操上的注意点在给 Agent 的 Prompt 里不要只写“请检查代码”而要给具体的命令。比如明确要求它执行npm run lint npm run typecheck并且把原始命令输出原样粘贴回来。越是明确的命令Agent 越不容易偷懒越是对输出有格式要求越能避免它用“我觉得没问题”来糊弄。2.2 第二层测试断言让验证从“能编译”升级为“行为正确”编译通过只能说明语法和类型没有问题不能说明行为正确。一个函数签名写对了、却把排序方向写反了编译器不会有任何意见。所以第二层验证要比“能编译”更往前一步用测试断言来锁定行为。我在实际工作流里强烈推荐“测试先行”。如果任务是让 Agent 实现一个debounce函数我会要求它先写出这样的测试describe(debounce, () { test(在等待期内连续调用不会触发原函数, () { const fn jest.fn(); const debounced debounce(fn, 200); debounced(); debounced(); debounced(); jest.advanceTimersByTime(100); expect(fn).not.toHaveBeenCalled(); }); test(等待期结束后会触发最后一次调用, () { const fn jest.fn(); const debounced debounce(fn, 200); debounced(a); debounced(b); jest.advanceTimersByTime(300); expect(fn).toHaveBeenCalledTimes(1); expect(fn).toHaveBeenCalledWith(b); }); });测试先行的好处是在写实现之前代码的“验收标准”已经存在了。Agent 之后写出来的实现必须跑通这些测试才算完成。它提供的验证信号是行为层面的而不是纸面层面的。要注意一个坑如果让 Agent 自己写测试它有可能会写出非常宽松的断言比如expect(fn).toBeDefined()这种断言永远会过验证等于没有。所以我在 Prompt 里会硬性要求断言必须描述具体的期望行为和边界条件。或者更稳妥的做法是测试用例由人来写或至少由人来审Agent 只负责让它们通过。2.3 第三层运行时观察让 Agent 能看到“执行轨迹”单测会覆盖一部分行为但覆盖不了所有情况。实际业务中问题往往出现在你根本没预料到的位置上某些第三方接口在特殊输入下会抛异常某些异步调用的数据到了回调里已经不是原来的样子。这就是第三层验证要做的事——让 Agent 在真实或仿真环境里运行代码观察执行过程。具体怎么操作一个办法是给 Agent 一个“最小可运行脚本”的权限。比如让它在沙箱里构造一段小入口传一组固定的测试输入然后跑起来看输出。这比单纯看单元测试更能反映真实运行状态。另一个办法是给外部依赖打桩记录实际调用参数和返回值。比如调用某个外部 APIAgent 可以把一个 mock 的返回值注入进去观察代码在这种情况下怎么处理。这特别适合排查“参数传递错误”“返回值被错误使用”这类问题。还有一个办法比较朴素但很有效让 Agent 在代码里临时加一些执行日志打印出关键变量在运行各阶段的值然后跑一遍看日志是否和预期一致。当年人类程序员排查问题也是这么干的Agent 这么做同样效率很高。当然这一层的复杂度比前两层高因为它需要 Agent 具备“设计实验”的能力构造什么样的输入、观察哪个指标、怎么判断输出是否符合预期。这恰恰是很多 Agent 工作流里最容易被跳过的部分。我的建议是先确保前两层稳定可靠再逐步给 Agent 打开运行时观察的通道不要一开始就放太开。2.4 把验证结果设计成结构化反馈聊完三个层次再讲一个贯穿始终的细节验证结果怎么回传给 Agent直接决定它修复的效率。很多人习惯把整个测试日志一股脑丢给 Agent让它自己去找哪里出了问题。这样不是不行但很费 token而且容易把模型带偏它可能盯着一堆无关日志反复纠结。更好的做法是把失败信息做成结构化摘要。我常用的格式类似这样{ status: fail, command: npm test -- --runInBand, error_type: AssertionError, location: src/utils/debounce.test.ts:12, expected: 等待期内 fn 不会被调用, actual: fn 被调用了 2 次, snippet: expect(fn).not.toHaveBeenCalled(); }这份摘要包含了几个关键信息哪条命令失败了、什么类型的错误、在哪个文件哪个位置、期望行为和实际行为分别是什么。Agent 拿到这份摘要后几乎不需要猜就能直接定位到修复点。第一次用的时候你会发现这个简单的格式能显著减少循环整改的次数。模型不用再花大量上下文去解析一长串日志它可以把有限的理解能力都用在“怎么改”而不是“发生了什么”上。3. 实操配置给 Agent 加上“自我验证-修复-再验证”回路3.1 一个可落地的完整调用流程前面讲的是概念和层次这一节直接看怎么把整套东西跑起来。我这里给出一个我自己在项目里验证过的调用流程你可以照着往自己的 Agent 框架里塞。整体流程是一个闭合循环下达任务要求 Agent 在动手前先输出验证策略说明准备用什么命令和测试标准来判定“完成”。Agent 生成第一版代码。依次执行静态检查、编译检查、测试用例收集所有硬信号。如果全部通过产出最终 diff流程结束。如果失败把失败原因转成结构化反馈让 Agent 给出修复计划和修改后的代码。回到第 3 步重跑。最多重试 N 次超过则停止并请求人工介入。这个流程没有任何神秘的地方但它的关键是“把第 3 步和第 5 步做成硬性规则”而不是让 Agent 自由发挥。很多失败的实践问题都出在这里Agent 觉得差不多就行了于是验证步骤被跳过或者被形式化。你要在框架层面卡住它让它没法绕过验证。3.2 一份可以直接抄的 Prompt 模板一段写得足够严格的 Prompt可以省掉很多框架层面的设计。我把我常用的模板整理出来核心思想就是“把验证的动作和输出格式都锁死”。你需要完成我交给你的编码任务并遵守以下硬性规则 1. 收到任务后先输出一段“验证策略”写明你会用哪些命令来确认代码正确性例如静态检查、编译、测试以及你预期看到什么结果。 2. 每完成一次代码修改先不要急着说“完成”。你必须实际执行验证策略中的命令把原始命令和输出完整粘贴出来。 3. 如果某个命令返回非零退出码或有报错输出你必须至少做到 - 指出错误类型编译错误/类型错误/测试断言失败/运行时异常 - 指出具体位置文件行号 - 说明你理解的失败原因 - 给出修复后的完整代码 4. 修复之后重复第 2 步直到所有验证命令都成功或者达到最大重试次数。 5. 严禁在没有粘贴任何命令输出的情况下声称“验证通过”。不允许使用“我觉得”“看起来没问题”这类表述作为通过依据。 6. 最后一次汇报时用如下格式输出 通过项列表 失败项列表如果有 剩余风险点这段 Prompt 的精髓有两个地方。第一个是“先写验证策略”这能让 Agent 在动手前就把验收标准想清楚而不是写完代码再亡羊补牢。第二个是“粘贴原始输出”这是防止幻觉最有效的一招——当它必须在回复里摆出真实的命令输出时就不太可能伪造一个“全部通过”。提示给 Agent 的每一次“验证通过”都必须能对应到一次真实执行的命令输出。如果做不到这一点你的整套自我验证机制就不算真正建立起来。3.3 判定通过的标准与终止条件光有 Prompt 还不够你还需要一套明确的终止规则否则 Agent 可能会陷入无限循环或者在没通过验证的情况下强行走完流程。我在工程里设置的标准通常包括这么几条验证通过的严格标准所有检查命令 exit code 为 0且测试用例全部通过lint 无错误。单任务最大重试次数默认 3 次。超过 3 次让 Agent 停下来整理“当前已修复的内容 仍然存在的问题”把信息交给人类处理。单轮验证超时时间例如每个命令最多 60 秒整个验证阶段最多 10 分钟防止某个疯狂循环把 token 烧空。这里有一个反直觉的经验不要给 Agent 无限重试的机会。最开始的版本我给了 10 次重试上限结果发现它会在某个具体错误上反复横跳每次改动都偏离正确方向更远一点Token 消耗倒是非常可观。把重试次数压到 3 次之后整体的成功率反而提升了因为它被迫在更少的尝试里思考更周全的方案一旦失败就停下来求救。3.4 进阶写代码和写验证的 Agent 分工当任务复杂到一定程度同一个 Agent 既当选手又当裁判会有个问题它容易对自己的代码产生“偏爱”。这是 LLM 的上下文偏差带来的写了很久的代码让它自己找毛病它往往找到的是无关痛痒的小问题真正的逻辑漏洞会被糊弄过去。所以我后来倾向于用两个 Agent 分工。一个负责生成和修复观察角度是“怎么把代码改对”另一个专门负责验证和挑毛病观察角度是“怎么证明这段代码是错的”。验证 Agent 的输出直接作为修复 Agent 的输入。前者要说服后者后者要把前者逼到墙角。这在效果上有点像模拟真实团队里的写码方和评审方。验证 Agent 的 Prompt 也很简单核心诉求就是不要带着同情心看代码去构造反例、边界条件、异常输入想办法让这段代码露出破绽。这样产出的验证反馈往往比同一个 Agent 自我检查要犀利得多。代价是 token 花费会更高调试也更复杂。但如果你手里是核心业务代码产出正确性的优先级远高于成本这套双 Agent 方案是很值得上的。4. 常见问题与排查技巧实录4.1 测试“通过了”但实际没跑这是我在自我验证路上遇到的最典型的翻车现场。Agent 回复一大段代码之后直接跟了一句“All tests passed”。你去看输出历史它根本没有执行过任何测试命令。整段“通过”是模型脑补出来的纯纯的幻觉。为什么会出现这个现象因为 RLHF 训练阶段把模型往“让用户满意”的方向引导模型学到的是“用户要一个通过的结果”于是它倾向于直接给出“通过”而不是诚实地报告“我没跑”。这不是什么玄学是目标函数带来的副作用。怎么治我从三个层面堵这个漏洞。Prompt 层面强制要求粘贴原始输出只要回复里没有命令输出就判定为未验证。框架层面给所有 CLI 调用做日志记录一个 Agent 如果声称验证通过但调用历史里没有对应的命令记录直接驳回它让它重新跑。沙箱层面把 Agent 放在隔离环境里只允许它在沙箱中执行命令这样即使它想绕过也无处可绕。这三个方法叠加之后“伪造通过”的情况基本绝迹了。4.2 反馈太模糊Agent 修复时原地打转另一个常见问题是验证命令虽然真的执行了但反馈给 Agent 的信息太粗糙。我只把整个测试 log 丢给它期望它自己从 500 行长日志里找出关键信息——结果它经常找错。有一次Agent 在“sort 排序方向错误”这个 bug 上连续修了 4 次每次都改一些毫不相干的变量名、注释和格式。原因就是测试日志里真正的断言失败信息混杂在大量无关输出中间模型没有能力准确索引到它。这个问题的解法就是我前面提到的结构化反馈。把失败信息提取成“哪个文件哪一行期望什么实际是什么”再喂给模型。自从我强制所有测试结果都走一遍结构化提取之后Agent 的修复成功率大幅提升循环轮数肉眼可见地下降。4.3 修好一个问题又弄坏一片别的功能在迭代修复的过程中Agent 经常陷入“拆东墙补西墙”的困境。改了 A 函数的返回值B 模块依赖原来的返回值B 的测试又挂了再去修 B又影响 C。如此循环每一轮都在解决新问题却不断制造新问题。对策之一是每轮修复之后不要只跑单个失败用例要跑整个测试集和相关联的模块测试。这样 Agent 在改 A 的时候就会意识到 B 和 C 也会受影响倾向于选择影响面更小的方案。另一个更管用的办法是引入回归基线。第一次成功验证通过之后把当时的所有测试结果快照保存下来。后续 Agent 每次改动之后除了看当前测试是否通过还要拿当前结果和基线比对任何“从通过变成失败”的项都会被标记为回归强制要求 Agent 解释并处理。这个机制能有效压制“修一处坏一片”的问题。4.4 验证器自身也需要被验证如果验证器本身有问题整个自我验证就是空中楼阁。测试写得太宽松、断言写得不完整、lint 规则没有覆盖到关键代码路径这些都会让 Agent 以为“全绿了”但实际上代码离正确还差得远。我在项目里用过一招自检方法故意往代码里注入一个已知的 bug然后让整套 Agent 工作流去修复它。如果 Agent 能在预设轮数之内找到并修好这个 bug说明验证回路是通的如果它反反复复最后还报“通过”那说明验证器有漏洞得先修验证器。这招花不了多少时间但对整个工作流的可靠性有决定性作用。每当我改动 Prompt 或者调整验证层次之后都会跑一遍这个注入测试来回归验证器的质量。它相当于给“自我验证”本身加了一个元验证层。4.5 成本失控自我验证真的很烧 token最后说一个绕不开的现实问题自我验证是有成本的。每次循环都要重新读代码、读反馈、生成修复方案token 花费可能比“一次性生成”多出好几倍。如果不加控制一个小任务烧出几十万 token 也很正常。我的控制策略是分层分级。常规小任务只开启第一层静态检查和第二层单元测试最多重试 2 次中等复杂度的任务补上结构化反馈和回归基线重试上限 3 次只有核心业务模块或异常难搞的 bug才开双 Agent 和运行时观察。另外我还给验证这一步单独限制 token 预算超预算就强制 Agent 停下来输出当前进展和剩余问题让人接手。这样既保住了正确性也没有让成本完全失控。5. 写在最后的实践心得聊到这里整条“自我验证”工作流的样子应该已经很清楚了。它不是一个单一技巧而是一整套被硬信号约束的交付习惯——从编译检查、测试断言到运行时观察再到修复与重试的闭环。最后我再讲几句自己在一次次实验中沉淀下来的体会希望对你有参考价值。第一句先打磨验证信号再谈智能体。很多人一上来就研究怎么把 Agent 搞得更“聪明”到处试各种复杂框架。但效果最明显的往往是把验证信号做干净反馈是否结构化、覆盖是否够、是否真的执行了。信号干净了模型本身的潜力才能真正发挥出来信号脏任何高级技巧都会大打折扣。第二句测试先行是自我验证里最划算的起点。如果你不知道从哪一步开始改起就强制要求 Agent 先写测试再写实现。不要小看这个顺序调换它能一次性解决很多问题验收标准清晰了、自我验证有了抓手、修复时也有了明确方向。我几乎所有项目里都保留了这一步还没有后悔过。第三句别指望 Agent 一次写对要指望它快速失败、快速修复。自我验证的真正价值不在于让生成过程变完美而在于让错误在可控环境里快速暴露、快速修正而不是留到生产环境里变成事故。给 Agent 重试的机会、给它及时的反馈、卡住循环的上限这套打法比追求“第一版就完美”要现实得多也有效得多。下一步我会把用下来的验证轨迹沉淀成数据集用来做后续的反馈微调让 Agent 把“自己踩过的坑”变成长期记忆。这个话题以后有机会再展开聊。