☰
Agent可观测性实战:从分布式追踪到Token成本归因
2026/10/1 15:10:37 网站建设 项目流程

跑 Agent 项目的人大概都经历过这种绝望:昨天还能正常跑完的任务,今天重新执行,Agent 在第三步开始绕圈,最后触发超时;你以为只是模型抽风,打开日志发现它压根没拿到期望的工具返回;更糟的是,月底看账单发现 Token 花费是上个月的五倍,但你完全不知道钱烧在了哪次对话里。这些问题的本质,是你把 Agent 当成一个黑盒在调用,而不是当成一套分布式系统在运维。Agent 可观测性(Observability)要解决的,就是用分布式追踪、链路诊断和 Token 成本精细化核算,把"黑盒"变成"白盒"——让你能回答三个问题:系统走到哪一步了?每一步为什么这么走?这一步花了多少钱?这篇文章我按自己在多个 Agent 项目里的落地经验,从原理、埋点、排查和成本归因四个角度拆开讲。

先说一个基本判断:传统的 APM 监控思路,放在 Agent 场景下基本失灵。原因不是工具不好,而是 Agent 的执行模型和传统服务根本不在一个维度上。传统后端是"请求-响应"模型,一次 HTTP 调用从入口到出口,链路是确定的,超时和错误码是清晰的。Agent 不一样,它的核心是一个循环——规划、调用工具、读取结果、再规划。这个循环的次数不固定,每一步的分支不固定,模型可能根据中间结果临时改主意,甚至同一个任务跑两遍,路径完全不同。这种不确定性的执行模型,决定了你不能再只盯着 QPS、错误率、P99 延迟这些经典指标,你得先看清楚"它到底在干什么"。

1.1 本地开发能跑,一到线上就变黑盒

我在项目里反复遇到的情况是:本地调试时,Agent 每一步都打印日志,看起来一切正常;一旦部署成服务,日志被大量并发请求淹没,出现问题时只能看到"某次对话失败了",但找不到是哪一步失败、调了哪个工具、模型当时收到了什么上下文。原因很简单——本地调试你面对的是一个进程,线上你面对的是一个由 LLM 调用、工具执行、上下文管理、记忆检索组成的复杂流水线。这个流水线往往会横跨多个服务:API 网关、Agent 编排服务、工具执行器、向量数据库、模型网关。任何一个环节出问题,都可能让整个任务失败,但失败的表现却五花八门:工具抛异常、模型返回格式不对、上下文被截断、Token 超限、回调超时。

1.2 Agent 流水线里到底藏了什么

拆开看,一个典型的 Agent 执行单元包含五个可观测节点:

  • LLM 调用节点:模型选择、Prompt 组装、温度等参数、输入输出 Token 数、延迟和限流。
  • 工具调用节点:工具选择逻辑、入参构造、执行耗时、返回结果大小、异常信息。
  • 上下文管理节点:历史消息截断策略、重要信息摘要、缓存命中情况。
  • 记忆检索节点:向量检索的 TopK、相似度阈值、召回内容质量。
  • 路由与决策节点:Agent 选择哪个子任务、是否结束循环、是否切换模型。

这五个节点之间还有强耦合——工具返回的结果会进入下一轮 LLM 调用的上下文,记忆检索的内容会影响模型的决策,上下文截断策略又决定了模型能不能看到关键信息。传统监控工具孤立地看每个节点的指标,但 Agent 问题恰恰是跨节点的因果问题:比如"工具返回了一个超长 JSON,导致下一轮 LLM 调用的上下文超限,触发截断,模型丢失关键信息,最终决策错误"。这种因果链条,只有靠链路追踪才能完整还原。

1.3 Token 成本不只是"调一次 API 收费"

Token 成本核算是 Agent 可观测性里最容易低估的部分。很多人以为 Token 成本和调用次数成正比,实际完全不是。同样一个业务操作,在不同情况下 Token 消耗可能相差几十倍。比如一个工具返回了 12KB 的原始数据,Agent 把它原封不动塞进下一轮 Prompt,随后又做一轮摘要,再把摘要塞回上下文——这个"信息搬运"过程会产生大量的输入 Token 重复计费。再比如重试机制,模型输出格式不对导致重试一次,Token 消耗直接翻倍。还有多 Agent 协同场景,每个子 Agent 都要携带系统提示词和任务背景,这些都属于"每次调用都要付钱"的固定开销。成本失控从来不是某一个 API 太贵,而是整个链路上"看不见的 Token 浪费"太多。

