给 AI Agent 装上"裁判眼"3:上下文之争——多智能体上下文管理的利弊博弈
给 AI Agent 装上"裁判眼"3:
上下文之争——多智能体上下文管理的利弊博弈
MainAgent 持续累积,Sub-Agent 独立隔离,EvalAgent 完全无状态——三种策略各有代价
上一篇:从任务分解到评估反馈的多智能体协作全链路01 一条消息,三种命运
用户说:"把它改成蓝色。"
三个智能体面对同一条用户消息,看到的东西完全不同:
同一条消息的三种"视野"
[MainAgent 看到的]
用户 5 分钟前说 "做一个登录页面"
用户 3 分钟前说 "加上表单验证"
用户刚才说 "把它改成蓝色" ← 知道"它"是按钮,"蓝色"是 #3B82F6
[Sub-Agent 看到的]
System Prompt: 你的任务是修改样式
前置结果: [子任务1生成了 HTML 文件]
用户需求: "把它改成蓝色" ← 能看到前置文件,但不知道用户为什么这么说
[EvalAgent 看到的]
System Prompt: 评估执行质量
执行结果: [修改后的 HTML 文件]
原始需求: "把它改成蓝色" ← 只看最终产物,完全不知道之前发生了什么
这就是多智能体系统中上下文管理的核心矛盾:给得太多会污染,给得太少会丢失关键信息。buddyMe 用了三种截然不同的策略来应对这个问题,每一种都有不得不付出的代价。
本文将深入 MainAgent、Sub-Agent、EvalAgent 三层智能体的上下文机制,逐一拆解每种策略的设计动机、实现细节和利弊权衡。
02 四个角色,四个上下文边界
buddyMe 系统中共有 4 个活跃角色,各自拥有独立的 LLM 客户端和上下文边界:
| 角色 | 类名 | LLM 客户端 | 上下文策略 |
|---|---|---|---|
| 主 Agent | AgentMain | _client | 持续累积,跨轮次保留 |
| 子 Agent 执行引擎 | TaskRunner + TaskPipeline | _client + _sub_client | 每任务独立,消息不继承 |
| 预执行审核器 | PlanReviewer | _sub_client + _review_client | 完全独立,无状态对话 |
| 评估 Agent | TaskEvalAgent | _eval_client | 完全隔离,参数化传入 |
三个 LLM 客户端在 AgentMain.__init__() 中同时创建:
LLM 客户端初始化(agent.py:68-72)
self._client = self._create_client(model_name) # 主模型 self._sub_client = basic_llm.create_client("sub_agent_code_plan") # 子任务辅助 self._eval_client = self._create_client(model_name) # 评估专用(独立)
三个客户端生命周期不同:
_client 贯穿整个会话,_sub_client 用于上下文压缩和计划审核的辅助调用,_eval_client 仅在评估阶段使用。这种分离是上下文隔离的物理基础——不同的"脑子"天然不会串台。
03 上下文生命周期:从用户输入到评估报告
理解利弊之前,先看清上下文在整个系统中是如何流转的:
上下文流转全链路 ──────────────────────────────────────────────────►
[规划阶段]
│
├─ 1. 对话历史 ← MessageHistory(最近 20 条)
├─ 2. 长期摘要 ← memory_summary.md(≤6000 字符)
├─ 3. 用户记忆 ← USER.md(偏好与画像)
│ │
│ ▼
│ 拼接为 conversation_context 文本
│ │
│ ▼
├─ 4. 任务规划 ← _client(主模型),一次性调用
│ │
│ ▼
├─ 5. 预执行审核 ← _sub_client + _review_client(Generator-Evaluator 对审)
│ │
│ ▼
│ 审核通过的任务计划 + 需求文档(冲刺合同)
│
[执行阶段] ← 循环 N 个子任务
│
├─ 6. 构建 System Prompt
│ ├── SUB_AGENT.md(子 Agent 行为规则)
│ ├── 技能元数据
│ ├── 需求文档(冲刺合同)
│ └── 前置子任务结果(从 subtask_results.json 读取)
│
├─ 7. 构建 User Prompt
│ ├── conversation_context(从 MainAgent 传入)
│ ├── 当前子任务描述
│ └── 前置子任务结果(再次注入)
│
├─ 8. 独立执行
│ └── task_messages = [system, user] ← 全新构建,不继承历史
│ └── 工具循环(最多 11 步)
│ └── 结果写入 subtask_results.json
│
│ │
│ ▼
│ 合并后的最终结果
│
[评估阶段]
│
├─ 9. 参数化数据传入
│ ├── 用户原始需求
│ ├── 任务计划
│ ├── 子任务结果
│ ├── 工具调用记录
│ ├── 写入文件列表 + 文件内容
│ └── 真实执行验证结果
│
├─ 10. 独立评估
│ └── messages = [_SYSTEM_PROMPT, eval_prompt] ← 固定 2 条消息
│ └── temperature=0.0
│
└─ 11. 评估报告 → 仅控制台输出,不回传
注意三个关键设计决策:
决策 1 · 对话隔离
子任务间 不共享 LLM 对话历史,每个任务从零构建 messages
决策 2 · 状态共享
SessionState 在 MainAgent / TaskRunner / TaskPipeline 间共享引用
决策 3 · 文件桥接
子任务间通过 subtask_results.json 传递结果,推理过程不传递
04 MainAgent:持续上下文的代价
MainAgent 是整个系统的"大脑",在整个会话生命周期内维护三层记忆:
L1
短期记忆(内存)
MessageHistory — 滑动窗口保留最近 20 条消息(首条固定 + 最新 19 条)每轮
invoke() 自动追加用户输入和助手回复
│
▼
L2
中期记忆(磁盘)
memory_summary.md — 记录近 5 日对话摘要,最大 6000 字符每轮执行前通过
_build_conversation_context() 注入子任务 prompt
│
▼
L3
长期记忆(磁盘)
USER.md + MemoryStore — 用户画像与偏好会话结束时执行记忆衰减(低分记忆归档或删除)
利:为什么需要持续上下文
优势 1 · 意图连续性
多轮对话中用户常说"把它改成蓝色"、"加上刚才说的那个功能"。持续上下文让 MainAgent 能理解指代关系,无需用户重复背景信息。Turn 1: "做一个登录页" → Turn 2: "加上表单验证" → Turn 3: "把它改成蓝色" — "它"指的是按钮
优势 2 · 纠错闭环
用户发现结果不符合预期时,直接说"不对,我说的是 XXX"。MainAgent 能在历史上下文中定位误解并修正,而不是从零开始。
优势 3 · 渐进式构建
复杂项目经常需要多轮迭代——"先生成基础版" → "加上动画" → "修复那个按钮"。持续上下文让每轮基于上一轮结果推进,形成自然的构建链。
优势 4 · 记忆累积
用户偏好(编码风格、常用框架、语言选择)在会话中被逐步学习并持久化到磁盘,跨会话生效。第二个项目的第一条消息就能利用第一个项目的经验。
弊:持续上下文的四个隐患
隐患 1 · 上下文膨胀
随着会话推进,_message_history 不断累积。虽然滑动窗口截断了消息数量,但注入到子任务的 conversation_context 文本会越来越长,消耗 Token 预算。隐患 2 · 信息污染
早期对话中的错误决策、已废弃的需求可能残留在历史中,误导后续执行。尤其是当用户在会话中途改变方向时,旧上下文可能成为噪音。Turn 3: "用 React" → Turn 7: "改成 Vue" → Turn 10 的上下文中仍有 React 相关的讨论
隐患 3 · 幻觉传播
MainAgent 没有在轮次间做上下文重置机制(仅有 reset_episode() 清理执行状态,不清消息历史)。如果某一轮产生幻觉,错误信息可能传播到后续所有轮次。
隐患 4 · 客户端生命周期错位
invoke() 末尾关闭所有 LLM 客户端,但 _message_history 仍在内存中。会话状态(消息)和连接状态(客户端)的生命周期不完全匹配——如果 REPL 循环在客户端关闭后继续调用,可能产生连接错误。
一句话总结:持续上下文是刚需——编码任务天然多轮、迭代、需要指代消解。但代价是 Token 预算随会话线性增长,且缺乏有效的信息衰减和重置机制。
05 Sub-Agent:独立上下文的得与失
每个子任务的执行都是一次全新的 LLM 对话。核心设计原则:消息不继承,状态通过文本注入。
子任务消息构建过程
子任务 #1:
messages = [system_prompt_1, user_prompt_1] ← 全新构建
→ LLM 调用 + 工具循环(最多 11 步)
→ 结果写入 subtask_results.json
子任务 #2:
messages = [system_prompt_2, user_prompt_2] ← 全新构建
→ system_prompt_2 包含子任务 #1 的结果(从 JSON 读取)
→ LLM 调用 + 工具循环
→ 结果追加到 subtask_results.json
子任务 #N:
messages = [system_prompt_N, user_prompt_N] ← 全新构建
→ 包含子任务 #1~N-1 的全部结果
→ 最终结果返回给 MainAgent
三条数据传递通道
| 通道 | 媒介 | 范围 | 可控性 |
|---|---|---|---|
| 通道 1 JSON 文件 | subtask_results.json | 整个任务计划 | 明确读写,可截断 |
| 通道 2 共享状态 | SessionState | 整个任务计划 | 字段级控制 |
| 通道 3 上下文文本 | 字符串拼接 | 每次构建 | 动态截断 |
利:独立上下文带来的稳定性
优势 1 · 上下文干净
每个子任务从零开始,没有历史消息的噪音。LLM 只看到当前任务需要的上下文,不会被之前的工具调用、中间结果、错误信息干扰。代码证据:task_runner.py:227-230 — 每个子任务创建全新 messages 列表
优势 2 · 错误隔离
子任务 #2 的幻觉不会通过消息历史传播到子任务 #3。错误被限定在单个子任务的执行范围内。一个任务崩溃,不会拖垮整条链。
优势 3 · Token 可控
每个子任务的上下文大小通过截断策略(max_subtask_result_len、max_tools_compress_len)显式控制。不会因为前序任务的复杂度而无限膨胀。
优势 4 · 并行潜力
独立上下文意味着无状态依赖的子任务理论上可以并行执行。当前实现是顺序的,但架构上预留了可能性。
优势 5 · 压缩友好
当单个子任务的对话超过 20 条消息时,ContextCompressor 可以安全地压缩——保留前 2 条 + 压缩中段 + 保留最近 5 条。压缩后的摘要仍包含关键信息。
弊:独立上下文的五个盲区
盲区 1 · 推理过程丢失
子任务 A 决定用 CSS Grid 布局(排除 Flexbox),但这个决策过程不会传递给子任务 B。子任务 B 可能选了 Flexbox,导致风格不一致。只传递结果文本,不传递推理链 — SubtasksManager.update() 只记录 result,不含中间推理步骤
盲区 2 · 信息截断损失
上下文传递依赖文本摘要,截断不可避免:result[:max_subtask_result_len] 可能丢失关键细节——长代码输出、复杂错误信息、细微的上下文线索。盲区 3 · 重复理解开销
每个子任务都要重新理解项目结构、文件内容、需求背景。虽然通过 prompt 注入缓解,但 LLM 需要重新"消化"这些信息,增加了推理开销和延迟。
盲区 4 · 全局视图缺失
子任务 #3 看不到子任务 #1 的工具调用过程和中间推理。如果子任务 #1 的结果依赖于某个隐含假设,子任务 #3 可能无法察觉,导致后续输出与前置假设冲突。
盲区 5 · 双重注入冗余
read_completed() 的结果同时注入到 system prompt 和 user prompt(task_pipeline.py:354 和 :392),对同一信息的双重注入浪费了 Token 预算。
一句话总结:独立上下文换来了错误隔离和 Token 可控,但代价是子任务间的推理断层——后续任务不知道前面的 Agent "为什么"做出某个选择。
06 EvalAgent:最干净的评估者
EvalAgent 是系统中最"干净"的智能体——每次评估都是一次无状态的单次 LLM 调用,固定 2 条消息,temperature=0.0。
EvalAgent 的极简上下文(verify.py:266-271, 316-320)
# 构造函数 — 唯一的实例属性是客户端 def __init__(self, client): self._client = client # 独立客户端,防止上下文冲突 # 每次评估 — 固定 2 条消息 response = await self._client.chat( messages=[ {"role": "system", "content": self._SYSTEM_PROMPT}, {"role": "user", "content": prompt}, ], temperature=0.0, )
评估数据来源
所有数据通过方法参数传入,从 SessionState(每轮重置)和磁盘文件(实时读取)获取:
| 数据 | 来源 | 说明 |
|---|---|---|
| 用户原始需求 | user_input 参数 | 直接传入 |
| 任务计划 | SessionState.task_plan | 每轮重置 |
| 子任务结果 | SessionState.last_episode | 每轮重置 |
| 工具调用记录 | SessionState.used_tools | 每轮重置 |
| 文件内容 | 磁盘实时读取 | _read_written_files() |
| 真实执行结果 | 子进程执行 | _run_verification() |
利:绝对独立的评估
优势 1 · 绝对客观
无历史评估记忆意味着每次评估都是独立判断,不会因为之前的评估结果(好评/差评)产生锚定效应。第三次评估和第一次评估标准完全一致。
优势 2 · 零污染
EvalAgent 使用独立的 LLM 客户端实例(_eval_client),与主 Agent 的客户端完全隔离。不存在上下文交叉污染的任何可能性。
优势 3 · 确定性可复现
temperature=0.0 + 固定 system prompt + 纯数据驱动的 user prompt,使评估结果在相同输入下高度一致,便于调试和回归测试。
优势 4 · 真实执行验证
不仅评估代码质量,还真实执行生成的文件并验证运行结果。入口点识别有 5 级优先级:智能体输出中提到的文件 > 标准入口文件名 > __name__ == "__main__" > HTML 文件 > Shell 脚本。"能跑"比"看起来对"重要得多——这是代码评估中最硬的证据
弊:最干净也最孤独
代价 1 · 无学习反馈
评估结果不回传给 MainAgent,不持久化,不影响后续行为。如果同一类错误反复出现,EvalAgent 每次都独立发现但无法形成"教训"。EvalAgent 的评估报告是 fire-and-forget:打印到控制台,然后消失
代价 2 · 评估视野受限
单次调用只能看到当前任务的快照数据,无法评估跨任务的累积质量——多个任务的代码风格是否一致?是否重复造轮子?是否存在依赖冲突?这些问题 EvalAgent 无从回答。
代价 3 · 重试机制粗糙
MAX_RETRIES = 2 的重试只是简单重复相同调用,不会根据失败原因调整 prompt。如果第一次评估因为 JSON 解析失败,重试也不会改变策略。
代价 4 · 系统提示词静态
_SYSTEM_PROMPT 是类级常量,无法根据项目类型(前端/后端/数据科学)或任务复杂度动态调整评估标准。用同一套标准评估"Hello World"和"分布式系统",精度必然打折。
一句话总结:EvalAgent 的独立性保证了评估的客观和一致,但也意味着它是一个没有记忆的裁判——每次从零开始评判,无法积累经验,也无法将发现反馈给系统。
07 跨框架对比:buddyMe 不是唯一的解法
buddyMe 的上下文管理策略并非唯一选择。不同框架对"隔离 vs 共享"的取舍截然不同:
上下文策略光谱
上下文策略光谱 ──── 从完全共享到完全隔离
完全共享 完全隔离
│ │
├─ AutoGen ──── Agent 间共享完整对话历史
│ 利: 全局视野最强 弊: Token 消耗爆炸 + 污染风险极高
│
├─ LangGraph ── State 在节点间流转,可自定义共享范围
│ 利: 灵活可控 弊: 配置复杂,需要显式管理状态 Schema
│
├─ buddyMe ──── 对话隔离 + 状态共享 + 文件桥接
│ 利: 错误隔离 + 稳定性高 弊: 推理断层 + 信息截断
│
├─ CrewAI ────── 每个 Agent 独立上下文,通过任务输出传递
│ 利: 角色边界清晰 弊: 上下文传递依赖用户设计
│
└─ 单次调用 ── 每次请求完全独立(如 EvalAgent)
利: 零污染 + 确定性高 弊: 无连续性 + 无记忆
详细对照
| 维度 | AutoGen | LangGraph | buddyMe | CrewAI |
|---|---|---|---|---|
| 对话历史 | 共享(群聊模式) | 可配置 | 隔离(独立构建) | 隔离 |
| 状态传递 | 消息内嵌 | State Schema | SessionState + JSON | 任务输出 |
| 上下文压缩 | 无内置 | 可插拔 | ContextCompressor | 无内置 |
| 评估隔离 | 无内置 | 无内置 | 独立客户端 + temperature=0 | 无内置 |
| 记忆系统 | 无 | Checkpointer | 三层记忆(短/中/长) | 短期记忆 |
| Token 可控性 | 低 | 高 | 高 | 中 |
| 推理连贯性 | 高 | 中 | 低 | 低 |
| 错误隔离性 | 低 | 中 | 高 | 高 |
关键洞察:不存在"最优"策略。推理连贯性和错误隔离性是天然矛盾的——共享上下文提升连贯性但增加污染风险,隔离上下文保证稳定性但牺牲推理深度。选型的核心是理解你的应用场景更怕哪个:信息丢失还是信息污染。
08 五个设计张力点
buddyMe 的上下文架构中存在五个核心张力,每个都是在工程实践中不得不做的取舍:
张力 1 · Main 持续 vs Sub 独立
Main 累积的对话历史通过文本摘要注入 Sub,但截断不可避免。长会话中 Sub 获得的上下文越来越"稀释"——像把一杯浓茶不断兑水
张力 2 · Sub 间 JSON 文件通信
结果被截断后写入 subtask_results.json,后续子任务可能丢失关键代码细节。子任务 1 生成了 500 行代码,传递到子任务 2 时只剩 200 行摘要
张力 3 · EvalAgent 无反馈闭环
评估结果不回传,不持久化,不影响后续行为。系统无法从评估中"学习"和改进。EvalAgent 发现同一个错误第 10 次,和第 1 次一样惊讶
张力 4 · 共享客户端风险
主 Agent 和子任务执行共用 _client,客户端层面的资源(连接池、速率限制)是共享的。子任务的大量工具调用可能触发 API 速率限制,影响主 Agent
张力 5 · 双重注入的 Token 浪费
前置子任务结果同时注入到 system prompt 和 user prompt(task_pipeline.py:354 和 :392)。对同一信息的双重注入浪费了 Token 预算——相当于让学生在考卷上把题目读两遍。优化方向:按需注入——system prompt 放摘要,user prompt 放完整数据
09 三层利弊全景对比
| 维度 | MainAgent | Sub-Agent | EvalAgent |
|---|---|---|---|
| 上下文类型 | 持续累积 | 完全独立 | 完全独立 |
| 状态存储 | 内存 + 磁盘 | JSON 文件(临时) | 无状态 |
| 消息继承 | 跨轮次继承 | 不继承,文本注入 | 不继承 |
| 错误传播范围 | 整个会话 | 单个子任务 | 单次评估 |
| Token 可控性 | 低(滑动窗口仅截数量) | 高(显式截断策略) | 极高(固定 2 条消息) |
| 并行可行性 | 不可(状态依赖) | 可行(独立上下文) | 可行(无状态) |
| 学习/改进能力 | 有(记忆系统) | 无 | 无 |
| 推理连贯性 | 强(多轮累积) | 弱(仅结果传递) | 最弱(仅看最终产物) |
通俗化类比:公司项目组
MainAgent = 项目经理
维护项目档案,记住客户每次沟通的细节。好处是能理解"上次说的那个"指什么,坏处是档案越积越厚,找东西越来越难。持续上下文 = 项目经理的笔记本
Sub-Agent = 执行团队
每个人只看自己的任务单 + 前面人的工作成果。专注、不被打扰,但不知道同事在想什么。前面人为什么选了方案 A 而不是 B?不知道。独立上下文 = 每个人的独立工位
EvalAgent = 质检部门
完全独立于项目团队,不参与任何执行过程,只看最终成果 + 执行记录。会真实运行交付的代码看能不能跑通。但质检报告不会反馈给项目组——下次同样的问题会再次出现。无状态评估 = 外部审计,审计完就走
10 改进路线图:从"够用"到"优秀"
当前设计在 buddyMe 的应用场景(AI 辅助编程)中是合理且务实的。但在更复杂的多 Agent 场景下,以下改进方向值得探索:
短期优化(1-2 周)
优化 1 · 决策摘要传递
在 SubtasksManager 中增加 decisions 字段,记录关键设计决策及其理由减少重复探索,提高跨任务一致性
优化 2 · 双重注入去重
避免 system prompt 和 user prompt 中重复注入相同的前置子任务结果system 放摘要,user 放完整数据
优化 3 · 独立子任务客户端
为子任务执行创建独立的 LLM 客户端实例,避免速率限制干扰隔离客户端层面的资源竞争
中期改进(1-2 月)
A
结构化上下文传递
用 JSON Schema 定义子任务间的上下文格式,替代纯文本拼接。减少信息损失,提高可解析性。
│
B
EvalAgent 反馈闭环
将评估报告中的关键发现(代码风格不一致、常见错误模式)持久化到记忆系统,供后续任务参考。让裁判不仅能发现问题,还能帮团队记住教训
│
C
基于重要性的消息选择
取代简单的滑动窗口截断,引入重要性评分机制——保留对后续任务最相关的消息,而非最近的 N 条。
长期演进(3-6 月)
多轮评估反馈闭环
评估结果反馈给 MainAgent → 触发针对性修复 → 重新评估实现自我改进循环
上下文预算管理
为每个阶段分配 Token 预算,动态调整上下文压缩策略精细化 Token 使用
Agent 间通信协议
定义标准化的 Agent 间消息格式,支持更灵活的多 Agent 编排为更复杂的多 Agent 场景奠定基础
核心原则:不是传递更多上下文,而是传递更有质量的上下文。buddyMe 当前的设计是在"信息丢失"和"信息污染"之间选择了前者——对于文件生成类的确定性任务,这是正确的取舍。未来的改进方向是缩小两者的距离:既不丢关键信息,也不引入噪音。
下一篇预告:我们将深入 buddyMe 的上下文压缩引擎——
ContextCompressor 如何在保留关键信息的前提下将 20 条消息压缩为 7 条?压缩策略的参数如何影响 Agent 的表现?这是一个关乎 Token 预算和任务质量的精密工程问题。