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 的核心,不是多一个角色,而是多一个干净上下文。
往期回顾:
- Agent 开发入门(一):从「会说」到「会做」,先搞懂 Agent Loop
- Agent 开发入门(二):加工具不改循环,靠的是这个设计
- Agent 开发入门(三):为什么你的 Agent 总是在「瞎干」?试试 TodoWrite
下期预告:当上下文越来越长怎么办?聊聊 Context Compact(上下文压缩)技术。