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_thought |
| Plan | 高层计划拆分 | plan、todos、sub_goals |
| Tool Call | 工具调用意图 | tool_calls、function_calls、tool_use |
| Tool Result | 工具返回观察 | tool_results、function_results、observation |
| Final Action | 最终动作 | final_answer、final_message、action |
| Step 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 5
Fable 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_result
-
step 7:final_action
每个 step 是独立的 content block,可以直接取出来做 diff。
坑点 :Fable 5 的 reasoning 字段不是 100% 确定性的。同样输入 + 同样 context + seed=42,我自己测出来 reasoning 文字有 3% 概率不完全一致。但 tool_use 是确定性的,tool_use 重放就够定位大部分 bug 了。
2. DeepSeek R1
R1 的特点: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-Max
Qwen3.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.2
GLM-5.2 的步骤暴露思路比较老派:function_call 是单独字段,reasoning 是另一个字段,中间靠 model 自己对齐。
实测里发现一个问题:同一个工具调用,reasoning 里描述的语义和 function_call 里的参数经常略有偏差 。比如 reasoning 里说"查询北京天气",function_call 里写的是 city="Beijing"。看起来一致,但偶尔会出现 reasoning 说"上海"、function_call 写"Beijing"的情况。
这种"语义漂移"对可审计是致命的------你没法完全相信 reasoning 里写的和实际工具调一致。生产建议:以 function_call 为准,reasoning 仅做辅助参考。
5. Kimi K2.7 Code
Kimi 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。理论上应该是 0
-
tool_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 的会稍微老派一点。
Python
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_ascii=False) 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(f"step {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_level="L1")
rid_plan = recorder.record_step("plan", {"text": "我会先想,再搜,再答"})
rid_think = recorder.record_step(
"reasoning",
{"text": "用户问的是 X,我需要搜 ... ", "model": model},
parent_step_id=rid_plan,
)
rid_tool = recorder.record_step(
"tool_call",
{"tool": "web_search", "query": "2026 Q3 deterministic agent"},
parent_step_id=rid_think,
)
rid_obs = recorder.record_step(
"observe",
{"results": ["..."], "from": "web_search"},
parent_step_id=rid_tool,
)
rid_final = recorder.record_step(
"final_action",
{"text": "综合搜索结果 ...", "model": model},
parent_step_id=rid_obs,
)
return {
"task_id": recorder.task_id,
"trace": recorder.dump(),
"last_step": recorder.steps[-1],
}
if __name__ == "__main__":
out = call_agent_with_replay("调研 Agent 步骤回放", model="claude-fable-5")
print(out["trace"])
实战提示:这段代码里 uuid.uuid7() 在 Python 3.14+ 才原生支持,更早的版本建议用 uuid6 或者手写个基于毫秒时间戳的实现。生产环境我推荐先在 recorder 这一层把 step 结构定死,不要让上层框架去碰 step 字段,否则审计和回放都对不齐。
七、Agent 可审计实战细节 FAQ
Q1:步骤回放应该存多久?
看任务等级。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,资源开销不小。
八、参考资料
-
炻光 AI 接入管理平台 - 多模型 Agent 接入 (用于本篇文章的横向调测)
九、写在最后
最后给三条踩坑后的经验,适合刚开始做 Agent 步骤审计的同学:
-
可审计的第一步是模型选型,不是 Agent 框架。框架只能帮你存结构化字段,但如果模型本身不暴露 step,你存的就是黑盒。先看模型,再看框架------这个顺序反了,后面再调也是事倍功半。
-
别一上来就上 L2 全量回放。从 L1 存 step 块开始,等真正定位过一个 bug 之后再升级。盲目上 L2 只会让账单起飞,价值评估还没建立,事后说不清楚这是不是值得。
-
reasoning 字段不等于可审计。reasoning 是模型自己的"叙述",不是真正的决策痕迹。可追溯的应该是 tool_call、tool_result 这类结构化字段,而非 reasoning 字符串本身------我看到太多团队把 reasoning 当审计证据用,其实它是"事后讲故事"。