双Agent架构实战:上下文隔离与工具调用的最佳实践

📅 发布时间:2026/10/12 13:18:05
双Agent架构实战:上下文隔离与工具调用的最佳实践
1. 从单个大脑到双大脑这次要解决的是上下文污染不是模型不够聪明个人AI助手做到这个阶段前几个版本我一直坚持把所有能力塞进同一个Agent。对话理解、意图解析、工具调用、结果润色全都靠一套上下文和一套System Prompt撑起来。早期跑简单任务的时候没觉得有什么问题但随着接的工具越来越多我发现真正的瓶颈开始浮出水面不是底层模型不够聪明而是单个上下文里同时塞了太多互不相关的信息模型处理起来越来越糊。一个很典型的场景是用户让我查天气然后再把结果写成一段朋友圈文案。插件的返回结果是一大段结构化的天气数据里面还有风力、湿度、紫外线指数这些字段。这些原样堆在上下文里等到模型要写文案的时候注意力很容易被那几十行技术数据带走写出来的东西生硬又别扭。更别提有些工具返回的是很长的JSON调用一次就占掉大半段上下文后面再叠加多轮对话整个上下文窗口很快就变成一个垃圾桶——早期信息被挤出窗口之外系统提示词的约束力也在逐渐衰减。我观察到的现象是任务链一旦超过五步模型开始频繁犯低级错误比如把时间单位弄混、把两个不同用户的诉求拼接成一条指令。与其继续在一个Agent里打补丁不如直接把架构拆开。这个阶段我决定将系统升级为双Agent一个负责理解用户和表达结果另一个负责调度和执行工具。这其实是个很经典的分层思路。人类团队里也是这么干的——有人专门面对客户有人专门负责干活中间通过一张工单来传递需求而不是让客户直接冲进机房盯着工程师操作。对个人AI助手来说这个拆分的核心价值不是两个模型比一个聪明而是让每套上下文各自保持纯净。面对用户的Agent只需要关心用户说了什么、需要什么、结果怎么表达面对工具的Agent只需要关心工具调用的参数怎么填、返回的数据怎么解析、异常怎么处理。两边的System Prompt都能写得很专注不会被另一半职责干扰。要说清楚这套架构可以先记住两个角色的代号主控Agent面对用户的那一侧和执行Agent面对工具的那一侧。接下来的内容全部围绕这两个角色的分工、它们之间的通信协议、上下文隔离策略以及我在实测中踩到的坑展开。2. 角色拆解主控Agent和执行Agent的职责边界怎么划2.1 主控Agent只做三件事主控Agent的核心职责可以压缩成三个动作听懂需求、拆任务、把结果说人话。听懂需求不仅仅是理解字面意思。用户说帮我看看明天适合不适合出门跑步背后其实带着多重意图需要查天气、需要判断是否适合运动这涉及温度、降水、风力等多个维度的综合判断、可能还隐含了如果适合的话帮我定个闹钟这种后续诉求。主控Agent要做的是把这句话解析成一条明确的内部指令决定哪些是必须做的、哪些是可选的。拆任务是主控Agent最关键的产出。它要根据用户意图决定是不是需要调用工具、调用哪些工具、参数怎么填充。还是上面那个例子主控Agent要拆出查询某地某日天气这个子任务并明确约束条件需要精确到小时级的数据需要重点关注降水概率和风力。拆完任务之后主控Agent把工单发给执行Agent自己就退到后台等待结果。把结果说人话则是收尾工作。执行Agent返回的天气数据是一堆结构化字段主控Agent要根据用户最初的问题把这些字段组织成一段自然流畅的回答——明天下午三点到五点适合跑步温度18度微风降水概率10%。这个环节需要结合上下文里的对话历史因为用户可能之前提到过I prefer morning runs那么主控Agent应该主动调整建议时段。2.2 执行Agent只处理工具和数据结构执行Agent的职责更底层它接收主控Agent下发的工单把工单翻译成具体的工具调用序列拿到返回结果之后做基本的格式校验和数据清洗再把结果结构化地回传给主控Agent。执行Agent不需要理解用户的语气、不需要揣摩深层意图、不需要考虑回答的礼貌程度。它看着一份工单上面写着查询天气地点某市日期明天需要全天逐小时数据然后就去找对应的天气工具把参数填好发请求拿到响应解析出需要的字段打包返回。有人可能会问那执行Agent不就是个带工具调用的普通模型吗跟原来单Agent的时候有什么区别区别在于它不再需要应付用户对话这部分工作。我实测下来把对话能力剥离出去之后执行Agent的工具调用成功率有明显提升。原因是多方面的一是System Prompt可以写得极其简洁没有任何对话策略类指令干扰二是它的上下文里不会混入用户语气词和历史闲聊工具返回的大段JSON也不会被用来做语言润色上下文占用和语义干扰都小很多。2.3 一句话划分原则角色划分讲完了我给一个最实用的判断标准如果某件事可以从用户原话里直接找到答案那是主控Agent的活如果某件事必须从工具返回值里提取答案那是执行Agent的活。举个例子帮我定个明早八点的闹钟。时间是明早八点用户原话直接给出了主控Agent解析出来填进工单就行至于闹钟工具需要的闹钟ID、设备标识、时区换算这些由执行Agent去处理。查一下我上个月在生鲜平台花了多少钱——时间是上个月来源是生鲜平台金额要等工具返回任务拆解在主控侧数据提取在执行侧。这个原则看似简单但非常管用。我早期设计的时候曾经想把从工具返回结果里提炼答案也交给主控Agent导致执行Agent只负责抛出一段原始JSON每次主控Agent都要跟这些JSON搏斗。后来把数据清洗的职责还给了执行Agent主控Agent拿到手的都是干净的结构化结果两边都轻松很多。3. 消息协议与任务令牌双Agent之间定下的最小约定3.1 消息结构类型、任务ID和状态码两个Agent之间通信最忌讳的是没有协议地随便传一段文本。我在阶段4的时候就吃过这个亏当时主控Agent直接以自然语言的方式告诉执行Agent你帮我查一下天气执行Agent理解出来的结果五花八门。所以这一版我从一开始就定了一套极简的JSON消息协议核心字段就五六个message_id全局唯一的消息编号用于追踪和去重message_type消息类型主要用TASK_ASSIGN、TASK_RESULT、STATUS_UPDATE这三种task_id任务编号按时间戳加序号生成sender / recipient发送方和接收方标识payload具体内容随消息类型不同而不同timestamp时间戳用于超时判断和日志追踪下面是一个实际操作中的消息示例任务ID和字段我都做了脱敏处理{ message_id: msg_20260119_00017, message_type: TASK_ASSIGN, sender: coordinator, recipient: executor, task_id: task_20260119_00017, payload: { operation: query_weather, params: { location: 当前城市, date: 明天, granularity: hourly, focus_fields: [precip_probability, wind, temp] }, expected_output: [weather_summary, suitable_for_running] }, timestamp: 2026-01-19T15:02:1108:00 }payload里我固定使用operation、params和expected_output三个子字段。operation告诉执行Agent要做什么params是参数列表expected_output则是主控Agent期望拿到的输出结构样例。这个字段特别有用它等于告诉执行Agent我不关心工具原始的返回格式我只想拿到这些关键信息。3.2 同步还是异步简单场景不搞复杂机制双Agent通信最直接的实现方式就是同步调用主控Agent发出任务阻塞等待执行Agent返回结果然后继续下一步。这种方式简单可靠适合大部分个人助手场景我目前也是这么跑的。不过有一个细节要做对超时处理。执行Agent偶尔会卡住——可能是底层模型响应慢也可能是工具接口超时。我在协议里约定执行Agent必须在收到任务后处理完毕并通过TASK_RESULT消息返回或者如果处理时间会超过30秒必须先发一条STATUS_UPDATE消息说明进度。主控Agent侧设了一个60秒的硬超时超过之后主动发一条CANCEL消息然后把这轮任务标记为失败回给用户这个操作耗时太长我已中断你可以稍后再试。异步机制我暂时没有用因为个人助手场景里用户通常期望对话得到响应同步调用配合超时已经够用。如果是做批量任务或者后台任务队列才需要考虑异步回调那套东西目前阶段不必过度设计。3.3 任务令牌的意义防止两个Agent互相甩锅我在消息协议里另外引入了一个很小但很关键的设计——task_id的重用规则。同一轮对话里主控Agent可能会给执行Agent下发多个子任务每个子任务都有独立的任务ID。这就要求执行Agent在返回TASK_RESULT的时候必须把对应任务的task_id原样带回。否则就会出现两个任务都在执行中返回结果的时候主控Agent分不清哪个结果是哪个任务的。这个设计还能防止一种很隐蔽的问题主控Agent重发指令。假设执行Agent第一次调用工具超时了但工具其实已经在服务端执行成功并写入了数据比如闹钟已经创建成功。主控Agent自动重试时如果还是带着同一个task_id执行Agent就能根据任务ID查重发现这个任务已经执行过了就会返回重复任务已忽略而不是真的再创建一遍闹钟。这个幂等机制听起来高大上实现起来其实很简单核心就是执行Agent侧维护一个最近执行过的任务ID列表查重一下就好。4. 上下文隔离与信息回填什么该共享什么该隔离4.1 全局上下文主控Agent自己的记忆库双Agent架构里最容易踩的坑就是上下文管理过于激进——要么全共享要么全隔离。我试过全共享让执行Agent也能看到完整的历史对话结果执行Agent在处理工具参数的时候会莫名其妙地被历史对话干扰。比如它看到用户之前说了一句最近天气好干后面在给天气工具填参数时把湿度字段的期望值给填错了。所以我最后定为全局上下文只属于主控Agent。它维护着完整的对话历史、用户偏好、长期记忆比如用户的常用地址、常用时间段、之前对类似任务的处理偏好。执行Agent看不到这些历史对话。它每次执行任务时只拿到主控Agent从全局上下文中提炼出来的一份任务摘要。这个设计理念可以用一个比喻来解释主控Agent像一位经验丰富的项目经理执行Agent像一位专注的工程师。项目经理知道客户的所有背景和喜好但工程师只需要拿到图纸和规格说明不需要听客户闲聊。4.2 任务摘要的提炼规则给执行Agent刚好够用的信息刚好够用四个字执行起来比听起来难得多。给少了执行Agent缺参数得反复回来问给多了执行Agent又被多余信息干扰。我建议遵循两个提炼原则。第一只包含当前任务相关的信息。比如用户说按老规矩帮我订下周二的会议室主控Agent需要从长期记忆里查出老规矩是什么某会议室、下午两点到四点、需要投影把这三个信息放进任务摘要而不是把用户之前订会议室的全部聊天记录都甩给执行Agent。第二明确标注约束和偏好。主控Agent知道的信息里有一部分是硬约束必须要投影有一部分是软偏好用户上次订的是朝南的房间但如果没有也行。硬约束和软偏好分开写执行Agent才知道哪些条件下可以灵活处理。我现在的实现里任务摘要是一个纯文本的结构块大概长这样[任务背景] 用户是项目组核心成员需要预订会议室进行项目评审。 [硬性要求] - 日期下周二1月27日 - 时间14:00 - 16:00 - 人数约10人 - 需要投影设备 [软性偏好] - 用户偏好朝阳房间 - 如果有空余尽量安排不强制这份摘要会被压缩进TASK_ASSIGN消息的payload里和operation、params一起传给执行Agent。执行Agent拿到之后自己规划工具调用步骤。4.3 信息回填与压缩执行结果不直接进全局上下文执行Agent返回的TASK_RESULT主控Agent拿来直接用吗我建议不要直接丢进全局上下文。因为工具返回的原始结果经常包含大量不需要的信息比如查询天气时返回的逐小时40行数据实际上用户只关心下午那一段。我的做法是在主控Agent侧加一步结果摘要化从TASK_RESULT中提取出回答用户问题所需的关键事实然后再写入全局上下文。这样全局上下文里存的是明天下午14-16点适合跑步温度18度降水概率10%微风而不是那一大段原始JSON。全局上下文的体积增长被控制在一个很小的量级对话轮次多了也不会很快撑爆窗口。这一步操作有一个非常重要的副作用全局上下文保持干净主控Agent回答问题的风格和准确性就有了保障。它不需要从几十行字段里找关键信息信息已经被压缩成可以直接使用的句子后续多轮对话的连贯性也更好。我在实际测试中发现上下文隔离和摘要化带来的收益非常大。之前单Agent跑10轮对话时工具返回的原始数据早把上下文污染得不成样子改成双Agent之后同样的场景跑了30轮上下文的混乱程度还远低于之前的水平。4.4 异常情况的信息流通失败原因必须完整传递上面说一切信息都要压缩但有一个例外执行失败时失败原因不要压缩。执行Agent在调用工具时可能遇到各种奇怪的错误比如参数校验不通过、服务端返回错误码、网络超时、数据格式不合法。这些错误详情在主控Agent侧很难靠猜还原所以必须原样传递只是最后呈现给用户的时候才做一层过滤。我在实践中的做法是TASK_RESULT的payload里固定有一个error_detail字段执行Agent把原始错误信息填进去主控Agent收到后先记录异常日志再判断这个错误是否可以自己修复比如发现参数填错可以重新下发一次修正后的任务最后才决定怎么跟用户说。5. 实测中的意外状况循环调用、上下文膨胀与状态竞争5.1 问题一主控Agent反复下发同一个失败任务第一次跑通双Agent的时候我遇到了一个非常尴尬的场景执行Agent调用会议室预订工具时因为鉴权过期失败了返回了TASK_RESULT并标注了错误。主控Agent收到错误信息之后因为它的System Prompt里有如果任务失败应尝试重新执行以确保用户体验这句话它居然原封不动地重新下发了一次任务然后又失败又重试……整整循环了六轮才被我手动终止。这个问题的根源在于主控Agent的自我修复逻辑没有和任务执行模块做联动。修复应该基于对错误类型的判断而不是无脑重试。我后来做了两项调整一是主控Agent的System Prompt里明确写着同一任务在收到失败结果后最多自动重试一次再次失败则直接向用户说明情况并提供替代方案二是在任务协议里加入重试次数统计字段执行Agent每次重试都会递增计数达到上限后自动拒绝接收。另外我还要提一个思路层面的修正不要让Agent在提示词里做太复杂的策略判断规则越显式越好。温和地引导模型你应该考虑用户的体验远不如失败后自动重试一次再次失败则停止并寻求用户指令这种带明确边界的指令来得可控。5.2 问题二全局上下文还是膨胀了——摘要式记忆的缺陷一开始我做了上下文摘要化以为全局上下文就能一直保持苗条。跑了段时间后发现一个盲区摘要本身也是会累积的。每轮对话结束后主控Agent都会把这一轮的关键信息浓缩成一小段摘要写回上下文10轮之后10段摘要加起来已经占了很大的窗口空间而且有些摘要之间还有信息冗余和冲突。这个问题在单Agent时代不太明显因为上下文里的原始对话本身就很长有没有摘要影响不大。但双Agent架构里全局上下文被刻意压缩得很干净摘要累积的膨胀效应反而凸显出来。最终我的解决方案是做一个两级记忆结构短期记忆区只保留最近两轮的完整摘要更早的摘要会被归档到长期记忆区不再常驻上下文窗口。只有当新任务涉及历史信息时主控Agent才按需从长期记忆区检索一段相关摘要放回短期区。这个机制原理不复杂但确实帮我把全局上下文的体积稳定在一个相对恒定的水平。5.3 问题三状态竞争——两个Agent同时写同一个配置文件这个问题是我在给双Agent加配置文件热更新功能时撞上的。我最初的设计是主控Agent和执行Agent都可以修改一个共享的配置文件主控写用户偏好执行写工具状态。结果某天执行Agent在更新一个工具缓存的同时主控Agent正好在写用户偏好两个写入互相覆盖导致配置里出现了已损坏的JSON结构。事件发生后我复盘了两点教训。第一共享可写状态是双Agent系统的毒药能不共享就不共享。我现在把配置拆成了两个独立文件主控Agent写user_preferences.conf执行Agent写tool_state.json两边互不干扰。第二涉及到交叉信息的同步一律通过消息传递不通过直接改对方的文件来实现。比如执行Agent想提醒主控Agent某个工具已经切换到了测试环境它就发一条带状态变更信息的STATUS_UPDATE消息过去主控Agent收到后在合适的时候自主更新自己的认知而不是让执行Agent直接改主控Agent的配置。5.4 问题四双Agent底层模型同源导致的同质化最后这一个问题比较隐蔽我也是偶然发现的。我的主控Agent和执行Agent底层用的是同一个大模型接口只是System Prompt不同。在很长一段时间里我都没发现问题直到有一天我让执行Agent独立处理一个非常复杂的工具调用序列时发现它对参数的解析习惯、对错误信息的措辞、甚至对JSON格式的偏好都跟主控Agent出奇地一致。背后的逻辑是同一个大模型在相似的输出习惯上很容易趋同两个Agent的思维模式实质上还是同一个只是被两套提示词临时地装扮成不同角色。如果想让两个Agent真正发挥出差异化的能力更好的做法是让它们使用不同的底层模型——哪怕一个用轻量快速型、一个用复杂推理型效果都要比两个同源模型好。我目前的情况是手头可用的模型接口有限暂时只能继续用同一套底层模型但在实践里我会刻意地在两边的System Prompt里强调不同的表达习惯和思考方式尽量拉开它们的“性格”差异。这个问题的启发是双Agent架构如果只是复制同一个模型的两次调用增益很有限。架构的价值要用差异化来兑现——不同的模型能力、不同的上下文视野、不同的任务角色三个维度至少要有两个是明显不一样的这个双Agent才值得拆。6. 一个小技巧与下一步规划如果让我只讲一个对双Agent系统最有用的调优技巧那就是给每个Agent的System Prompt里写上一段你的边界。主控Agent的System Prompt里明确写着你不直接调用任何工具所有工具操作通过TASK_ASSIGN消息发给执行Agent”执行Agent的System Prompt里明确写着你不回答用户的任何非任务问题你只处理收到的任务数据。这段边界宣言虽然只有两行字但对减少两个Agent的角色漂移非常有效比把职责描述写成一大段文档管用得多。下一步我准备在这套双Agent架构上尝试三项扩展一是给执行Agent增加子Agent调度能力把多工具协作的大任务再分解成可并行的执行单元二是把当前的非持久化消息日志升级为可查询的事件溯源存储方便事后排查和复盘三是针对执行Agent做独立的评测集每次调整System Prompt之后能快速回归测试工具调用的成功率。这套双Agent架构是我个人AI助手项目里改动最大的一次也是收益最明显的一次。从实际使用体验来说用户侧感受到的是回答更稳了、连续多轮对话不容易跑偏我自己感受到的是调试成本降下来了——出问题的时候能一眼看出是理解环节的锅还是工具环节的锅不用再像以前那样在一口大锅里捞针。如果你也在搭个人AI助手并且发现单Agent开始频繁出错不妨试试这个拆成两个大脑的思路注意本文里提到的角色边界、消息协议、上下文隔离这几个关键设计点应该可以少走不少弯路。