第 19 章

事件流(Event Streaming)

从 Deep Agents 流式传输子代理、消息、工具调用和最终输出

让用户实时看到 Agent 的思考与行动过程

为什么需要它

你有没有试过让 Agent 执行复杂任务,却只能干等最终结果?

没有事件流的困境

  • Agent 执行 30 秒,用户只看到一个空白加载画面
  • 子代理启动了多少个、完成了几个?完全不知道
  • 工具调用正在执行什么?无法展示进度
  • 协调器和子代理的消息混在一起,无法按来源分离显示
  • 用户体验差——看起来像卡死了

事件流的解法

  • stream.subagents 实时获取每个子代理的状态
  • stream.messages 按来源逐字流式展示消息
  • stream.toolCalls 实时展示工具调用进度
  • 子代理消息与协调器消息独立流句柄,按来源分离
  • 并发消费所有流,UI 即时更新
问题核心:长时间运行的 Agent 如果不能实时反馈进度,用户体验会极差。事件流让你从"黑盒等待"变成"透明协作"——用户能看到 Agent 在做什么、做了多少、还剩什么。

流式传输子代理

用 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 一个句柄。你可以选择性订阅该句柄上的消息、工具调用或最终输出。
图:stream.subagents 投影与惰性求值

子代理流字段

每个子代理流暴露与父运行相同类型的投影

每个子代理流句柄提供以下字段,让你按需访问不同层级的数据:

字段类型描述
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);
  }
}
分离显示:在 UI 中,你应该把协调器消息和子代理消息分开渲染——协调器消息通常展示在对话主区域,子代理消息展示在对应的任务卡片内。

流式传输工具

在代理树的每一层级暴露工具调用的实时状态

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 展示
  • 面向最终用户的抽象
选择原则:对于面向用户的 UI,请使用 stream.subagents。它隐藏了内部图节点的实现细节,直接暴露用户能理解的"任务"和"子代理"概念。只有在调试底层图执行时才需要 stream.subgraphs。

本章回顾

到这里,你已经掌握了 Deep Agents 事件流的完整用法

你学到了什么

  • stream.subagents 是核心投影,提供每个子代理的独立流句柄
  • 子代理流暴露 name、messages、toolCalls、output、subagents 等字段
  • 用 output Promise 跟踪子代理的生命周期(启动/完成/失败)
  • stream.messages 获取协调器消息,subagent.messages 获取子代理消息
  • 工具调用有 status 状态,需先检查再获取 output 或 error
  • 并发消费用 Promise.all,原始时序用 namespace 判断来源
  • 面向用户用 subagents,调试用 subgraphs

后续章节预告

  • 流式传输:通用代理的流式传输详细机制
  • 前端子代理流:将协调器消息与子代理卡片分离的 UI 模式
  • 前端 TODO 列表:实时展示任务进度的 UI 组件
  • 前端沙箱:可视化展示代码执行过程
核心结论:事件流是 Deep Agents 与用户之间的桥梁。通过 stream.subagents 提供的分层投影,你可以按需获取任意层级的数据——从简单的"哪个子代理完成了"到精确的"每个 token 来自哪里"。选择合适的消费方式,让你的 UI 既实时又清晰。

延伸阅读

继续本课程

相关文档

背景知识