前阵子在做一个复杂的 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 上都要记录三类 token:prompt_tokens、completion_tokens、cached_tokens。以及上下文窗口使用率。这些数据聚合起来,会让你发现很多诡异现象:某个 agent 明明只有一个简单任务,结果每次 prompt 都注入了几万字的系统提示词,因为框架默认把一堆工具 schema 全塞进去了。你不做成本观测,这种浪费根本看不见,等账单出来才傻眼。
3. 实操过程:搭建一套 Multi-Agent 可观测性栈
3.1 技术选型与整体架构
我的路线是“开源标准 + 轻量自研”。首选 OpenTelemetry 做采集端,它已经成了可观测性的事实标准,SDK 覆盖 Python、Node.js、Go 等主流语言。采集到数据后,统一交给 OpenTelemetry Collector 做过滤、采样、脱敏,再分别发到下游存储。日志可以放 Elasticsearch,trace 可以先由 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(f"agent.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(agent=agent_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_exit=False): 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 B,B 发现信息不够,又回去调用 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.com |
| API 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,他自己就能看完整条执行过程。这种经验一旦沉淀下来,对整个团队的效率提升是长期的。如果你现在正被困在“日志一堆但说不清问题在哪”的状态,别急着加更多日志,先把结构化和追踪架起来。后面你会回来感谢今天的自己。