☰
双Agent架构实战:上下文隔离与工具调用的最佳实践
2026/10/12 13:17:59 网站建设 项目流程

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:11+08: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开始频繁出错,不妨试试这个"拆成两个大脑"的思路,注意本文里提到的角色边界、消息协议、上下文隔离这几个关键设计点,应该可以少走不少弯路。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询