2026 前端 AI Agent 工程化实战营系列第 16 篇

本文整理自《2026 前端 AI Agent 工程化实战营》课程资料,作为系列文章第 16 篇。


theme: channing-cyan

generated-image-1781105060139.png

本章demo地址:feat/deepagents

第十四章完成了 DeepAgent 的入门装配:通过需求分析系统的主干场景,验证了 write_todos、虚拟文件系统、task 子 Agent 与 Skills 注入等基础能力。那个示例仍然属于短链任务:一次需求分析、一份报告,不涉及执行恢复、长期记忆、人工审批或跨需求编排。

本章继续沿用同一条业务主线,将 DeepAgent 放到更接近生产环境的长链任务中讨论。核心场景是:将第九章的 createAnalysisGraph() 作为 DeepAgent 的 CompiledSubAgent 接入,由 DeepAgent 在外层负责多需求拆解、委派、文件暂存与最终汇总。

因此,本章不再停留在“如何创建一个 DeepAgent”,而是回答更工程化的问题:长链任务如何判定,内置中间件按什么顺序装配,消息压缩何时触发,虚拟文件系统如何持久化,已有 LangGraph 图如何接入,人工审批和模型运行配置应该放在运行链路的哪一层。

本章定位:第十四章解决 DeepAgent 的基本使用;本章解决 DeepAgent 在长链任务中的运行时设计、子 Agent 适配、上下文管理、持久化与工程边界。

本章验收点

版本与运行假设

本章新增代码统一放在 services/chat/src/llm/deepagent/,新增脚本 services/chat/scripts/run-deepagent-orchestrator-demo.ts,新增测试 services/chat/test/chapter15-deepagent-advanced.spec.ts。为保证第九章图能够作为子 Agent 运行,本章仅对第八/九章代码做最小兼容性修复(见 15.12.2),不改变既有业务流程。


15.1 什么样的任务属于「长链」

generated-image-1781105103757.png

不是「步骤多」就叫长链。一个执行 20 个步骤但每步都很短、上下文不持续增长的批处理,本质上仍然是短任务循环。长链任务的判定标准是下面这几条同时出现:

特征 含义 短任务为什么不需要
上下文超窗 中间产物会导致 messages 快速膨胀 短任务一轮就结束,不堆积
需要持久化 执行到一半要能暂停、恢复 短任务通常一次执行完成,失败后重新执行即可
需要委派与隔离 子任务上下文要和主回路隔开 短任务没有子任务
需要人工介入 敏感动作要等审批 短任务没有敏感写操作

三类典型的长链场景:

  1. 跨需求联合分析(本章主场景,接第九章 9.5):「评估 REQ-001/002/003 对核心系统的总体影响」。每个需求要单独深度分析,再汇总。
  2. 多轮研究对话:用户和 Agent 来回几十轮,逐步收敛一个调研结论,上下文持续增长。
  3. 数小时的 batch 任务:执行一批需求评估,中途可能需要暂停、换机器、继续执行。

回顾我们手写过的应对方式:

DeepAgent 做的事,就是把这些散落在各章的手写机制,抽象成统一的中间件层,让你不必每开一个长任务 Agent 就重写一遍。


15.2 拆开 DeepAgent:中间件装配全景

第十四章主要按默认装配使用 DeepAgent。本章要把它拆开:createDeepAgent 的能力,全部来自一组内置中间件,每个中间件在 LangGraph 的 state + node 之上注入工具、改写执行、管理状态。

15.2.1 完整装配顺序

createDeepAgent 内部按以下顺序装配中间件。理解这个顺序对调试和定制至关重要——中间件是有序的,后装的可以覆盖先装的行为:

序号 中间件 触发条件 职责
1 todoListMiddleware 始终装配 注入 write_todos 工具,维护任务计划
2 createSkillsMiddleware 传入 skills 参数 扫描 SKILL.md 目录,注入技能索引
3 createFilesystemMiddleware 始终装配 注入 ls/read_file/write_file/edit_file/glob/grep,加权限控制
4 createSubAgentMiddleware 始终装配 注入 task 工具,管理子 Agent 调度
5 createSummarizationMiddleware 始终装配 消息超阈值时自动压缩,不注入工具
6 createPatchToolCallsMiddleware 始终装配 修复跨模型的 tool_call / ToolMessage 配对问题
7 createAsyncSubAgentMiddleware subagents 中有 AsyncSubAgent 注入 5 个异步任务工具
8 用户自定义 middleware 传入 middleware 参数 开发者追加的业务中间件
9 Anthropic Cache Middleware 模型为 Anthropic 自动添加 cache_control
10 createMemoryMiddleware 传入 memory 参数 加载 AGENTS.md 到 system prompt
11 humanInTheLoopMiddleware 传入 interruptOn 参数 指定工具调用前中断
12 模型运行配置中间件 模型有匹配的 profile 模型级调优
13 工具排除中间件 profile 排除了某些工具 按 profile 隐藏工具
flowchart TD
  Model["BaseChatModel\n(gpt-5.4)"] --> Agent["createDeepAgent"]
  Agent --> M1["① todoListMiddleware\nwrite_todos"]
  Agent --> M2["② skillsMiddleware\nSKILL.md 注入(可选)"]
  Agent --> M3["③ filesystemMiddleware\nls/read/write/edit/glob/grep"]
  Agent --> M4["④ subAgentMiddleware\ntask 委派"]
  Agent --> M5["⑤ summarizationMiddleware\n上下文自动压缩"]
  Agent --> M6["⑥ patchToolCallsMiddleware\n跨模型兼容"]
  Agent --> M7["⑦~⑬ 按需中间件\nmemory/HITL/profile/..."]
  M1 --> Graph["LangGraph 运行时\nstate + reducer + node"]
  M3 --> Graph
  M4 --> Graph
  M5 --> Graph
  M7 --> Graph

1.png

15.2.2 每个中间件对应我们手写过的什么

中间件 提供的能力 对应前面哪一章
todoListMiddleware write_todos 规划 9.5 plannerNode 显式拆步骤
filesystemMiddleware 虚拟文件系统 6 个工具 第十章 Token 经济学 + 8.7 业务持久化
subAgentMiddleware task 委派 + 上下文隔离 9.2 Supervisor + 9.4 Handoff
summarizationMiddleware 消息历史自动压缩 第十章 摘要式记忆
patchToolCallsMiddleware 修复 Gemini 等模型的 tool_call 配对 手写时靠排查解决
skillsMiddleware 从 backend 加载 SKILL.md 第十三章 13.8
memoryMiddleware 跨会话注入 AGENTS.md 长期记忆 无直接对应,新能力
humanInTheLoopMiddleware(interruptOn) 节点级中断审批 8.7.2 interrupt()

15.2.3 关键认知:没有新原语

DeepAgent 没有发明新的图原语。它建立在第八章 8.2 讲过的 state / reducer / node 之上——所谓「中间件」,本质就是往图里注入工具 + 在节点前后挂钩子 + 在 state 上加 channel。每个中间件最多做三件事:

  1. 向 Agent 注入工具(如 write_todos、read_file)
  2. 在模型调用或工具调用前后插入逻辑(如 summarization 在模型调用前检查 token)
  3. 向 State 增加字段(如 todos、files、asyncTasks)

理解了这一点,你就知道:既然子 Agent 也只是 state 进、state 出的 Runnable,那也可以将第九章的图作为子 Agent 接入 DeepAgent。

内置工具名冲突检查

createDeepAgent 会检查你传入的 tools 是否和内置工具名冲突。以下名字不能作为业务工具名:

ls, read_file, write_file, edit_file, glob, grep, execute,
task, write_todos,
start_async_task, check_async_task, update_async_task,
cancel_async_task, list_async_tasks

