Agent 开发入门(四):上下文爆炸怎么办?试试 Subagent

4 阅读5分钟

Agent 开发入门(四):上下文爆炸怎么办?试试 Subagent

一个问题拆出十个任务,十个任务全塞进主对话,结果上下文撑爆了...这一篇聊聊子智能体的设计思路。

问题的本质

回顾一下前三章:

  • s01:有了 Agent Loop,模型能调用工具了
  • s02:有了工具分发系统,模型能读写文件了
  • s03:有了 TodoWrite,模型能规划任务了

但新的问题来了——当一个任务变大时,父对话会被大量中间过程填满。

举个例子:

用户:这个项目用什么测试框架?

Agent(读 pyproject.toml)
Agent(读 requirements.txt)
Agent(搜索 pytest 相关)
Agent(跑测试命令)
Agent(查测试配置)
Agent(分析测试覆盖率)
Agent(最后给出一句总结)

用户只想知道「用什么测试框架」,但 Agent 为了回答这个问题,读了一堆文件、跑了一堆命令。

这些中间过程全部堆在主对话里,后面想聊点别的,上下文已经被污染了。

解决方案:上下文隔离

核心思路:

把局部任务扔进独立上下文里做,做完只把结果带回来。

就像把脏衣服扔进洗衣篮,而不是全都摊在床上。

什么是 Subagent(子智能体)

父智能体
  │
  │ 1. 发现有个子任务
  v
子智能体(独立上下文)
  │
  │ 2. 读文件 / 搜索 / 执行
  │    (在自己的小屋里干活,不打扰别人)
  v
摘要返回
  │
  │ 3. 只带回必要结果
  v
父智能体继续

关键点:子智能体有自己独立的 messages,它的中间过程不会污染父对话。

核心数据结构

最简版本只需要:

class SubagentContext:
    messages: list      # 子智能体自己的上下文
    tools: list         # 它能用的工具
    handlers: dict      # 工具名 -> 处理函数
    max_turns: int      # 最多跑几轮,防止无限转

最小实现

第一步:给父智能体加一个 task 工具

{
    "name": "task",
    "description": "把子任务交给独立上下文执行,返回摘要",
    "input_schema": {
        "type": "object",
        "properties": {
            "prompt": {"type": "string"}  # 子任务描述
        },
        "required": ["prompt"]
    }
}

第二步:子智能体用空白上下文

def run_subagent(prompt: str) -> str:
    # 关键!不是共享父的 messages,而是从空白开始
    sub_messages = [{"role": "user", "content": prompt}]

    # 子智能体在自己的上下文里跑
    while True:
        response = client.messages.create(
            model=MODEL,
            messages=sub_messages,
            tools=SUB_TOOLS,
            max_tokens=8000,
        )
        # ... 执行工具 ...

        if response.stop_reason != "tool_use":
            break

    # 只返回摘要,不返回全部历史
    return extract_summary(sub_messages)

第三步:只带回必要结果

results.append({
    "type": "tool_result",
    "tool_use_id": block.id,
    "content": run_subagent(prompt),  # 返回摘要,不是全部历史
})

为什么它真的有用

好处 1:给父上下文减负

局部任务的中间噪声不会污染主对话。

好处 2:任务描述更聚焦

子智能体接到的 prompt 可以非常具体:

  • “读完这几个文件,给我一句总结”
  • “检查这个目录里有没有测试文件”
  • “对这个函数写一个最小修复”

好处 3:防止工具滥用

可以给子智能体更小的工具集:

SUB_TOOLS = [
    "read_file",   # 允许读文件
    "grep",        # 允许搜索
    "bash",        # 只读命令
    # "task" 禁止!防止无限递归派生子智能体
]

好处 4:安全保护

必须设置停止条件:

if turn >= MAX_TURNS:
    break  # 最多跑 N 轮

if error_count >= MAX_ERRORS:
    break  # 错误太多就退出

进阶:Fork 是什么

最小版本是「空白上下文」:

sub_messages = [{"role": "user", "content": prompt}]

但有时子任务需要知道「父智能体之前在做什么」:

# Fork:继承父上下文
sub_messages = list(parent_messages)  # 先复制
sub_messages.append({"role": "user", "content": prompt})  # 再追加任务

Fork 的本质:继承上下文,而不是重头开始。

但注意:这是「下一步」,不是「起步」。先跑通空白上下文版本,再考虑 fork。

子智能体 vs TodoWrite:什么时候用?

场景用哪个
一个任务需要做很多步骤,会话变长TodoWrite
一个任务要读很多文件、产生很多中间结果Subagent
任务简单,不需要大量工具调用TodoWrite
任务可能污染主上下文Subagent

核心区别

  • TodoWrite:同上下文,聚焦下一步
  • Subagent:新上下文,避免污染

常见误区

❌ 子智能体不是为了「炫技」

不是为了展示「我有 5 个 Agent」,而是为了解决上下文问题。

❌ 不要把子历史全部带回父对话

# ❌ 错误:把子智能体全部历史粘回去
messages.extend(sub_messages)

# ✅ 正确:只返回摘要
messages.append({"content": summary})

❌ 不要一上来就搞复杂角色

exporer / reviewer / planner / tester / implementer

这些都可以做,但先把这个做好:

一个干净上下文的子任务执行器

❌ 不要忘记停止条件

没有 max_turns 和异常处理,子智能体会无限转。

对比总结

父智能体子智能体
messages主对话历史独立干净上下文
工具全部工具精简工具集
目的主线任务局部子任务
结果完整对话摘要返回

一句话总结

Subagent 的核心,不是多一个角色,而是多一个干净上下文。


往期回顾:

下期预告:当上下文越来越长怎么办?聊聊 Context Compact(上下文压缩)技术。