1. 你的 Agent 一直在记账,只是没人看账本
Elastic Agent Builder 从正式发布那天起,就默认把每一次对话轮次写成完整的 OpenTelemetry trace。LLM 请求、工具执行、token 计数,全部落到 Elasticsearch 的 data stream 里,用 ES|QL 就能查。问题在于,绝大多数团队只在出事之后才翻这些数据——某次对话悄悄烧掉整月预算,或者某个工具调用慢得离谱,都是事后才发现。
这篇要解决的就是这件事:把 Agent Builder 内置的 OTel traces,通过 TaoToken 统一 Key/API 通道接进来,在 Kibana 里搭一个能看 token 成本的仪表板,再配一条超阈值告警。适合已经在用 Elastic Agent Builder、手里有 Kibana space、但还没把 trace 数据变成运营指标的人。读完你能拿到可复制的config.toml、settings.json骨架,OTel exporter 配置片段,以及在 Kibana 里验证 traces 落库和成本字段的具体检查动作。
先说清楚 trace 里有什么。一次 agent 运行就是一个 trace,可以理解成一次对话轮次的记录凭证。每个 LLM 请求、工具调用、agent 操作都是一个独立 span。span 类型大致分四层:invoke_agent <name>的 CHAIN 覆盖整轮生命周期,AGENT 是单次 agent 执行,chat <model>是一次 LLM 请求(带模型、延迟、token 数量),execute_tool <toolName>是工具调用(带参数、耗时、结果)。数据写进每个 Kibana space 专属的 data stream,默认 space 对应traces-agent_builder.otel-default。查询时直接指定索引,别用通配符,否则会把不同 space 的数据混在一起。
2. 用 TaoToken 统一 Key 打通 OTel 出口
Agent Builder 自己会产 trace,但如果你想让 trace 里的模型调用走统一通道、方便按 Key 维度对账,就需要一个统一的 API 入口。TaoToken 在这里的角色是统一 Key/API 通道:官网 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,API 入口 https://taotoken.net/api 。把模型调用的 base_url 指到它,Key 统一管理,trace 里的gen_ai.*字段就能和你的成本口径对上。
前置准备分三步。第一,在 TaoToken 控制台建一个 API Key,路径是 https://taotoken.net/console/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api_keys&utm_campaign=rewrite ,建完复制出来,后面写进环境变量。第二,确认你的 Kibana space 里 Gen AI 设置下 Agent Traces 的开关状态。agentBuilder:tracing:enabled默认开启,管 trace 采集;下面还有一组隐私控制,默认屏蔽提示词和工具输出,需要更完整 trace 才打开:agentBuilder:tracing:includeUserPrompts、includeLlmResponses、includeToolDetails、includeSystemPrompt、includeRealNames、includeRealIds。最后两个要特别小心,includeRealIds会保留真实对话标识符而不是哈希版本,等于把 trace 和具体用户会话关联起来,属于 PII 范畴。只有在你清楚 agent 处理的数据类型、并且有数据治理机制时才开。
第三,确认 OTel exporter 的出口。Agent Builder 用 OpenTelemetry 语义约定,span 属性名是gen_ai.usage.input_tokens、gen_ai.usage.output_tokens、gen_ai.conversation.id、gen_ai.operation.name这一套。你的 exporter 要把这些属性原样送进 Elasticsearch,别在中间做字段改名,否则后面 Lens 面板的公式会对不上。
3. 可复制的 config.toml 与 settings.json 骨架
下面这份config.toml是 OTel Collector 的骨架,负责把 Agent Builder 产出的 trace 转发到 Elasticsearch,同时把模型调用的出口指向 TaoToken。字段按你的实际环境替换,注释保留。
# config.toml - OTel Collector 骨架 receivers: otlp: protocols: grpc: endpoint: 0.0.0.0:4317 http: endpoint: 0.0.0.0:4318 processors: batch: timeout: 5s send_batch_size: 512 # 保留 gen_ai.* 语义约定字段,不做改名 attributes: actions: - key: gen_ai.usage.input_tokens action: upsert - key: gen_ai.usage.output_tokens action: upsert exporters: elasticsearch: endpoints: ["https://your-es-endpoint:9200"] # 写入 space 专属 data stream,默认 space 用 otel-default logs_index: traces-agent_builder.otel-default api_key: "${ES_API_KEY}" mapping: mode: otel service: pipelines: traces: receivers: [otlp] processors: [batch, attributes] exporters: [elasticsearch]模型调用侧的settings.json骨架,把 base_url 指向 TaoToken 的 API 入口,Key 从环境变量读,别硬编码进文件。
{ "llm": { "provider": "openai-compatible", "base_url": "https://taotoken.net/api", "api_key_env": "TAOTOKEN_API_KEY", "model": "your-model-name", "timeout_seconds": 60 }, "tracing": { "enabled": true, "otlp_endpoint": "http://localhost:4318", "service_name": "elastic-agent-builder", "include_user_prompts": false, "include_llm_responses": false, "include_tool_details": false, "include_real_ids": false } }环境变量这样设,Key 从控制台复制的那串填进去:
export TAOTOKEN_API_KEY="sk-你的key" export ES_API_KEY="你的es-api-key"OTel exporter 片段单独拎出来,方便你贴进已有的 collector 配置。注意mapping.mode: otel这行,它让 Elasticsearch 按 OTel 语义约定建映射,gen_ai.usage.*才会被识别成数值字段而不是字符串,否则后面求和会报类型错。
exporters: elasticsearch: endpoints: ["https://your-es-endpoint:9200"] logs_index: traces-agent_builder.otel-default api_key: "${ES_API_KEY}" mapping: mode: otel retry: enabled: true max_requests: 34. 验证 traces 落库与成本字段
配置写完别急着搭面板,先在 Kibana 里确认数据真的进来了。打开 Discover,索引模式直接指定traces-agent_builder.otel-default,时间范围拉到最近 15 分钟,跑一次 agent 对话,看有没有新文档进来。有文档说明 exporter 通了。
接着验证成本字段。在 Discover 的字段列表里找gen_ai.usage.input_tokens和gen_ai.usage.output_tokens,点进去看类型是不是 number。如果是 keyword 或 text,说明映射没按 OTel 约定建,回上一步检查mapping.mode。确认类型后,用 ES|QL 跑一条聚合,看单次对话的 token 总量:
FROM traces-agent_builder.otel-default | WHERE @timestamp >= NOW() - 15 minutes | WHERE gen_ai.operation.name == "chat" | STATS total_tokens = SUM(gen_ai.usage.input_tokens) + SUM(gen_ai.usage.output_tokens) BY conversation_id = gen_ai.conversation.id | SORT total_tokens DESC | LIMIT 10返回结果里每行是一个对话 ID 和它的 token 总量,这就是成本仪表板的数据源。如果返回空,先确认gen_ai.operation.name的值确实是chat,不同版本的 span 命名可能有差异,用| STATS COUNT(*) BY gen_ai.operation.name先看一眼实际值。
落库验证通过后,搭第一个 Lens 面板:水平条形图,x 轴gen_ai.conversation.id降序取前 10,y 轴用公式sum(gen_ai.usage.input_tokens) + sum(gen_ai.usage.output_tokens)。这就是「按 token 消耗排序的最活跃对话」。第二个面板可以换成 ES|QL 可视化,统计 LLM 往返次数最多的对话:
FROM traces-agent_builder.otel-default | WHERE @timestamp >= ?_tstart AND @timestamp < ?_tend | WHERE gen_ai.operation.name == "chat" | STATS `Chat Span Count` = COUNT(*) BY `Conversation ID` = gen_ai.conversation.id, `Span Name` = span.name | SORT `Chat Span Count` DESC | LIMIT 100告警规则也基于同一份数据。导航到 Observability > Alerts > Manage Rules > Create Rule,选 Elasticsearch query 类型,用下面这条 ES|QL,单次对话超过 256000 token 就触发:
FROM traces-agent_builder.otel-default | WHERE @timestamp > NOW() - 15 minutes | STATS total_tokens = SUM(gen_ai.usage.input_tokens) + SUM(gen_ai.usage.output_tokens) BY gen_ai.conversation.id | WHERE total_tokens > 256000 | KEEP gen_ai.conversation.id, total_tokens调度周期设 15 分钟,触发动作配 Slack 通知或 PagerDuty incident。告警负载里的gen_ai.conversation.id就是你要去查的那次对话。
5. 本篇常见错排查
trace 没进 Elasticsearch:先看 collector 日志有没有 export 失败。最常见的是logs_index写错,默认 space 必须是traces-agent_builder.otel-default,写成别的 space id 数据就进错地方了。其次是ES_API_KEY没权限写 data stream,检查 API Key 的角色。
token 字段求和报类型错:gen_ai.usage.input_tokens被映射成字符串了。原因是 exporter 没开mapping.mode: otel,或者字段第一次写入时就是字符串。删掉索引重新灌一次,或者用 index template 强制映射。
面板里对话 ID 是哈希值:agentBuilder:tracing:includeRealIds没开。默认是哈希版本,开了才保留真实 ID。但开之前想清楚 PII 问题,别为了好看把用户会话暴露了。
告警一直不触发:检查调度周期和查询时间窗口是否匹配。查询里写NOW() - 15 minutes,调度周期也设 15 分钟,两者对齐。如果调度 5 分钟但窗口 15 分钟,会重复统计同一批数据。
不同 space 数据混在一起:查询用了通配符索引。永远指定具体 space 的索引名,别用traces-agent_builder.otel-*。
模型调用 401:TaoToken 的 Key 没设对,或者base_url写成了带路径的形式。API 入口就是https://taotoken.net/api,别自己拼/v1之类的后缀,具体路径以接入文档为准。
6. 把 Key 和文档收好,下次直接查
排障和接入相关的入口放这里,下次遇到 401 或者字段对不上,直接翻:API Keys 管理在 https://taotoken.net/console/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api_keys&utm_campaign=rewrite ,接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite 。想先验证模型通不通,用模型对话页 https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_content=models&utm_campaign=rewrite 跑一轮,确认返回正常再回去看 trace。如果你是要长期跑编码类 agent、或者把 agent 接进 CI 流程,Coding Plan 的入口在 https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding_plan&utm_campaign=rewrite ,按套餐走比单次调用好对账。
最后留一个实操习惯:每次改完 collector 配置,先在 Discover 里跑一遍| STATS COUNT(*) BY gen_ai.operation.name,确认 span 类型分布正常,再去动 Lens 面板。这一步花三十秒,能省掉后面半小时的字段排查。