🐎 第10章 炉火纯青:工程化实战
📄 一句话概括

三层证据:最终回答、Hook 日志、完整流。能回答的问题数差得很远。 实测一次走偏的会话:最终回答答不出三个问题里的任何一个,Hook 日志答一个,完整流三个全答。 而走偏那次调用是「成功」的。

📝 本章和前面几章的关系

上一节算的是钱,这一节算的是「出事之后能不能查清」。 这一节有真跑:一段十三步的会话记录,分三层提取,看每层能回答几个问题。

一、三层证据各记什么

最终回答:只留最后那句话。它是给用户看的,不是给别人查的。 回调日志:每次工具调用的名字、参数、成败、耗时。在工具执行完之后挂一个脚本写一行 JSON 就行。 完整流:连思考文本和中间回复都留。这个要开流式输出,逐条落盘。

三层是「越往下越贵、越往下越准」。日志几乎免费,完整流要落全量数据。 所以选哪层不是看「哪个好」,是看「这次要回答什么问题」。

二、真跑:三个问题,三层各答几个

造的这段会话七秒多,任务是「把日期格式化统一成 ISO」。它最后说「改好了」。 但它其实改了两次:第一次失败(找不到要替换的原文),第二次改的却是 backup_utils.py。 三个问题:一共几次工具调用、哪次失败、为什么比预期慢。

最终回答:零个。它只说「改好了」,连调用了几次都看不出来。 回调日志:一个。六次调用、一次失败,看得出「有一次没成」。但为什么慢,看不出来。 完整流:三个全答。它跑去读备份文件、把备份也改了——这次调用是「成功」的。

这正是最要命的地方:只看成败的日志,会把一次走偏算成一次正常操作。

三、为什么回调日志最划算

它不需要改任何代码:挂一个脚本,把标准输入里的 JSON 追加一行就完事。 它跨会话保留。完整流一般只留最近几轮,日志可以留几个月。 它体积小:一条记录一百来字节,一次任务几十条也就几 K。

它的边界也很清楚:只知道工具层发生了什么,不知道它当时在想什么。 所以它是「第一层筛子」:先用它定位到某一次调用,再去翻那一轮的完整流。

四、诊断顺序:从外往里

第一步看最后一步:它以为自己做完了吗?说「改好了」的时候,它检查过吗? 第二步看工具层:有没有失败、有没有重复、有没有改了不该改的地方。 第三步才看思考层:它的假设是什么,这个假设从第几步开始不成立的。

顺序不能倒。先翻思考文本很容易被它的措辞带跑——它写得很自信,读起来就像是对的。 而工具层是硬事实:成功了还是失败了、改了哪个文件、花了几毫秒,没有解释空间。

五、会话存档的价值

存下来才能事后复盘。不存就只能重跑一遍,而重跑未必复现——模型有随机性。 尤其是「偶尔才出一次」的问题,不存档基本查不了。 存档的成本很低:一次任务一份 JSONL,压缩后几 K 到几十 K。

所以调试能力的上限,其实不取决于你多会分析,取决于你平时记了多少。

六、动手 Demo:一次走偏的会话,三层证据各能看出多少

十三步的会话记录,按「最终回答 / 回调日志 / 完整流」三层分别提取,回答同样三个问题。 注意第二次改动是「成功」的——这是这节的关眼。

tools = [e for e in LOG if e[u"kind"] == u"tool"]
fails = [e for e in tools if not e[u"ok"]]
stray = [e for e in tools
         if e[u"ok"] and u"backup" in e[u"arg"] and e[u"name"] == u"Edit"]

实跑输出:

--------------------------------------------------------------------
一次走偏的会话,三层证据各能看出多少
--------------------------------------------------------------------

  这段会话一共七秒多,最后一句是「改好了」。但它其实改了两次文件,其中一次改错了地方。
  同一个故障,三种证据层能看出多少,差别很大。

  ① 三层证据分别记什么
  --------------------------------------------------------------------
| 层 | 记了什么 | 怎么拿到 |
|---|---|---|
| 最终回答 | 只留最后那句话 | 给用户看的,不是给别人查的 |
| Hook 日志 | 每次工具调用的名字、参数、成败、耗时 | PostToolUse 里写一行 JSON 就行 |
| 完整流 | 连思考文本和中间回复都留 | stream-json 逐条落盘 |


  ② 真跑:三个问题,三层各答几个
  --------------------------------------------------------------------
