agent 自查了一轮还是错:自省环节什么时候才真能提高正确率

2026-07-29

数据截至 2026-07,文中命令以你本机 git 版本的 --help 为准,各家模型服务的可用区域与能力以官方最新说明为准。

多数人把”自省没用”归到模型不够聪明头上,我的判断是:八成情况下,是自省这一步压根没拿到任何新信息。 同一个模型、同一段上下文、同一套隐含假设,让它把刚写完的东西再读一遍,它做的事情不是检查,是复述——把原来的推理链条重新走一遍,然后补一句”已确认逻辑正确”。原来错在哪,现在还错在哪,只是多了一层自信的措辞。这不是能力问题,是信息论问题:检查者和被检查者共享全部先验,检查就退化成了同义改写。

站内另外两篇可以配合着看:想知道怎么给 agent 建一套任务集、量化它在一批场景下的通过率,看 agent 效果评测方法;想搞清模型凭空编出不存在的接口、参数、文件时怎么识别和拦截,看 大模型幻觉。本篇只管中间那个窄口子:自省这一步本身,在什么条件下能真的把正确率往上抬,什么条件下纯属多烧一轮

一、先分清三类自省,它们的上限完全不同

排查之前先归类,不然后面所有动作都会用错地方。我把实践里见到的自省环节分成三类。

第一类,复读式自查。 在同一轮对话里追问”你确认这段代码没问题吗”,或者在指令末尾写一句”完成后请自行检查”。没有新输入,没有新视角,模型看到的还是自己刚生成的 token。这类自省能捞回来的,只有那种”生成时手滑”的表层错误——变量名前后不一致、漏了个 import、Markdown 表格列数对不上。凡是需要外部知识才能判定的错(这个 API 到底存不存在、这个字段业务上该不该为空),它捞不出来。

第二类,带外部信号的自查。 检查这一步能读到编译器输出、类型检查结果、测试跑完的 stdout、lint 报告、git diff 的实际改动范围、运行时日志。这时候检查者手上多了一份被检查者没有的证据,自省才第一次具备”发现新事实”的能力。这类的效果是三类里最实在的,因为信号来自程序而不是来自另一段生成。

第三类,带独立视角的自查。 换一条干净的上下文,只把产物(代码、文档、结论)和原始需求交给它,不给推理过程。或者换一个模型来看。它没有前一轮的沉没成本,也没有那条把自己说服了的推理链。这类能捞出”方向性错误”——需求理解偏了、选了个根本不该用的方案、把一个只该改三行的任务扩散成了改二十个文件。

顺序上有个规律:一二三类的成本递增,但能发现的错误层级也递增。真正的浪费不是”用了自省”,是用第一类的成本结构去期待第三类的效果

二、判别表:先定位自省失效在哪一环

现象拿到手先别改,对着下面这张表判一下。

现象大概率成因怎么验证处置动作
每次自查都回”检查通过”,但实际有错复读式自查,检查者无新信息故意在产物里埋一个明显错误(改个变量名、删一行 return),跑同一套自省流程,看它能否报出来把外部信号接进去:编译、类型检查、测试输出至少接一项
自查能发现小错(拼写、格式),发现不了逻辑错检查判据太笼统,只有”是否正确”这种空判据看自省环节的输出,如果通篇是”整体结构清晰、实现合理”这类评价词,就是判据缺失换成可判定的检查项清单,每条都要能回答是/否
自查报了一堆问题,但大半是误报检查范围没限定,把无关文件和历史代码也当成本次产物在挑对比自省提到的文件列表和 git status --porcelain -uall 的结果(别只用 git diff,它看不见新建文件)用 diff 圈定范围,只让它检查本次改动
第一轮自查有效,之后越查越离谱上下文里堆了太多前几轮的检查记录,互相污染观察后几轮的意见是否在反复推翻前几轮的结论开新上下文重新查,只带需求和产物
自查通过、测试也绿,线上还是崩测试本身是假的(断言恒真、mock 掉了被测逻辑)把被测函数体整个换掉——第一行就抛异常,或直接返回一个明显的错值——再看测试是否还绿先修测试,见 测试假通过,再谈自省
自查发现问题后自己修,修完又引入新问题自省和修复混在一步,没有回归验证看每轮修完是否重跑了完整验证把”发现”和”修复”拆成两步,修复后必须重跑外部信号
自省环节把一个小改动扩散成大重构独立视角缺少改动范围约束统计前后 diff 行数在检查任务里显式写死”只判断是否满足需求,不提改进建议”

