ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

多Agent系统可观测性实战:从埋点设计到故障排查的完整方法论

多Agent系统可观测性实战:从埋点设计到故障排查的完整方法论 我们团队最近把三条业务线全部迁到了基于大模型的多Agent协作架构上系统跑起来的那一瞬间所有人都很兴奋。但等兴奋劲过了真正让人头疼的事情才刚开始系统在测试环境一切正常一到生产环境就开始“抽风”某个Agent偶尔会把关键字段写错另一个Agent在特定上下文下会重复调用工具还有一次整个链路直接卡死既没有报错也没有超时就像凭空消失了一样。事后排查花了一整晚最后发现是一个Agent把返回结果吞掉了——它以为自己成功了实际上下游根本没人接住。这类问题只靠看日志和拍脑袋已经完全搞不定了。多Agent系统的复杂程度远不是传统单体应用或者普通微服务能比的。今天这篇我就把我们在构建多Agent系统可观测性这件事上的完整思路和实践经验整理出来包括我们踩过的坑、反复试错之后沉淀下来的监控方案以及一套可以直接抄走的调试方法论。1. 为什么说Multi-Agent系统是最难调试的一类应用1.1 状态分叉同一份输入五种路径五种结果传统软件的调试核心逻辑是“确定性”。同一份输入只要代码没变输出基本是稳定的。就算有并发、有竞态也可以通过日志、断点、链路追踪把问题定位到某一个函数、某一个线程。但多Agent系统完全不是这样。Agent的每一步都可能基于大模型的概率输出加上上下文窗口的波动导致同一份输入五次运行系统可能走出五条完全不同的路径。第一次Agent A直接把答案算出来给了用户第二次A觉得信息不足转而调用了BB又拉了外部API最后绕了一大圈才回来。路径不同耗时不同中间经过的节点也不同。这种不确定性带来的是传统“跟着代码执行”的调试方式全盘失效。你没法在Agent内部打一个断点因为这条路径下次运行根本不会重现你也没法说“这段逻辑错了”因为Agent可能压根没有走那段逻辑。这时候能依赖的只有一层层全链路记录下来的“真实执行轨迹”——从用户请求进入系统开始到内部所有Agent的决策、动作、工具调用、结果返回再到最终响应输出每一步都要可回放、可查看。这就是可观测性最基础、也最核心的诉求。1.2 长链路依赖链条越深信息衰减越严重多Agent系统另一个让人头疼的规律是链路越长信息在传递过程中的损耗就越严重。举个例子。用户问“帮我查一下上周华东区的销售数据并对异常情况做个分析”。这个请求进来之后首先由规划Agent拆解任务拆成了“查数据”和“做分析”两个子任务然后下游的检索Agent去数据库里把原始数据拉出来接着分析Agent基于这批数据做解读最后汇总Agent把结果整理成自然语言回复给用户。问题是最初用户的需求里隐含了很多背景信息——什么叫“异常”是环比下降还是同比恶化是只看销售额还是要连带退货率这些语义经过Agent A的解读传给Agent B时可能已经丢了一半等B再传给C时剩下的那点信息又被吃了不少。最终呈现给用户的答案看起来结构完整但里面分析的角度和用户预期的已经偏了十万八千里。这种信息衰减如果你只盯着最终的输出是绝对发现不了问题的——因为整个链路上没有任何一个环节真的“错了”每一步看起来都合理。唯一的办法是把所有Agent在每一层收到的输入、生成的中间结论、传给下游的信息全部记录下来。真实环境里排查一次这类问题你就知道一个能展开看每一跳请求内容的追踪面板有多重要。1.3 传统监控手段失效的本质原因我强调一个观点不是Prometheus不好用也不是ELK不好用而是它们是给“确定性的系统”设计的。这类工具假设你事先知道要监控什么指标、记录什么日志然后基于这些稳定的信号做告警和排查。但多Agent系统恰恰违背了这个假设。一次故障可能不是由一个明显的错误触发的而是Agent之间的一次协作失误、一个上下文的覆盖、一次工具调用的异常返回甚至模型自己生成的回复格式突变导致的解析失败。这些问题事前很难预判到具体是哪一行代码、哪一个接口——你在写监控方案的那一刻根本不知道该埋哪个点。所以多Agent系统需要的不是“更多更好的监控工具”而是一套能覆盖整个执行过程、能把不确定性变成可追溯信息的观测机制。这也是行业里反复强调“可观测性”Observability而不是“监控”Monitoring的原因之一。2. 可观测性建设先想清楚要记录什么2.1 “三个支柱”到了Multi-Agent场景该怎么理解可观测性领域的经典模型是“三大支柱”日志Logs、指标Metrics、追踪Traces。到了多Agent场景这三根柱子都得存在但含义要重新理解。日志Logs仍然是最基本的事件记录但不再是“用户登录成功”这种应用日志而是Agent的每一个决策动作、每一次提示词构造、每一次工具调用、每一段模型返回的原始内容。日志最关键的是要记录原始的未加工信息方便事后回溯“这个Agent当时到底看到了什么”。指标Metrics则是用来反映系统整体健康度的量化数据。比如Agent平均响应时间、工具调用成功率、Token消耗速率、Agent上下文占用率、循环调用次数等等。这些数据用来回答“系统当前是不是健康的”适合做告警。追踪Traces我认为这是Multi-Agent可观测性里真正核心的支柱。它记录一次完整请求在系统内部经过的所有节点、每个节点的耗时和结果、节点之间的依赖关系能把整个链路的执行路径还原出来。没有追踪多Agent系统出了问题基本只能靠猜。2.2 容易被忽略的新维度决策与推理的可观测性日志、指标、追踪之外多Agent系统还有一个其他软件领域不太涉及的维度决策过程的可观测性。传统程序“怎么做”是写死的源代码就摆在那里。但Agent的决策是基于大模型推理的你在代码里看不到它为什么选择调用A工具而不是B工具。缺乏决策记录你只能看到“它做了什么”看不到“它为什么这么做”这对排查潜在风险、评估输出质量非常致命。我们的做法是在每个Agent执行时把关键的推理摘要也记录下来。比如规划Agent拆解任务时把原始任务、拆解出的子任务列表、每个子任务分派给哪个Agent、为什么要这么分派全部记录下来再比如某个Agent在决定是否调用工具时把它看到的上下文摘要、可用的工具列表、它最终的选择和触发原因都落到日志里。这样做的好处是排查问题时我们不仅能看出“哪一环出了错”还能还原“这个Agent当时的判断依据到底是什么”很多莫名其妙的Bug一看到当时的推理过程原因立刻就清晰了。2.3 要记录什么一套可复用的观测字段清单基于我们的实践一套可落地的观测字段可以拆成这样几类请求基本信息请求ID、用户会话ID、时间戳、入口Agent名称、完整输入内容。Agent执行信息当前Agent名称、角色定位、本轮目标、从上游接收到的输入、本轮生成的中间输出、传给下游的输出、执行耗时。推理决策信息关键的Prompt摘要、Agent的决策理由或推理摘要、可用工具列表、最终选择的动作。工具调用信息工具名称、入参、返回值、异常信息、单次调用耗时、重试次数。上下文相关信息该Agent当前维护的上下文长度、消息历史摘要、上下文截断/压缩事件。性能与成本信息Token消耗输入/输出、模型延迟、总耗时、费用估算。这份清单可以直接当成一份伪标准来用。我们初期也是靠这份清单来设计数据库表结构和日志格式的。3. 落地实践一套基于OpenTelemetry的Multi-Agent监控方案3.1 技术选型为什么最终选了OpenTelemetry市面上其实没有现成的“多Agent系统监控全家桶”大部分方案都得自己拼。我们的选型结论是核心用OpenTelemetryOTel指标用Prometheus日志用ELK追踪可视化用Jaeger和Grafana。OpenTelemetry目前已经是可观测性领域事实上的标准了它最让我看重的是“统一埋点API”这个特性。不管底层是Python、Go还是Node.js写的Agent都可以用同一种方式产生Span和Trace数据格式也统一后续不管是换追踪后端还是接自家平台都很方便。当然如果项目工期紧、不太想自己搭全套也可以先用托管型APM比如市面上常见的那些APM服务或者LangSmith这类专门面向LLM应用的可观测平台先跑起来比什么都重要。但对我们来说数据安全和控制力是硬要求所以我们选择自建。3.2 埋点设计在Agent的骨架里做统一打点多Agent系统的埋点最大的坑是“不知道埋哪里”。我的经验是不要按业务逻辑去埋而是按Agent的执行骨架去埋。什么叫执行骨架所有Agent不管内部逻辑多复杂本质上都逃不开这几个阶段接收输入 → 理解任务 → 决策规划 → 执行动作含工具调用→ 产出输出 → 传递结果。我们就在这个公共骨骼上统一埋点写了一个包装器/装饰器所有Agent的execute方法都套上它自动生成Span。核心埋点逻辑的伪代码Python示例大致长这样from opentelemetry import trace from opentelemetry.trace import Status, StatusCode import json tracer trace.get_tracer(agent.tracer) def trace_agent(agent_name): def decorator(func): def wrapper(self, *args, **kwargs): with tracer.start_as_current_span(fagent.{agent_name}) as span: span.set_attribute(agent.name, agent_name) span.set_attribute(agent.input, truncate(json.dumps(kwargs.get(input, ), ensure_asciiFalse), 2000)) # 记录开始时间 start time.time() try: result func(self, *args, **kwargs) span.set_attribute(agent.output, truncate(json.dumps(result, ensure_asciiFalse), 2000)) span.set_status(StatusCode.OK) return result except Exception as e: span.record_exception(e) span.set_attribute(agent.error, str(e)) span.set_status(StatusCode.ERROR) raise finally: span.set_attribute(agent.duration_ms, (time.time() - start) * 1000) return wrapper return decorator实操中要注意几个点输入输出要塞进Span属性时务必做截断不要无脑全量放进去。有些Agent的上下文可能有几十万字符全塞进追踪系统存储压力很快就爆了。Tag和Attribute的命名一定要形成团队规范统一用agent.xxx、tool.xxx这种前缀不然以后检索会很难受。异常不要只记录要把调用栈、当时的输入、模型返回原文都一起记下来。3.3 核心数据结构用Trace把“跨Agent调用链”串起来多Agent系统追踪的难点不在于单个Agent的Span怎么记录而在于如何把多个Agent的Span串成一条完整的Trace。比如Agent A在规划后决定调用Agent B那Agent B的执行就应该作为Agent A的一个子Span存在这样在Jaeger里才能看到A下面挂着一个B、然后B下面又挂着一个工具调用一整棵调用树。原理上跟微服务的分布式追踪一样靠的是Trace ID和Parent Span ID。但多Agent场景有一个特点要注意Agent之间的调用不是在HTTP Request里完成的而是在同一个进程内通过函数调用或消息传递完成的。进程内传递上下文需要把当前的Span对象手动传给下游或者用一个全局的Context变量。用OpenTelemetry的Context大体是这个模式from opentelemetry import context as otel_context from opentelemetry import trace # Agent A在调用下游Agent之前把当前Context注入到一个消息对象里 def call_downstream(payload): ctx otel_context.get_current() payload[otel_ctx] ctx # Agent B在启动时从消息里恢复Context def handle_downstream(payload): if otel_ctx in payload: token otel_context.attach(payload[otel_ctx]) try: # B自己的逻辑 pass finally: if otel_ctx in payload: otel_context.detach(token)如果你用的是LangGraph这类成熟的Agent编排框架它们通常已经内置了对回调和追踪SDK的支持实现思路是类似的但不需要你手工传递Context。这里有个容易踩的坑如果Agent之间是通过异步任务队列比如Celery传递的OTel的Context是跨不过去的。要么你手动把Trace ID和Parent Span ID塞进任务消息里在Worker端重新建立Span关系要么直接用OpenTelemetry的跨进程传播机制W3C Trace Context。我们早期就是没处理这块导致所有异步Agent的Trace是断的排查起来相当痛苦。3.4 追踪数据怎么可视化才“好用”Trace数据收集上来之后需要一个能直观查看调用链的界面。我们用的是JaegerGrafana里也配了Tempo做备用。Jaeger看多Agent调用链有一个非常好的视角因为每个Agent、每次工具调用都对应一个Span你能在一张图里看到整棵调用树每个节点的耗时、状态、输入输出摘要都点开即看。但也必须提醒一下多Agent系统的Trace通常比传统微服务的Trace胖得多。一个简单的问题可能就会生成几十个Span如果Agent内部还有多次循环调用几百个Span也不算夸张。所以可视化面板一定要做好过滤能力比如只看ERROR状态的Span、只看工具调用、只看超过500ms的Span等等。我们的Grafana Dashboard上最常用的一张图是“按Agent维度的平均耗时和错误率热力图”再配一张“工具调用成功率趋势”。这两张图基本能看出系统大部分健康问题。后面我们还做了一步进阶操作把每次Trace的摘要信息存进Elasticsearch建了一个“会话检索”页面。输入用户的请求ID或关键词就能直接看到这次会话完整走了哪些Agent、每步耗时多少、中间产出是什么。这个功能救了大命。4. 调试方法论从“不知道哪错了”到“一眼定位”4.1 分阶段隔离调试先确定问题层再深入细节监控系统建好之后真正要解决的是调试效率问题。我们总结了一套分层排查的方法强烈建议所有做多Agent系统的团队都试一下。第一层看指标确定问题面。先看Grafana大盘是哪类问题整体响应变慢、某个Agent的错误率飙高、某个工具调用大面积失败还是Token消耗异常上涨这一层能快速把问题范围圈到某一个Agent、某一个工具或某一条链路上。第二层看Trace还原执行路径。问题范围锁定了直接去Jaeger里查这一段时间内的对应Trace。重点是看两件事一是调用树长什么样是否出现了意外的循环、重复调用、步骤缺失二是每个Span的状态和耗时找出异常节点。第三层看日志和Span详情定位根因。异常节点找到了点进去看当前Agent的输入输出、当时的决策记录、工具调用返回的原文、模型的原始回复。到这里大部分问题的根因基本就浮出水面了。这套分层法的核心价值是避免一上来就陷入日志的汪洋大海里。先宏观再微观效率会高很多。4.2 会话重放把现场的“黑匣子”捡回来不过最难排查的是那种偶发性问题——系统在线上跑着跑着某次响应就错了但你去查的时候日志里只有寥寥几行根本还原不出当时的完整上下文。所以我们在设计可观测性方案时特意加了一个“会话重放”能力。核心思路很简单对每一次完整的用户请求我们把链路中所有Agent的输入、输出、决策依据、工具调用结果全部持久化存储起来。出问题后可以一键把这次对话“重放”一遍——不需要真正再调用一次大模型而是把所有Agent当时看到的数据按时间线重新展示出来。这个机制在排查所谓“幻觉问题”时帮助极大。有一次用户的Agent在汇总时凭空多了一段数据我们打开会话重放看到上游数据Agent传给它的上下文里确实没有这段数据但汇总Agent自己“脑补”了出来——这就把问题定性成了模型幻觉而不是数据链路Bug。定位到这个层级后续解决方案就非常清晰了。4.3 让Agent“自解释”强制透出决策依据还有一个很实用的调试技巧在设计Agent Prompt时就要求Agent在执行关键动作时同步输出决策理由。听起来像是在给Agent增加额外负担但实际上对大模型来说多写两行解释几乎不消耗额外推理能力。以规划Agent为例我们在Prompt里的描述是“当你做出任务规划时请同步输出每个子任务被创建的原因以及你为什么不采用其他替代方案。”Agent在输出决策理由后我们把这些理由结构化地记录下来作为当前Span的一个Attribute。调试时这一条特别有用——你不需要猜“这个Agent为什么要调用搜索工具”它自己已经把答案写在了那里。当然这个方案会增加一定的Token开销但对排查问题带来的价值远超这点成本。我们在生产环境实测下来增加的Token消耗在1%-3%左右可以接受。5. 真实案例复盘一次典型的多Agent故障排查5.1 现象与初判上个月我们线上系统遇到一次典型故障。用户反馈部分复杂的分析类请求返回结果会“缺少某一整块内容”但没有任何报错看起来是一个结构完整但是残缺的答案。从指标大盘上看问题非常隐蔽——平均耗时没有明显变化错误率几乎为零工具调用成功率也正常。如果只看传统监控这个故障是无论如何都发现不了的。但因为Trace一直有记录我们直接在Jaeger里查了下其中一个出问题的会话问题一下子暴露了原本应该执行三个子任务的规划Agent在实际执行时只生成了两个子任务第三个“异常数据分析”的子任务压根没有出现在调用树里。5.2 通过追踪数据定位根因进一步点开规划Agent的Span详情查看它的推理摘要我们发现原因清清楚楚那次请求的原始输入特别长用户贴了近万字的上下文材料进去规划Agent在处理时出现了“注意力偏置”把重心全部放在了前面的信息上漏掉了最后关于“分析异常数据”的需求。同时我们还发现规划Agent在生成子任务列表时没有做“完整性校验”导致少了一个子任务也没被拦下来。这个不是大模型的错是我们设计里缺了一个检查和兜底的环节。补充一个细节这个问题在传统测试环境几乎无法复现因为测试用的输入不会这么长、这么脏。只有借助全链路的Trace和决策记录才能抓到这个隐蔽的上下文窗口问题。5.3 修复方案与效果根因清楚了修复就很快了。我们做了三个改动第一在规划Agent的Prompt里增加了一个强制要求“生成子任务列表之前先判断原始需求中是否包含多个独立主题并逐条列出。”同时在Prompt末尾加了一句“请确认你的子任务列表已经覆盖了用户需求的所有方面”让模型在输出前做一次自我检查。第二增加了一个程序层面的完整性兜底将子任务列表和原始输入一起做一次关键词/语义相似度浅校验如果发现明显的主题缺失比如原始输入出现了“异常分析”关键词但子任务列表没有相关任务就让规划Agent重新规划一次。第三在流程上增加了“计划确认”步骤——规划Agent生成的计划不再直接执行而是先给用户或者上游Agent看一眼确认无遗漏后再继续。上线后观察了两周这一类“静默残缺”问题再没出现过而且因为Add了一层完整性检查整体结果质量也有提升。5.4 实践中沉淀的常见问题速查表我在调试多Agent系统的过程中沉淀了一张问题速查表这里整理出来按Trace现象分类方便大家遇到问题直接对号入座。排查方向典型现象常见根因快速验证方法Trace链路中断子Agent没有出现在调用树里跨进程Context未传递查看任务消息体中是否携带Trace IDTrace链路异常变长同一个工具被反复调用Agent陷入循环缺少终止条件查看工具调用Span的重复次数Agent状态报错某Agent返回Error状态模型返回格式不合预期解析失败查看Span里的模型原始输出结果静默残缺整体无报错但答案不完整上游Agent决策遗漏缺少校验兜底查看规划Agent的推理摘要响应异常缓慢某个Agent耗时几千毫秒上下文过长或外部工具调用慢查看该Agent子Span的耗时分布Token消耗异常涨单次请求Token数暴增上下文重复累积、未做裁剪查看各Agent的上下文长度Attribute5.5 系统级检查除了排查Bug还要注意Agent间的结构性协作问题要说排查实践经验还有一件事值得单独提Agent之间的“结构性协作问题”光靠看单次Trace是看不出来的。这个怎么理解呢大多时候Agent协作架构在正常负载下看不出什么问题但生产环境往往有流量高峰。我们遇到过几次比较难查的案例——入口流量涨上去之后某几个Agent同时都在等待一个共享的检索服务彼此之间互相竞争资源导致整体吞吐掉了一半。单看某一条Trace每条都很正常没有报错执行时间也就涨了一点点。这种问题的突破口在Metrics层面而不是Traces层面。我们后来专门给每个Agent加了一个“并发执行数”的仪表盘配合工具调用队列长度一眼就能看出是不是出现了资源竞争或死锁。另外我们还在Agent的消息传递层做了一次“扇入/扇出比”分析——统计每个Agent平均向多少个下游Agent发送了消息、同时接收了多少个上游消息用来衡量协作结构的平衡性和潜在瓶颈。这些数据帮助我们在没有做大规模架构改造的情况下只通过给共享服务加限流并把几个Agent的检索频率做错峰就解决了高并发下的瓶颈问题。5.6 一件事先想清楚再动手监控数据的接口规范和团队协作除了技术问题还有一件容易轻视但实际很磨人的事监控数据的接口格式。多Agent系统的团队通常不只一两个人。有人负责Agent的编排和规划逻辑有人负责接入外部工具还有人负责优化模型Prompt。如果不提前定好“Span的命名规则”“Attribute的统一前缀”“日志的JSON结构”过不了两周数据就会乱得根本没法用。我们经历了一次惨痛的教训后专门整理了一份内部规范文档明确了Span的命名统一为agent.{agent_name}、tool.{tool_name}、workflow.{step_name}。关键Attribute的命名统一为agent.input、agent.output、agent.decision_reason、tool.parameters、tool.result、model.prompt_tokens、model.completion_tokens。日志必须使用结构化JSON格式且在Header里带上request_id方便跟Trace关联。所有Agent执行的对外输出必须保留一份原始版本不截断、不脱敏存入冷存储方便深度排查。定好规范后整个团队的排查效率提升了不止一个档次。这一点也是我特别想对正在做多Agent系统的团队说的观测体系的设计一定要前置而且要当成一份接口契约来维护不能边做边补。6. 几点值得反复琢磨的经验6.1 可观测性是“设计”出来的不是“补”出来的这是我做过最有感触的一点。如果你等系统出问题才开始想着加日志、加追踪那基本追不回来。因为多Agent系统的执行路径是动态的很多关键信息比如模型原始回复、决策推理过程、上下文截断前的完整状态一旦执行结束就永远消失了后补是补不出来的。所以不管是自研Agent框架还是用LangGraph这类现成工具可观测性的能力一定要在设计阶段就接入进来。凡是Agent、工具、工作流这几个层面的代码都要过一遍“观测需求检查表”这个动作失败后我需要哪些信息才能定位这两步之间是否有可能出现静默传递如果出现循环我怎么发现把这些问题在设计时过一遍后续省下的排查时间是非常可观的。6.2 在成本与信息之间找平衡全量、无死角地记录所有信息理论上是最理想的但现实不允许。多Agent系统的Token消耗本来就大如果日志和追踪再全量保存海量数据存储成本会直线上升。我们的经验是分级处理核心的指令信息Span的输入输出摘要、耗时、状态、错误信息全量保留保留30天。模型原始输入输出这种大体量数据按5%的采样率或仅对有错误/超时的调用全量保留存到冷存储。用于成本趋势分析和告警的指标数据全量保留这个体量不大。用于模型分析的数据按业务需求定期做二次加工提取摘要后保留。这个分级策略执行下来每月存储成本大概控制在纯全量方案的20%以内但绝大部分问题场景都能覆盖到。最后再分享一个小技巧给每个Agent执行时注入一个“预算上限”比如这个Agent最多调多少次工具、最多执行多少秒、最多消耗多少Token一旦超了就强制终止并打上特殊的异常标记。这个机制一方面能防止Agent失控另一方面每次触达预算上限的记录其实也是系统瓶颈的重要信号——哪里总在超预算哪里就是架构上最脆弱的地方。我们现在看一个多Agent系统健不健康第一眼看的不是可用性而是“预算触达率”这个Malformed指标。以上就是我们整个团队在半年的多Agent系统迭代过程中沉淀下来的可观测性构建思路和实践经验。工具会更新框架会换代但“完整记录执行轨迹、保证链路可追溯、让决策过程可解释”这三件事是做好Multi-Agent系统监控和调试永远不会过时的基石。
返回列表