先说结论:模型那层你已经能测,工具那层基本是黑的

大部分团队对模型的把控已经不错了:有评测集、有回归测试、有失败样例归档。但 agent 一旦开始自己调工具——读文件、跑命令、发请求、写数据库——这部分的记录往往只剩终端里滚过去的一屏输出。等到出了事,你只能看到「最后的状态不对」,看不到中间每一步是谁、在什么理由下做的。

这篇不讲理论,给的是一套能直接套到现有代码里的记录骨架。前提只有一个:你认同「agent 做过的每一步都该留下可回看的痕迹」。

每次工具调用固定记四样东西

第一样是「谁要的」:不是指用户是谁,而是哪一次会话、哪一轮、由哪个 agent 角色发起。第二样是「碰了什么」:目标的类型与标识(哪个文件、哪条记录、哪个接口),不要只记一句自然语言摘要。第三样是「改了什么」:如果是写操作,记前后差异或差异的指纹;如果是读操作,记读到的范围。第四样是「成没成」:成功、失败、还是超时,失败时带上错误类别。

这四样定下来之后,最忌讳的是「顺手加一句人话描述就当记录了」。自然语言适合给人看,不适合给排查用——排查时要的是能过滤、能对比、能重放的结构化字段。

参数里最容易出事的是凭据,落盘前必须脱敏

工具调用的参数里经常混着 token、cookie、密钥、内网地址、用户手机号。直接把原始参数写进日志,等于把一份长期可读的凭据副本摊在了磁盘上,而且往往还有多份备份。

做法是:按「字段名命中敏感词」加「值长得像凭据」两道判断,命中就替换成固定占位符并只保留可校验的指纹(比如哈希后几位)。注意别只做名字匹配——参数经常是嵌套结构,敏感值可能出现在 payload 里的任意深度;也别把整个参数删掉,删了你后面就查不了「当时传的到底是什么形状」。

用一个 id 把模型调用、工具执行和写库串成一条链

排障最费力的情况是:日志都在,但拼不起来。解决方式是让一个请求 id 从用户输入那一刻就生成,往下贯穿模型调用、每一次工具执行、每一次写库,直到最终响应。所有记录都带上这个 id(以及它的父 id,如果用到了子任务)。

这样你排查时的入口就只有一个:拿到一个 id,把这一串按时间排出来,就能看到「模型决定做什么 → 工具真的做了什么 → 结果被写到哪里」。没有这个 id,你只能在几个不同的日志文件里用时间戳硬对,误差往往就是排查的全部成本。

失败时要保留一份「最小可重放的现场」

失败记录常见两种极端:一种只写「失败了」,一种把上下文整个 dump 出来。前者没用,后者既有成本又有隐私风险。中间的做法是记「重放这一次所需的最小输入」:工具名与版本、脱敏后的参数、当时的输入摘要、返回码、以及依赖的外部状态标识(哪个 commit、哪条记录、哪个版本的数据)。

判断标准很简单:拿着这条记录,你能不能在隔离环境里把同一次调用重跑一遍?能,就够;不能,就还缺东西;如果重跑需要的东西超过了这一条记录,那说明它太依赖当时的环境——这本身也是一个值得改的设计问题。

把「谁批准的」也记下来

自主运行的 agent 有两种动作:它自己决定做的,和人明确批准过的。这两种在事后追责时性质完全不同,所以记录里要区分开:是命中了一条预先定好的允许规则,还是这一次有人点了确认。

如果你们的权限规则是「按动作和资源」写的(比如允许某条只读命令、允许改某个目录),那规则命中的是哪一条也要记。这样当你发现某条规则过宽时,能立刻查出它过去拦了什么、放过什么,而不是靠猜。

边界:这是排障和回溯用的,不等于合规审计

最后说清这套东西挡不住什么。它记录的是「发生了什么」,不保证「记录本身没被改过」——如果需求是合规级审计,还需要追加写入不可篡改的存储、独立的权限边界和保留策略,那是另一件事。

同样,证据链不能替代权限控制:日志是事后知道,权限是事中拦住。两者都要,但如果只能先做一件,先做权限——因为一次不该发生的写操作,记录下来也回不去。