探索型子 Agent
探索器是最简单构建的子 Agent,也是最实用的起步工具。
它能读取文件。它可以搜索。它别无选择。没有write,没有 bash,也没有向用户询问。它调查一个问题,总结发现,然后消失。
这听起来像是个限制。是功能。探索器不能漂移,不能无意中做出改变,也不能用创意find -exec毁掉你的项目。它做了一件事,完成后,父 Agent会得到一个干净的答案,而不是中间文件读取的四十步。
学习成果
面向父 Agent的task工具会生成一个全新的ToolLoopAgent,只有read和 grep,是廉价的模型,预算是五步。父 Agent可以委派研究工作,并获得文本摘要。
快速路径
- 定义一个
task工具,其schema接受description子 Agent - 在
execute内,用read和grep新实例化一个新的ToolLoopAgent - 使用
claude-haiku-4-5和stopWhen: stepCountIs(5) - 将子 Agent的文本回复返回父 Agent,并以尝试/接住的形式包裹
动手练习 6.2
从父 Agent加上一个分组接缝,变成探索器子 Agent。
要求:
- 在你的工具注册表中添加一个
task工具 - schema会用
description: string,父 Agent用来告诉子 Agent调查什么 - 在
execute里,创建一个新ToolLoopAgent,用read和grep(不bash,不askUser) - 选一个快的模型(
claude-haiku-4-5),把步数限制在5 - 返回探索器的文本回复,错误被捕捉并以字符串形式返回
实现提示:
- 探索器是按每个调用实例化的。不要重复使用。每个代表团都会获得新的一上下文窗口
- 重复使用
read和grep已有的工具。它们封闭在父 Agent使用的沙箱上,这正是你想要的 - 用尝试/捕捉和返回
"Subagent error: ${e.message}"包裹explorer.generate(...),而不是让异常传播。父 Agent期望任何工具都能回线
task 工具
import { ToolLoopAgent, stepCountIs, tool } from "ai";
import { z } from "zod";
import type { Sandbox } from "./sandbox";
export function createTaskTool(sandbox: Sandbox, parentTools: {
read: ReturnType<typeof createReadTool>;
grep: ReturnType<typeof createGrepTool>;
}) {
return tool({
description: `Delegate research to a read-only subagent.
WHEN TO USE: investigating a codebase, finding patterns, gathering context
across many files.
WHEN NOT TO USE: making changes (the subagent cannot write or run commands).
DO NOT USE FOR: tasks that need decisions or askUser interactions.`,
inputSchema: z.object({
description: z.string().describe("What the subagent should investigate"),
}),
execute: async ({ description }) => {
const explorer = new ToolLoopAgent({
model: "anthropic/claude-haiku-4-5",
instructions: `You are an explorer agent. Investigate and report back concisely.
Working directory: ${sandbox.workingDirectory}`,
tools: { read: parentTools.read, grep: parentTools.grep },
stopWhen: stepCountIs(5),
});
try {
const { text, steps } = await explorer.generate({ prompt: description });
return text
? `[Explorer: ${steps.length} steps]\n${text}`
: "(no response from subagent)";
} catch (e: any) {
return `Subagent error: ${e.message}`;
}
},
});
}以下是一些值得指出的设计选择:
- Fresh Agent 根据调用。 探索器无法跨越调用存活。每个任务都有自己的上下文窗口,这正是委派工作的全部意义所在
- **No
bash,没有,askUser.**探索器可以读取和搜索。它无法修改项目,也无法暂停用户输入。父 Agent继续掌控决策权 - **Haiku,探索Sonnet.**不是阅读和总结,而不是深度推理。更快、更便宜的模型才是合适的选择
- **Five steps.**足够看几份文件并反馈。如果探索器需要更多,父 Agent应该把任务拆成更小的部分
- **Errors返回为strings.**工具返回字符串到模型。未接住的例外打破了工具循环。返回错误文本让父 Agent决定下一步
接入父 Agent
const tools = {
read: createReadTool(sandbox),
grep: createGrepTool(sandbox),
bash: createBashTool(sandbox, createApproval({ mode: "interactive" })),
};
const tools_with_task = {
...tools,
task: createTaskTool(sandbox, { read: tools.read, grep: tools.grep }),
};
const agent = new ToolLoopAgent({
// ...
tools: tools_with_task,
});父 Agent现在有四个工具:read、grep、bash和task。前三个是直接的。第四位代表。
**Warning:边做边添加日志debug**
当子 Agent什么都不返回或送错东西时,你根本不知道它运行过程中发生了什么。开发过程中,在任务工具中记录子 Agent的步数和文本长度。没有这些,你会看到混乱的父 Agent输出,不知道子 Agent是跑了一级还是五级,发现了什么,还是悄悄失败了。
动手试试
请父 Agent探索器适合的相关内容:
bun run index.ts . "Delegate to a subagent: find every place this project uses zod and tell me which files import from it."父 Agent应该调用 task这个描述。探索器应该运行,找到导入数据并返回摘要。父 Agent应该把这个摘要传回给你。
作为对比,运行同一个任务,但没有显式委派指令:
bun run index.ts . "Find every place this project uses zod and tell me which files import from it."父 Agent可能会授权,也可能不会。如果有强有力的工具描述,可能会直接影响调用 grep。没关系。当搜索需要浏览大量文件,而父 Agent不希望所有文本都包含在上下文中时,委托工具才真正发挥作用。
npx tsc --noEmit提交
git add src/tools.ts index.ts
git commit -m "feat(subagents): add explorer via task tool"完成标准
- [ ]
createTaskTool存在并返回一个task工具 - [ ] 任务工具每调用生成一个新的
ToolLoopAgent - [ ] 探索器只有
read和grep - [ ] 探索器使用
claude-haiku-4-5,停在5步处 - [ ] 错误以字符串形式返回,而非例外
- [ ] 父 Agent可以委派研究,并获得一个干净的总结
- [ ]
npx tsc --noEmit
**注意:平行探险者**
单一的探索器是协程。从父 Agent的工具循环并行生成两个机器人才是真正的并行。试着更改任务工具的schema,让它接受一系列描述并用 Promise.all 运行。现在父 Agent可以同时研究代码库的三个不同部分并综合结果。为了利用这一点,父 Agent提示词发生了哪些变化?
参考实现
import { ToolLoopAgent, stepCountIs, tool } from "ai";
import { z } from "zod";
import type { Sandbox } from "./sandbox";
export function createTaskTool(
sandbox: Sandbox,
parentTools: { read: any; grep: any },
) {
return tool({
description: `Delegate research to a read-only subagent.
WHEN TO USE: investigating a codebase, finding patterns, gathering context.
WHEN NOT TO USE: making changes (the subagent cannot write or run commands).
DO NOT USE FOR: tasks that need decisions or askUser interactions.`,
inputSchema: z.object({
description: z.string().describe("What the subagent should investigate"),
}),
execute: async ({ description }) => {
const explorer = new ToolLoopAgent({
model: "anthropic/claude-haiku-4-5",
instructions: `You are an explorer agent. Investigate and report back concisely.
Working directory: ${sandbox.workingDirectory}`,
tools: { read: parentTools.read, grep: parentTools.grep },
stopWhen: stepCountIs(5),
});
try {
const { text, steps } = await explorer.generate({ prompt: description });
return text
? `[Explorer: ${steps.length} steps]\n${text}`
: "(no response from subagent)";
} catch (e: any) {
return `Subagent error: ${e.message}`;
}
},
});
}