可观测性与调试:看清 Agent 的每一步 · 第 2 / 6 节

第 2 课:第一手证据:原始记录,而不是它的自述

学习目标:

  • 说清 Agent 的自述为什么只能算二手材料,并知道「它省略的比它写出来的更要紧」具体指向记录里的哪些位置
  • 认得出什么算原始记录:一次运行里每个工具调用的工具名与完整参数、每个工具响应的完整返回或报错,并能把一处可疑行为定位到「第几条消息的哪个字段」
  • 用四问(调错了工具、参数传错了、调得太少、把响应处理错了)把一段记录读成分类结论,并知道哪些问题人眼读得出、哪些该交给程序

前置要求:读完第 1 课,理解非确定性怎么让「复现再调试」失灵、一个症状底下压着几个不可区分的原因 | 上一课 第 1 课 << | 下一课 第 3 课 >>

它说它交叉核对了三个来源

一个调研 Agent 跑完,收尾时给你一段话:

我检索了三个权威来源并做了交叉核对,数据一致。

你信了。这句话读起来专业、有分寸,还主动交代了方法。你把结果发给了业务方。

三周后用户投诉:那个数字错得离谱。你翻开这次运行的原始记录一条条看下来——它只成功调了一次搜索;第二次调用是抓取官方页面,返回的是一行超时报错;第三次调用压根没发生过。「交叉核对」这四个字,只存在于它给你的那段总结里。

这不是模型在骗你——它写那段话时并没有「核对次数」可查,只是在续写一段听起来该有的收尾。代价却是实打实的:你信了自述、跳过了记录,花三周才知道这次运行从第二步就断了。

第 1 课的结论是:Agent 在两次运行之间是非确定性的,「复现一下再打断点」这套传统直觉直接失灵,出路是让运行过程自己留下证据。本系列第 10 门课讲评测时顺带说过一句「Agent 的自述不能当证据」,那是在裁判和打分的语境里一句带过,这一课把它展开成一套读法。

自述是二手材料

Agent 给你的自述——收尾总结、思维链(chain of thought,模型在给出答案前写出来的那段推理过程,下面简称 CoT)、工具执行完之后的自我评价——都属于它写的关于它自己的文字。这些文字有用,但它们和「它实际做了什么」之间隔着一层。

隔的这一层,最要命的地方不是它会写错,而是它会不写。Anthropic 的工具工程文章讲得很直白:Agent 在反馈与回复里省略的内容,往往比它写出来的更要紧;LLM 说出来的,未必就是它心里那回事1。省略比写错难对付——写错了你有机会撞见矛盾,省略了你连撞的机会都没有。上面那个例子里它没写「第二个来源抓取失败」,只是不提,而缺失的部分不会举手。

所以顺序是:读它的推理和反馈(也就是 CoT)能帮你找到毛糙的地方,但要抓出 CoT 里没有明说的行为,得去看原始记录,包括工具调用与工具响应1。CoT 是线索,记录是证据。两者颠倒,你就会反复得出「它说它做了,那大概是做了」这种结论。落到操作上:拿自述里的每个具体数字去记录里找出处,找不到出处、也从记录里的数字推不出来的,就是它自己生成的。

什么算「原始记录」

「原始记录」不是新东西,你在本系列第 7 门课已经亲手造过:那个 messages 数组。每轮循环往里追加一条 assistant 消息、再追加一条带工具结果的 user 消息,跑完一次任务,数组里沉淀下来的就是这次运行的原始记录。

摊开看就两类块。tool_use是模型点名要调哪个工具,里面有工具名和一份完整的参数对象,参数由模型自己生成。tool_result是宿主把工具真跑一遍之后回传的东西,成功是完整返回值,失败是报错内容加一个错误标记。两个块必须成对读:单看 tool_use 只知道它干什么,单看 tool_result 只知道环境了什么。

为什么它们够格当证据?Anthropic 关于 Agent 模式的文章讲的是 Agent 自己的视角:执行过程中,Agent 必须在每一步从环境获得 ground truth(环境给出的、不由模型自己编的事实——比如工具调用结果、代码执行结果)来评估自己的进展2。同一批东西换你来读,作用一样——它判断自己走到哪了靠这些,你判断它走到哪了也只能靠这些。

