一句话价值:当一个 Agent 要管的事越来越多,它自己的上下文会越来越脏、工具清单越来越长、一个 prompt 越来越塞不下。解法是把任务派出去 ------父 Agent 保留一个"委派"工具,把子任务丢给一个全新上下文的子 Agent 去干。这一篇讲清楚:为什么拆、子 Agent 的"上下文隔离"怎么设计,以及怎么防止子 Agent 无限"套娃"。
先看:一个 Agent 塞太多,会发生什么
回到前三篇搭起来的那个 Agent------它现在有 4 个工具,能读、写、编辑、跑命令,还有个 while 循环。功能已经不错,但一旦任务变复杂,问题就冒出来:
- 上下文越来越脏 :一个复杂任务要来回几十轮,历史里堆满了"我读了文件 A"、"写文件 B 失败重试"......这些过程记录全留在
messages里。到了任务后半段,模型要在一堆陈年旧账里翻"当前到底该干嘛",注意力被严重稀释。 - 工具清单越来越长 :所有工具一股脑塞给一个模型,每个工具的
description都是 token,都是噪音。一次调用只用其中一两个,但每次都得背着全部。 - 职责塞不下:既要规划"先查什么再写什么",又要执行"读这个文件、改那行代码",一个 prompt 里两种职责互相打架。
于是你自然想到:拆。让一个"老板"负责规划和委派,让"工人"负责埋头干具体子任务,干完只交回一句结论。
这就是父---子代理(Sub-Agents)的由来。而拆的关键,不是"多写一个循环"(那只是复制粘贴),而是怎么设计它们之间的边界。
委派机制:父多一个 task 工具
先看工具集的差别------这是整个设计的入口:
python
PARENT_TOOLS = CHILD_TOOLS + [task_tool] # 父 = 子工具 + 一个 task
CHILD_TOOLS 是那四个干活工具(bash / read / write / edit),父 Agent 在它们之上多一个 task 工具:
python
{
"type": "function",
"function": {
"name": "task",
"description": "Spawn a subagent with fresh context. It shares the filesystem but not conversation history",
"parameters": {
"type": "object",
"properties": {
"prompt": {
"type": "string",
"description": "The complete task for the subagent. Include all context it needs, since it cannot see this conversation.",
}
},
"required": ["prompt"],
},
},
}
注意这个工具 description 里藏了两句设计契约:
- "shares the filesystem but not conversation history" ------子 Agent 和父共享文件系统,但不共享对话历史。这是"上下文隔离"的定义。
- "Include all context it needs, since it cannot see this conversation" ------因为子 Agent 看不到父的对话,所以父在
prompt里必须给全上下文。这是隔离的代价,下面会展开。
父 Agent 在循环里遇到 task 工具,就调 run_subagent 把活派下去:
python
if tool_call.function.name == "task":
content = run_subagent(prompt) # 派活,等子 Agent 干完拿回结论
tool_results.append({"role": "tool", "tool_call_id": tool_call.id, "content": content})
整个链路是:父决定"这活派给别人" → 调 task → 子 Agent 独立跑完 → 返回一句结论 → 结论作为 tool 结果回到父的对话。
子 Agent 的两大设计:全新上下文 + 不能套娃
子 Agent 的实现 run_subagent,外表看就是个缩小版的 agent_loop,但有两个关键区别。
区别一:全新上下文------子 Agent 看不到父的对话
python
def run_subagent(prompt: str) -> str:
child_messages: list[ChatCompletionMessageParam] = [
{"role": "user", "content": prompt}
]
system_message = {"role": "system", "content": SUB_SYSTEM}
while True:
response = client.chat.completions.create(
model=MODEL,
messages=[system_message, *child_messages],
tools=CHILD_TOOLS,
max_tokens=8000,
)
msg = response.choices[0].message
child_messages.append(cast(ChatCompletionMessageParam, msg.model_dump()))
if not msg.tool_calls:
return msg.content or ""
...
看到没------child_messages 从零开始 ,只有一条 user 消息(就是父给的 prompt)。父 Agent 之前和用户聊了什么、调过什么工具,子 Agent 一概不知。
这正是"上下文隔离"的核心:
- 收益 :子 Agent 不会被父那段冗长、可能已经跑偏的历史干扰,专注干自己这一件事;同时也省 token------不用把父的整个历史塞进来。
- 代价 :
prompt是子 Agent 唯一的输入 。所以父派任务时必须给全上下文,否则子 Agent 就是"睁眼瞎"------这也正是task工具 description 里那句 "Include all context it needs" 的由来。
一句话:上下文隔离不是免费的,它把"要传什么信息"的责任转嫁给了父 Agent 的 prompt。
区别二:只带 CHILD_TOOLS,不能套娃
再看子 Agent 的循环里,tools=CHILD_TOOLS------没有 task。
这意味着:
- 父 Agent 能派生子 Agent(有
task); - 子 Agent 不能再派生子子 Agent (没有
task)。
这是刻意的防套娃设计。如果不限制,子 Agent 又能派孙子 Agent,孙子再派曾孙......递归深度失控,token 和时间成本直接爆炸,还可能无限循环。所以一刀切:
只有顶层能派活,底层只能干活。
"派活权"被严格限制在一层。这也是为什么 PARENT_TOOLS 和 CHILD_TOOLS 要分开定义------它们之间只差一个 task,但这一"差"就是"能不能派活"的边界。
父子循环对比:几乎同构,差在"权"上
把父的 agent_loop 和子的 run_subagent 摆一起看,你会发现它们几乎是同一段代码,只有三处不同:
agent_loop(父) |
run_subagent(子) |
|
|---|---|---|
| 历史起点 | 沿用调用方传入的 messages |
只有一条 prompt |
| 工具集 | PARENT_TOOLS(含 task) |
CHILD_TOOLS(无 task) |
| 特殊分支 | 有 task 工具 → 递归派生子 Agent |
无,只执行普通工具 |
这个"几乎同构"是很有味道的设计------父和子共用同一套 Agent 循环,差别只是"给什么工具、从什么历史开始"。它意味着:
- 不需要为"子 Agent"发明一套新机制,它就是一个"剥夺了派活权、换了全新历史"的普通 Agent;
- 如果你想让某个子 Agent 也能派活(比如三层架构),只需给它也配上
task工具------权限不是写死在代码结构里,而是写在"给哪个工具集"里。
心智模型:老板和工人
把这一篇收束成一个画面:
父 Agent 是老板,子 Agent 是工人。老板掌握"派活权"(
task工具),工人的工具箱里只有"干活的家伙"(读写编辑跑命令),没有"再招人"的权力。每次派活,老板都得把任务说全了(因为工人看不见老板之前的对话),工人干完,只交回一句浓缩的结论。
顺着这个模型,几个设计决策都有了朴素的解释:
- 为什么共享文件系统、不共享对话?------工人和老板在同一间办公室(文件系统),但工人不知道老板之前和客户聊了什么(对话历史),他只接一份"任务单"(prompt)。
- 为什么工人不能再招人?------否则层级失控,一个人招十个人、十个再各招十个,管理成本爆炸。
- 为什么工人只交结论、不交过程?------老板要的是"这活干没干完、结果是什么",不是工人干活的每一个动作,交过程只会把老板自己的上下文也搞脏。
本篇小结
- 单个 Agent 塞太多会"上下文变脏、工具变吵、职责打架",所以要拆出子 Agent。
- 委派机制 = 父 Agent 多一个
task工具(PARENT_TOOLS = CHILD_TOOLS + [task])。 - 子 Agent 的核心是上下文隔离 :
child_messages从零开始,只有一条prompt,看不到父的对话。 - 隔离的代价是
prompt必须给全上下文 ------这是task工具 description 里写死的契约。 - 防套娃 :子 Agent 只带
CHILD_TOOLS(无task),不能派生子子 Agent,"派活权"严格限在一层。 - 父和子共用同一套循环,差别只在"给什么工具、从什么历史开始"------权限写在工具集里,不写在结构里。
- 心智收束:父是老板、子是工人------老板有派活权、任务要说全,工人只干活、只交结论。
系列预告
- 下一篇(最后一篇):当 Agent 有了手,就得给它戴手套 ------
safe_path怎么防路径逃逸、命令黑名单、超时,以及更进一步的进程隔离与沙箱分层。