1. 多Agent协作到底在解决什么问题
单Agent跑任务,跑到一定复杂度就会撞墙。这不是模型能力不够,而是架构层面的天花板。我拿一个真实场景来说明:让一个Agent去完成“调研某个技术方向、输出一份带数据支撑的分析报告”这件事,它需要依次完成信息检索、数据清洗、逻辑组织、文字撰写、事实校验这几个环节。单Agent的做法是把所有指令塞进一个上下文窗口,让它一口气跑完。结果呢?跑到第三步的时候,前面检索到的原始数据已经被上下文挤掉了,模型开始凭记忆编造数据来源;跑到第五步校验的时候,它已经忘了自己写过什么,校验变成了走过场。
多Agent协作要解决的核心问题就三个:上下文隔离、角色专业化、任务可调度。把一个大任务拆成若干子任务,每个子任务交给一个独立的Agent,每个Agent有自己的上下文窗口、自己的系统提示词、自己的工具集。Agent之间通过结构化的消息传递来协同,而不是共享一个越来越臃肿的对话历史。
这个思路其实不新鲜,微服务架构就是这么干的。单体应用拆成微服务,每个服务独立部署、独立扩缩容、通过API通信。多Agent系统本质上是把“服务”换成了“Agent”,把“API调用”换成了“消息传递+任务调度”。理解了这个类比,后面很多设计决策就顺理成章了。
那为什么是现在这个时间点多Agent突然火起来了?两个原因。第一,大模型的指令遵循能力和结构化输出能力到了可用的水平,Agent之间的消息传递可以做到格式可控、语义清晰。第二,工具调用生态成熟了,每个Agent可以挂载不同的工具集,检索Agent挂搜索引擎,代码Agent挂代码执行器,写作Agent挂模板引擎。这两件事凑在一起,多Agent协作才从论文里的概念变成了工程上可落地的东西。
这篇文章适合谁看?如果你已经在用单Agent跑一些任务,但发现复杂任务总是差口气,或者你正在设计一个需要多个步骤、多个角色配合的AI系统,那这篇内容就是写给你的。我会从架构设计讲到任务调度,再讲到实操搭建,最后把踩过的坑都摊开来说。
2. 协作架构的核心设计思路与选型考量
2.1 三种主流协作拓扑的取舍
多Agent系统的协作拓扑,说到底就是Agent之间怎么连接、怎么通信。目前工程上常见的有三种:中心化调度、去中心化协商、层级化编排。这三种没有绝对的好坏,关键看你的任务特征。
中心化调度就是一个Orchestrator Agent负责拆解任务、分配任务、收集结果。其他Agent都是Worker,只负责执行自己那一块。这种架构的好处是控制流清晰,调试方便,Orchestrator可以看到全局状态,出问题容易定位。坏处是Orchestrator容易成为瓶颈,而且它对任务的拆解质量直接决定了整个系统的上限。我做过一个测试,让Orchestrator把一个“分析竞品定价策略”的任务拆成子任务,它拆出来的粒度时粗时细,有时候拆成三个子任务就覆盖了,有时候拆成八个还在纠结要不要加一个“收集用户评价”的步骤。这种不确定性在中心化架构里会被放大。
去中心化协商是Agent之间平等通信,通过消息传递来协调各自的工作。比如一个Agent完成检索后,把结果广播出去,其他Agent根据自己的需要决定是否消费这个结果。这种架构灵活性高,但调试难度大,而且容易出现“消息风暴”——Agent之间来回确认,消耗大量token却没推进实际任务。我在一个实验性项目里试过这种架构,五个Agent协作写一份报告,结果它们花了40%的token在互相确认“你需不需要这个数据”“你这个数据格式对不对”上面。
层级化编排是前两种的混合。顶层有一个调度Agent负责宏观任务分解,中间层有若干专业Agent负责领域内的子任务拆解,底层是执行Agent。这种架构适合任务层级深、领域跨度大的场景。比如“开发一个数据看板”这种任务,顶层拆成“数据层”“逻辑层”“展示层”,每层再往下拆。缺点是架构复杂度高,Agent数量多了之后,通信开销和状态同步会成为新问题。
我的建议是:从中心化调度开始,遇到瓶颈再考虑升级。大部分场景下,一个设计良好的Orchestrator加三到五个专业Worker就能覆盖80%的需求。不要一上来就搞层级化,那是给自己找麻烦。
2.2 Agent角色划分的粒度控制
角色划分是多Agent设计里最容易翻车的地方。划得太粗,每个Agent还是什么都干,等于没拆;划得太细,Agent数量爆炸,通信成本超过协作收益。
我总结了一个实用的判断标准:一个Agent的角色应该对应一个独立的“能力单元”。什么叫能力单元?就是这个Agent完成的任务,需要一套独立的工具集、一套独立的提示词策略、一套独立的输出格式。如果两个任务共享同一套工具和提示词,那它们就应该合并成一个Agent。
举个例子。在一个“技术文档撰写”的多Agent系统里,我最初划分了六个角色:需求分析Agent、资料检索Agent、大纲设计Agent、正文撰写Agent、代码示例Agent、质量校验Agent。跑了几轮之后发现,大纲设计和正文撰写其实共享同一套提示词策略和输出格式,合并成一个“内容生成Agent”之后效果反而更好,因为大纲和正文之间的衔接更自然了。代码示例Agent和正文撰写Agent虽然输出格式不同,但代码示例需要嵌入正文的特定位置,拆开之后反而增加了协调成本,最后也合并了。最终稳定在四个角色:需求分析、资料检索、内容生成、质量校验。
这个案例说明一个事:角色划分不是越细越好,而是要让每个Agent的职责边界清晰且自洽。判断标准就是看两个Agent之间是否需要频繁通信来对齐状态,如果需要,那它们大概率应该合并。
2.3 通信协议与消息格式设计
Agent之间怎么说话,这个问题的工程重要性被严重低估了。我见过太多多Agent项目,架构设计得很漂亮,但Agent之间的消息格式一团糟,导致整个系统跑起来像一群人在鸡同鸭讲。
消息格式设计的核心原则是:结构化、可校验、带上下文。结构化意味着消息有固定的字段,不是自由文本。可校验意味着接收方可以验证消息是否符合预期格式。带上下文意味着消息里要包含足够的背景信息,让接收方不需要回溯整个对话历史就能理解当前消息的含义。
我常用的消息格式是这样的:
{ "msg_id": "uuid", "sender": "retrieval_agent", "receiver": "content_agent", "task_id": "task_001", "msg_type": "result", "payload": { "content": "...", "metadata": { "source": "...", "confidence": 0.85, "timestamp": "..." } }, "context": { "original_query": "...", "previous_steps": ["step_1", "step_2"] } }这个格式里,msg_type字段区分消息类型(任务分配、中间结果、最终结果、错误报告等),payload放实际内容,context放上下文信息。接收方拿到消息后,先校验msg_type和payload的格式,再根据context决定怎么处理。
注意:消息格式一旦确定,所有Agent的系统提示词里都要明确写出这个格式的规范,并且要求Agent严格按照格式输出。我试过让Agent“尽量按照JSON格式输出”,结果它有时候输出JSON,有时候输出Markdown表格,有时候输出纯文本,下游Agent解析起来苦不堪言。后来改成“必须输出符合以下schema的JSON,否则视为任务失败”,格式一致性立刻上来了。
2.4 状态管理与上下文传递策略
多Agent系统里的状态管理,核心问题是:每个Agent需要知道多少上下文。给少了,Agent做决策时信息不足;给多了,上下文窗口被撑爆,而且引入无关信息会干扰Agent的判断。
我的做法是分层传递。每个Agent的上下文分三层:全局上下文(任务目标、整体约束)、局部上下文(当前子任务的相关信息)、即时上下文(当前消息的内容)。全局上下文在所有Agent之间共享,但只保留最核心的信息,比如任务目标、输出格式要求、关键约束条件。局部上下文由调度Agent在分配任务时注入,只包含与当前子任务直接相关的信息。即时上下文就是当前消息本身。
这种分层策略的好处是,每个Agent的上下文窗口里只有它真正需要的信息,不会被无关内容干扰。我实测下来,同样的任务,分层传递比全量传递的token消耗降低了约60%,而且输出质量更稳定,因为Agent不会被无关信息带偏。
3. 任务调度机制的核心细节与实操要点
3.1 任务分解的粒度与依赖分析
任务分解是多Agent调度的第一步,也是最关键的一步。分解得好,后面调度顺风顺水;分解得不好,要么Agent之间频繁等待,要么某些Agent闲着没事干。
我用的分解方法是基于依赖关系的递归分解。先识别任务的主要阶段,再分析阶段之间的依赖关系,最后把每个阶段拆成可并行执行的子任务。举个例子,“撰写一份技术分析报告”这个任务,主要阶段是:资料收集、资料分析、大纲设计、内容撰写、质量校验。依赖关系是:资料分析依赖资料收集,大纲设计依赖资料分析,内容撰写依赖大纲设计,质量校验依赖内容撰写。这是一个线性依赖链,没有并行空间。
但如果任务变成“分析三个竞品的技术方案并输出对比报告”,那资料收集阶段就可以拆成三个并行的子任务,每个子任务负责一个竞品。资料分析阶段也可以并行,但大纲设计需要等三个竞品的分析都完成之后才能开始。这种依赖关系用有向无环图(DAG)来表示最清晰。
# 任务依赖图的简化表示 task_graph = { "collect_competitor_a": [], "collect_competitor_b": [], "collect_competitor_c": [], "analyze_competitor_a": ["collect_competitor_a"], "analyze_competitor_b": ["collect_competitor_b"], "analyze_competitor_c": ["collect_competitor_c"], "design_outline": ["analyze_competitor_a", "analyze_competitor_b", "analyze_competitor_c"], "write_content": ["design_outline"], "quality_check": ["write_content"] }这个图里,collect_competitor_a/b/c三个任务没有依赖,可以并行执行。analyze_competitor_a/b/c各自依赖对应的收集任务,也可以并行。design_outline需要等三个分析任务都完成。调度器根据这个图来决定任务的执行顺序和并行度。
实操心得:任务分解的粒度控制在“一个Agent一次能完成”的范围内。如果一个子任务需要Agent进行多轮工具调用或者多步推理,那说明粒度太粗,需要继续拆。但如果一个子任务只需要Agent做一次简单的格式转换,那说明粒度太细,应该合并到相邻任务里。
3.2 调度策略:轮询、优先级与动态调整
任务调度的核心决策是:下一个执行哪个任务,分配给哪个Agent。这个决策的质量直接影响系统的吞吐量和响应时间。
最简单的策略是轮询:按照任务图的拓扑顺序,依次执行就绪任务。这种策略实现简单,但效率不高,因为不同任务的执行时间差异很大,轮询会导致某些Agent空闲等待。
优先级调度给每个任务分配一个优先级,调度器总是选择优先级最高的就绪任务。优先级的设定可以基于任务的关键路径长度(关键路径上的任务优先级高)、任务的预期执行时间(短任务优先级高,提高吞吐量)、或者任务的依赖深度(深层任务优先级高,避免阻塞后续任务)。
我实际用的是动态优先级调整。初始优先级基于关键路径长度设定,但在执行过程中,如果某个任务被阻塞(依赖的任务还没完成),它的优先级会临时降低,让其他就绪任务先执行。如果某个任务成为关键路径上的瓶颈,它的优先级会动态提升。这种策略在任务图比较复杂、执行时间不确定的场景下效果很好。
def dynamic_priority(task, task_graph, completed_tasks): # 基础优先级:关键路径长度 base_priority = calculate_critical_path_length(task, task_graph) # 动态调整:如果任务被阻塞,降低优先级 if not all(dep in completed_tasks for dep in task_graph[task]): base_priority -= 10 # 动态调整:如果任务是关键路径瓶颈,提升优先级 if is_bottleneck(task, task_graph, completed_tasks): base_priority += 20 return base_priority3.3 Agent能力匹配与负载均衡
调度器把任务分配给Agent时,需要考虑两个因素:能力匹配和负载均衡。能力匹配是指Agent的技能集是否覆盖任务需求。负载均衡是指避免某些Agent过载而其他Agent空闲。
能力匹配的实现方式是在Agent注册时声明它的能力标签,调度器根据任务的能力需求来筛选候选Agent。比如一个“代码生成”任务需要code_generation能力,调度器就只考虑声明了该能力的Agent。
负载均衡的实现方式有多种。最简单的是轮询分配,在候选Agent之间轮流分配任务。稍微复杂一点的是基于当前队列长度的分配,优先分配给队列最短的Agent。更精细的是基于历史执行时间的分配,优先分配给平均执行时间最短的Agent。
我实际用的是加权轮询加队列长度修正。每个Agent有一个权重(基于历史表现),调度器按照权重比例分配任务,但如果某个Agent的队列长度超过阈值,就临时降低它的权重。这种策略在Agent能力有差异、任务执行时间有波动的场景下比较稳健。
| 调度策略 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 轮询 | 任务同质、Agent能力相近 | 实现简单、公平 | 不考虑能力和负载差异 |
| 优先级 | 任务重要性差异大 | 关键任务优先执行 | 优先级设定需要经验 |
| 动态优先级 | 任务图复杂、执行时间不确定 | 自适应、效率高 | 实现复杂度高 |
| 能力匹配 | Agent能力差异大 | 任务分配精准 | 需要维护能力标签 |
| 负载均衡 | 任务量大、Agent数量多 | 资源利用率高 | 可能牺牲能力匹配 |
3.4 超时处理与失败重试机制
多Agent系统里,Agent执行失败是常态而不是异常。网络抖动、工具调用超时、模型输出格式错误,这些都会导致任务失败。调度器必须有完善的超时处理和失败重试机制。
超时处理的核心是分级超时。每个任务有一个预期执行时间,超过这个时间的一定倍数(比如1.5倍)就判定为超时。超时后,调度器可以选择重试、降级处理、或者标记失败并继续执行其他任务。
失败重试的策略需要区分失败类型。如果是瞬时故障(网络抖动、工具临时不可用),直接重试即可。如果是格式错误(Agent输出不符合schema),需要把错误信息反馈给Agent,让它重新生成。如果是能力不足(Agent无法完成任务),需要降级处理或者转交给其他Agent。
def handle_task_failure(task, error, retry_count): if retry_count >= MAX_RETRY: # 超过最大重试次数,标记失败 mark_task_failed(task, error) return if is_transient_error(error): # 瞬时故障,直接重试 retry_task(task) elif is_format_error(error): # 格式错误,反馈给Agent重新生成 retry_task_with_feedback(task, error) elif is_capability_error(error): # 能力不足,降级处理或转交 degrade_or_reassign(task)注意:重试次数不要设太多,一般2到3次就够了。我见过一个项目设了10次重试,结果一个格式错误的Agent反复重试了10次,消耗了大量token,最后还是失败。重试次数多了不仅浪费资源,还会延迟整个任务的完成时间。
4. 完整实操:从零搭建一个多Agent协同系统
4.1 环境准备与基础框架选型
搭建多Agent系统,第一步是选框架。目前工程上常用的有AutoGen、CrewAI、LangGraph这几个。AutoGen的强项是对话驱动的多Agent协作,适合需要Agent之间频繁交互的场景。CrewAI的强项是角色定义和任务分配,适合角色边界清晰的场景。LangGraph的强项是状态管理和流程控制,适合任务图复杂的场景。
我这次实操用的是基于消息传递的自研轻量框架,原因有两个:一是自研框架可以完全控制消息格式和调度逻辑,方便调试和优化;二是依赖少,部署简单,不需要引入额外的运行时。如果你不想自研,LangGraph是更稳妥的选择,它的状态管理和流程控制能力比较成熟。
环境准备方面,需要Python 3.10以上版本,以及一个大模型API的访问权限。我用的模型是Qwen2.5-7B的API版本,主要是因为它对中文任务的支持比较好,而且结构化输出的稳定性不错。如果你用本地部署的模型,建议至少用7B参数以上的模型,太小的模型在结构化输出和指令遵循上容易出问题。
# 创建虚拟环境 python -m venv multi_agent_env source multi_agent_env/bin/activate # 安装基础依赖 pip install requests pydantic python-dotenv4.2 Agent基类的设计与实现
Agent基类是整个系统的核心抽象。它需要封装Agent的通用能力:接收消息、处理消息、发送消息、调用工具、管理上下文。我设计的基类包含以下几个核心方法:
class BaseAgent: def __init__(self, name, role, capabilities, tools, model_config): self.name = name self.role = role self.capabilities = capabilities self.tools = tools self.model_config = model_config self.context = [] self.message_queue = [] def receive_message(self, message): """接收消息并加入队列""" self.message_queue.append(message) def process_message(self, message): """处理单条消息,返回结果""" # 构建提示词 prompt = self.build_prompt(message) # 调用模型 response = self.call_model(prompt) # 解析输出 result = self.parse_output(response) return result def build_prompt(self, message): """构建提示词,包含角色、上下文、消息内容""" system_prompt = f"你是{self.role}。你的能力包括:{self.capabilities}。" context_str = self.format_context() message_str = self.format_message(message) return f"{system_prompt}\n\n上下文:\n{context_str}\n\n当前消息:\n{message_str}" def call_model(self, prompt): """调用大模型API""" # 实际调用逻辑 pass def parse_output(self, response): """解析模型输出,校验格式""" # 实际解析逻辑 pass def send_message(self, receiver, msg_type, payload): """发送消息给其他Agent""" message = { "msg_id": generate_uuid(), "sender": self.name, "receiver": receiver, "msg_type": msg_type, "payload": payload, "context": self.get_context_summary() } return message这个基类的设计要点是:消息处理是同步的,但消息队列是异步的。Agent接收消息后先入队,然后由调度器决定什么时候处理。这样调度器可以控制Agent的执行节奏,避免所有Agent同时调用模型导致API限流。
4.3 调度器的核心逻辑实现
调度器是整个系统的大脑。它负责维护任务图、管理Agent注册表、分配任务、监控执行状态、处理失败重试。核心逻辑如下:
class Scheduler: def __init__(self): self.task_graph = {} self.agents = {} self.completed_tasks = set() self.running_tasks = {} self.failed_tasks = {} def register_agent(self, agent): """注册Agent""" self.agents[agent.name] = agent def add_task(self, task_id, dependencies, capability_required): """添加任务到任务图""" self.task_graph[task_id] = { "dependencies": dependencies, "capability_required": capability_required, "status": "pending" } def get_ready_tasks(self): """获取所有就绪任务(依赖已完成)""" ready = [] for task_id, task_info in self.task_graph.items(): if task_info["status"] != "pending": continue if all(dep in self.completed_tasks for dep in task_info["dependencies"]): ready.append(task_id) return ready def select_agent(self, task_id): """为任务选择最合适的Agent""" task_info = self.task_graph[task_id] required_cap = task_info["capability_required"] # 筛选具备所需能力的Agent candidates = [ agent for agent in self.agents.values() if required_cap in agent.capabilities ] if not candidates: raise NoCapableAgentError(f"没有Agent具备能力:{required_cap}") # 按队列长度排序,选择队列最短的 candidates.sort(key=lambda a: len(a.message_queue)) return candidates[0] def dispatch(self): """调度主循环""" while not self.is_all_completed(): ready_tasks = self.get_ready_tasks() for task_id in ready_tasks: agent = self.select_agent(task_id) task_info = self.task_graph[task_id] task_info["status"] = "running" # 构建任务消息 message = { "msg_type": "task_assignment", "task_id": task_id, "payload": task_info.get("payload", {}), "context": self.get_global_context() } agent.receive_message(message) self.running_tasks[task_id] = agent.name # 处理Agent的输出 self.process_agent_outputs() # 检查超时 self.check_timeouts() def process_agent_outputs(self): """处理Agent的输出消息""" for agent in self.agents.values(): while agent.message_queue: message = agent.message_queue.pop(0) result = agent.process_message(message) if result["status"] == "success": task_id = message["task_id"] self.completed_tasks.add(task_id) self.task_graph[task_id]["status"] = "completed" self.task_graph[task_id]["result"] = result["data"] elif result["status"] == "error": self.handle_task_failure(message["task_id"], result["error"])这个调度器的核心是dispatch方法里的主循环:获取就绪任务、分配Agent、处理输出、检查超时。循环直到所有任务完成或失败。
4.4 一个完整案例:技术调研报告生成
我用一个具体案例来演示整个系统的运行过程。任务是“调研大模型多Agent协作的技术现状,输出一份3000字的分析报告”。
任务分解结果:
| 任务ID | 任务描述 | 依赖 | 所需能力 |
|---|---|---|---|
| T1 | 检索多Agent协作的学术论文 | 无 | retrieval |
| T2 | 检索多Agent协作的工程实践 | 无 | retrieval |
| T3 | 分析检索结果,提取关键信息 | T1, T2 | analysis |
| T4 | 设计报告大纲 | T3 | content_generation |
| T5 | 撰写报告正文 | T4 | content_generation |
| T6 | 校验报告事实准确性 | T5 | quality_check |
Agent配置:
| Agent名称 | 角色 | 能力 | 工具 |
|---|---|---|---|
| Retriever-A | 学术检索专家 | retrieval | 学术搜索引擎 |
| Retriever-B | 工程检索专家 | retrieval | 技术社区搜索 |
| Analyst | 技术分析师 | analysis | 文本分析工具 |
| Writer | 技术写作者 | content_generation | 模板引擎 |
| Checker | 质量校验员 | quality_check | 事实核查工具 |
执行过程:
调度器首先发现T1和T2就绪(无依赖),分别分配给Retriever-A和Retriever-B。两个Agent并行执行检索任务,各自返回检索结果。T1和T2完成后,T3就绪,分配给Analyst。Analyst拿到两份检索结果,进行交叉分析,提取出关键信息点。T3完成后,T4就绪,分配给Writer。Writer根据分析结果设计报告大纲。T4完成后,T5就绪,继续分配给Writer。Writer根据大纲撰写正文。T5完成后,T6就绪,分配给Checker。Checker对正文进行事实校验,标记出需要修正的地方。如果Checker发现问题,会触发T5的重试,Writer根据反馈修正内容。
整个流程跑下来,从开始到结束大约需要3到5分钟,取决于模型响应速度和检索工具的效率。相比单Agent方案,多Agent方案的优势在于:检索阶段可以并行,节省时间;每个Agent的上下文更聚焦,输出质量更稳定;质量校验环节真正起到了作用,而不是走过场。
5. 常见问题与排查技巧实录
5.1 Agent输出格式不一致的排查与解决
这是多Agent系统里最高频的问题。Agent有时候输出JSON,有时候输出Markdown,有时候输出纯文本,导致下游Agent解析失败。
排查思路:先检查系统提示词里是否明确规定了输出格式。如果规定了但Agent还是不遵守,检查提示词里是否有“尽量”“建议”这类模糊词汇。如果有,改成“必须”“否则视为失败”。如果还是不行,检查模型本身的结构化输出能力,有些小模型在复杂schema上的遵循能力确实不行。
解决方案:我用的方法是双重保障。第一层是在提示词里明确规定输出格式,并给出示例。第二层是在解析输出时做格式校验,如果不符合格式,自动触发重试,并把格式错误信息反馈给Agent。实测下来,加了第二层之后,格式一致性从70%左右提升到了95%以上。
def parse_output_with_validation(response, expected_schema): try: data = json.loads(response) validate_schema(data, expected_schema) return {"status": "success", "data": data} except json.JSONDecodeError: return {"status": "error", "error": "输出不是合法JSON"} except SchemaValidationError as e: return {"status": "error", "error": f"schema校验失败:{e}"}5.2 任务死锁与循环依赖的处理
任务死锁是指两个或多个任务互相等待对方完成,导致谁也无法推进。循环依赖是死锁的常见原因。
排查思路:检查任务图的依赖关系,看是否存在环。可以用拓扑排序来检测,如果拓扑排序无法完成,说明存在环。
解决方案:在添加任务时做依赖检查,如果发现添加新任务会导致环,直接拒绝并报错。另外,设置任务的最大等待时间,如果某个任务等待依赖超过阈值,强制标记为失败并触发人工介入。
def has_cycle(task_graph): """检测任务图是否存在环""" visited = set() rec_stack = set() def dfs(node): visited.add(node) rec_stack.add(node) for dep in task_graph.get(node, {}).get("dependencies", []): if dep not in visited: if dfs(dep): return True elif dep in rec_stack: return True rec_stack.remove(node) return False for node in task_graph: if node not in visited: if dfs(node): return True return False5.3 上下文膨胀导致模型输出质量下降
多Agent系统跑久了,每个Agent的上下文会越来越长,最终导致模型输出质量下降,甚至超出上下文窗口限制。
排查思路:监控每个Agent的上下文长度,如果超过模型上下文窗口的70%,就需要警惕了。观察输出质量是否随着上下文增长而下降。
解决方案:我用的方法是上下文压缩加滑动窗口。对于历史消息,只保留最近N条完整消息,更早的消息做摘要压缩。摘要只保留关键信息:任务目标、已完成步骤、当前状态。这样既保留了必要的上下文,又控制了长度。
实操心得:上下文压缩的摘要质量很关键。我试过用规则做摘要(只保留消息的
msg_type和task_id),结果Agent丢失了太多上下文,输出质量明显下降。后来改成用模型做摘要,保留关键语义信息,效果好很多。摘要的提示词是:“请用不超过100字总结以下对话的关键信息,包括任务目标、已完成步骤、当前状态、待解决问题。”
5.4 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| Agent输出格式不一致 | 提示词不明确、模型能力不足 | 检查提示词、测试模型结构化输出 | 明确格式要求、加格式校验和重试 |
| 任务死锁 | 循环依赖 | 拓扑排序检测 | 添加任务时检查环、设置超时 |
| 上下文膨胀 | 历史消息累积 | 监控上下文长度 | 上下文压缩、滑动窗口 |
| Agent能力不匹配 | 能力标签错误 | 检查Agent注册信息 | 修正能力标签、增加候选Agent |
| 调度效率低 | 优先级设置不合理 | 分析任务执行时间分布 | 动态优先级调整 |
| 重试风暴 | 重试次数过多 | 检查重试日志 | 限制重试次数、区分错误类型 |
| API限流 | 并发请求过多 | 监控API调用频率 | 调度器控制并发度、加退避策略 |
5.5 性能优化的几个实用技巧
第一个技巧是批量处理。如果多个任务需要调用同一个工具,可以把它们合并成一次调用。比如三个检索任务都需要调用搜索引擎,可以合并成一次批量检索,减少API调用次数。
第二个技巧是缓存复用。如果某个Agent的输出会被多个下游Agent使用,把它缓存起来,避免重复生成。我实测下来,缓存命中率在30%左右的时候,整体token消耗能降低20%以上。
第三个技巧是异步执行。调度器的主循环里,Agent的消息处理可以异步执行,不需要等一个Agent处理完再处理下一个。这样在Agent数量多的时候,整体吞吐量能提升不少。但要注意控制并发度,避免API限流。
import asyncio async def process_agent_outputs_async(agents): tasks = [] for agent in agents: while agent.message_queue: message = agent.message_queue.pop(0) tasks.append(asyncio.create_task(agent.process_message_async(message))) if tasks: results = await asyncio.gather(*tasks, return_exceptions=True) return results return []6. 多Agent系统的扩展方向与个人体会
这套系统跑通之后,我做了几个扩展实验。一个是动态Agent创建,当任务图里出现新的能力需求时,调度器动态创建一个具备该能力的Agent,而不是预先注册所有Agent。这个扩展在任务类型不确定的场景下很有用,但需要解决Agent创建的开销和一致性问题。
另一个是跨系统协作,把多个独立的多Agent系统通过消息队列连接起来,形成一个更大的协作网络。这个方向适合企业级场景,但通信协议和状态同步的复杂度会显著上升。
还有一个是人机混合协作,在关键决策点引入人工审核,Agent生成的结果经过人工确认后再进入下一环节。这个方向在质量要求高的场景下很实用,比如技术报告的事实校验环节,人工审核能显著降低错误率。
我个人在实际操作中的体会是:多Agent系统的核心价值不在于Agent数量多,而在于每个Agent的职责边界清晰、通信协议规范、调度逻辑可控。我见过太多项目追求Agent数量,搞了十几个Agent,结果通信开销比单Agent还大,输出质量也没提升。真正有效的多Agent系统,往往是三到五个Agent,每个Agent都经过精心设计,调度逻辑简洁高效。
最后分享一个小技巧:在系统上线之前,先用模拟Agent跑一遍完整流程。模拟Agent不调用真实模型,而是返回预设的响应。这样可以快速验证调度逻辑、消息格式、依赖关系是否正确,而不需要消耗真实的token。等模拟跑通了,再切换到真实模型,能省下不少调试成本。