三层证据:最终回答、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 秒实验
- 把第二次失败改成「失败但参数正确」,重跑一遍。日志那一层的「答对」会变成几个?
- 给日志加一个「本轮前缀长度」字段,重跑一遍。哪个问题现在能答了?
- 把会话里的「改好了」换成「改好了,我检查过 utils.py」,重跑一遍。最终回答那一层能多答一个吗?
常见误区
- 只看最终回答就判断成败。它说「改好了」,可能改错了文件。
- 只看成败不看参数。成功的调用也可能改错地方,这类最容易被漏。
- 先翻思考文本。它写得越自信,越容易把人带偏。
- 不落日志。出了问题只能重跑,而重跑未必复现。
- 把完整流当默认。数据量太大,留不住,而且大部分时候用不上。
自测三问(点开对答案)
1. 三层证据分别是什么? —— 最终回答、回调日志、完整流。越往下越贵,也越能回答「为什么」。 2. 为什么说「走偏的那次调用最危险」? —— 它是成功的,所以只看成败的日志会把它算成正常操作,问题被藏起来。 3. 诊断为什么要从外往里? —— 工具层是硬事实,思考层容易被措辞带跑。先用硬事实定位,再看意图。能查清之后,下一节讲怎么让它从一开始就不越界:六条安全准则。
- ☐ (本篇完,下一篇 10.3 安全准则:最小权限是底线)