流式传输
从 Deep Agent 运行和 Subagent 执行中流式传输实时更新
从"等结果"到"看过程"——实时掌握 Agent 的每一步为什么需要它
你有没有调过一个跑了好几分钟的 Agent,全程盯着黑屏等结果?
不用流式传输的困境
- Agent 跑了 30 秒,完全不知道在干什么
- 多个 Subagent 并行执行,看不到各自的进度
- LLM 回复要等全部生成完才能显示,用户体验差
- 工具调用黑箱操作,调试时无从追踪
- 出了问题只能看最终输出猜原因,没有实时诊断能力
流式传输的解法
- 实时追踪每个 Subagent 的执行步骤
- 逐 token 输出 LLM 回复,像 ChatGPT 一样打字
- 流式传输工具调用和结果,每一步透明可见
- 自定义进度事件,精准控制 UI 反馈
- 组合多种流模式,获取完整执行视图
四大流式能力
Deep Agent 流式传输可以做到的四种实时更新
Subagent 进度
跟踪每个 Subagent 并行运行时的执行情况。
用途:显示哪些 Subagent 处于活跃状态,它们各自完成了哪些工作。
streamMode: "updates"
LLM Token
从主 Agent 和每个 Subagent 逐 token 流式输出。
用途:像 ChatGPT 一样实时显示生成中的文本。
streamMode: "messages"
工具调用
流式传输 Subagent 执行中的工具调用和结果。
用途:查看 Agent 调了什么工具、传了什么参数、返回了什么。
streamMode: "messages"
自定义更新
从 Subagent 工具内部发出用户定义的进度信号。
用途:实现业务相关的进度反馈(百分比、状态文案等)。
streamMode: "custom"
启用子图流式
一行配置打开 Subagent 事件的大门
Deep Agents 基于 LangGraph 的流式传输基础设施构建。要接收 Subagent 事件,关键就是在调用 stream() 时启用 subgraphs: true。
import { createDeepAgent } from "deepagents";
const agent = createDeepAgent({
systemPrompt: "You are a helpful research assistant",
subagents: [
{
name: "researcher",
description: "Researches a topic in depth",
systemPrompt: "You are a thorough researcher.",
},
],
});
for await (const [namespace, chunk] of await agent.stream(
{ messages: [{ role: "user", content: "Research quantum computing" }] },
{
streamMode: "updates", // 每个步骤完成时推送事件
subgraphs: true, // 关键开关:启用后才能收到子代理事件
}
)) {
// namespace 是字符串数组:空数组=主 Agent,含 "tools:" 前缀=子代理
if (namespace.length > 0) {
// Subagent 事件 —— namespace 标识了事件来源
console.log(`[subagent: ${namespace.join("|")}]`);
} else {
// 主 Agent 事件
console.log("[main agent]");
}
console.log(chunk);
}
subgraphs: true 是开关。没有它,你只能看到主 Agent 的事件;加上它,每个 Subagent 的内部事件都会被推送到你的流里。
理解命名空间
每个事件都带"身份证",告诉你它是谁发的
启用 subgraphs 后,每个事件都包含一个命名空间(namespace),用于标识是哪个 Agent 生成了该事件。命名空间是由节点名称和任务 ID 组成的路径。
| 命名空间 | 来源 |
|---|---|
()(空数组) | 主 Agent 的事件 |
("tools:abc123",) | 主 Agent 的 task 工具调用 abc123 派生的 Subagent |
("tools:abc123", "model_request:def456") | Subagent 内部的模型请求节点 |
for await (const [namespace, chunk] of await agent.stream(
{ messages: [{ role: "user", content: "Plan my vacation" }] },
{ streamMode: "updates", subgraphs: true }
)) {
// 检查 namespace 中是否含 "tools:" 前缀的段
const isSubagent = namespace.some(
(segment) => segment.startsWith("tools:")
);
if (isSubagent) {
// 从命名空间中提取工具调用 ID(如 "tools:call_abc123" 中的 "call_abc123")
const toolCallId = namespace
.find((s) => s.startsWith("tools:"))
?.split(":")[1];
console.log(`Subagent ${toolCallId}:`, chunk);
} else {
console.log("Main agent:", chunk);
}
}
namespace.length === 0 表示主 Agent,namespace[0].startsWith("tools:") 表示 Subagent。不要拿整个数组做字符串匹配。
Subagent 进度
用 streamMode: "updates" 跟踪每个步骤的完成情况
stream_mode="updates" 在每个步骤完成时推送事件。主 Agent 和 Subagent 的事件可以通过命名空间长度区分。
import { createDeepAgent } from "deepagents";
const agent = createDeepAgent({
systemPrompt:
"You are a project coordinator. Always delegate research tasks " +
"to your researcher subagent using the task tool. Keep your final response to one sentence.",
subagents: [
{
name: "researcher",
description: "Researches topics thoroughly",
systemPrompt:
"You are a thorough researcher. Research the given topic " +
"and provide a concise summary in 2-3 sentences.",
},
],
});
for await (const [namespace, chunk] of await agent.stream(
{
messages: [
{ role: "user", content: "Write a short summary about AI safety" },
],
},
{ streamMode: "updates", subgraphs: true },
)) {
// 空命名空间 = 主 Agent 步骤
if (namespace.length === 0) {
for (const [nodeName, data] of Object.entries(chunk)) {
if (nodeName === "tools") {
// "tools" 节点返回 tool 类型消息时,说明子代理已完成
for (const msg of data.messages ?? []) {
if (msg.type === "tool") {
console.log(`\nSubagent complete: ${msg.name}`);
console.log(` Result: ${String(msg.content).slice(0, 200)}...`);
}
}
} else {
// 其他节点(如 model_request)是主 Agent 的推理步骤
console.log(`[main agent] step: ${nodeName}`);
}
}
}
// 非空命名空间 = Subagent 步骤
else {
for (const [nodeName] of Object.entries(chunk)) {
console.log(` [${namespace[0]}] step: ${nodeName}`);
}
}
}
tools:call_xxx 命名空间 = Subagent 步骤。当主 Agent 的 tools 节点返回 type: "tool" 消息时,说明 Subagent 已完成。
进度输出解读
理解流式事件的时间线——从启动到完成
上面的代码运行后,你会看到类似下面的输出。让我们逐行分析它的时间线:
Main agent step: model_request # 主 Agent 开始推理
[tools:call_abc123] step: model_request # Subagent A 开始推理
[tools:call_abc123] step: tools # Subagent A 调用工具
[tools:call_abc123] step: model_request # Subagent A 再次推理
Subagent complete: task # Subagent A 完成
Result: ## AI Safety Report...
Main agent step: model_request # 主 Agent 收到结果后继续
[tools:call_def456] step: model_request # Subagent B 开始推理
[tools:call_def456] step: model_request # Subagent B 再次推理
Subagent complete: task # Subagent B 完成
Result: # Comprehensive Report on AI Safety...
Main agent step: model_request # 主 Agent 最终回复
时间线关键节点
- 主 Agent 先推理,决定委派任务
- Subagent A 启动,经历多轮推理+工具调用
- Subagent A 完成,结果返回主 Agent
- 主 Agent 可能继续委派更多 Subagent
- 所有 Subagent 完成后,主 Agent 生成最终回复
实用技巧
- 用
nodeName过滤不需要的内部步骤 - 只关注
model_request和tools两个节点 - 多个 Subagent 的 ID 不同,可以分别追踪
- 进度 UI 可以按
toolCallId分组显示
LLM Token 流
用 streamMode: "messages" 逐 token 输出 LLM 回复
使用 streamMode: "messages" 可以从主 Agent 和 Subagent 流式传输单个 token。每个消息事件都包含标识来源 Agent 的元数据。
let currentSource = ""; // 跟踪当前输出源,切换时打印分隔符
for await (const [namespace, chunk] of await agent.stream(
{
messages: [{
role: "user",
content: "Research quantum computing advances",
}],
},
{ streamMode: "messages", subgraphs: true }, // messages 模式逐 token 输出
)) {
const [message] = chunk; // 每个事件包含一条消息
// 通过 namespace 区分主 Agent 和子代理的 token
const isSubagent = namespace.some((s) => s.startsWith("tools:"));
if (isSubagent) {
// 子代理的 token,提取命名空间作为标识
const subagentNs = namespace.find(
(s) => s.startsWith("tools:")
)!;
if (subagentNs !== currentSource) {
// 来源切换时打印分隔符,避免不同来源的文本混在一起
process.stdout.write(
`\n\n--- [subagent: ${subagentNs}] ---\n`
);
currentSource = subagentNs;
}
if (message.text) {
process.stdout.write(message.text); // 逐 token 写入,形成打字效果
}
} else {
// 主 Agent 的 token
if ("main" !== currentSource) {
process.stdout.write(`\n\n--- [main agent] ---\n`);
currentSource = "main";
}
if (message.text) {
process.stdout.write(message.text);
}
}
}
currentSource 变量跟踪当前输出源,来源切换时打印分隔符,这样用户才能看清哪段文字是谁写的。
工具调用流
实时查看 Subagent 正在调什么工具、传什么参数
当 Subagent 使用工具时,工具调用事件出现在 messages 流模式下。你可以区分三种事件类型:工具调用名称、参数增量、工具结果。
import { AIMessageChunk, ToolMessage } from "langchain";
for await (const [namespace, chunk] of await agent.stream(
{
messages: [{
role: "user",
content: "Research recent quantum computing advances",
}],
},
{ streamMode: "messages", subgraphs: true },
)) {
const [message] = chunk;
// 通过 namespace 判断事件来源
const isSubagent = namespace.some((s) => s.startsWith("tools:"));
const source = isSubagent
? namespace.find((s) => s.startsWith("tools:"))!
: "main";
// 1. 工具调用数据块:流式传输工具名称和参数
// AIMessageChunk 包含 tool_call_chunks 数组
if (AIMessageChunk.isInstance(message)
&& message.tool_call_chunks?.length) {
for (const tc of message.tool_call_chunks) {
if (tc.name) {
console.log(`\n[${source}] Tool call: ${tc.name}`);
}
if (tc.args) {
// args 是增量的,每次只返回一小段,需要拼接
process.stdout.write(tc.args);
}
}
}
// 2. 工具执行结果:以 ToolMessage 形式返回
if (ToolMessage.isInstance(message)) {
console.log(
`\n[${source}] Tool result [${message.name}]: ${
message.text?.slice(0, 150)
}`
);
}
// 3. 常规 AI 文本内容(排除工具调用消息)
if (AIMessageChunk.isInstance(message)
&& message.text
&& !message.tool_call_chunks?.length) {
process.stdout.write(message.text);
}
}
工具事件解析
三种工具事件的含义和处理方式
工具调用名称
tc.name 有值时,表示 Agent 决定调用某个工具。
UI 建议:显示"正在调用 xxx 工具..."
AIMessageChunk.tool_call_chunks
参数增量
tc.args 有值时,是工具参数的增量片段。
UI 建议:像打字机一样逐步显示参数 JSON。
多次触发,需拼接
工具执行结果
ToolMessage.isInstance(message) 为 true 时,是工具返回的结果。
UI 建议:显示结果摘要,可展开查看详情。
ToolMessage 实例
AIMessageChunk 里,结果在 ToolMessage 里。不要把它们混为一谈。
tool_call_chunks 中的 args 是增量的,每次只返回一小段。你需要拼接它们才能得到完整的参数 JSON。如果只取最后一个 chunk,你会拿到不完整的参数。
自定义更新
用 config.writer 从工具内部发出进度信号
有时候你需要的不是 LLM token 或工具调用,而是业务级别的进度——比如"正在获取数据 50%"。这时就用 config.writer 发出自定义事件。
import { createDeepAgent } from "deepagents";
import { tool, type ToolRuntime } from "langchain";
import { z } from "zod";
// 通过 config.writer 发出自定义进度事件的工具
const analyzeData = tool(
async ({ topic }, config: ToolRuntime) => {
// writer 用于向 streamMode: "custom" 推送自定义数据
const writer = config.writer;
// 发出"启动"进度事件
writer?.({ status: "starting", topic, progress: 0 });
await new Promise((r) => setTimeout(r, 500));
// 发出"分析中"进度事件(50%)
writer?.({ status: "analyzing", progress: 50 });
await new Promise((r) => setTimeout(r, 500));
// 发出"完成"进度事件(100%)
writer?.({ status: "complete", progress: 100 });
// 返回值作为工具调用结果传给 Agent
return `Analysis of "${topic}": Customer sentiment is 85% positive, driven by product quality and support response times.`;
},
{
name: "analyze_data",
description:
"Run a data analysis on a given topic. " +
"This tool performs the actual analysis and emits progress updates. " +
"You MUST call this tool for any analysis request.",
schema: z.object({
topic: z.string().describe("The topic to analyze"),
}),
},
);
writer 发出的数据会出现在 streamMode: "custom" 中。你需要用对应的 stream mode 来消费它。
消费自定义事件
从流中读取 writer 发出的进度数据
定义了 writer 之后,在消费端用 streamMode: "custom" 来接收自定义事件:
const agent = createDeepAgent({
systemPrompt: "You are a coordinator. Delegate analysis to analyst.",
subagents: [{
name: "analyst",
description: "Performs data analysis with progress tracking",
systemPrompt: "You MUST call analyze_data for every request.",
tools: [analyzeData],
}],
});
for await (const [namespace, chunk] of await agent.stream(
{
messages: [{
role: "user",
content: "Analyze customer satisfaction trends",
}],
},
{ streamMode: "custom", subgraphs: true }, // custom 模式接收 writer 推送的事件
)) {
const isSubagent = namespace.some((s) => s.startsWith("tools:"));
if (isSubagent) {
const subagentNs = namespace.find((s) => s.startsWith("tools:"))!;
console.log(`[${subagentNs}]`, chunk);
} else {
console.log("[main]", chunk);
}
}
# 输出:自定义进度事件按时间顺序到达
[tools:call_abc123] { status: 'starting', progress: 0 }
[tools:call_abc123] { status: 'analyzing', progress: 50 }
[tools:call_abc123] { status: 'complete', progress: 100 }
progress: 0~100)、状态文案(status: "analyzing")、阶段切换通知。你可以自由定义 writer 传入的对象结构。
组合多种模式
用数组传入多个 streamMode,一次获取全部信息
实际项目中,你通常需要同时看到步骤进度、token 输出和自定义事件。把 streamMode 设为数组即可组合:
const INTERESTING_NODES = new Set(["model_request", "tools"]);
let lastSource = "";
let midLine = false;
for await (const [namespace, mode, data] of await agent.stream(
{
messages: [{
role: "user",
content: "Analyze the impact of remote work on productivity",
}],
},
// 组合三种流模式,数组形式启用多模式
{ streamMode: ["updates", "messages", "custom"], subgraphs: true },
)) {
const isSubagent = namespace.some((s) => s.startsWith("tools:"));
const source = isSubagent ? "subagent" : "main";
// mode 字段标识当前事件的流模式类型
if (mode === "updates") {
// 步骤更新:只关注感兴趣的节点
for (const nodeName of Object.keys(data)) {
if (!INTERESTING_NODES.has(nodeName)) continue;
console.log(`[${source}] step: ${nodeName}`);
}
} else if (mode === "messages") {
// token 流:逐 token 写入
const [message] = data;
if (message.text) {
if (source !== lastSource) {
process.stdout.write(`\n[${source}] `);
lastSource = source;
}
process.stdout.write(message.text);
}
} else if (mode === "custom") {
// 自定义事件:writer 推送的业务进度数据
console.log(`[${source}] custom event:`, data);
}
}
[namespace, chunk],多模式时多了一个 mode 字段,变成 [namespace, mode, data]。别搞混了!
Subagent 生命周期
监控 Subagent 的启动、运行、完成三个阶段
这是一个常见的实战模式——用 updates 模式追踪 Subagent 从创建到完成的完整生命周期:
阶段 1:启动
主 Agent 的 model_request 包含 task 工具调用
阶段 2:运行
收到 tools:UUID 命名空间的事件
阶段 3:完成
主 Agent 的 tools 节点返回 tool 消息
const activeSubagents = new Map();
for await (const [namespace, chunk] of await agent.stream(
{ messages: [
{ role: "user", content: "Research the latest AI safety developments" },
]},
{ streamMode: "updates", subgraphs: true },
)) {
for (const [nodeName, data] of Object.entries(chunk)) {
// 阶段 1:检测 Subagent 启动
// 主 Agent 的 model_request 节点中包含 task 工具调用时,说明子代理即将启动
if (namespace.length === 0 && nodeName === "model_request") {
for (const msg of data.messages ?? []) {
for (const tc of msg.tool_calls ?? []) {
if (tc.name === "task") {
// 记录子代理信息,状态为 pending
activeSubagents.set(tc.id, {
type: tc.args?.subagent_type, // 子代理类型名称
description: tc.args?.description?.slice(0, 80), // 任务描述
status: "pending", // 初始状态
});
console.log(
`[lifecycle] PENDING → "${tc.args?.subagent_type}" (${tc.id})`
);
}
}
}
}
// 阶段 2:检测 Subagent 运行中
// 收到 tools:UUID 命名空间的事件说明子代理已开始执行
if (namespace.length > 0 && namespace[0].startsWith("tools:")) {
const pregelId = namespace[0].split(":")[1];
// 注意:pregel 运行时 ID 与 tool_call_id 不同,无法直接关联
// 因此将所有 pending 状态的子代理标记为 running
for (const [id, sub] of activeSubagents) {
if (sub.status === "pending") {
sub.status = "running";
console.log(`[lifecycle] RUNNING → subagent "${sub.type}" (pregel: ${pregelId})`);
break;
}
}
}
// 阶段 3:检测 Subagent 完成
// 主 Agent 的 tools 节点返回 tool 类型消息,说明子代理执行完毕
if (namespace.length === 0 && nodeName === "tools") {
for (const msg of data.messages ?? []) {
if (msg.type === "tool") {
// 通过 tool_call_id 匹配之前记录的子代理
const sub = activeSubagents.get(msg.tool_call_id);
if (sub) {
sub.status = "complete";
console.log(
`[lifecycle] COMPLETE → subagent "${sub.type}" (${msg.tool_call_id})`,
);
console.log(
` Result preview: ${String(msg.content).slice(0, 120)}...`,
);
}
}
}
}
}
}
// 打印最终状态
console.log("\n--- Final subagent states ---");
for (const [id, sub] of activeSubagents) {
console.log(` ${sub.type}: ${sub.status}`);
}
生命周期输出
三种状态转换的完整输出示例
运行上面的生命周期追踪代码,你会看到类似这样的输出:
[lifecycle] PENDING → "researcher" (call_abc123)
[lifecycle] RUNNING → "researcher"
[lifecycle] COMPLETE → "researcher" (call_abc123)
Result preview: ## AI Safety Report
Recent developments in AI safety include...
[lifecycle] PENDING → "writer" (call_def456)
[lifecycle] RUNNING → "writer"
[lifecycle] COMPLETE → "writer" (call_def456)
Result preview: # AI Safety Summary
Based on the research findings...
--- Final subagent states ---
researcher: complete
writer: complete
PENDING → RUNNING 延迟
注意:pregel 任务 ID 与 tool_call_id 不同,所以收到第一个 Subagent 事件时,将所有 pending 状态标记为 running。
多个 Subagent 并行
如果有多个 Subagent 同时运行,Map 中会同时存在多个 pending/running 状态的条目。按 tool_call_id 区分即可。
模式速查表
四种 streamMode 的用途、事件结构和使用场景一览
| streamMode | 事件内容 | 典型用途 | 解构方式 |
|---|---|---|---|
| "updates" | 节点名称 + 更新数据 | 跟踪执行步骤、Subagent 生命周期 | [namespace, chunk] |
| "messages" | LLM token / 工具调用 / 工具结果 | 逐字输出、工具追踪 | [namespace, chunk] |
| "custom" | writer 发出的自定义对象 | 进度条、业务状态通知 | [namespace, chunk] |
| ["updates","messages","custom"] | 以上全部 | 完整的执行视图 | [namespace, mode, data] |
单模式 vs 多模式
单模式:解构 [namespace, chunk],简单直接。
多模式:解构 [namespace, mode, data],需要用 mode 字段分支处理。
命名空间判断技巧
namespace.length === 0 → 主 Agent
namespace.some(s => s.startsWith("tools:")) → Subagent
namespace.find(s => s.startsWith("tools:"))?.split(":")[1] → 提取 ID
本章回顾
到这里,你已经掌握了 Deep Agent 流式传输的完整能力
你学到了什么
- subgraphs: true 是接收 Subagent 事件的开关
- 命名空间 用于标识事件来源——空=主 Agent,
tools:=Subagent - updates 模式跟踪步骤进度
- messages 模式流式传输 token 和工具调用
- custom 模式接收 writer 发出的自定义事件
- 多模式组合 获取完整执行视图
- Subagent 生命周期:PENDING → RUNNING → COMPLETE
相关章节
- Subagent — 配置和使用子 Agent
- Event Streaming — 类型化的投影 API(新项目推荐)
- 前端流式传输 — 用 useStream 构建 React UI