1. 多智能体上下文工程的核心命题
1.1 从单兵作战到协同群集:为什么提示词不够用了
很多人第一次接触多智能体系统,脑子里想的还是“给每个Agent写一段好提示词,然后让它们各干各的”。这个思路在单Agent场景下勉强够用,一旦Agent数量超过三个、任务链路超过五步,立刻崩盘。我踩过最典型的一个坑:三个Agent分别负责检索、推理、汇总,每个Agent的提示词都打磨得很精细,单独测试效果都不错,但串起来跑的时候,检索Agent返回的内容推理Agent根本接不住,汇总Agent拿到的中间结果格式对不上,整个链路像一条到处漏水的管子。
问题出在哪里?出在我们把“提示词”当成了系统的全部输入。实际上,一个Agent在运行时接收到的信息远不止提示词——它还包括上游Agent的输出、共享内存里的历史记录、工具调用的返回值、当前任务的全局状态、以及系统级的路由规则。这些东西加起来,才是这个Agent真正的“上下文”。提示词只是上下文的一个子集,而且往往是最静态的那个子集。
这就是上下文工程要解决的核心问题:把Agent在决策时刻真正需要看到的信息,以正确的格式、在正确的时机、送到正确的位置。它比提示词工程高一个维度,因为提示词工程关心的是“怎么把话说清楚”,上下文工程关心的是“该让谁在什么时候看到什么”。
1.2 上下文工程与RAG的本质区别
有人会问:这不就是RAG吗?把知识检索出来塞进提示词里,不就是上下文工程?这个理解只对了一半。RAG解决的是“外部知识如何进入模型”的问题,它的核心是检索增强。上下文工程解决的是“整个多智能体系统在运行过程中,信息如何流转和呈现”的问题,它的核心是信息架构。
打个比方:RAG像是一个图书馆管理员,你问它要一本书,它帮你找出来递给你。上下文工程像是整个图书馆的运营系统——它决定了哪些书放在哪个架子上、新到的书怎么编目、读者在不同区域能拿到什么、馆际互借怎么流转、过期资料怎么清理。RAG是上下文工程的一个子模块,但上下文工程还包括Agent间的消息传递协议、共享状态的版本管理、推理链的可见性设计、以及上下文的压缩与摘要策略。
我在实际项目中做过对比:同一个多智能体任务,只做RAG不做上下文工程,任务完成率大概在60%左右,失败案例中超过七成是因为Agent拿到了错误格式的上下文或者上下文里缺少关键状态信息。加上上下文工程之后,完成率能拉到85%以上,而且失败模式从“莫名其妙跑偏”变成了“可以定位的链路断点”。
1.3 透明架构引擎的设计目标
标题里提到的“透明架构引擎”,核心诉求是三个词:可观测、可干预、可复现。
可观测意味着任何一个Agent在任何一个时刻的决策依据,你都能看到完整的上下文快照。不是只看提示词,而是看它当时接收到的全部信息——上游输出、检索结果、工具返回、全局状态、路由标记。可干预意味着当链路跑偏时,你能在中间某个节点注入修正信息,而不是只能从头重跑。可复现意味着同样的输入和上下文,系统应该给出可预期的输出,而不是每次跑都像开盲盒。
这三个目标决定了架构设计的基本约束:上下文不能是隐式的、散落在各个Agent内部的,必须是显式的、集中管理的、带版本号的。我见过太多团队把上下文藏在各个Agent的私有变量里,调试的时候靠打日志拼凑,效率极低。正确的做法是把上下文当成一等公民,有独立的存储、独立的生命周期、独立的访问接口。
2. 上下文流转的架构拆解
2.1 三层上下文模型:全局、任务、局部
我在多个项目中沉淀下来的做法是把上下文分成三层,每层有不同的生命周期和访问权限。
全局上下文是整个系统共享的,包括系统配置、全局知识库的索引、所有Agent的能力注册表、以及跨任务的长期记忆。这一层变化频率最低,但所有Agent都能读。它的作用是让每个Agent知道“系统里有什么、我能调用谁、整体目标是什么”。
任务上下文是单个任务链路内共享的,包括当前任务的原始输入、任务分解后的子目标列表、已完成步骤的输出摘要、以及任务级的约束条件。这一层随着任务推进不断更新,链路内所有Agent都能读写。它的作用是让每个Agent知道“这个任务现在进行到哪了、前面的人做了什么、我还需要产出什么”。
局部上下文是单个Agent私有的,包括它的角色提示词、它自己的推理草稿、它调用工具时的中间参数。这一层只有该Agent自己能访问,其他Agent看不到。它的作用是保留Agent的个体性和推理空间,避免所有信息都变成公共的导致上下文爆炸。
这三层的划分不是拍脑袋定的,而是基于一个实际约束:如果所有信息都全局共享,上下文长度会迅速超过模型窗口,而且Agent会被无关信息干扰;如果所有信息都局部私有,Agent之间就无法协同,链路会断。三层模型是在共享和隔离之间找的平衡点。
2.2 上下文对象的标准化结构
要让上下文在Agent之间可靠流转,必须有一个标准化的数据结构。我用的方案是一个JSON Schema定义的ContextObject,核心字段包括:
{ "context_id": "ctx_20250101_001", "version": 3, "scope": "task", "task_id": "task_abc", "created_at": "2025-01-01T10:00:00Z", "updated_at": "2025-01-01T10:05:00Z", "payload": { "task_input": "...", "sub_goals": ["...", "..."], "completed_steps": [ {"agent": "retriever", "output": "...", "confidence": 0.85} ], "constraints": ["...", "..."], "shared_memory_refs": ["mem_001", "mem_002"] }, "metadata": { "producer": "orchestrator", "consumers": ["reasoner", "summarizer"], "ttl": 3600 } }这个结构的关键设计点有几个。version字段用于乐观锁控制,防止两个Agent同时写同一个上下文导致覆盖。scope字段标记这个上下文属于哪一层,决定它的可见范围。payload里的completed_steps是链路追踪的核心,每个步骤的输出和置信度都记录在案,后续Agent可以据此判断上游结果的可靠性。metadata里的consumers字段让系统知道这个上下文该推给谁,而不是让每个Agent自己去拉。
注意:payload不要塞太大的原始数据,比如完整的检索文档。大块数据应该存在外部存储里,payload里只放引用ID和摘要。我见过有人把整篇PDF的文本塞进payload,结果上下文对象膨胀到几十KB,每次传递都拖慢链路。
2.3 上下文版本管理与冲突消解
多智能体系统里最头疼的问题之一就是并发写冲突。两个Agent同时基于版本3的上下文做决策,各自产出了新版本,谁覆盖谁?我的做法是引入版本分支与合并机制。
每个Agent在读取上下文时拿到的是某个版本的快照。当它要写回时,系统检查当前版本是否还是它读取时的版本。如果是,直接写入新版本;如果不是,说明中间有其他人写过,这时候触发合并逻辑。合并逻辑根据字段的归属来决定:如果两个Agent改的是不同字段,自动合并;如果改的是同一字段,标记冲突,交给仲裁Agent或者人工介入。
这套机制听起来复杂,但实现起来并不难,核心就是一个版本号加一个字段级的diff。实际跑下来,冲突发生的频率并不高,因为大多数情况下Agent的写操作是串行的。但一旦发生冲突,如果没有这套机制,就会出现“后写的覆盖先写的”这种静默数据丢失,排查起来非常痛苦。
2.4 上下文压缩与摘要策略
上下文长度是稀缺资源。当任务链路很长时,completed_steps会越积越多,如果不做压缩,很快就会撑爆模型窗口。我的策略是分级压缩。
最近三步的输出保留完整内容,因为后续Agent最可能需要这些细节。第四步到第十步的输出压缩成摘要,保留关键结论和置信度,丢掉中间推理过程。十步以上的输出只保留一行元数据,比如“步骤12:检索Agent完成了对X的查询,返回3条结果,置信度0.8”。如果后续Agent需要某一步的完整内容,可以通过引用ID去外部存储里拉取。
压缩的触发时机也很关键。我试过两种方案:一种是每步完成后立即压缩,另一种是上下文长度超过阈值时批量压缩。实测下来,阈值触发效果更好,因为立即压缩会导致频繁的摘要生成调用,增加延迟,而且有些中间步骤可能马上就会被用到,压了又得解压。阈值我一般设在模型窗口的60%,留40%的余量给当前步骤的推理和输出。
3. 推理透明化的实现路径
3.1 推理链的显式记录与回放
“透明架构”的核心诉求之一是推理过程可回放。这意味着每个Agent的推理不能是一个黑盒,必须把关键决策点记录下来。我的做法是在Agent的输出里强制包含一个推理轨迹字段,记录它考虑了哪些选项、排除了哪些、最终选择的原因是什么。
{ "agent": "reasoner", "output": "最终答案...", "reasoning_trace": [ {"step": 1, "action": "retrieve", "query": "...", "result_count": 5}, {"step": 2, "action": "filter", "criteria": "...", "kept": 3}, {"step": 3, "action": "synthesize", "sources": ["doc_1", "doc_3"], "conclusion": "..."} ], "confidence": 0.82 }这个轨迹不是给模型看的,是给人看的。当链路跑偏时,你可以直接看某个Agent的推理轨迹,定位它是在哪一步走错的。是检索查询写错了?还是过滤条件太严?还是综合的时候漏掉了关键来源?没有这个轨迹,你只能看到最终输出不对,但不知道为什么不对。
回放功能则是把整个任务链路的所有上下文快照和推理轨迹按时间顺序串起来,形成一个完整的执行历史。我一般会提供一个简单的Web界面,左边是时间轴,右边是每个节点的上下文和推理详情,点击任意节点可以展开看完整内容。这个工具在调试复杂链路时能省掉大量时间。
3.2 Agent间消息传递的协议设计
多智能体系统里,Agent之间的消息传递是最容易出问题的地方。我见过太多项目在这里翻车:消息格式不统一、字段缺失、编码不一致、异步调用没有超时处理。我的做法是定义一个消息信封协议,所有Agent间的通信都必须走这个信封。
信封的核心字段包括:发送者、接收者、消息类型、优先级、关联的上下文ID、超时时间、重试策略。消息体则根据类型不同有不同的Schema。这样做的好处是,消息的路由、重试、超时、日志都可以在信封层面统一处理,Agent本身不需要关心这些基础设施。
实操心得:消息的优先级字段非常有用。当系统负载高的时候,低优先级的消息可以排队等待,高优先级的消息优先处理。比如“任务完成通知”是高优先级,“日志上报”是低优先级。没有优先级区分的话,日志上报可能把任务通知挤掉,导致链路卡住。
3.3 工具调用与外部知识的注入时机
Agent调用工具(比如搜索引擎、数据库查询、代码执行)时,返回结果如何注入上下文,是一个容易被忽视但影响很大的问题。注入太早,上下文里塞了一堆还没用到的数据,浪费窗口;注入太晚,Agent做决策时看不到工具结果,等于白调。
我的策略是按需注入加预取。Agent在决定调用工具之前,先声明它需要什么类型的信息。系统根据这个声明,提前把可能相关的数据预取到上下文的“待用区”。Agent真正需要时,从待用区直接读取,不需要等待。如果预取的数据没用上,在步骤结束后清理掉。
这个策略的关键是预取的准确率。预取太多,浪费资源;预取太少,还是要等。我一般会根据历史执行数据来优化预取规则,比如某个Agent在80%的情况下调用工具后都需要某类数据,那就默认预取这类数据。
3.4 上下文可见性控制与安全边界
不是所有Agent都应该看到所有上下文。检索Agent不需要看到汇总Agent的推理草稿,汇总Agent也不需要看到检索Agent的原始查询日志。过度共享不仅浪费窗口,还可能引入干扰甚至安全问题。
我在上下文对象里加了可见性标签,每个字段可以标记为public、task-private、agent-private。系统在把上下文推给某个Agent时,根据标签过滤掉它不该看到的内容。这个机制在调试时也可以临时关闭,让所有Agent看到全部上下文,方便定位问题。
安全边界方面,涉及敏感数据的上下文字段要加密存储,只有特定角色的Agent才能解密读取。这个在多租户场景下尤其重要,不同租户的任务上下文必须严格隔离。
4. 实操落地:从零搭建上下文引擎
4.1 技术选型与依赖清单
搭建这套引擎不需要特别重的技术栈。我的参考选型是:
| 组件 | 选型 | 理由 |
|---|---|---|
| 上下文存储 | Redis + PostgreSQL | Redis做热上下文的快速读写,PostgreSQL做冷上下文和历史归档 |
| 消息队列 | RabbitMQ或Redis Stream | 轻量,支持优先级和延迟队列 |
| 向量检索 | 本地向量库或托管服务 | 根据数据量选择,小规模用本地库足够 |
| 编排框架 | 自研轻量编排器 | 不建议用太重的工作流引擎,多智能体的动态性太强 |
| 可观测 | OpenTelemetry + 自研面板 | 标准协议,方便对接现有监控 |
注意:不要一上来就上Kafka这种重装备。多智能体系统的消息量通常不大,RabbitMQ甚至Redis Stream完全够用。我见过团队为了“架构先进”上Kafka,结果运维成本翻了三倍,收益为零。
4.2 上下文对象的创建与生命周期管理
上下文对象的生命周期从任务创建开始,到任务结束(成功或失败)后保留一段时间用于回放,然后归档。具体流程:
- 任务创建时,编排器生成一个根上下文对象,scope为task,payload里放入任务输入和初始约束。
- 每个Agent执行前,从上下文存储拉取最新版本,根据可见性标签过滤,注入自己的局部上下文。
- Agent执行后,产出输出和推理轨迹,写回任务上下文,版本号加一。
- 编排器检查任务是否完成,未完成则路由到下一个Agent,重复步骤2-3。
- 任务完成后,根上下文标记为closed,触发归档流程。
归档策略我一般设两个阈值:时间阈值(比如7天)和数量阈值(比如保留最近1000个任务)。超过阈值的上下文压缩后存入冷存储,需要回放时再解压。
4.3 编排器的核心逻辑与路由策略
编排器是整个引擎的大脑,它决定下一步该谁执行、传什么上下文、超时怎么处理。核心逻辑是一个状态机加一个路由表。
状态机跟踪任务的整体状态:初始化、执行中、等待中、已完成、已失败。路由表则根据当前状态和上一步的输出,决定下一个Agent是谁。路由策略我常用三种:
- 顺序路由:按预定义的Agent列表依次执行,适合流程固定的任务。
- 条件路由:根据上一步输出的某个字段值决定分支,适合有判断逻辑的任务。
- 动态路由:由一个轻量级的路由Agent根据当前上下文决定下一步,适合探索性任务。
动态路由最灵活但也最难控制,我一般只在任务前期用,后期收敛到条件路由,避免链路无限发散。
4.4 一个完整任务的上下文流转实录
以一个“竞品分析报告生成”任务为例,走一遍完整流程。
任务输入:“分析A、B、C三个竞品在定价策略上的差异,输出对比报告。”
步骤1:编排器创建根上下文,scope=task,payload包含任务输入和约束(输出格式为Markdown表格,字数不超过2000)。
步骤2:路由到检索Agent。检索Agent从上下文读取任务输入,生成三个查询,分别检索A、B、C的定价信息。检索结果写入局部上下文,摘要和引用ID写回任务上下文。版本号从1变为2。
步骤3:路由到分析Agent。分析Agent读取任务上下文里的检索摘要,发现B的定价信息只有一条,置信度低。它在推理轨迹里标记这个不确定性,输出初步分析结果,置信度0.7。版本号变为3。
步骤4:编排器检测到分析Agent的置信度低于阈值0.8,触发补充检索。路由回检索Agent,这次只针对B做深度检索。版本号变为4。
步骤5:路由到分析Agent(第二次)。这次B的信息充足,分析Agent输出完整对比,置信度0.88。版本号变为5。
步骤6:路由到汇总Agent。汇总Agent读取任务上下文里的分析结果,生成Markdown表格,写入最终输出。版本号变为6。
步骤7:编排器标记任务完成,触发归档。
整个过程中,每个Agent看到的上下文都是经过可见性过滤的,检索Agent看不到分析Agent的推理草稿,汇总Agent看不到检索Agent的原始查询日志。但编排器能看到全部,用于路由决策。
5. 常见问题与排查技巧实录
5.1 上下文膨胀导致模型截断
现象:任务跑到一半,Agent输出突然变得很短或者答非所问,日志显示模型输入被截断。
排查:检查上下文对象的payload大小,看completed_steps是否积累过多。我遇到过一次,一个任务跑了20多步,completed_steps里每步都保留了完整输出,上下文膨胀到30KB,模型窗口直接爆了。
解决:启用分级压缩,把早期步骤的输出压缩成摘要。同时检查是否有Agent把大块原始数据写进了payload,如果有,改成只写引用ID。
避坑技巧:在上下文对象里加一个size字段,每次写入时更新。设置一个告警阈值,比如超过10KB就打印警告日志。这样能在膨胀到截断之前就发现问题。
5.2 Agent间消息丢失或重复
现象:链路卡住,某个Agent一直等不到上游消息;或者同一个消息被处理了两次,导致重复执行。
排查:检查消息队列的确认机制。我见过最常见的原因是Agent处理完消息后没有正确发送ack,导致消息被重新投递。另一个原因是消息没有唯一ID,重试时无法去重。
解决:每条消息带一个全局唯一ID,接收方维护一个已处理ID的集合,收到重复ID直接丢弃。ack机制要确保在业务逻辑成功后才发送,而不是收到消息就ack。
5.3 推理轨迹与实际输出不一致
现象:Agent的推理轨迹显示它检索了5条结果,但最终输出里只用了1条,而且没有解释为什么排除其他4条。
排查:这通常是提示词设计问题。Agent被要求输出推理轨迹,但没有被要求轨迹和输出保持一致。模型可能会“编造”一个看起来合理的轨迹,但实际决策过程并不是那样。
解决:在提示词里明确要求推理轨迹必须反映真实决策过程,并且要求对每个排除项给出理由。同时,在系统层面做一致性检查:如果轨迹里提到的来源在输出里没有出现,标记为可疑,触发人工复核。
5.4 上下文版本冲突导致数据覆盖
现象:两个Agent几乎同时写上下文,后写的覆盖了先写的,先写的Agent的输出丢失。
排查:检查上下文写入逻辑是否有版本号检查。如果没有,就是典型的乐观锁缺失。
解决:写入时必须带版本号,存储层做compare-and-swap。版本不匹配时返回冲突错误,Agent重新读取最新版本,合并自己的修改后再写入。合并逻辑按字段级diff处理,不同字段自动合并,同字段冲突则标记待仲裁。
5.5 工具调用超时拖垮整个链路
现象:某个Agent调用外部工具(比如搜索API)超时,整个任务卡住,后续Agent全部等待。
排查:检查工具调用是否有超时设置和降级策略。很多团队只设了超时,但没有降级,超时后Agent不知道该怎么办,只能干等。
解决:每个工具调用必须设超时(我一般设10-30秒,根据工具类型调整),超时后返回一个降级结果(比如“工具不可用,基于已有信息继续”),Agent根据降级结果继续推理,而不是阻塞。同时记录超时事件,用于后续优化。
5.6 常见问题速查表
| 问题 | 可能原因 | 快速排查 | 解决方向 |
|---|---|---|---|
| 模型输出截断 | 上下文膨胀 | 检查payload大小 | 启用分级压缩 |
| 链路卡住 | 消息丢失 | 检查ack机制 | 加消息ID去重 |
| 推理不一致 | 提示词缺陷 | 对比轨迹与输出 | 加一致性检查 |
| 数据覆盖 | 版本冲突 | 检查写入逻辑 | 加乐观锁 |
| 任务超时 | 工具阻塞 | 检查工具超时设置 | 加降级策略 |
| Agent跑偏 | 上下文污染 | 检查可见性过滤 | 收紧可见性标签 |
6. 上下文工程的边界与取舍
6.1 什么时候不该做上下文工程
不是所有多智能体项目都需要这套东西。如果你的系统只有两个Agent、任务链路固定在三步以内、每天调用量不到一百次,那直接写提示词就够了,上上下文工程是过度设计。我见过一个团队做内部工具,总共就两个Agent,非要搭一套完整的上下文引擎,结果开发周期从一周拖到一个月,收益几乎为零。
判断标准很简单:当你的调试时间超过开发时间,或者失败案例无法定位原因时,才需要考虑上下文工程。在那之前,先把提示词写好、把链路跑通。
6.2 上下文工程的成本与收益分析
上下文工程的成本主要在三个方面:开发成本(搭建引擎和工具链)、运行成本(额外的存储和消息传递开销)、维护成本(版本管理、压缩策略调优)。收益则体现在:调试效率提升、任务完成率提升、系统可扩展性提升。
我的经验数据是:当Agent数量超过5个、任务链路超过10步、或者日均任务量超过1000次时,上下文工程的投入产出比开始转正。低于这个规模,收益不明显。高于这个规模,不做上下文工程基本无法维护。
6.3 与RAG、提示词工程的协同关系
这三者不是替代关系,是层次关系。提示词工程在最底层,解决单个Agent的输入表达问题。RAG在中间层,解决外部知识注入问题。上下文工程在最上层,解决整个系统的信息流转问题。
一个常见的误区是有了上下文工程就不需要提示词工程了。恰恰相反,上下文工程把正确的信息送到了Agent面前,但Agent怎么理解这些信息、怎么组织推理,还是靠提示词。我见过上下文引擎做得很完善但提示词写得很烂的项目,Agent拿到了一堆好数据,但输出依然一塌糊涂。
6.4 后续扩展方向
这套引擎目前主要解决的是文本上下文的管理。后续可以扩展的方向包括:多模态上下文的统一管理(图片、音频、视频的引用和摘要)、跨任务的知识沉淀(把成功任务的上下文模式抽象成可复用的模板)、以及自适应压缩策略(根据任务类型自动调整压缩阈值)。
我个人最感兴趣的是跨任务知识沉淀。现在每个任务都是从零开始构建上下文,如果能从历史任务里学习到“这类任务通常需要哪些上下文字段、哪些步骤容易出问题”,就能在新任务开始时预置更合理的上下文结构,减少试错成本。
我在实际项目里最大的体会是:上下文工程的核心不是技术,是纪律。它要求每个Agent的输出都结构化、每个上下文的变更都留痕、每个决策都有据可查。这些纪律在项目初期看起来是负担,但到了中后期,当系统复杂度上来之后,它们就是你能继续维护这个系统的唯一依靠。踩过几次坑之后,我现在宁可前期多花两天把上下文结构设计好,也不愿意后期花两周去排查一个“不知道为什么就错了”的问题。