2.1 先想清楚要追踪什么:Trace 是决策链,不是调用链

做分布式追踪,第一件事不是选工具,而是定义 Trace 的粒度。传统后端里,一个 Trace 对应一次外部请求,一个 Span 对应一次 RPC 或数据库访问,层级清晰。Agent 场景下如果照搬这套,你会发现所有 Span 都挂在同一个 HTTP 入口下,根本看不出 Agent 的思考过程。

我的做法是:把一次完整的 Agent 执行(从用户提问到最终回复)定义为一个 Trace,然后按照 Agent 的决策单元来划分 Span,而不是按照系统调用划分。也就是说,一个 Span 应该代表"Agent 做了一次决策并执行了一个动作"的完整闭环,包括:接收输入、调用模型、解析结果、执行工具、返回观察结果。这个闭环内部的子过程(具体的 LLM HTTP 调用、工具 HTTP 请求)作为 Span 的属性和事件记录,而不是另起一层 Span。原因很简单:排错时你最需要回答的是"这一步为什么这么做",而不是"这一步调用了几次 HTTP"。

2.2 一个最小可落地的 Span 模型

我在生产环境里用的 Span 结构大概是这样的,核心字段用代码表示:

{ "name": "agent.step.execute", "trace_id": "0a1b2c3d4e5f60718293a4b5c6d7e8f9", "span_id": "1a2b3c4d5e6f7080", "parent_span_id": "0a1b2c3d4e5f6000", "agent_run_id": "run_20250607_093012_abc123", "start_time": "2025-06-07T09:30:12.123Z", "end_time": "2025-06-07T09:30:15.456Z", "attributes": { "agent.id": "customer_service_bot", "agent.version": "2.1.0", "step.index": 7, "step.type": "tool_call", "step.name": "query_order_status", "llm.model": "gpt-4o-mini", "llm.input_tokens": 4820, "llm.output_tokens": 156, "llm.prompt_hash": "sha256:9f2c8e6d...", "tool.name": "order_service.query", "tool.input": "{\"order_id\":\"SO-20240607-001\"}", "tool.result_size_bytes": 12480, "tool.result_summary": "订单状态:已发货,物流单号:SF123456", "context.window_tokens": 28100, "context.max_tokens": 32000 }, "events": [ {"time": "2025-06-07T09:30:13.010Z", "name": "llm.response.received", "attributes": {"finish_reason": "tool_calls"}}, {"time": "2025-06-07T09:30:14.220Z", "name": "tool.execution.started", "attributes": {"timeout_ms": 5000}} ] }

这里有几个我特别想强调的设计:

  • agent_run_id 必须贯穿整个 Trace。一次用户会话可能包含多轮 Agent 执行,如果只靠 trace_id,你很难把"同一会话里的多次执行"关联起来看趋势。
  • prompt_hash 是隐藏的排障利器。它可以帮助你快速判断"这次奇怪的行为是不是因为 Prompt 变了",两个 Trace 的 prompt_hash 不同,优先怀疑 Prompt 版本问题。
  • 工具结果要双轨记录:既要记录原始大小(定位数据膨胀问题),也要记录一个截断后的摘要(方便人眼快速浏览)。摘要字段控制在 200 字以内,既直观又省存储。
  • context.window_tokens 必填。定位"上下文超限"和"截断导致模型变蠢"时,这个字段比什么都管用。

2.3 用 run_id 把子 Agent 和多轮循环串成树

实际项目中很少只有一个 Agent 单打独斗。常见的是编排 Agent(Orchestrator)调度多个子 Agent,子 Agent 再调用工具,形成一棵调用树。这里有个容易踩的坑:子 Agent 可能是异步执行的,也可能是独立进程/微服务,如果不在链路上下文里显式传递标识,Trace 就会在子 Agent 的边界断开。

我的方案是依赖 W3C Trace Context 标准,把 trace_id、parent_span_id 以及自定义的 agent_run_id 塞进子 Agent 调用的 Headers 或者消息队列的消息属性里。子 Agent 启动时接收这些参数,作为自己 Trace 的根上下文。这样整个编排过程在 Jaeger 或者 Grafana Tempo 里就能呈现为一棵完整的树:根节点是编排 Agent 的决策,子节点是各个子 Agent 的执行,叶子节点是工具调用和模型调用。排查"哪个子 Agent 拖慢了整体"或者"哪个子 Agent 烧了最多 Token"时,直接从树上聚合就行。