| 证据层 | 几次调用 | 哪次失败 | 为什么慢 | 答对 |
|---|:---:|:---:|:---:|---|
| 最终回答 | 看不出 | 看不出 | 看不出 | 0 / 3 |
| Hook 日志 | 6 次 | 1 次(utils.py) | 看不出 | 1 / 3 |
| 完整流 | 6 次 | 1 次 + 原因 | 改了 backup_utils.py | 3 / 3 |

  真跑的数:6 次工具调用,1 次失败,1 次「成功但改错了地方」,总共 7.6 秒。
  第三个问题——为什么慢——只有完整流答得出:它跑去读 backup_utils.py,把备份也改了。
  而这次调用是「成功」的,所以只看成败的日志会把它算成一次正常操作。

  ③ 为什么 Hook 日志最划算
  --------------------------------------------------------------------
  它不需要改任何代码:在 PostToolUse 里挂一个脚本,把 stdin 的 JSON 追加到一行。
  它跨会话保留,而且比 stream-json 小得多——一条记录一百来字节。
  代价是它只知道「工具层发生了什么」,不知道「它当时在想什么」。

  [Hook 日志 · 本沙箱不执行]
  | {"ts":"...","tool":"Edit","arg":"backup_utils.py","ok":true,"ms":100}
  | {"ts":"...","tool":"Edit","arg":"utils.py","ok":false,"ms":95}
  两条记录就能看出「先失败、后成功」,但看不出中间那步走偏——参数在,意图不在。


  ④ 诊断的顺序:从外往里
  --------------------------------------------------------------------
  - 先看最后一步:它以为自己做完了吗?说「改好了」的时候,它检查过吗?
  - 再看工具层:有没有失败、有没有重复、有没有改了不该改的地方。
  - 最后看思考层:它的假设是什么,这个假设从第几步开始不成立的。
  顺序不能倒。先翻思考文本容易被它的措辞带跑,而工具层是硬事实。

  ⑤ 会话存档的价值
  --------------------------------------------------------------------
  存下来才能事后复盘;不存就只能重跑一遍——而重跑未必复现,因为模型有随机性。
  所以调试能力的上限,其实取决于你平时记了多少。
  ! 这段会话是为演示构造的,不在真机上跑;时间、耗时都是设定的数。

①三层:最终回答(只留最后一句)、回调日志(工具名、参数、成败、耗时)、完整流(连思考文本都留)。 越往下越贵,也越准。 ②三个问题:最终回答答 0 个、日志答 1 个、完整流答 3 个。 六次工具调用、一次失败、一次「成功但改错了地方」。 ③最要命的是走偏那次调用是「成功」的——只看成败会把走偏算成正常操作。 ④诊断从外往里:先看最后一步、再看工具层、最后才看思考层。先翻思考文本容易被它的措辞带跑。

30 秒实验

  1. 把第二次失败改成「失败但参数正确」,重跑一遍。日志那一层的「答对」会变成几个?
  2. 给日志加一个「本轮前缀长度」字段,重跑一遍。哪个问题现在能答了?
  3. 把会话里的「改好了」换成「改好了,我检查过 utils.py」,重跑一遍。最终回答那一层能多答一个吗?

常见误区

  1. 只看最终回答就判断成败。它说「改好了」,可能改错了文件。
  2. 只看成败不看参数。成功的调用也可能改错地方,这类最容易被漏。
  3. 先翻思考文本。它写得越自信,越容易把人带偏。
  4. 不落日志。出了问题只能重跑,而重跑未必复现。
  5. 把完整流当默认。数据量太大,留不住,而且大部分时候用不上。
自测三问(点开对答案) 1. 三层证据分别是什么? —— 最终回答、回调日志、完整流。越往下越贵,也越能回答「为什么」。 2. 为什么说「走偏的那次调用最危险」? —— 它是成功的,所以只看成败的日志会把它算成正常操作,问题被藏起来。 3. 诊断为什么要从外往里? —— 工具层是硬事实,思考层容易被措辞带跑。先用硬事实定位,再看意图。

能查清之后,下一节讲怎么让它从一开始就不越界:六条安全准则。

相关笔记