LLM 请求怎样关联 trace id 与 token 统计
LLM 请求一旦经过网关、业务服务和模型适配器,单看某一行日志很难回答三个问题:这次调用属于哪条用户请求链路、实际消耗了多少 token、异常发生在模型调用还是业务处理?比较稳妥的做法是把它拆成两条线:用 trace_id 关联请求,用响应里的 usage 统计 token,再把两者作为低敏元数据写入结构化日志。
本文使用 Go 的 context.Context 和 log/slog 演示这条链路。OpenTelemetry 官方地址:https://opentelemetry.io/docs/concepts/context-propagation/;Hugging Face Transformers 日志文档地址:https://huggingface.co/docs/transformers/main_classes/logging。示例不记录提示词、模型正文、API 密钥或用户隐私。
核心原则是:span 负责“这次调用在哪条链路上”,usage 负责“这次调用用了多少 token”,结构化日志负责“让两者可以被一起检索”。不要把提示词或响应正文当作关联键。
先把 trace 和 token 统计分成两件事
trace_id 标识一次跨服务的完整请求,span_id 标识其中一个局部操作。入口服务创建或接收 trace,调用 LLM 时再创建一个子 span;这样一次请求里可以区分“排队”“业务编排”和“模型调用”三个阶段。
token 统计来自模型供应商的响应,常见字段是输入 token、输出 token 和总 token,但不同接口可能使用 prompt_tokens、completion_tokens,也可能使用 input_tokens、output_tokens。因此不要直接把某个供应商的响应结构扩散到业务日志,而是先转成自己的统一结构。
| 字段 | 用途 | 是否建议落日志 |
|---|---|---|
trace_id | 关联一次请求链路 | 建议 |
span_id | 定位 LLM 子操作 | 建议 |
model | 按模型比较延迟与用量 | 建议 |
input/output/total_tokens | 统计调用用量 | 建议 |
| prompt、completion、API key | 请求内容和凭据 | 不要落日志 |
在请求上下文中创建 LLM span
OpenTelemetry 的上下文传播用于让不同服务产生的 trace、metric 和 log 可以关联。Go 代码里最重要的习惯是:不要在 LLM 适配器内部重新使用 context.Background(),而是接收上游传来的 ctx,并从它创建子 span。

package llmobs
import (
"context"
"log/slog"
"time"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/trace"
)
var tracer = otel.Tracer("example/llm-client")
// CallLLM 在现有请求上下文上创建子 span,只把低敏结果写入日志。
func CallLLM(ctx context.Context, logger *slog.Logger, model string) error {
ctx, span := tracer.Start(ctx, "llm.request")
defer span.End() // 无论成功或失败都结束 span,避免链路悬挂。
started := time.Now()
err := doProviderRequest(ctx, model) // 这里代表具体 SDK 调用。
status := "ok"
if err != nil {
status = "error"
span.RecordError(err) // 错误写入 span,但不把提示词塞进属性。
}
sc := trace.SpanContextFromContext(ctx)
logger.InfoContext(ctx, "llm request finished",
slog.String("trace_id", sc.TraceID().String()),
slog.String("span_id", sc.SpanID().String()),
slog.String("model", model),
slog.String("status", status),
slog.Int64("latency_ms", time.Since(started).Milliseconds()),
)
return err
}
func doProviderRequest(ctx context.Context, model string) error {
// 实际项目在这里传入 ctx,让 HTTP 客户端继续传播请求上下文。
return nil
}
这里的 trace.SpanContextFromContext 只读取关联信息,日志本身仍由 InfoContext 接收同一个上下文。即使日志采集器暂时不支持自动关联,也可以显式输出 trace_id 和 span_id,后端按字段检索即可。
把供应商 usage 归一化
统一结构应该描述业务真正关心的结果,而不是复制供应商全部响应。下面的结构只保留模型、输入、输出和总 token;如果某个接口没有返回 usage,就用 UsageKnown=false 表示未知,不能把缺失值悄悄当成零。

