RSS

第 6 章 / 共 21 章

子代理:用上下文隔离换取专注

约 6 分钟 更新于

上一章留下的问题:清单解决了”忘记目标”,但没解决”上下文被撑爆”。设想 Agent 要定位一个跨越三十个文件的 bug:它读文件、看调用链、试探性地加日志、跑测试——六十轮下来,messages 里塞满了三十份文件内容和几十条命令输出。而这些中间过程,对于接下来要做的事几乎毫无价值:真正有用的只有一句”问题出在 parser.py 第 88 行的空值处理”。

你自己排查 bug 时是怎么做的?大概率是新开一个终端窗口,在那里翻来翻去,找到结论以后记在笔记里,然后关掉那个窗口,回到原来的窗口继续干活。那些翻找过程不需要留在你的主工作区里。

6.1 把子任务丢进一个新的对话

给 Agent 加一把 task 工具:它接收一段任务描述,开一个全新的 messages 数组,在里面跑一个独立的小循环,最后只把最终那段文字返回。

SUB_SYSTEM = "你是一个专注的子任务执行者。完成任务后,用简洁的文字给出结论,不要复述过程。"

def spawn_subagent(description: str) -> str:
    sub_tools = [t for t in TOOLS if t["name"] != "task"]     # 关键:不给它 task
    messages = [{"role": "user", "content": description}]

    for _ in range(30):                                        # 安全上限
        response = client.messages.create(
            model=MODEL, system=SUB_SYSTEM,
            messages=messages, tools=sub_tools, max_tokens=8000,
        )
        messages.append({"role": "assistant", "content": response.content})
        if response.stop_reason != "tool_use":
            break

        results = []
        for block in response.content:
            if block.type == "tool_use":
                blocked = trigger_hooks("PreToolUse", block)   # 权限照旧生效
                if blocked:
                    results.append({"type": "tool_result", "tool_use_id": block.id,
                                    "content": str(blocked)})
                    continue
                output = TOOL_HANDLERS[block.name](**block.input)
                results.append({"type": "tool_result", "tool_use_id": block.id, "content": output})
        messages.append({"role": "user", "content": results})

    return extract_text(messages[-1]["content"])               # 只带结论回去

TOOL_HANDLERS["task"] = spawn_subagent

这段代码里有四个设计决定,每一个都值得单独理解:

决定怎么做的为什么
上下文隔离全新的 messages子任务的六十轮翻找不会进入主线
只回结论extract_text(messages[-1])主线拿到的是笔记,不是录像
防递归子代理的工具列表里剔掉 task否则子代理可以再开子代理,无限套娃
权限不放松子循环里照样触发 PreToolUse隔离的是上下文,不是权限

最后一条最容易被误解。“独立进程”听起来像是自由了,其实不是——子代理仍然在同一台机器上操作同一个文件系统,护栏一寸都不能松。

第6章:主线堆积 vs 隔离回传
图 6.1:同一个子任务,两种上下文结局。要看的是右边那条细箭头——六十轮的过程留在了子对话里,回到主线的只有一行结论。你换来的是主线上下文的干净,付出的是”过程不可回溯”。

6.2 走一遍:一次委派的收支账

任务:“这个项目的 CI 最近变慢了,找出原因并修掉。”

不用子代理时,主线上下文大致是这样堆起来的:CI 配置文件(约 3K token)、最近二十次构建日志(约 40K)、五个测试文件(约 15K)、十几条命令输出(约 8K)——加起来 60K 出头,而其中真正影响后续修复动作的信息不到 1K。

用子代理时:主线调用一次 task("分析最近的 CI 构建日志,找出耗时增加的根本原因"),子代理在自己的对话里烧掉那 60K,返回一句”test_integration.py 里新增的 sleep(30) 是主要原因,占新增耗时的 82%“。主线上下文只增加了这一句。

账要这么算:你省下的是主线上下文,付出的是一次额外的模型调用开销,以及”中间过程不可回溯”——如果子代理的结论是错的,主线看不到它是怎么错的。所以子代理适合结论明确、过程冗长的任务(搜索、定位、调研、批量核对),不适合需要主线持续参与判断的任务。

6.3 工程现实:隔离的边界在哪里

有一件事必须说明白:**只有对话被隔离了,副作用没有。**子代理调用 write_file 写下的文件是真实存在的,跑的命令是真实执行的。它不是沙箱,是一个干净的笔记本。

真正的并行安全需要文件系统级别的隔离——那是第18章 worktree 要解决的问题。

另外,那个 range(30) 的硬上限也不是随手写的。子代理没有人在旁边看着,一旦陷进死循环,你不会立刻发现。给它一个明确的上限,让它”失败得快一点”。

6.4 常见坑

6.5 算一笔委派的上下文账

6.6 说出委派时你放弃了什么