如果传入同名工具,createDeepAgent 会抛出 ConfigurationError。


15.3 把第九章 createAnalysisGraph 接成子 Agent

这是本章的主轴,也是第十四章 Q4 留下的预告:把 createAnalysisGraph() 作为 DeepAgent 的子 Agent 接入。

15.3.1 关键决策:主图 vs 子 Agent

有两种接法:

我们走方案 A。架构如下:

flowchart TD
  User["用户长链任务\n评估 REQ-001/002/003 总体影响"] --> Deep["DeepAgent 外层编排\nwrite_todos + VFS + task"]
  Deep -->|"task('分析 REQ-001')"| Adapter["analysisSubagent 适配器\nRunnableLambda"]
  Adapter -->|"{ input, retrievedContext, messages }"| Graph["createAnalysisGraph\n第九章 Supervisor + 4 专家 + Critic-Refine"]
  Graph -->|"summary"| Adapter
  Adapter -->|"AIMessage(summary)"| Deep
  Deep --> VFS["VFS 落盘\n每个需求一个 .md"]
  Deep --> Report["主回路只看摘要\n汇总成总报告"]

15.3.2 三种子 Agent 类型的选择

DeepAgent 支持三种子 Agent 类型,选对类型是接入的前提:

类型 接口 运行方式 适用场景
SubAgent { name, description, systemPrompt, tools? } DeepAgent 内部自动组装 用 prompt + tools 定义的新 Agent
CompiledSubAgent { name, description, runnable } 传入已编译的 Runnable 接入已有的 LangGraph 图
AsyncSubAgent { name, description, graphId, url? } 远程 Agent 服务(Agent Protocol Server) 跨进程长时子任务

本章用的是 CompiledSubAgent——因为第九章的图已经编译好了,我们需要直接传入它的 runnable。

15.3.3 为什么不能直接把编译后的图作为子 Agent 使用

CompiledSubAgent 的结构很简单:

interface CompiledSubAgent {
  name: string;
  description: string;
  runnable: ReactAgent | Runnable;
}

一种直接做法是:既然 createAnalysisGraph(model) 返回的已经是编译后的 Runnable,是否可以直接传给 CompiledSubAgent.runnable。

结论是不建议直接使用,原因是两边的输入输出格式不一致。先看 deepagents 中 task 工具调用子 Agent 的方式(SDK 源码,已核验):

// 伪代码:task 工具内部
subagentState.messages = [new HumanMessage({ content: description })];
const result = await subagent.invoke(subagentState, config);
const content = result.messages[result.messages.length - 1].content;

也就是说,DeepAgent 调用子 Agent 时,输入和输出都围绕 messages 组织——它把委派描述写入 messages,再读最后一条消息当结果。

而第九章的分析图使用另一种输入格式:

await graph.invoke({ input: userInput, retrievedContext: '', messages: [] });

它靠 state.input(一个字符串)驱动 triage→extract→clarify→analysis/risk→summary,产出在 state.summary。它根本不读 messages 来决定做什么。

两边的输入输出格式不一致:如果直接使用,DeepAgent 写入 messages 的描述,该 LangGraph 图的 triage 节点读 state.input 时拿到的是空字符串。

技术原理:这是 DeepAgent 调用子 Agent 时的格式约束。task 工具统一使用 messages,是为了让主 Agent 和子 Agent 之间的输入输出形式保持一致,也与 LLM 的对话式交互模型一致。但现有的 LangGraph 图往往有自己的 state 输入格式(特别是自行定义 State 的图),因此需要适配层。

15.3.4 适配器:用 RunnableLambda 转换输入输出格式

解决办法是在中间加一层适配器,把 messages 输入转换成图需要的 state 输入,再把图的输出转换回 messages。这就是 services/chat/src/llm/deepagent/deep-orchestrator.service.ts 的核心:

import { type CompiledSubAgent } from 'deepagents';
import { RunnableLambda } from '@langchain/core/runnables';
import { AIMessage, type BaseMessage } from '@langchain/core/messages';
import { BaseChatModel } from '@langchain/core/language_models/chat_models';
import { createAnalysisGraph } from '../graph/requirement-analysis-graph';

export const ANALYSIS_SUBAGENT_NAME = 'requirement_analyst';

export function extractLatestUserText(messages: BaseMessage[]): string {
  for (let i = messages.length - 1; i >= 0; i--) {
    const m = messages[i];
    if (m.getType() === 'human') {
      return typeof m.content === 'string' ? m.content : JSON.stringify(m.content);
    }
  }
  const last = messages[messages.length - 1];
  return last ? (typeof last.content === 'string' ? last.content : JSON.stringify(last.content)) : '';
}

export function createAnalysisSubagent(model: BaseChatModel): CompiledSubAgent {
  const runnable = RunnableLambda.from(async (state: { messages: BaseMessage[] }, config) => {
    const userInput = extractLatestUserText(state.messages ?? []);   // messages → input
    const graph = createAnalysisGraph(model);
    const result = await graph.invoke(
      { input: userInput, retrievedContext: '', messages: [] },
      config,                                                          // ← 透传 config
    );
    const summary =
      result.summary || result.queryResponse || result.chatResponse || '(需求分析子 Agent 未产出内容)';
    return { messages: [new AIMessage(summary)] };                   // summary → messages
  });

  return {
    name: ANALYSIS_SUBAGENT_NAME,
    description:
      '需求分析专家。输入单个需求的描述文本,内部执行第九章 Supervisor + 4 专家并行 + Critic-Refine,返回该需求的综合分析摘要。',
    runnable: runnable as unknown as CompiledSubAgent['runnable'],
  };
}

三个细节值得停下来看:

  1. 为什么只回 { messages: [AIMessage(summary)] }? 因为 task 工具会把子 Agent 返回的 state 合并回主 Agent 的 state。如果把第九章那张图的全部字段(intent、analysisResult、各专家结论……)都返回,会把子 Agent 的中间状态带入 DeepAgent 的主 state。所以适配器只回一条摘要消息——这实现了 Handoff 的精神:主回路只看摘要,不看子 Agent 的中间过程。
  2. summary 的回退链 result.summary || result.queryResponse || result.chatResponse:第九章那张图按 intent 走不同分支(analyze 出 summary、query 出 queryResponse、chat 出 chatResponse),适配器把它们统一成一个「子 Agent 的最终回答」。
  3. config 透传:RunnableLambda 第二个参数 config 必须传给 graph.invoke(input, config)——否则父级的 callbacks / streamEvents / 追踪不会穿透到子 Agent,你在外层看到 task 委派那一步就「断」了,看不到子 Agent 内部的 LLM 调用。

15.3.5 组装外层 DeepAgent

有了子 Agent,外层编排就是常规的 createDeepAgent。完整代码在 services/chat/src/llm/deepagent/deep-orchestrator.service.ts:

import { createDeepAgent, FilesystemBackend, type DeepAgent, type FilesystemPermission } from 'deepagents';
import { type BaseCheckpointSaver } from '@langchain/langgraph';
import { DynamicStructuredTool } from '@langchain/core/tools';
import { z } from 'zod';

export const SAVE_REPORT_TOOL_NAME = 'save_report';

const ORCHESTRATOR_SYSTEM_PROMPT = `你是一个跨需求分析协调者。

工作方式:
1. 先用 write_todos 把任务拆成「逐个需求分析 + 最终汇总」的步骤。
2. 对每个需求,用 task 工具委派给 requirement_analyst 子 Agent 做深度分析。
3. 把每个需求的分析摘要用 write_file 落到单独的 .md 文件,主回路只保留摘要。
4. 全部需求分析完后,读取这些文件,输出一份「总体影响评估」报告。`;

