YAOTU INSIGHTS

Agent可观测性实战:从Token核算到链路追踪的排障指南

Agent可观测性实战:从Token核算到链路追踪的排障指南
Agent 应用上线容易上线之后怎么盯住它别跑偏是另一回事。我最近帮一个团队排查生产事故现象很典型Agent 稳定运行一周后突然响应变慢Token 账单翻了三倍可 CPU、内存、带宽全都正常日志里也找不到一条报错。他们唯一的“可观测性”工具就是数据库慢查询慢查询确实变多了——但真正的问题根本不在数据库而在 Agent 的规划循环一个工具返回了异常格式Agent 没有识别出来进入无限重试每次重试都重新调用一次大模型单个会话烧掉了几十万 Token。这种故障传统监控三板斧根本抓不到。这正是 Agent 可观测性要解决的问题。把分布式追踪、链路诊断、Token 成本精细化核算合起来看本质上是在回答三件事这一次 Agent 任务到底经历了什么出问题时根因在哪一环整个过程烧了多少 Token、花在哪些环节这篇文章写给正在做 Agent 应用、又觉得“上线了但心里没底”的团队也写给准备搭 Agent 可观测体系的平台工程师。文章里不会给你一堆天花乱坠的概念都是我在实际项目里验证过的埋点方案、排障路径和成本核算方法你可以直接拿去抄作业。1. 为什么通用监控套件在 Agent 场景下会失灵从确定性请求到动态决策链1.1 传统监控三板斧的假设条件在 Agent 这里全都不成立了在传统 Web 服务里一次请求的路径是确定的网关进来经过认证服务调业务 A业务 A 调数据库再调业务 B最后返回。链路上每个节点的耗时、错误率、饱和度都可以预先定义好所以指标Metrics、日志Logs、链路Traces这三板斧能很好工作。你画一张拓扑图哪个节点红了问题就在哪。Agent 应用把这件事彻底打乱了。一次用户请求进来Agent 要根据当前上下文自己决定下一步做什么。同一个问题昨天走三步就完成了今天可能绕了二十步还回不来。链路是模型动态生成的不是代码静态定义的。拓扑图没法画因为边和节点都是运行时才出现的。更麻烦的是LLM 调用本身有随机性同一段 prompt 两次调用结果可能不同这导致你很难通过复现来定位问题——日志里能找到的只是大量相似但不完全相同的调用记录。还有个被多数团队忽略的点传统监控衡量系统健康度靠“错误率”和“延迟”但 Agent 场景下一个任务即使没有报错也可能已经跑偏了很久。我见过一个数据抓取 Agent上游接口返回的 JSON 结构变了它反复尝试用各种 prompt 去解析结果虽然“成功”了但多花了 17 倍 token耗时从 8 秒涨到 130 秒。从监控面板看延迟变高了但没有任何错误没有人知道发生了什么。你说这是故障吗用户体验变差、成本失控当然是故障。但传统监控会说一切正常。1.2 一个真实事故无报错的性能劣化与“Token 黑洞”我复盘一下那个事故的完整过程你就能直观感受到 Agent 可观测性和传统监控的差异。这个 Agent 的核心功能是帮运营人员批量整理商品信息给定一批商品 ID调用商品接口拿原始数据再让 LLM 总结成结构化文案。上线第一周一切正常。每个商品平均 2 次工具调用、1 次 LLM 调用单商品成本可控。第二周开始响应时间从平均 10 秒涨到 40 秒运营同事开始投诉。当时团队能看到的指标服务端 CPU 20%内存 40%全部正常日志里没有 error数据库慢查询有几条但都是后台任务和用户投诉时间对不上。一天后账单出来token 费用涨了 3 倍团队才意识到问题出在 LLM 调用上。后来我帮他们把 LLM 调用日志单独拉出来按天聚合发现问题出在第 9 天。那天有一条技能发布改动了“商品摘要”的 prompt把“如果商品描述缺字段返回示例格式”改成了“请尝试从商品图片信息中补齐”。效果是Agent 开始对一个缺字段的商品反复调用图像理解工具每次工具返回结果它都觉得不够继续让 LLM“再想想”。单个商品的 LLM 调用次数从 1 次变成 18 次工具调用从 2 次变成 35 次。没有报错只有 token 账单在加速狂奔。这就是我常说的 Token 黑洞一切指标正常只有 token 消耗在无声扩张。没有 token 计量这个事故只能等账单出来才发现有了 token 计量和链路追踪你可以在第一小时就看到单商品成本异常并顺着 trace 找到是哪个 prompt 改动造成的。1.3 Agent 可观测性要回答的三个新问题所以我的结论是Agent 可观测性不是把原来的监控体系延长一截而是新增三个必须被观测的对象。第一执行轨迹。Agent 在运行时调用哪些工具、每次调用的参数和结果、模型做了哪些决策、有没有重试和循环。这需要分布式追踪但 Span 的语义要重新设计不能用“HTTP 调用”这种粗粒度概念。第二Token 消耗。Token 是 Agent 世界的 CPU 时间是唯一能统一衡量“思考代价”的指标。一次请求烧了多少输入 token、多少输出 token、缓存命中多少这个数据必须被埋点捕获否则你既没法控制成本也没法判断一条链路是不是高效。这个观点和“AI 应用成本分析”领域的主流思路是一致的把 token 消耗当作应用的一等监控指标来看待。第三决策质量。Agent 输出了什么内容、有没有偏离目标、工具结果是否被正确理解。这部分很难自动化评估但至少要把工具调用的返回状态、模型输出的摘要记录下来便于事后复盘。很多团队上线 Agent 之后不看日志就是因为他们觉得“反正模型会自己处理”但模型处理得好不好恰恰是最需要看的。2. 给 Agent 做分布式追踪Span 类型、埋点设计与全链路串联2.1 Trace 与 Span 在 Agent 语义下的重新定义分布式追踪的基本概念大家应该都听过Trace 是一次完整请求的端到端执行轨迹Span 是轨迹里的独立工作单元Span 之间通过 parent-child 关系串联。这个框架本身没有问题问题在于 Span 怎么定义。在传统后端一个 Span 通常对应一次 HTTP 请求、一次 RPC 调用、一次数据库查询。在 Agent 场景如果你也这么埋得到的就只是一堆零散的 HTTP 调用记录根本看不出 Agent 的决策过程。我习惯把 Span 定义为“Agent 在这次任务中做的一件有语义的事情”而不是“一次网络请求”。举例来说一次用户请求可能是Agent 先分析意图这是一个 llm.call Span决定要查订单这是一个 agent.plan Span于是调用订单接口这是一个 tool.call Span拿到结果后又让模型总结这是第二个 llm.call Span。这四个 Span 串起来才叫“一次 Agent 任务”。如果只记录 HTTP 调用你会看到两次 LLM 调用分散在树里它们之间是什么关系、为什么会有第二次完全不可见。2.2 我常用的五类 Span 与关键属性清单我做了几个 Agent 项目后沉淀了一套相对固定的 Span 类型在团队里用下来效果不错。不一定是最佳实践但至少能覆盖大部分诊断场景。Span 类型span.name 示例父子关系关键属性任务级agent.task根 Spantask_id、session_id、user_id、goal摘要、agent_name决策级agent.planparent 为 agent.task决策内容摘要、选择工具、推理摘要模型调用llm.callparent 为 agent.plan 或 agent.taskprovider、model_name、prompt_tokens、completion_tokens、cache_read_tokens、temperature、max_tokens工具调用tool.callparent 为 agent.plantool_name、tool_args摘要、tool_result_status、error_type检索调用retrieval.callparent 为 agent.plantop_k、命中文档数、检索耗时task_id、session_id、trace_id 这三层关联标识是我特别想强调的。trace_id 关联单次任务执行链路像一张“任务过程快照”session_id 关联同一个用户的多轮对话用来观察上下文如何累积task_id 则用来关联业务侧的订单号、工单号等业务实体。我在实际项目里踩过坑一开始只埋了 trace_id结果用户来投诉说“昨天那个对话回答错了”我们对着 trace 查了半天发现查到了用户昨天的另外一次任务因为系统把 trace_id 重置了。后来强制要求三个 ID 同时落业务排查效率高了一个量级。2.3 埋点实现一个不污染业务代码的 LLM 调用追踪装饰器很多团队不乐意做埋点是因为觉得埋点代码会污染业务逻辑。实际上用装饰器或者中间件可以完全解决这个问题LLM 调用的埋点尤其适合做在统一出口上。下面这个例子是我在一个 Python 项目里实际用过的简化版核心思路是所有 LLM 调用都走同一个网关函数装饰器在这个函数外自动创建 Span 并捕获 token 用量。from opentelemetry import trace from opentelemetry.trace import SpanKind from functools import wraps tracer trace.get_tracer(llm-gateway) def trace_llm_call(provider, model): def decorator(func): wraps(func) def wrapper(messages, **kwargs): span tracer.start_span(llm.call, kindSpanKind.CLIENT) span.set_attribute(llm.provider, provider) span.set_attribute(llm.model, model) span.set_attribute(llm.prompt_tokens_estimate, estimate_tokens(messages)) try: resp func(messages, **kwargs) usage resp.get(usage, {}) span.set_attribute(llm.prompt_tokens, usage.get(prompt_tokens, 0)) span.set_attribute(llm.completion_tokens, usage.get(completion_tokens, 0)) span.set_attribute(llm.total_tokens, usage.get(total_tokens, 0)) cached usage.get(prompt_tokens_details, {}).get(cached_tokens, 0) span.set_attribute(llm.cache_read_tokens, cached) return resp except Exception as e: span.set_attribute(error.type, type(e).__name__) trace.get_current_span().record_exception(e) raise finally: span.end() return wrapper return decorator这里面有几个细节值得说。第一在调用前先估算 prompt_tokens虽然不准但能让你在模型真正返回前就看到 token 量级的初步信号对追踪超长请求特别有用。第二从响应里读取 usage 字段是标准做法但要注意不同模型服务商的字段结构略有差异有的放在顶层有的放在 response_usage 里网关层要做好适配。第三缓存读取 token 这个字段太容易被忽略但很多服务商对缓存 token 和普通输入 token 分别计价而且差价很大不单独记录会导致你的成本核算偏差巨大。2.4 采样策略元数据全量采集内容按需快照Agent 场景的采样策略和传统后端不一样。传统 APM 通常建议概率采样比如采样 10% 的请求因为流量大、低价值。但 Agent 请求往往低频高价值——一个任务可能消耗几千上万 token而且模型行为随机如果只采样 10%出问题的那个任务很可能恰好没被采到留下的都是正常请求链路诊断失去意义。我的做法是分层策略所有 Span 的元数据全量采集包括模型名、token 数、工具名、耗时、错误类型但 prompt 内容和模型输出内容默认不采因为在生产环境里 prompt 可能包含用户隐私或业务敏感信息全量落盘会有合规风险。只有当链路诊断需要时才针对特定 trace_id 开启内容快照。实现上也不难在网关层设一个开关命中采样规则的请求把 prompt 摘要存到专门的存储设置 7 天过期。我甚至见过更细的做法只存 prompt 的前 200 个 token 和模型输出的前 200 个 token对大部分问题诊断已经够了而且把敏感信息暴露面降到最低。用摘要代替全量这个思路在 Agent 可观测性里非常重要。3. 链路诊断实战Token 交换失败与上下文超限的完整排障路径3.1 案例一登录链路“token exchange failed”反复出现怎么定位Agent 应用里 token 这个词有两层含义很多人一开始会被绕晕。一层是模型计费用 token另一层是认证鉴权用 tokenJWT、OAuth token。我在项目里遇到最多的坑是后者用户登录时控制台报“sign-in could not be completed, token exchange failed”或者对接外部 API 时偶发“token exchange failed: 400 bad request”。这类报错排查起来特别容易踩坑因为问题往往不在报错的那一行代码里。我总结了一套可复用的排查路径核心就是让链路追踪告诉你“token 交换到底卡在哪一步”。第一步先看错误率趋势。打开认证服务的错误率曲线区分这是瞬时抖动还是持续故障。瞬时抖动大概率是某个基础组件重启或网络瞬断持续故障才需要深入链路。第二步在追踪系统里按 trace_id 拉出登录链路的完整 Span 列表你会看到认证服务调用身份提供商的 Span 标红展开后能看到 HTTP 状态码、请求 URL、响应体摘要。第三步根据状态码分流400 Bad Request常见于 refresh_token 为空或格式非法。我在一个项目里遇到过“invalid refresh_token: empty string. expected a string with minimum length 1”的报错最后定位到代码里从本地存储取 token 时key 拼错了取到的是 None不是 token 真的失效。这类问题没有链路追踪的话只能靠人肉读代码猜。403 Forbidden常见于账号所属区域、IP 策略不匹配。这个和代码关系不大问题在账号配置或网关策略。链路里记录的 provider 名和请求目标 IP 能帮你快速确认是这个原因。网络层错误比如“error sending request”则是 API 不可达需要检查网络策略、防火墙、域名解析和 token 本身没有关系。这个案例里我用“auth.example.com/oauth/token”这种通用形式来举例实际项目里换成你的真实认证端点即可。3.2 案例一背后的共性根因Token 生命周期与续签机制设计排查完单次故障之后我会建议团队不要急着关 Issue因为你很可能碰到了 Agent 长任务场景下最常见的共性问题——token 过期。JWT 和 OAuth access token 一般都有有效期短则 30 分钟长则几小时。Agent 任务往往比这更久一个复杂的调研任务跑十几分钟很常见如果中间调用了大量外部 API很可能任务还没跑完token 已经过期。热词里“jwt实现token续签”被反复搜索就是因为这个需求在 Agent 项目里特别普遍。我的建议是三层防护。第一接入层用双 token 机制access_token 短时效 refresh_token 长时效网关捕获 401 后自动用 refresh_token 续签续签动作本身也作为一个 Span 被追踪这样你随时能看出“这个任务中途续签了几次、失败了几次”。第二在工具调用 Span 里记录 token 剩余有效期接近过期时提前刷新避免请求打到一半突然失败。第三监控 refresh_token 的失败率因为 refresh_token 一般是长期有效的如果它频繁失败说明本地存储有问题或者被服务端撤销了那就不只是续签能解决的而是整个会话体系出问题了。3.3 案例二单会话 Token 消耗逐轮上涨最后撞上上下文窗口第二个案例是这样的一个客服 Agent 上线三周后开始出现“context length exceeded”报错而且用户反馈对话越来越慢。当时团队的第一反应是调大模型上下文窗口但成本不允许。后来我帮他们看 Token 计量曲线发现一个规律prompt_tokens 是逐轮线性上涨的每轮对话大约增加 1500 token。顺着这个特征去查 llm.call Span果然在属性里看到每一轮都把完整历史消息塞进 prompt没有做任何压缩。客服对话长一点prompt 里就塞了几万 token一来慢二来贵三来把上下文挤爆。这个根因用链路追踪很好定位在 Span 属性里新增两个字段compression_before_tokens 和 compression_after_tokens分别记录压缩前后的 token 数再用时序曲线叠加看一目了然。修复方案是“滚动摘要 滑动窗口”每轮对话结束时用一个小模型把历史对话压缩成 300 token 的摘要只保留最近 8 轮完整对话加摘要一起进 prompt。上线后观察 Span 属性prompt_tokens 从 4 万降到 6000响应时间从 30 秒降到 8 秒单会话 token 成本下降 70%。3.4 Token 用量曲线本身就是最重要的健康信号这两个案例放在一起看你会发现一个共同点真正定位问题靠的不是错误日志而是 Token 用量曲线和链路属性的组合。Token 用量在这个场景里就是 Agent 的“体温曲线”你不需要理解业务逻辑单看曲线形态就能发现异常。我常用的判断规则有这几条第一单会话 token 消耗呈线性增长多半是上下文没有压缩第二单任务 token 消耗出现阶梯式跳升多半是 prompt 模板改动或工具返回异常第三错误率没涨但 token 翻倍多半是重试循环第四缓存命中率突然下降多半是 prompt 前缀被改动缓存全部失效。这几条规则看起来简单但真正把它变成监控告警的团队不多绝大部分是等到账单爆了才回头查。4. Token 成本精细化核算从网关计量埋点到业务维度分账4.1 为什么常规“看账单”式核算根本不够用很多团队算 token 成本的方式特别粗犷月底看模型服务商账单总额多少就记多少。这种“事后总额”完全支撑不了决策。你没法回答三个问题钱花在哪个功能上哪个 Agent 技能成本最高哪个用户烧掉了最多的 token我记得有次和一位做 Agent 产品的朋友聊他说自己也知道成本高但说不清高在哪只能无差别地限制所有用户的使用次数。结果把重度付费用户也限了客户满意度直线下降。精细化核算本质上不是为了算账是为了让你能“基于成本做决策”是这个技能价值高值得保留还是这个技能设计有问题导致成本虚高没有分账数据你只能一刀切。4.2 计费埋点记录什么一张审计字段表我的做法是在 LLM 调用网关统一埋点每个请求生成一条计费审计记录字段表大致如下。这个表我调整过好几轮前四类字段是必须的后面两类是进阶。类别字段说明模型信息model、provider用于按模型单价核算Token 用量prompt_tokens、completion_tokens、total_tokens、cache_read_tokens核心计量数据业务标签user_id、session_id、trace_id、agent_name、skill_name用于按业务维度分账性能字段latency_ms、request_time用于联动性能分析结果字段error_flag、retry_count、finish_reason用于异常识别进阶字段app_version、prompt_template_version用于定位模板改动影响特别要强调 skill_name 这个字段它代表“这次调用是哪个具体技能触发的”。很多团队只记录 agent_name粒度太粗一个 Agent 里可能挂了十几个技能成本差异巨大。按技能分账后你才能真正看到原来“订单查询”这个技能单次成本是“天气查询”的 20 倍。4.3 成本如何算输入、输出、缓存读取要分开计价计费字段拿到之后成本计算本身也有一些门道。大多数模型服务商对输入 token 和输出 token 分别计价而且现在越来越多的服务商支持 prompt 缓存缓存命中的输入 token 价格远低于普通输入 token。如果核算时不区分这三类成本会严重失真。计算公式我建议这样写单次调用成本 输入token数未命中缓存 × 输入单价 缓存读取token数 × 缓存单价 输出token数 × 输出单价下面拿一个假设的模型服务商价目表举个例子仅用于演示算法实际单价请以你对接模型服务商的最新合同或官网价格为准。计费项单价示例每百万 token用量成本普通输入3 美元8,0000.024 美元缓存读取0.3 美元20,0000.006 美元输出15 美元1,2000.018 美元合计--0.048 美元同样的调用如果缓存命中率差把缓存读取的 2 万 token 记为普通输入成本就变成 0.060.0180.078 美元偏差超过 60%。所以缓存命中这个字段必须记录它是成本核算准确性的基石。4.4 三个高价值分析维度与一个成本黑洞案例有了明细数据我建议优先做三个维度的分析。第一个是按会话聚合找单会话 Token 特别高的异常会话。正常用户的对话平均几千 token突然出现一个 50 万 token 的会话大概率是上下文泄漏或者死循环这是最重要的成本异常信号。第二个是按 Agent 技能聚合做成本排行。排行 Top 的技能不一定是坏事但要计算单位成本对应的业务价值。第三个是错误率和成本的联合分析。高错误率配高成本是最危险的组合意味着系统在重复做无用功。我实际做过的一个客服 Agent 案例很典型。按会话维度看平均每会话成本 0.35 美元看起来完全正常。但按技能拆分后发现一个“订单查询”技能的单次成本高达 2.1 美元占了总成本的 42%。再钻取 trace 找到原因查询时使用了全文检索Top-K 设为 20每次把 20 份文档全塞进 prompt而真正有用的只有 1 到 2 份。调整后把 Top-K 降到 5引入缓存迭代了 prompt 模板单次成本从 2.1 美元降到 0.8 美元整体成本下降 37%。同样的业务量省下来的都是利润。5. 观测大盘的建设顺序与落地工具建议5.1 别一上来就铺 OpenTelemetry按这个顺序三步走很多团队看别人上了分布式追踪心痒痒立刻引入 OpenTelemetry 把 Agent 框架的每一个节点都埋一遍。我劝你先冷静一下。全链路埋点改动量巨大要动业务代码、要部署 Collector、要选存储后端通常还没看到效果业务团队就抗拒了。我建议倒过来按成本从低到高分三步走。第一步先做 LLM 调用网关的计量埋点。这一步不涉及业务代码只需要在统一网关函数里加日志当天就能上线当周就能看到 token 消耗成本全景。这笔投资回报率最高。第二步统一业务日志格式强制所有 Agent 任务在日志里输出 trace_id、session_id、task_id让日志服务能够按 ID 聚合。这一步也不需要引入新组件只是规范日志输出但能解决 80% 的“根据用户反馈定位问题”场景。第三步等团队意识到链路追踪的价值后再逐步引入 OpenTelemetry 做完整 Span 埋点覆盖 plan、tool、llm 等核心流程。这套顺序我在两个团队里都实践过第一阶段通常一两周就能完成但带来的感知非常强。相比之下直接铺 OpenTelemetry 的项目大部分三个星期后还在折腾 Collector业务方已经失去耐心。5.2 看板上的核心指标一张你抄得走的指标清单观测大盘不用做得很花哨关键是放对指标。我经过几轮精简现在团队看板上固定放下面这组指标每个指标背后都有具体的告警逻辑。指标计算粒度告警规则示例请求成功率全任务5 分钟成功率低于 95%P95 端到端延迟全任务环比上升超 30%LLM 调用错误率全任务错误率超过 5%Token 日消耗总量全任务环比上升超 50%缓存命中率全任务低于 60% 时预警工具调用失败率按工具单个工具失败率超 10%单会话 Token 消耗 P95按会话超过阈值触发检查平均单任务 Token 数按 Agent环比上升超 30%这里面有两个指标是我用很久之后才意识到重要的。一个是缓存命中率它直接反映 prompt 模板是否稳定模板频繁改动会导致缓存大量失效成本上涨但业务量没涨。另一个是单会话 Token 消耗 P95相比平均值分位数更能暴露“最差的那批用户”在烧多少钱。5.3 几个容易踩的坑和我的应对习惯最后分享几个我在实践里反复踩过的坑以及现在养成的应对习惯。第一个坑埋点本身拖垮了业务。LLM 调用的响应时间本身就慢如果你在装饰器里做同步写日志、同步上报追踪数据等于把慢路径又加长了。我现在一律要求异步上报日志先写本地缓冲区后台批量推送绝不在业务线程里做网络 IO。还有就是要给追踪 SDK 加熔断开关极端情况下观测系统自身故障不能影响业务。第二个坑只埋点不设预算熔断。Token 成本不像服务器费用是相对固定的一个 bug 可以让成本在几小时内翻几倍。我现在会在网关层设置“预算熔断”逻辑当某个 Agent 技能的日消耗超过设定阈值时自动降级到更便宜的模型或者暂停非核心技能优先保住核心链路。这个和传统的限流不太一样它的目的不是保护服务而是保护钱包。第三个坑prompt 模板的版本没有纳入观测。改 prompt 是调成本最快的方式也是最容易翻车的方式。我把 prompt_template_version 作为 Span 属性和计费字段记录之后就找到过一次很隐蔽的成本上涨一个运营同学改了 prompt 里的一个词导致缓存命中率从 85% 掉到 20%整体成本涨了 40%。如果没记版本号这种问题只能大海捞针。第四个坑过度采集导致存储成本失控。全量采集元数据听起来很好但一个 Agent 平台一天可能产生上千万条 Span存储成本也不便宜。我的习惯是 Span 元数据保留 30 天内容快照保留 7 天超过时间的自动清理。可观测性本身也是成本也要被纳入成本核算别把监控做成了新的账单炸弹。做 Agent 可观测性这段时间我最大的体会是这个领域还没有像 Web 监控那样的成熟标准很多方案要自己摸索但核心思路是清晰的——把 Agent 的执行轨迹和 Token 消耗当作第一等公民来观测。你先从网关计量埋点开始把成本账算清楚再逐步加链路追踪和诊断能力这条路是通的。至于要不要上 OpenTelemetry、选 Jaeger 还是 ClickHouse 当后端这些都是后话先把最基本的 trace_id 打通你就已经比大多数团队领先了。