3.1 症状一:Agent 在循环里出不来

这是我遇到最多的问题,也是最难靠"打印日志"定位的。现象是:Agent 反复调用同一个工具,输入参数几乎一致,模型输出的内容也高度相似,直到触发步数上限。光看业务日志,你只能看到几十条"调用工具 X"的记录,但不知道模型在每一步给自己传达了什么样的"想法"。

正确的排查链路是:

  1. 打开该次执行的 Trace,按时间排序,发现step.type在llm_call和tool_call之间反复横跳,且tool.name基本不变。
  2. 对比相邻两个 Span 的llm.prompt_hash,发现 hash 只差最后一段——因为工具返回结果每次都一样,模型在下一轮看到的"新信息"其实没有变化。
  3. 点开中间某个 Span 的events,查看llm.response.received事件里的finish_reason和模型完整输出,发现模型的 reasoning 里出现"用户可能想知道更多细节,我继续调用"这类自洽话术。
  4. 最终结论:不是工具出错,而是模型的停止条件失效——它认为"每次获取一次新数据都算进展",但数据根本没变化。

这类问题的修复通常在业务层:给循环加上严格的收敛判断(比如连续两次工具返回的数据哈希相同就终止),或者把"判断是否继续"的逻辑从纯模型决策改为"模型建议 + 代码校验"混合模式。但如果没有 Trace 数据,你可能要花一整天抓日志去猜。

3.2 症状二:工具调用"边说边做",数据对不上

另一种常见故障是:Agent 在回复里声称自己调用了某个工具,但实际没有调用;或者调用了工具,但入参和它声称的不一致。这类问题的隐蔽性很高,因为单看 LLM 的回复文本一切正常,单看工具日志也一切正常,两者割裂后就找不到关系了。

我曾经排查过一个典型案例:客户反馈"Agent 查天气时说室外温度 46 度,明显不对"。打开 Trace 后发现,Agent 在步骤 3 调用了天气工具,但tool.input里的城市参数被模型从"北京"改成了"武汉"——模型在生成工具入参时自行"脑补"了一个城市。更蹊跷的是,步骤 2 的 LLM 调用里,模型输出明明包含"我要查询北京的天气"的文本,但紧接着的tool_call入参却是武汉。这属于典型的"模型生成工具参数与自身表述脱节"问题。如果没有把 LLM 的输出内容和工具的实际入参放在同一个 Span 里对比,这个问题几乎没法定位。

这类问题的经验是:必须在 Span 里同时记录模型选择工具的决定(包括原始文本)和实际执行的入参(包括 JSON 序列化后的完整值)。两者不一致时,不要怀疑是数据链路丢了,大概率是模型本身的问题,需要在 Prompt 工程或工具参数约束上解决。

3.3 症状三:Token 用量暴涨,但找不到是哪一步

现象是:同一个功能,昨天每次调用平均消耗 5 万 Token,今天突然变成 20 万。业务上没有任何变更,代码也没有发布新版本。用我下面会讲到的 Token 归因方法,按agent_run_id聚合每个 Span 的llm.input_tokens,很快发现暴涨集中在某个工具返回后。

进一步查看tool.result_size_bytes和context.window_tokens,发现该工具返回的数据量从昨天的 2KB 涨到了今天的 15KB——原因是上游服务有个字段从"开关量"变成了"全量历史明细"。数据被吞进 Agent 上下文后,每一轮后续决策都要带着这 15KB 反复计费。一次 8 步的任务,这 15KB 被重复计算了 8 次,Token 消耗自然爆炸。这种问题如果你只看"某次调用的 Token 数",永远找不到根因,因为单次调用并没有异常,异常在"跨步骤的重复计费"上。

4.1 成本的四个隐藏黑洞