export interface DeepOrchestratorOptions {
  model: BaseChatModel;
  rootDir?: string;
  checkpointer?: BaseCheckpointSaver;
  permissions?: FilesystemPermission[];
  interruptOn?: Record<string, boolean>;
}

export function createDeepOrchestrator(options: DeepOrchestratorOptions): DeepAgent {
  const { model, rootDir, checkpointer, permissions, interruptOn } = options;

  if (interruptOn && !checkpointer) {
    throw new Error('interruptOn 需要同时传入 checkpointer(HITL 依赖执行快照才能中断与恢复)。');
  }

  const saveReport = new DynamicStructuredTool({
    name: SAVE_REPORT_TOOL_NAME,
    description: '把最终的总体影响评估报告归档(敏感操作,可能需要人工审批)。',
    schema: z.object({
      title: z.string().describe('报告标题'),
      content: z.string().describe('报告正文'),
    }),
    func: async ({ title }) => `已归档报告:《${title}》`,
  });

  return createDeepAgent({
    model: model as never,
    tools: [saveReport] as never,
    subagents: [createAnalysisSubagent(model)],
    systemPrompt: ORCHESTRATOR_SYSTEM_PROMPT,
    ...(rootDir ? { backend: new FilesystemBackend({ rootDir, virtualMode: true }) } : {}),
    ...(checkpointer ? { checkpointer } : {}),
    ...(permissions ? { permissions } : {}),
    ...(interruptOn ? { interruptOn } : {}),
  }) as DeepAgent;
}

systemPrompt 的设计要点:显式写出期望的工作流步骤(先 write_todos、再 task、再 write_file、最后汇总)。DeepAgent 的内置 prompt 会引导模型使用这些内置工具,但业务 prompt 越清晰,模型越不容易偏离预期。

15.3.6 实跑与观测

脚本 scripts/run-deepagent-orchestrator-demo.ts 使用三个需求做端到端验证:

const agent = createDeepOrchestrator({ model });
const result = await agent.invoke({
  messages: [{ role: 'user', content:
    '我们要评估以下三个需求对核心系统的总体影响,请逐个分析后给出整体结论:\n' +
    '- REQ-001:支持企业微信扫码登录\n' +
    '- REQ-002:订单导出支持百万行级别的异步下载\n' +
    '- REQ-003:为后台操作增加细粒度的审计日志' }],
});

配套测试 test/chapter15-deepagent-advanced.spec.ts 的 Layer 2 用例(单需求,控制时长)运行后,得到的真实调用链是:

调用链: write_todos → task → write_todos → write_file → write_todos → read_file → write_todos
todos: 3 项
输出前 300 字: # 总体影响评估报告
## 评估对象
- 需求:REQ-001
- 需求:支持企业微信扫码登录
## 总体结论
影响等级:中高 ...

这条链清楚地展示了 DeepAgent 长链编排的形态:

⚠️ LLM 非确定性:上面的调用链是某次实跑的真实记录。即使 temperature: 0,模型每次的工具调用顺序也不完全可复现。稳定的模式是:通常先调用 write_todos、用 task 委派分析、用文件系统暂存事实、最后汇总;具体的步数和穿插顺序会有出入。

用 streamEvents 观测每一步

agent.invoke(...) 只在最后给你一个结果,只看最终结果时,很难了解中间执行过程。要实时看到每一步,把 invoke 换成 streamEvents(v2)。

脚本 scripts/run-deepagent-orchestrator-demo.ts 的完整实现:

const oneLine = (v: unknown, n = 100) =>
  String(typeof v === 'string' ? v : JSON.stringify(v) ?? '')
    .replace(/\s+/g, ' ')
    .slice(0, n);

function toolArgs(data: unknown): any {
  const raw = (data as any)?.input;
  const inner = raw && typeof raw === 'object' && 'input' in raw ? (raw as any).input : raw;
  if (typeof inner === 'string') {
    try { return JSON.parse(inner); } catch { return inner; }
  }
  return inner;
}

let inSubagent = 0;
const indent = () => '  '.repeat(inSubagent > 0 ? 1 : 0);

let llmCalls = 0;
let rootRunId: string | undefined;
let finalState: any = null;

for await (const ev of agent.streamEvents(
  { messages: [{ role: 'user', content: task }] },
  { version: 'v2' },
)) {
  if (!rootRunId && ev.event === 'on_chain_start') rootRunId = ev.run_id;

  switch (ev.event) {
    case 'on_chat_model_start': {
      llmCalls++;
      const m = (ev.metadata as any)?.ls_model_name || 'llm';
      console.log(`${indent()}🧠 [真实 LLM 调用 #${llmCalls}] 模型=${m}`);
      break;
    }
    case 'on_tool_start': {
      const args = toolArgs(ev.data);
      if (ev.name === 'task') {
        inSubagent++;
        console.log(`📂 委派子 Agent:${args?.subagent_type ?? ''} —${oneLine(args?.description, 60)}`);
      } else {
        console.log(`${indent()}🔧 工具:${ev.name}  入参=${oneLine(args, 80)}`);
      }
      break;
    }
    case 'on_tool_end': {
      const out = (ev.data as any)?.output;
      const content = typeof out?.content === 'string' ? out.content : out;
      console.log(`${indent()}   ✅ 返回:${oneLine(content, 80)}`);
      if (ev.name === 'task') inSubagent = Math.max(0, inSubagent - 1);
      break;
    }
    case 'on_chain_end': {
      if (ev.run_id === rootRunId) finalState = (ev.data as any)?.output;
      break;
    }
  }
}

实跑某次输出(节选):

