一个 Agent 看完 30 个文件后,为什么该让它先退出?
本文是「从零理解 Claude Code:20 个 Agent Harness 机制」系列的第 6 篇。
源码仓库:shareAI-lab/learn-claude-code
本文基于开源仓库学习整理,具体实现以仓库代码为准。
上一章的 TodoWrite 解决了一个问题:复杂任务不能只靠模型临场判断,需要把待办事项和进度保留下来。
不过,待办列表只能告诉 Agent 还剩什么工作,并不能减少某一项工作本身的复杂度。
例如,计划里有一项:
确认项目使用的测试框架,并找出相关配置。这句话看起来只是一个待办项,执行起来却可能意味着搜索项目结构、读取依赖声明、查看测试目录、检查配置文件,最后还要判断哪些内容才是主 Agent 真正需要的结论。
如果这些搜索过程、文件内容和中间判断全都留在主对话里,主 Agent 的上下文会越来越长。
前面读过哪些文件,尝试过哪些搜索词,哪些路径最终被排除,这些过程对完成子任务很有价值;但对子任务结束后的主流程,往往只剩下一两句结论有用。
这就是子 Agent 要处理的场景。
一、待办可以拆任务,但不能隔离上下文
假设主 Agent 正在修一个认证模块的问题。
它先列出计划:
1. 定位认证调用链 2. 找到失败原因 3. 修改代码 4. 运行相关测试 5. 检查回归影响第一项定位认证调用链这个逻辑本身可能就很重。
为了查清楚调用关系,Agent 也许需要读取几十个文件,搜索多个关键字,反复调整判断。主对话里会持续累积文件内容、命令输出和工具结果。
这时,主 Agent 后面还要做修改、测试和收尾,但上下文里已经塞进了大量定位过程的细节。
问题不在于这些细节没有价值。
它们对定位调用链的那个过程很重要;任务完成后,主 Agent 通常只需要知道调用链经过哪些模块、问题大概在哪个位置,以及有没有需要注意的约束。
因此,这里需要的不是再写一张更长的待办列表,而是把一段独立的探索工作交出去,并在结束后只收回结论。
二、子 Agent 的基本做法
这一章新增了一个task工具。
主 Agent 调用它时,程序会创建一个新的子 Agent。子 Agent 有自己的消息列表,从一条明确的任务描述开始运行;它可以继续读文件、搜索代码、执行命令,并在完成后返回一段总结。
父 Agent 不会拿到子 Agent 全部的工具调用历史,那父 Agent 会拿到什么呢,其实只是拿到子 Agent 的返回结果而已。
用一个简单的例子表示:
主 Agent: 请确认项目使用什么测试框架,并找出相关配置。 子 Agent: 读取 pyproject.toml → 搜索 tests 目录 → 查看配置文件 → 得出结论 主 Agent: 收到总结,继续执行后续计划。这里最容易被忽略的一点是,子 Agent 的价值并不只是多了一个执行者,它还建立了一条上下文边界。
子 Agent 可以在自己的对话中搜索、试错和排除路径,主 Agent 只保留对后续决策有价值的信息。这样,主对话不会因为某个局部调查任务而持续膨胀。
下面这张图展示了父 Agent、子 Agent 和总结结果之间的关系。
三、子 Agent 从一份新的 messages 开始
这章的实现没有让子 Agent 复用父 Agent 的历史消息。
创建子 Agent 时,程序只给它一条任务描述:
defspawn_subagent(description:str)->str:messages=[{"role":"user","content":description,}]for_inrange(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,})# 工具调用和结果回填逻辑# 与主 Agent 的循环类似这里的messages是一份新的列表。
子 Agent 看不到父 Agent 前面读取过哪些文件、写过哪些待办,也不会受到父对话中大量中间结果的影响。它只知道当前被交付的子任务是什么,以及可以使用哪些工具。
例如,父 Agent 可以交给它:
阅读 agents/ 目录下所有 Python 文件,说明每个文件的职责,并指出主要入口文件。子 Agent 会围绕这一件事展开搜索和阅读。
任务结束后,程序会提取最后一条文本结论:
returnextract_text(messages[-1]["content"])主 Agent 看到的可能是:
agents/ 目录包含 4 个模块。 其中 runner.py 是主入口,tools.py 负责工具注册, memory.py 管理会话状态,config.py 读取运行配置。至于子 Agent 具体读了哪些文件、尝试过哪些搜索词、哪次判断被推翻,这些过程不会回填到父 Agent 的消息历史中。
四、只返回总结,实际上是一种信息筛选
把完整过程全部返回给主 Agent,看似信息更全,实际会削弱拆分任务的意义。
子 Agent 读取了十个文件,主 Agent 未必需要再读一遍十个文件的全部内容。它需要的是能够支持下一步决策的信息。
例如,子任务是确认测试框架。
父 Agent 最终需要的可能只是:
项目使用 pytest; 测试位于 tests/; 配置写在 pyproject.toml; 运行命令为 pytest。这些信息足以让主 Agent 后续运行测试、定位测试文件或修改配置。
当然,只返回总结也有代价。
如果子 Agent 的总结遗漏了关键证据,父 Agent 无法直接查看完整的推理过程。这意味着父 Agent 对重要结论不能完全照单全收,尤其是涉及代码修改、权限操作或架构判断时,仍然应该进行必要验证。
子 Agent 的总结更适合被当成调查结果,而不是不可质疑的事实。
五、上下文隔离不等于执行环境隔离
新的 messages 列表只隔离了对话上下文,并没有创建一个完全独立的工作空间。
子 Agent 和父 Agent 仍然在同一个项目目录下运行。
如果子 Agent 调用了write_file或edit_file,文件修改会真实保留在工作区中;如果它执行了命令,命令也会作用于同一个环境。
因此,父 Agent 与子 Agent 之间的关系可以这样理解:
| 内容 | 是否隔离 |
|---|---|
| 消息历史 | 隔离 |
| 模型的中间推理过程 | 不返回父 Agent |
| 文件系统修改 | 共享 |
| Shell 命令影响 | 共享 |
| 权限检查 | 继续生效 |
仓库里的子 Agent 在调用工具前,仍然会经过PreToolUseHook。
这意味着子 Agent 即使拥有独立上下文,也不能绕过前面建立的权限边界。它请求执行高风险命令时,权限 Hook 仍然会阻止或要求确认。
如果上下文隔离被误解成单独开了一个安全环境,后续很容易出现错误设计。
举个例子,父 Agent 让子 Agent 排查某个接口为什么返回 500。
子 Agent 为了验证猜测,可能会修改一份本地配置文件,或者执行一条启动测试服务的命令。虽然这些操作发生在子 Agent 自己的消息历史里,但文件修改仍然会写入当前项目目录,启动的进程也仍然会占用同一个运行环境。
子 Agent 完成调查后,父 Agent 只会收到一段总结,例如“问题出在数据库连接配置”。但父 Agent 随后读取项目文件时,看到的已经是子 Agent 修改后的版本。
因此,新的messages只意味着父 Agent 看不到子 Agent 的完整调查过程。
为了避免这两层概念混在一起,可以看下面这张图。
六、为什么教学代码不允许子 Agent 继续创建子 Agent
主 Agent 的工具列表里有task。
子 Agent 的工具列表里没有task,只保留了bash、read_file、write_file、edit_file和glob。
这样设计的原因很直接,如果子 Agent 也可以继续创建新的子 Agent,任务树可能在缺少约束的情况下持续向下展开。父任务没有完成,子任务又派生出更多子任务,最后很难判断谁负责收尾,也很难控制总调用次数和成本。
这章的实现还给每个子 Agent 设置了最多 30 轮的限制。
限制并不能保证任务一定完成,但可以防止一个局部任务因为反复调用工具而无限运行。达到上限后,程序会返回已有结果,或者说明子 Agent 没有在限制内产出最终答案。
生产系统通常还会增加超时、取消信号、预算限制和更细的任务状态管理。这一章先保留最基础的两层控制:
- 子 Agent 不能递归创建新的子 Agent;
- 单个子 Agent 的工具循环有最大轮数。
七、子任务描述决定了子 Agent 的上限
子 Agent 拿到的是一条独立指令。
这条指令如果范围太大,上下文隔离也救不了它。
例如:
分析整个项目,找出所有问题并修复。这不是一个适合直接委派的子任务。它没有明确边界,也没有可验证的完成条件。子 Agent 可能花大量时间阅读文件,最后给出一份泛泛的总结。
更合适的描述应该包含范围、目标和交付形式:
阅读 agents/ 目录下的 Python 文件, 说明每个模块的职责, 找出程序入口文件, 只返回不超过 200 字的总结,不修改任何文件。这个描述明确了:
| 项目 | 内容 |
|---|---|
| 范围 | agents/目录 |
| 动作 | 阅读和分析 |
| 目标 | 模块职责与入口文件 |
| 输出 | 简短总结 |
| 副作用 | 不修改文件 |
子 Agent 并不会自动把模糊任务变清楚。
父 Agent 负责拆分任务,子 Agent 负责在给定边界内完成调查、修改或验证。任务描述越具体,父 Agent 收到的结果越稳定。
八、什么时候值得使用子 Agent
子 Agent 有启动成本,也会增加一次结果汇总过程,因此不适合所有操作。
读取一个配置文件、改一个变量名、运行一次测试,像这些简单任务直接由主 Agent 完成通常更简单。
下面几类任务更适合拆出去:
| 场景 | 适合委派的原因 |
|---|---|
| 阅读一组文件并总结结构 | 中间文件内容多,最终结论相对短 |
| 调查某个调用链或依赖关系 | 搜索和排除过程容易占满主上下文 |
| 独立实现一个边界明确的小模块 | 可以限定输入、输出和验收方式 |
| 运行一组专项检查 | 主 Agent 只需要检查结果和异常项 |
判断标准不在于任务听上去是否复杂,而在于它是否能形成一个相对独立的工作单元。
如果子任务结束后,父 Agent 只需要一段总结或一个明确的产物,就值得考虑交给子 Agent。
九、跑一下这章代码
进入仓库目录后执行:
python s06_subagent/code.py可以先试一个只读任务:
Use a subtask to find what testing framework this project uses.终端中会看到子 Agent 被创建、调用工具并结束。主 Agent 最终只拿到总结文本。
再试一个范围更明确的任务:
Delegate: read all .py files in agents/ and summarize what each one does.如果希望观察共享工作区的效果,可以尝试:
Use a task to create s06_subagent/example/string_tools.py with a slugify(text: str) function, then verify it from the parent agent.子 Agent 创建文件后,父 Agent 仍然可以读取和验证这个文件,因为两者操作的是同一个工作区。
小结
TodoWrite 把复杂任务拆成了可跟踪的步骤。
子 Agent 进一步把其中一部分工作放到独立消息历史中完成,并将结果压缩为一段可供父 Agent 使用的结论。
这套设计包含三个需要同时成立的条件:
- 子任务的边界足够清楚;
- 子 Agent 的中间过程不会持续占用主对话;
- 文件修改和权限控制仍然处于统一的工作环境中。
下一篇将讨论 Skill Loading。
子 Agent 解决了任务拆分和上下文隔离的问题。不同类型的任务仍然需要不同知识,例如前端组件规范、数据库表结构或项目约定。把所有知识都塞进系统提示词会让上下文越来越重,后续会通过按需加载技能的方式处理这个问题。