表里第一行的验证方法值得单独说一句:埋错测试是判断自省有没有真在工作的最快手段。你只需要一次,就能知道你那套自查流程是在检查,还是在盖章。

三、按顺序做的四个动作

判完成因,动作按下面顺序上,不要跳步。跳步的典型后果是——你先上了最贵的第三类自省,结果发现真正的问题在第二步就能解决。

动作一:接一路外部信号。 优先级排下来:能编译的先接编译,能跑类型检查的接类型检查,有测试的接测试,都没有的至少接 git diff --stat HEAD。信号的关键属性是它不由生成过程产生。一条命令就能拿到的东西,别指望模型脑补:

git diff --name-only HEAD    # 已跟踪文件的改动清单
git diff --stat HEAD         # 每个文件的增删行数
git status --porcelain -uall # 补上新建的、还没被跟踪的文件

第三条不能省。git diff 只看得见已被 git 跟踪的文件,agent 新建的文件在 git diff 里是彻底隐身的——你会拿到一份空的改动清单,然后信以为真。这一点值得你现在就在自己仓库里试一次:随手 touch 一个新文件,git diff --name-only HEAD 依然什么都不输出,而 git status --porcelain -uall 会以 ?? 开头列出它。把这三条的输出原样喂给检查环节,它对”这次到底改了什么”的认知才算从猜测变成事实。

顺带说明一下这几条的口径差异,免得你把范围圈错:带 HEAD 参数比的是工作区与最近一次提交,包含已暂存和未暂存的全部改动;不带参数的 git diff 只比工作区与暂存区,agent 如果顺手 git add 过,它的改动就从这个视图里消失了。检查环节要的是”本轮相对起点的全部变化”,所以显式写 HEAD 比省略更安全。如果这一轮之前你已经有若干提交,起点应该换成那个提交号或分支名,而不是继续用 HEAD

动作二:把判据写成可判定的项。 “检查一下代码质量”是没法执行的,“检查以下五项,每项回答是或否,回答否时必须给出文件名和行号”才是。判据要满足两个条件:答案是二值的判定所需的证据在上下文里能找到。比如:

  • 本次改动是否只涉及需求里提到的模块?(对照 diff 文件列表)
  • 新增的每个外部依赖是否在依赖清单里已声明?
  • 是否有函数在异常路径上没有返回值?
  • 是否存在被注释掉但没删除的旧实现?
  • 错误处理里是否把原始异常信息吞掉了?

清单的长度要克制。项数一多,模型会开始平均分配注意力,每项都草草回一句”是”。宁可分两轮,每轮五项以内。

动作三:给独立视角准备干净输入。 这一步的核心不是”换个模型”,是切断推理链的传染。做法是新开一条上下文,输入只包含三样:原始需求、最终产物、外部信号输出。不要把上一轮的推理过程、纠结、试错记录带进去。带进去它就会被说服,然后你又回到了第一类自省。

需要提一句:如果你打算用另一家的模型来做这个交叉检查,海外模型的官方服务对中国大陆有区域限制、不支持直连,市面上确实存在第三方中转,但我不背书任何具体渠道,稳定性和数据合规都得你自己评估。工程上更稳的做法是在同一供应商内换个型号,或者干脆靠外部信号而不是靠第二个模型。

动作四:把有效的检查沉成硬门禁。 一条检查项如果连续几次都能捞到真问题,就别让它继续以”自觉执行”的形式存在了。写成脚本、挂进 pre-commit、放进 CI。自省能查的东西,程序能查就交给程序——程序不会累、不会说好话、不会因为上下文长了就忘。剩下给模型自省的,应该只有程序判不了的那部分:需求理解对不对、方案选型合不合适、边界条件想没想全。

四、什么情况下别再折腾

自省这件事有很明确的收益衰减点,踩到下面任一条,就该停手换路。

止损点一:同一个 bug,自省已经”修好”三次以上。 这说明它在自己的认知里已经闭上了循环——每次都能自圆其说,每次都不对。继续加自省只会加深这个循环。这时候正确动作是人接管,或者带着完整报错重开一条上下文,AI 修不好的思维循环 讲的就是这个场景。

止损点二:自省的开销超过重做的开销。 一个改动本身只值二十行代码,你为它套了三轮检查加两轮返工,那就直接自己写。判断依据很朴素:如果你已经把问题描述清楚到能写进检查项的程度,你多半也已经知道答案了。

止损点三:检查项开始互相矛盾。 一轮说该抽象,一轮说过度设计;一轮说加错误处理,一轮说吞异常。这是判据本身没定义清楚的信号,不是模型的问题。停下来把标准定死,再重启流程。

