Token 账单怎么看:给 Agent 加用量统计(第51篇-E37)

系列「企业级 AI Agent 实现拆解」E37 篇,Part 9 起步篇第七章。Agent 跑起来了,但每次请求花了多少 Token、每天大概要多少钱------这篇讲怎么把这些数字取出来、怎么算成本。

读完这篇你会知道

  • Token 用量数据在哪里:ResponseMeta.Usage
  • 用 Callback 不改主流程拿到每次调用的用量
  • 多轮对话的累计用量怎么追踪
  • 主流模型的定价和简单的成本估算公式
  • 生产环境的用量统计最小实现

为什么需要统计 Token

三个最直接的原因:

  1. 控成本。 LLM 按 Token 计费。一个每天问 1000 次的 Agent,每次平均 2000 Token,月成本可能几百块也可能几万块,取决于你用的模型和有没有做优化。
  2. 找优化点。 哪个环节 Token 最多?是 system prompt 太长,还是 history 没有截断,还是工具结果塞了太多内容------不统计就不知道。
  3. 用量限额。 企业级场景要对租户限额(每月最多消耗 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

ResponseMetaschema.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)

生产环境:写到数据库

开发时打个日志够了,生产环境通常要:

  1. 按租户统计(每个租户用了多少)
  2. 按 Agent 配置统计(哪个 Agent 最贵)
  3. 告警(单次请求超过阈值时报警)

最简单的实现是在 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 用量追踪的完整链路:

  1. LLM 响应的 usage 字段 → resp.ResponseMeta.Usage
  2. Eino OnEnd callback → model.CallbackOutput.TokenUsage
  3. 累加到 Context 里的 sessionUsage 结构体 → Turn 级别汇总
  4. 乘以模型单价 → 成本估算
  5. 异步写库 → 生产级统计

核心不超过 50 行代码。有了这个基础,租户限额、成本告警、模型选型优化(把低价值问题路由到便宜模型)才有数据支撑。


代码参考:eino callbacks · DeepFlux server/internal/billing/

相关推荐
小四的小六1 小时前
AI产品翻车实录:模型总结幻觉的三大类型与检测方案
前端·openai·ai编程
小蠢驴打代码1 小时前
AI 编程助手总是失忆?我做了一个纯 Markdown 的本地工程记忆库
ai编程
TrisighT1 小时前
Agent 流式响应你只拼 delta 就完事了?三个暗坑修完,白烧的 token 少了 68%
aigc·agent·ai编程
广东王多鱼1 小时前
第 08 章 — Agent 化编排
自动驾驶·agent
暗不需求1 小时前
目前最火AI协议-> MCP 协议实战:从原理到落地,让 AI 拥有"超能力"
面试·ai编程·mcp
广东王多鱼1 小时前
第 03 章 — Schema 检索(应对大库)
自动驾驶·agent
倔强的石头_1 小时前
10分钟搭建你的专属AI外教!基于魔珐星云参数流架构的AI口语陪练官实战指南
aigc
仙逆GPT2 小时前
Codex客户端找不到GPT-5.6模型怎么办?版本、配置和入口排查
chatgpt·ai编程·codex·sol·gpt-5.6
Artech2 小时前
[MAF预定义的AIContextProvider-05]CompactionProvider——采用多种策略压缩对话历史
ai·agent·maf·aicontextprovider·compactionprovider