做过多智能体系统的人大概都有这个体会:搭一个能跑起来的 demo 只要一个下午,真正让它稳定干活要磨三个月。多智能体系统这个词这两年被说得有点泛滥,很多团队一上来就拆出七八个角色、画一张漂亮的协作图,结果跑起来不是死循环就是互相甩锅,最后还不如一个大模型加两段提示词靠谱。我前后参与过几套不同形态的多智能体落地,从任务编排型到仿真推演型都趟过一遍,最深的感受是:这东西的难点从来不在"让多个模型说话",而在于信息怎么流动、权限怎么切分、什么时候该停。
这篇内容想做的事很明确:把多智能体系统从概念到可跑原型的整条链路拆开讲清楚,包括它的适用边界、核心概念的真实含义、架构分层的取舍逻辑、最小原型的代码骨架、关键参数怎么算,以及我在实际调试中反复遇到的几类故障和对应排查手法。适合两类人看:一类是刚接触这个概念、想知道它到底解决什么问题的新手;另一类是已经写过 demo、但卡在稳定性和成本上的开发者。涉及具体实现的部分,我会给可以直接改改就用的代码结构和参数表,不讲虚的。
1. 多智能体系统到底该用在哪:先划清能力边界
1.1 单智能体撑不住的三条信号线
判断要不要上多智能体,我的经验是看三条信号线,满足其中两条以上再考虑拆分,否则纯属给自己找麻烦。
第一条是任务本身存在明显的信息隔离需求。比如一个系统既要读内部知识库,又要调外部检索,还要做代码执行,如果全塞进一个上下文,不同来源的信息会互相污染,模型的注意力被稀释,输出质量断崖式下跌。这种情况拆成独立角色、各自只持有自己那份上下文,效果提升是肉眼可见的。
第二条是流程中存在需要独立校验的环节。写代码的不管测试,写文案的不管审核,这种"生产者—检查者"结构在多智能体里非常常见。它的价值不在于模型变聪明了,而在于检查者拿到的是一个干净的新上下文,不会被生产者的推理过程带偏。这一点在单智能体自检里几乎做不到,因为它会倾向于维护自己前面的判断。
第三条是任务可以被清晰地并行化。比如对一批文档做提取,十个文档之间没有依赖关系,那并发跑十个执行体就是纯粹的吞吐收益。反过来,如果任务是强串行的、每一步都依赖上一步的完整输出,拆多智能体只会增加通信开销和出错概率。
反过来讲,什么情况千万别拆:任务链路短、步骤少于三步的;需要全局一致视角的(比如整体风格把控);以及你还没把单智能体的提示词调到极限的。我见过太多案例,单智能体效果不好,直接归因于"能力不够",转头上多智能体,结果基础问题一个没解决,还多了一堆协调成本。
1.2 四类典型场景与各自的真实痛点
把常见场景摊开看,多智能体的介入点其实差异很大,不能一套架构打天下。下面这张表是我自己整理的一个对照,列的是场景、单体的瓶颈、多体的介入点和必须承受的代价。
| 场景类型 | 单智能体的典型瓶颈 | 多智能体的介入点 | 必须承受的代价 |
|---|---|---|---|
| 长流程任务编排 | 上下文随步骤线性膨胀,后期指令遵循度下降 | 按阶段切分角色,每段只保留必要状态 | 状态传递需要显式设计,容易丢信息 |
| 检索与生成分离 | 检索结果和生成指令混在一起,干扰严重 | 检索体只输出结构化片段,生成体专注写作 | 两段之间要做格式约束,否则信息失真 |
| 多轮评审与修订 | 自评倾向于自我肯定,改不动 | 独立评审体拿到干净上下文,给出具体问题 | 评审标准要外置,否则评审体也会跑偏 |
| 批量并行处理 | 串行耗时随数量线性增长 | 并发执行体,统一汇总 | 并发控制、失败重试、结果合并都要自己做 |
拿"检索与生成分离"这条举例,很多人以为拆开就是把检索结果塞给生成体,实际上真正起作用的是中间的格式契约。检索体输出的必须是带来源标记的结构化片段,生成体只被允许引用这些片段,不允许自由发挥。这个约束一旦缺失,多智能体就退化成了两次模型调用,一点增益都没有。
再看"多轮评审",这里有个反直觉的点:评审体的提示词不能写"请检查是否有错误",而要写"逐条对照以下五条标准,指出不符合的条目和原文位置"。前者会让评审体顺着作者的思路走,后者才逼它做逐项核对。这个差别我调了很久才摸清楚,本质上是用结构化输出约束替代了模型的自由判断。
注意:划边界这件事最好在动手写代码前做完,并且写成一句话的需求描述。如果你没法用一句话说清"哪个角色负责哪段、交接物是什么格式",那说明这个拆分还没想清楚,先别动手。
2. 核心概念拆解:角色、通信、协调、共识的真实含义
2.1 角色划分的本质是切分权限与信息边界
新手最容易踩的坑,是把角色理解成"给模型写一段人设"。写一句"你是一位资深架构师",并不会让模型真的具备架构师的能力,它只是改变了输出的语气。在多智能体系统里,角色的真实含义是权限集合加信息子集的绑定。
具体说,一个角色定义至少包含四样东西:它能访问哪些数据源、它能调用哪些工具、它的输入上下文包含哪些字段、它的输出必须符合什么结构。这四样定死了,角色才是可执行的;否则就是一段漂亮的提示词,运行时该出错还是出错。
我一般会用一个角色清单表来固化这件事,写代码的时候直接照着表生成配置:
| 字段 | 说明 | 反例 |
|---|---|---|
| 数据域 | 只允许访问的库表或文档集合 | 写"可访问全部资料",等于没限制 |
| 工具白名单 | 可调用的外部能力列表 | 写"按需调用工具",运行时会乱调 |
| 输入契约 | 上游必须提供的字段与类型 | 写"根据上下文判断",无法校验 |
| 输出契约 | 必须返回的结构与字段 | 写"给出结果",下游无法解析 |
这张表的价值在于,它把模糊的"角色"变成了可以校验的东西。上游输出不符合契约,调度器直接打回,不用等到最后生成出问题再回头查,那时候已经很难定位是哪一步开始偏的。
另外一个细节:角色数量不要贪多。我的经验阈值是首版不超过四个执行体加一个调度体。角色越多,通信路径是成倍增长的,四个角色两两通信是六条边,八个角色就是二十八条边,每一条边都是一个可能出错的地方。先跑通最短路径,再按实际瓶颈加角色,这个顺序不能反。
2.2 通信机制的三条主流路线与选型依据
通信是多智能体系统里最容易被低估的部分。很多人默认"角色之间直接对话"就行,实际落地时必须选定一个明确的通信范式,否则系统状态会变得不可观测。我见过的实现基本落在三条路线上。
第一条是点对点消息传递。每个角色有独立收件箱,消息带发送者、接收者、类型和载荷,调度器负责投递。这条路线的优点是链路清晰,每条消息都可以打日志、可以重放、可以单独做超时控制;缺点是当角色数量上升时,路由逻辑会变复杂,容易出现消息在某些角色之间来回弹的情况。
第二条是共享黑板。所有角色读写同一块结构化状态区,谁改了哪个字段都记录版本。优点是天然支持多方协作,不需要两两建连接;缺点是并发写入要做冲突处理,而且要防止某个角色把整个黑板读完导致上下文爆炸。我在做需要多方共同产出的任务时更偏向这种方式,因为最终产出本来就是多份贡献拼合的结果。
第三条是事件总线。角色不指定接收者,只发布事件,谁关心谁订阅。优点是扩展性最好,加一个角色不用改现有代码;缺点是链路追踪变难,一个事件被多个角色消费后,很难一眼看出最终结果是被谁改坏的。
选型上我的判断标准很朴素:链路能不能被追踪,比性能重要得多。所以首版我基本都从点对点消息起步,等角色数量和交互复杂度上来了,再考虑往事件总线迁。共享黑板则适用于那种"多人共同编辑一份产物"的场景,其他场景用它反而增加复杂度。
不管选哪条路线,有三个东西必须从第一天就有:消息的唯一标识、贯穿全链路的一次追踪号、以及每条消息的时间戳和状态。缺了这些,出问题的时候你只能靠打印日志猜。
2.3 协调与共识:谁在什么时候说了算
协调机制决定的是"下一步谁干活",共识机制决定的是"多个角色意见不一致时听谁的"。这两件事经常被混为一谈,实际上要分开设计。
协调层面,常见做法有两类。一类是中心调度,有一个明确的调度角色,根据当前状态决定派发任务给谁,所有流转都经过它。优点是逻辑集中、容易调试、容易加全局约束(比如总轮次上限、总预算上限);缺点是调度体本身会成为瓶颈和单点,调度体的判断质量直接决定整个系统的上限。另一类是去中心协商,角色之间自己发起请求,调度层只做路由。这种灵活度高,但调试难度陡增,我不建议首版就用。
共识层面,要看你的任务是不是真的需要"一致"。很多场景其实不需要共识,只需要仲裁——比如评审体和生产体意见不同,不需要它们讨论出统一结论,只需要一个明确的规则:评审体给出具体的不符合项,生产体必须逐条回应,回应不了的按评审体意见改。这个规则是硬编码的,比让两个模型自由辩论稳定一个数量级。
真正需要共识的场景是那种"多方独立判断后要合并"的任务,比如三个抽取体分别从同一份材料里提取关键信息,结果不一致时要合并。这时候我用的通常是多数加置信度加权,而不是让模型自己辩论达成一致。原因很实际:模型之间的辩论很容易收敛到那个表达最强势的角色身上,而不是最正确的那个。
提示:把"谁说了算"写成规则而不是交给模型判断,是多智能体系统从能跑变成能用的关键一步。凡是能用 if-else 表达清楚的决策,不要交给模型。
3. 架构分层与技术选型:每一层该管什么
3.1 编排层的职责边界与三种拓扑取舍
编排层是整个系统的大脑,但它该管多少事,很多实现没想清楚。我的划分是:编排层只负责状态机推进、任务派发、终止判定这三件事,不负责具体业务逻辑,也不直接生成内容。业务逻辑放在执行体里,编排层最多做结构校验。
拓扑结构上,实际用得最多的是三种。
星型(中心调度):一个调度体,N 个执行体,所有消息经调度体中转。适合流程线性、阶段清晰的场景。实现最简单,可观测性最好,是我默认的首选。缺点是调度体压力大,且调度体的提示词要写得非常克制,不能让它自己下场干活。
层级型:调度体下面再挂子调度体,每个子调度体管一小队执行体。适合任务可以自然分层的情况,比如一个大任务拆成几个子任务,每个子任务内部又有自己的流程。好处是每层的上下文都被限制在局部,坏处是跨层的信息传递要做额外的摘要,容易在摘要环节丢细节。
网状(对等协作):角色之间直接通信,没有中心。扩展性最好,但状态管理几乎全靠约定,一旦角色数量超过五六个,链路就基本没法追踪了。我一般只在做探索型任务、需要大量自由交互时才考虑,而且一定会加上全局的轮次上限和预算上限兜底。
选择上有个简单的判断法:如果你的流程图能画成一条主线加若干分支,用星型;如果能画成树,用层级型;如果画出来是一团乱麻,说明任务本身还没拆清楚,先回去拆任务。这句话我在评审别人方案时说过很多次,屡试不爽。
编排层还有一个常被忽略的职责:终止条件。这是多智能体系统最容易失控的地方。我通常会在编排层设三道闸:最大轮次、最大总耗时、最大累计消耗。任意一道触发,强制收敛并返回当前最优结果,同时在结果里标记"未完全收敛",让下游知道这个结果的可信度。这三道闸不是可选项,是必须项。
3.2 记忆与状态管理:三层结构别混用
多智能体系统的状态管理和单智能体完全不是一个难度。单智能体只要维护一份对话历史,多智能体要考虑谁在什么时候能看到哪部分历史。我一般采用三层结构。
第一层是单体的工作记忆,就是每个执行体自己那轮对话的上下文。这层是私有的,其他角色看不到。控制这层的关键是定期压缩:当轮次超过一定数量,把早期的交互摘要成一段结论,只保留结论和最近的原始交互,中间的丢弃。不压缩的话,上下文会线性膨胀,到后面模型对最新指令的遵循度会明显下降。
第二层是共享任务状态,是一份结构化的键值区,记录任务的整体进度、每个子任务的产出、当前阻塞点。这层是所有角色都能读的,但写入要走版本控制,每次写入记录是谁改的、改了什么。这层是整个系统的真相来源,调试的时候我第一眼看的就是它。
第三层是长期记忆,跨任务保留的经验、模板、失败案例。这层不是必需的,很多系统根本用不上。真要用的话,关键是检索质量而不是存储量,往里面塞几万条历史记录但检索不准,还不如不放。
三层之间要有明确的升降级规则:工作记忆里的关键结论要显式写入共享状态,否则任务结束就丢了;共享状态里的成功模式可以沉淀到长期记忆,但要有筛选,不能什么都存。
这里有个具体的坑值得说。我曾经做过一个系统,把共享状态设计成"所有角色读全量",结果每个角色每次调用都要带上完整的任务状态,上下文里一大半是和它无关的字段,模型注意力被严重稀释。后来改成"每个角色只读自己声明的字段加全局进度",效果立刻改善。所以状态字段的可见性要做成配置项,默认最小可见,需要时再开。
3.3 工具层与权限隔离:别让所有角色都能调所有工具
工具层是多智能体系统里风险最高的部分,因为一旦某个角色能调用写操作,它出错的代价就不只是"输出不好看",而是"数据被改坏了"。
我的做法是三条规矩。第一,工具按角色白名单分配,在角色定义里写死,运行时校验,不在白名单里的调用直接拒绝,而不是靠提示词约束。提示词约束在模型被长上下文干扰时会失效,代码校验不会。
第二,读写分离。查询类工具可以给多个角色共用,写入类工具只给一个明确的角色,而且这个角色的输入必须经过结构校验。比如"提交结果"这个动作,只允许汇总体调用,执行体只能返回结果对象,不能直接提交。
第三,高风险操作加人工确认点。什么算高风险要按业务定,但一般包括:对外发送、修改持久化数据、产生费用。这些动作在自动流程里要挂一个确认节点,可以是人确认,也可以是规则校验通过才算数。
还有一点容易被忽略:工具的返回值也要做约束。如果某个工具返回一大段原始数据,直接塞进角色上下文,就等于绕过了前面的所有上下文控制。我一般在工具层加一个截断和摘要,返回给角色的永远是结构化的精简结果,完整数据存在共享状态里供需要时取。
4. 动手搭一个最小可运行原型
4.1 需求拆解与角色定义
光讲概念没意思,直接上一个能跑的最小原型。假设我们要做一个"技术资料整理"的任务:给定一批原始材料,产出结构化的要点汇总。这个任务拆成三个角色够了。
抽取体:输入一段材料,输出结构化的要点列表,每条要点带原文位置标记。工具白名单为空,它只做文本处理。
校验体:输入抽取体的输出,逐条核对是否可以在原文中找到依据,输出问题清单。工具白名单为空。
汇总体:输入通过校验的要点集合,合并去重、按主题归类,输出最终结果。工具白名单只有"提交结果"一项。
再加一个调度体,负责派发、收集、判定是否进入下一轮。四个角色,刚好处在我前面说的首版阈值内。
角色定义我会直接写成配置,方便改:
ROLES = { "extractor": { "data_scope": ["chunk"], "tools": [], "input_schema": {"chunk_id": "str", "text": "str"}, "output_schema": {"points": "list[{text, anchor}]"}, }, "verifier": { "data_scope": ["chunk", "points"], "tools": [], "input_schema": {"chunk_id": "str", "text": "str", "points": "list"}, "output_schema": {"issues": "list[{point_idx, reason}]"}, }, "summarizer": { "data_scope": ["valid_points"], "tools": ["submit_result"], "input_schema": {"points": "list"}, "output_schema": {"result": "str"}, }, }这份配置看着简单,但它承担了三件事:运行时校验输入输出字段、决定每个角色能看到哪些数据、限制工具调用。没有这份配置,后面的调度逻辑就无从校验。
4.2 核心调度循环的代码骨架
调度循环是整个系统的骨架,我把它写成纯标准库实现,不依赖任何外部编排组件,方便你直接理解逻辑之后再套到自己的技术栈里:
import time import uuid from dataclasses import dataclass, field from enum import Enum class Status(Enum): PENDING = "pending" RUNNING = "running" DONE = "done" FAILED = "failed" @dataclass class Message: sender: str receiver: str kind: str # task / result / issue payload: dict trace_id: str = field(default_factory=lambda: uuid.uuid4().hex[:8]) ts: float = field(default_factory=time.time) @dataclass class Budget: max_rounds: int = 12 max_seconds: float = 180.0 max_tokens: int = 200_000 used_tokens: int = 0 started_at: float = field(default_factory=time.time) def exhausted(self) -> str | None: if self.used_tokens >= self.max_tokens: return "token_budget" if time.time() - self.started_at >= self.max_seconds: return "deadline" return None def run(task, role_impl, budget: Budget, log): queue = [(0, Message("scheduler", "extractor", "task", task))] rounds = 0 while queue: rounds += 1 if rounds > budget.max_rounds: log("stop", reason="max_rounds", rounds=rounds) break stop = budget.exhausted() if stop: log("stop", reason=stop, rounds=rounds) break _, msg = queue.pop(0) impl = role_impl[msg.receiver] try: out = impl.handle(msg) # 内部做输入校验 except Exception as e: log("role_error", role=msg.receiver, err=repr(e), trace=msg.trace_id) continue budget.used_tokens += out.get("tokens", 0) log("step", role=msg.receiver, trace=msg.trace_id, tokens=out.get("tokens", 0), rounds=rounds) for nxt in out.get("next", []): queue.append((rounds, nxt)) return {"rounds": rounds, "tokens": budget.used_tokens}这段代码里有几个点值得单独说。队列元素带轮次标记,是为了后面做回溯时能定位到具体哪一轮出的问题;trace_id一路透传,让同一个任务的所有消息能串成一条链;异常被捕获后只记录不抛出,是因为单个角色失败不应该让整个任务崩掉,后面会通过重试或者降级处理。这三处设计看起来都是小事,实际调试时能省下大量时间。
4.3 关键参数怎么算:并发、超时、重试与预算
参数不是拍脑袋定的,我用的是一套可以推演的计算方式,拿这个原型举例。
预算是这么估的:假设平均每段材料 1500 字,抽取体的输入约 2000 token,输出约 600 token;校验体输入 1200 token,输出 300 token;汇总体输入是全部要点,假设 20 条要点约 2000 token,输出 1500 token。单份材料走完一轮,消耗约 2000+600+1200+300+2000+1500 = 7600 token。如果一批 20 份材料,串行跑就是 152000 token。这个数是我设总预算的依据,再留 30% 余量,总预算定在 200000 左右比较合理。
并发数怎么定:并发受两个约束夹击,下游的速率限制和上游的失败成本。并发太高容易触发限流,重试反而更慢;并发太低吞吐上不去。我的经验是从 4 开始试,观察失败率和平均延迟,失败率超过 5% 就往下调。不要一上来就 16、32,那不是压测,那是自找麻烦。
超时怎么定:单次调用的超时按 P95 延迟乘 1.5 来定。比如实测 95% 的调用在 8 秒内返回,超时就设 12 秒。整个任务的超时按"预计轮次乘单次超时"再乘 1.2 的系数。这个系数是用来兜住重试的,没有它的话,最后几轮经常被整体超时打断,前功尽弃。
重试策略:只对可重试的错误重试,具体包括超时、限流、临时不可用;对格式校验失败、内容违规这类错误直接失败,重试只会浪费预算。重试次数最多两次,且第二次重试前要加退避等待。这条规则写在代码里,不写在提示词里。
| 参数 | 初始值 | 调整依据 | 调整方向 |
|---|---|---|---|
| 并发数 | 4 | 失败率 > 5% | 下调 |
| 单次超时 | P95 × 1.5 | 频繁超时且非限流 | 上调 |
| 任务总超时 | 预计轮次 × 单次超时 × 1.2 | 末期被截断 | 上调 |
| 最大轮次 | 角色数 × 3 | 正常收敛在更小轮次 | 可按实测收紧 |
| 总 token 预算 | 单轮消耗 × 任务量 × 1.3 | 频繁触发预算闸 | 上调或优化压缩 |
这张表我一般贴在项目文档里,新人接手时至少知道每个数字是从哪来的,不至于随便改。
4.4 观测与联调:没有可观测性就是黑箱
多智能体系统最要命的一点是,出问题时你看到的只是一个不好的最终结果,中间哪一步开始偏的完全看不到。所以观测要从第一版就有,不能等出问题再补。
我至少要记录四类信息。每个角色每次调用的输入摘要、输出摘要、消耗和时间,这是最基础的。每条消息的完整链路,靠追踪号串起来,能看到一个任务从开始到结束经过哪些角色、每个角色停留多久。共享状态的每次变更,谁改的、改前改后是什么,这个在排查"结果为什么不对"时特别有用。终止原因,是正常收敛还是被哪道闸拦下来的。
联调的顺序我一般是这样:先用一份最小的材料跑通全链路,确认消息能正确流转;再故意制造错误,比如让抽取体返回缺字段的结果,看校验和重试是不是按预期工作;最后才上批量,看并发下的表现。跳过中间这步直接上批量,出问题时你根本分不清是逻辑问题还是并发问题。
注意:联调阶段不要用真实业务数据,用构造的小样本。真实数据往往又长又脏,在逻辑还没跑通的时候引入这些变量,等于同时调两个未知数。
5. 常见故障与排查实录
5.1 无限对话与死循环:三个真实成因
这个问题我遇到过至少三次,每次成因都不一样。
第一种是两个角色互相打回。校验体说抽取体有问题,抽取体改完提交,校验体又说有问题,来来回回。根源往往是校验标准没有收敛机制——校验体每次都按最高标准挑毛病,抽取体每次都改一点但改不到点子上。解决办法是给校验体设一个逐轮放宽的规则:第一轮只报阻断级问题,第二轮报重要问题,第三轮之后只报阻断级且必须给出可执行的修改建议。同时给同一对角色之间设重试上限,超过就升级给调度体做人工裁决。
第二种是调度体误判。调度体看到上一轮有输出,就认为任务该继续,但实际那个输出是失败的。这类问题的特征是轮次一直在涨,但共享状态里的有效产出不增加。排查方法很简单:看每一轮共享状态有没有新增有效字段,没有就是空转。
第三种是广播消息被反复消费。用事件总线的时候容易出这个问题,一个事件被订阅多次,导致同一个任务被重复触发。解法是消费端做幂等,用消息的唯一标识去重。
5.2 上下文膨胀与信息衰减:比想象中更隐蔽
上下文的膨胀是渐进的,前期看不出来,跑到七八轮问题就集中爆发。表现是:模型开始忽略指令里的格式要求、开始重复前面的内容、开始对最新的输入不敏感。
我的应对是三条。第一,角色之间不传完整对话历史,只传结构化结果。这条规则如果一开始就定死,能省掉后面 80% 的上下文问题。第二,给每个角色的上下文设硬上限,超过就触发摘要压缩,压缩时保留结论和最近两轮原始交互。第三,定期做回归测试,用同一批输入在不同轮次下跑,看输出质量有没有随轮次下降。这个测试很土但很有效,我在两个项目里都靠它提前发现了衰减。
这里有个具体的数值参考:我实测下来,单角色上下文超过它窗口的 40% 之后,格式遵循的准确率会开始明显下降。所以我的软性上限就设在 40%,触发压缩。这个比例不是理论值,是实测调出来的,你的模型和任务不同,值会不一样,但方法可以照搬。
5.3 一致性冲突与错误放大:多智能体特有的坑
单智能体出错是一个错,多智能体出错是错上加错,因为错误会沿着链路被放大。抽取体漏了一条关键信息,校验体没查到,汇总体基于残缺信息生成结论,最后你拿到一个看起来很完整但漏了核心的结果。
控制这个风险,我用三个手段。第一,锚点校验:要求所有抽取类输出必须带原文位置,汇总时校验锚点是否在原范围内,对不上的直接丢弃。这一条能拦掉大部分凭空生成。第二,交叉验证:关键结论至少由两个独立角色产出,不一致的进人工复核队列,而不是取某一个。第三,失败可见:任务结果里必须带上完成度标记,哪些步骤失败、哪些被跳过,全部明确写出来,不允许静默降级。
第三条看着像产品设计问题,实际上非常关键。我见过一个系统,某个角色连续失败,调度器为了"保证有输出"直接跳过了校验环节,结果输出里带着明显错误,但下游完全不知道。这种静默降级比直接失败危险得多。
5.4 排查速查表
把上面这些整理成一张表,出问题时按症状对照着查,比漫无目的翻日志快得多。
| 症状 | 优先怀疑 | 排查动作 | 处理方式 |
|---|---|---|---|
| 轮次持续增长但无新产出 | 调度体误判或空转 | 对比每轮共享状态新增字段 | 给调度体加"有效产出"判定条件 |
| 两个角色反复互打 | 校验标准不收敛 | 看退回理由是否重复 | 加逐轮放宽规则和重试上限 |
| 后期输出格式错乱 | 上下文膨胀 | 查单角色上下文占用比例 | 设 40% 上限,触发摘要压缩 |
| 结果看似完整但漏关键信息 | 错误沿链路放大 | 查锚点是否可回溯原文 | 强制锚点校验,交叉验证 |
| 任务整体超时被截断 | 预算或超时设置过紧 | 看终止原因和已完成轮次 | 上调总超时或优化单步耗时 |
| 偶发失败集中在高并发时 | 触发下游限流 | 看失败时间分布 | 下调并发,加退避重试 |
| 同一任务被重复执行 | 消息被重复消费 | 查追踪号的重复出现 | 消费端按消息标识幂等 |
5.5 我个人踩过最疼的一个坑
最后说一个我印象最深的教训。早期做一套任务编排的时候,我把所有角色的失败都做了兜底:抽取失败就返回空列表,校验失败就默认通过,汇总失败就返回上一轮结果。当时的想法是"保证系统始终有输出",听起来很稳。
上线之后出了一次事故:某批材料因为格式变化,抽取体全部返回空列表,校验体拿到空列表就通过,汇总体基于空列表产出了一份"无关键要点"的结论,而且整个链路没有任何报错。下游拿着一份看起来正常的结果去做决策,等到发现问题已经是两天后。
后来我把兜底逻辑全改了:任何一个环节失败,必须显式标记为失败并向上传递,兜底只允许发生在最终展示层,且必须带醒目的完成度说明。这个改动让系统在失败时的表现从"安静地给出错误答案"变成了"响亮地报错",短期内看起来是退步,长期看是把可控性拿回来了。
多智能体系统跟单智能体最大的差别就在这里:单体的错误你一眼能看出来,多体的错误会被一层层的"容错"包装得漂漂亮亮。所以我现在做任何一套协作流程,第一件写的东西不是角色提示词,是失败传播规则和终止条件,把这两样定死,后面才敢往上加东西。角色提示词可以慢慢调,这两样一旦设计错了,后面所有的调试都建立在流沙上。