事件流(Event Streaming)
从 Deep Agents 流式传输子代理、消息、工具调用和最终输出
让用户实时看到 Agent 的思考与行动过程为什么需要它
你有没有试过让 Agent 执行复杂任务,却只能干等最终结果?
没有事件流的困境
- Agent 执行 30 秒,用户只看到一个空白加载画面
- 子代理启动了多少个、完成了几个?完全不知道
- 工具调用正在执行什么?无法展示进度
- 协调器和子代理的消息混在一起,无法按来源分离显示
- 用户体验差——看起来像卡死了
事件流的解法
stream.subagents实时获取每个子代理的状态stream.messages按来源逐字流式展示消息stream.toolCalls实时展示工具调用进度- 子代理消息与协调器消息独立流句柄,按来源分离
- 并发消费所有流,UI 即时更新
流式传输子代理
用 stream.subagents 获取每个委托子代理的独立流句柄
Deep Agents 在 LangGraph 流式传输基础上增加了子代理投影(projection)。当你希望每个委托的 task 调用都有独立的流句柄时,使用 stream.subagents。
这个投影是轻量级的:它首先发现子代理任务,只有当你访问子代理句柄上的消息、工具调用和值流时,才会打开这些流。
const stream = await agent.streamEvents(
{ messages: [{ role: "user", content: "Write me a haiku about the sea" }] },
{ version: "v3" } // 使用 v3 版本的事件协议
);
// subagents 是异步迭代器,每当有新子代理被委托时 yield 一个句柄
for await (const subagent of stream.subagents) {
console.log(subagent.name); // 子代理名称,用于 UI 标识
console.log(await subagent.taskInput); // 协调器传给 task 工具的 prompt
// 惰性求值:只有迭代 messages 时才会打开消息流
for await (const message of subagent.messages) {
console.log(await message.text); // 子代理生成的文本,逐 token 到达
}
}
stream.subagents 是一个异步迭代器——每当有新的子代理被委托时,它就会 yield 一个句柄。你可以选择性订阅该句柄上的消息、工具调用或最终输出。
子代理流字段
每个子代理流暴露与父运行相同类型的投影
每个子代理流句柄提供以下字段,让你按需访问不同层级的数据:
| 字段 | 类型 | 描述 |
|---|---|---|
name |
string | 子代理名称,用于标识来源 |
messages |
AsyncIterable | 子代理发出的消息流 |
subagents |
AsyncIterable | 嵌套的子代理调用(递归) |
output |
Promise | 子代理的最终状态或完成信号 |
taskInput |
Promise | 传递给 task 工具的 prompt |
toolCalls |
AsyncIterable | 子代理范围内的工具调用 |
命名约定
TypeScript 使用驼峰命名(camelCase):toolCalls、taskInput。与 Python 版本的 snake_case 命名不同,注意区分。
设计理念
字段都是惰性求值的——只有当你实际迭代它们时,才会打开对应的流通道。不访问 messages 就不会产生消息流的开销。
跟踪生命周期
用 output Promise 跟踪每个子代理的启动与完成
当你只需要显示哪些子代理已启动和已完成时,使用 stream.subagents 的 output 字段。无需订阅消息或值流,就能实时统计进度。
const stream = await agent.streamEvents(input, { version: "v3" });
let running = 0; // 正在运行的子代理计数
let completed = 0; // 已成功完成的子代理计数
let failed = 0; // 失败的子代理计数
const watchers: Promise<void>[] = []; // 收集所有 output Promise
for await (const subagent of stream.subagents) {
running += 1;
console.log(`${subagent.name}: started`);
// output 是 Promise:resolve=成功,reject=失败
watchers.push(
subagent.output.then(
() => {
running -= 1;
completed += 1;
console.log(`${subagent.name}: completed`);
},
() => {
running -= 1;
failed += 1;
console.log(`${subagent.name}: failed`);
}
)
);
}
// 等待所有子代理完成后再打印最终统计
await Promise.all(watchers);
console.log({ running, completed, failed });
output 的 Promise 有两个回调——resolve 表示成功完成,reject 表示失败。务必同时处理这两种情况,否则失败的子代理会被静默忽略。
流式传输消息
用 stream.messages 和 subagent.messages 分别获取不同层级的消息
Deep Agents 可以从协调器代理和委托的子代理分别发出消息。两者的流句柄不同:
协调器消息
使用 stream.messages 获取顶层协调器发出的消息。
顶层流
子代理消息
使用 subagent.messages 获取每个委托子代理的消息。
子代理流
const stream = await agent.streamEvents(input, { version: "v3" });
// 协调器消息:只包含顶层协调器发出的内容
for await (const message of stream.messages) {
console.log("[coordinator]", await message.text);
}
// 子代理消息:每个子代理有独立的消息流
for await (const subagent of stream.subagents) {
for await (const message of subagent.messages) {
console.log(`[${subagent.name}]`, await message.text);
}
}
流式传输工具
在代理树的每一层级暴露工具调用的实时状态
Deep Agents 在代理树的每一层级暴露工具调用。你可以分别获取协调器和每个子代理的工具调用,并实时查看执行状态。
const stream = await agent.streamEvents(input, { version: "v3" });
// 协调器层级的工具调用
for await (const call of stream.toolCalls) {
console.log("[coordinator tool]", call.name, call.input);
console.log(await call.status); // "running" | "finished" | "error"
}
// 每个子代理的工具调用,独立于协调器
for await (const subagent of stream.subagents) {
for await (const call of subagent.toolCalls) {
console.log(`[${subagent.name} tool]`, call.name, call.input);
// 必须先检查 status,再按需访问 output 或 error
const status = await call.status;
if (status === "finished") {
console.log(await call.output); // 工具执行成功,获取返回值
} else if (status === "error") {
console.error(await call.error); // 工具执行出错,获取错误信息
}
}
}
await call.status。如果你直接读 call.output 而不检查状态,当工具调用出错时会抛出未捕获的异常。先检查状态,再按需获取 output 或 error。
嵌套子代理流
递归进入子代理流,观察多层嵌套的工具调用和子代理
你可以递归进入子代理流来观察嵌套的子代理、消息和工具调用。这对于展示深层任务委派链特别有用。
const stream = await agent.streamEvents(input, { version: "v3" });
for await (const subagent of stream.subagents) {
console.log(`subagent ${subagent.name}: started`);
// 第一层子代理的工具调用
for await (const toolCall of subagent.toolCalls) {
console.log(`${toolCall.name}(${JSON.stringify(toolCall.input)})`);
const status = await toolCall.status;
if (status === "finished") {
console.log(await toolCall.output);
} else if (status === "error") {
console.error(await toolCall.error);
}
}
// 递归进入嵌套子代理,可继续向下遍历任意深度
for await (const nested of subagent.subagents) {
console.log(`nested subagent ${nested.name}: started`);
}
}
递归模式
你可以在 subagent.subagents 上继续迭代,访问任意深度的嵌套。每一层都有完整的 messages、toolCalls、output 等字段。
何时使用
大多数 UI 只需要前两层(协调器 + 子代理)。只有在展示深度任务分解——例如子代理再委托子代理——时才需要递归。
并发消费
用 Promise.all 并发消费协调器和子代理的流,实现实时 UI 更新
协调器和子代理的输出通常会交错出现。当你需要实时的 UI 更新时,必须并发地消费各投影。
方式一:用 Promise.all 并发迭代
const stream = await agent.streamEvents(input, { version: "v3" });
// 并发消费:协调器和子代理的事件交错到达,必须同时迭代
await Promise.all([
// 协调器消息流
(async () => {
for await (const message of stream.messages) {
console.log("[coordinator]", await message.text);
}
})(),
// 子代理消息流:每个子代理启动一个独立的异步消费协程
(async () => {
for await (const subagent of stream.subagents) {
// void 表示不等待内部协程,允许多个子代理并发消费
void (async () => {
for await (const message of subagent.messages) {
console.log(`[${subagent.name}]`, await message.text);
}
})();
}
})(),
]);
原始协议事件
用 namespace 精确区分协调器和子代理的内容
当你需要在协调器和所有子代理之间获取精确的到达顺序时,请迭代原始协议事件,并使用 namespace 来标识来源。
const stream = await agent.streamEvents(input, { version: "v3" });
// 迭代原始协议事件,获取严格的时间顺序
for await (const event of stream) {
// 只处理消息类型的事件
if (event.method !== "messages") continue;
const data = event.params.data;
// 只处理文本增量事件(每个 token 触发一次)
if (data.event !== "content-block-delta") continue;
const block = data.delta ?? {};
if (block.type === "text-delta") {
// namespace 中含 "tools:" 前缀说明事件来自子代理
const isSubagent = event.params.namespace.some(
(seg) => seg.startsWith("tools:")
);
const source = isSubagent ? "subagent" : "coordinator";
console.log(`[${source}] ${block.text}`);
}
}
namespace 规则
如果 namespace 中有以 tools: 开头的段,说明事件来自子代理。否则来自协调器。
适用场景
这种方式适合需要严格时序的场景——比如聊天界面中消息必须按真实到达顺序渲染,不能因为分了多个迭代器而打乱顺序。
子代理与子图
stream.subagents 与 stream.subgraphs 的区别
stream.subgraphs
- 显示图的执行结构
- 暴露底层图节点细节
- 适合调试和开发阶段
- 面向开发者的抽象
stream.subagents
- 显示产品级别的任务委托
- 隐藏内部图节点,直接暴露子代理概念
- 适合面向用户的 UI 展示
- 面向最终用户的抽象
stream.subagents。它隐藏了内部图节点的实现细节,直接暴露用户能理解的"任务"和"子代理"概念。只有在调试底层图执行时才需要 stream.subgraphs。
本章回顾
到这里,你已经掌握了 Deep Agents 事件流的完整用法
你学到了什么
stream.subagents是核心投影,提供每个子代理的独立流句柄- 子代理流暴露
name、messages、toolCalls、output、subagents等字段 - 用
outputPromise 跟踪子代理的生命周期(启动/完成/失败) stream.messages获取协调器消息,subagent.messages获取子代理消息- 工具调用有
status状态,需先检查再获取 output 或 error - 并发消费用
Promise.all,原始时序用namespace判断来源 - 面向用户用
subagents,调试用subgraphs
后续章节预告
- 流式传输:通用代理的流式传输详细机制
- 前端子代理流:将协调器消息与子代理卡片分离的 UI 模式
- 前端 TODO 列表:实时展示任务进度的 UI 组件
- 前端沙箱:可视化展示代码执行过程
stream.subagents 提供的分层投影,你可以按需获取任意层级的数据——从简单的"哪个子代理完成了"到精确的"每个 token 来自哪里"。选择合适的消费方式,让你的 UI 既实时又清晰。
延伸阅读
相关文档
- LangGraph.js 官方文档 — 底层图流式传输模型
- LangChain 事件流 — 通用代理的消息和工具调用流式传输