在讲归因方法之前,先盘一下 Agent Token 成本为什么这么难核算。除了最直观的"模型单价 x 调用次数",还有四个隐藏因素:

  • 上下文重放(Context Replay):每次 LLM 调用都要携带历史消息。一个步数为 N 的 Agent 执行,总输入 Token 约等于历史消息长度与步数的乘积在常数级别上的增长。步骤越多,重放开销越大,呈二次增长趋势。
  • 工具返回吞没(Tool Result Swallowing):工具接口返回一个 10KB 的 JSON,Agent 原样塞进上下文,下一轮再用,再下一轮还用,直到被截断或摘要。这个"反复搬运"是输入 Token 的头号元凶。
  • System Prompt 的隐性放大:System Prompt 本身不大,但它随每次调用都要带上。多 Agent 场景下,每个子 Agent 都有一份几十上百行的系统提示词,乘以调用次数就是一笔不小的固定成本。
  • 重试与幂等缺失:模型输出格式不合法触发重试、外部工具失败后整步重来、网络超时导致重复提交。每一次重试都是完整 Token 消耗的翻倍。

4.2 建立按 Trace 归因的成本模型

传统的成本核算以"API 调用"为单位——每个接口记录 input/output token,再乘以单价。这在单轮 Chat 场景够用,但 Agent 场景必须把成本归因到业务操作路径上。否则你只知道"某模型花了多少钱",但不知道"哪个功能、哪类用户、哪种任务模式在烧钱"。

我的做法分三步:

  1. 在每个 LLM Span 上记录 token 明细:model、prompt_tokens、completion_tokens、cache_read_tokens、cache_creation_tokens(如果厂商支持提示词缓存)。
  2. 把 Span 成本归属到 Trace 和业务路径。一个 Trace 代表一次端到端的业务操作,Trace 上打上业务属性,比如business_type=order_query、user_tier=vip、entry_point=app。
  3. 在报表层聚合:按 Trace 聚合得到单次操作的"全链路成本",按业务类型聚合得到"每类功能的成本占比",按步骤聚合得到"成本分布在哪一步"。

成本计算公式很简单,我直接给出参考(单位统一为美元,汇率按账单结算折算):

Span 成本 = (prompt_tokens * 输入单价 / 1M) + (completion_tokens * 输出单价 / 1M) Trace 成本 = sum(所有 LLM Span 的 Span 成本)

注意:如果厂商提供了提示词缓存(Prompt Caching),cache_read_tokens一般按输入单价的约 1/10 计费,prompt_tokens字段里要区分开"未命中缓存的部分",否则成本会被高估。不同厂商的缓存计费规则不一样,建议在代码里用独立字段存放,而不是在计算时猜。

4.3 一个可执行的 Token 账单示例

我整理过一份真实场景下的 Trace 级成本账单(数值做过脱敏处理),用来和团队对齐"钱到底花在哪":

业务路径模型输入 Token输出 Token估算成本(人民币)占比
用户提问 -> 检索知识库 -> 生成回复(4 步)主力模型 A128,4003,200约 2.68 元41%
工具返回异常 -> 重试 2 次主力模型 A45,6001,100约 0.94 元14%
多 Agent 编排:分析 -> 计划 -> 执行(子任务)主力模型 A + 轻量模型 B220,3009,800约 4.12 元63%
系统提示词 + 零散验证请求轻量模型 B18,9002,400约 0.19 元3%

从这份账单能直接看出:多 Agent 编排是成本大头,工具重试次之,而"用户提问直接回答"反而占比不高。如果你只按模型维度看账单,你会得出"主力模型 A 花了很多钱"这个正确但毫无指导意义的结论。按业务路径归因后,你才敢拍板说"我们的多 Agent 编排设计过度了,需要裁剪"。

5.1 方案矩阵:链路采集与存储怎么选

Agent 可观测性的基础设施,本质上还是"埋点 -> 采集 -> 存储 -> 展示 -> 告警"这条链路。区别在于 Agent 场景的 Trace 具有三个特点:单条 Trace 的 Span 数量多、Span 属性携带的文本和 JSON 体积大、Token 成本类属性需要单独索引。所以在选型时要围绕这三点来做。

环节可选方案适用场景需要注意的问题
埋点 SDKOpenTelemetry SDK(Python/Node/Go 等)标准场景,语言覆盖广Agent 框架的自定义 Span 需要手动封装
采集器OpenTelemetry Collector标准场景,推荐注意 Payload 截断策略,不要原样上报工具返回全文
存储Jaeger / Grafana Tempo / ClickHouse开发或中小规模用 Jaeger/Tempo;需要长周期成本分析的选 ClickHouseTempo 对 Trace 内嵌关键属性的查询支持需要额外配置
展示Grafana / Jaeger UI可视化分析用 Trace 视图排查单次执行,用表格聚合做成本趋势
告警Prometheus + Alertmanager指标类告警Token 成本告警要基于 Trace 聚合,不是 Span 聚合

