AI Agent可观测性实战:从原理到落地
1. 为什么说 AI Agent 的可观测性是一道“硬骨头”这两年搞 AI Agent 开发的人越来越多了如果你问一个真正在线上跑过 Agent 服务的工程师什么是最让人头疼的环节十有八九不是模型选型也不是 Prompt 调优——而是“这玩意儿到底在干什么我完全看不到”。传统软件开发里的可观测性已经够折腾人了但至少逻辑可控、变量可查、栈信息可回溯。AI Agent 完全不是这么回事。它的运行逻辑本质上是循环推理加工具调用的组合大模型感知环境、生成下一动作、调用外部工具、解析返回结构、再思考再行动。这个循环里每一步都可能出问题而且很多问题还是概率性的——同一个输入今天跑通明天就挂GPT 这次调用正常下次就开始胡说八道。你没法靠“复现”来排查问题只能靠完整的观测链路来还原现场。我之前在团队里推进 Agent 项目时就吃过这个亏。前期上线时做的是最基础的日志打印结果一遇到线上用户反馈“Agent 回答得很奇怪”我打开日志一看好家伙只有一行行孤立的 LLM 调用记录和工具返回结果根本看不出它为什么要这么调用、为什么选了这个工具、中间经历了多少次失败的推理。完全是两眼一抹黑排查一个看起来不复杂的问题花了我将近四个小时。那次之后我悟出一个道理Agent 的可观测性和传统可观测性有着本质区别。我们不是只关心“系统健康度”和“请求成功率”而是要回答几个更深层的问题Agent 当前的运行处于哪个阶段是思考中还是调工具中模型每一步的输入输出是什么Prompt 到底传进去了什么工具调用的参数拼接是否正确返回信息有没有被模型正确理解整个过程耗时多长哪个环节是瓶颈用户的最终体验和内部动作之间是怎么关联的这几件事没一个是传统监控面板能直接告诉你的。所以这篇内容我就来好好拆一下AI Agent 的可观测性到底应该怎么做从底层原理到落地实操把我踩过的坑和沉淀的方法一次讲清楚。2. 先搞懂 Agent 的运行机制才能知道该观测什么2.1 Agent 的循环推理结构要想做好观测第一步是理解你的 Agent 到底是怎么“思考”的。目前主流的 Agent 架构基本都遵循一个模式模型与环境交互通过工具调用完成任务。简化来看就是一个循环接收用户输入系统 Prompt 与任务上下文组装完成大模型根据当前状态推理决定下一步动作如果动作是调用工具就生成结构化参数执行工具调用工具返回结果拼接到消息历史中模型拿到新上下文继续推理直到生成最终回复或达到最大轮次这个循环本身就是可观测性的天然抓手。每一个循环单元都产生三类关键信息模型请求的完整快照含 Prompt、工具的入参与出参、以及模型对工具结果的解读方式。这三类信息就是 Agent 可观测性的原始素材。很多开发者刚上手 Agent 时观测粒度停留在“HTTP 请求层面”。FastAPI 写个接口LLM 调一次日志里记下耗时和 token 数就完事。这在单轮问答场景下够用但放到真正的 Agent 场景中远远不够——一个复杂任务可能需要循环五轮、调用三个不同工具每轮之间还有信息依赖关系。你看到的“一次请求”背后是多次内部循环观测不到循环级别就等同于瞎。理解这个差异是做好 Agent 可观测性的第一步。我的经验是直接放弃“请求/响应”的思维模型改用“轨迹Trace”的思维模型。一次用户请求就是一条工作轨迹轨迹里包含若干个推理步骤和工具执行片段每个片段都有自己的父子关系、时间戳和内容快照。后面讲的 Tracing 系统本质上就是把这种思维模型变成了可运行的工程方案。2.2 哪些关键节点必须被记录搞清楚了循环结构接下来要明确“在哪些位置埋点”。我总结了一个算是我个人经验的清单覆盖了 Agent 运行的所有关键信息节点节点一初始输入快照。包括原始用户输入、系统 Prompt、模型参数temperature、top_p、max_tokens、模型版本、会话上下文长度。注意上下文长度这个信息非常重要Agent 跑多轮之后上下文膨胀往往就是从这个节点开始出问题的。节点二每一轮模型推理的完整 Prompt。这块很多人会偷懒只记录输入输出的摘要。我强烈不建议这么做。Agent 出问题一半以上的原因就是 Prompt 组装错误——工具描述没传进去、历史消息重复、系统指令被用户输入覆盖。没有完整 Prompt 快照这些问题根本没法定位。节点三模型的原始响应。不仅仅是最终文本还有工具调用的结构化参数。如果你用 OpenAI 的 function calling这里就要记录完整的 function_call 参数 JSON。我用 LangChain 的时候这块直接序列化 AIMessage 对象连同函数调用参数一起入库。节点四工具执行详情。工具名称、入参 JSON、出参结构、执行耗时、错误信息如果有。这里要注意工具返回结果的内容长度。有些工具比如搜索引擎返回几千字的网页摘要记录时一定要截断或者摘要化存 JSON不然后续分析时存储开销会非常大。节点五单轮循环的耗时拆解。模型推理耗时与工具执行耗时分开记。同样是耗时数据分开了才能看出瓶颈到底在自然语言理解上还是外部服务依赖上。节点六Agent 的终止状态。循环是因为什么结束的是模型判断任务完成、还是超过最大轮次被强制中断、还是异常退出这个信息很多日志系统不记录但它往往是用户投诉“Agent 没解决问题”的第一线索。这六个节点串起来就是一条完整的“Agent 运行 DNA”。把这份 DNA 记录下来想复盘任何一次劣质回答几乎都能找到根因。3. 可观测性的三大支柱在 Agent 场景下的变形3.1 Tracing用“轨迹思维”替代“请求思维”传统后端可观测性的三大支柱是 Metrics指标、Logging日志、Tracing链路追踪。到了 Agent 场景这三个支柱仍然有效但内涵和实现方式发生了显著变化。先说 Tracing。传统 Tracing 解决的是分布式系统中一个请求经过多个服务的调用链追踪问题核心结构是“Span 树”。Agent 场景同样适用这种结构但 Span 的语义需要做定制扩展。我在实践中把 Agent 的 Span 分成四类AgentSpan代表整个 Agent 会话的生命周期是最顶层的根 SpanLLMSpan一次大模型调用的全过程内部记录完整 Prompt、响应、token 统计ToolSpan一次工具调用的执行过程记录工具名、入参、出参、执行状态ChainSpan如果你用的是 LangChain那 Chain 作为编排节点也需要有自己的 Span 类型用来标记一个完整的处理管道这种分类方式对应了 Agent 运行的不同阶段每一类 Span 都有自己的属性集。比如 LLMSpan 里存的是 provider、model_name、prompt_tokens、completion_tokensToolSpan 里存的是 tool_name、tool_input、tool_output_status。关键点在于 Span 之间的关联。Agent 的循环结构决定了 LLM 调用和工具调用是交替嵌套的——第一个 LLMSpan 结束后跟着一个 ToolSpanToolSpan 结束后继续嵌套下一个 LLMSpan。这种嵌套关系必须忠实反映出循环的时序结构只有这样才能还原“模型先思考、再调工具、再基于结果思考”的完整链路。实现上OpenTelemetry 的 Span 机制天然支持这种嵌套。你在 Agent 主流程里开启 AgentSpan每次循环迭代里开启子 Span模型调用再往下开一层。跑完整个 Agent 流程你就得到一棵完整的 Span 树了。3.2 Metrics别只盯着 Token 数和延迟Metrics 这块是很多团队容易走偏的地方。常规监控平台给出的指标比如请求量、QPS、平均延迟、Token 消耗对 Agent 来说是“必要但不充分”的。这些问题当然要监控但只是 Agent 健康度的冰山一角。我认为 Agent 场景下真正有效的 Metrics 应该包含下面这些维度轮次分布Turn Count一次任务完成需要多少个推理循环。这个指标的分布形态非常能说明问题——如果大量用户的请求都在最大轮次附近被截断说明 Agent 的规划能力有问题或者工具返回的信息不足以让模型做出决策。工具成功率这个必须分工具监测。有些 Agent 应用了一个搜索工具但搜索结果经常被模型误解为无效返回这类问题在平均指标里看不出来一定要单拆。上下文使用率输入 token 占总上下文窗口的比例。这是个非常有价值的预警指标。当使用率超过 80%模型输出质量通常会有断层式下降但系统本身不会有任何报错。工具参数解析失败次数模型生成了工具调用请求但参数不合法导致解析失败这种事件虽然不会让请求直接挂掉不少框架会自动重试或者让模型自我纠正但会对用户体验产生负面影响统计起来也很有价值。Agent 的 Metrics 体系设计时有一个容易被忽视的心得要区分“技术指标”和“业务指标”。技术指标是模型延迟、token 数、工具响应时间——直接反映系统状态业务指标是任务完成率、平均轮次、用户修正请求的次数——反映 Agent 的实际效果。这两类指标收集方式不同分析时也必须一起看才能完整还原 Agent 的表现。3.3 Logging从“异常日志”转向“全量记录”说到 LoggingAgent 场景和传统后端有个很大的差异。传统后端的日志系统重在检测异常——错误日志、警告日志、超时日志。你要有一行 Error 才算需要关注的日志。但 Agent 的运行过程中大量所谓的“问题”根本不会产生传统意义上的错误。举个典型的例子模型在工具调用时选错了搜索关键词导致返回了不相关内容最后 Agent 基于错误内容生成了错误回答。这个过程中没有任何异常工具调用成功了模型也没有报错但你得到的结果是错的。从这个例子能看出Agent 场景下的日志系统不能只做“异常采集”必须做“全量语义记录”。模型的每个输入输出、每个工具调用的参数和返回值、每轮的组装消息历史都应该持久化存储。这给存储带来压力但这是 Agent 调试不可回避的代价。我在实际项目里的做法是分两级存储。热数据存在 ClickHouse 或 Elasticsearch 中保留最近 30 天支持在线检索排查。冷数据定期转储到对象存储里只保留核心字段用于后续的数据分析、模型评测、Prompt 迭代。这样做下来存储成本可控排查效率也有保障。有个细节值得一提日志记录时的上下文拼装顺序一定要保持与运行时一致。我遇到过因为日志记录时重新组装消息历史顺序错乱结果调试时看到的信息和实际运行信息不一致白白浪费了一上午排查那个根本不存在的问题。4. 落地实操用 OpenTelemetry 和 Langfuse 搭建一套可用的观测体系4.1 工具选型为什么我最终选了 OTel Langfuse 的组合铺垫了这么多理论现在说说怎么落地。工具生态方面目前业界已经有一些专门做 Agent/LLM 可观测性的方案比如 Langfuse、LangSmith、WB Weave、Phoenix 等。各有各的侧重点但对我来说工程化落地最顺的还是 OpenTelemetry 和 Langfuse 的组合。给出我的选型心路方便你参考单用 OpenTelemetry 的话它能提供完整的语义约定和传输管道但要把原始的 Trace 数据重构成“Agent 友好”的可视化界面开发成本高我团队也没必要为了画个界面专门投入开发资源。单用 Langfuse 这类商业/开源平台界面和 Agent 概念的结合非常紧密——可以直接看到模型的 Prompt、响应、Token 消耗、成本分析但如果你不止一个服务做观测想统一纳入整个微服务链路的追踪体系里就会比较封闭。所以现在的方案是双轨制Opentelemetry 负责链路数据的采集、传输和标准存储Langfuse 做 Agent 业务语义层的可视化和分析。OTel SDK 集成到 Agent 服务中生成标准 Trace 数据Langfuse 提供 Python/JS SDK专门记录 LLM 调用相关的语义数据两者在我的架构里是互补关系。技术上还有一个点值得提OpenTelemetry GenAI 语义约定。这是 OpenTelemetry 社区正在推进的一个标准把大模型请求、响应、Token 统计等字段标准化避免各家自己造轮子。虽然还没有完全成熟但方向已经比较明确。你如果从零开始建设直接参考这套语义约定来设计属性字段未来会少走很多弯路。4.2 最小化落地配置从零接入 Agent 服务下面用一段实际配置给各位做个演示场景基于一个典型的 LangChain Agent 服务from opentelemetry import trace from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter from opentelemetry.sdk.resources import Resource resource Resource.create( attributes{ service.name: order-agent, service.version: 1.2.0, deployment.environment: prod } ) provider TracerProvider(resourceresource) provider.add_span_processor( BatchSpanProcessor( OTLPSpanExporter(endpointhttp://otel-collector:4318/v1/traces) ) ) trace.set_tracer_provider(provider) tracer trace.get_tracer(__name__)设置完成后在每个关键的 Agent 运行节点手动埋点。这一步很关键尤其是用 LangChain 内置的 callback 记录工具调用时要同时在 OTel 的 Span 上打上对应的属性from opentelemetry import trace import json tracer trace.get_tracer(agent.tracer) with tracer.start_as_current_span(agent.run) as root_span: root_span.set_attribute(user_query, user_query) root_span.set_attribute(session_id, session_id) with tracer.start_as_current_span(llm.reasoning) as llm_span: llm_span.set_attribute(model_name, gpt-4o) llm_span.set_attribute(prompt_tokens, response.usage.prompt_tokens) llm_span.set_attribute(completion_tokens, response.usage.completion_tokens) llm_span.set_attribute(model_response, response.content) with tracer.start_as_current_span(tool.execute) as tool_span: tool_span.set_attribute(tool_name, es_search) tool_span.set_attribute(tool_input, json.dumps(tool_input, ensure_asciiFalse)) tool_span.set_attribute(tool_output, json.dumps(tool_output, ensure_asciiFalse)[:2000])这里有几个容易踩的坑提前说明。第一不要在整个 Agent 运行过程里只开一个 Span。很多初学者图省事把整个 Agent 的逻辑包在一个大 Span 里结果 OpenTelemetry 的 UI 上只看到一个耗时 10 秒的大块完全无法定位瓶颈。第二Tool 的输出记录必须做截断。有些工具返回内容轻松超过几千甚至上万字符直接全量打进 Span attribute 里可能导致 exporter 的 payload 超限丢失整条链路数据。我的做法是默认截断到 2000 字符必要时在数据库里存全量。第三Span Attribute 的值类型有限制。OpenTelemetry 的 attribute 只支持字符串、布尔、数字、数组这些基础类型结构化的 JSON 必须序列化成字符串再存。调试的时候注意别把复杂对象直接塞进去。4.3 更细粒度的观测在 LangChain 回调中注入自定义指标LangChain 是目前用的最多的 Agent 编排框架它的回调系统做了一层薄封装。你可以在回调里挂上自定义事件把需要观测的内部过程记录下来。下面是一个实际用过的回调注入例子from langchain_core.callbacks import BaseCallbackHandler from opentelemetry import trace class AgentObservabilityHandler(BaseCallbackHandler): def __init__(self): self.tracer trace.get_tracer(agent.chain) def on_llm_start(self, serialized, prompts, **kwargs): with self.tracer.start_as_current_span(llm.start) as span: span.set_attribute(prompt, prompts[0][:1000]) def on_llm_end(self, response, **kwargs): with self.tracer.start_as_current_span(llm.end) as span: span.set_attribute(llm_output, response.generations[0][0].text[:2000]) if response.llm_output: span.set_attribute(token_usage, response.llm_output.get(token_usage, {})) def on_tool_start(self, serialized, input_str, **kwargs): with self.tracer.start_as_current_span(tool.start) as span: span.set_attribute(tool_name, serialized.get(name)) span.set_attribute(tool_input, input_str[:2000]) def on_tool_end(self, output, **kwargs): with self.tracer.start_as_current_span(tool.end) as span: span.set_attribute(tool_output, str(output)[:2000])这套回调的价值在于不需要改动 Agent 的主体逻辑代码——只是把它挂到链上。LangChain 提供了相应的 handler 注册机制代码侵入非常小。而且能单独把 LLM 调用和工具调用摘出来做精细记录底层逻辑清晰。注册方式很简单agent create_agent(...) agent.run(user_input, config{callbacks: [AgentObservabilityHandler()]})需要注意一点回调示例中的 Span 是独立的它不会自动与最外层 Agent Span 形成父子关系。要想串起来需要你在初始化这个 handler 时就把它放在同一个 Trace 上下文里最简单的方法是把外层根 Span 的 context 传进 handler 构造函数然后用trace.use_span(context)重新绑定。5. 深度场景拆解从一次线上事故看观测数据的价值5.1 事故现场还原理论讲得再多不如来一个真实案例。去年我们上线了一个企业知识库问答 Agent底层接的是内部文档搜索引擎加向量数据库。某天用户反馈量突然变大说“回答质量明显变差经常给出答非所问的结论”。因为不是系统报错常规监控面板上看不到异常——CPU 正常、内存正常、接口 P99 延迟也没有明显波动。换了没做过 Agent 观测的团队这种问题几乎没法查。但因为我们当时已经把 OTel Trace 接到 Agent 上了直接把出问题的会话 ID 拎出来看这条 Trace 的 Span 树。结果一目了然。问题出在向量检索环节向量库近期新导入了一批新的文档但是 Embedding 用的模型版本不一致导致新文档的向量分布与旧文档完全不同。结果检索时用户问题相关的旧文档排在了很后面排在前面的全是语义不相干的新文档Agent 基于错误检索结果生成了回答自然答非所问。整个排查过程不到十五分钟。没有完整的链路追踪这个问题光靠日志和指标至少得折腾大半天还不一定查得到。5.2 事故背后的观测数据拆解来复盘一下到底哪些观测数据关键起了作用关键一LLMSpan 中的完整 Prompt 快照。我们直接看到了模型在推理时拿到的检索结果是哪几段文档发现这几段文档和用户问题之间几乎没有语义重叠。这一步把问题范围锁定在检索环节。关键二ToolSpan 中的检索参数。进一步检查时发现向量检索工具的入参里query字段是正确的但collection_name参数指向了那个混入不一致文档的新集合。关键三工具返回的 score 分布。排在前面的文档相似度分数普遍在 0.55 左右属于低置信度匹配。结合这一点基本可以判断向量索引本身的检索质量出了问题。关键四历史轨迹对比。把几个质量正常的会话和质量差的会话拉出来对比发现正常会话的第一轮检索结果里相似度分数通常在 0.8 以上。这个对比直接确定了问题的严重程度。这四个观测维度传统监控里一个都没有。模型调用耗时监控、接口错误率监控在整场事故里的价值趋近于零。这里额外补充一个经验Agent 的 Trace 数据一定要存够一定的量再做分析。单看一两个 Trace 看不出规律但把同类型问题的 Trace 拉开做横向对比规律往往就很清晰了。所以数据存储的成本不是白花的它是排查问题的基础设施。6. 常见问题与排查技巧实录Agent 可观测性建设过程中踩坑几乎是必然的。这里把我碰到过的问题按频率排个序给各位做个直接可查的速查表问题现象根本原因排查思路预防措施Trace 数据断链子 Span 找不到父 Span异步代码里没有正确传递 Trace 上下文检查异步任务启动时是否用context.attach恢复 Trace 上下文封装统一的异步任务入口强制传入 parent_span_contextSpan 数量爆炸存储成本飙升没有对工具循环做合并或采样查看单条 Trace 的 Span 分布定位高重复度 Span对工具执行 Span 开启采样策略相同工具同类参数只记录摘要Langfuse 界面看不到 OTel 的自定义 Span两套 SDK 各自为政没有打通检查 Langfuse SDK 初始化时的 trace 关联配置在代码层为 Langfuse 生成trace_id与 OTel 的统一关联字段排查问题时发现 Prompt 记录不完整只记录了最终 Prompt没有记录中间组装过程检查 LLMSpan 上下文的组装代码在每个 LLM 调用点打印完整的 messages 列表Agent 卡在工具循环中Trace 看起来一切正常模型对工具结果产生了错误解读反复重试查看同一轮循环的 LLM 输出看它是否误解了工具返回的错误提示在系统 Prompt 中加入更明确的工具返回格式指导另外有一个很实用的技巧想单独分享给每个 Agent 会话生成一个全局唯一的session_id让它在日志、Trace、数据库记录、前端反馈工单之间通用。当时我们在前端接入了用户反馈按钮提交问题时自动携带session_id。用户说“这个回答有问题”时后台直接输入这个 ID 就能拉出完整的 Agent Trace。这个设计让“用户主观反馈”变成可追溯的客观数据对产品迭代和模型优化带来了极大的帮助。还有个经验是埋点的标准统一问题。团队大了以后每个人写的 Agent 分支都给自己加属性字段命名混乱最后查询时非常痛苦。我们后来干脆把埋点属性定义整理成一份团队文档统一了字段规范如llm.model_name、tool.execution_status、agent.turn_index等代码评审时也检查埋点是否符合规范。这个管理上的小动作带来的协作效率提升是一本万利的。7. 实践经验之外的思考与建议Agent 可观测性这个领域还在快速发展中标准也在逐步统一。从我个人的实践体会看有几个方向特别值得持续关注。第一是评测驱动的可观测。目前很多团队的观测数据只是“事后诸葛亮”——问题发生了才去看。真正先进的做法是把观测数据用于高频的回归评测每个用例跑完后自动分析 Trace检测是否存在工具调用异常、推理轮次过多、上下文超限等情况如果出现则自动标记为失败并推送告警。这等于把可观测性从被动排障工具升级为质量守护系统。第二是多模态 Agent 的观测。现在 Agent 不只是处理文本了还会看截图、听语音、操作浏览器界面。这些非文本输入输出在观测系统里怎么结构化、怎么存储、怎么检索都是新的挑战。我们团队已经开始在内部试点把模型生成的中间视觉状态也纳入 Trace 记录但目前还没有形成成熟方案。第三是成本可观测性。Agent 的成本和传统 API 完全不同——一个任务可能调用模型五次、工具十次每个环节都有成本。把成本数据纳入可观测体系按用户、按会话、按功能模块拆解是预算控制和定价决策的重要依据。我见过不少 Agent 项目上线两个月才发现成本远超预估就是因为没建立 Token 粒度的成本观测。最后再分享一个我在实际项目里反复用到的技巧给 Agent 的每个主要路径分支打上“可解释标签”。比如用户请求是查询类任务标签就是intent.query如果中间触发了多轮工具调用就用path.toolchain标记。这些标签不直接反映异常但在大流量分析、用户行为画像、Agent 规划策略评估时它们提供的切片维度远比你想象的丰富。Agent 应用的质量保障说到底就是那句话看不到的东西就无法改进。一套完整的观测体系不只是在出问题时帮你排查它更大的价值在于让你每天都在和真实、高质量的运行数据打交道靠数据说话而不是靠运气和感觉迭代产品。希望大家都能把这块基础能力早日补上别等线上出了问题才想起要做可观测性。
