ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

AI Agent可观测性实战:从调用链追踪到故障排查的完整指南

AI Agent可观测性实战:从调用链追踪到故障排查的完整指南 做AI Agent开发这一年多我最大的感受不是模型能力不够而是“出了问题根本不知道问题出在哪”。传统后端服务请求进来、逻辑执行、数据库查询、响应返回每一步都能靠日志和链路追踪还原现场。Agent完全不一样——同一个Prompt今天调这个工具明天调另一个工具一次任务里可能夹杂着五六轮LLM调用、十几次工具执行、若干次向量检索中间任何一环出问题最终结果都可能看起来“挺正常但就是不对劲”。这也是为什么我越来越觉得AI Agent的可观测性不是锦上添花而是能不能把Agent从demo推进到生产环境的生死线。这篇文章把我自己从零搭建Agent可观测体系的过程、踩过的坑、沉淀下来的方案完整梳理一遍覆盖调用链追踪、状态快照、指标成本、工具选型、故障排查实战这几块内容。不管你是刚入门Agent开发还是已经在生产环境里被各种诡异问题折磨过几轮这篇都值得读完。1. 为什么Agent比普通应用更让人“失控”1.1 从“确定函数”到“非确定性推理”难度完全不是一个量级很多人刚开始写Agent时容易把它当成一个“更智能的接口”给个输入拿个输出完事。真正跑起来才知道Agent本质是一个会自己决定执行路径的推理系统。它内部有计划、有反思、有工具选择、有记忆检索每一步都在动态变化。同一个问题上午问和下午问走的路径可能完全不同。比如一个负责订机票的Agent第一次可能先查天气再比对价格第二次可能直接调API查航班。路径不一样出问题的点就不一样。传统服务是确定性代码同样的输入必然走同样的代码分支日志打在哪一行基本能猜出来。Agent是模型在每个决策点做选择断点在哪、走没走岔路不埋点根本看不见。这正是可观测性在Agent场景下变得极其困难的原因。传统可观测性解决的是“已知路径上的异常”而Agent要解决的是“未知路径上的漂移”。1.2 失败模式变了从“崩溃”变成“悄悄跑偏”传统系统出故障表现很直接报错、超时、接口500、数据库连接失败。Agent的失败几乎所有都是静默的。我见过最典型的一个案例一个客服Agent某天开始频繁给用户推荐完全不相关的产品。代码没改模型没换数据没动看起来一切正常就是回答质量变差了。排查下来才知道是上游知识库某个文档被误删除向量检索结果质量下降Agent拿到错误上下文后“自信地”编了一个答案。这种问题没有异常堆栈没有错误码只有结果与预期的偏差。想发现它必须把Agent内部的每一次决策、每一条检索结果、每一轮工具返回都记录下来才可能在事后回溯到底是哪一步引入了偏差。所以我理解的Agent可观测性不是一个单一工具而是一套围绕“推理轨迹”展开的记录、还原、量化体系。它要回答的问题很朴素这个Agent刚才到底想了什么、做了什么、为什么这么做以及做得好不好。2. 三层观测体系把Agent拆开看清楚2.1 第一层调用链追踪Tracing先解决“做了什么”调用链追踪是最基础的一层解决“一次任务请求从头到尾调用了哪些东西”的问题。传统微服务里的Trace是一棵树A服务调B服务B服务再调C服务节点确定关系清晰。Agent里的Trace树结构类似但节点类型更复杂。我一般把Agent的Span分成几类agent_step一次思考决策、llm_call模型生成、tool_call工具执行、retrievalRAG检索、user_message用户输入、system_message系统提示或中间结果。每个Span记录自己的父Span ID这样就能还原出完整的决策树。举个实际例子用户问“帮我查一下明天上海到北京的高铁然后订最早那班”。Agent的Trace树大概是user_message用户原始请求agent_stepAgent决定先查询车次tool_call调用高铁查询APIretrieval如果Agency有价格政策文档可能还会检索内部规则agent_stepAgent决定下单tool_call调用订票接口llm_call生成给用户的确认话术这套树结构一旦能完整还原就等于拿到了Agent执行的“行车记录仪”。哪个环节耗时最长、哪个工具被反复调用、哪个分支走错了一眼就能看出来。2.2 第二层内部状态与轨迹快照Trajectory看清“为什么这么做”调用链只能告诉外部发生了哪些调用但Agent内部“思考”的过程同样关键。这一层我习惯称为轨迹快照核心是记录模型每个决策点的输入输出上下文。具体要做三件事。第一记录每次LLM调用的完整Prompt和Completion。这里的“完整”指消息列表包括System、User、Assistant历史、工具返回结果而不是只记一句话。第二记录模型决策后的下一步动作比如模型输出了一段JSON说要调用某个工具那就把这JSON原样存下来。第三记录上下文窗口的占用情况包括当前轮次用了多少Token、还剩多少空间、历史消息被压缩或截断过没有。有人会问LLM调用日志不是已经有了吗有但OpenAI或通义等模型的调用日志只记录请求和响应Agent内部对历史消息的裁剪、重写、摘要合并这些是发生在应用层的模型侧看不到。而这些恰恰是Agent“精神分裂”的高发地。我遇到过一个很经典的场景Agent在长对话中突然忘记用户之前的偏好。查轨迹快照才发现中间某轮为了控制Token长度系统把早期对话做了摘要压缩结果摘要把“用户不吃辣”这个关键信息丢了。没有轨迹快照这种问题只能靠猜。2.3 第三层指标、成本与质量量化回答“做得好不好”有了Trace和轨迹只解决了定性问题还得有定量的指标来衡量Agent的运行健康度。我日常最关注五类指标。第一类是成本指标每次请求的Token消耗、模型单价、工具调用费用。第二类是性能指标包括首token时延、整体响应时延、工具调用耗时、向量检索耗时。第三类是成功率指标工具调用成功占比、JSON解析失败占比、重试次数、达到最大迭代次数的占比。第四类是质量指标用户反馈点赞点踩、人工接手率、任务最终完成率。第五类是资源指标上下文窗口占用率、历史消息被压缩的比例。这五类指标分开看各有价值合在一起能发现很多隐藏问题。比如一个Agent工具成功率只有60%但用户满意度并不低因为Agent失败后会自我纠错重试最终结果还行。这时候只看结果指标发现不了问题只有把工具调用失败率单独拉出来才知道底层工具接口其实已经不稳定了。3. 落地实操给Agent做“全身CT”的关键埋点3.1 请求级唯一ID与上下文传播埋点的地基所有可观测性方案的第一步都是给一次完整的Agent任务分配一个全局唯一的Trace ID。用户点一个按钮后端可能触发多次LLM调用、多次工具调用甚至异步任务所有Span必须带着同一个Trace ID才能串起来。ID的传播在单体应用里比较简单用上下文变量传递就行。但很多Agent系统已经拆成了多个服务网关服务负责接收请求规划服务负责决策工具服务负责执行外部API。这时候就需要把Trace ID塞进每个内部请求的Header或消息体。我习惯在入口处生成trace_id然后通过一个统一的上下文管理器传递类似这样# context.py 简单的Trace上下文传递示例 import contextvars import uuid current_trace contextvars.ContextVar(current_trace, defaultNone) def new_trace(): trace_id uuid.uuid4().hex current_trace.set({trace_id: trace_id, spans: []}) return trace_id def get_trace_id(): ctx current_trace.get() return ctx[trace_id] if ctx else None注意一个容易被忽略的点Agent如果有重试逻辑或异步任务要确保重试和子任务复用同一个Trace ID。我见过不少团队因为异步回调时忘了传递trace_id导致一次Agent任务在观测平台上被拆成了好几个互不关联的孤儿请求。3.2 四类核心事件的埋点设计具体记什么字段跑通ID传播后就要开始埋事件。我总结下来Agent系统最少要埋四类事件每类事件的字段设计都是有讲究的。第一类agent_step事件记录Agent的每一次决策循环。核心字段包括当前步骤序号、决策前的推理内容、决策结果类型tool_call / reply / finish、可选的错误信息。这个事件的价值在于还原Agent从头到尾的“心路历程”判断它是不是在某些问题上反复纠结。第二类llm_call事件记录每次大模型调用的细节。字段包括模型名、版本、temperature、max_tokens、输入消息列表截断策略要提前想好、输出内容、token使用明细。这里我要特别提醒输入消息列表原始内容可能很大直接全量存储成本很高建议默认截断到每个消息前2000字符同时做一层敏感信息脱敏把密钥、手机号、身份证号替换成占位符。第三类tool_call事件记录工具调用的开始、结束、成功、失败。字段包括工具名、入参、出参、耗时、状态码、错误信息。工具调用的入参出参最容易踩坑Agency内部服务还好如果调用了外部第三方API返回体可能非常庞大我建议设置一个最大记录长度比如10KB超过部分用[truncated]标记避免把存储打爆。第四类retrieval事件专门给RAG场景用。字段包括检索的query、检索到的文档ID列表、每个文档的得分、最终被组装进上下文的文档列表。这个事件对排查“为什么模型没参考正确的知识”至关重要。3.3 成本与Token占用的计算口径别算糊涂账Token成本是Agent上线后最容易被低估的问题。我接手的不少项目初期都只统计模型返回的usage里的total_tokens但他们忽略了几个关键点。第一Agent可能一轮对话里多次调用LLM所以成本要看单次完整任务累计而不是每次调用单独看。第二不同模型甚至同一模型不同版本的价格差异很大最好把单价配置化统一计算。第三缓存Token要单独统计。很多平台现在支持prompt缓存缓存的成本远低于重新计算这部分不单独统计会让成本估算失真。我给团队定的成本统计公式是这样的# cost.py 成本计算示例 def calc_cost(model, usage): price PRICE_TABLE[model] # 形如 {input: 0.001, output: 0.002, cache_read: 0.0001} return ( usage.get(prompt_tokens, 0) * price[input] usage.get(completion_tokens, 0) * price[output] usage.get(prompt_tokens_details, {}).get(cached_tokens, 0) * price[cache_read] ) / 1000价格表建议用一个配置文件或者配置中心维护调价的时候全局生效。成本统计是后续做预算治理的前提没有精确的成本口径谈“控制Agent成本”就是空话。4. 工具选型开源平台和自研怎么选4.1 主流可观测性方案横向对比各有各的脾气现在市面上Agent可观测性方案已经不少我实际用过和深度调研过的有四个方向全托管平台、开源自托管平台、通用可观测平台的GenAI扩展、完全自研。全托管平台以Langfuse、LangSmith、Arize Phoenix云版、Helicone为代表特点是接入快SDK一装埋点事件自动上报自带可视化面板、评测工具和团队协作功能。LangSmith和LangChain生态绑定深如果项目就是LangChain写的接入成本极低。Langfuse则更中立OpenAI、Claude、LlamaIndex、LangChain都支持社区活跃开源版可以自己部署。开源自托管方案典型是Langfuse自托管版和Arize Phoenix单机版适合对数据敏感、要求数据不出内网的团队。Phoenix对本地开发特别友好Jupyter里就能跑调试阶段拿来分析轨迹很好用。通用可观测平台的GenAI扩展是另一个方向比如OpenTelemetry已经有一套GenAI语义约定可以把LLM调用、向量检索用标准化的Span记录再接入Jaeger、Grafana Tempo这类链路系统。好处是跟原有微服务观测体系统一坏处是面向Agent的轨迹分析、评测功能几乎没有全得自己开发。我整理了一张对比表供参考方案接入成本数据管控轨迹分析评测能力适用场景LangSmith低云端强强LangChain项目、快速验证Langfuse低可自托管强中中立生态、私有化需求Arize Phoenix中可自托管中中本地调试、开源偏好OpenTelemetry扩展高完全自控弱无已有统一监控体系的大厂完全自研很高完全自控取决于自己取决于自己深度定制、规模化治理4.2 自研采集层和接现成平台判断标准就三条很多团队在自研和接入之间反复纠结我给三条判断标准。第一你的Agent链路是否高度定制。如果Agent用的是LangGraph这类框架现成SDK能覆盖大部分埋点需求接现成平台省时省力。如果你的Agent是完全自己写的决策循环平台SDK识别不了你自定义的组件埋点代码一样要手写那自研采集层和接平台的成本差距就不大了。第二你的数据是否敏感。模型输入输出里可能带业务机密如果公司规定数据不能出内网自托管或自研基本是唯一选择。我见过有团队用云平台结果法务评审直接不通过最后推倒重来。第三你的复盘流程是否依赖深度定制。如果要做复杂的轨迹聚类、行为对比、Prompt回归测试现成平台的通用功能往往不够大概率还是要自己开发数据分析模块。这种情况下平台的价值更多在可视化展示和存储分析逻辑还得自己写。我的建议是起步阶段无脑选一个成熟平台比如Langfuse先把观测跑起来。等到你真的需要做深度定制分析再考虑把采集层抽出来自研。不要一上来就自研Agent迭代这么快你花一个月写观测系统业务早就领先你三个版本了。4.3 一套最小可用的埋点代码直接照着改下面给一套最简埋点实现的思路不依赖特定平台核心是自定义Span管理器可以后续替换成任意后端存储。# agent_observer.py 最小可用的Agent观测器 import json import time import uuid from contextlib import contextmanager class AgentObserver: def __init__(self, trace_idNone): self.trace_id trace_id or uuid.uuid4().hex self.spans [] self.stack [] contextmanager def span(self, span_type, name, **attrs): span_id uuid.uuid4().hex parent_id self.stack[-1] if self.stack else None start time.time() self.stack.append(span_id) try: yield self.spans.append({ trace_id: self.trace_id, span_id: span_id, parent_id: parent_id, type: span_type, name: name, start: start, end: time.time(), status: ok, **attrs, }) except Exception as e: self.spans.append({ trace_id: self.trace_id, span_id: span_id, parent_id: parent_id, type: span_type, name: name, start: start, end: time.time(), status: error, error: str(e), **attrs, }) raise finally: self.stack.pop() def dump(self): return json.dumps(self.spans, ensure_asciiFalse)使用方式很简单在Agent主循环里包一层# agent.py 调用示例 observer AgentObserver() def run_agent(user_query): with observer.span(agent_step, plan, queryuser_query): plan llm_call(...) # 实际生成计划 with observer.span(tool_call, search_train): result search_train(plan[departure]) with observer.span(llm_call, final_reply): reply llm_call(..., contextresult) return reply这套代码解决了最核心的“能记录、能串联、能回溯”问题。接后端存储的时候把dump()改成批量写入即可。数据量上来后建议加个缓冲队列不要每条Span都同步写库容易拖慢Agent响应速度。5. 高频故障模式与排查实录5.1 别只会看错误日志Agent故障模式速查表Agent的故障不像传统系统那样集中在“抛异常”上我把实际中高频出现的故障模式整理成了一张速查表每类都对应可观测的线索和排查方向故障表现观测线索大概率原因Agent反复调用同一个工具轨迹中同工具连续出现多次工具返回不满足条件Agent一直重试回答正确但Token消耗异常高单次任务LLM调用次数超过正常值决策循环缺乏退出条件陷入无效迭代答非所问上下文无关检索文档得分都很低RAG检索质量差或召回链路配置错误突然忘记历史信息轨迹快照显示历史消息被摘要压缩上下文压缩策略过激进重要信息丢失工具调用报JSON解析错误模型输出的JSON格式非法模型输出不稳定或temperature过高长任务中途静默停止最后一个Span后没有后续Span达到最大迭代次数或被超时强制中断同一问题不同天结果差异大轨迹对比发现决策路径漂移上游数据或Prompt被改动未做回归测试这张表我贴在工位上看了很久排查问题时的第一反应都是先对号入座。5.2 实战排查一Agent重复调用工具陷入死循环有个内部问答Agent上线后运营反馈它经常“卡住”一个问题要等很久才回答。我去观测平台拉轨迹发现了一个典型模式tool_call连续出现六次而且调的是同一个库存查询接口参数几乎没变。第一反应是看工具返回了什么。轨迹里存了每次调用的出参发现接口一直返回“库存不足请稍后再试”。Agent拿到这个结果后没有停止也没有换策略而是继续用相同参数重试。原因很快定位规划模块给Agent的指令是“必须保证下单成功”且没有设置最大重试次数模型在“必须成功”的强约束下选择了无脑重试。修复方案做了两处。第一在工具层增加结果分类明确告诉Agent“库存不足”属于终态失败不要再重试。第二在Agent循环里加硬性最大重试次数超过3次就停止并上报用户。这两处改动很小但效果立竿见影。没有轨迹数据这种问题根本不可能发现——普通监控里它只是“响应时间变长”而已。5.3 实战排查二费用异常飙升谁偷走了Token另一个项目上线一周后账单出来吓一跳成本是预估的三倍。我去查观测指标先看单次任务的平均Token消耗发现从上线第二天的2万Token一路涨到第三天的6万Token。拉出轨迹对比发现问题出在对话历史管理上。项目用了OpenAI的Responses API会自动累积历史消息但系统的历史清理策略有bug导致每次LLM调用都带着前几轮全部原始消息。Agent每次决策都要重放完整对话历史Token消耗自然指数级上涨。我当时的处理是加了一层历史消息管理超过一定轮次的早期消息自动摘要化同时把工具返回的大段内容截断后存入历史而不是原样塞回去。这个改动让单任务Token消耗从6万降回2.5万。另外我把成本指标加到每日监控告警里超过阈值就提醒避免下个月再来一次“账单惊吓”。5.4 排查方法论的通用套路三步定位问题踩了这么多坑我总结出一套通用的Agent问题排查流程。第一步看轨迹树还原现场。打开观测平台先看整体决策树确认Agent做了什么、调用顺序对不对、在哪一步开始异常。第二步对比正常与异常的轨迹。拉出历史成功案例和当前失败案例做比对找到分叉点异常几乎都是从一个不同的决策点开始的。第三步定位输入差异。到了分叉点对比该步骤的输入Prompt、工具返回、检索结果看是从哪里开始出现偏差。这套流程适合绝大多数Agent问题。它背后的逻辑是Agent的所有行为都由“输入上下文”和“内部决策”驱动上下文能通过轨迹还原决策能通过模型输出还原两者一对照问题基本就浮出水面了。6. 观测数据不只是用来“修Bug”6.1 用轨迹数据做回归评测Agent迭代不再提心吊胆Agent迭代最怕的是“改了这个坏了那个”。传统单元测试在Agent面前很无力因为输出是概率性的没法做严格断言。但有了可观测性沉淀的轨迹数据就能做一套更务实的回归评测。我的做法是每次上线新Prompt或新工具前先准备一批历史真实请求跑一遍并记录每条请求的完整轨迹快照。等新版本上线后用同一批请求再跑一遍对比两版轨迹的差异。对比的重点不是最终答案是否一字不差而是三条决策路径是否合理、成本是否增加、关键动作是否缺失。这套方法本质上是用历史轨迹当“测试用例”。它有局限不能覆盖没见过的场景但至少能挡住最常见的回归问题。我现在已经把轨迹快照纳入了CI流程每次Agent配置变更都强制跑一遍差分对比比人肉测试可靠得多。6.2 从观测到治理预算、护栏与体验优化观测数据沉淀多了之后它的价值就会从“被动排查”升级为“主动治理”。我自己用的最顺手的是三类治理场景。第一类是成本治理。有了精确的单任务成本数据就能给不同业务设定不同的成本预算超过自动降级到小模型或简化推理步骤。第二类是护栏治理。通过检索成功率、工具失败率这些指标可以反过来约束Agent行为比如检索得分低于阈值就强制拒绝回答而不是硬着头皮编造。第三类是体验优化。从轨迹里能统计用户最常触发哪条路径、最常在哪个环节放弃等待据此优化Agent的步数和响应速度。这三类治理能力都建立在“可观测数据完整、准确、及时”的基础上。没有数据治理就是空谈。这也是我反复跟团队强调的观测体系不是成本中心它本质上是Agent系统的驾驶舱仪表盘。6.3 我踩过的三个坑写下来免得大家再踩第一个坑是埋点力度失衡。一开始恨不得把每个变量都记录下来结果存储成本爆炸查询变慢大家都不爱用了。后来我只保留“决策点”和“外部交互点”两级埋点内部临时变量不进观测系统。观测的目的是还原现场不是备份全部内存。第二个坑是忽略了敏感信息处理。早期直接把完整Prompt和工具返回写入观测系统等安全评审才发现里面可能包含用户手机号。后来加了一层脱敏处理在写入前用正则和NLP规则识别并替换敏感字段。这个一定要前置等出了问题再补代价非常大。第三个坑是观测系统本身不够稳。Agent架构调整后旧埋点经常失效观测平台上一片空白等于没有。我后来给观测SDK加了自检告警如果一段时间没收到某个Agent实例的心跳就主动报警。观测系统本身也需要观测这个道理花了很长时间才彻底想明白。回到开头那句话Agent的不可预测性决定了它必须被仔细“解剖”。可观测性不是某个团队的专属工作而是每一个认真把Agent推向生产环境的人都绕不开的基础设施。先把调用链打通再把轨迹记全最后让数据反哺迭代这条路走通了Agent才能真正从玩具变成可靠的生产工具。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进