Multi-Agent系统Tracing实战:从线上事故到执行链记录体系

发布时间:2026/10/15 5:38:22
Multi-Agent系统Tracing实战:从线上事故到执行链记录体系
1. 从一次线上事故说起为什么Multi-Agent系统必须做Tracing去年冬天我们团队上线了一套基于多智能体协作的自动化内容处理系统四个Agent分别负责信息抽取、逻辑校验、格式转换和最终输出。上线第三天运营同学反馈说有一批文档的处理结果出现了明显的逻辑断层——前半段还在讲A方案后半段突然跳到C方案的结论中间的推理过程完全消失了。我接到问题后第一反应是去看日志。结果打开日志文件的那一刻我整个人是懵的四个Agent各自的日志分散在四个不同的文件里时间戳精度只到秒级同一个任务在不同Agent之间的流转没有任何关联标识。我花了整整一个下午靠人工比对时间戳和输入输出内容才勉强还原出问题链路——原来是逻辑校验Agent在处理某类边界输入时返回了一个空结果格式转换Agent拿到空结果后没有报错而是基于自己的默认逻辑补全了一段内容最终导致输出断层。这次事故让我深刻意识到一个问题Multi-Agent系统的复杂度不是线性叠加的而是指数级放大的。单个Agent的逻辑你可以用单元测试覆盖但当多个Agent以某种拓扑结构协作时它们之间的交互路径、数据流转、状态变化会形成一个巨大的状态空间。没有Tracing你就是在盲人摸象。这篇文章我想把我们在Multi-Agent系统Tracing方面踩过的坑、总结的方案、以及最终落地的一套执行链记录体系完整地分享出来。无论你是在做基于LLM的多智能体协作还是传统的多模块任务编排只要涉及多个执行单元之间的链路追踪这套思路都能直接参考。2. Multi-Agent系统的执行链到底复杂在哪里2.1 单Agent与Multi-Agent的调试难度差异先说说单Agent的情况。一个独立的Agent输入确定、逻辑确定、输出确定出了问题你只需要检查三个地方输入数据是否符合预期、内部逻辑是否有分支遗漏、输出结果是否满足约束。用传统的日志加断点调试基本能覆盖90%的问题场景。但Multi-Agent系统完全是另一回事。我总结下来复杂度的来源主要有四个维度第一个维度是拓扑结构的多样性。Agent之间可能是串行流水线可能是并行扇出再汇聚也可能是带条件分支的动态路由。更麻烦的是有些系统允许Agent在运行时动态决定下一个执行者这意味着执行路径不是预先确定的而是运行时生成的。第二个维度是数据流转的隐式性。在单Agent里数据从输入到输出是一条清晰的线。但在Multi-Agent系统里Agent A的输出可能经过一个消息队列、一个共享内存、或者一个中间件才到达Agent B。这个过程中数据可能被序列化、被截断、被转换格式等你看到Agent B的输入时它可能已经和Agent A的输出不一样了。第三个维度是状态管理的分散性。每个Agent可能有自己的内部状态同时系统层面还有一个全局状态。当问题发生时你很难判断是某个Agent的内部状态出了问题还是全局状态在传递过程中被污染了。第四个维度是时序的不确定性。并行执行的Agent之间谁先谁后可能是不确定的。同一个任务跑两次执行顺序可能不同导致问题难以复现。2.2 没有Tracing时我们会遇到哪些典型问题我把我们实际遇到过的问题整理了一下大概可以分成这几类问题类型典型表现排查难度链路断裂某个Agent没有收到上游的输出或者下游没有收到该Agent的输出高需要逐段比对数据污染中间某个环节修改了数据但没记录导致最终结果与预期不符极高需要全链路数据快照死循环Agent之间互相调用形成环路任务永远无法结束中但定位到具体环路需要链路图性能瓶颈整体耗时过长但不知道时间花在哪个Agent或哪段交互上中需要细粒度耗时统计状态不一致多个Agent对同一份数据的理解不同导致行为冲突极高需要状态变更历史这些问题的共同点是你无法通过查看单个Agent的日志来定位根因。你必须有一个跨Agent的、端到端的执行链视图。2.3 Tracing要解决的核心问题定义基于上面的分析我给Multi-Agent系统的Tracing定了一个明确的目标能够完整还原任意一次任务执行的全过程包括经过了哪些Agent、每个Agent的输入输出是什么、Agent之间的数据是如何流转的、每个环节耗时多少、最终结果是如何一步步产生的。这个目标拆解下来对应几个核心能力链路关联同一次任务的所有执行记录必须能关联到一起不管经过了多少个Agent、多少层嵌套。上下文捕获每个Agent执行时的输入、输出、关键中间状态都要被记录。时序还原能够按照实际执行顺序还原整个链路包括并行分支的先后关系。异常标记链路中出现的错误、重试、超时等异常事件要被显式标记。可视化呈现最终要能以直观的方式展示链路而不是让开发者去读原始日志。3. 执行链记录的核心设计Trace、Span与Context3.1 三个核心概念的定义与关系在设计具体方案之前先把三个核心概念理清楚。这三个概念借鉴了分布式追踪领域的成熟经验但在Multi-Agent场景下有一些特殊的考量。Trace追踪代表一次完整的任务执行。在Multi-Agent系统里一个Trace对应从用户发起请求到最终返回结果的整个过程。Trace有一个全局唯一的ID所有属于这次执行的记录都挂在这个ID下面。Span跨度代表Trace中的一个执行单元。在Multi-Agent场景下一个Span通常对应一个Agent的一次执行。但Span的粒度可以更细——比如Agent内部的一次工具调用、一次模型推理、一次外部API请求都可以作为独立的Span。Span之间通过父子关系或前后关系组织成树状或图状结构。Context上下文代表在链路中传递的数据载体。它包含了Trace ID、当前Span ID、父Span ID以及业务层面的上下文信息。Context是串联整个链路的关键——每个Agent在执行时从Context中读取上游信息执行完成后将自己的信息写入Context传递给下游。这三者的关系可以用一个类比来理解Trace是一次完整的快递配送Span是配送过程中的每一个中转站Context是随包裹一起流转的运单信息。运单上记录了这单快递的完整路径每到一个中转站就盖一个章。3.2 Trace ID的生成与传播策略Trace ID的生成看起来简单但在Multi-Agent系统里有几个容易踩坑的地方。首先是生成时机。Trace ID应该在任务最开始被创建时生成而不是在第一个Agent执行时才生成。因为任务创建和第一个Agent执行之间可能还有排队、调度等环节这些环节也应该被纳入Trace。其次是生成算法。我们用的是时间戳随机数机器标识的组合方案。时间戳保证趋势递增随机数避免碰撞机器标识用于分布式环境下区分来源。具体格式是{timestamp}-{random}-{node}。这个方案的好处是ID本身携带了时间信息在排查问题时可以快速判断任务的先后顺序。然后是传播策略。在Multi-Agent系统里Trace ID的传播有两种模式显式传播Context作为参数在Agent之间传递每个Agent都能拿到完整的链路信息。这种方式最可靠但要求所有Agent都遵循统一的接口约定。隐式传播通过线程本地变量、协程上下文或消息头来传递。这种方式对Agent代码侵入小但在异步、并行场景下容易丢失。我们的选择是显式传播为主隐式传播为辅。核心链路用显式传播保证可靠性一些辅助性的记录用隐式传播减少侵入。注意如果你的系统里有跨进程或跨服务的Agent调用Trace ID必须放在消息头或请求参数里传递不能依赖进程内的上下文。我们曾经因为一个Agent通过消息队列调用另一个Agent时没有传递Trace ID导致链路在那里断了一截。3.3 Span的数据结构设计Span的数据结构设计直接决定了Tracing系统的能力上限。我们经过几轮迭代最终确定的结构包含以下字段{ trace_id: 全局追踪ID, span_id: 当前跨度ID, parent_span_id: 父跨度ID, agent_name: 执行该跨度的Agent名称, span_type: 跨度类型agent_call/tool_call/llm_inference等, start_time: 开始时间毫秒精度, end_time: 结束时间毫秒精度, duration_ms: 耗时毫秒, status: 状态success/error/timeout/retry, input: 输入数据快照, output: 输出数据快照, attributes: 自定义属性键值对, events: 跨度内发生的事件列表, error_info: 错误信息如果状态为error }这里有几个设计决策值得展开说说为什么用毫秒精度而不是秒级因为Multi-Agent系统里很多Agent的执行时间在几百毫秒级别秒级精度根本区分不出先后顺序。我们实测下来毫秒精度是基本要求如果条件允许微秒精度更好。为什么input和output要存快照而不是引用因为数据在链路中可能被修改。如果只存引用等你事后排查时数据可能已经被后续环节改得面目全非了。存快照虽然会增加存储开销但排查问题时能救命。当然对于大体积数据可以存摘要加引用但关键字段必须存快照。attributes和events的区别是什么attributes是静态的键值对描述Span的整体特征比如模型名称、工具类型、重试次数等。events是时间线上的离散事件比如开始调用工具、收到模型响应、触发重试等。events对于还原执行过程特别有用。3.4 Context在Agent间的传递机制Context的传递机制是Tracing系统能否真正串联起来的关键。我们设计了一套基于Context对象的传递方案核心思路是每个Agent在执行前接收Context执行后返回更新后的Context。Context对象的结构大概是这样的class TraceContext: def __init__(self, trace_id, parent_span_idNone): self.trace_id trace_id self.parent_span_id parent_span_id self.current_span_id None self.baggage {} # 业务层面的上下文数据 self.metadata {} # 系统层面的元数据 def create_child_span(self, agent_name, span_type): 创建子Span并更新上下文 span Span( trace_idself.trace_id, parent_span_idself.current_span_id or self.parent_span_id, agent_nameagent_name, span_typespan_type ) self.current_span_id span.span_id return span在实际使用中Agent的入口函数会接收Context然后基于它创建自己的Span执行完成后将Span记录到Tracing系统并将更新后的Context传递给下游。这里有一个容易忽略的细节并行分支的Context处理。当一个Agent需要并行调用多个下游Agent时每个分支应该基于当前Context创建一个独立的副本而不是共享同一个Context对象。否则并行分支之间的Span ID会互相覆盖。我们最初就踩了这个坑导致并行分支的链路全部串到了一起。4. 落地实操从零搭建一套执行链记录系统4.1 技术选型与架构设计在技术选型上我们评估了三种方案方案一自研轻量级Tracing。基于日志系统自己实现Trace ID的生成、传播和存储。优点是灵活可控缺点是工作量大尤其是可视化和查询部分。方案二基于OpenTelemetry标准。利用OpenTelemetry的SDK和生态将Agent执行建模为Span。优点是标准成熟、生态丰富缺点是需要适配Multi-Agent的特殊场景比如动态拓扑、Agent内部状态捕获等。方案三混合方案。核心链路用OpenTelemetry标准特殊场景自研扩展。这是我们最终选择的方案。架构上我们分了四层采集层负责在Agent执行过程中捕获Span数据。包括SDK埋点、装饰器自动埋点、以及手动埋点三种方式。传输层负责将采集到的Span数据发送到存储层。我们用的是异步批量发送避免影响Agent执行性能。存储层负责Span数据的持久化和索引。我们选了支持列式存储的数据库因为Tracing数据的特点是写入量大、查询模式固定。展示层负责链路查询和可视化。我们做了一个简单的Web界面支持按Trace ID查询、按时间范围筛选、按Agent名称过滤。4.2 埋点方案自动埋点与手动埋点的结合埋点是Tracing系统落地的第一步也是最影响开发体验的一步。我们的原则是能自动的绝不手动必须手动的提供便捷接口。自动埋点主要覆盖两类场景第一类是Agent的标准执行入口。我们定义了一个统一的Agent接口所有Agent都必须实现这个接口。然后通过装饰器的方式在接口调用前后自动创建Span、记录输入输出、计算耗时。Agent开发者不需要写任何Tracing相关的代码。trace_span(span_typeagent_call) def execute(self, input_data, context): # Agent的业务逻辑 result self._process(input_data) return result第二类是常见的工具调用和模型推理。我们封装了统一的工具调用接口和模型推理接口在这些接口内部自动埋点。这样Agent开发者调用工具时不需要关心Tracing。手动埋点主要覆盖两类场景第一类是Agent内部的关键中间步骤。比如一个Agent内部有多个处理阶段每个阶段的输入输出都需要记录。这时候可以用手动埋点的方式在关键位置插入Span记录。第二类是自定义的业务事件。比如触发了降级策略、命中了缓存、检测到异常输入等。这些事件对于排查问题非常有价值但无法自动捕获。手动埋点的接口设计得很简单with tracer.start_span(custom_step, span_typeinternal) as span: span.set_attribute(step_name, data_validation) # 业务逻辑 span.add_event(validation_passed, {record_count: 100})实操心得埋点不要追求大而全一开始只埋最关键的几个点等真正遇到排查困难时再补充。我们最初试图把所有细节都埋上结果性能开销太大后来精简到只埋Agent入口、工具调用、模型推理和错误事件这四个点性能影响降到了3%以内。4.3 存储方案如何高效存储和查询链路数据Tracing数据的存储有两个特点写入量大、查询模式相对固定。基于这两个特点我们在存储方案上做了几个关键决策。第一个决策是选择列式存储而不是行式存储。因为Tracing数据的查询通常是按Trace ID查整条链路或者按时间范围加Agent名称筛选。列式存储在按列扫描和聚合方面有明显优势。我们实测下来同样的数据量列式存储的查询速度是行式存储的5到10倍。第二个决策是对Span数据进行分区。我们按天分区每天一个数据分区。这样查询时可以快速定位到目标时间段的数据避免全表扫描。同时过期数据的清理也变得很简单直接删除整个分区即可。第三个决策是对大字段做特殊处理。input和output字段可能非常大尤其是当Agent处理的是长文本或大JSON时。我们的做法是如果字段大小超过阈值我们设的是64KB就只存摘要和前N个字符完整数据存到对象存储在Span里存一个引用。第四个决策是建立合适的索引。我们建了三个索引Trace ID索引用于查完整链路、时间范围索引用于按时间筛选、Agent名称索引用于按Agent过滤。这三个索引覆盖了90%以上的查询场景。存储 schema 大概长这样CREATE TABLE spans ( trace_id VARCHAR(64), span_id VARCHAR(64), parent_span_id VARCHAR(64), agent_name VARCHAR(128), span_type VARCHAR(64), start_time TIMESTAMP, end_time TIMESTAMP, duration_ms INT, status VARCHAR(32), input_summary TEXT, output_summary TEXT, input_ref VARCHAR(256), output_ref VARCHAR(256), attributes JSON, events JSON, error_info TEXT, partition_date DATE ) PARTITION BY RANGE (partition_date);4.4 可视化让执行链一目了然可视化是Tracing系统价值的最终体现。再好的数据如果展示不直观排查问题时还是得靠人脑去拼图。我们的可视化界面主要包含三个视图第一个是链路总览视图。以时间轴的方式展示整个Trace的执行过程每个Span用一条横条表示横条的长度代表耗时颜色代表状态绿色成功、红色错误、黄色重试。Span之间的父子关系用缩进或连线表示。这个视图能让你一眼看出整个链路的耗时分布和瓶颈所在。第二个是Span详情视图。点击某个Span后展示该Span的完整信息包括输入输出、属性、事件、错误信息等。对于输入输出我们做了格式化展示JSON会自动展开长文本会折叠。第三个是链路对比视图。这个视图是我们在排查偶发问题时加的。可以选择两条Trace进行对比系统会自动高亮出差异部分。比如同一个任务一次成功一次失败对比视图能快速定位到差异发生在哪个Span。注意可视化界面不要追求花哨实用最重要。我们最初做了一个很炫的3D链路图结果实际排查问题时根本用不上后来改成了朴素的甘特图样式反而效率最高。5. 实战中的典型问题与排查技巧5.1 链路断裂的三种常见原因链路断裂是Tracing系统最常见的问题表现为某个Span的parent_span_id找不到对应的父Span或者整条链路中间缺了一段。根据我们的经验原因主要有三种第一种是Context传递丢失。最常见的情况是Agent通过异步任务或消息队列调用下游时没有把Context传递过去。排查方法是检查所有跨进程、跨线程的调用点确认Context是否被正确传递。第二种是Span上报失败。Span数据在传输或存储过程中丢失了。排查方法是检查Tracing系统自身的监控指标看是否有上报失败率异常。我们曾经因为存储层磁盘满了导致部分Span写入失败链路就断了。第三种是采样策略导致的数据缺失。如果Tracing系统开启了采样部分Trace可能只记录了部分Span。排查方法是检查采样配置确认是否对关键链路开启了全量采集。5.2 数据污染问题的定位方法数据污染是Multi-Agent系统里最棘手的问题之一。表现是最终结果与预期不符但每个Agent单独看都正常。定位这类问题的核心思路是找到数据第一次偏离预期的位置。具体操作步骤从最终输出反推确认期望的输入是什么。在Tracing系统里找到产生这个输出的Span查看它的实际输入。如果实际输入与期望输入不符继续向上游追溯找到产生这个输入的Span。重复步骤2和3直到找到输入输出不一致的那个Span。这个过程听起来简单但实际操作时有一个关键点要区分数据被修改和数据被重新生成。有些Agent在收到不符合预期的输入时不会报错而是基于自己的逻辑重新生成一份数据。这种情况下从输出反推输入是找不到问题的因为输入和输出之间没有直接的对应关系。我们的做法是在Agent内部对重新生成这种行为做显式标记记录到Span的events里。5.3 性能瓶颈的快速定位Multi-Agent系统的性能问题往往不是单个Agent慢而是某个环节的等待或重试导致的。Tracing系统在性能排查方面的价值主要体现在两个能力上第一个是耗时分解。通过Span的duration字段可以快速看出时间花在了哪个Agent、哪个工具调用、哪次模型推理上。我们通常会把Span按耗时排序前几个基本就是瓶颈所在。第二个是并行度分析。通过链路总览视图可以看出哪些环节是串行的、哪些是并行的。如果发现本应并行的环节实际是串行的那可能就是性能优化的切入点。我们遇到过一个典型案例整体耗时突然从2秒涨到了10秒但每个Agent的单次执行耗时都正常。通过Tracing系统一看发现是某个Agent触发了重试重试了5次才成功。每次重试的耗时加起来就是多出来的8秒。如果没有Tracing你只能看到总耗时变长了根本不知道是重试导致的。5.4 常见问题速查表问题现象可能原因排查方法解决方案链路中间断开Context传递丢失检查跨进程调用点的Context传递在消息头或参数中显式传递Trace ID并行分支链路串线Context对象被共享检查并行分支是否创建了Context副本每个并行分支创建独立的Context副本Span数据缺失上报失败或采样检查上报失败率和采样配置修复上报通道调整采样策略耗时统计不准时钟不同步检查各节点的时钟同步情况统一使用NTP同步时钟存储增长过快大字段未做处理检查input/output字段大小分布大字段存引用只存摘要查询速度慢索引缺失或分区不合理分析查询模式检查索引使用情况补充索引优化分区策略6. 进阶话题让Tracing数据产生更大价值6.1 基于执行链的自动化测试Tracing数据不仅可以用于排查问题还可以用于自动化测试。我们的做法是把生产环境中成功的Trace记录下来作为回归测试的基准。每次代码变更后用相同的输入跑一遍对比新产生的Trace和基准Trace的差异。如果某个Span的输入输出发生了非预期的变化就标记为潜在回归。这个方案的好处是测试用例来自真实场景覆盖面比人工编写的测试用例更广。而且对比的是执行链而不是最终结果能发现一些最终结果正确但中间过程有问题的隐性bug。6.2 执行链数据的分析与优化当Tracing数据积累到一定量后可以做很多有价值的分析热点Agent识别统计各Agent的调用频率和平均耗时找出系统的热点。异常模式发现分析错误Span的分布规律找出高频错误模式。链路优化建议分析执行链的拓扑结构找出可以并行化或可以合并的环节。容量规划基于历史Trace数据预测未来的资源需求。我们目前做的最有价值的一个分析是重试模式分析。通过统计哪些Agent最容易触发重试、重试的原因是什么我们针对性地优化了上游数据的质量整体重试率下降了60%。6.3 与监控告警系统的联动Tracing系统单独使用价值有限和监控告警系统联动后才能发挥最大价值。我们做了几个联动错误率告警当某个Agent的错误Span比例超过阈值时自动触发告警。耗时告警当某个Span的P99耗时超过阈值时自动触发告警。链路异常告警当出现链路断裂、死循环等异常模式时自动触发告警。告警信息里会直接附带相关Trace的链接点击就能跳到Tracing系统查看详情。这样从发现问题到定位问题的路径被大大缩短了。7. 我在实际落地中总结的几条经验第一Tracing系统的建设要趁早。不要等到系统复杂了、问题多了才开始做。我们是在上线后第三天才开始补Tracing的那三天里遇到的问题全靠人工排查效率极低。如果一开始就有Tracing很多问题几分钟就能定位。第二埋点要克制。不要试图记录所有东西那样性能开销太大而且数据太多反而干扰排查。只记录真正有助于定位问题的信息其他的等需要时再加。第三Context的设计要考虑并行场景。这是我们在实际踩坑后才重视的。并行分支必须使用独立的Context副本否则链路会串得一塌糊涂。第四可视化要实用不要花哨。甘特图加详情面板的组合比任何炫酷的3D图都管用。排查问题时开发者需要的是快速定位信息而不是欣赏视觉效果。第五Tracing数据要定期清理。Tracing数据的增长速度很快如果不做清理存储成本会迅速上升。我们的策略是保留最近30天的全量数据30天以上的只保留错误Trace和慢Trace。第六把Tracing纳入开发流程。我们要求每个新Agent上线前必须验证其Span能正确产生和上报。这作为代码审查的一个检查项确保Tracing的覆盖度不会随着系统演进下降。最后分享一个我们最近在尝试的方向把Tracing数据和Agent的Prompt版本关联起来。每次Prompt变更后对比变更前后的Trace看执行链有没有发生非预期的变化。这个做法在Prompt调优时特别有用能快速判断一次Prompt修改是正向优化还是引入了新问题。