🧠 [真实 LLM 调用 #1] 模型=gpt-5.4
🔧 工具:write_todos  入参={"todos":[{"content":"分析需求 REQ-001:支持企业微信扫码登录","status":"in_progress"}, ...]}
🧠 [真实 LLM 调用 #2] 模型=gpt-5.4
📂 委派子 Agent:requirement_analyst — 你是需求分析专家。请对以下单个需求做深度分析 ...
  🧠 [真实 LLM 调用 #3] 模型=gpt-5.4        ← 缩进的都是子 Agent 内部第九章图的调用
  🧠 [真实 LLM 调用 #4] 模型=gpt-5.4
  [Supervisor] 选中的专家:functional, security
  🔧 工具:check_security_policy  入参={"description":"REQ-001:支持企业微信扫码登录 ..."}
     ✅ 返回:安全策略检查 - REQ-001:⚠️ 涉及用户认证功能 ...
  ...

这条事件流可以回答三个关键问题:

技术技巧:toolArgs 函数处理了 LangGraph streamEvents 中工具入参的嵌套包装——事件的 data.input 有时是 JSON 字符串,有时是嵌套对象,这个函数统一解包。这是在不同 LangGraph 版本间保持兼容的实践模式。


15.4 自主规划:write_todos 的深层机制

第九章 9.5 使用 plannerNode 显式规划:通过结构化输出让模型产出 plan 数组,再由 executorNode 逐步执行。DeepAgent 则采用引导式规划:todoListMiddleware 注入 write_todos 工具,模型在面对复杂任务时可以主动调用它。

15.4.1 write_todos 的运行时行为

write_todos 在 state 上维护一个 todos channel(数组),每次调用时整体覆盖。模型通常在三个阶段调用它:

  1. 初始规划:把用户目标拆成多个可执行步骤,全部设为 pending,第一步设为 in_progress。
  2. 进度更新:完成一步后把状态改为 completed,并把下一步置为 in_progress。
  3. 计划调整:发现新信息后增加、删除或修改步骤。

对照 15.3.6 的实跑:我们没有写任何「先分析 REQ-001、再分析 REQ-002」的步骤代码,DeepAgent 自己产出了 3 项 todos。这就是「自主规划」——把规划的控制权交给模型。

15.4.2 自主规划 vs 显式 planner:选型标准

维度 自主规划(DeepAgent write_todos) 显式 planner(9.5 plannerNode)
步骤是否固定 不固定、随输入变化 固定流程、可枚举
是否需要审计每一步 不强制 需要(plan 是结构化数据,可存库、可回放)
是否要对 plan 做程序校验 难(todo 是半结构化文本) 易(plan 是 Zod schema 校验过的数组)
偏离预期时的可控性 靠 prompt 约束 靠代码降级处理
灵活性 高(模型可动态调整) 低(代码写死了分支)

经验法则:对外暴露、需要 SLA 和审计的流程用显式 planner;内部探索性、步骤不固定的长任务用自主规划。 两者也可以叠:外层 DeepAgent 自主规划,某一步委派给一张固定图——这正是 15.3 在做的事。

实践建议:提高 write_todos 触发稳定性

在实践中,write_todos 是否被触发高度依赖 system prompt 的措辞。以下技巧可以提高规划的可靠性:

  1. 显式提到工具名:在 system prompt 中直接说「先用 write_todos 把任务拆成步骤」比说「先制定计划」更可靠——模型更容易把文字描述映射到具体工具。
  2. 给出步骤模板:「步骤 1: 分析…、步骤 2: 评估…、步骤 3: 汇总…」这种模板会引导模型产出更结构化的 todos。
  3. 不要在业务 prompt 中与内置引导冲突:如果业务 prompt 写了「直接回答、不调用工具」,内置的规划引导就会被压制。
  4. 短任务不强求:天气查询这类任务不触发 write_todos 是正常行为,不要为了触发而强制。

15.5 上下文管理:Summarization × VFS × 超长内容截断(Token Eviction)

generated-image-1781105108725.png

长链任务首先遇到的限制通常是上下文窗口。DeepAgent 通过三条协同机制处理,每条解决不同层面的问题:

flowchart LR
  subgraph context [上下文管理三件套]
    VFS["① 虚拟文件系统\n主动外移:把事实从 messages 搬到 files"]
    SUM["② Summarization\n被动压缩:messages 超阈值时自动折叠"]
    EVICT["③ 超长内容截断(Token Eviction)\n工具结果过大时自动截断"]
  end

15.5.1 虚拟文件系统:主动的上下文外移

15.3 的实跑里 write_file / read_file 就是这条机制:每个需求的分析摘要落到一个 .md 文件,主 Agent 的 messages 里只保留「我把 REQ-001 的分析存到了 xxx.md」这种指针,而不是把几千字的分析正文全部放入上下文。等要汇总时再 read_file 取回。

配合关系一句话总结:思考链留在 messages,事实落到 files,接近窗口上限时压缩 messages。三者分工明确,互不替代。

15.5.2 Summarization 深度:触发模式与配置

createSummarizationMiddleware 是 DeepAgent 的被动降级处理。当 createDeepAgent 调用它时,默认只传入 { backend },触发参数由 computeSummarizationDefaults(model) 根据模型自动计算。

三种触发模式

Summarization 的触发和保留都用 ContextSize 类型描述:

type ContextSize = {
  type: "messages" | "tokens" | "fraction";
  value: number;
};
模式 含义 典型值 适用场景
messages 按消息条数 { type: "messages", value: 20 } 简单场景,不需要精确 token 计算
tokens 按 token 数 { type: "tokens", value: 170000 } 固定阈值,不依赖模型上下文窗口
fraction 按模型窗口的百分比 { type: "fraction", value: 0.85 } 自适应,随模型窗口大小自动调整

默认阈值如何计算

computeSummarizationDefaults(model) 的逻辑(SDK 源码,已核验):

技术原理:fraction 模式是最优选择——它自适应模型窗口大小。GPT-5.4 的窗口和 Claude Sonnet 的窗口不同,固定 170000 tokens 的阈值在一个模型上可能浪费空间、在另一个模型上可能太晚触发。fraction: 0.85 意味着「不管窗口多大,用到 85% 就开始压缩」。

Summarization 的执行流程

当消息历史达到 trigger 阈值时,中间件执行以下步骤:

  1. 切分:按 keep 保留最近的消息原文,其余标记为待压缩。
  2. 摘要生成:用当前模型对待压缩部分生成一段摘要(默认用 summaryPrompt 模板,最多 trimTokensToSummarize: 4000 tokens 的输入)。
  3. 历史归档:把被压缩的原始消息写入 backend 的 /conversation_history/{sessionId}.md(可通过 historyPathPrefix 配置)。
  4. 替换:在 messages 中用一条摘要 HumanMessage(标记 lc_source: "summarization")替换被压缩的历史。
  5. 修复配对:确保切分点不破坏 AI/Tool message 的配对完整性。

参数截断配置(TruncateArgsSettings)

Summarization 中间件还有一个常被忽略的功能:对旧消息中超长的工具参数自动截断。典型场景是 write_file 工具曾经写过一个 5000 字的文件——这个参数值留在历史消息里会持续消耗 token。

interface TruncateArgsSettings {
  trigger: ContextSize;     // 开始截断的阈值
  keep: ContextSize;        // 最近多少条消息的参数不截断
  maxLength: number;        // 默认 2000 字符
  truncationText: string;   // 默认 "...(argument truncated)"
}

默认行为:当历史超过阈值时,早期消息中的 write_file / edit_file 等大参数会被截成 2000 字符 + 省略标记。最近的消息不受影响——模型需要看到近期操作的完整参数。

15.5.3 超长内容截断(Token Eviction):文件系统层的自动截断

filesystemMiddleware 还有两个常被忽略的配置项:

参数 默认值 作用
toolTokenLimitBeforeEvict 20,000 tokens 工具返回结果超过此限时自动截断
humanMessageTokenLimitBeforeEvict 50,000 tokens 用户消息中内嵌的文件内容超过此限时截断

这意味着:如果 read_file 读回了一个超长文件,middleware 会在结果进入 messages 之前就截断,避免单次工具调用直接超过上下文窗口。

实践建议:这三条机制的调参没有万能公式。建议在你的真实任务上用 LangSmith(15.12.2)观测 token 曲线,看 summarization 触发了几次、VFS 写了多少次、大参数截断了多少。根据观测结果再决定是否需要调低 trigger 阈值或增加 toolTokenLimitBeforeEvict。


15.6 Backend 体系:从内存到磁盘到沙箱

虚拟文件系统的底层是 Backend——决定文件存在哪里、能不能执行命令。DeepAgent 提供了一个完整的 Backend 递进体系,从最简单的内存态到最复杂的远程沙箱。

15.6.1 Backend 一览

Backend 存储位置 支持 execute? 持久化? 适用场景
StateBackend LangGraph state(内存) 否 仅当有 checkpointer 默认后端,短任务
StoreBackend LangGraph BaseStore 否 跨 thread 持久化 需要跨会话共享文件
FilesystemBackend 真实磁盘 否 是 需要落盘或读取磁盘文件
CompositeBackend 按路径前缀路由 委托给子 backend 取决于子 backend 混合场景
LocalShellBackend 真实磁盘 + 本地 shell 是 是 需要执行命令
LangSmithSandbox 远程沙箱 是 否 安全隔离执行
flowchart LR
  State["StateBackend\n内存态\n执行结束即消失"] --> Store["StoreBackend\n跨 thread 持久化"]
  State --> FS["FilesystemBackend\n真实磁盘"]
  FS --> Composite["CompositeBackend\n路径路由"]
  FS --> Shell["LocalShellBackend\n磁盘 + shell"]
  Shell --> Sandbox["LangSmithSandbox\n远程沙箱"]

15.6.2 StateBackend(默认)

不传 backend 时,createDeepAgent 使用 StateBackend。文件存在 LangGraph 的 state 里,随图的执行存在。

15.6.3 FilesystemBackend(本章可本地验证)

new FilesystemBackend({ rootDir: '/tmp/deepagent-ch15', virtualMode: true })
参数 类型 默认 说明
rootDir string 无 虚拟根目录,/foo.md 落到 rootDir/foo.md
virtualMode boolean false 开启后禁止 ..、~ 越界
maxFileSizeMb number 无 单文件大小限制

virtualMode: true 是关键安全措施。不开虚拟模式时,绝对路径会按真实文件系统解析,容易写到 rootDir 之外。

本章 Layer 1 用例直接验证了跨实例读回:

it('一个实例写入、另一个实例读回,文件真实落盘', async () => {
  const root = mkdtempSync(join(tmpdir(), 'ch15-fs-'));
  const backend = new FilesystemBackend({ rootDir: root, virtualMode: true });
  await backend.write('/REQ-001.md', 'REQ-001 总体影响:高,涉及登录链路改造。');

  expect(existsSync(join(root, 'REQ-001.md'))).toBe(true);

  const reopened = new FilesystemBackend({ rootDir: root, virtualMode: true });
  const r = await reopened.read('/REQ-001.md');
  expect(String(r.content)).toContain('REQ-001');
});

15.6.4 CompositeBackend(路径路由)

当你需要「Skills 从磁盘读、工作文件存内存」这种混合场景时,CompositeBackend 把不同路径前缀路由到不同 backend:

// ⚠️ 示意代码:展示 CompositeBackend 的组合模式
import { CompositeBackend, StateBackend, FilesystemBackend } from 'deepagents';

const backend = new CompositeBackend(
  new StateBackend(),                                       // 默认:工作文件存内存
  { '/skills': new FilesystemBackend({ rootDir: SKILLS_DIR }) },  // /skills/** 从磁盘读
);

这种模式的价值:让 Agent 的工作文件(/analysis/REQ-001.md)走内存态(不落盘、不泄露),而 Skills(/skills/requirement-analysis/SKILL.md)从真实磁盘读取。

15.6.5 LocalShellBackend(本地命令执行)

继承 FilesystemBackend,额外支持 execute 工具——在宿主机上执行 shell 命令:

// ⚠️ 示意代码:需要谨慎使用,execute 不受 permissions 约束
import { LocalShellBackend } from 'deepagents';

const backend = LocalShellBackend.create({
  rootDir: '/tmp/agent-workspace',
  virtualMode: true,
  timeout: 120000,        // 命令超时 120 秒
  maxOutputBytes: 100000, // 输出最大 100KB
});

安全警告:execute 工具不受 permissions 约束——它直接在宿主机上执行命令。生产环境应优先用 LangSmithSandbox 或自定义 BaseSandbox 子类来隔离执行。

15.6.6 LangSmithSandbox(远程沙箱)

通过 LangSmith Sandbox API 在隔离环境中执行。适合生产环境中需要 Agent 执行代码但不能信任宿主机安全的场景:

// ⚠️ 示意代码:需要 LangSmith 凭证和 Sandbox 服务
import { LangSmithSandbox } from 'deepagents';

const sandbox = await LangSmithSandbox.create({
  templateName: 'python-3.11',
  defaultTimeout: 30000,
});

本仓库未启用 LocalShellBackend 和 LangSmithSandbox。本章只使用 StateBackend 和 FilesystemBackend 做本地验证。

选择建议:如何选择 Backend

你的需求 推荐 Backend
最简单、不需要持久化 StateBackend(默认)
需要从磁盘读 Skills FilesystemBackend
需要文件跨进程可读回 FilesystemBackend + virtualMode: true
需要跨会话共享文件 StoreBackend
Skills 从磁盘读 + 工作文件存内存 CompositeBackend
需要 Agent 执行命令(可信环境) LocalShellBackend
需要 Agent 执行命令(不可信环境) LangSmithSandbox


15.7 长任务持久化:三层模型

长任务要能「停下来、换机器、续上跑」,靠的是持久化。要区分三个不同的层,它们解决不同的问题、互不替代:

flowchart LR
  subgraph persist [长任务持久化三层]
    CP["1. 执行快照\nCheckpointer\n(Agent 执行到哪一步)"]
    ST["2. 跨会话记忆\nStore\n(跨 thread 的长期事实)"]
    FS["3. 文件\nFilesystemBackend\n(中间产物 / 报告)"]
  end

层 存什么 用什么 跨进程? 跨 thread?
执行快照 Agent 当前的完整 state(节点位置、messages、todos、files) checkpointer(MemorySaver / PostgresSaver) 取决于后端 否
跨会话记忆 用户偏好、长期事实、AGENTS.md store(BaseStore) + memory 参数 取决于后端 是
文件 中间产物、报告、分析结果 FilesystemBackend 是 是

15.7.1 本地验证与生产持久化边界

createDeepOrchestrator 接受 checkpointer 和 rootDir:

生产持久化示意(本仓库未启用)

生产环境的「跨进程、可恢复」持久化通常需要切换到数据库后端。下面是架构示意:

// ⚠️ 示意代码:需自备 Postgres,本仓库未启用、未做端到端验证
import { PostgresSaver } from '@langchain/langgraph-checkpoint-postgres';

const checkpointer = PostgresSaver.fromConnString(process.env.PG_URL!);
await checkpointer.setup();

const agent = createDeepOrchestrator({ model, checkpointer });

⚠️ 数据流边界声明

启用 PostgresSaver 会新增 LangGraph 自带的表(checkpoints、checkpoint_writes),这属于数据模型变更。它和第八章 8.7.1 的业务表是两套东西,互不替代:

  • 业务层 conversations / messages 表:存「用户视角的对话」。
  • DeepAgent Checkpointer:存「Agent 视角的执行状态」(执行到哪个节点、中间 state)。

是否在生产启用 Postgres 持久化,需要结合部署方式、恢复目标、数据治理和运维成本统一评估,不应默认开启。


15.8 Memory 中间件:跨会话的长期记忆

generated-image-1781105108967.png

第十四章提到了 MemoryMiddleware 但留到本章展开。它解决的问题和 Summarization 不同:

维度 SummarizationMiddleware MemoryMiddleware
作用范围 当前会话(当前 thread) 跨会话(跨 thread)
解决什么 当前对话太长,需要压缩 新会话缺少上下文,需要注入背景知识
数据来源 自动压缩消息历史 从文件(AGENTS.md)加载
注入位置 替换旧消息 拼入 system prompt

15.8.1 使用方式

createDeepAgent 的 memory 参数是一个便捷入口:

const agent = createDeepAgent({
  model,
  tools: [...],
  memory: ['./AGENTS.md', '~/.deepagents/AGENTS.md'],  // 多个来源,按序合并
  backend: new FilesystemBackend({ rootDir: '/' }),     // 必须能读到文件
});

等效于手动装配 createMemoryMiddleware:

createMemoryMiddleware({
  backend,
  sources: ['./AGENTS.md', '~/.deepagents/AGENTS.md'],
  addCacheControl: true,  // Anthropic 模型时自动加 cache_control
})

15.8.2 AGENTS.md 模式

memory 的典型用法是加载 AGENTS.md——一种在 AI 编码工具中流行的模式。AGENTS.md 文件包含项目级别的规范、约束和上下文,让每次新会话都能「记住」项目背景:

# AGENTS.md
## 项目背景
这是一个需求分析系统,使用 NestJS + LangGraph 构建。

## 编码约束
-所有分析报告必须包含风险评估
-复杂度估算使用 T-shirt size(S/M/L/XL)
-新功能不得修改第八/九章既有业务逻辑

Memory 中间件会在 Agent 启动时通过 backend 读取这些文件,将内容拼入 system prompt 的前段。

15.8.3 与 Skills 的区别

维度 Memory(AGENTS.md) Skills(SKILL.md)
加载时机 Agent 启动时立即加载 按需加载(模型判断需要时才读)
内容性质 全局背景和约束 特定领域的方法论
上下文影响 始终占用 system prompt 空间 只在使用时进入上下文
适用场景 项目级规范、团队约定 需求分析方法、竞品调研流程

实践建议:AGENTS.md 适合放「始终需要知道的背景」(项目架构、编码约束、安全红线);SKILL.md 适合放「按需调用的方法论」(需求分析步骤、竞品调研流程)。前者小而精(几百字),后者可以更详细(因为按需加载不浪费 token)。


15.9 HITL:在敏感节点等审批

generated-image-1781105109718.png

第八章 8.7.2 我们用 interrupt() + checkpointer 实现「执行到敏感节点停下来等用户」。DeepAgent 把这件事做成了声明式的 interruptOn:

import { MemorySaver } from '@langchain/langgraph';

const agent = createDeepOrchestrator({
  model,
  checkpointer: new MemorySaver(),     // HITL 必须配 checkpointer
  interruptOn: { save_report: true },  // 调用 save_report 前暂停,等人工审批
});

15.9.1 机制原理

底层和第八章完全一样:interruptOn 只是 humanInTheLoopMiddleware 的声明式包装,它在指定工具执行前触发中断,把控制权交回调用方;调用方审批后用同一个 thread_id 恢复执行。

这就是为什么 createDeepOrchestrator 里有那道守卫:

if (interruptOn && !checkpointer) {
  throw new Error('interruptOn 需要同时传入 checkpointer(HITL 依赖执行快照才能中断与恢复)。');
}

中断/恢复要靠执行快照(checkpointer)才能记住「停在哪、state 是什么」。这道守卫由 Layer 1 用例确定性验证:

it('interruptOn 缺少 checkpointer 时抛错', () => {
  expect(() =>
    createDeepOrchestrator({ model: makeStubModel(), interruptOn: { save_report: true } }),
  ).toThrow(/checkpointer/);
});

补充:interruptOn 的高级配置

除了简单的 { toolName: true } 布尔值,interruptOn 还支持更细粒度的配置:

interruptOn: Record<string, boolean | InterruptOnConfig>

InterruptOnConfig 可以控制中断的条件(如只在特定参数值时中断),具体接口随 deepagents 版本变化。

15.9.3 典型 HITL 场景

应该上 HITL 的工具:

判断标准:做了就不可逆的动作。write_file 到 VFS 通常不需要 HITL(VFS 是临时的、可覆盖的),但 save_report 到数据库就需要。


15.10 异步 subagent(示意,需要远程 Agent 服务(Agent Protocol Server))

第十四章 14.8 提过:DeepAgent 的 task 目前是串行的——主 Agent 委派一个子任务,要等它跑完才继续。对于「让几个专家在后台并行跑数分钟」的场景,DeepAgent 提供了异步 subagent。

15.10.1 接口与工具

异步子 Agent 用 AsyncSubAgent 接口声明:

interface AsyncSubAgent {
  name: string;
  description: string;
  graphId: string;        // Agent Protocol server 上的图/assistant ID
  url?: string;           // Agent Protocol server 地址
  headers?: Record<string, string>;
}

当 subagents 数组中存在 AsyncSubAgent 时,createAsyncSubAgentMiddleware 会自动装配,注入 5 个工具:

工具 用途
start_async_task 启动后台子任务
check_async_task 查询子任务状态
update_async_task 追加信息给子任务
cancel_async_task 取消子任务
list_async_tasks 列出所有子任务

和第九章 9.3 的并行分发相比,异步 subagent 的并行可跨进程、可恢复、可被主 Agent 主动 cancel。

判断标准:isAsyncSubAgent(subAgent) 通过检查 graphId 字段来区分同步和异步子 Agent。

运行边界:为什么本章只做架构说明

AsyncSubAgent 必须指定 graphId 并连到一个运行中的远程 Agent 服务(Agent Protocol Server)(LangGraph Platform / langgraph dev 起的服务)。它不是进程内 Runnable,无法像 15.3 的同步子 Agent 那样在本地直接跑通。

实务建议:除非你确实有「子任务要执行很久、需要跨进程调度」的硬需求,否先用 15.3 的同步 CompiledSubAgent——它可以在本地运行、测试和调试,接入复杂度更低。


15.11 模型运行配置:根据模型调整运行时行为

generated-image-1781105110784.png

DeepAgent 内置了模型运行配置机制——根据不同的模型自动调整运行时行为。这是一个常被忽略但在多模型部署中非常有用的能力。

15.11.1 什么是模型运行配置

不同模型在工具调用、system prompt 遵循度、结构化输出稳定性上差异很大。模型运行配置让 DeepAgent 针对每个模型系列预置最佳的运行时配置:

interface HarnessProfile {
  systemPromptSuffix?: string;           // 追加到 system prompt 的模型特定指令
  excludedTools?: string[];              // 对该模型隐藏的工具
  excludedMiddleware?: string[];         // 对该模型跳过的中间件
  generalPurposeSubagent?: { enabled: boolean };  // 是否启用通用子 Agent
  extraMiddleware?: AgentMiddleware[];   // 模型特定的额外中间件
  maxInputTokens?: number;              // 模型最大输入 token,影响 summarization 阈值
}

15.11.2 内置 Profile

DeepAgent 当前为以下模型系列提供内置 profile(已核验):

模型系列 关键调整
Anthropic Claude(Opus 4.7 / Sonnet 4.6 / Haiku 4.5) 自动添加 cache_control、调整 summarization 阈值
OpenAI Codex 调整工具调用格式和 system prompt

15.11.3 自定义 Profile

如果你用的是非内置模型(如自部署的开源模型),可以注册自定义 profile:

import { registerHarnessProfile } from 'deepagents';

registerHarnessProfile('my-provider:my-model', {
  maxInputTokens: 32000,
  systemPromptSuffix: '工具调用必须严格遵循 JSON schema,不要省略必填字段。',
  excludedTools: ['execute'],
});

技术原理:Profile 在 createDeepAgent 内部被查找并合并。maxInputTokens 直接影响 computeSummarizationDefaults 的 fraction 计算——如果你的自定义模型窗口是 32K,不设这个值,summarization 会按 170K 的默认阈值来算,几乎永远不触发,最终撞上窗口限制报错。

常见问题场景

在实际项目中,你可能会遇到以下场景:

  1. 换模型后 Agent 行为突变:从 GPT-5.4 换到 Claude Sonnet 后,write_todos 不再被触发。原因可能是 profile 里的 systemPromptSuffix 对 Claude 有不同的引导策略。
  2. Summarization 时机不对:模型窗口明明是 128K,但 summarization 在 170K 才触发——因为 fallback profile 不知道你的模型窗口大小。
  3. 工具调用格式错误:某些模型的 tool calling 返回格式和 OpenAI 不一致,createPatchToolCallsMiddleware 就是为了修复这类跨模型兼容问题。

15.12 工程化补齐

15.12.1 权限(本章可本地验证)

createDeepAgent 接受 permissions,作用于文件系统的 ls/read_file/write_file/edit_file/glob/grep,规则按声明顺序匹配、首个命中生效、默认放行:

const agent = createDeepOrchestrator({
  model,
  rootDir: '/tmp/deepagent-ch15',
  permissions: [
    { operations: ['write'], paths: ['/readonly/**'], mode: 'deny' },
  ],
});

FilesystemPermission 的完整形状(已核验):

type FilesystemOperation = "read" | "write";
type PermissionMode = "allow" | "deny";

interface FilesystemPermission {
  operations: readonly FilesystemOperation[];
  paths: string[];       // 必须以 / 开头的绝对 glob,支持 ** / * / {a,b}
  mode?: PermissionMode; // 默认 "allow"
}

重要约束:

  1. paths 必须是以 / 开头的绝对路径 glob,不支持 .. 和 ~。
  2. execute 工具不受 permissions 约束——shell 命令可以绕过路径规则。这是 LocalShellBackend 和 LangSmithSandbox 的安全考量之一。
  3. 默认行为是放行——只有显式 deny 的路径才被阻止。
  4. 子 Agent 会继承主 Agent 的 permissions,除非子 Agent(SubAgent)声明了自己的 permissions。

15.12.2 失败降级:本章遇到的问题

长链任务中,任何一步的依赖都可能失败:子 Agent 报错、结构化输出解析失败、工具超时。工程原则是:依赖失败应转化为可观测、可分类、可恢复的降级结果;不能静默忽略,也不能让单点失败中断整条长链。

本章接入第九章分析图时,实际遇到了这个问题。createAnalysisGraph 在当前模型/网关下无法稳定运行,有两类故障:

  1. 并行通道写冲突:第九章 9.3 的 4 个专家子图并行执行,每个都会把 input 等透传字段回写。这些字段原本是 LastValue 通道,不允许同一步多次写入,于是抛 InvalidUpdateError。
  2. 结构化输出解析报错:triageNode / supervisorNode 用 withStructuredOutput,但模型经网关返回的 JSON 不规范,触发 ZodError。

为了让第九章的分析图能够运行通过,本章对第八/九章做了最小外科手术式修复:

// 修复 1:给并行分支会回写的通道加 keep-last reducer,容忍同一步多次写入
input: Annotation<string>({ reducer: (_, newValue) => newValue }),

// 修复 2:结构化输出降级——解析失败时退回安全默认值
let result: z.infer<typeof triageSchema>;
try {
  result = (await structured.invoke([...])) as z.infer<typeof triageSchema>;
} catch (err) {
  console.warn('[triage] 结构化输出解析失败,降级为 analyze:', String(err).slice(0, 120));
  result = { action: 'handoff_to_analysis', response: null, reason: null };
}

这里可以看到失败降级的基本做法:

15.12.3 子 Agent 失败隔离

适配器层(15.3.4)也遵循失败降级思路。createAnalysisSubagent 的 RunnableLambda 内部,如果 graph.invoke() 抛异常,异常会通过 task 工具回传给主 Agent——主 Agent 收到的是一条包含错误信息的 ToolMessage,而不是进程级崩溃。主 Agent 可以据此决定是重试、跳过还是换策略。

这和第十四章 14.11 Q7 提到的机制一致:task 在独立上下文中执行,子 Agent 的失败以结果摘要的形式返回主 Agent,不会把子 Agent 的中间状态直接写入主 Agent 上下文。

15.12.4 createPatchToolCallsMiddleware:跨模型兼容

这个不起眼但始终装配的中间件解决一个普遍问题:不同模型返回的 tool_call 格式和配对关系不一致。典型症状:

createPatchToolCallsMiddleware 在每次模型调用前后自动修复这些不一致。你通常不需要关心它,但如果换模型后遇到 tool_call 相关的异常行为,知道有这个中间件在做修复,有助于定位问题。


15.13 createDeepAgent 参数完整速查

第十四章 14.3.1 给出了常用参数。本节补全完整列表(基于 deepagents@1.10.2,已核验):

参数 类型 默认值 作用
model BaseLanguageModel | string "anthropic:claude-sonnet-4-6" 底层推理模型
tools Tool[] [] 业务工具,不能与内置工具名冲突
systemPrompt string | SystemMessage 与内置 prompt 组合(不覆盖) 业务提示词
subagents (SubAgent | CompiledSubAgent | AsyncSubAgent)[] [] 子 Agent 声明
backend BackendProtocol | factory StateBackend 文件系统后端
skills string[] — SKILL.md 目录路径
memory string[] — AGENTS.md 文件路径
checkpointer BaseCheckpointSaver | boolean — 持久化与断点续跑
store BaseStore — 跨 thread 长期存储
permissions FilesystemPermission[] — 文件系统权限规则
interruptOn Record<string, boolean | InterruptOnConfig> — HITL 审批
middleware AgentMiddleware[] [] 自定义中间件(追加在内置之后)
name string — Agent 名称元数据
responseFormat SupportedResponseFormat — 结构化输出配置
contextSchema InteropZodObject — 按调用传入的上下文 schema
streamTransformers StreamTransformer[] — 自定义流式投影(v3 streaming)

关键提醒:systemPrompt 与内置 prompt 是组合关系,不是覆盖。DeepAgent 先提供引导模型使用规划、文件系统和子 Agent 的内置基础 prompt,再拼接你的业务 prompt。


15.14 何时用 DeepAgent,何时不用

沿用第十三章 13.10 的写法,给一张选型对照表:

场景 推荐方案 理由
单一固定流程的需求分析 第八章 LangGraph StateGraph 步骤固定、要精确控制每个节点
已知专家集 + 固定并行分发 第九章 Multi-Agent 并行粒度可控、需要精确汇合
跨需求联合分析 + 自动规划 本章 DeepAgent 步骤不固定、需要规划 + VFS + 委派
长上下文研究对话 本章 DeepAgent 需要 summarization + VFS
简单工具调用 createReactAgent 一两步工具调用不需要规划
仅做 RAG 问答 第十一章 不需要 Agent 运行时
仅作工具接入层 第十二章 MCP 不需要规划和上下文管理

一句话:步骤固定、要审计、要精确控制 → LangGraph;步骤不固定、需要规划 + 上下文管理 + 动态委派 → DeepAgent。 而且两者能组合——本章的主轴就是「DeepAgent 做外层,LangGraph 图做子 Agent 内核」。

组合模式的价值

15.3 的组合模式(DeepAgent + LangGraph 图)不是本章特有的技巧,它是一种通用的架构思路:

外层:动态编排(DeepAgent)
  → 步骤 1: write_todos 规划
  → 步骤 2: task 委派给固定图
  → 步骤 3: VFS 暂存中间结果
  → 步骤 4: 汇总

内层:精确控制(LangGraph 图)
  → 固定节点:Supervisor → 专家 → Critic → Refine
  → 固定并行:4 个专家同时跑
  → 固定质量闭环:Critic-Refine 最多 3 轮

这种分层的好处:

  1. 外层容许变化:用户要求分析 3 个需求还是 10 个,DeepAgent 自己规划,不需要改代码。
  2. 内层保证质量:每个需求的深度分析走固定流程,有 Critic-Refine 闭环保障,不会因为模型自由发挥而跳过重要步骤。
  3. 可独立测试:外层(orchestrator)和内层(analysis graph)各有独立的测试用例,修改一边不影响另一边。

能力金字塔

把全书的 Agent 能力栈放在一张图里收束:

flowchart TD
  Deep["DeepAgent(运行时框架)\n把下面四层组织成长链任务"] --> Skills["Skills(方法资产)\n第十三章"]
  Deep --> Agents["Agents(编排)\n第八/九章 LangGraph"]
  Skills --> RAG["RAG(知识)\n第十一章"]
  Agents --> RAG
  Agents --> MCP["MCP(工具接入层)\n第十二章"]
  RAG --> Model["Model(基座模型)"]
  MCP --> Model

DeepAgent 是把前面四层(MCP / RAG / Agents / Skills)组织起来的运行时框架,本身不引入新的能力维度,只引入新的「打包方式」:

理解了这一点,你看任何「Agent 框架」都能快速定位它在能力栈中的哪一层——是在某一层引入了新能力,还是在做上层的打包与编排。


15.15 常见问题(FAQ)

Q1:第九章的 createAnalysisGraph 还有意义吗?

有,而且本章正是它的最佳使用姿势。固定流程、要精确控制的部分(Supervisor 选专家、4 专家并行、Critic-Refine 闭环)继续用图来表达;动态的、跨需求的外层编排交给 DeepAgent。图作为 DeepAgent 的子 Agent 被复用,而不是被替代。

Q2:DeepAgent Checkpointer 和业务层 conversations 表会不会数据冗余?

不会,它们存的是不同视角的状态(见 15.7 边界声明)。业务表存「用户看到的对话」,Checkpointer 存「Agent 的执行状态」。两者可以并存,也可以只用其一。

Q3:异步 subagent 必须部署 LangGraph 服务吗?

是。AsyncSubAgent 依赖 graphId + 运行中的远程 Agent 服务(Agent Protocol Server)(见 15.10)。如果你只是想并行并行执行几个子任务且能接受同进程,优先用 15.3 的同步 CompiledSubAgent。

Q4:自动 summarization 会不会丢关键事实?

会有这个风险,所以本章强调「事实落 files、思考留 messages」(15.5)。关键事实写进虚拟文件系统后,即使 messages 被压缩,事实仍可通过 read_file 取回,不依赖未被压缩的对话历史。

Q5:怎么调试 DeepAgent 不按预期拆任务的问题?

三步:① 检查 systemPrompt 是否清楚地描述了期望的工作流(15.3.5 的 ORCHESTRATOR_SYSTEM_PROMPT 就是显式写了「先 write_todos、再 task、再 write_file、最后汇总」);② 开 LangSmith trace(15.12.2)看实际的委派树;③ 如果拆解必须可控、可审计,退回第九章 9.5 的显式 planner(15.4.2 的判断标准)。

Q6:为什么把第九章的图接成子 Agent 需要一个适配器,不能直接传?

因为 DeepAgent 的 task 工具围绕 messages 调用子 Agent,而 createAnalysisGraph 依赖 state.input 字符串驱动、产出在 state.summary。两边的输入输出格式不同,因此需要用 RunnableLambda 做一次转换(15.3.3 / 15.3.4)。

Q7:本章对第八/九章改了什么?会影响我已运行通过的旧代码吗?

只做了两类最小修复:给并行回写的 state 通道加 keep-last reducer;给 triageNode / supervisorNode 的结构化输出加解析失败降级(见 15.12.2)。修复后第九章 49 个 mock 单元测试全部通过,无回归。

Q8:memory 参数和 skills 参数有什么区别?

memory 在 Agent 启动时立即加载所有内容到 system prompt,适合全局背景和约束(如 AGENTS.md);skills 只把 name/description 放进索引,内容按需读取,适合特定领域的方法论(如 SKILL.md)。详见 15.8.3。

Q9:Summarization 是用当前模型还是单独模型来做摘要?

默认用当前模型。createSummarizationMiddleware 接受 model 参数可以覆盖,用一个更小、更快的模型做摘要(如用 GPT-4o-mini 做摘要、GPT-5.4 做主推理),但本章未做端到端验证这种配置。

Q10:permissions 对 execute 工具有效吗?

没有。execute 工具(由 LocalShellBackend / LangSmithSandbox 提供)不受 permissions 约束。这是设计限制——shell 命令可以做任何事,路径级别的权限约束没有意义。安全隔离应该在 Backend 层(沙箱)而不是 permissions 层解决。


15.16 术语速查表

术语 一句话解释
长链任务 上下文超窗 + 需持久化 + 需委派隔离 + 需人工介入的任务
CompiledSubAgent 接受已编译 Runnable 的子 Agent 接口,适合接入现有 LangGraph 图
AsyncSubAgent 连接远程 Agent 服务(Agent Protocol Server) 的异步子 Agent,通过 graphId 标识
ContextSize Summarization 的度量单位,支持 messages / tokens / fraction 三种模式
TruncateArgsSettings 旧消息中超长工具参数的自动截断配置
StateBackend 默认内存态文件系统后端,文件随当前 Agent 执行存在
FilesystemBackend 基于真实文件系统的后端,virtualMode 防止路径越界
CompositeBackend 按路径前缀路由到不同 Backend 的组合后端
LocalShellBackend 磁盘 + 本地 shell,支持 execute 工具
LangSmithSandbox 远程沙箱后端,安全隔离执行
MemoryMiddleware 从 AGENTS.md 等文件加载跨会话背景知识的中间件
HarnessProfile 模型级运行时配置,自动调整 prompt、工具集和 summarization 阈值
patchToolCallsMiddleware 修复跨模型 tool_call/ToolMessage 配对问题的兼容层
超长内容截断(Token Eviction) 文件系统中间件对超长工具返回结果的自动截断机制
interruptOn 声明式 HITL 配置,指定工具调用前中断等待审批
输入输出适配器 用 RunnableLambda 在两种输入输出格式之间转换,例如把 messages 输入转换成目标 Runnable 需要的 state 输入
Runnable LangChain 的统一可执行接口,支持 invoke、stream、batch 等调用方式
RunnableLambda 将普通函数包装为 Runnable 的适配器,常用于转换不同输入输出格式
Checkpointer LangGraph 的执行快照组件,用于保存当前 thread 的 state 和节点位置
BaseStore LangGraph 的长期存储接口,用于跨 thread 保存共享事实或用户偏好
人工审批(HITL) 在敏感工具调用前中断执行,等待人工确认后再恢复
远程 Agent 服务(Agent Protocol Server) 用于托管远程 Agent / 图的服务端组件,异步 subagent 依赖它启动和查询后台任务
Reducer LangGraph State 中用于合并多次写入的函数,决定同一字段如何处理并发更新
LastValue 通道 LangGraph 默认的单值通道,同一步多次写入时会报冲突,需要 reducer 显式处理

15.17 本章小结

本章在第十四章的基础上,将 DeepAgent 从“最小可用”推进到“长链任务运行时”:

可以将本章的核心结论概括为三点:

  1. DeepAgent 适合外层动态编排:多需求、多步骤、上下文持续增长的任务,可以交给 DeepAgent 做规划、委派和中间产物管理。
  2. LangGraph 仍适合内层精确控制:固定节点、并行汇合、质量闭环、强审计流程,仍应由显式图结构承载。
  3. 生产化重点不在“能否调用模型”,而在运行时治理:持久化、压缩、权限、审批、失败降级和观测能力,决定长链任务能否稳定进入真实系统。

因此,DeepAgent 与 LangGraph 的关系不是替代,而是分层:DeepAgent 负责长链任务的外层 harness,LangGraph 负责可控业务流程的内层 graph。

后续章节预告

下一章进入可观测性主题。长链任务一旦引入规划、子 Agent、文件系统和多轮模型调用,单靠最终输出已经无法判断系统是否可靠。第十六章将围绕 LangSmith trace、事件流、token 统计、工具调用链和质量评估,回答“如何看见 Agent 正在做什么,以及如何基于观测结果优化它”。

写在最后

这里是言萧凡的 AI 编程实验室。本系列持续记录 AI 工具、编程实践与可复用的工程方法,尽量同时覆盖概念、代码和验证路径,帮助读者在真实项目中完成探索、实践与沉淀。

欢迎通过微信号【Cookieboty】交流。