我的建议是:不要一开始就上全套大而全的平台。先用 OpenTelemetry SDK 把 Span 埋好,采集到 Jaeger 或 Tempo,够你肉眼排错了。当你开始频繁回答"上周每种业务操作的平均成本是多少"这类问题,再考虑把 trace 数据同步到 ClickHouse 做 OLAP 分析。我之前就吃过亏,一开始就建了 ClickHouse 数仓,结果埋点数据质量太差,花了大把时间清洗,还是不如先把埋点做扎实。

5.2 日志先行:结构化的"观测层"兜底方案

不是所有团队都有条件快速上全链路追踪。如果资源有限,我的建议是先做一套结构化的"观测层"日志,格式固定、字段完整,后续接 Trace 时能无缝迁移。这套日志的 JSON Schema 我贴在下面,已经按 Agent 场景踩过一遍坑:

{ "timestamp": "2025-06-07T09:30:12.123Z", "level": "INFO", "agent_run_id": "run_20250607_093012_abc123", "session_id": "sess_8888", "step_index": 7, "step_type": "tool_call", "event": "tool.execution.completed", "llm_model": "gpt-4o-mini", "llm_input_tokens": 4820, "llm_output_tokens": 156, "tool_name": "order_service.query", "tool_duration_ms": 1200, "tool_input_summary": "order_id=SO-20240607-001", "tool_result_size": 12480, "tool_result_status": "success", "context_tokens": 28100, "error": null, "metadata": { "env": "prod", "agent_version": "2.1.0" } }

这套日志的关键在于:把"模型层信息"和"工具层信息"放在一条日志里,而不是拆成两条。Agent 的因果链条是"模型决定调工具 -> 工具执行结束 -> 结果回到模型",如果你拆成两条日志,中间会夹杂其他并发请求的日志,排错时要把两条日志从一堆数据里"配对",痛苦不堪。合为一条后,直接按agent_run_id和step_index排序,就能完整复原每一步的决策和执行结果。

这套日志我建议所有字段保持扁平,避免嵌套层级过深。Elasticsearch 或 Loki 对扁平字段的索引和过滤都更友好。嵌套 JSON 在排错时写查询条件很别扭。

5.3 让开发生效的最后一公里:从 Trace 到告警

埋点做好了,Trace 也能查了,但如果不配告警,可观测性就只停留在"出事能查"层面。我个人认为,Agent 项目上线后最该配的五类告警是:

  1. 单次执行步数超阈值:比如正常任务最多 15 步,配一个"单次执行超过 25 步"的告警,大概率是循环失控。
  2. 单次执行 Token 成本超阈值:按业务路径分别设阈值,防止"某个功能悄悄变成烧钱黑洞"。
  3. 工具失败率突变:Agent 对工具的结果信任度很高,工具一旦开始失败,Agent 行为会跟着螺旋变差。
  4. 上下文窗口使用率过高:比如context.window_tokens / max_tokens超过 85%,说明截断风险极高,模型可能在"失忆"状态下决策。
  5. 模型返回格式异常率:finish_reason不是预期的调用行为,或 JSON 解析失败次数上升,通常是底层模型升级或 Prompt 结构改动导致。

告警渠道用常见的值班群就行,关键是告警里要带上agent_run_id和 Trace 链接。这样收到告警的人点进去就能看到完整链路,不需要再去日志系统里搜。我见过太多团队告警文案里只有"Agent 异常"四个字,点开不知道查什么,最后告警沦为背景噪音。

真到了这一步,我觉得 Agent 可观测性最值得记住的一句话是:先解决可见性,再谈优化和成本。我见过很多团队一上来就想着"怎么把 Token 成本降一半",结果连每个业务路径花了多少都不知道,谈何优化。反过来,把 Trace 埋点、成本归因、告警阈值这三件事踩实,成本优化方案会自动浮现——因为数据会告诉你该裁哪个 Agent、该摘哪段工具返回、该给哪类重试加熔断。

最后分享一个我个人很受益的小技巧:给每条 Agent Trace 打上prompt_template_version和agent_framework_version两个 tag。Prompt 调优和框架升级是 Agent 项目里最频繁的变更,这两个 tag 能让你快速回答一个灵魂拷问——"这个行为变化是模型的问题,还是我改了什么导致的?" 我自己靠这两个 tag 至少避免过十次无效争论。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询