拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Multi-Agent系统可观测性实战:从黑盒监控到链路追踪

前阵子在做一个复杂的 Multi-Agent 系统每次出问题想定位根因都感觉自己对着一个黑箱。单体应用再乱你还能靠打日志、加断点、翻监控面板一点点逼近可一旦把任务拆给七八个智能体它们之间还会互相调用、递归规划、动态选工具问题就开始藏在“链路”里而不是“单点”里。后来我认真把“可观测性”当成系统的一部分来做才算把监控和调试从“拆盲盒”变成了工程问题。这篇文章不打算讲玄学就是把我这段时间搭监控、做链路追踪、排线上事故的过程整理成一份可以照着做的实践记录。适合正在做 Agent 编排、遇到了“日志没报错但结果不对”“任务卡死没人知道”“哪一步慢全靠感觉”这些问题的人。哪怕你团队只有两三个人也可以先用里面最小可用的方案把观测体系跑起来。1. 为什么 Multi-Agent 系统会变成“黑盒”1.1 单体程序和 Multi-Agent 的区别以前写传统服务一次请求从进入网关到落库调用路径基本是线性的。哪怕上了微服务只要链路统一打了 trace顺着 spans 往下查总能找到问题点。Multi-Agent 系统完全不是这个逻辑一个用户请求进来可能先被“规划器”拆成多个子任务每个子任务分给不同的 agent有的 agent 要调外部工具有的还要再派生子任务最后所有结果再汇总成一个答案。执行路径不是设计时画好的而是运行时临时决定的。这带来了两个很难受的后果。第一你没法通过“看代码”推断一次任务到底走了哪条路。同一个问题上一次走 A 分支这一次可能走 B 分支因为模型输出有随机性。第二你很难复现问题。线上某个 agent 抽风了你把同样的输入拿到本地再跑一遍可能一次就过了。所以传统后端那种“抓现场”的思路在 Agent 系统里基本失效。现场到底是什么是整个 trace 快照里记录的状态、上下文、调用序列而不是某台机器的内存和堆栈。我对这个阶段的体感特别深系统刚跑起来的时候全是惊喜跑了一周之后全是惊吓。没有观测手段的时候每次事故都要靠猜猜哪个 agent 的 prompt 又不行了猜是不是模型上下文超了猜工具返回格式是不是变了。后来我才意识到问题不是某个 agent 写得差而是整个系统缺乏“过程记录”的能力。可观测性就是把这个过程记录从“事后补救”变成“系统性设计”。1.2 传统监控手段失效的几个信号很多团队一提到监控第一反应就是 Zabbix 盯主机、Prometheus 盯指标、GDB 盯进程。这些手段在基础设施和嵌入式开发里非常成熟但放到 Multi-Agent 系统里大概率帮不上忙。你说机器 CPU 高不高、内存吃紧不紧Agent 任务的瓶颈通常不在这些地方而在模型的调用延迟、token 消耗、上下文占用、工具调用的成败。你监控一百台服务器不如把“当前这轮任务的上下文窗口使用率”看得清楚。另一个信号也很典型系统“没有报错”但结果不对。任务返回码是 200日志里全是 success可用户拿到的是完全跑偏的答案。这种故障最坑因为从传统监控角度看系统是健康的没有任何红色警报。问题出在语义层某个 agent 理解错了任务或者上游把关键信息传丢了或者上下文被覆盖了。要发现这类问题你得记录“智能体到底看了什么、做了什么、为什么决定做这个”这些内容传统日志体系根本不会去采集。我也不建议一上来就排斥老工具。在底层链路里比如 agent 依赖的某些外部服务Zabbix、Prometheus 这类监控依然有用。关键是认清它们的边界它们解决“机器和进程是否正常”不解决“智能体是否在做正确的事”。Multi-Agent 系统的可观测性必须在这个基础上再加一层面向“任务、上下文、决策、工具调用”的观测体系。监控对象传统手段Multi-Agent 额外需要主机资源Zabbix、Prometheus帮助有限主要看模型和任务侧函数/服务调用日志、断点、GDB结构化事件、调用序列快照执行过程请求级日志Trace 级上下文、输入输出摘要任务质量无法感知目标完成度、反馈评测2. 核心设计先定好可观测性的三个层次2.1 事件日志把智能体的“思考过程”结构化成事件日志是最基础也最容易踩坑的一层。很多 Agent 框架默认会把 prompt、response 打到控制台看起来信息量很大实际上没法用。你要按 agent、按 task、按 trace 去过滤结果一堆 print 挤在一起连时间顺序都要靠肉眼对。我的建议很明确所有关键节点用 JSON 输出结构化事件一行一个事件字段固定。我自己常用的最小事件结构长这样{ timestamp: 2025-04-01T10:12:33.221Z, trace_id: a1b2c3d4e5f6, span_id: span-001, task_id: task-8f4a, agent_name: planner, event_type: tool_call, payload: { tool: web_search, query: 2025年行业趋势报告, status: success, duration_ms: 820 } }这里的精髓在于 event_type。不要只记 error 和 warning还要记录 agent 的“意图变化”和“关键决策”。比如 planner 决定分几个子任务、执行器选了哪个工具、reflection 阶段有没有推翻上一轮结果这些都要作为事件落下来。它们不会直接暴露错误但事后复盘时你能顺着事件流还原出一次任务的全部思考轨迹。有段时间我排查“为什么这轮回答变差了”就是靠事件日志发现的reflection agent 在一半情况下会把第一次结果整体推翻然后重跑重跑后的答案不一定更好。这个行为在日志里只是一个 event_type但在排障时价值极大。如果你只打 error这类“决策漂移”问题永远发现不了。2.2 链路追踪用 Trace ID 把散落的智能体串起来如果说事件日志是“点”链路追踪就是“线”。Multi-Agent 系统的复杂性主要在线不在点。你需要知道一次任务从进入到结束经过了哪些节点每个节点耗时多久父子关系是什么样。这就是 trace 要解决的问题。具体实现上不要自己造轮子。OpenTelemetry 的 Trace 模型足够用给一次请求生成全局 trace_id后面所有 agent、模型调用、工具调用都挂到这条 trace 下用 span 表示一个个阶段。关键点在于 Span 的父子关系要符合业务逻辑比如“plan”是根 span“execute_subtask_1”“execute_subtask_2”是它的子 span“tool_call_1”又是子任务的子 span。这样在追踪平台里展开整个任务的结构一目了然。还有一个特容易断链的地方异步任务队列。Agent 系统里大量用消息队列做异步编排生产者和消费者不在同一个进程默认情况下 trace 上下文根本传不过去。解决办法是生产端把当前 context 注入到消息头里消费端再取出来作为父 context。这块不做好你看到的 trace 全是断的一条完整任务被拆成好几个孤立片段等于白搭。提示链路追踪的意义不只是“出问题时看调用链”更是帮助你发现结构性问题比如某个 agent 一直被同一个上游 agent 调用但上游经常传错参数。这种模式靠看日志是看不出来的只有把调用关系聚合成拓扑图才能暴露。2.3 指标与健康检查给系统装上“体温计”日志和 trace 解决“某个请求怎么了”指标解决“系统整体健康度怎么样”。Multi-Agent 系统至少应该盯四类核心指标任务量每秒新任务数、各 agent 节点处理量、排队积压数。错误率任务失败率、工具调用失败率、解析失败率、模型返回异常率。延迟任务端到端耗时以及 plan、execute、reflect 各阶段的 P50/P95/P99。资源消耗token 总量、上下文窗口占用、并发数、内存与网络开销。这些指标要用标签区分维度至少打上 agent_name、task_type、model_name 三个 label。否则指标会变成一堆没法切分的平均数失去了监控意义。比如你看到整体错误率只有 2%觉得没问题但切到“负责工具调用的那个 agent”一看错误率已经到 20%问题一直被平均值掩盖了。告警配多少合适我的经验是少于五条宁缺毋滥。核心就三类任务失败率超过阈值、P95 延迟突刺、某个 agent 的上下文占用率接近上限。告警太多值班的人就会疲掉最后重要告警也没人看。具体的阈值要根据你的流量和模型表现算我后面第 3 节会细讲。2.4 上下文与成本一份很容易被忽略的观测维度这一层传统监控完全没有对应物但在 Multi-Agent 系统里极其关键上下文窗口占用率和 token 消耗。每个 agent 的上下文里堆了多少轮历史对话、塞了多少工具返回结果、补了多少段检索材料直接决定模型的表现和成本。上下文快满了模型可能“忘记”最早的任务要求回答就会跑偏token 消耗暴涨成本核算就会失控。所以我在每个 span 上都要记录三类 tokenprompt_tokens、completion_tokens、cached_tokens。以及上下文窗口使用率。这些数据聚合起来会让你发现很多诡异现象某个 agent 明明只有一个简单任务结果每次 prompt 都注入了几万字的系统提示词因为框架默认把一堆工具 schema 全塞进去了。你不做成本观测这种浪费根本看不见等账单出来才傻眼。3. 实操过程搭建一套 Multi-Agent 可观测性栈3.1 技术选型与整体架构我的路线是“开源标准 轻量自研”。首选 OpenTelemetry 做采集端它已经成了可观测性的事实标准SDK 覆盖 Python、Node.js、Go 等主流语言。采集到数据后统一交给 OpenTelemetry Collector 做过滤、采样、脱敏再分别发到下游存储。日志可以放 Elasticsearchtrace 可以先由 Collector 直接导出到 Jaeger 或 Tempo指标走 Prometheus最后统一用 Grafana 看面板和接收告警。整体数据流长这样Agent Runtime - OpenTelemetry SDK - OTel Collector - 下游存储(ES/Jaeger/Tempo/Prometheus) - Grafana选型时别一上来就铺太重的链路。如果你们团队只有两三个人我建议先只接日志和 trace 两层prometheus 指标可以先用现成的计数器顶一阵。先体验一下“排障从盲猜变成有据可查”的差距再逐步补存储和告警。过早把分布式架构铺全运维负担会反过来吃掉开发效率。Collector 层有一个关键配置采样和脱敏。Agent 系统的日志量天然巨大因为每个任务的 prompt、response、tool 输出都可能达到几万 token。全部落库要么烧钱要么拖垮存储。我的默认策略是正常链路按 10% 采样错误链路和审计链路 100% 全量。这里用的是 tail_sampling 处理器等 trace 跑完再决定采不采样避免把不完整的 trace 留一半。processors: tail_sampling: decision_wait: 10s policies: - name: errors-full type: status_code status_code: status_codes: [ERROR, UNSET] - name: normal-sample type: probabilistic probabilistic: sampling_percentage: 10脱敏则放在采样之前因为没处理过的原始数据可能已经落过一遍了。Agent 系统会触碰大量用户输入里面经常裹着手机号、邮箱、密钥不能原样写进日志。用 Collector 的 attributes processor 或 transform processor 做一次替换能挡住大多数低级泄露事故。3.2 采集端接入与关键代码细节以 Python 为例给 agent 编排器埋点是件非常顺手的事。下面这段代码包装了一个最基础的 agent 运行过程from opentelemetry import trace from opentelemetry.trace import Status, StatusCode tracer trace.get_tracer(agent.orchestrator) def run_agent(agent_name: str, payload: dict, task_context): with tracer.start_as_current_span(fagent.run.{agent_name}) as span: span.set_attribute(task_id, task_context.task_id) span.set_attribute(input_preview, truncate(payload.get(input, ), 500)) start time.time() try: result agent.run(payload) span.set_attribute(output_preview, truncate(result.get(output, ), 500)) span.set_status(StatusCode.OK) return result except Exception as e: span.record_exception(e) span.set_status(Status(StatusCode.ERROR, str(e))) metric_agent_errors.labels(agentagent_name).inc() raise finally: span.set_attribute(duration_ms, (time.time() - start) * 1000)这段代码里有几个细节都是踩过坑之后才加的。第一input_preview 和 output_preview 要截断不能把完整内容全塞进 span 属性。否则 trace 存储会爆炸而且敏感信息更容易被曝光。取五百字足够定位大多数问题。第二异常一定要 record_exception 并且 set_status否则追踪平台里链路显示是绿的但实际已经失败特别误导人。第三错误指标要在异常分支里明确加一条这是一个原始的信号入口后面告警全靠它。另一个高频场景是消息队列的 trace 上下文传递。使用 OpenTelemetry 自带 API 就能解决from opentelemetry import propagate def enqueue_task(task: dict): carrier {} propagate.inject(carrier) task[trace_carrier] carrier queue.put(task) def process_next_task(): task queue.get() carrier task.pop(trace_carrier, {}) ctx propagate.extract(carrier) with trace.use_span(trace.get_current_span(ctx), end_on_exitFalse): run_agent(task[agent_name], task[payload], task_context(task))生产端把当前 trace 上下文注入到 carrier消费端再取出来包一层这样异步任务就能续上同一个 trace。如果不做这一步一条完整任务在队列处断成两截排查时你会看到两个孤立的 trace根本对不上号。当时我排查一个“任务跑到一半莫名消失”的问题折腾了很久最后就是靠把 queue 这一环打通之后才发现是某个消费者抛异常后直接把任务丢了完全没重新入队。3.3 仪表盘、面板与告警规则设计仪表盘别堆指标。堆太多图只会让面板变成装饰品真正排障时没人看。我建议分为三层总览层放任务量、成功率、平均耗时、token 总量给你一个“系统现在到底健康不健康”的快速判断。链路层放最近一小时的 trace 列表支持按 agent_name、status、task_type 筛选。线上出问题先看这里。明细层点开某一条 trace看完整 span 列表、输入输出摘要、工具调用序列、token 消耗明细。告警规则的三档设计是我自己趟出来的方案。最简单的做法是任务失败率超过 5% 就告警但并发量低的时候一次失败就能把比例打上去直接误报。后来我改成了“短窗口快速发现 长窗口确认升级”的策略。告警级别触发条件响应方式Warning最近 5 分钟错误率超过 10%且任务数不少于 10只生成告警卡片不打扰人Critical最近 15 分钟错误率超过 10%或 P95 延迟超过阈值通知到 IM 群值班人跟进Page持续 30 分钟或错误率超过 30%电话级别的紧急通知立即介入这个分级逻辑的核心是“确认事件已经持续一段时间而不是单点抖动”。Multi-Agent 系统里模型调用偶尔超时是正常的不必一遇挫折就报警。报警的目的是让人做出有效动作而不是制造恐慌。我见过太多团队被 prometheus 的默认规则搞得天天爆炸最后所有人把所有告警都静音了真出事反而没人理。4. 常见问题与排查技巧实录4.1 结果错误不报错如何定位“答非所问”Agent 系统最常见的故障不是崩溃而是“任务完成了答案不对”。这类问题看着像玄学实际上用可观测性排查逻辑非常清晰。我的排查顺序是先看 trace 里每个关键节点的输入输出摘要确认上下文有没有被正确传递再看这一步到底有没有把该看的资料放进 prompt最后才怀疑模型本身的稳定性。有一次我们遇到用户问 A系统回答 B查了半天找不到原因。后来展开 trace 输入预览发现一个 agent 在更新上下文时把旧的系统提示词追加进了用户消息里。也就是说传给模型的 user prompt 变成了“你是某某助手现在用户问你的是……”。问题本身是代码层面的 bug但只靠看传统日志根本发现不了因为整个链路没有报错全是成功状态。要不是 trace 里保留了 input_preview这种问题大概率要拖几天。我后来整理了一份排查清单基本能覆盖八成“答非所问”检查 trace 中每个 span 的输入输出预览看上下文有没有被覆盖或丢失。检查 prompt_tokens 和 cached_tokens看是不是缓存失效导致模型没吃到最新内容。如果是 RAG 场景检查检索结果有没有真正进入最终的 prompt。如果前面都正常再考虑是不是模型随机性导致的此时可以重跑一次做对比。4.2 编排死循环与模型调用超时定位Multi-Agent 系统天然容易出现失控循环。最典型的是 agent A 调用 agent BB 发现信息不够又回去调用 A形成父子循环或者某个 reflection 流程不停重试永远不收敛。表现是任务一直不结束CPU 不高内存也不高但就是挂着不动。我的对策是在编排器层加强制约束每个任务设置全局超时每个 agent 设置最大调用次数和最大嵌套深度。超出直接中断并把中断原因写进 trace。同时给每个 span 记录 attempt_count 指标这个指标可以直观反映“某个 agent 是不是一直在重试”。上次线上有个任务跑了一个多小时没结束就是靠 trace 里的一长串 attempt_count 定位到 reflection agent 在死循环接着改掉它的停止条件才解决。模型调用超时的定位反而简单一些常见原因是上下文太长导致首 token 延迟飙升。我们把每次模型调用的上下文占用率记录下来超过阈值就触发“先压缩再调用”的逻辑。有人可能觉得直接提上限就行但实际上很多模型在上下文接近上限时表现会明显退化单纯加长窗口并不能真正解决问题。可观测性在这里的价值是让你提前看到“上下文占用正在涨”而不是等故障发生了才去猜。4.3 日志爆炸和敏感信息泄露的处理如果一开始不加节制Agent 系统的日志量会很快失控。每个模型请求的完整 prompt 动辄几千 token一次任务跑下来几个 MB 都很正常。不加采样Elasticsearch 磁盘几天就被打满。前面提过的 tail_sampling 就是一种手段但还不够还要有一个“数据分层保留”的策略。热数据保留 7 天冷数据保留 30 天超期自动删除。审计或诉讼场景可能要求更长的保留期那就在合法合规的前提下单独走长期归档。敏感信息这关必须做扎实。因为 Agent 系统会把用户输入、工具返回、模型输出全都沉淀下来中间很可能夹着密码、密钥、身份证号、手机号。我的处理原则是能脱敏的就脱敏不能脱敏的原始报文默认不采集。敏感字段类型脱敏方式备注手机号正则替换为 138****1234同时保留地区前缀可辅助排查邮箱保留前缀首个字符和后缀例如 a***example.comAPI Key / Token全部替换为 ***不应出现在任何存储中完整会话明文默认不采集除非有明确审计需求脱敏这件事要在采集端做不能等数据落库了再靠脚本清洗。更严谨一点还可以在 OpenTelemetry Collector 里用 attributes processor 对敏感字段做同一套替换逻辑确保不管从哪里进的数据都过一遍这个规则。4.4 本地调试与快速回放的小工具经验线上监控体系再完善本地 debug 也还是离不开。我最常用的技巧是“trace 回放”把一次任务的 trace 数据从追踪平台导出成 JSON然后在本地脚本里重放一遍看同一个 prompt 在哪个环节产生了不同的输出。这有点像做回归测试但针对的是模型行为而不是纯逻辑。具体做法不复杂跑一次任务把每个 span 的输入和输出预览保存下来导出成一个 mini trace 文件。本地调试时把这个文件解析成一连串步骤用 mock 或直接调用模型逐步执行对比各步骤的输出差异。这样做的好处是不用等模型跑到一半你可以随时暂停、改 prompt、换模型版本然后重跑。对“同样的输入为什么线上和本地结果不一样”这类问题几乎是秒杀。另外一个建议是把关键编排器的链路设计写进单元测试。虽然 Multi-Agent 系统没法完全像纯函数那样测试但至少可以断言“调用 agent 之后 trace 里有预期的 span 和属性”。这样做能防止后面重构时观测埋点被无意间丢掉。观测埋点一旦缺失后续排障又要退回到“猜谜模式”代价远大于当初那十几行代码的成本。5. 一些更深的体会最后说几点我做这套系统下来比较真实的感受。监控和调试 Multi-Agent 系统本质上不是“选个工具”的问题而是把“事件、链路、指标”这三件事想清楚再落地的过程。工具是现成的OpenTelemetry 也好、Prometheus 也好都只是承载思路的容器。真正决定排障效率的是你在哪些节点打了事件、对哪些维度做了统计、告警阈值定得是否合理。我现在最得意的一件事不是面板多好看而是新同学接手系统时不用再追着我问“为什么这个 agent 不干活”。给他一个 trace ID他自己就能看完整条执行过程。这种经验一旦沉淀下来对整个团队的效率提升是长期的。如果你现在正被困在“日志一堆但说不清问题在哪”的状态别急着加更多日志先把结构化和追踪架起来。后面你会回来感谢今天的自己。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门