1. 从“一个万能Prompt”到多个专用代理:我为什么改架构
1.1 单一智能体的三大瓶颈
最早做智能体的时候,我习惯把所有逻辑塞进一个超长Prompt,让大模型既当产品经理,又当工程师,还当测试员。结果很典型:需求只要稍微复杂一点,输出就开始“精神分裂”。同一个任务里,前半段还在分析业务,后半段突然开始写代码;写代码的时候又把需求分析结论重新解释了一遍,解释完自己都忘了前面定的接口约束。
这种单代理模式的瓶颈其实非常明确。第一条是上下文长度有限,超长Prompt会把有效信息稀释,模型在中间层就容易丢失关键约束;第二条是指令冲突,你在一个Prompt里同时要求“先分析再编码最后测试”,模型很难稳定地遵守顺序,经常跳步;第三条最难处理——你没法定位错误。一旦最终结果不对,你不知道是分析阶段就错了,还是编码阶段跑偏了,还是在测试阶段没发现回归。没有过程数据,等于没有排查路径。
我意识到要换思路:把一个大而全的代理,拆成多个各司其职的子代理,每个子代理只负责一个明确子问题,再通过一套标准接口把它们串起来。这就是多代理实战的核心动机。
1.2 什么样的任务适合拆分给多代理
不是说所有场景都适合多代理。我拆的时候给自己定了一个判断标准:任务里是否包含明显不同的“工种”。比如一个流程里既有信息检索,又有方案设计,又有代码生成,又有结果验证,这些工种对提示词、工具、模型参数的需求都不一样,硬塞在同一个代理里,必然互相干扰。
而多代理的价值就在于隔离。每个子代理可以单独调优,单独测试,单独换模型,出问题也能精准定位到某个环节。我做这套系统时选了研发流水线作为载体,因为研发本身就有非常清晰的角色划分:需求澄清、代码实现、测试验证、文档整理。这个天然结构特别适合代理化。
2. 五个子代理的角色分工:把一条研发流水线装进系统
2.1 调度子代理:任务拆解与结果聚合
第一个子代理是调度者,它不直接写代码,也不分析业务,只做一件事:把用户提交的原始需求拆解成可执行子任务,并决定子任务的执行顺序。
我给它定义的职责有三层:第一步,识别需求属于哪个范畴,是纯文档任务、代码任务,还是混合任务;第二步,把混合任务拆成原子子任务,并按依赖关系排序;第三步,调用对应的下游子代理,再收集它们的结果,生成最终的汇总报告。
这里的关键是:调度子代理的输出必须是结构化JSON,不能是自由文本。我定义的输出格式包含tasks数组,每个元素有task_id、description、assignee、depends_on四个字段。为什么强制结构化?因为下游子代理不能靠“读自然语言”来理解任务,它们需要一个稳定、可解析的输入接口,否则整个系统在第一个环节就会陷入文本解析的泥潭。
实际开发中我用了一张“任务卡”来约束调度子代理的输出格式,大致如下:
{ "overview": "基于用户需求生成的可执行计划", "tasks": [ { "task_id": "T001", "description": "整理用户需求中的功能清单和非功能指标", "assignee": "requirement_agent", "depends_on": [] }, { "task_id": "T002", "description": "根据需求清单生成单元测试用例并执行", "assignee": "tester_agent", "depends_on": ["T001"] }, { "task_id": "T003", "description": "实现核心功能代码并通过测试用例", "assignee": "developer_agent", "depends_on": ["T001", "T002"] } ] }2.2 需求分析子代理:把模糊诉求变成明确输入
第二个子代理是需求分析师。很多人一开始会忽略它,觉得“用户已经把需求说清楚了,直接让模型写代码不就行了?”但实际跑下来,十个原始需求里有八个是模糊的,比如“做一个用户登录功能”“优化一下性能”这种描述,根本没有可执行性。
需求分析子代理的职责,是向用户追问,或者根据上下文补全缺失信息,最终输出一份结构化的需求清单。我给它定义的核心输出包括:功能列表、边界条件、验收标准、依赖资源。其中验收标准是最重要的一项,因为它直接喂给后面的测试验证子代理,是下游所有验证动作的基准。
有一个细节值得单独说:需求分析子代理不能无限制追问,否则用户会烦。我在它的ReAct循环里加了“追问次数上限”,默认最多两轮。如果两轮后信息还不足,就基于合理默认值继续向下执行,并在需求清单中把“假设条件”单独标注出来。这样既保证了流程不卡死,也让后续环节知道哪些判断是基于假设的。
2.3 开发实现子代理:代码产出的核心执行者
第三个子代理是开发者,它是整套系统里工作量最大的角色。它的输入是需求分析子代理输出的结构化需求清单,它的职责是按清单实现代码,并且主动调用工具进行代码质量检查。
这个子代理我做了最重的ReAct配置。它注册的工具包含:文件读写工具、代码搜索工具、以及一组代码检查工具。开发者代理在生成代码后,必须自己先跑一遍静态检查,把明显的问题修掉,再把结果交给测试验证子代理。这里有一个很多人容易忽略的点:开发者代理不能直接访问测试结果,否则它会陷入“改代码-跑测试-再改”的无限制循环。它只负责产出“自我检查通过”的版本,是否真正满足验收标准,由独立的测试验证子代理来判断。
我在开发者代理的System Prompt里明确写了一条规则:所有代码修改都必须附带修改原因,并且在最终交付结果中列出“未能实现的需求项”。这条规则帮我过滤了大量“看起来完成,实际上偷工减料”的情况。
2.4 测试验证子代理:用目标反馈兜住质量
第四个是测试验证子代理,它相当于质量网关。它不信任任何下游代理的“自我评估”,只相信实际运行结果。它的核心动作是:把开发实现子代理交付的代码,在模拟环境中编译、运行,执行它自己生成的测试用例,并把“通过/失败”结果回传。
测试验证子代理的技术含量在于测试用例生成。我要求它必须覆盖三类用例:正常流程、边界条件、异常分支。正常流程保证主链路通,边界条件检查空值、超长输入、并发场景,异常分支验证程序在出错时不会崩溃,而是能给出正确提示。
跑测试时它还会记录失败信息,包括错误日志、堆栈、触发步骤。这些信息会被原样返回给调度者,由调度者决定是让开发者子代理二次修改,还是直接判定任务失败。把质量判断权完全独立出来之后,系统的最终交付质量有了非常明显的提升。原因是:开发和测试使用不同的提示词语境,模型对同一条产出的“自我感觉良好”不会传染到质检环节。
2.5 文档沉淀子代理:交付物闭环的最后一块拼图
第五个子代理负责文档,它在上游所有环节的结果都产出之后开始工作。输入是需求清单、实现代码、测试结果三份材料,输出是使用说明、变更记录和技术要点。
为什么要单独做一个文档子代理,而不是让开发者顺手写完?因为“写代码”和“写文档”的语言风格差距很大,一个代码提示词优化的代理去写文档,经常把说明书写得跟代码注释一样,毫无可读性。文档子代理我单独配置了长文本生成的Prompt,要求它面向两类读者:普通使用者和后续维护者。
它在部分程度上有“编”的风险,这是所有生成式系统的通病。所以我给它加了约束:文档里涉及功能名、参数名、接口路径时,必须从开发者代理的交付物里提取并逐字引用,不得自行发挥。每次生成完成后,它还要输出一份“未引用原文清单”,方便我人工抽查。
如果用一个表格概括这五个角色,大概是这个样子:
| 子代理 | 核心输入 | 核心输出 | 关键约束 |
|---|---|---|---|
| 调度者 | 用户原始需求 | 结构化任务清单 | 输出必须是JSON,任务必须排序 |
| 需求分析师 | 结构化任务卡 | 需求清单与验收标准 | 追问最多两轮,假设条件要标注 |
| 开发者 | 需求清单 | 通过自检的代码 | 必须附带修改原因,列明未实现项 |
| 测试验证者 | 代码与需求清单 | 测试结果与失败日志 | 覆盖正常/边界/异常三类用例 |
| 文档沉淀者 | 需求+代码+测试结果 | 使用说明与技术文档 | 关键信息必须逐字引用交付物 |
3. ReAct架构实现:让子代理一步一步“想清楚再做”
3.1 ReAct循环拆解:Thought、Action、Observation三个状态
每个子代理的核心循环我都用ReAct架构来实现。ReAct这个名字听起来玄,其实就是一个“思考-行动-观察”的循环:先让模型输出当前思考结论,再根据思考结论决定要不要调用工具、调用哪个工具,拿到工具返回结果后继续思考,直到它认为不需要调用工具,可以给出最终答案。
拆成状态来看,ReAct循环一共三步:
- Thought:模型输出“当前我应该做什么”。比如需求分析代理拿到任务卡后,可能先输出“用户需求里缺少验收指标,我需要先查看历史对话中是否包含性能要求”。
- Action:模型从工具列表中选择一个,并填写参数。它可能是搜索知识库、读文件、执行命令,或者请求用户补充信息。
- Observation:系统把工具的返回结果拼到上下文里,让模型获得新信息,再进入下一轮Thought。
这个循环的关键价值和普通“直接生成答案”的区别在于:每次工具返回都会变成可追溯的中间记录。我可以回放某个子代理的全部思考路径,找到它为什么会做出错误决策。单代理模式下,你拿到的是一个已完成的黑盒;ReAct模式下,你拿到的是完整决策日志。
3.2 基于LangChain的ReAct子代理实现
我整套系统是基于LangChain搭的,但只用到了它的create_react_agent和AgentExecutor两个核心组件。每个子代理共享同一个ReAct执行框架,只是Prompt和工具注册表不同。
以测试验证子代理为例,核心代码长这样:
from langchain.agents import create_react_agent, AgentExecutor from langchain_core.tools import tool from langchain_openai import ChatOpenAI @tool def run_test_case(project_path: str, test_file: str) -> str: """在指定项目中运行一个测试文件,返回测试结果与失败日志。""" # 这里会通过本地命令行执行 pytest result = execute_pytest(project_path, test_file) return result tools = [run_test_case] prompt = PromptTemplate.from_template( """你是测试验证子代理,负责生成并执行测试用例。 你的输入是需求清单和代码交付结果。 你需要先思考哪些场景需要覆盖,然后调用 run_test_case 工具执行测试。 只有当你拿到全部测试结果后,才能输出最终验收结论。 思考过程:{agent_scratchpad}""" ) agent = create_react_agent( llm=ChatOpenAI(model="gpt-4o", temperature=0), tools=tools, prompt=prompt ) executor = AgentExecutor( agent=agent, tools=tools, max_iterations=8, handle_parsing_errors=True, verbose=True )注意这里的max_iterations=8,它不是随便设的。我统计过测试验证代理正常处理一个规模适中的任务,平均需要4到5轮循环,所以8作为上限留出了余量,又不会让它在出错时无限空转。
3.3 不引入框架,用原生Python实现的最小ReAct循环
如果你不想引入LangChain,想彻底搞清楚ReAct内部原理,也可以自己实现一个最小版本。底层逻辑其实很直接:把Thought、Action、Observation拼成多轮消息,不断交给大模型,直到模型给出最终答案。
下面这是我早期验证原型时写的核心循环:
import json from openai import OpenAI client = OpenAI() def react_loop(user_input, tools, max_steps=5): messages = [{"role": "user", "content": user_input}] for step in range(max_steps): response = client.chat.completions.create( model="gpt-4o", messages=messages, tools=tools, tool_choice="auto" ) msg = response.choices[0].message messages.append(msg) if not msg.tool_calls: return msg.content for call in msg.tool_calls: result = dispatch_tool(call.function.name, call.function.arguments) messages.append({ "role": "tool", "tool_call_id": call.id, "content": result }) return "达到最大循环次数,任务未完成"这个版本的思路是:让模型自己决定是否需要调用工具,如果需要,代码就负责执行工具并返回结果,然后模型继续下一轮思考。LangChain内部做的也就是类似的事,只是多了提示词解析、错误恢复和迭代控制。
自己实现一遍最大的收获,是理解了为什么很多子代理会“跑偏”。问题通常不出在模型本身,而在于你给它的工具定义不够清晰。工具描述写得太含糊,模型会把它不需要的工具也选进来,造成无效循环。
4. 子代理之间的通信与结果汇聚
4.1 传递白板式的共享消息结构
五个子代理之间不是直接互相调用的,而是通过一个统一的消息总线交换数据。我把它称为“共享白板”,本质上就是一个全局字典,每个子代理执行完任务后,把结果写入白板,下一个子代理从白板里读自己需要的字段。
为什么不用直接函数调用?因为直接调用会让系统变成一张缠成一团的网状结构,后期加一个角色,就要改一堆上下游接口。共享白板把耦合降到了最低,每个子代理只依赖白板,不依赖其他子代理的具体实现。
白板的结构我定义如下:
whiteboard = { "original_request": "用户的原始需求", "requirements": None, # 需求分析子代理写入 "implementation": None, # 开发子代理写入 "test_results": None, # 测试子代理写入 "documentation": None, # 文档子代理写入 "execution_meta": { "task_status": {}, "error_logs": [], "token_usage": {} } }4.2 阶段调用链与异常回滚
调用链是按阶段顺序执行的,但并不是简单的“做完A再做B”。调度子代理会根据任务卡中的depends_on字段动态生成执行图。没有依赖关系的任务可以并行,有依赖关系的任务必须等前置任务完成。
我用一个轻量级状态机管理这个过程。每个任务有四种状态:pending、running、succeeded、failed。子代理返回结果后,状态机根据结果更新任务状态。一旦某个任务失败,所有依赖它的下游任务都会被标记为blocked,调度者会重新决定是尝试修复还是宣告整个流程失败。
当时有一个细节差点坑了我:某个子代理虽然在流程上失败了,但它白板上其实已经写入了部分有效结果,比如测试代理已经生成了20个用例,只是在执行时挂了。后来我加了一条规则:失败的任务必须保留所有部分产物到白板,并且用状态字段标记这是一份“不完整交付物”。这条规则在后来的问题排查中帮了大忙。
4.3 日志与可观测性设计
多代理系统做出来之后,调试难度是成倍增加的。单代理你只需要看一次对话,多代理你至少要同时看五段思考日志加一段调度日志。如果不做可观测性设计,系统一跑起来就是一团乱麻。
我给系统加了一个环形缓冲日志模块,每个子代理的Thought、Action、Observation都被记录为独立事件,附带一个trace_id贯穿整个流程。排查问题时,按trace_id过滤日志,就能看到从用户原始需求到最终交付文档的完整链路。
日志里我额外记录了两个指标:每个子代理消耗的token数和每一步的执行耗时。这两个指标最终导出了一张成本分布表,让我一眼看出哪个环节最烧钱,是后续优化的重要依据。
5. 落地过程中踩过的坑和最终调优方案
5.1 子代理不结束:无限循环与重复思考
第一个让我头大的坑是子代理陷入无限循环。表现很典型:模型反复输出Thought,说“我需要再确认一下需求”,然后重复调用同一个查询工具,拿回一样的答案,又继续思考。循环了十几轮就是不结束。
这个问题本质上是大模型在试图规避风险——它觉得信息不够,不敢下最终结论。解决办法是双管齐下:一方面设置max_iterations强制终止,另一方面在System Prompt里明确提示“当工具返回结果不足以支撑决策时,基于现有信息做出合理默认判断,并注记假设条件”。加了这条之后,极少再出现空转循环。
5.2 上下文污染:结果被拼成“四不像”
第二个坑是子代理之间的上下文污染。早期我为了让调度者能给出更全面的最终报告,会把前序所有子代理的完整输出都塞进它的Prompt里。结果调度者被文件字节数、测试失败日志、代码片段干扰,输出的总结反而偏离了用户关心的重点。
这正是做多代理系统最容易犯的错:试图让一个代理掌握全部信息,结果把所有代理都变成大杂烩。修复方式是“按需取读”,调度者只读取白板中的摘要字段,而不是原始长文本。我在每个子代理交付时都会额外生成一份200字以内的摘要,供上游汇总使用。这样信息量没有损失太多,噪声却过滤掉了大半。
5.3 成本失控:一次全流程的教训
当时有天真地让系统自动跑了一个规模较大的需求,结果单次流程消耗了非常大的token量,其中大头不是代码生成,而是测试失败之后的重复循环。开发者改一次,测试跑一次,不通过,开发者再改,测试再跑,来回转圈。
我后来引入两个降本机制:第一个是“快速失败”,测试代理在执行测试之前会对代码做静态检查,如果静态检查有明显语法错误,直接打回,不启动测试环境;第二个是“限次迭代”,开发者代理总共只允许处理两次返工,两次之后任务直接标记失败交给人工处理。这两个机制把成本降低了约一半,而且并没有明显降低任务成功率。
5.4 超时与流程卡死:生产可用的容错机制
最后一个问题是超时。某子代理调用外部工具时可能长时间无响应,比如网络抖动、服务超时,或者是生成的测试命令卡在等待输入。没有超时控制,整个流程就永远挂在那里。
我为每个工具调用都包了一层异步超时控制,超过指定时间自动终止,并把“工具超时”作为Observation返回给子代理,让它决定是重试还是切换方案。配合整体流程的超时保护,系统在长时间运行场景下终于敢留夜间自动执行了。
6. 实测效果、适用范围与未来可扩展方向
6.1 与单代理对比的实测数据
我拿同一批需求做过对比测试:一组用调优前的单代理超长Prompt处理,一组用现在这套五子代理系统处理。在任务完成率、过程可追溯性和成本三个维度上,差异非常明显。
单代理在需求稍微复杂时完成率开始断崖下跌,而且失败原因不固定,很难定位。五子代理系统的完成率明显更高,尤其是在带有“先分析后开发再验证”这类天然分阶段需求上,优势特别突出。虽然多代理的总token消耗略高于单代理,但这个差值完全在可接受范围内,换来的是过程可控性和结果稳定性,绝对值上其实非常划算。
6.2 什么项目不适合这种架构
也必须说实话,不是所有项目都适合多代理。如果你的需求非常短,比如“把这段文本翻译成英文”,把它拆成五个子代理只会增加成本和延迟。多代理架构适合的是流程长、工种多、需要高可靠性的任务;对于简单任务,单代理仍然是最优解。
另外要提醒一点:多代理系统对下游模型能力的要求更高。如果你用的是能力偏弱的模型,子代理可能连基本的Thought都写不清晰,更不用说规划任务和调用工具了。这套架构至少要使用当前主流旗舰模型,否则体验会很难受。
6.3 下一步迭代思路:记忆、路由与人工介入
目前系统已经能稳定跑通端到端流程,但我的迭代清单上还排着几个方向。第一个是长时记忆:把每次任务完成后的经验沉淀下来,下次遇到相似需求时,调度者可以读取历史经验调整任务拆解策略。第二个是动态路由:现在五个子代理是固定角色,将来可以做成插件式注册,根据任务类型动态加载不同角色。
第三个更重要,也是我一直坚持的想法:给系统加上“人工审批点”。在某些关键节点,比如调度者拆分任务之前、测试代理判定失败准备打回之前,先暂停并通知负责人确认。这种半自动模式比全自动在真实生产环境里更稳妥,也更接地气。
做多代理实战这段时间,我最深的体会是:不要想着用一个大而全的代理解决所有问题,也不要用一堆功能互相重叠的代理制造混乱。给每个代理明确的职责、清晰的结构化输入、可控的工具集合,以及完整的过程日志,系统自然就会稳定下来。你自己动手搭一套的话,建议先从一个最小闭环开始,哪怕只有两个子代理,先跑通ReAct循环和数据传递,再逐步加角色。过程中保留好每一次的决策日志,那才是整个系统最值钱的部分。