ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

从单体Agent到Multi-Agent:架构演进与实战避坑指南

从单体Agent到Multi-Agent:架构演进与实战避坑指南 1. 从一次线上事故说起单体 Agent 到底卡在哪去年冬天我接手了一个内部工单系统的智能化改造需求听起来很朴素让 Agent 自动读取用户提交的问题描述判断问题类型检索知识库生成回复草稿必要时调用工单接口创建子任务。一开始我用的是最经典的单体 Agent 方案——一个 ReAct 循环一个系统提示词挂上五六个工具跑起来看着挺美。前两周确实很稳。第三周开始出问题用户描述稍微长一点Agent 就开始忘事明明前面已经查过订单号后面又重复调用查询接口再后来更离谱它会在检索知识库和创建子任务之间反复横跳一次请求烧掉几十万 token最后返回一个我无法完成该任务。我去翻日志发现根因不是模型不行而是单体 Agent 的上下文被塞爆了——工具返回的原始 JSON、历史对话、系统提示、few-shot 示例全挤在一个 context window 里模型在噪声中迷失了方向。这次事故让我彻底想明白一件事单体 Agent 有天花板而且这个天花板不是靠换更强的模型就能捅破的。它本质上是架构问题不是能力问题。复杂任务必然走向 Multi-Agent这不是跟风是被现实逼出来的。这篇文章我就把这一年多在 Agent 开发上踩过的坑、做过的取舍、验证过的方案完整地摊开讲一遍。不管你是刚接触 agent 开发的新手还是已经在写 ReAct 循环的老手应该都能从中找到点能直接抄作业的东西。2. 单体 Agent 的能力边界三个绕不过去的硬约束2.1 上下文窗口不是越大越好而是越用越乱很多人对 context 有个误解觉得模型支持 100 万 token 了那我把所有东西都塞进去不就行了我实测下来的结论是能塞进去不等于能用好。这里有个很反直觉的现象业内一般叫lost in the middle——当上下文很长时模型对开头和结尾的信息记得比较牢中间部分的信息召回率会明显下降。我做过一个对比实验同一个知识库检索任务分别把检索结果放在上下文的头部、中部、尾部让模型回答同一个问题。结果头部和尾部的准确率在 85% 以上中部直接掉到 60% 出头。这意味着什么意味着你就算把窗口撑到 100 万 token中间那几十万 token 的信息对模型来说约等于不存在。你花的是真金白银的 token 费用换来的是模型的选择性失明。更麻烦的是单体 Agent 的上下文是单调增长的。每一轮 ReAct 循环都要把上一轮的思考、动作、观察结果追加进去。一个稍微复杂的任务跑十几轮上下文轻松突破几万 token。这时候你会发现两个连锁反应第一推理延迟肉眼可见地变长第二模型开始注意力涣散工具调用准确率断崖式下跌。2.2 工具数量一多选择准确率就崩单体 Agent 的第二个硬约束是工具选择。我做过一个粗略的统计当工具数量在 5 个以内时模型选对工具的概率大概在 90% 以上到 10 个左右掉到 75% 上下超过 20 个基本就靠运气了经常出现该查数据库的时候去调了发邮件接口这种离谱操作。原因其实不复杂。工具描述本身也是要占上下文的每个工具的名称、参数说明、使用示例加起来少说一两百 token20 个工具就是三四千 token 的工具说明书常驻在上下文里。这些说明之间还会互相干扰尤其是功能相近的工具比如查询订单和查询物流模型经常分不清该用哪个。我试过用更详细的工具描述来提升准确率结果适得其反——描述越长占的上下文越多干扰越严重。后来我换了个思路与其让一个 Agent 记住所有工具不如让每个 Agent 只记住自己那两三个工具。这个念头其实就是 Multi-Agent 的雏形。2.3 单一职责的反面一个提示词想干所有事单体 Agent 最隐蔽的问题是系统提示词的精神分裂。你既要它严谨做数据查询时不能瞎编又要它 creative写回复草稿时要自然还要它谨慎涉及敏感操作时要确认。这些要求写在同一段提示词里模型执行时就会左右为难。我遇到过最典型的情况Agent 在检索知识库时表现得过于谨慎明明知识库里有明确答案它却回复根据现有信息无法确定。排查后发现是因为系统提示词里有一句不确定时不要臆测这句话在写草稿场景下是对的但在检索场景下就成了绊脚石。一个提示词不可能同时服务好所有子任务这是单体 Agent 的宿命。3. Multi-Agent 的核心思路分而治之各司其职3.1 拆分的本质是上下文隔离Multi-Agent 最容易被误解成多个 Agent 一起干活其实它的核心价值不在多而在隔离。每个子 Agent 拥有自己独立的上下文窗口只装自己需要的信息只挂自己需要的工具只用自己专属的提示词。这样一来前面说的三个硬约束全部被绕开了。打个比方单体 Agent 像一个什么都要管的项目经理桌上堆满了所有部门的文件电话响个不停最后哪个决策都做不好。Multi-Agent 则像把工作分给几个专员检索专员只管查资料分析专员只管做判断写作专员只管出稿子每个人桌上只有自己那摊事效率自然高。我现在的项目里一个典型的 Multi-Agent 编排是这样的一个Orchestrator编排者负责理解用户意图、拆解任务、分发给子 Agent若干Worker Agent各管一摊最后可能还有一个Reviewer Agent做质量把关。Orchestrator 的上下文里只有任务拆解逻辑和子 Agent 的能力清单Worker 的上下文里只有自己的工具和输入互不污染。3.2 什么时候该拆什么时候不该拆不是所有任务都值得上 Multi-Agent。我踩过的坑是一开始觉得 Multi-Agent 高级什么任务都想拆结果一个简单的查天气也要走三个 Agent延迟翻了三倍纯属自找麻烦。我的判断标准很简单满足下面任意两条才考虑拆任务步骤超过 5 步且步骤之间有明确的先后依赖需要用到 8 个以上的工具且工具可以按功能分组单个子任务的上下文需求超过 2 万 token不同子任务对模型风格的要求差异很大比如既要严谨又要 creative反过来如果任务就是查一下、算一下、回一句单体 Agent 完全够用硬拆只会增加复杂度和故障点。架构是为需求服务的不是为炫技服务的。3.3 三种主流编排模式我实际用下来怎么选Multi-Agent 的编排模式我实际项目里主要用过三种各有各的适用场景编排模式核心机制适用场景我的实测感受顺序流水线Agent 按固定顺序依次执行步骤明确、流程固定的任务最稳调试最容易但灵活性差中心调度Orchestrator 动态决定下一个调谁任务路径不固定、需要判断灵活但 Orchestrator 容易成为瓶颈层级嵌套子 Agent 内部再拆子 Agent超复杂任务、多层级分解能力强但调试成本高慎用我个人的经验是能用顺序流水线解决的绝不上中心调度能用中心调度解决的绝不上层级嵌套。每增加一层动态决策就多一份不确定性线上出问题时排查难度是指数级上升的。我现在的项目里80% 的场景用的是顺序流水线加一点点条件分支只有真正需要动态判断的任务才上中心调度。4. 手写一个 Multi-Agent 编排从零到能跑4.1 环境准备与依赖选择先说环境。Python 版本我建议 3.10 以上因为要用到一些新的类型标注语法写起来更清爽。依赖方面核心就几个pip install openai pydantic tenacity这里解释一下为什么选这几个。openai是各家模型 API 的事实标准大部分国产模型也兼容它的接口格式换模型时改动最小。pydantic用来做工具参数的结构化校验Agent 调用工具时参数格式错了能第一时间发现而不是等工具执行到一半才报错。tenacity用来做重试模型 API 偶尔抽风是常态没有重试机制线上会很难看。提示不要一上来就引入 LangChain、AutoGen 这类重框架。我早期用框架出问题时根本不知道是框架的锅还是我的锅排查成本极高。手写一遍编排逻辑你对整个链路的理解会完全不一样之后再决定要不要用框架。4.2 定义 Agent 的基类把公共逻辑抽出来每个 Agent 都有一些共性调用模型、解析工具调用、执行工具、维护自己的上下文。这些逻辑抽成一个基类子 Agent 只需要定义自己的系统提示词和工具集。from abc import ABC, abstractmethod from typing import Any import json from openai import OpenAI from tenacity import retry, stop_after_attempt, wait_exponential class BaseAgent(ABC): def __init__(self, name: str, model: str gpt-4o-mini): self.name name self.model model self.client OpenAI() self.context: list[dict] [] self.tools: list[dict] [] abstractmethod def system_prompt(self) - str: 每个子 Agent 必须定义自己的系统提示词 ... abstractmethod def tool_map(self) - dict[str, callable]: 工具名到实际函数的映射 ... retry(stopstop_after_attempt(3), waitwait_exponential(min1, max10)) def _call_llm(self, messages: list[dict]) - Any: return self.client.chat.completions.create( modelself.model, messagesmessages, toolsself.tools if self.tools else None, temperature0.2, ) def run(self, user_input: str, max_turns: int 8) - str: self.context [ {role: system, content: self.system_prompt()}, {role: user, content: user_input}, ] for turn in range(max_turns): response self._call_llm(self.context) msg response.choices[0].message self.context.append(msg.model_dump(exclude_noneTrue)) if not msg.tool_calls: return msg.content or for call in msg.tool_calls: fn self.tool_map().get(call.function.name) if fn is None: result f错误未知工具 {call.function.name} else: try: args json.loads(call.function.arguments) result fn(**args) except Exception as e: result f工具执行失败{e} self.context.append({ role: tool, tool_call_id: call.id, content: str(result), }) return 达到最大轮次任务未完成这段代码有几个设计点值得说。max_turns是必须的防止 Agent 陷入死循环烧钱我一般设 8 到 10 轮。temperature设 0.2Agent 场景下不需要创意稳定压倒一切。工具执行用 try-except 包住任何工具报错都转成字符串喂回模型让模型自己决定下一步而不是直接崩掉整个流程。4.3 三个子 Agent 的具体实现假设我们要做一个用户反馈处理的 Multi-Agent 系统拆成三个角色分类 Agent判断反馈类型检索 Agent查知识库回复 Agent生成最终回复。分类 Agent 最简单不需要工具纯靠提示词class ClassifierAgent(BaseAgent): def system_prompt(self) - str: return 你是一个用户反馈分类器。将用户反馈归类到以下类别之一 - 产品咨询 - 功能异常 - 账单问题 - 投诉建议 只输出类别名称不要输出任何其他内容。 def tool_map(self) - dict: return {}检索 Agent 挂一个知识库查询工具class RetrieverAgent(BaseAgent): def system_prompt(self) - str: return 你是一个知识库检索专员。根据用户问题调用 search_kb 工具 检索最相关的资料。如果一次检索结果不理想可以换关键词再检索一次 但最多检索两次。检索完成后用简洁的语言总结检索到的关键信息。 def tool_map(self) - dict: return {search_kb: self._search_kb} def _search_kb(self, query: str, top_k: int 3) - str: # 实际项目里这里接向量库或全文检索 return f[模拟检索结果] 关于 {query} 的前 {top_k} 条资料...回复 Agent 负责把检索结果组织成自然语言class ResponderAgent(BaseAgent): def system_prompt(self) - str: return 你是一个客服回复撰写专员。根据提供的检索资料和用户原始问题 撰写一段专业、友好、简洁的回复。不要编造资料中没有的信息。 如果资料不足以回答明确告知用户会转人工处理。 def tool_map(self) - dict: return {}4.4 编排层把三个 Agent 串起来编排层是整个系统的大脑它不调用模型只负责调度class FeedbackPipeline: def __init__(self): self.classifier ClassifierAgent(classifier) self.retriever RetrieverAgent(retriever) self.responder ResponderAgent(responder) def process(self, user_feedback: str) - dict: category self.classifier.run(user_feedback).strip() if category 投诉建议: return { category: category, reply: 已收到您的反馈我们会尽快安排专人跟进。, need_human: True, } retrieved self.retriever.run( f用户反馈{user_feedback}\n请检索相关知识。 ) reply self.responder.run( f用户问题{user_feedback}\n检索资料{retrieved} ) return { category: category, reply: reply, need_human: False, }跑起来大概是这样pipeline FeedbackPipeline() result pipeline.process(我上个月买的会员怎么还没生效) print(result[category]) # 账单问题 print(result[reply])这套代码不复杂但它体现的架构思想是关键每个 Agent 的上下文是干净的、独立的编排层只传递必要的信息。分类 Agent 看不到知识库内容检索 Agent 看不到最终回复回复 Agent 看不到分类逻辑。职责清晰出问题时定位也快。5. 踩坑实录Multi-Agent 的五个典型故障5.1 Agent 之间信息传递丢失最常见的坑。编排层把检索结果传给回复 Agent 时如果只是简单拼接字符串很容易丢掉关键信息。我遇到过检索 Agent 返回了一大段带格式的文本回复 Agent 只提取了其中一句导致回复不完整。解决办法是约定结构化的传递格式。我现在所有 Agent 之间的数据传递都用 JSON字段名固定比如{summary: ..., raw: ..., confidence: 0.8}。这样下游 Agent 知道该读哪个字段不会漏。5.2 子 Agent 越权调用工具有一次检索 Agent 不知道从哪学会了调用回复工具直接生成了回复导致回复 Agent 拿到的是二手信息。排查后发现是工具描述写得太宽泛检索 Agent 误以为生成回复也是自己的职责。解决办法是工具集严格隔离每个 Agent 的tool_map只包含自己该用的工具物理上就调不到别的。同时系统提示词里明确写你只负责 X不要做 Y。5.3 编排层成为性能瓶颈中心调度模式下Orchestrator 每轮都要调用一次模型来决定下一步如果子任务有 10 步光编排就要 10 次模型调用延迟直接爆炸。我的优化是把能静态化的决策静态化。比如先分类再检索再回复这个顺序是固定的那就写死在代码里不要每次都让模型判断。只有真正需要动态决策的地方比如检索结果不理想时是否重试才交给模型。5.4 错误传播导致雪崩上游 Agent 返回了错误结果下游 Agent 基于错误结果继续执行最后产出一个看似合理实则完全错误的答案。这种问题最隐蔽因为表面上流程跑通了。我的做法是在关键节点加校验。比如检索 Agent 返回空结果时编排层直接短路返回未找到相关资料而不是硬着头皮传给回复 Agent。校验逻辑用简单的规则就行不需要再调模型。5.5 调试困难不知道哪一步出的错Multi-Agent 最大的痛点就是调试。单体 Agent 出问题看一遍上下文就知道Multi-Agent 出问题你得挨个 Agent 看日志。我的经验是给每个 Agent 的每次调用打上 trace_id把输入、输出、耗时、token 消耗全部记下来。出问题时按 trace_id 一拉整条链路一目了然。这个投入在项目初期看着麻烦但线上出问题时能救命。故障类型典型表现排查方向解决手段信息传递丢失下游回复不完整检查 Agent 间数据格式统一 JSON 结构越权调用子 Agent 干了别人的活检查工具集和提示词工具物理隔离编排瓶颈延迟随步骤线性增长统计模型调用次数静态决策写死错误传播结果看似合理实则错误检查中间节点输出关键节点加校验调试困难不知道哪步出错检查日志完整性trace_id 全链路追踪6. 几个提升稳定性的实操心得6.1 提示词要窄不要全给子 Agent 写提示词我最大的心得是越窄越好。分类 Agent 的提示词就只讲分类不要顺带提一句如果分类不确定可以检索知识库这一句就会让它去调检索工具。每个 Agent 的提示词只描述它自己那一亩三分地边界越清晰行为越稳定。6.2 给每个 Agent 设 token 预算我现在的项目里每个子 Agent 都有独立的 token 预算比如检索 Agent 单次不超过 8000 token回复 Agent 不超过 4000 token。超了就强制截断或报错。这个机制能有效防止某个 Agent 失控烧钱也能倒逼你把提示词写精简。6.3 用规则兜底不要什么都靠模型Multi-Agent 里有很多决策其实不需要模型。比如分类结果是投诉就直接转人工这种规则写死在代码里比让模型判断又快又稳。模型只用在真正需要理解语义的地方能用 if-else 解决的绝不调模型这是我踩了无数坑后的血泪教训。6.4 灰度上线先跑影子模式Multi-Agent 系统上线前我强烈建议先跑一段时间的影子模式真实请求进来系统照常处理但结果不返回给用户只记录日志。对比 Agent 的输出和人工处理的结果找出差异调优提示词和流程。直接上线风险太大一个 Agent 的行为异常就可能影响所有用户。7. 关于 Multi-Agent 的几个常见疑问QMulti-Agent 一定比单体 Agent 好吗不一定。简单任务用 Multi-Agent 是杀鸡用牛刀徒增复杂度和延迟。判断标准我前面说过步骤多、工具多、上下文需求大、风格要求冲突满足这些才值得拆。Q子 Agent 用同一个模型还是不同模型我一般混用。分类、校验这类简单任务用小模型快且便宜检索总结、回复生成用大模型质量有保障。同一个系统里混用不同模型成本能降不少效果基本不受影响。QMulti-Agent 的延迟怎么控制核心是减少模型调用次数。能并行的子任务并行跑能静态化的决策写死能规则兜底的不用模型。我现在的系统里一个典型请求的模型调用次数控制在 3 到 4 次端到端延迟在 3 秒以内用户基本无感。Q怎么评估 Multi-Agent 系统的效果我主要看三个指标任务完成率、平均 token 消耗、人工介入率。任务完成率反映能力token 消耗反映成本人工介入率反映系统的可靠性。这三个指标一起看比单看准确率全面得多。8. 写在最后的一点个人体会从单体 Agent 走到 Multi-Agent我最大的感受是这不是一个技术升级而是一个思维转变。单体 Agent 的思维是我要让一个足够聪明的家伙搞定所有事Multi-Agent 的思维是我要把复杂问题拆成简单问题让每个简单的执行者都做好自己的事。后者更接近工程世界的常识——复杂系统从来不是靠单个组件变强而是靠合理的分工和清晰的边界。我现在回头看那次线上事故其实挺感谢它的。如果没被单体 Agent 的上下文问题折磨过我可能永远不会认真思考架构层面的解法。如果你现在也卡在单体 Agent 的某个瓶颈上不妨试试拆一拆哪怕先拆出两个 Agent 跑跑看你会对上下文隔离这件事有全新的理解。
返回列表