ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

多Agent协作测试通过却系统崩溃?四个致命缝隙与可观测性解决方案

多Agent协作测试通过却系统崩溃?四个致命缝隙与可观测性解决方案

1. 多 Agent 协作的“测试通过”幻觉

1.1 一个让无数团队栽跟头的真实场景

你带了一个小团队做多 Agent 系统,每个 Agent 单独跑测试的时候都是绿的。规划 Agent 的单元测试覆盖率 92%,检索 Agent 的集成测试全过,执行 Agent 的端到端测试也没毛病。CI 流水线上三盏绿灯亮得让人安心。然后你把它们拼在一起,跑一个真实任务——系统崩了。不是那种报错崩,是那种“每个 Agent 都说自己没问题,但最终输出驴唇不对马嘴”的崩。

这个场景我在过去一年里见过太多次了。多 Agent 协作系统最反直觉的地方就在这里:局部正确性完全不保证全局正确性。传统软件工程里我们习惯了“单元测试过了,集成测试大概率也能过”的思维惯性,但这套逻辑在多 Agent 系统里几乎完全失效。原因不复杂——Agent 之间的交互不是简单的函数调用,而是带有语义模糊性的自然语言通信,每一次消息传递都是一次信息压缩和再解释的过程。

这篇文章我想把这个问题彻底拆开讲清楚。适合谁看?如果你正在做或者准备做多 Agent 系统,不管是用现成框架还是自己搭,只要你遇到过“单测全绿但系统行为诡异”的情况,这篇内容就是写给你的。我会从设计思路、核心细节、实操过程、问题排查四个维度展开,把“为什么每个 Agent 都测过了系统还是崩”这个问题从根上解释清楚,并给出可直接参考的解决方案。

1.2 为什么“单独测试”在多 Agent 场景下天然不够用

先说一个核心认知:多 Agent 系统的 bug 大部分不在 Agent 内部,而在 Agent 之间的“缝隙”里。你单独测一个 Agent,测的是它的输入输出映射关系。但在真实协作中,一个 Agent 的输出会变成另一个 Agent 的输入,而这个输入的质量、格式、语义完整性,在单独测试时根本没有被验证过。

举个具体例子。假设你有一个“需求分析 Agent”和一个“任务拆解 Agent”。需求分析 Agent 的测试用例是:输入一段用户需求描述,输出结构化的需求列表。测试通过。任务拆解 Agent 的测试用例是:输入结构化的需求列表,输出任务清单。测试也通过。但真实运行时,需求分析 Agent 输出的“结构化需求列表”里可能包含了一些模糊描述,比如“提升用户体验”这种没法直接拆解的条目。任务拆解 Agent 在面对这种输入时,不会报错,它会“尽力拆解”,生成一堆看起来合理但实际不可执行的任务。整个系统没有崩溃,但输出质量已经烂了。

这就是所谓的语义缝隙——每个 Agent 在自己的测试分布内表现良好,但协作时输入分布发生了偏移,偏移到了某个 Agent 没见过的区域。传统软件里这种问题靠接口契约解决,但 Agent 之间的“接口”是自然语言,契约天然模糊。

2. 核心细节解析:多 Agent 协作的四个致命缝隙

2.1 缝隙一:消息格式的“薛定谔兼容”

多 Agent 系统里,Agent 之间传递的消息格式往往没有严格约束。你可能在 prompt 里写了“请输出 JSON 格式”,但 LLM 的输出是不确定的。今天输出纯 JSON,明天可能带个 markdown 代码块标记,后天可能在 JSON 前面加一句“好的,以下是结果”。

我做过一个统计:在一个包含 5 个 Agent 的协作系统里,连续运行 200 次任务,Agent 间消息格式出现偏差的概率大约是 17%。也就是说每 6 次任务就有 1 次会出现格式问题。单独测试的时候你用的是固定输入,格式偏差根本不会暴露。但协作运行时,上游 Agent 的输出直接喂给下游 Agent,格式偏差就会级联放大。

解决这个问题的核心思路是在 Agent 之间加一层“消息规范化”处理,而不是指望每个 Agent 都严格输出规范格式。具体做法后面实操部分会详细讲。

2.2 缝隙二:上下文窗口的“信息截断”

这是最隐蔽的杀手。多 Agent 协作时,每个 Agent 都有自己的上下文窗口限制。当上游 Agent 的输出很长,或者协作链路很长时,下游 Agent 拿到的信息可能是被截断的。

更麻烦的是,截断往往发生在“看起来不重要”的地方。比如一个任务规划 Agent 输出了 10 个步骤,每个步骤有详细说明。下游执行 Agent 的上下文窗口只够放前 7 个步骤的完整信息,后 3 个步骤被截断了。执行 Agent 不会报错,它会基于前 7 个步骤“脑补”剩余部分。结果就是系统跑完了,但漏掉了关键步骤。

注意:上下文截断不会产生任何错误日志。这是它最危险的地方。你只能通过对比上游输出和下游实际接收到的内容来发现。

2.3 缝隙三:语义漂移的累积效应

