资讯动态

Multi-Agent系统Tracing实战:从线上事故到全链路可观测性建设

发布时间:2026/10/20 8:50:37 来源:尧图企业网站定制
1. 从一次线上事故说起为什么Multi-Agent系统必须要有Tracing去年冬天我参与的一个多智能体协作项目在凌晨两点突然报警。用户反馈说一个原本应该三步完成的文档处理任务跑了整整四十分钟还没结束最后返回的结果还缺了一半。我们连夜排查打开日志一看傻眼了——三个Agent之间的调用记录散落在四个不同的日志文件里时间戳还对不上有的用UTC有的用本地时间有的干脆没打时间戳。我们花了三个小时才定位到问题一个负责检索的Agent在某个边界条件下陷入了重试循环而负责汇总的Agent一直在等它的结果整个链路就这么卡死了。那次事故之后我下定决心要给系统加上完整的Tracing能力。这篇文章就是我这几个月折腾Multi-Agent系统Tracing的完整记录包括为什么要做、怎么做、踩了哪些坑、最后怎么解决的。如果你正在构建或者维护一个多智能体系统不管是用LangChain、AutoGen还是自己手写的调度框架这篇文章应该都能帮你少走一些弯路。先说清楚这篇文章适合谁看。如果你只是跑一个单Agent的Demo那Tracing的需求还没那么迫切简单的logging就够了。但一旦你的系统里有两个以上的Agent需要协作或者一个任务需要多个步骤串联执行那Tracing就不是“锦上添花”而是“救命稻草”。我会从最基础的概念讲起逐步深入到具体的实现方案包括数据结构设计、上下文传递、存储选型、可视化展示等最后还会分享几个我在实际项目中总结的排查技巧。2. Multi-Agent系统的执行链到底复杂在哪里2.1 单Agent与多Agent的本质差异单Agent系统的执行路径相对线性接收输入调用模型可能调用一两个工具返回输出。这种场景下你甚至可以用print大法来调试。但Multi-Agent系统完全是另一回事。我画个简单的例子你就明白了。假设有一个“研究报告生成”的任务系统里有四个Agent规划Agent负责拆解任务检索Agent负责找资料分析Agent负责提炼观点写作Agent负责成文。这四个Agent之间不是简单的流水线关系而是存在大量的交叉调用和条件分支。规划Agent可能根据任务复杂度决定要不要启动第二轮检索分析Agent可能发现资料不足要求检索Agent补充写作Agent可能对分析结果不满意打回去重做。这就导致了一个核心问题执行链不再是线性的而是一张有向图。每个节点是一个Agent的一次执行每条边是一次消息传递或调用关系。更麻烦的是这张图是动态生成的每次任务的执行路径可能都不一样。2.2 执行链追踪的三个核心难题在实际项目中我发现Multi-Agent的Tracing面临三个特有的难题这也是为什么传统的APM工具直接拿来用往往不够。第一个难题是上下文传递。在微服务架构里你可以用TraceID贯穿整个调用链。但在Multi-Agent系统里Agent之间的通信往往是通过自然语言消息进行的这些消息里不会自动携带TraceID。你需要设计一套机制让TraceID能够随着消息一起传递同时还要保证Agent在生成新消息时不会把它弄丢。第二个难题是嵌套与并发。一个Agent在执行过程中可能调用另一个Agent被调用的Agent又可能调用第三个形成嵌套结构。同时多个Agent可能并行执行它们的执行时间段会重叠。这就要求Tracing系统能够正确表达父子关系和并行关系而不是简单的时间线排列。第三个难题是非确定性。同样的输入Agent可能因为模型采样的随机性而走出完全不同的执行路径。这意味着你不能像传统系统那样用固定的调用图来预期行为Tracing记录必须能够完整还原每一次具体的执行过程。2.3 没有Tracing会怎样几个真实场景我再举几个没有Tracing时遇到的真实场景你感受一下。场景一任务超时。用户提交了一个任务等了十分钟没结果。你去看日志发现最后一个记录是“检索Agent开始执行”然后就没了。它是在检索中卡住了还是检索完了但结果没传出去还是传出去了但下游Agent没收到没有Tracing你只能靠猜。场景二结果错误。最终输出里有一个事实性错误你需要追溯这个错误是哪个环节引入的。是检索Agent找错了资料是分析Agent理解错了还是写作Agent在改写时扭曲了原意没有Tracing你只能把每个Agent的中间输出都打出来人工比对效率极低。场景三性能优化。系统跑得慢你想知道瓶颈在哪里。是模型调用太慢是Agent之间的通信开销太大还是某个Agent在做无意义的重复工作没有Tracing你连每个环节的耗时都拿不到。这三个场景基本涵盖了Tracing的核心价值可观测性、可追溯性、可优化性。接下来我会详细讲怎么实现。3. Tracing系统的核心设计数据结构与上下文传递3.1 Span、Trace与Run的层级关系在设计Tracing系统之前先把几个核心概念定义清楚。我参考了OpenTelemetry的思路但根据Multi-Agent的特点做了一些调整。Trace是一次完整任务的执行记录。比如用户提交了一个“生成季度报告”的任务从接收到返回的整个过程就是一个Trace。每个Trace有一个全局唯一的TraceID。Span是Trace中的一个执行单元。在Multi-Agent场景下一个Span通常对应一个Agent的一次执行。Span有开始时间、结束时间、输入、输出、状态等属性。每个Span有一个SpanID以及一个ParentSpanID指向它的父Span。Run是我自己加的一个概念用来表示Agent内部的一次具体操作。比如一个Agent在一次执行中可能调用了三次模型、两次工具每次调用就是一个Run。Run挂在Span下面形成Span→Run的两级结构。为什么要加Run这一层因为在实际排查问题时我发现很多问题出在Agent内部的某个具体操作上而不是Agent整体。比如模型调用返回了格式错误的内容或者工具调用超时了。如果只有Span级别的记录你只能看到“这个Agent执行了5秒”但不知道这5秒里发生了什么。有了Run级别你就能看到“模型调用花了3秒返回了错误格式然后Agent重试了一次又花了2秒”。三者的关系可以用下面的表格来概括层级对应实体关键属性典型数量级Trace一次完整任务TraceID、开始时间、结束时间、状态1Span一个Agent的一次执行SpanID、ParentSpanID、Agent名称、输入输出几个到几十个RunAgent内的一次操作RunID、类型模型/工具、耗时、结果几十到几百个3.2 上下文传递的三种方案对比上下文传递是Multi-Agent Tracing最核心的技术难点。我试过三种方案各有优劣。方案一消息内嵌TraceContext。在Agent之间传递的消息体里额外加一个字段存放TraceContext包含TraceID、当前SpanID等信息。接收方解析消息时提取这个字段创建子Span时把ParentSpanID设为它。这个方案的好处是显式、可靠不依赖任何外部机制。坏处是侵入性强需要修改消息格式而且如果Agent在生成回复时把消息内容重新组织了一遍可能会丢失这个字段。方案二ThreadLocal/ContextVar。如果所有Agent都在同一个进程内运行可以用线程本地存储或异步上下文变量来传递TraceContext。每个Agent执行前从上下文里读取当前的Span信息执行时创建子Span并更新上下文。这个方案的好处是对消息格式无侵入坏处是只适合同进程场景而且异步编程模型下容易出问题。我就遇到过一次因为某个Agent用了线程池执行上下文没传过去导致那部分执行记录成了孤儿Span。方案三集中式Context Store。用一个外部的存储比如Redis来维护每个任务的当前执行上下文。Agent执行时根据TaskID去Store里查询当前的Span信息。这个方案的好处是解耦彻底适合分布式场景。坏处是引入了额外的网络开销和单点故障风险而且需要处理并发更新的问题。最终我采用的是方案一为主、方案二为辅的混合策略。消息传递时显式携带TraceContext同进程内的调用则用ContextVar做补充。这样既保证了跨进程的可靠性又减少了同进程内的消息改造工作量。3.3 数据结构设计一个Span应该记录什么Span的数据结构设计直接决定了Tracing系统的可用性。我经过几轮迭代最终确定了一个包含以下字段的结构dataclass class Span: trace_id: str # 全局追踪ID span_id: str # 当前Span唯一ID parent_span_id: str # 父Span ID根Span为None agent_name: str # Agent名称 agent_role: str # Agent角色描述 start_time: float # 开始时间戳毫秒 end_time: float # 结束时间戳毫秒 status: str # 状态running/success/error/timeout input_summary: str # 输入摘要截断到500字符 output_summary: str # 输出摘要截断到500字符 error_message: str # 错误信息如果有 metadata: dict # 扩展元数据 runs: List[Run] # 子操作列表这里有几个设计决策值得说明。为什么输入输出只存摘要因为Agent的输入输出往往是长文本全量存储会让存储成本急剧上升。摘要保留前500字符既能帮助定位问题又不会太占空间。如果需要完整内容可以另外存到对象存储里Span里只放一个引用。为什么要有metadata这个字段非常有用。我通常会把模型名称、token消耗、重试次数、工具调用参数等信息放在这里。不同项目可以根据需要扩展不影响核心结构。为什么runs是内嵌的而不是独立的因为Run的数量通常不多一个Agent一次执行可能就几个到十几个Run内嵌存储查询更方便。如果Run数量很大可以考虑拆成独立的集合。4. 实操从零搭建一套Multi-Agent Tracing系统4.1 技术选型为什么我最终选了OpenTelemetry自研Collector市面上可选的方案不少我调研了以下几种方案优势劣势适用场景纯日志文件简单、无依赖查询困难、无法表达嵌套关系单Agent、临时调试LangSmith与LangChain集成好绑定框架、数据在云端用LangChain的项目OpenTelemetry标准、生态好、可自托管需要自己适配Agent语义任意框架、需要自托管自研全套完全可控工作量大、生态缺失有特殊需求的团队我最终选择了OpenTelemetry作为数据模型和SDK自研Collector做Agent语义适配。原因有三第一OpenTelemetry的Span模型天然支持嵌套和并发正好匹配Multi-Agent的需求第二它的生态成熟有现成的存储和可视化方案比如Jaeger第三自研Collector可以灵活处理Agent特有的语义比如把自然语言消息转成结构化的Span属性。4.2 埋点实现在Agent执行的关键节点插入Tracing埋点是整个Tracing系统的基础。我在以下几个关键节点插入了埋点Agent执行入口。每个Agent的execute方法开始时创建一个Span结束时更新Span状态。这是最基本的埋点。class BaseAgent: def execute(self, input_data, context): tracer get_tracer() with tracer.start_span( namefagent.{self.name}, attributes{ agent.name: self.name, agent.role: self.role, input.summary: truncate(input_data, 500) } ) as span: try: result self._do_execute(input_data, context) span.set_attribute(output.summary, truncate(result, 500)) span.set_status(success) return result except Exception as e: span.set_status(error) span.set_attribute(error.message, str(e)) raise模型调用。每次调用LLM时创建一个Run记录模型名称、输入token数、输出token数、耗时等。工具调用。每次调用外部工具时创建一个Run记录工具名称、参数、返回结果摘要、耗时。Agent间消息传递。这是Multi-Agent特有的埋点。发送消息时记录一条事件接收消息时记录另一条事件两条事件通过消息ID关联。4.3 上下文注入让TraceID跟着消息走上下文注入是保证执行链完整的关键。我的做法是在Agent间消息的元数据里加一个_trace_context字段def send_message(self, target_agent, content, trace_context): message { from: self.name, to: target_agent, content: content, metadata: { _trace_context: { trace_id: trace_context.trace_id, parent_span_id: trace_context.current_span_id } } } self.message_bus.send(message)接收方在处理消息时从metadata里提取trace_context创建子Span时把parent_span_id设为它。这样就保证了执行链的连续性。注意如果Agent在生成回复时会对消息内容做改写一定要确保metadata不被丢弃。我遇到过好几次因为Agent把消息重新格式化了一遍导致trace_context丢失的情况。后来我在消息处理层加了一个校验如果发现metadata里没有trace_context就自动从当前上下文补上。4.4 存储与查询ElasticsearchJaeger的组合实践存储方面我用了Elasticsearch存原始Span数据Jaeger做可视化展示。为什么这么组合因为Jaeger自带的存储Badger、Cassandra在查询灵活性上不如Elasticsearch而Elasticsearch的全文检索能力对于按内容搜索Span非常有用。数据写入流程是这样的Agent产生Span数据后通过OpenTelemetry SDK发送到自研CollectorCollector做两件事一是把数据转成Jaeger格式发给Jaeger Collector二是把原始数据写入Elasticsearch。查询时日常的可视化追踪用Jaeger的UI复杂的检索需求比如“找出所有包含‘超时’错误的Span”用Kibana查Elasticsearch。4.5 可视化把执行链画成一张可交互的图Jaeger自带的UI是时间线视图对于理解嵌套关系够用但对于Multi-Agent这种图状执行链不够直观。所以我额外做了一个简单的可视化页面用D3.js把Span渲染成有向图。每个节点是一个Span节点颜色表示状态绿色成功、红色错误、黄色运行中节点大小表示耗时。边表示调用关系边的粗细表示消息传递的次数。点击节点可以展开查看该Span下的所有Run。这个可视化页面在排查问题时特别有用。有一次一个任务执行了异常长的时间我打开图一看发现有一个Span的节点特别大耗时很长点进去一看是检索Agent在反复重试同一个查询。如果没有这个图我可能要在日志里翻很久才能发现。5. 踩坑记录那些让我熬夜的Tracing问题5.1 时间戳不一致导致的“时空错乱”这个问题我遇到过两次每次都很头疼。第一次是因为不同Agent运行在不同的容器里容器的系统时间有偏差导致Span的时间戳对不上。表现出来就是子Span的开始时间早于父Span在时间线视图上看起来像是“时空错乱”。解决方案是统一用逻辑时钟。具体做法是每个Span除了记录物理时间戳还记录一个逻辑序号。逻辑序号由父Span分配保证子Span的序号一定大于父Span。在可视化时如果物理时间戳有冲突就用逻辑序号来排序。第二次是因为有的Agent用毫秒时间戳有的用秒时间戳混在一起就乱了。这个纯粹是代码规范问题后来我在SDK层做了统一所有时间戳都用毫秒。5.2 异步执行下的上下文丢失Python的asyncio用起来很爽但在Tracing上给我挖了不少坑。问题出在ContextVar上如果Agent在异步任务里执行而ContextVar没有正确传递子任务就获取不到当前的Span上下文创建的Span就成了孤儿。我试过几种解决方案。最简单的是在每个异步任务创建时手动传递上下文async def run_agent(agent, input_data, ctx): token current_span.set(ctx.current_span) try: return await agent.execute(input_data) finally: current_span.reset(token)但这样每个调用点都要写容易漏。后来我封装了一个装饰器自动处理上下文的设置和恢复才算彻底解决。5.3 大消息体导致的存储爆炸Agent之间的消息往往是长文本有的甚至上万字。如果每个Span都把完整的输入输出存下来存储很快就会爆掉。我算过一笔账一个中等复杂度的任务产生约50个Span每个Span平均存2KB的输入输出那就是100KB。如果每天跑1000个任务一天就是100MB一个月就是3GB。这还只是输入输出没算metadata和Run的数据。解决方案是分级存储。Span里只存摘要前500字符完整内容存到对象存储比如S3兼容的存储Span里放一个引用ID。查询时如果需要看完整内容再根据引用ID去取。这样Span的体积能控制在200字节以内存储成本降低了90%以上。5.4 常见问题速查表问题现象可能原因排查方法解决方案执行链断裂出现孤儿Span上下文未正确传递检查消息metadata里是否有trace_context在消息处理层加校验和自动补全时间线错乱时间戳不一致对比不同Agent的时钟统一时间戳单位引入逻辑时钟存储增长过快大消息体全量存储统计Span平均大小分级存储Span只存摘要查询慢索引设计不合理分析慢查询日志对trace_id、agent_name建索引可视化图节点过多埋点粒度过细统计Span数量合并细粒度Run只保留关键节点6. 进阶让Tracing数据产生更大价值6.1 基于Tracing的自动化测试Tracing数据不仅能用于排查问题还能用于自动化测试。我的做法是把一次成功执行的Trace录下来作为“黄金轨迹”然后在回归测试时用同样的输入跑一遍对比两次的Trace结构是否一致。当然由于Agent的非确定性完全一致是不可能的。所以我定义了几个关键指标Span数量偏差不超过20%关键路径上的Agent调用顺序一致最终输出通过质量检查。只要满足这几个条件就认为测试通过。这个方法帮我发现了好几次因为代码改动导致的执行路径变化。有一次我优化了一个Agent的提示词结果它不再调用某个工具了虽然最终输出看起来没问题但实际上是绕过了必要的验证步骤。如果没有Trace对比这个问题很难被发现。6.2 用Tracing数据做性能分析Tracing数据是性能分析的金矿。我写了一个简单的分析脚本定期统计以下指标每个Agent的平均执行耗时和P99耗时模型调用的平均token消耗和耗时Agent间消息传递的平均延迟任务的整体成功率这些指标帮我定位了好几个性能瓶颈。比如有一次发现检索Agent的P99耗时特别高查了Trace发现是某个外部API在特定条件下会超时重试。后来加了一个缓存层P99耗时降了60%。6.3 从Trace中挖掘Agent协作模式这个是我最近在探索的方向。通过分析大量的Trace数据可以发现一些有趣的协作模式。比如某些Agent总是成对出现某些Agent的调用往往预示着任务会失败。我目前做的一个简单应用是如果检测到某个Trace里出现了特定的Span序列比如“检索Agent连续重试三次”就提前预警通知人工介入。这比等到任务超时再处理要主动得多。7. 我个人的几条实操建议折腾了这几个月最后分享几条我觉得最有价值的经验。第一Tracing要从第一天就做。不要想着“先跑通再说后面再加Tracing”。Multi-Agent系统的调试难度是指数级增长的没有Tracing你连问题出在哪里都不知道。我见过太多项目在出问题后才匆忙加日志结果发现关键路径上根本没有埋点只能重现问题。第二摘要比全量更重要。一开始我总想把所有东西都记下来结果存储爆炸、查询缓慢。后来想明白了Tracing的目的是快速定位问题不是做数据归档。摘要引用的方式既省空间又够用。第三可视化不是锦上添花。在Multi-Agent场景下文本日志的排查效率极低。一张图能让你在几秒钟内看出问题所在而翻日志可能要几分钟甚至更久。如果预算允许一定要做可视化。第四定期清理和归档。Tracing数据增长很快如果不做清理几个月后存储成本就会很可观。我的策略是最近7天的数据保留完整7到30天的只保留Span摘要30天以上的归档到冷存储。第五把Tracing和告警结合起来。单纯记录不够还要能主动发现问题。我设置了几个告警规则单个Span耗时超过阈值、任务整体耗时超过阈值、错误Span比例超过阈值。这样不用等用户反馈系统自己就能发现问题。这套Tracing系统上线后我们排查问题的平均时间从原来的两三个小时缩短到了十几分钟。更重要的是它让我们对系统的运行状态有了前所未有的可见性很多潜在问题在爆发之前就被发现了。如果你也在做Multi-Agent系统强烈建议把Tracing作为基础设施的一部分来建设这笔投入绝对值得。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