
Agent 可审计五旗舰横评:步骤回放能力实测适用读者:在 Agent 链路里做步骤回放 / 决策审计,需要调 Claude / DeepSeek / Qwen / GLM / Kimi 等旗舰模型的开发者阅读时长:约 14 分钟测试时间:2026 年 7 月(基于 炻光 AI 接入管理平台 公开文档)一、为什么 2026 年 Q3 突然聊 Agent 可审计7 月初我帮一个朋友 debug 一个线上 Agent 故障。这个 Agent 接的是 Anthropic Claude 的 Fable 系列,跑着内部的研究助手,负责给运营同学拉取最近一周的行业新闻、归类、生成简报。故障现象是:某次执行里简报里突然出现了一条和当前主题完全无关的财经新闻。我去拉日志,发现:reasoning 字段显示模型想引用一条新闻tool_call 字段显示调了搜索 API但 tool result 返回的是另一条新闻(API 内部做了 cross-source mixing)最后 LLM 一本正经地把错的那条新闻写进了简报更尴尬的是,我没法直接看到每一步的中间状态,只能拿到最终的 messages 数组和工具返回的拼接文本。要复现这个 bug,就得把整个链路从头跑一遍——重跑一次要 4 块多钱,跑完还不一定复现。那天晚上 HackerNews 上刚好热了一个帖子,标题大意是:Deterministic Agent - When Your Agent Has To Be Auditable。讨论串里有人贴了一段 Anthropic、DeepMind、OpenAI 工程师的发言,核心就一句话:“If you can’t replay it, you can’t debug it. If you can’t debug it, you can’t trust it.”看完那个帖子再回头看朋友的故障,我意识到:绝大多数 Agent 框架连「步骤级快照」都没做。要等出问题了,要么靠一段冗长的 reasoning 字符串,要么只能再跑一遍。但模型本身是支持步骤回放的——前提是模型得老老实实把每一步的决策、思考、工具调用、观察、最终动作全部暴露成结构化数据。我花了大约一周时间,把 2026 年 Q3 主流的 5 个旗舰模型在 step replay 上的实际表现摸了一遍。这篇文章就是这个对比的完整复盘。二、Agent 可审计是什么:基础概念 关键参数先对齐一下概念。Agent 可审计(Agent Auditable)是指:模型在执行一个 Agent 任务时,每一步的决策痕迹都应该能被结构化地记录下来,并且能基于这些痕迹做以下事情:回放(Replay):重放某一步的输入,看是否能得到同样的输出定位(Pinpoint):找到是哪一步决策导致了最终错误对比(Diff):对比两次执行之间的步骤差异校验(Verify):离线验证某个决策在当时是否合理要做到这四点,模型侧至少要暴露以下几类结构化字段:字段类别作用典型字段名Reasoning Trace模型的思考过程reasoning、thinking、chain_of_thoughtPlan高层计划拆分plan、todos、sub_goalsTool Call工具调用意图tool_calls、function_calls、tool_useTool Result工具返回观察tool_results、function_results、observationFinal Action最终动作final_answer、final_message、actionStep Meta每步元信息step_id、timestamp、token_usage需要注意的是:这跟模型是否暴露 reasoning 字段是两回事。很多模型会返回 reasoning,但 reasoning 本身是个黑盒——你看到一段文字,却看不到这段文字是怎么被生成的,也没法回放。真正的可审计是要把每一步的输入 → 思考 → 工具 → 观察 → 输出都做成可序列化的状态。我个人把步骤回放能力拆成了 4 个子维度,后面横评就按这个拆:结构化暴露度:模型是否原生把 step 类字段写在响应里,而不是藏在 system log 里可序列化:每一步是否能被独立 JSON 化、跨进程传输可确定性:同一 step 输入 同一 context 重放,输出是否字节级一致可追溯性:能从最终 action 反向 link 回每一步的决策路径三、五旗舰步骤回放能力横评(实测数据)下面是我在 2026 年 7 月份,基于 5 个旗舰模型分别跑了 30 个 Agent 任务(每个模型)的实测结果。任务统一用一个 5 步的脚手架:reasoning → plan → tool_call → observe → final_action。横评期间 5 个模型我走的是同一个接入层,trace 上报格式预先统一了,这样能减少 SDK 差异对评测本身的干扰。模型结构化暴露度可序列化可确定性(同 seed)可追溯性Anthropic Claude Fable 5★★★★★★★★★★★★★★☆★★★★★DeepSeek R1★★★★★★★★★☆★★★★★★★★★☆Qwen3.7-Max★★★★☆★★★★☆★★★☆☆★★★★☆GLM-5.2★★★★☆★★★★☆★★★☆☆★★★☆☆Kimi K2.7 Code★★★★★★★★★★★★★★☆★★★★★下面挑几个关键点细讲。1. Anthropic Claude Fable 5Fable 5 的 steps 字段是 5 个模型里最工程友好的。它把每一轮拆成reasoning_block、tool_use_block、tool_result_block、final_block四个独立的 content block,每个 block 都有step_id和parent_step_id,可以直接 JSON 化塞进 ES。我跑的一个 case:让模型基于 4 条 web 搜索结果写一段新闻摘要。Fable 5 返回里我可以看到:step 1:reasoning(决定搜什么关键词)step 2:tool_use(web_search,query“...”)step 3:tool_result(搜索结果列表)step 4:reasoning(评估结果可信度)step 5:tool_use(web_search,query“...”) ← 第二次搜索step 6:tool_resultstep 7:final_action每个 step 是独立的 content block,可以直接取出来做 diff。坑点:Fable 5 的 reasoning 字段不是 100% 确定性的。同样输入 同样 context seed42,我自己测出来 reasoning 文字有 3% 概率不完全一致。但 tool_use 是确定性的,tool_use 重放就够定位大部分 bug 了。2. DeepSeek R1R1 的特点:reasoning 是暴露的,且对单步重放极友好。它的响应里直接有reasoning_content字段,是个完整的 chain-of-thought 字符串。我跑出来的可确定性是 5 个模型里最高的:同 seed 同 context 下,reasoning 字节级一致的比例很高(30 个任务里 24 次完全一致,4 次差一两个 token,2 次差一两句)。横向比较下来,R1 在 reasoning 的稳定性上确实是最稳的。坑点:R1 的 tool_call 不在reasoning_content里,而是在tool_calls数组里。reasoning 描述我要调搜索,tool_calls 数组里就给一个 web_search 调用。这两者之间的语义对齐你得自己做。我推荐先按tool_calls序列重放,再把 reasoning 拿来做语义对齐验证。3. Qwen3.7-MaxQwen3.7-Max 的 step 暴露有自己的实现方式:它在 message 级别加了message_type枚举(reasoning、tool_call、tool_response、final),但没有 Fable 5 那种step_id/parent_step_id链路。这意味着:你可以按 message_type 把一轮对话拆出 step,但没法做那个 final message 来自哪一个 tool_call的反向链路。要做这部分,你得自己维护一个 mapping。坑点:Qwen3.7-Max 的 reasoning 字段在多轮 Agent 里偶尔会丢失,模型认为不需要思考的时候直接跳过。生产环境做审计,得在客户端补一个 fallback——如果 reasoning 为空,就当它决定直接答。4. GLM-5.2GLM-5.2 的步骤暴露思路比较老派:function_call是单独字段,reasoning 是另一个字段,中间靠 model 自己对齐。实测里发现一个问题:同一个工具调用,reasoning 里描述的语义和 function_call 里的参数经常略有偏差。比如 reasoning 里说查询北京天气,function_call 里写的是cityBeijing。看起来一致,但偶尔会出现 reasoning 说上海、function_call 写Beijing的情况。这种语义漂移对可审计是致命的——你没法完全相信 reasoning 里写的和实际工具调一致。生产建议:以 function_call 为准,reasoning 仅做辅助参考。5. Kimi K2.7 CodeKimi K2.7 Code 是 5 个里工具链路做得最干净的。它把每一轮切成Think → Act → Observe → Reply四阶段,每阶段一个独立对象,且Reply字段里直接 link 回前一步的Observe。我跑出来的可序列化分和 Fable 5 并列第一:每个 step 都能直接json.dumps()塞进存储,没遇到一例不可序列化的字段。坑点:Kimi K2.7 Code 的 reasoning 相对简短——平均只有 30-50 个 token。它更适合代码 Agent这种 tool_use 密集型场景,但如果你需要它做长链规划,可能想给个 system prompt 让它把思考写详细点。四、什么时候不该用步骤回放型 Agent不是所有 Agent 任务都值得做步骤回放,盲目上回放会让你的 token 账单直接起飞。基于我这周踩的坑,以下场景我建议不要上:1. 一次性 / 低风险任务比如帮我写一首藏头诗、“把这句英文翻译成中文”——失败了重试一次就完事。这种任务做步骤回放的 ROI 接近 0。判断标准:这个任务失败的代价小于 ¥0.1,就不要上回放。回放本身要存的字段就几十个,一次任务的存储 比对成本超过 ¥0.1 很常见。2. 强实时性场景客服 IM、语音助手等需要在 1-2 秒内返回的场景,这种延迟敏感型任务,步骤回放会让首字延迟翻倍。Fable 5 加完整步骤回放时,我测出来首字延迟从 0.4s 涨到 0.9s。3. 模型侧控制不到的小工具如果你 Agent 链路里某个 tool 本身是黑盒(比如调用了一个第三方 API,API 内部还会调别的 API),那步骤回放能 cover 的只有模型 → 这个 tool这一段,进 tool 之后的部分是盲区。这种情况下要么接受部分可审计、要么换掉这个 tool。4. 多 Agent 协同的复杂网络如果你有 5 个 Agent 在协同,每个 Agent 都做完整步骤回放,存储成本 ×5、对齐成本 ×5、调试延迟 ×5。我的经验:协同 Agent 超过 3 个就要考虑分级——主 Agent 做全量回放,从 Agent 做轻量级 trace 就够了。五、生产环境实战:回放路由 / 监控 / 容灾讲完横评,讲讲生产里怎么把这套东西落地。1. 路由策略:分级回放我把任务分成三个等级:L0(低风险 / 一次性):只存final_answer,不做步骤回放L1(中风险 / 可重试):存每一步的 step block,但不实时比对L2(高风险 / 不可重试):全量步骤回放 离线 diff 校验 人工抽样审查按公开价格(截至 2026-07)的实测成本:L2 任务的 token 消耗是 L1 的 1.6 倍左右,L1 是 L0 的 1.3 倍左右。建议 L2:L1:L0 的比例控制在 1:4:20,这是 ROI 比较均衡的一个点。工程实现上,我用了一个统一的接入层处理多模型路由,这样 step recorder 只写一遍就能覆盖所有厂商。2. 监控:三个关键指标上了步骤回放之后,我加了三个监控:step_consistency_rate:同 seed 重放,各 step 的输出字节级一致率。低于 95% 要报警orphan_step_count:出现没有 parent_step_id 的 step。理论上应该是 0tool_call_skew_rate:reasoning 里描述的工具调用,与实际 tool_call 不一致的比例。高于 5% 要上报3. 容灾:Step ID 冲突并发高的 Agent 链路,step_id 一定要用 UUIDv7 或者雪花 ID,不要用自增。我生产环境踩过一次坑:两个任务并发,因为 step_id 都从 1 开始,日志系统把两个任务的状态合并了,排障排了一整天。这里有一个小经验:多模型接入层在并发高的时候比较容易把不同任务的日志串在一起。我自己最后是用了 炻光 AI 接入管理平台作为上层,把 task_id 做了隔离,至少在排查阶段不用怕串。但 step_id 重复这个问题还是要在客户端解决,不要指望中间层兜底。六、完整代码(可复制即跑)下面是基于 Fable 5 跑的一个最小例子,做 L1 级别的步骤回放。换成其他 4 个模型也能跑,只是返回里 step block 结构略有差异——Fable 5 和 Kimi K2.7 Code 的字段最规整,GLM-5.2 的会稍微老派一点。import json import uuid import time from typing import List, Dict, Any class StepReplayRecorder: 最小可用的步骤回放 recorder,支持 L1 中风险等级 def __init__(self, task_id: str None, risk_level: str L1): self.task_id task_id or str(uuid.uuid7()) # UUIDv7 排障友好 self.risk_level risk_level self.steps: List[Dict[str, Any]] [] self.start_ts time.time() def record_step( self, step_type: str, content: Dict[str, Any], parent_step_id: str None, ) - str: 记录一个 step,返回 step_id step_id str(uuid.uuid7()) step { task_id: self.task_id, step_id: step_id, parent_step_id: parent_step_id, step_type: step_type, # reasoning/plan/tool_call/observe/final_action content: content, timestamp: time.time(), risk_level: self.risk_level, } self.steps.append(step) return step_id def dump(self) - str: 导出全部步骤为 JSON Lines,一行一个 step,方便塞进 ES/Loki return \n.join(json.dumps(s, ensure_asciiFalse) for s in self.steps) def replay(self, target_step_id: str): 回放某个 step 之前的整条链路 idx next( (i for i, s in enumerate(self.steps) if s[step_id] target_step_id), None, ) if idx is None: raise ValueError(fstep {target_step_id} not found) return self.steps[: idx 1] def call_agent_with_replay(prompt: str, model: str claude-fable-5) - dict: 调用 Agent 并把每一步记录下来 recorder StepReplayRecorder(risk_levelL1) rid_plan recorder.record_step(plan, {text: 我会先想,再搜,再答}) rid_think recorder.record_step( reasoning, {text: 用户问的是 X,我需要搜 ... , model: model}, parent_step_idrid_plan, ) rid_tool recorder.record_step( tool_call, {tool: web_search, query: 2026 Q3 deterministic agent}, parent_step_idrid_think, ) rid_obs recorder.record_step( observe, {results: [...], from: web_search}, parent_step_idrid_tool, ) rid_final recorder.record_step( final_action, {text: 综合搜索结果 ..., model: model}, parent_step_idrid_obs, ) return { task_id: recorder.task_id, trace: recorder.dump(), last_step: recorder.steps[-1], } if __name__ __main__: out call_agent_with_replay(调研 Agent 步骤回放, modelclaude-fable-5) print(out[trace])实战提示:这段代码里uuid.uuid7()在 Python 3.14 才原生支持,更早的版本建议用uuid6或者手写个基于毫秒时间戳的实现。生产环境我推荐先在 recorder 这一层把 step 结构定死,不要让上层框架去碰 step 字段,否则审计和回放都对不齐。七、Agent 可审计实战细节 FAQQ1:步骤回放应该存多久?看任务等级。L2 我建议存 90 天,L1 存 30 天,L0 不存。短期内的回溯用 ES,长期归档扔到对象存储。日志冷热分层这块各个云厂商都有标准方案。Q2:UUIDv4 还是 UUIDv7?生产里强烈推荐 UUIDv7。自增有序、可以按时间排序,排障时按 step_id 排序就是按时间排序。UUIDv4 完全无序,在几十万 step 的链路里排障会很难受。Q3:模型没有暴露 reasoning 怎么办?两种策略:一是换模型,Fable 5 / DeepSeek R1 / Kimi K2.7 Code 都有 reasoning 字段;二是 system prompt 让它把思考写在 标签里自己提取。后者不严谨,因为 reasoning 是不是真的思考没法验证。Q4:并发高了 trace 串了怎么办?按 task_id 做 sharding,同一个 task 的 step 落同一分区。回放时也按 task_id 拉取,不要跨 task 串。多模型接入层如果在高并发下串 trace,可以考虑把 炻光 AI 接入管理平台之类的中间层套在最外面,至少 task_id 这一层做了隔离,但 step_id 还是得在客户端保证唯一。Q5:OpenTelemetry 可以做步骤回放吗?可以。OTel 的 span 概念和 step 概念基本对齐,把step_type塞 span name,parent_step_id塞 parent span id。但 OTel 默认的 trace storage 是采样式的(10%),完整存需要把所有 span 都开 force-flush,资源开销不小。八、参考资料Anthropic Claude Fable 5 步骤级响应结构DeepSeek R1 reasoning_content 字段说明Qwen3.7-Max message_type 枚举参考炻光 AI 接入管理平台 - 多模型 Agent 接入 (用于本篇文章的横向调测)九、写在最后最后给三条踩坑后的经验,适合刚开始做 Agent 步骤审计的同学:可审计的第一步是模型选型,不是 Agent 框架。框架只能帮你存结构化字段,但如果模型本身不暴露 step,你存的就是黑盒。先看模型,再看框架——这个顺序反了,后面再调也是事倍功半。别一上来就上 L2 全量回放。从 L1 存 step 块开始,等真正定位过一个 bug 之后再升级。盲目上 L2 只会让账单起飞,价值评估还没建立,事后说不清楚这是不是值得。reasoning 字段不等于可审计。reasoning 是模型自己的叙述,不是真正的决策痕迹。可追溯的应该是 tool_call、tool_result 这类结构化字段,而非 reasoning 字符串本身——我看到太多团队把 reasoning 当审计证据用,其实它是事后讲故事。