同一篇文章还给了一条设计原则:优先保证透明度,把 Agent 的规划步骤显式地展示出来2。这条常被当成 UI 建议,但它对调试的含义更实在——凡是没落到工具边界上的动作,都不会在记录里留下痕迹。模型「在脑子里比对了两份数据」不留痕,调一次 diff 工具就留痕。你想让哪件事可查,就得把它挤到工具边界上去。

人眼能看见评测看不见的东西

到这儿会有人问:本系列第 10 门课不是刚建好评测集吗?跑分不就完了,为什么还要人一条条翻记录?

因为评测集只能发现你已经想到的失败。人工测试能抓到评测漏掉的边缘情况——罕见查询上的幻觉、系统性的故障、以及微妙的来源选择偏差3。第三类最值得琢磨:Anthropic 的多 Agent 研究系统里,人工测试者注意到早期的 Agent 一贯偏爱经过 SEO 优化的内容农场,而放过学术 PDF、个人博客这类权威但排名没那么靠前的来源3

这个偏差在评测里几乎看不出来:答案不一定是错的,内容农场也会抄对事实,跑分一直挺好看。它从头到尾只写在一个地方——记录里那一串它真实挑中的 URL。

工具这一层也有同性质的例子。Claude 的网页搜索工具上线时,团队发现 Claude 会没必要地往查询参数里追加 2025,这让搜索结果产生偏斜、表现变差;修法是改进工具描述,把 Claude 引导回正轨1。这个 bug 不崩溃、不报错、不超时,工具每次都调用成功,返回也是合法的搜索结果。指标顶多告诉你「表现变差了」;要指名道姓说出「差在 query 里多了一个 2025」,只能靠逐条读参数这一层。

记录不只给人读

上一节容易读出一个误解:翻记录 = 人肉逐条看。不是——记录是结构化文本,模型很擅长读它。工具工程文章给了一个近乎粗暴的做法:把评测 Agent 的记录直接拼接起来,粘进 Claude Code——模型是分析记录的行家,还能顺手一次性重构一大批工具,比如保证工具实现与工具描述保持自洽1

「读记录」的瓶颈在耐心,不在智力。分工于是很清楚:模型适合覆盖式粗筛,把一批记录压成一份可疑清单;人适合定性,判断捞出来的样本算不算问题。

别人的记录格式是别人的内部实现

