跳到主要内容

AI 链路日志字段别随便起:traceId、sessionId、toolCallId 到底各指什么

· 阅读需 5 分钟
一介布衣
全栈开发者

最尴尬的一次排查,不是没日志,而是四拨人都拿着“自己的那条 id”在说话。前端同学贴了一个 sessionId,BFF 说自己只有 requestId,工作流平台那边只认 runId,到了工具服务层又冒出一个没人见过的 trace。会议开了十几分钟,大家连“我们查的是不是同一次请求”都还没对齐。

那次之后我对日志字段这件事的态度彻底变了。问题从来不只是“打得够不够多”,而是这些字段背后有没有稳定语义。AI 链路本来就跨前端、BFF、工作流、模型和外部工具,字段一旦在中途换了含义,再多日志也只是给排查现场增加噪音。

最浪费时间的,往往是先翻译彼此口中的 id

很多项目表面上也有一套看起来差不多的名字:

  • traceId
  • sessionId
  • requestId
  • toolCallId

真正查起来,最容易出事的是同名异义。比如:

  • 前端把页面首次打开生成的 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 拉全链路,再往下钻 requestIdtoolCallId。不是问题变简单了,而是系统终于愿意先把事实整理好。

好日志的标准,从来不是字更多,而是对齐更快

我现在看日志设计,已经很少把它当成单纯的代码风格问题。字段命名本质上是在决定事故响应时,团队需要先翻译多久,才能开始判断问题。对 AI 系统来说,这一点尤其重要,因为真正的故障通常跨越多层,不会只停留在模型或前端某一层。

所以如果今天要我只保留一条经验,我会选这句:日志字段的任务不是把信息写多,而是让不同角色以最快速度确认自己看的是同一件事。做到这一点,traceIdsessionIdrequestIdtoolCallId 这些名字才不只是命名习惯,而是真正的协作基础设施。