按需上下文与子 Agent 隔离
专栏:Agent 工程 · 第 8 / 18 篇压缩是被动防御,这篇讲主动策略:与其把所有信息塞进窗口,不如设计成”需要时才取、用完即走”。三个手段按侵入性递增。
:::info 学习目标 完成本篇后你能够:实现一个上下文隔离的子 agent 函数;为工具结果设计截断协议;说出”工具化 / 子 agent / 笔记 / RAG”四种手段的选择顺序。 前置:第 6~7 篇完成。预计时长:60 分钟。 :::
:::note 本章术语速查(新手建议先读)
- Sub-agent(子智能体):主 agent 派出去干探索活的”下属”——它自己翻箱倒柜,只把结论汇报回来,过程不占主 agent 的记忆。
- 上下文隔离:子 agent 的中间过程不污染主 agent 的窗口。
- 注入(Inject):把信息悄悄放进 agent 的待办清单,不打断当前工作。
- 按需检索:信息不预先塞满窗口,模型需要时才通过工具去取。 :::
子 agent 的最小实现(30 行)
第 8 篇方法论落地的最快路径——同模型、不同工具面、独立历史:
def research(task: str, max_steps: int = 10) -> str:
"""探索外包:子 agent 只带只读工具,把结论(而非过程)交回主 agent。"""
messages = [
{"role": "system", "content":
"你是探索专员。只读不改。最终回复必须是一段 ≤200 字的结论,"
"附关键文件:行号。不要执行任何写操作。"},
{"role": "user", "content": task},
]
tools = [t for t in TOOLS if t["function"]["name"] in ("run_bash", "read_file")]
for _ in range(max_steps):
reply = chat_with_tools(messages, tools)
messages.append(reply)
calls = reply.get("tool_calls")
if not calls:
return reply["content"] or "(无结论)"
for call in calls:
result = execute_safe(call) # 与主循环同一执行器,只读工具
messages.append({"role": "tool", "tool_call_id": call["id"],
"content": result[:2000]})
return "(探索未收敛) " + last_assistant_text(messages)
主循环里新加一个 research 工具把这段包起来——主 agent 的历史只多一行工具结果,子 agent 的十次翻箱倒柜全部留在它自己的上下文里。
手段一:按需检索(工具即上下文阀门)
最朴素的形态:把信息放进工具的返回值,而不是塞进提示词。模型需要时才调工具拿——文件内容、命令输出、API 响应天然如此(这就是 coding agent 不内置 RAG 的原因之一,第 9 篇详谈)。推而广之:
- dsh 的 runtime 上下文投影:时间、工作区状态等动态信息注册为 context,
project()只在快照变化时生成一条消息并入批次——信息按需上新,前缀不动; - Codex 的注入式上下文:
agent.inject()类的输入先进 inbox、等下一次有活时捎带进上下文(dsh 的inject()语义:不唤醒、躺到下一步),文件变更通知、子目录 AGENTS.md 都走这条道; - 大结果的截断协议:dsh 的工具结果超过阈值折叠(spill 策略:头部 + 尾部 + 中间省略),Codex 的
max_output_tokens让模型自己声明愿意带多少——取多少上下文,本身是模型决策的一部分。
三种输入入口:inject / steer / followup
第 4 篇的循环只支持”用户说一句、agent 跑一轮”。工业版都把输入入口精细化——以 dsh 的 inbox 设计为例:
| 入口 | 目标队列 | 是否唤醒 | 用途 |
|---|---|---|---|
followup() | next-turn | 是 | 开新轮次(普通对话) |
steer() | next-step | 是 | 当前轮次中途纠偏(“别用那个库”) |
inject() | next-step | 否 | 静默注入(文件变更通知、AGENTS.md),等下次有活捎上 |
Codex 同构:start_or_steer_turn 统一入口,pending input 每步领取。inject 不唤醒是最精妙的——上下文”要时才有、不扰当前”(对应第 6 篇的缓存纪律:不打断当前请求前缀)。
手段二:sub-agent 上下文隔离
长任务的上下文爆炸往往来自”探索过程”:为了找一个函数定义读了十个文件,十个文件的内容全留在历史里。sub-agent 的解法是把探索外包:子 agent 带着任务去翻箱倒柜,只把结论一行交回主 agent。
两个系统的实现:
- Codex:
ThreadManager::spawn_subagent+multi_agents工具族,子 agent 是完整线程(有自己的 rollout);agent 间通信(InterAgentCommunication)也进会话日志。code-mode一族更激进:工具调用变成”运行时会话里写代码”,批量组合调用不占对话历史。 - dsh:
ctx.subagents服务 +tool-subagent工具,后端可插拔(进程内 spawn/fork、ACP、真实 Codex/Claude Code——“把一个轮次委托给另一个产品”与新建子 agent 在同一接口之后)。关键机制是作用域隔离:每个 agent 有独立的 scope fiber,经agent.ctx注册的工具与提示词段只对本 agent 生效,随 agent 销毁整体撤销——子 agent 的注册永远不会污染主 agent。
隔离的另一半是提问权:dsh 规定子 agent 不能调用 ask_user_question(DELEGATED_CALLER 拒绝)——面向人类的交互只属于直接面对人类的那个 agent。这类边界在多 agent 篇(第 14 篇)会展开。
手段三:结构化笔记(上下文的外挂记忆)
Anthropic 上下文工程专文强调的技巧:让模型把中间结论写下来(todo、笔记文件),需要时读回。历史可以压缩,笔记不丢事实。dsh 的 todo_write(状态从会话事件折叠、轮次边界自动清理)与 Codex 的 update_plan 工具都是这个模式的产品化——计划即工具,笔记即记忆(第 7 篇的第 0 层)。
三手段的选择顺序
| 场景 | 手段 | 理由 |
|---|---|---|
| 信息量大但访问有规律 | 工具化(按需取) | 模型决策成本最低 |
| 探索过程噪声大 | sub-agent 隔离 | 过程留子上下文,结论回主上下文 |
| 中间结论未来要用 | 结构化笔记 | 免疫压缩与轮次边界 |
| 真的海量、无规律 | RAG(下一篇) | 需要索引基础设施 |
随堂练习(带验收标准)
- 实装
research():选一个”为了回答要读 ≥5 个文件”的真实问题,对比主 agent 直接做 vs 外包探索的主上下文 token 曲线。验收:外包版主历史增量 ≤ 直接版的 1/3,且结论质量不降; - 给工具结果加 4000 字符截断(头 3000 + 尾 1000,中间标注
…[省略 N 字符]…),观察模型对截断提示的续读行为; - 踩坑观察:允许子 agent 调用
research自己(递归委托),设一个深度上限并触发它——理解为什么 dsh 禁止子 agent 提问、Codex 对子 agent 深度有限制; - 读
codex-rs/core/src/codex_thread.rs的start_or_steer_turn文档注释,体会”start / steer / decline”三态对 UI 的意义。