按需上下文与子 Agent 隔离

专栏:Agent 工程 · 第 8 / 18 篇
Agent上下文工程子 Agent

压缩是被动防御,这篇讲主动策略:与其把所有信息塞进窗口,不如设计成”需要时才取、用完即走”。三个手段按侵入性递增。

:::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。

图表(jit-context-and-subagents.md)

两个系统的实现:

  • CodexThreadManager::spawn_subagent + multi_agents 工具族,子 agent 是完整线程(有自己的 rollout);agent 间通信(InterAgentCommunication)也进会话日志。code-mode 一族更激进:工具调用变成”运行时会话里写代码”,批量组合调用不占对话历史。
  • dshctx.subagents 服务 + tool-subagent 工具,后端可插拔(进程内 spawn/fork、ACP、真实 Codex/Claude Code——“把一个轮次委托给另一个产品”与新建子 agent 在同一接口之后)。关键机制是作用域隔离:每个 agent 有独立的 scope fiber,经 agent.ctx 注册的工具与提示词段只对本 agent 生效,随 agent 销毁整体撤销——子 agent 的注册永远不会污染主 agent。

隔离的另一半是提问权:dsh 规定子 agent 不能调用 ask_user_questionDELEGATED_CALLER 拒绝)——面向人类的交互只属于直接面对人类的那个 agent。这类边界在多 agent 篇(第 14 篇)会展开。

手段三:结构化笔记(上下文的外挂记忆)

Anthropic 上下文工程专文强调的技巧:让模型把中间结论写下来(todo、笔记文件),需要时读回。历史可以压缩,笔记不丢事实。dsh 的 todo_write(状态从会话事件折叠、轮次边界自动清理)与 Codex 的 update_plan 工具都是这个模式的产品化——计划即工具,笔记即记忆(第 7 篇的第 0 层)。

三手段的选择顺序

场景手段理由
信息量大但访问有规律工具化(按需取)模型决策成本最低
探索过程噪声大sub-agent 隔离过程留子上下文,结论回主上下文
中间结论未来要用结构化笔记免疫压缩与轮次边界
真的海量、无规律RAG(下一篇)需要索引基础设施

随堂练习(带验收标准)

  1. 实装 research():选一个”为了回答要读 ≥5 个文件”的真实问题,对比主 agent 直接做 vs 外包探索的主上下文 token 曲线。验收:外包版主历史增量 ≤ 直接版的 1/3,且结论质量不降;
  2. 给工具结果加 4000 字符截断(头 3000 + 尾 1000,中间标注 …[省略 N 字符]…),观察模型对截断提示的续读行为;
  3. 踩坑观察:允许子 agent 调用 research 自己(递归委托),设一个深度上限并触发它——理解为什么 dsh 禁止子 agent 提问、Codex 对子 agent 深度有限制;
  4. codex-rs/core/src/codex_thread.rsstart_or_steer_turn 文档注释,体会”start / steer / decline”三态对 UI 的意义。

← 返回文章列表