03. 任务规划、子 Agent 与动态协作#
复杂任务不一定需要更多 prompt,而是需要合理的工作分解。本章将四种机制放在一起比较:显式 todo、同步子 Agent、可管理的异步任务、动态 fan-out。它们都能“拆任务”,但可靠性和控制权完全不同。
1. 规划为什么有用#
短问题可以直接回答;研究十个来源、整理证据和写报告则需要阶段状态。计划让 Agent 和用户知道:已完成什么、下一步是什么、哪里失败了。
计划的价值包括:
- 降低长任务中途迷失的概率;
- 让 UI 能显示进度;
- 为文件和子 Agent 提供顺序约定;
- 让评估器检查任务是否漏项。
但计划不是工作流引擎。模型可能跳过、重排或修改计划,不能只看 todo 状态判断副作用是否发生。
2. 当前版本显式启用 todo#
从 v0.7 起,write_todos 不是默认工具,需要通过 middleware 加入:
import { createDeepAgent } from "deepagents";
import { todoListMiddleware } from "langchain";
const agent = createDeepAgent({
model,
middleware: [todoListMiddleware()],
systemPrompt: `你负责完成多步骤研究任务。
只有复杂任务才创建计划,最多五项。
每完成一项就更新状态,计划项要包含可验证的完成证据。`,
});计划状态通常包括 pending、in_progress 和 completed,保存在 Agent state 中。middleware 让工具可用,但不保证模型一定调用它;应通过集成测试和轨迹观察确认行为。
3. 计划项怎么写#
用“动作 + 对象 + 完成证据”写计划:
1. 明确问题范围(in_progress)
2. 收集 3–5 个官方来源(pending)
3. 将证据写入 /report/evidence.md(pending)
4. 委派 report-writer 生成初稿(pending)
5. 校对引用并等待发布审批(pending)“研究 API”“完成报告”过于宽泛,UI 无法判断是否完成。计划也不应该写模型没有权限完成的动作;计划不是权限申请单。
可以在应用层限制计划规模:
function checkPlan(items: Array<{ content: string }>) {
if (items.length > 8) throw new Error("plan exceeds product limit");
if (items.some((item) => item.content.length > 240)) {
throw new Error("plan item is too verbose");
}
}4. 计划和确定性图的边界#
如果业务要求严格顺序、重试、事务或每一步审批,直接写 LangGraph 图:
collect_sources ──成功──> write_evidence ──成功──> draft
│ │ │
└─失败/重试 └─失败 └─审批Deep Agent 适合决定“搜索哪些角度”和“如何总结”;LangGraph 适合保证“证据写成功后才能进入草稿”。todo 可以作为用户可见计划,但不要把它误当成 DAG。
5. 子 Agent 的核心价值#
主 Agent 不应该亲自承接所有搜索、解析和校对。子 Agent 的主要价值是 context quarantine:子任务内部几十次工具调用不会全部塞回主上下文,主 Agent 只收到最终摘要。
import { createDeepAgent, type SubAgent } from "deepagents";
const researcher: SubAgent = {
name: "researcher",
description: "针对复杂问题搜索、交叉核验并返回带来源摘要。",
systemPrompt: `你是研究员。
先拆检索问题,再搜索和交叉核验。
只返回关键结论、证据摘要、URL和不确定项。
不要返回原始工具响应。`,
tools: [searchDocs],
};
const coordinator = createDeepAgent({
model,
subagents: [researcher],
systemPrompt: "你是研究协调员。复杂研究优先委派给 researcher。",
});默认是同步、隔离上下文的委派。子 Agent 只看到分配给它的任务,不能自动看到主对话中的隐含约束。
6. 委派合同#
一个可靠的委派合同应包含输入范围、允许工具、输出格式、失败策略和禁止事项:
const delegationPrompt = `
任务:核验下面的一个断言。
输入:{{claim}}
范围:只使用官方文档和公开来源。
输出:summary、confidence、sources、uncertainties。
禁止:修改文件、发送外部副作用、把未找到来源的内容写成事实。
`;description 解决“交给谁”,systemPrompt 解决“怎么做”,responseFormat 解决“如何解析”。三者缺一,委派很容易退化成自由发挥。
7. 结构化子 Agent 输出#
import { z } from "zod";
const Findings = z.object({
schemaVersion: z.literal("v1"),
summary: z.string(),
confidence: z.number().min(0).max(1),
sources: z.array(z.string().url()),
uncertainties: z.array(z.string()),
});
const factChecker: SubAgent = {
name: "fact-checker",
description: "核验报告事实并返回结构化结论。",
systemPrompt: "逐条核验断言,没有来源时写入 uncertainties。",
tools: [searchDocs],
responseFormat: Findings,
};schema 变更会影响父 Agent 和历史运行,因此增加 schemaVersion。父 Agent 解析失败时应标记子任务失败并重试或降级,不能把坏 JSON 当作正常结论。
8. 工具集与 Skills 继承#
子 Agent 只给完成任务所需的最小工具。研究员不需要邮件发送,邮件 Agent 不需要数据库删除。默认 general-purpose 子 Agent 会继承主 Agent 的 Skills;自定义子 Agent 默认不会继承,需要显式配置自己的 skills。
如果子 Agent 必须读主 Agent 的结果,优先通过文件路径和结构化摘要传递,而不是共享整段消息历史。这样能控制上下文大小和敏感数据扩散。
9. 异步任务不是未等待的 Promise#
同步 task 适合一次性隔离工作;长任务需要可取消、可观察和跨请求恢复的运行单元:
type ChildRun = {
id: string;
parentThreadId: string;
inputKey: string;
status: "queued" | "running" | "succeeded" | "failed" | "cancelled";
startedAt?: string;
finishedAt?: string;
error?: string;
};普通 Node 进程退出后,内存中的 Promise 不会自动继续。异步运行需要 checkpointer、队列或部署运行时,并要保存任务 ID、状态、超时、取消和重试信息。
10. fan-out 与动态子 Agent#
动态子 Agent 允许模型在 Interpreter 中用代码循环、分支和批量调用 task()。当前能力仍属于 beta,需要配置 subagents 和 createCodeInterpreterMiddleware():
import { createCodeInterpreterMiddleware } from "@langchain/quickjs";
const agent = createDeepAgent({
model,
middleware: [createCodeInterpreterMiddleware()],
subagents: [
{
name: "file-reviewer",
description: "审查一个文件,返回风险等级、行号和建议。",
systemPrompt: "只审查被分配的一个文件,不修改文件,不产生副作用。",
},
],
});“逐个”“批量”“汇总”是帮助模型选择动态编排的提示信号,不是可靠协议。如果验收要求每个文件必须执行一次,应用代码或 LangGraph 应负责枚举、限流、重试和结果归并。
11. 动态协作的工程约束#
每个工作项都应带 ID,子 Agent 原样返回 ID:
type WorkItem = { id: string; path: string; content: string };
type WorkOutput = { id: string; severity: string; findings: string[] };
function assertComplete(input: WorkItem[], output: WorkOutput[]) {
const expected = new Set(input.map((item) => item.id));
const actual = new Set(output.map((item) => item.id));
if (expected.size !== actual.size || [...expected].some((id) => !actual.has(id))) {
throw new Error("fan-out did not cover every work item");
}
}同时限制并发、单项超时、总预算和结果大小。共享 backend 写入时使用唯一文件,或由父 Agent 统一写入,避免覆盖。
12. 什么时候回到 LangGraph#
遇到以下要求时,直接使用确定性图或队列更稳:每项必须执行、固定顺序、失败重试两次、全部成功才发布、事务性审批、可预测的取消。Deep Agent 可以作为其中的智能节点,负责研究和解释,但不必替代所有编排。
本章练习与验收
- 给五个文件建立子 Agent 审查任务,故意让一个任务失败。
- 比较同步委派、动态 fan-out 和应用层确定性 fan-out。
- 检查是否漏项、重复 ID、结果错位和超预算。
- 给复杂任务加入
todoListMiddleware(),给短问题不生成计划。
验收标准:能说明 todo 与 DAG 的差别;子 Agent 工具最小且输出可解析;动态任务失败时不会汇报“全部完成”;需要强约束时知道回到 LangGraph。