AI 链路日志字段别随便起:traceId、sessionId、toolCallId 到底各指什么
最尴尬的一次排查,不是没日志,而是四拨人都拿着“自己的那条 id”在说话。前端同学贴了一个 sessionId,BFF 说自己只有 requestId,工作流平台那边只认 runId,到了工具服务层又冒出一个没人见过的 trace。会议开了十几分钟,大家连“我们查的是不是同一次请求”都还没对齐。
那次之后我对日志字段这件事的态度彻底变了。问题从来不只是“打得够不够多”,而是这些字段背后有没有稳定语义。AI 链路本来就跨前端、BFF、工作流、模型和外部工具,字段一旦在中途换了含义,再多日志也只是给排查现场增加噪音。
最浪费时间的,往往是先翻译彼此口中的 id
很多项目表面上也有一套看起来差不多的名字:
traceIdsessionIdrequestIdtoolCallId
真正查起来,最容易出事的是同名异义。比如:
- 前端把页面首次打开生成的 id 叫
traceId - BFF 也生成一个新的
traceId - 工作流把每一步重试都算成新的
requestId - 工具调用只在自由文本里写了一句
calling search
结果就是每一层都觉得自己打了日志,真正跨层排查时却像在拼四套不兼容的账本。
我后来固定下来的做法,是先给字段写词典
我现在会先把几类字段的所有权和语义钉死,不让它们在不同服务里自由漂移:
traceId:一次完整用户动作的全局追踪 id,从前端创建,后面整条链路只透传不重生。sessionId:用户会话 id,描述“这个人这一段连续操作”。requestId:一次具体 HTTP 或 RPC 请求的唯一 id,到了下一次请求就应该换。toolCallId:一次逻辑工具调用的 id,重试时不变;如果要区分第几次重试,就再补toolAttempt。modelLatencyMs:只记录模型侧耗时,不把排队、序列化和数据库写入一起塞进去。
这看起来像细节洁癖,实际收益非常具体。至少当有人在会议里说“这条请求”,你可以立刻问清楚他说的是完整链路、一次接口、一个会话,还是一次工具调用。只要这个问题能在十秒内说清楚,后面很多扯皮都会少掉。
一条能拿来排查的日志,字段关系应该一眼能看懂
我现在更愿意让日志接近下面这种结构:
{
"timestamp": "2026-03-11T14:32:19.203Z",
"level": "info",
"traceId": "tr_7f12",
"sessionId": "sess_901",
"requestId": "req_321",
"workflowRunId": "wf_88",
"toolCallId": "tool_77",
"toolAttempt": 2,
"scene": "content-review",
"layer": "workflow",
"toolName": "policy_check",
"model": "gpt-4.1",
"modelLatencyMs": 842,
"retryCount": 1,
"handoffToHuman": false,
"errorCode": null
}
这里最关键的不是字段多,而是字段之间的层级关系稳定。traceId 用来把整条链路串起来,requestId 让你落回到单次请求,workflowRunId 负责定位工作流运行态,toolCallId 则把某一次工具动作从整条大链路里拎出来。字段分工一清楚,很多查询就会变得非常直接。
别把关键事实继续埋进自由文本里
很多团队愿意给 msg 打一大串自然语言,却不愿意多加几个结构化字段。短期写起来省事,后面查问题时就会发现,真正关键的东西都只能靠全文搜索和人工理解。AI 系统尤其吃这个亏,因为链路里常见的几个问题几乎都应该结构化:
- 这次失败是不是可重试,应该放进
retryable - 这次为什么转人工,应该放进
handoffReason - 这次返回为什么终止,应该放进
finishReason - 这是模型超时、工具报错还是权限拒绝,应该落到
errorCode
如果这些信息还留在一段 msg: "tool failed but maybe retry" 里,系统明明已经知道答案,排查的人却还得自己读一遍再提取出来。
改字段命名之后,收益通常不体现在“更优雅”,而体现在少走弯路
我见过最典型的一类坏日志,长这样:
{
"id": "123",
"time": 921,
"step": "call",
"msg": "ok"
}
它并不是完全没用,但只对写这段代码的人有用。换一个团队、换一个排查时刻、换一次跨层协作,这条日志几乎帮不上忙。
把字段词典定下来以后,收益最明显的地方反而很朴素:
- 前端能用
traceId把用户点击和后端请求串起来 - BFF 能按
requestId看到限流、鉴权和路由决策 - 工作流层能按
workflowRunId看步骤状态、补偿和重试 - 工具侧能按
toolCallId判断究竟是逻辑失败还是某次重试失败
以前一次故障会常常先花二十分钟做名词翻译,现在通常先按 traceId 拉全链路,再往下钻 requestId 或 toolCallId。不是问题变简单了,而是系统终于愿意先把事实整理好。
好日志的标准,从来不是字更多,而是对齐更快
我现在看日志设计,已经很少把它当成单纯的代码风格问题。字段命名本质上是在决定事故响应时,团队需要先翻译多久,才能开始判断问题。对 AI 系统来说,这一点尤其重要,因为真正的故障通常跨越多层,不会只停留在模型或前端某一层。
所以如果今天要我只保留一条经验,我会选这句:日志字段的任务不是把信息写多,而是让不同角色以最快速度确认自己看的是同一件事。做到这一点,traceId、sessionId、requestId、toolCallId 这些名字才不只是命名习惯,而是真正的协作基础设施。
