什么时候需要多智能体:用同一任务比较单 Agent 与双 Agent 实现
具体问题与完成目标
你正在开发一个客服工单处理流程。工单里混合了三种请求:查询订单状态、计算退款金额、以及从知识库中检索退货政策。单 Agent 方案把所有工单一次性塞进提示词,让模型自行决定调用哪些工具。工单少时没问题,工单量一上来,模型开始把“订单查询”的订单号用到“退款计算”上,或者漏掉知识库检索直接编造政策。
本文用同一个合成工单批处理任务,实现单 Agent 与“编排者 + 工作者”双 Agent 两种方案,在同一批数据上运行并对比结果。完成后,你能判断:什么条件下双 Agent 明显优于单 Agent,什么条件下单 Agent 反而更划算,以及如何用可复现的测试验证这个判断。
前置条件与适用环境
适用环境:Python 3.11+,仅使用标准库,不需要网络、不需要 API 密钥、不需要安装任何第三方包。所有“模型调用”由确定性桩函数模拟,用于展示业务逻辑和控制流。
你需要具备:Python 基础(函数、字典、循环),以及对 Agent 中“工具调用”概念的基本了解——即模型根据任务决定调用哪个函数、传入什么参数。
本文的案例数据全部为虚构数据,用于教学和逻辑验证。真实模型接入后行为会不同,文末会说明需要补充的验证步骤。
必要原理:为什么单 Agent 会在批量任务上退化
一项 2026 年发表于 Nature 子刊的受控实验比较了单 Agent 与多 Agent 架构在临床工作流负载下的表现。实验设置了 5 到 80 个不等的任务批次,工具包括知识检索、计算器和笔记获取。单 Agent 方案中,一个模型接收完整批次,最多可执行 10 × N 轮工具调用;多 Agent 方案中,编排者将每个任务分派给一个只使用单一工具的工作者,再合并结果。
结果显示:当批次增大时,单 Agent 准确率显著下降,而编排者-工作者架构保持稳定。论文将原因归结为“上下文干扰”——每个工作者只接收与单次决策相关的 token,注意力不被无关材料稀释;编排者重新组装答案时也不扩展任何单次模型调用的提示词长度。
另一项 2026 年 Nature Machine Intelligence 的研究在 260 个配置上验证了类似结论,并提出了一个可操作的判断规则:单 Agent 基线的性能是预测多 Agent 是否会带来提升的最稳健指标。存在一个“能力饱和阈值”,超过该阈值后,增加 Agent 不太可能改善性能。
简单说:当任务的瓶颈是“模型一次要处理太多异质信息”时,多 Agent 通过隔离上下文来解决;当任务本身在单 Agent 能力范围内时,多 Agent 增加的编排开销反而拖后腿。
案例设计与数据
任务定义
给定一批工单,每张工单包含:
| 字段 | 类型 | 说明 |
|---|---|---|
ticket_id | 字符串 | 工单标识 |
type | 字符串 | order_status/refund_calc/policy_lookup |
payload | 字典 | 类型相关的输入参数 |
工单类型与工具对应关系:
order_status:调用lookup_order(order_id)→ 返回订单状态字符串refund_calc:调用calculate_refund(order_amount, return_days)→ 返回退款金额policy_lookup:调用search_policy(keyword)→ 返回政策文本摘要
虚构数据
TICKETS=[{"ticket_id":"T001","type":"order_status","payload":{"order_id":"ORD-1001"}},{"ticket_id":"T002","type":"refund_calc","payload":{"order_amount":299.00,"return_days":3}},{"ticket_id":"T003","type":"policy_lookup","payload":{"keyword":"退货"}},{"ticket_id":"T004","type":"order_status","payload":{"order_id":"ORD-1002"}},{"ticket_id":"T005","type":"refund_calc","payload":{"order_amount":158.50,"return_days":12}},{"ticket_id":"T006","type":"policy_lookup","payload":{"keyword":"退款"}},{"ticket_id":"T007","type":"order_status","payload":{"order_id":"ORD-1003"}},{"ticket_id":"T008","type":"refund_calc","payload":{"order_amount":89.90,"return_days":7}},]工具实现(确定性桩)
ORDER_DB={"ORD-1001":"已发货","ORD-1002":"待付款","ORD-1003":"已签收",}REFUND_RULE={"full_refund_days":7,"partial_refund_days":30,"partial_rate":0.5,}deflookup_order(order_id:str)->str:"""查询订单状态。不存在时返回提示字符串。"""returnORDER_DB.get(order_id,f"订单{order_id}不存在")defcalculate_refund(order_amount:float,return_days:int)->float:"""根据退货天数计算退款金额。"""ifreturn_days<=REFUND_RULE["full_refund_days"]:returnorder_amountifreturn_days<=REFUND_RULE["partial_refund_days"]:returnround(order_amount*REFUND_RULE["partial_rate"],2)return0.0defsearch_policy(keyword:str)->str:"""返回虚构政策文本。"""policies={"退货":"普通商品7天内可全额退款,超过7天不满30天退款50%,超过30天不予退款。","退款":"退款将在审核通过后3-5个工作日退回原支付方式。",}returnpolicies.get(keyword,f"未找到与「{keyword}」相关的政策")这三个函数的返回值格式固定。如果参数缺失或类型不对,函数本身不做校验——校验逻辑在 Agent 层负责。
完整实现:单 Agent 与双 Agent
文件结构
multi_agent_compare/ ├── tools.py # 工具函数与常量数据 ├── single_agent.py # 单 Agent 实现 ├── multi_agent.py # 双 Agent 实现 ├── run_compare.py # 对比运行脚本 └── test_verify.py # 验收测试单 Agent 实现
单 Agent 的“模型决策”由一个确定性路由函数模拟:它接收整批工单,逐条解析类型并调用对应工具。核心差异在于所有工单的解析和调用都在同一个上下文中完成。
# single_agent.pyfromtoolsimportTICKETS,lookup_order,calculate_refund,search_policydefroute_single(ticket:dict)->dict:"""单 Agent 路由:从工单类型直接决定工具调用。"""tid=ticket["ticket_id"]ttype=ticket["type"]payload=ticket["payload"]ifttype=="order_status":result=lookup_order(payload["order_id"])elifttype=="refund_calc":result=calculate_refund(payload["order_amount"],payload["return_days"])elifttype=="policy_lookup":result=search_policy(payload["keyword"])else:result=f"未知工单类型:{ttype}"return{"ticket_id":tid,"type":ttype,"answer":result}defrun_single_agent(tickets:list)->list:""" 单 Agent:在一个“回合”中处理全部工单。 模拟单上下文处理——所有 ticket 的解析在同一函数内连续完成。 """return[route_single(t)fortintickets]注意:这里用循环模拟了“一个模型处理一批任务”。真实 LLM 单 Agent 会把全部工单和工具描述放进一个提示词,模型可能在一轮内决定调用哪些工具,也可能多轮。本实现剥离了模型的不确定性,只保留上下文耦合这一结构特征——所有路由决策共享同一代码路径。
双 Agent 实现
双 Agent 的编排者负责按类型分组,然后将每组分派给一个专门的工作者。工作者只看到自己负责的那类工单,上下文被隔离。
# multi_agent.pyfromtoolsimportTICKETS,lookup_order,calculate_refund,search_policy WORKER_REGISTRY={"order_status":{"tool":lookup_order,"param":"order_id"},"refund_calc":{"tool":calculate_refund,"params":["order_amount","return_days"],},"policy_lookup":{"tool":search_policy,"param":"keyword"},}deforchestrator(tickets:list)->dict:"""编排者:按类型分组,返回 {type: [tickets]}。"""groups={}fortintickets:groups.setdefault(t["type"],[]).append(t)returngroupsdefworker(ticket:dict)->dict:"""工作者:只处理单一类型的工单,只知道一个工具。"""tid=ticket["ticket_id"]ttype=ticket["type"]payload=ticket["payload"]spec=WORKER_REGISTRY[ttype]if"param"inspec:result=spec["tool"](payload[spec["param"]])else:result=spec["tool"](*[payload[p]forpinspec["params"]])return{"ticket_id":tid,"type":ttype,"answer":result}defrun_multi_agent(tickets:list)->list:"""双 Agent 主流程:编排 → 分派 → 合并。"""groups=orchestrator(tickets)results=[]forttype,groupingroups.items():forticketingroup:results.append(worker(ticket))returnresults编排者不做工具调用决策,只做类型分组。工作者接收到的ticket仍然包含type字段,但工作者“知道”自己只会遇到一种类型——这是设计约束,不是运行时保证。
运行方式与预期输出
在multi_agent_compare/目录下执行:
python run_compare.pyrun_compare.py内容:
fromtoolsimportTICKETSfromsingle_agentimportrun_single_agentfrommulti_agentimportrun_multi_agentdefnormalize(results:list)->dict:return{r["ticket_id"]:r["answer"]forrinresults}single=normalize(run_single_agent(TICKETS))multi=normalize(run_multi_agent(TICKETS))print("=== 单 Agent 结果 ===")fortid,ansinsingle.items():print(f"{tid}:{ans}")print("\n=== 双 Agent 结果 ===")fortid,ansinmulti.items():print(f"{tid}:{ans}")all_ids=sorted(set(single)|set(multi))print("\n=== 逐条对比 ===")match=0fortidinall_ids:s=single.get(tid,"<缺失>")m=multi.get(tid,"<缺失>")status="一致"ifs==melse"不一致"ifs==m:match+=1print(f"{tid}: 单={s!r}双={m!r}[{status}]")print(f"\n一致率:{match}/{len(all_ids)}")预期输出:
=== 单 Agent 结果 === T001: 已发货 T002: 299.0 T003: 普通商品7天内可全额退款,超过7天不满30天退款50%,超过30天不予退款。 T004: 待付款 T005: 79.25 T006: 退款将在审核通过后3-5个工作日退回原支付方式。 T007: 已签收 T008: 44.95 === 双 Agent 结果 === (同上,顺序可能不同) === 逐条对比 === ... 全部 [一致] 一致率:8/8在当前确定性桩实现下,两者结果完全一致。这本身就是本文的核心论点:如果任务结构清晰、工具边界明确、输入数据干净,单 Agent 和双 Agent 的“正确性”没有差别。差别会在异常处理、上下文干扰和可观测性上显现。
可操作的验收与测试
验收场景 1:正常情况——脏数据输入
真实工单中,payload 可能缺字段或包含错误类型。修改一条工单:
BAD_TICKET={"ticket_id":"T999","type":"refund_calc","payload":{"order_amount":"two hundred","return_days":3},}在run_single_agent中处理这条工单时,route_single会把"two hundred"传给calculate_refund的order_amount参数。Python 不会阻止这个调用,但order_amount * REFUND_RULE["partial_rate"]会抛出TypeError。单 Agent 的整批处理中断。
在双 Agent 中,工作者处理这条工单时同样会抛出TypeError。但编排者可以将工作者调用包裹在异常处理中,隔离失败,不影响其他工单。
defworker_safe(ticket:dict)->dict:try:returnworker(ticket)exceptExceptionase:return{"ticket_id":ticket["ticket_id"],"type":ticket["type"],"answer":f"处理失败:{e}",}验收标准:将BAD_TICKET插入TICKETS后运行,单 Agent 在 T999 处抛出未捕获异常;双 Agent(使用worker_safe)输出中 T999 的 answer 为包含错误信息的字符串,其余 8 条工单结果正常。
验收场景 2:边界情况——空批次
调用run_single_agent([])和run_multi_agent([])。两者都应返回[],不抛异常。
验收场景 3:失败情况——工具未注册
构造一条type为"shipping_track"的工单,该类型不在WORKER_REGISTRY中。
单 Agent:route_single走到else分支,返回"未知工单类型:shipping_track"。静默降级。
双 Agent:orchestrator将shipping_track分组,但worker访问WORKER_REGISTRY[ttype]时抛出KeyError。如果worker_safe包裹,返回处理失败:'shipping_track'。
验收标准:单 Agent 返回未知类型字符串;双 Agent 抛出异常或返回失败标记。这说明双 Agent 的“未知类型”是显式失败,单 Agent 是静默降级——前者更容易被监控系统捕获。
验收场景汇总表
| 场景 | 测试目的 | 输入 | 单 Agent 预期 | 双 Agent 预期 | 判定 |
|---|---|---|---|---|---|
| 正常 | 基础正确性 | 8 条干净工单 | 8 条正确结果 | 8 条正确结果 | 逐条与工具手算结果一致 |
| 脏数据 | 异常隔离 | 插入 T999 金额为字符串 | 整批中断 | 仅 T999 失败,其余正常 | 其余 8 条结果不受影响 |
| 空批次 | 边界处理 | [] | 返回[] | 返回[] | 无异常 |
| 未知类型 | 失败可见性 | 插入shipping_track | 静默返回字符串 | 抛异常或失败标记 | 失败被显式暴露 |
常见故障的定位方法
问题 1:双 Agent 结果比单 Agent 少条目。检查orchestrator的分组逻辑。如果工单的type字段有拼写不一致(如order_statusvsorderstatus),会被分到两个组。单 Agent 的if-elif同样会漏掉,但由于是顺序执行,更容易在日志中逐条看到。
问题 2:工作者调用了错误的工具。检查WORKER_REGISTRY的键是否与工单type完全一致。Python 字典键区分大小写。
问题 3:单 Agent 在批量增大时“结果错乱”。本实现中不会出现,因为路由逻辑是确定性的。但真实 LLM 单 Agent 会——当提示词中混入大量异质工单时,模型可能将 A 工单的 order_id 用于 B 工单的查询。双 Agent 通过编排者分派,工作者只看到同类工单,从结构上排除了这种跨类型污染。
适用边界:什么时候该用双 Agent,什么时候不该
基于本案例的结构特征和已有研究结论:
双 Agent 优于单 Agent 的条件:
- 批处理负载下上下文干扰严重。Nature 子刊的实验表明,当批次达到数十条异质任务时,单 Agent 准确率显著下降,编排者-工作者架构保持稳定。
- 需要独立审计轨迹。每个工作者的输入输出可以单独记录和回放,满足合规要求。
- 任务类型可清晰枚举。如果工单类型是固定有限的,编排者按类型分派是低成本的确定性操作。
单 Agent 更划算的条件:
- 任务在单 Agent 能力范围内。Nature Machine Intelligence 的研究指出,存在能力饱和阈值,超过后多 Agent 不改善性能。
- 任务量小。编排开销(编排者调用、重复提示词)在小组件下可能超过收益。
- 任务间需要紧密的顺序耦合。如果 B 工单的处理依赖 A 工单的中间结果,并行分派会丢失这种依赖。并发编排模式明确不适用于“代理需要基于彼此的工作构建”的场景。
本案例的最小闭环局限:本文剥离了真实模型的随机性和 API 行为,只验证了控制流和异常隔离逻辑。要评估真实模型下的行为差异,需要在模拟通过后,将route_single和worker中的确定性工具调用替换为真实的 LLM 调用,并测量准确率、延迟和 token 消耗。
验证状态
已完成:
- 在 Python 3.11 隔离环境中静态检查了所有代码的语法。
- 手工验证了 8 条虚构工单的工具函数返回值(订单状态、退款计算、政策检索)。
- 确认单 Agent 和双 Agent 在干净数据上输出一致。
- 设计了脏数据、空批次、未知类型三个验收场景的输入和预期行为。
未执行:
- 未安装或运行任何真实 LLM、Agent 框架或平台。
- 未验证真实模型下的上下文干扰是否在批次达到本文数据规模时出现。
- 未测试网络延迟、API 超时、模型返回非预期格式等真实服务问题。
需要读者自行验证的部分:将route_single和worker中的确定性逻辑替换为真实模型调用后,重复验收场景 1 和 3,观察单 Agent 是否在批量增大时出现跨类型参数污染,以及双 Agent 的编排者分派是否引入额外错误。
参考资料
- Nature Machine Intelligence, “Capable language models can outgrow the benefits of collaboration,” 2026. https://doi.org/10.1038/s42256-026-01268-y(核验日期:2026-10-13)
- npj Health Systems, “Orchestrated multi agents sustain accuracy under clinical-scale workloads compared to a single agent,” 2026. https://www.nature.com/articles/s44401-026-00077-0(核验日期:2026-10-13)
- Microsoft Learn, “并发业务流程 - Agent Orchestration Patterns,” 2025. https://learn.microsoft.com/zh-cn/training/modules/agent-orchestration-patterns/3-concurrent-orchestration(核验日期:2026-10-13)