回滚点:自省触发的修改让原本能跑的东西跑不了了。 这条要果断。前提是进入任何一轮”自查加自动修复”之前,先把锚点打好——要么工作区是干净的,要么先落一个可回退的提交:

git add -A && git commit -m "自省前锚点"   # 锚点:之后随时可以回到这里

出事之后退回去,按你想不想留住这批改动二选一,别两条都敲:

# 方案 A:改动还想留着日后翻,先收进 stash(工作区随即回到锚点状态)
git stash push -u -m "自省产出,待评估"

# 方案 B:确定不要了,直接丢弃
git checkout -- .   # 只还原已跟踪文件的修改
git clean -fd       # 再删掉新建的文件和目录,危险操作,先用 -n 预览

两个方案的常见误用是连着敲:git stash 已经把改动收走、工作区已经干净,后面那句 git checkout -- . 无事可做,纯属心理安慰。另外注意方案 A 的 -u——不加它,agent 新建的未跟踪文件不会被 stash 收走,会原地留在工作区,你以为回滚干净了其实没有。方案 B 的 git clean -fd 会真删文件且不进回收站,先加 -n 看一眼要删什么再执行。没有回滚点的自省是在赌博。

换条路的判断依据: 如果外部信号接不上——项目编译不了、没有测试、跑不起来——那自省的上限就锁死在第一类,无论你怎么调都不会好。这种情况下的正确投资是先把验证链路建起来(哪怕只是一个能跑通的冒烟脚本),而不是继续优化检查话术。验证能力决定自省能力,反过来不成立。

五、避坑清单

坑一:在同一轮对话里追加”请检查”。 为什么会踩——这是最省事的写法,看起来也确实有回应。怎么避——检查必须发生在能读到外部信号之后,至少要等编译或测试跑完。如果流程上做不到,就承认这一步只能捞表层错误,别把它当质量门。

坑二:让自省环节同时负责发现和修复。 为什么会踩——合成一步看起来效率高。怎么避——分开。合成一步时,模型有动机把问题描述得刚好等于它想做的修改,于是”发现”变成了”为修改找理由”。拆开之后,发现环节的产出是一个问题清单,修复环节拿清单干活,中间你有机会否掉。

坑三:把评价性语言当成检查结果。 为什么会踩——“整体实现合理、结构清晰”读起来像通过了。怎么避——规定输出格式为逐项的是/否加证据位置,凡是没有文件名行号的判断一律视为未检查。

坑四:检查清单一路加长。 为什么会踩——每发现一次漏检就往清单里加一条,半年后清单三十条。怎么避——定期清理,把能程序化的挪进 CI,把连续多次没捞到东西的删掉。清单是有维护成本的资产,不是垃圾桶。

坑五:自省结果没有留痕。 为什么会踩——查完就过去了,下次同样的错再犯一遍。怎么避——把每次真捞到问题的检查项记下来,这就是你项目最真实的错误分布。哪一类错误反复出现,说明的是需求描述方式或者工程约束有系统性缺口,不是运气不好。

坑六:用自省替代重试策略。 为什么会踩——两者都在处理”第一次没做对”。怎么避——分清楚:任务失败该走重试,agent 失败重试 讲的是那套机制;任务表面完成但结果不对,才是自省的战场。用错了就会出现无限重跑同一个必然失败的动作。

坑七:给检查环节塞完整历史。 为什么会踩——觉得信息越全越好。怎么避——检查者要的是需求和产物,不是过程。历史越全,它越容易复述被检查者的推理,独立性就没了。上下文只保留需求、产物、外部信号三块,其余一概不带。

六、收束

一句话概括本篇的判断:自省的价值不来自”再看一遍”这个动作,来自检查者手上比生成者多出来的那份信息。 多出来的可能是编译器的输出,可能是 diff 的事实,可能是一条没被推理链污染的干净视角。没有这份增量,加多少轮都只是把同一个错误说得更漂亮。

下次准备给流程加自省时,先过一遍这五条:

  1. 检查这一步能读到什么生成时读不到的东西?说不出来就先别加。
  2. 判据能不能用是/否回答,答”否”时能不能指到具体位置?
  3. 检查范围是不是用 diff 圈死了,不会去挑历史代码的刺?
  4. 发现和修复拆开了吗?修完有没有重跑外部信号?
  5. 有没有回滚点,止损条件写清楚了吗(比如同一问题修三次不成就人接管)?

五条里有两条答不上来,这个自省环节大概率是在消耗预算而不是在提高正确率。先去把验证链路补上,那笔投入的回报比调检查话术高得多。

想系统学会用 AI?报名体系课或加入会员,照着学、照着用。