系列「企业级 AI Agent 实现拆解」E37 篇,Part 9 起步篇第七章。Agent 跑起来了,但每次请求花了多少 Token、每天大概要多少钱------这篇讲怎么把这些数字取出来、怎么算成本。
读完这篇你会知道
- Token 用量数据在哪里:
ResponseMeta.Usage- 用 Callback 不改主流程拿到每次调用的用量
- 多轮对话的累计用量怎么追踪
- 主流模型的定价和简单的成本估算公式
- 生产环境的用量统计最小实现
为什么需要统计 Token
三个最直接的原因:
- 控成本。 LLM 按 Token 计费。一个每天问 1000 次的 Agent,每次平均 2000 Token,月成本可能几百块也可能几万块,取决于你用的模型和有没有做优化。
- 找优化点。 哪个环节 Token 最多?是 system prompt 太长,还是 history 没有截断,还是工具结果塞了太多内容------不统计就不知道。
- 用量限额。 企业级场景要对租户限额(每月最多消耗 N 个 Token)、对 Agent 配置预算上限。没有用量数据,限额无从谈起。
Token 用量在哪里
每次 chatModel.Generate() 返回的 *schema.Message 里带着用量数据:
go
resp, err := chatModel.Generate(ctx, messages)
if err != nil {
return err
}
usage := resp.ResponseMeta // ResponseMeta 包含 LLM 的元数据
// usage.Usage.PromptTokens ← 输入 Token 数
// usage.Usage.CompletionTokens ← 输出 Token 数
// usage.Usage.TotalTokens ← 总 Token 数(两者之和)
// usage.FinishReason ← 停止原因:stop / tool_calls / length
ResponseMeta 是 schema.Message 的一个字段,底层数据来自 LLM 响应体里的 usage 对象(OpenAI 协议):
json
{
"usage": {
"prompt_tokens": 342,
"completion_tokens": 78,
"total_tokens": 420
}
}
DeepSeek、Claude、GPT-4o 都遵循这个格式(通过 OpenAI 兼容接口时)。
用 Callback 统一收集用量
直接在每个 Generate 调用后读 ResponseMeta 可以,但如果你有多个节点(Chain/Graph),每个节点都要加,很繁琐。
用 Callback 的 OnEnd 钩子统一收集:
go
import (
"github.com/cloudwego/eino/callbacks"
"github.com/cloudwego/eino/components/model"
)
type TokenTracker struct {
mu sync.Mutex
PromptTokens int64
CompleteTokens int64
}
func (t *TokenTracker) OnEnd(ctx context.Context, info *callbacks.RunInfo, output callbacks.CallbackOutput) context.Context {
// 只处理 ChatModel 节点的输出
if info.Component != components.ComponentOfChatModel {
return ctx
}
out, ok := output.(*model.CallbackOutput)
if !ok || out.TokenUsage == nil {
return ctx
}
t.mu.Lock()
t.PromptTokens += int64(out.TokenUsage.PromptTokens)
t.CompleteTokens += int64(out.TokenUsage.CompletionTokens)
t.mu.Unlock()
return ctx
}
// 实现 callbacks.Handler 接口的其他方法(可以空实现)
func (t *TokenTracker) OnStart(...) context.Context { return ctx }
func (t *TokenTracker) OnError(...) context.Context { return ctx }
func (t *TokenTracker) OnStartWithStreamInput(...) context.Context { return ctx }
func (t *TokenTracker) OnEndWithStreamOutput(...) context.Context { return ctx }
注册到 Agent:
go
tracker := &TokenTracker{}
ctx = callbacks.CtxWithHandlers(ctx, []callbacks.Handler{tracker})
sr, err := agent.Stream(ctx, messages)
// 消费完 sr 后...
log.Printf("本次请求 prompt=%d completion=%d",
tracker.PromptTokens, tracker.CompleteTokens)
Stream 模式的用量
Stream 模式下,Token 用量不在每个 chunk 里,而在最后一帧:
go
// eino-ext/libs/acl/openai/chat_model.go
req.StreamOptions = &openai.StreamOptions{IncludeUsage: true}
// IncludeUsage = true → SSE 流的最后一帧包含 usage 字段
Eino 的 OnEndWithStreamOutput callback 会在流全部消费完后触发,此时 TokenUsage 才有值。所以如果你用 Stream 模式,要在 OnEndWithStreamOutput 里读用量,不是 OnEnd:
go
func (t *TokenTracker) OnEndWithStreamOutput(ctx context.Context, info *callbacks.RunInfo,
output *schema.StreamReader[callbacks.CallbackOutput]) context.Context {
// output 是流,先全部消费(Eino 内部会处理)
// 用量在 StreamReader 关闭后才可用------用 callbacks 框架提供的工具函数
return ctx
}
实际上,Eino 的 callback 框架在流消费完毕后会自动调 OnEnd,并且把合并后的 TokenUsage 填进去。所以 OnEnd 在 Generate 和 Stream 两种模式下都能拿到用量------不需要特殊处理 Stream 情况。
成本估算
有了 Token 数,乘以单价就是成本。主流模型定价(截至 2026 年中,仅供参考,以官网为准):
| 模型 | 输入价格 | 输出价格 |
|---|---|---|
| DeepSeek V3 | $0.27 / 1M tokens | $1.10 / 1M tokens |
| Claude Sonnet 4.5 | $3.00 / 1M tokens | $15.00 / 1M tokens |
| GPT-4o | $2.50 / 1M tokens | $10.00 / 1M tokens |
| Ollama(本地) | $0 | $0 |
Go 实现:
go
type ModelPricing struct {
InputPerMillion float64 // 美元
OutputPerMillion float64
}
var Pricing = map[string]ModelPricing{
"deepseek-chat": {0.27, 1.10},
"claude-sonnet-4-5": {3.00, 15.00},
"gpt-4o": {2.50, 10.00},
}
func EstimateCost(model string, promptTokens, completionTokens int64) float64 {
p, ok := Pricing[model]
if !ok {
return 0
}
return float64(promptTokens)/1_000_000*p.InputPerMillion +
float64(completionTokens)/1_000_000*p.OutputPerMillion
}
一个参考数量级:
一次中等复杂的 Agent 对话(system prompt 500 Token + 5 轮对话 + 工具调用 2 次):
- 输入约 3000 Token,输出约 800 Token
- DeepSeek V3 成本:约 $0.0009(不到一厘钱)
- Claude Sonnet:约 $0.021(约一分半钱)
多轮对话的累计追踪
ReAct Agent 每个 Turn 可能调多次 LLM(think → act → observe → think...)。如果你要追踪一个完整 Turn 的总用量,用 Context 传递累计器:
go
type sessionUsage struct {
PromptTokens int64
CompleteTokens int64
}
type contextKey struct{}
func withSessionUsage(ctx context.Context) context.Context {
return context.WithValue(ctx, contextKey{}, &sessionUsage{})
}
func getSessionUsage(ctx context.Context) *sessionUsage {
v, _ := ctx.Value(contextKey{}).(*sessionUsage)
return v
}
// 在 OnEnd 里累加
func (t *TokenTracker) OnEnd(ctx context.Context, ...) context.Context {
if usage := getSessionUsage(ctx); usage != nil {
usage.PromptTokens += int64(out.TokenUsage.PromptTokens)
usage.CompleteTokens += int64(out.TokenUsage.CompletionTokens)
}
return ctx
}
Turn 结束时读累计值:
go
ctx = withSessionUsage(ctx)
ctx = callbacks.CtxWithHandlers(ctx, []callbacks.Handler{tracker})
// 跑完一个 Turn 后
usage := getSessionUsage(ctx)
cost := EstimateCost("deepseek-chat", usage.PromptTokens, usage.CompleteTokens)
log.Printf("Turn 用量: prompt=%d complete=%d cost=$%.6f",
usage.PromptTokens, usage.CompleteTokens, cost)
生产环境:写到数据库
开发时打个日志够了,生产环境通常要:
- 按租户统计(每个租户用了多少)
- 按 Agent 配置统计(哪个 Agent 最贵)
- 告警(单次请求超过阈值时报警)
最简单的实现是在 OnEnd 里异步写库:
go
func (t *TokenTracker) OnEnd(ctx context.Context, info *callbacks.RunInfo,
output callbacks.CallbackOutput) context.Context {
// ...读 TokenUsage...
go func() {
// 异步写,不阻塞 Agent 主流程
_ = usageRepo.Record(ctx, UsageRecord{
TenantID: tenantFromCtx(ctx),
AgentConfigID: agentConfigFromCtx(ctx),
Model: modelFromCtx(ctx),
PromptTokens: int(out.TokenUsage.PromptTokens),
CompleteTokens: int(out.TokenUsage.CompletionTokens),
RequestedAt: time.Now(),
})
}()
return ctx
}
DeepFlux 的实现更完整:用量写 usage_metrics 表(按月分区)+ QuotaLedger 扣减租户配额,超额时 QuotaGuard 返回 429------但核心数据来源就是这里:OnEnd 里的 TokenUsage。
小结
Token 用量追踪的完整链路:
- LLM 响应的
usage字段 →resp.ResponseMeta.Usage - Eino
OnEndcallback →model.CallbackOutput.TokenUsage - 累加到 Context 里的
sessionUsage结构体 → Turn 级别汇总 - 乘以模型单价 → 成本估算
- 异步写库 → 生产级统计
核心不超过 50 行代码。有了这个基础,租户限额、成本告警、模型选型优化(把低价值问题路由到便宜模型)才有数据支撑。
代码参考:eino callbacks · DeepFlux server/internal/billing/