多 Agent 协作链路越长,语义漂移越严重。每个 Agent 在理解上游输出时都会做一次“解释”,每次解释都可能引入微小偏差。3 个 Agent 的链路可能偏差还不明显,5 个以上就开始显著了。

我做过一个实验:让 5 个 Agent 依次传递一句话,每个 Agent 的任务是“用自己的话复述上一句”。传到第 5 个 Agent 时,原始信息已经面目全非。真实的多 Agent 系统虽然不会这么极端,但原理是一样的——每次跨 Agent 传递都是一次有损压缩。

2.4 缝隙四:错误传播的“雪崩效应”

单个 Agent 出错时,如果错误没有被及时捕获,会沿着协作链路传播并放大。上游 Agent 的一个小错误,到了下游可能变成完全离谱的输出。更糟糕的是,下游 Agent 往往会把上游的错误输入当作“事实”来对待,基于错误前提继续推理。

这四种缝隙有一个共同特点:它们都不会导致系统抛出异常。系统会“正常运行”,只是输出质量不可控。这就是为什么“每个 Agent 都测过了”和“系统还是崩”可以同时成立。

3. 实操过程:构建可观测的多 Agent 协作系统

3.1 第一步:建立 Agent 间消息的“契约层”

不要指望每个 Agent 都严格输出规范格式。正确的做法是在 Agent 之间加一层消息规范化处理。具体实现方式:

import json import re def normalize_message(raw_output: str, expected_schema: dict) -> dict: """ 将 Agent 的原始输出规范化为标准格式。 处理常见问题:markdown代码块包裹、前后多余文本、字段缺失。 """ # 去除 markdown 代码块标记 cleaned = re.sub(r'```(?:json)?\s*', '', raw_output) cleaned = cleaned.strip() # 尝试提取 JSON 部分 json_match = re.search(r'\{.*\}', cleaned, re.DOTALL) if json_match: cleaned = json_match.group() try: parsed = json.loads(cleaned) except json.JSONDecodeError: # 尝试修复常见 JSON 错误 cleaned = cleaned.replace("'", '"') cleaned = re.sub(r',\s*}', '}', cleaned) cleaned = re.sub(r',\s*]', ']', cleaned) try: parsed = json.loads(cleaned) except json.JSONDecodeError: return {"error": "PARSE_FAILED", "raw": raw_output} # 校验必需字段 for field in expected_schema.get("required", []): if field not in parsed: parsed[field] = expected_schema.get("defaults", {}).get(field) return parsed

这层处理的核心价值在于:把“格式不确定性”这个变量从协作链路中消除掉。每个 Agent 的输出都经过规范化再传给下游,下游 Agent 拿到的永远是标准格式。

3.2 第二步:给每个 Agent 的输出加“质量标记”

光有格式规范化还不够,你还需要知道每个 Agent 输出的“语义质量”。具体做法是让每个 Agent 在输出时附带一个置信度评估:

AGENT_OUTPUT_TEMPLATE = """ 请完成以下任务,并按照指定格式输出: 任务:{task_description} 输出格式: {{ "result": "你的任务结果", "confidence": 0.0-1.0之间的浮点数,表示你对这个结果的置信程度, "assumptions": ["你在完成任务时做的关键假设"], "missing_info": ["你希望上游提供但没提供的信息"] }} """

这个模板的关键在于assumptions和missing_info两个字段。它们让 Agent 的“内心活动”变得可观测。当系统输出异常时,你可以回溯每个 Agent 的假设和缺失信息,快速定位问题出在哪个环节。

3.3 第三步:实现协作链路的“全链路追踪”

多 Agent 系统必须有完整的消息追踪能力。每次 Agent 间消息传递都要记录:发送方、接收方、原始输出、规范化后输出、时间戳、token 消耗。

class CollaborationTracer: def __init__(self): self.trace_log = [] def record(self, from_agent: str, to_agent: str, raw_output: str, normalized_output: dict, token_usage: dict): self.trace_log.append({ "from": from_agent, "to": to_agent, "raw_length": len(raw_output), "normalized_keys": list(normalized_output.keys()), "has_error": "error" in normalized_output, "confidence": normalized_output.get("confidence"), "assumptions": normalized_output.get("assumptions", []), "missing_info": normalized_output.get("missing_info", []), "token_usage": token_usage, "timestamp": time.time() }) def detect_anomalies(self) -> list: """检测协作链路中的异常模式""" anomalies = [] for i, entry in enumerate(self.trace_log): # 检测置信度骤降 if entry["confidence"] and entry["confidence"] < 0.5: anomalies.append({ "type": "LOW_CONFIDENCE", "position": i, "agent": entry["from"], "detail": f"置信度仅 {entry['confidence']}" }) # 检测信息缺失累积 if len(entry["missing_info"]) > 2: anomalies.append({ "type": "INFO_STARVATION", "position": i, "agent": entry["from"], "detail": f"缺失 {len(entry['missing_info'])} 项关键信息" }) return anomalies

这套追踪机制的价值在于:它把“系统崩了”这个模糊现象转化为可定位的具体问题。你不再需要猜测哪个环节出了问题,追踪日志会直接告诉你。

