在工具调用边界记录可回放轨迹
文章区分对话记录与可复现执行轨迹,建议在工具函数边界记录派发前参数、解析前响应、延迟、重试次数及模型、提示词和工具模式版本。这些记录可用于构建确定性回放桩、离线评测和行为差异对比。
AI 深度解读
Agent 在线上把客户 ID 传错了,日志里看得清清楚楚,修完提示词却再也复现不了。六周后,同类问题又出现在另一个工具上。原文讨论的就是这个缺口:知道当时发生了什么,不等于能把那次执行重新跑起来。
作者建议在工具调用边界记录输入和返回值,用严格回放做回归测试,再用真实模型评估和少量线上探测发现变化。这个思路实用,但文中的简易实现与它承诺的「可复现」之间,还有几处需要补齐。
为什么对话记录不够用
对话记录(transcript)侧重模型说了什么;执行追踪(trace)还需要保存进程实际接触过的东西,包括派发前的工具参数、解析前的原始响应、耗时、重试次数,以及模型、提示词和工具 schema 的确切版本。
例如,模型调用 search_orders 时传错客户 ID,对话记录能帮你定位错误参数,却未必保留当时接口返回了什么、是否重试、解析器如何处理异常。缺少这些信息,修复之后就很难验证同一条失败路径。
作者选择把记录器放在工具函数边界。这样,记录既能作为回放时的替身返回值,也能进入离线评估,还能比较 Agent 实际调用了什么与提示词要求它做什么。
LangSmith、Langfuse、OpenTelemetry spans 仍有观测价值。作者担心的是:如果框架记录到的已经是解析后的对象,原始响应就丢了,解析器的错误也无法通过这份记录重现。这里应检查具体接入位置,不能只看有没有开启 tracing。
CI 应该在没录过的调用上直接失败
原文给出无第三方依赖的 replay.py,把记录写入 traces/run.jsonl。调用键来自工具名和参数,经 json.dumps(sort_keys=True) 序列化,再取 SHA-256 的前 16 个十六进制字符。
下面这段装饰器决定调用真实工具,还是返回历史结果:
def tool(fn):
@functools.wraps(fn)
def wrapper(**kwargs):
key, recorded = _key(fn.__name__, kwargs), _load()
if MODE == "record":
result = fn(**kwargs)
with TRACE.open("a") as f:
f.write(json.dumps({"key": key, "tool": fn.__name__,
"args": kwargs, "result": result}) + "\n")
return result
if key not in recorded:
# 严格模式会在调用未被记录时直接失败,
# 提醒你 Agent 走到了追踪未覆盖的路径。
if MODE == "strict-replay":
raise RuntimeError(f"unrecorded call: {fn.__name__}({kwargs})")
return fn(**kwargs)
return recorded[key]
return wrapper
要盯住的是未命中后的 return fn(**kwargs):它会重新执行真实调用。
三种模式的区别由此展开。record 执行真实操作并保存结果;strict-replay 遇到未记录的调用就抛错;宽松回放则在未命中时访问真实服务。
CI 默认使用严格回放,才能让新增调用路径显式暴露。
新路径未必是回归,也可能正是修复的结果,但它需要被检查。作者提到曾见过一套通过的评估,有 30% 的调用偷偷访问了真实 API。这是作者的经历,没有提供可核验的实验记录,但它准确指出了宽松回放的风险:测试通过可能依赖当天的远端状态。
每次运行还应保存提示词 hash、model ID 与版本、工具 schema hash 和时间戳。否则出现差异时,很难区分代码错误与配置变化。
参数稳定了,也不代表模型和外部服务稳定
时间戳、UUID、无序集合和浮点数都会让调用键变化。
作者建议注入冻结时钟和固定种子的 ID,而不是删除这些字段。删除时间参数,可能顺手掩盖要检查的顺序错误。sort_keys=True 只排序字典键,不排序列表;由集合生成的列表,要在进入工具前稳定排序。0.1 + 0.2 与 0.3 的差异,则应在工具签名处明确精度规则,不能由记录器悄悄修改。
这里的原则是保留参数语义。确实需要删除字段时,也应把规则写进 trace 头部,避免六个月后无人知道回放忽略过什么。
但工具结果固定,不等于整个 Agent 固定。作者指出,即使 temperature=0,稳定模型名背后的权重、量化和路由仍可能变化。历史记录只是某次采样的快照,不能证明模型明天还会走同一条路。
因此需要两类补充验证:定期用失败 trace 驱动当前真实模型的评估,检查行为漂移;再用少量 live canary 探测第三方端点,检查接口、访问限制、robots.txt 或条款是否变化。昨天录下的网页响应,只能证明代码如何处理昨天的数据。
我的判断
我认同把回放当作回归测试设施,但原文没有给出修复效率或缺陷检出率的对照数据。它是一套工程经验,尚不能据此断言比现有框架方案更有效。
更关键的是,示例还没有完整兑现「解析前记录」:http_get 使用 urllib.request.urlopen(url, timeout=10),随后在函数内部执行 json.loads(r.read()),装饰器拿到的已是解析结果。要测试 JSON 解析错误,记录位置还得下移,保存原始响应。
头部设计也不能直接照加。示例 _load() 假设每行都有 key 和 result;插入元数据行后,需要先区分记录类型。它还对同一调用键采用最后一次结果覆盖,无法保留重试先失败、后成功的完整序列,更不能核对调用次数和顺序。
生产化还需要处理文件轮转、脱敏和副作用。作者建议每次运行一个 JSONL,并缓存加载结果;文中的每次调用加载耗时 400ms 是风险示例,不是性能测试结论。写操作应与读操作分离,通过显式 dry-run 控制,避免宽松模式重新执行 send_email。
相较于直接给工具编写 mock,录制回放更容易保留真实失败样本,但也更容易把过期响应当成事实。工具 schema 变化时应使旧记录失效。原始参数和响应还可能包含客户资料、token 和文档,必须写入时脱敏,设置保留期限与访问控制。作者提到的 NVIDIA OpenShell 只是约束网络出口和文件写入的一种运行环境示例,记录器本身不会提供这些隔离能力。
能拿来做什么
- 给现有 Agent 挑一条失败的只读调用链,录下工具参数与原始响应,把解析器也纳入回归验证;先明确记录边界,再接入框架追踪。
- 在 Node 项目的 CI 中默认启用
strict-replay,冻结时钟并固定 ID 生成方式;确需宽松回放的测试,应断言未命中次数。 - 为每份记录关联提示词、模型和工具 schema 版本,同时区分元数据与调用记录;涉及重试时保存事件序列,避免只留下最后一次成功结果。
- 把失败记录加入定期真实模型评估,并保留少量第三方接口探测;发送邮件等写操作单独走受控
dry-run,不要让回放缺失触发真实执行。