既然记录这么重要,直接读现成产品落在磁盘上的记录行不行?拿 Claude Code 举例,它会把会话转录持久化下来,落在 ~/.claude/projects/*/*.jsonl 这些 JSONL 文件里4。看上去很理想:一行一条消息,现成的、结构化的原始记录。

但官方文档紧跟着一条告诫:转录条目的格式是 Claude Code 的内部实现、会在版本之间发生变化,所以一个依赖这些字段做联接的管道可能在任何一次发版之后失效,这类联接应当按「版本特定」来对待,而不是当成稳定契约4。hooks 文档还有第二条:hook 拿到的那个转录文件是异步写入的,可能滞后于内存里的对话,所以 hook 触发时它还不一定包含当前这一轮最近的几条消息5

两条教的是同一个道理:别人的格式不归你管,写入时机也不归你管。你可以读它、可以用它排查,但不能把自己的观测建在上面。结论是:你自己的 harness,要自己落自己的记录。第 3 课讲怎么设计这份记录的字段,第 6 课把它写进你在本系列第 7 门课写的那个 harness 里。

读记录的四问

翻开一段记录,从哪儿开始看?工具工程文章给了一个现成的分类:Agent 可能调错了工具、可能调对了工具但传错了参数、可能调得太少、也可能把工具响应处理错了1。这四项本来是讲工具设计的,但当成读记录的检查清单极好用,因为它们在记录里各有各的指认位置:

一问:调错了工具。 看每个 tool_use 块的 name 字段,对照当时的任务和上一条工具响应,判据是「有没有一个更该用的工具摆在那儿没被用」。

二问:调对了工具,参数传错了。tool_use 块的 input 字段,一个键一个键地看。前面那个往查询词里塞 2025 的例子就住在这一问里。它最容易被跳过,因为参数又长又像模像样——专门盯那些你能独立判对错的键:日期、路径、ID、范围上下界、单位。

三问:调得太少。 这一问的证据是缺失,所以最难读。指认方式是找那些「本该有下一次调用却没有」的位置:报错之后没有重试、搜索返回五条却只抓了一条、任务要求两个来源而记录里只有一次成功往返。缺失不会自己跳出来,你得拿任务要求当模板去数。

四问:把工具响应处理错了。 看每条 tool_result 之后紧跟的那条 assistant 消息,问一句「它接下来说的话,和它刚拿到的东西对得上吗」。报错被当成成功、返回的是 A 期数据却被当作 B 期用、返回里明写着「结果被截断」而它照单全收——都在这一问下面。它还和报错信息的写法直接相关:工具抛错时,可以对错误响应做提示词工程,让它传达具体、可执行的改进建议,而不是甩出不透明的错误码或 traceback1。看到 Agent 反复误读同一类报错,先去看那条报错本身写了什么。

分寸:不是每条记录都要人逐字读

人读记录是的动作:一次运行几十条消息,认真读一遍十几分钟起步,跑一百次就没人读得动了。所以人读只用在两个地方。一是抽查:定期随机挑几次运行读一遍,不为找具体的 bug,是校准你对「它平常怎么干活」的直觉——SEO 内容农场那类偏差,正是「不为找 bug 而读」才撞得见的东西。二是给新问题定性:碰上没见过的症状,头一两次必须人读,因为你还不知道该找什么;等你读明白它在哪个字段上现形,就该交给程序了。

日常的量靠程序化的读法扛:日志字段怎么设计是第 3 课,怎么把散落的记录关联成一次运行是第 4 课。到那时人读的入口会变成「先用指标定位到可疑的几次运行,再摊开记录细看」。证据先要存在,然后才谈得上怎么高效地读——这一课管前半句。

💻 练习

小结

  • Agent 的自述是二手材料:它在反馈与回复里省略的内容往往比写出来的更要紧,LLM 说出来的未必是它心里那回事1,而缺失的部分不会在总结里举手
  • 读 CoT 能找到毛糙的地方,但要抓出 CoT 里没有明说的行为得看原始记录,包括工具调用与工具响应1——CoT 是线索,记录是证据
  • 原始记录就是本系列第 7 门课那个 messages 数组沉淀下来的东西。它够格当证据,是因为工具结果、代码执行是环境每一步给出的 ground truth2;官方还有一条「优先保证透明度、把规划步骤显式展示出来」2——反过来读就是本课的引申:凡是没落到工具边界上的动作都不会留痕
  • 人工测试能抓到评测漏掉的边缘情况:罕见查询上的幻觉、系统性故障、微妙的来源选择偏差3;真实案例是早期 Agent 一贯偏爱 SEO 内容农场、放过学术 PDF 与个人博客3——这类偏差落在记录里,就是那串它真实挑过的 URL。工具层的 bug 也住在参数里:Claude 曾往搜索工具的查询参数里没必要地追加 2025,让结果偏斜、表现变差,修法是改进工具描述1
  • 记录不必只由人读:把评测 Agent 的记录拼接起来粘进 Claude Code,模型很擅长分析记录,还能顺手保证工具实现与描述保持自洽1。读记录的四问是:调错了工具、调对了工具但传错了参数、调得太少、把工具响应处理错了1
  • 别人的记录格式是别人的内部实现。Claude Code 把会话转录落在 ~/.claude/projects/*/*.jsonl4,但官方明说这个格式是内部实现、版本之间会变,靠它做联接的管道可能在任何一次发版后失效,应按版本特定对待4;hook 拿到的转录文件还是异步写入的,可能滞后于内存里的对话5。所以你自己的 harness 要自己落自己的记录——第 3 课设计字段,第 6 课动手写

>> 第 3 课:结构化日志与指标:把每一步变成数据

Footnotes

  1. Writing effective tools for agents — with agents — Anthropic Engineering — https://www.anthropic.com/engineering/writing-tools-for-agents 2 3 4 5 6 7 8 9 10 11

  2. Building Effective AI Agents — Anthropic Engineering — https://www.anthropic.com/engineering/building-effective-agents 2 3 4

  3. How we built our multi-agent research system — Anthropic Engineering — https://www.anthropic.com/engineering/multi-agent-research-system 2 3 4

  4. Monitoring — Claude Code 官方文档 — https://code.claude.com/docs/en/monitoring-usage 2 3 4

  5. Hooks reference — Claude Code 官方文档 — https://code.claude.com/docs/en/hooks 2

