1. 从一个真实请求说起:为什么单Agent不够用了
假设你对着一个智能助手说:“帮我查一下上季度华东区的销售数据,做个同比分析,然后生成一份PPT,顺便把关键结论发邮件给相关负责人。”这句话里其实藏了至少四个独立任务:数据查询、统计分析、文档生成、消息发送。如果只用一个Agent去扛,它得同时具备数据库查询能力、数据分析能力、PPT排版能力、邮件协议对接能力,还要在一条推理链里把所有事串起来。这种“全能型Agent”在Demo阶段看着很美好,一旦落到真实业务里,维护成本会指数级上升。
我最早接触多Agent协作是在一个内部知识管理项目里。当时的需求是让系统自动处理用户提交的各种格式文档,提取关键信息,分类归档,再根据内容生成摘要推送给不同团队。一开始我们写了一个巨大的单体Agent,提示词塞了三千多字,工具挂了十几个。结果就是:它经常在“该调哪个工具”这一步就犯迷糊,偶尔还会把归档操作和推送操作搞混。后来拆成三个各司其职的Agent——一个负责解析文档,一个负责分类决策,一个负责通知分发——问题立刻少了一大半。
这就是A2A(Agent to Agent)要解决的核心问题:当任务复杂度超过单个Agent的合理承载范围时,如何让多个Agent各司其职、高效协作。A2A不是某个具体的框架或协议,而是一套设计思路,涵盖Agent怎么选、怎么分工、怎么通信、状态和记忆放在哪里。它适合那些正在从“玩具级Agent”向“生产级Agent系统”过渡的开发者,也适合想理解多Agent协作底层逻辑的技术管理者。
这篇文章我会沿着一条完整的请求链路,把A2A涉及的几个关键决策点逐一拆开讲。每个决策点我都会说清楚:当时面临什么选择、为什么选了这个方案、实际跑下来效果如何、踩过哪些坑。你不需要认同我的所有选择,但至少能拿到一套可参考的决策框架。
2. Agent选型:什么活该交给什么Agent
2.1 先搞清楚Agent的能力边界在哪里
选Agent的第一步不是看市面上有哪些框架,而是先把自己的任务拆清楚。我习惯用一个很简单的方法:把整个请求拆成原子任务,然后给每个原子任务打两个标签——确定性和交互复杂度。
确定性指的是这个任务的输出是否可预期。比如“从数据库查一条记录”确定性很高,“判断一段文本的情感倾向”确定性中等,“根据模糊需求生成一份报告”确定性很低。交互复杂度指的是这个任务需要和外部系统打交道的次数和深度。查一次数据库算低,需要多轮API调用加结果校验算高。
把任务按这两个维度分完,你大概能得到四类:
| 类型 | 确定性 | 交互复杂度 | 适合的Agent形态 |
|---|---|---|---|
| 工具型 | 高 | 低 | 函数调用型Agent,甚至直接写死逻辑 |
| 流程型 | 高 | 高 | 带状态机的Workflow Agent |
| 推理型 | 低 | 低 | 纯LLM推理Agent,不需要挂太多工具 |
| 协作型 | 低 | 高 | 需要多Agent协商的复杂Agent |
这个分类不是绝对的,但能帮你快速判断:哪些任务根本不需要“智能”,直接写代码就行;哪些任务需要LLM的推理能力;哪些任务需要多个Agent来回商量。
2.2 工具型Agent:能写死就别让LLM猜
我见过很多项目犯的一个通病:明明是一个固定的数据查询逻辑,非要用LLM去理解用户意图再生成SQL。结果就是用户换个说法,LLM就生成一条错误的SQL,然后整个链路崩掉。
工具型Agent的核心原则是:LLM只负责参数提取,不负责逻辑决策。比如用户说“查一下上个月华东区的销售数据”,LLM只需要提取出时间范围=上个月、区域=华东、指标=销售数据这三个参数,然后调用一个预先写好的查询函数。查询逻辑本身是确定的,不需要LLM参与。
这样做的好处很明显:稳定、可测试、可监控。你可以给这个查询函数写单元测试,可以加日志,可以在参数提取失败时给出明确的错误提示。如果让LLM直接生成SQL,你连测试用例都不好写。
实操心得:工具型Agent的提示词要尽可能短,只保留参数提取的指令和输出格式要求。提示词越长,LLM越容易“自由发挥”,反而降低稳定性。
2.3 推理型Agent:给它思考的空间,但别让它碰执行
推理型Agent适合处理那些没有标准答案、需要结合上下文判断的任务。比如“判断这份合同里有哪些风险条款”,或者“根据用户的历史行为推荐下一步操作”。这类任务的特点是:输入是非结构化的,输出也是非结构化的,中间需要LLM的推理能力。
但推理型Agent有一个很大的风险:它容易“想太多”。如果你给它挂了工具,它可能会在推理过程中突然决定去调用某个工具,然后基于工具返回的结果继续推理,最后得出一个你完全没预期的结论。这种不确定性在Demo里看着很智能,在生产环境里就是灾难。
我的做法是:推理型Agent只负责输出结构化的推理结果,不直接执行任何操作。比如让它输出一个JSON,里面包含风险等级、风险描述、建议操作,然后由下游的流程型Agent去决定要不要执行、怎么执行。这样既保留了LLM的推理能力,又把执行权收回到可控的流程里。
2.4 协作型Agent:什么时候真的需要多个Agent商量
协作型Agent是最复杂的一类,也是A2A真正发挥价值的地方。但我要先泼一盆冷水:大部分场景下你不需要协作型Agent。很多开发者一上来就搞三四个Agent互相聊天,结果发现它们要么陷入无限循环,要么互相推诿,要么给出互相矛盾的结论。
真正需要协作型Agent的场景通常满足两个条件:第一,任务没有唯一的正确路径,需要根据中间结果动态调整策略;第二,不同子任务之间需要共享上下文,且共享的内容会随着推理过程变化。
举个例子:一个智能客服系统,用户的问题可能涉及订单查询、退换货政策、物流跟踪三个领域。如果用户的问题很明确,直接路由到对应的工具型Agent就行。但如果用户的问题是“我上周买的那个东西到现在还没到,我想退了重新买一个”,这就涉及订单查询、物流跟踪、退换货政策、重新下单四个环节,而且中间可能需要根据物流状态决定是否真的要走退换货流程。这种场景下,一个协调者Agent加几个执行者Agent的协作模式就比较合适。
3. 协作机制:Agent之间怎么说话、怎么分工
3.1 通信协议:别让Agent用自然语言互相聊天
我见过最离谱的多Agent实现,是让两个Agent用自然语言互相发消息,然后另一个Agent去解析这些消息。这种做法的稳定性极差,因为自然语言有歧义,LLM每次生成的表述可能都不一样,下游Agent解析起来很容易出错。
更可靠的做法是:Agent之间的通信使用结构化消息。消息体至少包含以下几个字段:
{ "sender": "agent_a", "receiver": "agent_b", "task_id": "task_001", "action": "request_data", "payload": { "query": "华东区上季度销售数据", "format": "json" }, "timestamp": "2025-01-15T10:30:00Z" }这样做的好处是:消息可校验、可日志、可重放。你可以给消息定义严格的Schema,任何不符合Schema的消息直接拒绝,避免下游Agent收到乱七八糟的输入。同时,结构化消息也方便你做链路追踪,出问题时能快速定位是哪个环节出了错。
注意事项:结构化消息的Schema不要设计得太复杂。我见过一个项目定义了二十多个字段,结果每个Agent都要花大量时间在填充和校验消息上,反而降低了效率。通常五到八个核心字段就够了。
3.2 分工模式:协调者模式和流水线模式
多Agent协作的分工模式主要有两种:协调者模式和流水线模式。
协调者模式是一个中心化的Agent负责拆解任务、分配任务、汇总结果,其他Agent只负责执行。这种模式的好处是控制力强,协调者可以根据中间结果动态调整任务分配。缺点是协调者容易成为瓶颈,而且协调者本身的提示词设计很关键,它需要理解全局任务,还要知道每个执行者Agent的能力边界。
流水线模式是任务按固定顺序依次经过多个Agent,每个Agent处理完自己的部分就传给下一个。这种模式的好处是简单、可预测、容易调试。缺点是灵活性差,如果中间某个环节的输出不符合预期,整个流水线可能就卡住了。
我的经验是:先用流水线模式跑通,再根据实际需求决定是否引入协调者。很多任务其实用流水线就能解决,强行上协调者反而增加了复杂度和不确定性。
3.3 任务分配:怎么决定谁干什么
任务分配的核心问题是:协调者怎么知道该把任务分配给谁?有两种常见做法。
一种是能力注册:每个执行者Agent在启动时向协调者注册自己的能力描述,协调者根据能力描述来匹配任务。这种做法的好处是动态性强,新增Agent不需要改协调者的代码。缺点是能力描述本身可能不准确,而且协调者需要额外的逻辑来判断“这个任务到底需要什么能力”。
另一种是硬编码路由:协调者里直接写死什么任务交给什么Agent。这种做法简单直接,适合Agent数量少、任务类型固定的场景。缺点是扩展性差,新增Agent需要改协调者的代码。
我目前更倾向于一种混合做法:用能力注册做粗筛,用硬编码路由做精筛。协调者先根据能力描述筛选出候选Agent,然后根据预定义的路由规则决定最终分配给谁。这样既保留了一定的动态性,又避免了完全依赖能力描述带来的不确定性。
3.4 冲突处理:当两个Agent给出不同结论时怎么办
多Agent协作中一个很现实的问题是:两个Agent对同一件事给出了不同的结论。比如一个Agent说“这个订单可以退”,另一个Agent说“这个订单不符合退货条件”。这时候怎么办?
我的做法是引入一个仲裁机制。仲裁者可以是一个独立的Agent,也可以是一段确定性代码。如果是确定性代码,就预先定义好优先级规则,比如“政策类Agent的结论优先于推荐类Agent的结论”。如果是仲裁Agent,就让它根据两个结论的推理过程和依据来做判断。
但我要提醒一点:仲裁机制不要设计得太复杂。我见过一个项目搞了三层仲裁,结果仲裁本身成了最大的不确定性来源。大多数情况下,简单的优先级规则就够了。如果两个Agent的结论经常冲突,那说明任务拆分本身有问题,应该回头去调整分工,而不是在仲裁上堆逻辑。
4. 记忆管理:状态放在哪里、怎么共享
4.1 记忆的分类:短期记忆、长期记忆、共享记忆
在多Agent系统里,记忆管理是最容易被忽视、但出问题最多的环节。我习惯把记忆分成三类:
短期记忆是单个Agent在一次任务执行过程中的上下文。比如用户说了什么、Agent调用了哪些工具、工具返回了什么结果。这类记忆通常放在Agent的上下文窗口里,任务结束就丢弃。
长期记忆是跨任务持久化的信息。比如用户的历史偏好、之前处理过的类似案例、领域知识库。这类记忆需要外部存储,Agent在需要时主动查询。
共享记忆是多个Agent之间共享的上下文。比如协调者拆解任务时产生的任务描述、执行者Agent的中间结果、最终汇总的输出。这类记忆需要放在一个所有Agent都能访问的地方,同时要处理好并发读写的问题。
4.2 短期记忆:上下文窗口不是越大越好
很多开发者有一个误区:既然LLM的上下文窗口越来越大,那就把所有东西都塞进去。实际跑下来你会发现,上下文越长,LLM的注意力越容易分散,关键信息反而被淹没了。
我的做法是:短期记忆只保留当前任务必需的信息。具体来说,每个Agent的上下文里只放三样东西:当前任务的描述、最近一次工具调用的结果、以及一个精简的历史摘要。历史摘要由Agent自己生成,只保留关键决策点和结论,不保留完整的对话记录。
实操心得:我通常会给每个Agent设置一个上下文预算,比如4000个token。当上下文接近预算时,触发一次摘要压缩,把之前的对话压缩成一段简短的摘要。这样既能保留关键信息,又不会让上下文无限膨胀。
4.3 长期记忆:向量数据库不是唯一选择
提到长期记忆,很多人第一反应是向量数据库。向量数据库确实适合存储非结构化的文本记忆,但它不是唯一选择,也不一定是最优选择。
对于结构化的长期记忆,比如用户ID、订单号、历史操作记录,用传统的关系型数据库或者键值存储反而更合适。查询速度快、一致性有保障、运维成本低。我见过一个项目把所有记忆都塞进向量数据库,结果查一个订单状态还要做语义检索,延迟高得离谱。
我的建议是:根据记忆的类型选择存储方案。结构化数据用关系型数据库,非结构化文本用向量数据库,频繁访问的热数据加一层缓存。不要为了技术上的“统一”而牺牲实际性能。
4.4 共享记忆:并发读写怎么保证一致性
共享记忆最大的挑战是并发读写。多个Agent可能同时读取同一份共享记忆,也可能同时写入。如果不加控制,就会出现数据不一致的问题。
我的做法是引入一个记忆管理器,所有对共享记忆的读写都通过它来进行。记忆管理器负责加锁、版本控制、冲突检测。具体来说:
- 读操作:记忆管理器返回当前版本的数据,并记录读取者。
- 写操作:记忆管理器检查版本号,如果版本号与读取时不一致,拒绝写入并通知Agent重新读取。
- 冲突处理:如果两个Agent同时写入,后写入的Agent需要先合并前一个Agent的修改,再提交。
这套机制听起来简单,但实际实现时要注意锁的粒度。锁太粗会影响并发性能,锁太细会增加死锁风险。我通常按任务ID来加锁,同一个任务内的共享记忆用同一把锁,不同任务之间互不影响。
4.5 记忆的生命周期管理
记忆不是越多越好。随着系统运行时间增长,记忆会不断累积,如果不做清理,不仅存储成本上升,检索效率也会下降。
我通常会给记忆设置一个生命周期策略:
- 短期记忆:任务结束后立即清除。
- 共享记忆:任务结束后保留一段时间(比如24小时),用于问题排查和审计,然后归档或删除。
- 长期记忆:根据业务需求设置保留期限,比如用户偏好保留一年,操作日志保留三个月。
同时,我会定期对长期记忆做一次“记忆整理”,把相似的记忆合并,把过期的记忆标记为失效,把高频访问的记忆提升到更快的存储层。这个过程可以离线做,不影响在线服务。
5. 完整链路拆解:一次请求的A2A之旅
5.1 请求入口:意图识别与任务拆解
用户发来请求后,第一个接触请求的Agent是入口Agent。它的职责很单一:判断用户意图,把请求拆解成结构化的任务列表。
比如用户说“帮我查一下上季度华东区的销售数据,做个同比分析,然后生成一份PPT,顺便把关键结论发邮件给相关负责人”,入口Agent的输出应该是这样的:
{ "intent": "sales_report", "tasks": [ {"id": "t1", "type": "data_query", "params": {"region": "华东", "period": "上季度", "metric": "销售数据"}}, {"id": "t2", "type": "data_analysis", "params": {"analysis_type": "同比", "depends_on": ["t1"]}}, {"id": "t3", "type": "document_generation", "params": {"format": "ppt", "depends_on": ["t2"]}}, {"id": "t4", "type": "notification", "params": {"channel": "email", "recipient": "相关负责人", "depends_on": ["t2"]}} ] }入口Agent的提示词要尽可能聚焦在意图识别和任务拆解上,不要让它去执行任何具体任务。我见过一些实现让入口Agent直接去查数据,结果它查完数据就忘了后面还要生成PPT。
5.2 任务调度:谁来决定执行顺序
任务拆解完成后,调度Agent接手。调度Agent根据任务之间的依赖关系决定执行顺序。上面的例子中,t1没有依赖,可以立即执行;t2依赖t1,必须等t1完成;t3和t4都依赖t2,可以并行执行。
调度Agent还需要处理异常情况。比如t1执行失败了,t2、t3、t4都应该被标记为“阻塞”,而不是继续执行。调度Agent要能识别这种依赖失败,并决定是重试t1还是直接终止整个流程。
注意事项:调度Agent的重试策略要谨慎设计。我通常只对幂等操作做自动重试,比如数据查询。对于非幂等操作,比如发送邮件,重试前必须确认上一次是否真的失败了,否则可能发出两封重复邮件。
5.3 执行阶段:各Agent如何配合
执行阶段是A2A系统最热闹的时候。以t1为例,调度Agent把任务分配给数据查询Agent。数据查询Agent从共享记忆里读取任务参数,调用数据库查询工具,拿到结果后写入共享记忆,然后通知调度Agent“t1完成”。
t2依赖t1,调度Agent收到t1完成的通知后,把t2分配给数据分析Agent。数据分析Agent从共享记忆里读取t1的结果,执行同比分析,把分析结果写入共享记忆。
t3和t4可以并行。调度Agent同时把t3分配给文档生成Agent,把t4分配给通知Agent。文档生成Agent从共享记忆里读取t2的分析结果,生成PPT文件,把文件路径写入共享记忆。通知Agent从共享记忆里读取t2的分析结果,生成邮件内容,调用邮件发送工具。
整个过程中,共享记忆是各Agent之间传递数据的唯一通道。Agent之间不直接通信,所有数据交换都通过共享记忆完成。这样做的好处是解耦彻底,每个Agent只需要关心自己的输入和输出,不需要知道上游是谁、下游是谁。
5.4 结果汇总:谁来生成最终回复
所有任务完成后,汇总Agent负责生成最终回复。汇总Agent从共享记忆里读取所有任务的执行结果,整理成用户可读的格式,返回给用户。
汇总Agent的提示词要强调“简洁”和“重点突出”。用户不需要知道中间过程发生了什么,只需要知道最终结果是什么。比如上面的例子,汇总Agent的回复可能是:“上季度华东区销售数据已完成同比分析,PPT已生成并保存,关键结论已发送至相关负责人邮箱。”
如果中间有任务失败,汇总Agent要如实告知用户,并给出建议。比如“数据查询失败,可能是数据库连接问题,建议稍后重试”。
6. 常见问题与排查技巧实录
6.1 Agent之间消息丢失或重复
现象:调度Agent发出任务后,执行Agent没有收到,或者收到了两次。
排查思路:首先检查消息队列的配置。如果用的是内存队列,进程重启会导致消息丢失。如果用的是持久化队列,检查消费者的确认机制是否正确。重复消息通常是确认机制的问题,比如消费者处理完消息后没有及时确认,导致消息被重新投递。
解决方法:使用持久化消息队列,确保消息不丢失。消费者端实现幂等处理,即使收到重复消息也不会重复执行。给每条消息加唯一ID,消费者记录已处理的消息ID,遇到重复ID直接跳过。
6.2 共享记忆读写冲突
现象:两个Agent同时读写共享记忆,导致数据不一致。
排查思路:检查共享记忆的读写是否加了锁。如果没有加锁,或者锁的粒度不对,就会出现冲突。另外检查版本控制机制是否生效,写入时是否检查了版本号。
解决方法:引入记忆管理器统一管理读写,按任务ID加锁,实现乐观锁的版本控制。写入时如果版本号不匹配,拒绝写入并通知Agent重新读取。
6.3 Agent陷入无限循环
现象:两个Agent互相发消息,谁也不肯结束。
排查思路:检查Agent的终止条件是否明确。如果Agent的提示词里没有明确的“任务完成”判断逻辑,它可能会一直等待下游的反馈。另外检查消息的跳数限制,如果没有限制,两个Agent可能无限循环。
解决方法:给每个任务设置最大跳数限制,比如10跳。超过限制后强制终止,并记录错误日志。Agent的提示词里要明确“当你完成XX操作后,任务即告完成,不需要等待进一步指令”。
6.4 上下文膨胀导致响应变慢
现象:随着任务执行,Agent的响应时间越来越长。
排查思路:检查Agent的上下文窗口是否被塞满了。如果每次工具调用的结果都完整保留在上下文里,上下文会迅速膨胀。
解决方法:实现上下文压缩机制,当上下文接近预算时触发摘要。工具调用的结果只保留关键字段,不要保留完整响应。历史对话压缩成简短摘要,只保留关键决策点和结论。
6.5 某个Agent频繁失败
现象:某个Agent的执行成功率明显低于其他Agent。
排查思路:先看这个Agent的提示词是否过于复杂。提示词越长,LLM越容易出错。再看这个Agent挂载的工具是否稳定,工具的超时设置是否合理。最后看这个Agent的输入是否规范,上游Agent是否传入了不符合Schema的数据。
解决方法:简化提示词,只保留必要的指令。给工具调用设置合理的超时和重试策略。在Agent入口处加数据校验,拒绝不符合Schema的输入。
| 问题类型 | 典型现象 | 排查方向 | 解决手段 |
|---|---|---|---|
| 消息丢失 | 任务卡住不执行 | 消息队列配置 | 持久化队列+幂等消费 |
| 读写冲突 | 数据不一致 | 锁机制和版本控制 | 记忆管理器+乐观锁 |
| 无限循环 | 任务永不结束 | 终止条件和跳数限制 | 最大跳数+明确终止条件 |
| 上下文膨胀 | 响应变慢 | 上下文窗口大小 | 摘要压缩+精简工具结果 |
| Agent频繁失败 | 成功率低 | 提示词和工具稳定性 | 简化提示词+超时重试 |
6.6 独家避坑技巧
技巧一:给每个Agent起一个有意义的名字。不要用agent1、agent2这种命名,用data_query_agent、analysis_agent这种描述性名字。出问题时看日志能快速定位是哪个环节。
技巧二:在共享记忆里记录每个任务的执行耗时。这样你能快速发现哪个环节是瓶颈。我通常会在任务完成后记录开始时间、结束时间、耗时,汇总时按耗时排序,一眼就能看出哪个Agent最慢。
技巧三:给Agent的提示词加一个“自我检查”步骤。让Agent在输出结果前先检查一遍:参数是否完整、格式是否符合要求、是否有明显错误。这个简单的步骤能减少很多低级错误。
技巧四:不要迷信“全自动”。有些关键决策点,比如发送邮件、修改数据,最好加一个人工确认环节。我见过一个项目因为Agent误判,把内部测试数据发给了客户,造成了很尴尬的局面。
技巧五:定期做“混沌测试”。故意让某个Agent失败、让消息延迟、让共享记忆不可用,观察系统的表现。这样能在真实故障发生前发现薄弱环节。
7. 我个人的一些实践体会
跑过几个A2A项目之后,我最大的体会是:多Agent系统的复杂度不是线性增长的,而是指数增长的。两个Agent协作的复杂度可能是单Agent的三倍,三个Agent可能是九倍。每增加一个Agent,你就要考虑它和其他所有Agent的交互、它和共享记忆的交互、它在异常情况下的行为。
所以我的建议是:能用单Agent解决的就不要用多Agent,能用两个Agent解决的就不要用三个。多Agent的价值在于分工带来的专业化和可维护性,但如果任务本身不复杂,强行拆分反而得不偿失。
另一个体会是:记忆管理比Agent本身更重要。我见过太多项目把精力花在Agent的提示词调优上,结果共享记忆一团糟,Agent之间传的数据格式不统一、版本不一致、过期数据没清理。最后Agent再聪明也跑不起来。把记忆管理做好,Agent的提示词反而可以写得很简单。
最后分享一个小技巧:在开发阶段给每个Agent加一个“调试模式”。开启调试模式时,Agent会把完整的输入、输出、中间推理过程都写到日志里。这样出问题时你能看到Agent到底“想”了什么,而不是只看到一个失败的结果。这个技巧帮我省了很多排查时间。
这个方向后续还可以往几个方向扩展:一是引入更细粒度的权限控制,不同Agent只能访问自己权限范围内的记忆;二是做Agent的性能画像,根据历史执行数据动态调整任务分配;三是探索跨系统的A2A协作,让不同团队开发的Agent能够互相发现和调用。这些方向我还在摸索中,有新的进展再和大家分享。