3.4 第四步:设计“协作级”测试用例

单独测试每个 Agent 是不够的,你需要专门设计针对协作的测试用例。这类测试用例的核心特征是:输入是模糊的、不完整的、有歧义的,模拟真实场景中上游 Agent 可能产生的“不完美输出”。

我常用的协作测试用例设计方法:

测试类型输入特征预期行为检测目标
模糊输入测试包含“提升体验”等不可执行描述下游 Agent 应主动追问或标记语义缝隙
超长输入测试输入长度接近上下文窗口上限系统应截断并标记,而非静默丢失上下文截断
格式偏差测试输入包含 markdown 包裹、多余文本规范化层应正确处理格式兼容性
矛盾输入测试输入包含相互冲突的信息系统应检测并报告矛盾错误传播
链路压力测试5 个以上 Agent 串联语义漂移应在可接受范围内累积效应

这些测试用例跑一遍,你就能发现大部分“单测通过但系统崩”的问题。

4. 常见问题与排查技巧实录

4.1 问题速查表

现象可能原因排查方法解决方案
系统输出质量不稳定语义漂移累积对比首尾 Agent 输出减少链路长度或加中间校验
某个 Agent 频繁输出空结果上游信息被截断检查上游输出长度增加摘要层或分段传递
输出格式时好时坏消息格式未规范化检查原始输出加规范化层
系统“正常”但结果离谱错误传播未捕获检查各 Agent 置信度加质量门控
协作链路越长效果越差上下文窗口不足统计各环节 token 消耗优化 prompt 或分段处理

4.2 三个我踩过的坑

坑一:以为 prompt 里写了“必须输出 JSON”就够了。实测下来,LLM 在长对话或复杂任务中违反格式要求的概率远高于预期。后来我学乖了,所有 Agent 间通信都走规范化层,prompt 里的格式要求只是“第一道防线”,规范化层是“兜底”。

坑二:忽略了 token 消耗的累积效应。多 Agent 协作时,每个 Agent 的输入都包含上游的完整输出。5 个 Agent 串联,最后一个 Agent 的输入可能是第一个的 5 倍长。这不仅增加成本,还容易触发上下文截断。后来我在每个 Agent 之间加了“摘要压缩”步骤,把上游输出压缩到关键信息再传递。

坑三:没有给 Agent 设“拒绝回答”的选项。早期设计时,我要求每个 Agent 必须输出结果。结果就是当输入质量差时,Agent 会“硬编”一个看起来合理但实际错误的结果。后来我在 prompt 里加了“如果输入信息不足以完成任务,请输出INSUFFICIENT_INFO并说明缺少什么”。这个改动让系统的错误率直接降了一半。

4.3 一个实用的调试技巧

当你遇到“系统崩了但不知道哪里崩”的情况时,用这个二分法排查:

  1. 把协作链路从中间切开,只跑前半部分,检查中间输出质量
  2. 如果前半部分输出正常,问题在后半部分;反之在前半部分
  3. 对有问题的那一半继续二分,直到定位到具体 Agent

这个方法听起来笨,但实测下来比看日志快得多。因为多 Agent 系统的日志往往非常冗长,人工排查效率很低。二分法能把排查时间从小时级降到分钟级。

5. 多 Agent 协作的设计原则与经验总结

5.1 四条核心设计原则

原则一:永远不要信任上游 Agent 的输出。每个 Agent 在接收上游输入时,都应该做基本的有效性校验。这不是不信任,而是工程上的防御性设计。

原则二:协作链路越短越好。每增加一个 Agent,语义漂移和错误传播的风险就增加一层。能用 3 个 Agent 解决的问题,不要用 5 个。

原则三:每个 Agent 都要有“我不知道”的能力。强制 Agent 输出结果是最糟糕的设计。给 Agent 拒绝回答的权利,系统整体可靠性会显著提升。

原则四:可观测性优先于性能优化。多 Agent 系统的调试成本远高于单 Agent。在优化性能之前,先把追踪和监控做扎实。

5.2 关于测试策略的最终建议

回到标题的问题:“每个 Agent 都测过了,系统还是崩”。这个问题的本质是测试粒度错配——你用 Agent 级的测试覆盖了 Agent 级的正确性,但系统崩在 Agent 之间的交互上。

正确的测试策略应该是三层:

  • Agent 级测试:验证单个 Agent 在标准输入下的输出质量
  • 接口级测试:验证 Agent 间消息传递的格式兼容性和信息完整性
  • 协作级测试:验证多 Agent 在模糊、不完整、有歧义输入下的整体行为

大部分团队只做了第一层,所以才会出现“单测全绿但系统崩”的情况。把后两层补上,这个问题就能从根本上解决。

我个人在实际操作中的体会是:多 Agent 系统的可靠性不取决于最聪明的那个 Agent,而取决于最薄弱的那个交互环节。与其花时间优化单个 Agent 的 prompt,不如花时间加固 Agent 之间的“缝隙”。这个投入产出比高得多。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进