练习

01

下面是一次运行的原始记录,共 8 条消息。任务是查一家公司某个季度的分部营收,用两个独立来源交叉核对。工具三个:web_search、fetch_page(抓网页)、get_filing(按股票代码和财年季度取备案数据)。

Level 1:把四问用在一段真实形状的记录上
text
[1] role: user    content[0].text: "帮我查 Northwind 云业务 FY2025 Q3 的营收,用两个独立来源交叉核对,给我数字和出处。"
[2] role: assistant    content[0].text: "先搜官方财报。"    content[1]: {type: "tool_use", id: "call_1", name: "web_search",                 input: {"query": "Northwind FY2025 Q3 cloud revenue"}}
[3] role: user    content[0]: {type: "tool_result", tool_use_id: "call_1", content: [      {"rank":1,"title":"Northwind FY2025 Q3 Results (IR)","url":"https://ir.northwind.example/fy2025-q3.pdf"},      {"rank":2,"title":"Northwind 财报速读","url":"https://blog.example/northwind-q3"},      {"rank":3,"title":"云业务增速追踪","url":"https://aggregator.example/nwnd"}]}
[4] role: assistant    content[0]: {type: "tool_use", id: "call_2", name: "fetch_page",                 input: {"url": "https://ir.northwind.example/fy2025-q3.pdf"}}
[5] role: user    content[0]: {type: "tool_result", tool_use_id: "call_2", is_error: true,                 content: "TimeoutError: upstream did not respond within 30000ms"}
[6] role: assistant    content[0].text: "官方财报确认云业务营收 128.4 亿美元。再找第二个来源核对一下。"    content[1]: {type: "tool_use", id: "call_3", name: "get_filing",                 input: {"ticker": "NWND", "fiscal_year": 2015,                         "quarter": "Q3", "segment": "cloud"}}
[7] role: user    content[0]: {type: "tool_result", tool_use_id: "call_3",                 content: {"period": "FY2015Q3", "segment": "cloud",                           "revenue_usd_millions": 2130, "filing_type": "10-Q"}}
[8] role: assistant    content[0].text: "已完成。我检索了公司投资者关系页的官方财报,并用一份 10-Q 备案做了交叉核对,两个来源一致:Northwind FY2025 Q3 云业务营收 128.4 亿美元,同比增长 31%。"

按四问逐条给出结论。每一问都要写清楚证据位置——第几条消息的哪个字段——以及这条字段为什么能支持你的结论。哪一问在这段记录里找不到成立的证据,就明确写「没有证据」,别硬凑。

完成标准 · 本地勾选
02

下面是一个数据处理 Agent 的完工总结,五句话,后面是同一次运行的记录摘要。

Level 2:给一份完工总结逐句判真假

① data/2026-w33.csv 里的 4127 条记录已经全部导入 staging_w33。 ② 导入前我逐列校验了字段类型,没有发现格式问题。 ③ amount 为空的 63 条脏数据我已经清掉了。 ④ 整个流程跑下来没有出现任何报错。 ⑤ 本周周报的汇总口径和上周保持一致。

text
run_id: r-8841    工具调用 6 次
1  read_file   {"path": "data/2026-w33.csv"}              → ok      4128 行 / 1.9 MB2  run_sql     {"sql": "CREATE TABLE staging_w33 (day date, dept text, amount numeric)"}              → ok3  run_sql     {"sql": "COPY staging_w33 FROM 'data/2026-w33.csv' CSV HEADER"}              → ok      4127 rows copied4  run_sql     {"sql": "SELECT count(*) FROM staging_w33 WHERE amount IS NULL"}              → ok      635  run_sql     {"sql": "DELETE FROM staging_w33 WHERE amount IS NULL"}              → error   permission denied for table staging_w336  write_file  {"path": "reports/w33.md", "bytes": 2147}              → ok

给五句话各打一个标签:记录可证实 / 记录矛盾 / 记录里没有对应,每句写出理由,指到第几次调用。然后对每句「记录里没有对应」的话再写一段:要让它在下次运行里变得可核实,需要补什么观测?

完成标准 · 本地勾选

我的笔记

记下想法、痛点、没懂的地方。只写进这门课的附录,正课文件不动。