
1. 问题的根源为什么“自己审自己”这件事天然不靠谱1.1 一个几乎所有人都踩过的坑先说一个我亲身经历的场景。之前带过一个小项目用 AI agent 帮忙写了一个数据清洗模块逻辑大概是根据几个字段的优先级做去重和合并。代码跑通了测试用例也过了我图省事直接让同一个 agent 再“审一遍”自己的代码。它给出的结论是“逻辑清晰边界处理完善无明显问题。”我当时还挺满意结果上线第二天就出了事故——当两个关键字段同时为空时去重逻辑会把两条本不该合并的记录合成一条导致下游统计直接偏了将近百分之十五。回头再看那段代码问题其实非常明显if a or b写成了if a and b的等价逻辑而 agent 在“审计”时脑子里默认的还是它自己写代码时的那套假设所以它根本没觉得这里有问题。这件事让我彻底明白了一个道理让 AI agent 审计自己写的代码本质上等于让一个人给自己批改试卷而且这个人还坚信自己写的就是标准答案。这个标题问的是“为什么不应该”但我觉得更有价值的是搞清楚“为什么不行”以及“那该怎么办”。前者是认知后者是方法。只讲认知不讲方法等于只诊断不开药。1.2 核心矛盾生成与审计需要的是两种不同的思维模式要理解为什么自己审自己不行得先搞清楚“写代码”和“审代码”这两件事在认知层面上的根本差异。写代码的时候大脑无论碳基还是硅基处于一种建构模式目标是把一个模糊的需求翻译成确定的逻辑关注的是“怎么让它跑起来”“怎么覆盖主要路径”“怎么让结构看起来合理”。这个过程中会做大量假设比如“输入不会同时为空”“调用方会保证参数合法”“这个边界情况业务上不会出现”。这些假设在写的时候往往是无意识的甚至写的人自己都意识不到自己做了这些假设。审代码的时候需要的是解构模式目标是找出“这段逻辑在什么情况下会崩”“哪些假设可能不成立”“有没有遗漏的路径”。这要求审计者主动去质疑每一行代码的前提去构造那些写代码的人没想到的输入。问题在于同一个 agent 在完成生成任务后它的上下文里已经充满了“我为什么要这么写”的理由。这些理由会形成一种确认偏误——它会倾向于寻找支持自己写法的证据而不是寻找反驳自己的证据。这不是某个模型特有的缺陷而是所有基于上下文推理的系统都会面临的固有问题。人也是这样你刚写完一篇文章让你立刻挑自己的错别字和逻辑漏洞效果远不如放两天再回头看或者换个人来看。1.3 从信息论角度看审计需要的是“额外信息”还有一个更底层的解释。审计的价值在于发现生成过程中没有用到的信息。如果审计者和生成者拥有完全相同的信息集同一段需求、同一套上下文、同一个知识库那么审计能发现的问题上限就是生成时因为随机性而遗漏的问题。换句话说它只能发现“手滑”类的错误发现不了“认知盲区”类的错误。真正有价值的审计需要引入生成者没有的信息可能是不同的测试数据、不同的业务约束、不同的历史事故记录、不同的代码规范视角。当同一个 agent 既当运动员又当裁判时这些额外信息根本没有被引入审计就变成了走过场。我后来做过一个粗略的统计在同一个 agent 自审的情况下能发现的 bug 类型主要集中在语法错误、明显的变量名拼写错误、以及少数逻辑分支遗漏而对于边界条件、并发问题、资源泄漏、业务语义错误这几类自审的发现率低得可怜。换成另一个独立的 agent 来审后面几类的发现率能提升好几倍。这个对比很能说明问题。2. 自审失效的四种典型表现与背后的机制2.1 确认偏误它只看到了自己想看到的确认偏误在 AI agent 自审场景里表现得特别明显。举个例子agent 写了一个函数用来从一段文本里提取日期。它用的是正则表达式写的时候假设日期格式是“年-月-日”。审的时候它检查了正则有没有语法错误、有没有正确捕获分组、返回值类型对不对然后给出“通过”。但它不会去问“如果日期是‘月/日/年’格式呢”“如果文本里根本没有日期呢”“如果日期是‘2024年3月5日’这种中文格式呢”因为这些情况在它写代码时的“心理模型”里就不存在。审的时候它调用的还是同一个心理模型所以这些盲区依然是盲区。这就像一个人检查自己有没有带钥匙他摸的是自己以为放钥匙的那个口袋而不是把所有口袋都摸一遍。2.2 上下文污染生成时的推理链会干扰审计判断现在的 AI agent 在生成代码时通常会在上下文里保留大量的中间推理过程为什么选这个数据结构、为什么用这个算法、为什么加这个判断。这些推理链在生成阶段是有用的但在审计阶段就变成了噪音。我观察到一个很典型的现象当 agent 自审时它经常会引用自己生成时的理由来为代码辩护。比如它会说“这里加了非空判断因为前面已经保证了输入不为空”——但“前面已经保证”这个前提本身可能就不成立。它把生成时的假设当成了审计时的论据这就形成了一个闭环外面的事实进不来。要打破这个闭环一个简单粗暴但有效的办法是在审计时不要把生成阶段的推理过程放进上下文。只给审计者看最终的代码和需求描述让它从零开始判断。这个做法我试过很多次效果立竿见影。同一个 agent在不知道“自己为什么这么写”的情况下挑出来的问题明显更多、更尖锐。2.3 盲区共振同一个知识缺口会被继承两次每个 agent 都有自己的知识盲区。有些盲区是模型层面的比如对某个库的某个版本的行为理解有偏差有些是上下文层面的比如对某个业务规则的理解不完整。当同一个 agent 既生成又审计时它的知识盲区会被继承两次生成时因为盲区而写错审计时因为同一个盲区而看不出错。举个具体的例子。假设某个 agent 对 Python 中datetime模块的时区处理理解有偏差它写代码时可能把 naive datetime 和 aware datetime 混用。审的时候它依然认为这种混用是没问题的因为它的知识里就没有“这会导致比较时报错”这一条。换一个对时区处理更熟悉的 agent 来审一眼就能看出来。这种盲区共振是最难通过“让 agent 更仔细一点”来解决的因为问题不在态度在知识。唯一的办法是引入知识结构不同的审计者。2.4 激励错位它没有动力去否定自己这一点在基于强化学习或偏好优化的 agent 上尤其明显。这些 agent 在训练过程中被鼓励“给出有用的回答”“让用户满意”。当它面对自己的代码时“找出问题”和“维护自己之前的输出”之间是存在张力的。在很多情况下它会倾向于选择后者因为否定自己之前的输出在某种程度上等于承认自己之前做得不好。这不是说 agent 有意识地在“护短”而是它的输出分布会偏向于那些看起来更“和谐”的结论。你让它审自己的代码它给出“整体不错有几个小建议”的概率远高于给出“这段逻辑有严重缺陷建议重写”的概率。而后者往往才是真正有价值的结果。3. 那该怎么办四种可落地的替代方案3.1 方案一换一个独立的 agent 来审最直接的办法就是不要让同一个 agent 既写又审。你可以用同一个模型但开一个新的会话只给它代码和需求不给它生成时的上下文。更好的是用不同的模型比如一个模型写另一个模型审。不同模型的知识盲区不一样能互补。我自己的习惯是写代码用一个 agent审代码用另一个 agent而且审的那个 agent 的提示词要专门设计成“挑刺模式”。比如我会这样写提示词你是一个严格的代码审计员。你的任务是找出这段代码中所有可能的缺陷包括但不限于边界条件、异常处理、并发安全、资源泄漏、业务逻辑错误、性能问题。不要给出赞美不要总结优点只列问题。每个问题要给出具体的触发条件和可能的后果。这个提示词的关键在于“不要给出赞美”和“只列问题”。如果不加这两句agent 很容易花一半篇幅说“这段代码结构清晰、命名规范”然后轻描淡写地提两个无关痛痒的建议。加上之后它的输出会尖锐很多。3.2 方案二用测试用例代替“审阅”另一个思路是绕开“审阅”这个动作直接用测试来验证。让一个独立的 agent 根据需求描述生成测试用例尤其是边界用例和异常用例然后拿这些用例去跑代码。测试用例的好处是它是可执行的、客观的不依赖于审计者的主观判断。具体操作上我会让测试 agent 做这几件事根据需求列出所有输入字段的取值范围和约束对每个字段生成正常值、边界值、非法值三类测试数据构造字段之间的组合场景特别是那些“业务上不太可能但同时出现”的组合生成断言明确每个用例的预期输出然后把这些用例跑一遍看哪些失败。失败的用例就是实实在在的 bug不需要争论。这个方法的缺点是测试用例本身也可能有盲区但至少它把“发现 bug”这件事从主观判断变成了客观验证。3.3 方案三引入形式化检查工具对于关键模块光靠 agent 审是不够的还得上工具。静态分析工具能发现很多 agent 容易忽略的问题比如未处理的异常、可能的空指针、资源未释放、并发竞争条件等。类型检查工具能在编译期就拦住一批错误。这些工具的好处是它们是确定性的不会因为“心情好”就放过一个问题。我通常会在 agent 审完之后再跑一遍静态检查和类型检查。如果工具报出了问题而 agent 没报那说明 agent 的审计有遗漏我会把工具的输出再喂给 agent让它解释为什么没发现这个问题。这个过程本身也能帮助我了解 agent 的能力边界。3.4 方案四人工抽检关键路径不管 agent 多强关键路径上的人工抽检不能省。什么叫关键路径就是那些一旦出错会导致数据丢失、资金损失、安全漏洞、或者难以回滚的路径。这些地方的代码我会自己逐行看一遍不依赖 agent 的审计结论。人工抽检的时候我会重点看几个东西错误处理是不是完整、边界条件是不是覆盖、有没有硬编码的假设、日志是不是足够定位问题。这些地方往往是 agent 最容易想当然的地方。而且人工看的时候我会刻意去构造一些“刁钻”的输入看看代码会怎么反应。4. 一个完整的实操流程从生成到审计的闭环4.1 第一步生成阶段就为审计做准备很多人是在代码写完之后才想起来要审计这时候已经晚了。更好的做法是在生成阶段就为后续的审计铺路。具体来说我会要求生成 agent 在写代码的同时输出一份“假设清单”明确列出它在写这段代码时做了哪些假设。比如假设输入数据已经过清洗不会包含空值假设调用方会保证并发不超过 10假设数据库连接池的大小足够假设时区统一为 UTC这份清单在审计阶段非常有用。审计 agent 可以逐条检查这些假设是否成立而不是漫无目的地看代码。这相当于把“找假设”这个最难的部分提前完成了。4.2 第二步审计 agent 的提示词设计审计 agent 的提示词不能随便写。我试过很多版本最后固定下来一个结构包含四个部分第一部分是角色定义明确它是审计员而不是开发者。第二部分是审计范围列出要重点检查的维度。第三部分是输出格式要求每个问题都包含位置、触发条件、后果、严重程度。第四部分是禁止事项明确不要赞美、不要总结、不要给修改建议修改建议由另一个环节处理。这个提示词的关键在于把审计和修复分开。如果让审计 agent 同时给修改建议它会倾向于给出那些容易修改的问题而忽略那些需要大改的问题。分开之后审计 agent 可以毫无顾虑地报出所有问题。4.3 第三步交叉验证与去重如果条件允许我会用两个不同的 agent 分别审一遍然后对比它们的输出。两个 agent 都报的问题优先级最高只有一个报的问题需要人工判断是不是误报两个都没报但工具报了的说明 agent 有系统性盲区。这个交叉验证的过程还有一个好处它能帮我了解每个 agent 的“性格”。有的 agent 偏保守报的问题多但误报也多有的 agent 偏激进报的问题少但都是硬伤。了解这些之后我就能根据场景选择合适的审计者。4.4 第四步修复后的回归审计修完问题之后不能直接上线还要做一次回归审计。这次审计的重点不是找新问题而是确认修复没有引入新问题。很多 bug 是在修 bug 的过程中引入的尤其是当修复涉及逻辑调整时。回归审计的提示词和第一次审计不一样我会明确告诉审计 agent“这段代码刚刚经过了修改修改的目的是解决以下问题。请检查修改是否达到了目的以及是否引入了新的问题。”这样 agent 的注意力会集中在修改相关的区域效率更高。5. 常见问题与排查技巧实录5.1 审计 agent 说“没问题”但上线后出事了这是最常见也最让人头疼的情况。排查思路是这样的先看事故的触发条件是什么然后回头看审计 agent 有没有覆盖这个条件。如果没覆盖说明审计范围有遗漏如果覆盖了但没报说明 agent 的判断有偏差。我遇到过一次审计 agent 说“异常处理完善”但上线后因为一个网络超时导致整个流程卡死。回头看agent 确实检查了异常处理但它只检查了“有没有 try-catch”没有检查“catch 之后有没有正确的恢复逻辑”。这就是审计维度的粒度问题。解决办法是在提示词里把审计维度拆得更细比如把“异常处理”拆成“异常捕获”“异常分类”“恢复策略”“重试逻辑”“降级方案”五个子项。5.2 审计 agent 报了一堆问题但大部分是误报误报多了也很烦会浪费大量时间。减少误报的办法有几个一是给 agent 提供更完整的上下文比如相关的接口定义、数据模型、业务规则二是让 agent 在报问题之前先确认“这个问题在当前上下文中是否真的可能发生”三是用两个 agent 交叉验证只保留两个都报的问题。我自己的经验是误报率最高的往往是那些“理论上可能但实际不会发生”的问题。比如 agent 会说“如果传入 None 会报错”但实际上调用方永远不会传 None。要减少这类误报可以在提示词里加一句“只报告在当前系统约束下可能发生的问题对于理论上可能但实际不会发生的场景不要报告。”5.3 审计 agent 和生成 agent 互相“甩锅”有时候会出现这种情况生成 agent 说“我这么写是因为需求里说了 X”审计 agent 说“需求里没有说 X你理解错了”。这时候需要人工介入去核对原始需求。如果确实是需求理解有偏差那问题出在生成阶段需要重新生成如果需求本身有歧义那需要先澄清需求。为了避免这种扯皮我会在生成阶段就让 agent 把“需求理解”写出来明确列出它从需求中提取了哪些约束和假设。这样在审计阶段双方有一个共同的参照物减少理解偏差。5.4 审计通过后代码还是出了问题这种情况通常意味着审计的覆盖面不够。我会做一个“事故复盘”把出问题的场景补充到审计清单里下次审计时重点检查。同时我也会检查是不是有新的代码模式没有被审计覆盖到。比如如果引入了新的框架或库审计 agent 可能对它不够熟悉需要额外提供文档或示例。下面这张表是我整理的自审与独立审计的对比可以作为参考对比维度自审独立审计确认偏误严重较轻上下文污染严重无盲区共振存在可互补激励错位存在无发现边界问题能力弱强发现业务语义错误能力弱中误报率低因为报得少中适合场景快速检查语法关键模块审计6. 一些更深层的思考审计的本质是什么6.1 审计不是“再看一遍”而是“换一个角度看”很多人对审计的理解还停留在“再看一遍”的层面觉得只要足够仔细就能发现问题。但真正的审计是换一个角度看问题。同一个东西从生成者的角度看是合理的从使用者的角度看可能就有问题从维护者的角度看可能又有不同的问题。AI agent 自审的问题在于它没有真正“换角度”它只是把同样的角度又走了一遍。要让它换角度就得给它不同的角色设定、不同的关注点、不同的信息输入。这也是为什么我在审计提示词里会明确写“你是一个严格的审计员你的任务是挑刺”而不是简单地说“请检查这段代码”。6.2 审计的价值在于“意外”审计最大的价值不是确认“已知没问题”而是发现“未知的问题”。如果审计的结果完全在预期之内那这次审计的价值就很有限。好的审计应该能让你说出“啊我没想到这里还有这个问题”。从这个角度看自审几乎不可能产生“意外”因为生成者对自己的代码太熟悉了一切都在预期之内。只有引入外部视角才有可能产生意外。这也是为什么我坚持用不同的 agent 来审而且尽量用不同的模型。6.3 审计是一个系统工程不是一次动作最后想说的是审计不是一个孤立的动作而是一个系统工程。它需要生成阶段的配合提供假设清单、需要工具的支持静态检查、类型检查、需要流程的保障交叉验证、回归审计、需要人的判断关键路径抽检。把审计当成一个“让 agent 再看一遍”的动作注定效果有限。我在实际项目中的做法是把审计拆成多个环节每个环节用不同的方法和工具最后汇总结果。这样做虽然麻烦一点但能覆盖的盲区更全出事故的概率明显降低。毕竟代码出问题的代价远比多花点时间审计要大得多。6.4 一个实用的检查清单最后分享一个我常用的审计检查清单可以喂给审计 agent 作为提示词的一部分所有输入字段是否都做了合法性检查所有可能为空的字段是否都做了空值处理所有外部调用是否都有超时和重试机制所有异常是否都有明确的处理策略所有资源文件、连接、内存是否都有释放逻辑所有并发访问是否都有同步机制所有边界条件最大值、最小值、空集合、单元素集合是否都覆盖所有业务规则是否都有对应的测试用例所有日志是否包含足够的上下文信息所有硬编码的值是否都有合理的理由这个清单不是万能的但它能覆盖大部分常见问题。每次审计时我会让 agent 逐条检查并给出具体的检查结果。这样比漫无目的地看代码要高效得多。踩过几次坑之后我越来越觉得AI agent 是个好工具但它不是万能的。让它写代码可以让它审自己的代码还是算了吧。换个 agent换个角度多花不了多少时间但能省下的事故成本可能是几十倍甚至几百倍。