package llmobs
import "encoding/json"
// TokenUsage 是跨供应商的最小统计模型,不包含提示词和响应正文。
type TokenUsage struct {
InputTokens int64
OutputTokens int64
TotalTokens int64
UsageKnown bool
}
// providerResponse 只示意两套常见命名,真实项目按供应商 SDK 映射。
type providerResponse struct {
Model string `json:"model"`
Usage struct {
PromptTokens *int64 `json:"prompt_tokens"`
CompletionTokens *int64 `json:"completion_tokens"`
InputTokens *int64 `json:"input_tokens"`
OutputTokens *int64 `json:"output_tokens"`
TotalTokens *int64 `json:"total_tokens"`
} `json:"usage"`
}
// normalizeUsage 优先读取已返回的总数,缺失时再按输入和输出相加。
func normalizeUsage(raw []byte) (string, TokenUsage, error) {
var resp providerResponse
if err := json.Unmarshal(raw, &resp); err != nil {
return "", TokenUsage{}, err // 响应格式错误不能伪装成零用量。
}
var usage TokenUsage
switch {
case resp.Usage.InputTokens != nil || resp.Usage.OutputTokens != nil:
// Responses 风格字段:缺失的一侧按零处理,但保留已知状态。
if resp.Usage.InputTokens != nil {
usage.InputTokens = *resp.Usage.InputTokens
}
if resp.Usage.OutputTokens != nil {
usage.OutputTokens = *resp.Usage.OutputTokens
}
usage.UsageKnown = true
case resp.Usage.PromptTokens != nil || resp.Usage.CompletionTokens != nil:
// Chat Completions 风格字段:映射为统一命名。
if resp.Usage.PromptTokens != nil {
usage.InputTokens = *resp.Usage.PromptTokens
}
if resp.Usage.CompletionTokens != nil {
usage.OutputTokens = *resp.Usage.CompletionTokens
}
usage.UsageKnown = true
}
if resp.Usage.TotalTokens != nil {
usage.TotalTokens = *resp.Usage.TotalTokens
} else if usage.UsageKnown {
usage.TotalTokens = usage.InputTokens + usage.OutputTokens
}
return resp.Model, usage, nil
}
真实 SDK 返回的字段可能不是指针,但“缺失”和“数值为零”仍然是两个概念。若接口是流式响应,usage 往往只在结束事件出现,应该等到流结束再记录一次,而不是把每个增量块的累计值重复相加。
让结构化日志同时带上 trace 和 usage
Go 的 log/slog 支持在上下文中记录结构化属性。完成模型调用后,把归一化结果与 span 标识放在同一条完成日志里,排查时就不必先查一份日志再凭时间戳猜另一份日志。
package llmobs
import (
"context"
"log/slog"
"go.opentelemetry.io/otel/trace"
)
// LogUsage 把调用完成时的统计落成一条可检索记录。
func LogUsage(ctx context.Context, logger *slog.Logger, model string, usage TokenUsage, status string, latencyMs int64) {
sc := trace.SpanContextFromContext(ctx)
attrs := []slog.Attr{
slog.String("trace_id", sc.TraceID().String()),
slog.String("span_id", sc.SpanID().String()),
slog.String("model", model),
slog.String("status", status),
slog.Int64("latency_ms", latencyMs),
slog.Bool("usage_known", usage.UsageKnown),
}
if usage.UsageKnown {
// 只有 usage 已确认时才写 token 数,避免误报零用量。
attrs = append(attrs,
slog.Int64("input_tokens", usage.InputTokens),
slog.Int64("output_tokens", usage.OutputTokens),
slog.Int64("total_tokens", usage.TotalTokens),
)
}
logger.LogAttrs(ctx, slog.LevelInfo, "llm usage recorded", attrs...)
}
日志字段建议保持稳定:trace_id 用于聚合,model 用于比较,status 和 latency_ms 用于故障分层,三类 token 用于用量统计。不要把完整响应对象作为一个 JSON 字段写入,否则很容易把正文、工具调用参数或供应商内部信息一起带进日志。
流式响应和重试要怎样记账
流式请求最容易出现重复统计。中间事件可能只有增量文本,结束事件才带最终 usage;推荐以结束事件为准,在同一个 span 上写一次最终 usage。若流中断没有 usage,就记录 usage_known=false、错误类型和是否重试,不要用本地字符串长度估算 token 后冒充供应商账单。
重试也要先定口径:同一用户请求产生两个模型调用时,可以让每次尝试各自拥有一个子 span,并用 parent_trace_id 或业务 request_id 归到同一次业务请求;成本统计按 attempt 累加,用户请求统计则按顶层 trace 聚合。这样既能看到失败尝试,也不会把一次重试误认为一次成功调用。
| 场景 | span 记录 | usage 记录 |
|---|---|---|
| 普通成功 | 一个 LLM 子 span,状态成功 | 完成时写 input/output/total |
| 流式成功 | span 延续到结束事件 | 只采纳最终 usage 一次 |
| 超时或网络失败 | 记录错误和耗时 | 未知就写 usage_known=false |
| 自动重试 | 每次尝试单独子 span | 按尝试累加,按顶层 trace 聚合 |
隐私、采样和成本边界
trace_id 不是秘密,但外部传入的 trace header 不能无条件信任。OpenTelemetry 文档提醒,来自不受信任来源的传播数据可能被伪造;对外请求也要谨慎发送内部 trace、span 或 baggage。工程上可以只接受网关清洗后的上下文,并限制 baggage 的键和值。
日志字段还要遵守三条边界:
- 不记录提示词、完整响应、文件内容、密钥、Authorization 和用户可识别信息。
- 为高流量成功请求设置采样,但错误、超时和用量异常可以提高保留比例。
- 为单次请求和按模型的 token 总量设置阈值,发现异常时告警,而不是在日志里追加更多正文。
如果下游日志系统无法保证长期保留,可以把 trace/span 与 usage 汇总到指标或专门的成本表,但仍要保留一个可回查的关联键。目标不是收集越多越好,而是让一次调用能回答“从哪里来、用了多少、为什么失败”。
常见问题
只有 request_id,没有 OpenTelemetry 也能做吗?
可以。先用业务 request_id 贯穿服务,再逐步替换为 trace/span 语义。只是跨服务父子关系、采样和后端展示需要自己维护,规模变大后更容易出现字段不一致。
为什么不能把 prompt 哈希当成 trace_id?
两者语义不同。相同 prompt 可能属于不同用户请求,哈希也可能带来重放和隐私推断风险;trace_id 应表示一次链路,不能表示请求内容。
usage 缺失时应该填 0 吗?
不应该。零可能代表确实没有 token,也可能代表流式结束事件没有到达。用 usage_known=false 把“未知”表达出来,统计时再决定是否排除。
把 token 统计放在 span 属性里可以吗?
可以作为补充,但不要只依赖 span 属性。结构化日志更适合按模型、状态和时间窗口聚合;span 则适合查看一次调用在完整链路中的位置。两者使用同一个 trace_id,查询体验更稳定。
AI 应用怎样把输出过滤与业务规则分开
- 上一篇
- AI 应用怎样把输出过滤与业务规则分开
- 下一篇
- Hugging Face Hub 怎样用 commit hash 固定模型文件版本
-
- 科技周边 · 人工智能 | 1小时前 |
- Hugging Face Hub 怎样用 commit hash 固定模型文件版本
- 191浏览 收藏
-
- 科技周边 · 人工智能 | 1小时前 | ai ·
- AI 应用怎样把输出过滤与业务规则分开
- 317浏览 收藏
-
- 科技周边 · 人工智能 | 1小时前 |
- 视觉模型多个检测框怎样按阅读顺序排序
- 375浏览 收藏
-
- 科技周边 · 人工智能 | 1小时前 |
- LLM 推理批量大小怎样在显存和吞吐之间取舍
- 174浏览 收藏
-
- 科技周边 · 人工智能 | 1小时前 |
- AI Agent 工具超时后怎样保留可恢复状态
- 199浏览 收藏
-
- 科技周边 · 人工智能 | 1小时前 |
- Embedding 向量归一化怎样影响相似度计算
- 144浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
-
- PubMedQA
- 深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
- 521次使用
-
- H2O EvalGPT
- H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
- 578次使用
-
- LMArena
- LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
- 604次使用
-
- HELM
- 深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
- 554次使用
-
- MMBench
- MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
- 375次使用
-
- 量化模型本地推理的显存估算方法
- 2026-10-17 501浏览
-
- 本地大模型反复输出同一句话怎么调整生成参数
- 2026-09-06 501浏览
-
- Python 调用大模型时如何用结构化输出校验 JSON:从解析失败到可重试
- 2026-08-29 501浏览
-
- AI写作工具免费版安装教程(含豆包Clawdbot)
- 2026-05-30 501浏览
-
- WPS AI能自动生成PPT吗?输入主题一键制作演示文稿
- 2026-05-27 501浏览
