登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  科技周边 >  人工智能

AI Agent 怎样记录工具调用轨迹供回放

来源:17golang原创

时间:2026-10-19 22:28:05 444浏览 收藏

AI Agent 出问题时,最后一段回答往往只能说明“结果不对”,却说明不了它先调用了哪个工具、传了什么参数、拿到什么版本的数据。要做到可回放,核心不是把聊天记录越存越长,而是把一次运行拆成可定位的步骤,并为每个步骤固定输入、输出和执行上下文。

参考地址:https://huggingface.co/docs/hub/agent-traces

参考地址:https://opentelemetry.io/docs/specs/otel/trace/api/

要点速览
  • 用 trace_id 关联一次用户任务,用 run_id 区分重试,用 step_id 定位每个模型或工具步骤。
  • 工具调用至少记录名称、版本、规范化输入、输出摘要、耗时、错误和副作用等级。
  • 回放优先进入沙箱或替身;真实写入、删除、发消息等动作不能直接照着历史轨迹再执行。

先把一次 Agent 运行拆成三层标识

我更愿意把轨迹看成一棵树,而不是一条拼接后的字符串。trace_id代表用户任务的完整生命周期,run_id代表一次具体尝试,step_id代表模型决策、工具调用或工具返回。这样一来,第一次运行失败后再次尝试,不会覆盖第一次的证据;一个工具内部重试,也能保留父子关系。

每个 step 的最小字段可以按下面的结构设计:

# 这个记录只描述一次工具调用,不保存无法回放的隐式全局状态
step = {
    "schema_version": 1,
    "trace_id": "tr_20261019_001",
    "run_id": "run_02",
    "step_id": "step_04",
    "parent_step_id": "step_03",
    "kind": "tool_call",
    "tool_name": "orders.search",
    "tool_version": "orders-api@2026.10",
    "input": {"customer_id": "c_17", "limit": 20},
    "output": {"count": 2, "items_digest": "sha256:..."},
    "started_at": "2026-10-19T10:01:02Z",
    "duration_ms": 183,
    "status": "ok",
    "side_effect": "read_only"
}

input和output不一定都要存原文。大对象可以保存脱敏后的摘要与对象地址,但摘要必须能帮助定位“是否拿到了同一份输入”。日志、提示词、密钥、个人信息和返回正文也要有单独的保留策略,不能因为可回放就无限制落盘。

AI Agent 轨迹说明图,展示 trace、run、step 与工具输入输出的层级关系
图1:Agent 轨迹层级说明图,展示一次任务如何关联运行尝试和工具步骤。

记录字段时要把“能比较”和“会变化”分开

工具回放最容易踩的坑,是把所有字段都当作严格相等。时间戳、请求 ID、随机排序、令牌消耗和实时库存本来就可能变化;工具名、参数语义、返回状态和业务对象摘要则更适合参与回归比较。我通常会给字段加上比较策略,而不是在测试代码里到处写例外。

字段记录目的回放比较方式
tool_name / tool_version确认实际执行了哪个能力严格相等,版本变化单独标记
input复现调用条件规范化后比较,隐藏凭据
output观察工具给 Agent 的事实按业务字段比较,允许顺序差异
duration_ms定位慢调用只做趋势或阈值比较
side_effect决定能否直接回放作为执行门禁,不参与内容相等判断

OpenTelemetry 的 span 和 event 思路很适合承载这类上下文:把一次 Agent 运行作为父级范围,把模型调用和工具调用挂成子节点,再用事件记录异常或关键状态。无论最终落到 JSONL、数据库还是追踪系统,字段语义都应该先稳定下来,存储方式可以后换。

副作用工具必须用录制结果或沙箱回放

“回放”不是把旧的工具参数重新发送一遍。查询订单、读取文档这类只读动作可以指向冻结数据集;发送邮件、扣款、删除文件、写入生产数据库则必须拒绝直连。安全的做法是给工具声明副作用等级,并在 replay 模式下注入替身:

# replay 模式只允许读取录制夹具;写操作没有夹具就直接失败
def invoke_tool(tool, payload, mode, fixtures):
    # 先用稳定键查找夹具,避免把真实凭据放进回放请求
    fixture_key = make_fixture_key(tool.name, payload)
    if mode == "replay":
        if tool.side_effect != "read_only" or fixture_key not in fixtures:
            raise RuntimeError("replay denied: side effect or fixture missing")
        # 返回录制结果,不访问真实外部系统
        return fixtures[fixture_key]
    # live 模式才允许经过正常的鉴权、超时和审计链路
    return tool.call(payload)

夹具最好与工具版本、输入摘要和数据集版本一起保存。否则同一个参数在接口升级后可能得到完全不同的结构,回放失败时就分不清是 Agent 决策变了,还是工具事实变了。涉及隐私的轨迹还应在导出前脱敏,并设置保存期限。

AI Agent 回放边界说明图,展示只读工具、录制夹具、沙箱和副作用门禁的关系
图2:回放边界说明图,展示只读调用、夹具、沙箱与副作用门禁的关系。

最后按语义比较,而不是只比整段 JSON

回放完成后,先比较步骤树是否一致,再比较每个步骤的输入和输出。对列表按业务主键排序,对时间字段做归一化,对不可控的实时值只记录差异提示。真正需要升级处理的,是工具选择变了、参数缺失、错误类型改变,或 Agent 在没有新证据时走出了不同分支。

一个实用的检查顺序是:① trace/run 是否对应同一任务;② step 父子关系和工具名称是否一致;③ 输入是否只差脱敏或规范化字段;④ 输出差异是否来自数据集版本;⑤ 是否出现了本应被 replay 门禁拦截的副作用调用。这个顺序能把“轨迹不一致”和“业务数据变化”分开。

常见问题

只保存最终回答,后面补工具轨迹可以吗?

通常不行。最终回答无法恢复工具输入、返回顺序和错误分支,最多只能从下一次运行开始补采集。

工具输出要不要完整保存?

不必一刀切。对小型、低敏感结果可保存原文;大对象和敏感字段更适合保存脱敏摘要、版本号和可控的内部引用。

回放结果不同就说明 Agent 回归了吗?

不一定。先排除数据集、工具版本、时间和随机 ID 的变化,再判断工具选择、关键参数和错误语义是否真的改变。

真正可用的 Agent 轨迹,重点不在于记录更多,而在于让每个步骤都能回答三个问题:当时调用了什么、依据是什么、在什么边界内允许重做。把这三件事固定下来,调试、回归和事故复盘才会共享同一份证据。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>