惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Hacker News: Ask HN
Hacker News: Ask HN
H
Help Net Security
Microsoft Azure Blog
Microsoft Azure Blog
B
Blog RSS Feed
Jina AI
Jina AI
Stack Overflow Blog
Stack Overflow Blog
量子位
博客园_首页
Vercel News
Vercel News
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
Forbes - Security
Forbes - Security
IT之家
IT之家
N
News and Events Feed by Topic
S
Security Affairs
Recent Commits to openclaw:main
Recent Commits to openclaw:main
Webroot Blog
Webroot Blog
Recorded Future
Recorded Future
L
LangChain Blog
Y
Y Combinator Blog
AI
AI
MyScale Blog
MyScale Blog
大猫的无限游戏
大猫的无限游戏
小众软件
小众软件
Know Your Adversary
Know Your Adversary
AWS News Blog
AWS News Blog
Help Net Security
Help Net Security
Cyberwarzone
Cyberwarzone
L
Lohrmann on Cybersecurity
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Google Online Security Blog
Google Online Security Blog
V2EX - 技术
V2EX - 技术
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
PCI Perspectives
PCI Perspectives
I
Intezer
T
Tenable Blog
G
Google Developers Blog
Application and Cybersecurity Blog
Application and Cybersecurity Blog
T
Troy Hunt's Blog
L
LINUX DO - 最新话题
云风的 BLOG
云风的 BLOG
C
CXSECURITY Database RSS Feed - CXSecurity.com
有赞技术团队
有赞技术团队
O
OpenAI News
P
Proofpoint News Feed
TaoSecurity Blog
TaoSecurity Blog
C
Check Point Blog
Last Week in AI
Last Week in AI
S
Schneier on Security
Simon Willison's Weblog
Simon Willison's Weblog
Blog — PlanetScale
Blog — PlanetScale

博客园 - Zhentiw

[GenAI] Migration Plan: Flat API → Layered Architecture [Skill] Frontend Design Skill Claude Code Hooks: Complete Guide [GenAI] Pre-retieval overview [GenAI] About Indexing [GenAI] Indexing overview [Vibe Coding] 降低大模型幻觉 - 重试机制 [Vibe coding] 降低大模型幻觉 - JSON 安全输出提示词 [Node.js] WebSocket基础知识 [LangGraph] 应用结构 [LangGrpah] Unit testing [LangGraph] Functional API [LangGraph] 中断注意事项 [LangGraph] 中断相关细节 [LangGrpah] 静态断点 [LangGrpah] 非阻塞式中断 [LangGraph] 阻塞式中断 [LangGraph] 语义搜索 [LangGraph] 长期记忆 [LangGraph] 管理短期记忆 [LangGraph] 自定义checkpointer [LangGraph] 短期记忆 [LangGraph] 时间旅行 [LangGraph] checkpoint常用API [LangGraph] 元数据标记 [Vitest] mockClear, mockReset, mockRestore [LangGraph] 将子图添加为节点
[LangGraph] 流
Zhentiw · 2026-03-03 · via 博客园 - Zhentiw

LangGraph中将流式处理列为了核心功能之一,可以通过 stream() 方法来获取流式内容。

在获取流式内容的同时,还可以通过 streamMode 来指定不同的类型:

image-20251118105124570

这节课我们先来看前面2种流类型:

  • 值流
  • 更新流

值流和更新流

值流

每次节点执行后,直接把整个 state(完整对象)流给你。

特点

  • 简单粗暴,适合入门;
  • 每一步都能看到整个状态
  • 但 payload 可能比较大。
const stream = await graph.stream(input, {
  streamMode: "values",
});

更新流

只告诉你“变化了什么”,而不是给整份 state。

特点

  • 性能更高,输出更小
  • 能明确看到某一次节点只更新了哪些字段
  • 适合大状态对象(如企业级复杂 workflow)
const stream = await graph.stream(input, {
  streamMode: "updates",
});

消息流

前置知识

之前我们在使用langchain的时候,无论是模型还是链,常用的方法有两个:

  1. invoke
  2. stream
import { ChatOpenAI } from "@langchain/openai";

const model = new ChatOpenAI({
  model: "gpt-4o-mini",
  streaming: true,
});

async function runInvoke() {
  const result = await model.invoke("简单介绍一下 LangGraph");

  // invoke 会在模型完整生成答案后,一次性返回结果
  console.log(result.content);
}

runInvoke();
import { ChatOpenAI } from "@langchain/openai";

const model = new ChatOpenAI({
  model: "gpt-4o-mini",
  streaming: true,
});

async function runStream() {
  const stream = await model.stream("简单介绍一下 LangGraph");

  for await (const chunk of stream) {
    // 每一次循环,都会拿到模型生成的一部分内容
    process.stdout.write(chunk.content ?? "");
  }
}

runStream();

两个方法的区别:

  • invoke:非流式输出
  • stream:流式输出

无论是invoke还是stream,大模型那边其实都是按照流式进行计算的

  • invoke:把这些 token 先在内部“攒起来”,等模型生成结束后,再一次性把完整结果返回给你;
  • stream:是把“生成过程”直接暴露出来,每生成一段(一个 token 或一小段文本)就立刻交给你处理。

写一篇文章:一个字一个字写的

  • invoke:等待整篇文章写好之后,再一次性交稿
  • stream:一边写,一边将稿子内容对外开放

消息流

langgraph 消息流,就是在“图执行过程中”,把大模型生成过程中的 token 以及对应的节点元数据一起以流式暴露出来。

换句话说:无论模型节点内部使用的是invoke还是stream,langgraph的messages消息流都能够实时的接收到LLM返回的token.

流式每一次拿到的 chunk,大致如下:

[
  LLM token (AIMessageChunk), // 大模型这一次返回的内容
  metadata // 元数据信息
]
  • 第一项是消息token
  • 第二项则是节点元数据信息

因此 langgraph 这里的消息流,信息会更加丰富一些,不仅有模型 token,还包含“当前执行的是哪个节点”等元数据信息。

const stream = await graph.stream(input, {
  streamMode: "messages",
});

for await (const item of stream) {
  const message = item[0];
  if (typeof message.content === "string") {
    process.stdout.write(message.content);
  } else if (Array.isArray(message.content)) {
    // 说明当前的内容是一个 ContentBlock 内容结构块
    // 内容结构块有很多类型:Text块、Image块...
    // Text块: { "type": "text", "text": "Hello world", "index": 0 }
    // Reasoning块: { "type": "reasoning", "reasoning": "让我思考一下这个问题...", "index": 1 }
    // Image块: { "type": "image", "mimeType
    // 接下来就是针对不同类型的块,做不同的处理
    for (const block of message.content) {
      if (
        block &&
        typeof block === "object" &&
        "text" in block &&
        typeof block.text === "string"
      ) {
        console.log(block.text);
      }
    }
  }
}

消息流返回的每一项如下:

item>>> [
  AIMessageChunk {
    "id": "chatcmpl-CiaoCyawrhuUnI0POuvmDLNivmEgh",
    "content": "理解",
    "additional_kwargs": {},
    "response_metadata": {
      "model_provider": "openai",
      "usage": {}
    },
    "tool_calls": [],
    "tool_call_chunks": [],
    "invalid_tool_calls": []
  },
  {
    tags: [],
    name: undefined,
    langgraph_step: 1,
    langgraph_node: 'writeIntro',
    langgraph_triggers: [ 'branch:to:writeIntro' ],
    langgraph_path: [ '__pregel_pull', 'writeIntro' ],
    langgraph_checkpoint_ns: 'writeIntro:21213964-afa8-53ab-a00b-144e50cd45ed',
    __pregel_task_id: '21213964-afa8-53ab-a00b-144e50cd45ed',
    checkpoint_ns: 'writeIntro:21213964-afa8-53ab-a00b-144e50cd45ed',
    ls_provider: 'openai',
    ls_model_name: 'gpt-4o-mini',
    ls_model_type: 'chat',
    ls_temperature: undefined,
    ls_max_tokens: undefined,
    ls_stop: undefined
  }
]

stream 的每一项是一个数组,数组里面有两个对象:

  • item[0]:这是 LangChain 标准内容块的一部分。专门表示:

    • 模型输出的某一段 token(可能为空字符串)
    • 或者模型输出的部分 message 内容
    • 或者工具调用相关 chunk
  • item[1]:LangGraph元数据对象,包含当前chunk来自哪个节点、当前步骤、当前模型以及当前路径等信息:

    • LangGraph 当前正在执行哪个节点

      langgraph_node: "writeIntro";
      langgraph_step: 1;
      
    • 是什么触发了该节点执行

      langgraph_triggers: ["branch:to:writeIntro"];
      
    • 当前执行路径

      langgraph_path: ["__pregel_pull", "writeIntro"];
      
    • 当前节点的 checkpoint 空间

      langgraph_checkpoint_ns: "writeIntro:21213964-...";
      
    • LLM 调用的元数据

      ls_provider: "openai";
      ls_model_name: "gpt-4o-mini";
      ls_model_type: "chat";
      

    item[1] 是 LangGraph 给出的“执行事件元数据”。

事件流

graph.stream():面向“结果”的流

例如:

  • messages 模式下,可以拿到 LLM 生成的 message chunk
  • updates 模式下,可以拿到 state 的增量变化

这些内容通常是:

  • 已经整理好的
  • 适合直接给前端消费的
  • 不需要理解太多内部执行细节

因此,在大多数应用场景中,优先使用 graph.stream() 就够了。

graph.streamEvents():面向“过程”的流

相比之下,graph.streamEvents() 更偏底层,它暴露的是图执行过程中产生的各种事件。

这些事件可能包括:

  • 某个 node 开始执行
  • 某个 node 执行结束
  • LLM 产生了一个 token
  • 工具被调用
  • 子图被调度
  • 状态被写入 checkpoint

也就是说,streamEvents() 并不是只关注“输出是什么”,而是关注 “图是如何一步一步跑起来的”。

graph.streamEvents() 方法返回的是事件流,而非内容流,因此不依赖 streamMode

事件流举例

比如这是我们之前在讲工具调用时,写到的一段代码片段:

// 这里拿到的就是事件流,而非内容流
const events = await graph.streamEvents(
  {
    messages,
  },
  {
    version: "v2",
  },
);

process.stdout.write("助理:");
let finalText = ""; // 拼接最终完整的回复

// 针对每一次事件,做一个事件类型的判断
// 不同的事件类型,做不同的事情
for await (const ev of events) {
  if (ev.event === "on_chat_model_stream") {
    const text = ev.data.chunk.text; // 拿到此次 token 得到的文本值
    finalText += text;
    process.stdout.write(text);
  }

  if (ev.event === "on_tool_start") {
    process.stdout.write(`\n【正在调用工具 ${ev.name}】\n`);
  }
  if (ev.event === "on_tool_end") {
    process.stdout.write(`\n【调用工具 ${ev.name} 完成】\n`);
  }
}

这个地方就用到了事件流,然后根据事件的不同类型,做不同的事情。

常见类型

从架构角度看,streamEvents() 本质上是一个事件驱动接口(event-driven API),非常适合用来构建可观测性、调试工具和复杂 Agent 的执行监控。

常见的事件类型有这么几类:

第一类:模型相关事件

常见的包括:

  • on_chat_model_start:大模型开始一次调用
  • on_chat_model_stream:大模型正在流式生成 token
  • on_chat_model_end:大模型本次调用结束,完整结果已生成
  • on_chat_model_error:模型调用过程中发生错误

第二类:工具相关事件

常见的有:

  • on_tool_start:工具即将被调用
  • on_tool_end:工具调用完成
  • on_tool_error:工具执行过程中发生错误

第三类:节点执行相关事件

这一类是很多同学第一次接触时容易忽略,但非常重要的。

例如:

  • on_node_start:某个 graph node 开始执行
  • on_node_end:某个 graph node 执行完成
  • on_node_error:节点执行出错

第四类:链 / 子图 / 调度相关事件

在复杂图里,这类事件会非常有用。

例如:

  • on_chain_start
  • on_chain_end
  • on_chain_error

或者子图相关的调度事件。

示例代码:

// 遍历事件流
for await (const ev of events) {
  // 第一类:模型相关事件
  if (ev.event === "on_chat_model_start") {
    process.stdout.write(`\n【模型开始调用】\n`);
  }
  if (ev.event === "on_chat_model_stream") {
    const text = ev.data.chunk.text; // 拿到此次 token 得到的文本值
    finalText += text;
    process.stdout.write(text);
  }
  if (ev.event === "on_chat_model_end") {
    process.stdout.write(`\n【模型调用结束】\n`);
  }
  if (ev.event === "on_chat_model_error") {
    process.stdout.write(`\n【模型调用出错】\n`);
  }

  // 第二类:工具相关事件
  if (ev.event === "on_tool_start") {
    process.stdout.write(`\n【正在调用工具 ${ev.name}】\n`);
  }
  if (ev.event === "on_tool_end") {
    process.stdout.write(`\n【调用工具 ${ev.name} 完成】\n`);
  }
  if (ev.event === "on_tool_error") {
    process.stdout.write(`\n【工具调用出错 ${ev.name}】\n`);
  }

  // 第三类:节点执行相关事件
  if (ev.event === "on_node_start") {
    process.stdout.write(`\n【节点 ${ev.name} 开始执行】\n`);
  }
  if (ev.event === "on_node_end") {
    process.stdout.write(`\n【节点 ${ev.name} 执行完成】\n`);
  }
  if (ev.event === "on_node_error") {
    process.stdout.write(`\n【节点 ${ev.name} 执行出错】\n`);
  }

  // 第四类:链/子图/调度相关事件
  if (ev.event === "on_chain_start") {
    process.stdout.write(`\n【链 ${ev.name} 开始执行】\n`);
  }
  if (ev.event === "on_chain_end") {
    process.stdout.write(`\n【链 ${ev.name} 执行完成】\n`);
  }
  if (ev.event === "on_chain_error") {
    process.stdout.write(`\n【链 ${ev.name} 执行出错】\n`);
  }
}

自定义流

langgraph 的流式机制中,除了常见的:

  • values:完整状态
  • updates:状态差异
  • messages:LLM token

还有一个最灵活、企业级应用中最常用的模式:custom

这个模式允许开发者在节点内部随时主动 push 任意数据到前端。换句话说:

模型生成 token 不是唯一的数据来源,开发者也可以向前端不断推送事件。

来看一个具体的场景

例如一个企业级审批流,可能需要向前端推送:

  • 当前正在执行的中间风控流程
  • 风险评分计算步骤
  • 进度
  • 外部系统调用结果(耗时 / 成功 / 返回内容)

总之就是一些和业务相关的信息,这些信息既不是 LLM token,也不是 state 更新。要推送这种业务信息,就可以用 custom 就可以实现。

自定义流常用于下面的场景:

  • 进度条:{ progress: 37% }
  • 后端任务进度:{ stage: "fetching_data" }
  • 业务事件:{ event: "payment_verified" }
  • 中间计算结果:{ partialResult: ... }
  • 可视化所需数值,例如风控流水线、规则命中情况

前置知识

首先介绍一下 getWriter() 方法:

import { getWriter } from "@langchain/langgraph";

getWriter() 方法是 langgraph 提供的一个内部上下文函数:

  • 在节点执行期间自动绑定到当前执行上下文
  • 返回一个函数:writer(data)
  • writer(data) 可以把 data 推到外部流(graph.stream)
  • 数据类型完全由开发者控制(JSON 对象即可)

具体用法:

首先调用 getWriter() 方法得到一个 writer

const writer = getWriter();

返回的 writer 实质上是一个函数:

type Writer = (data: any) => void;

利用 writer 可以书写 custom 事件数据,例如:

writer({ step: 1, message: "处理中" });
writer({ progress: 50 });
writer({ rule: "R19", hit: true });
writer({ vectorChunk: [0.123, 0.55, ...] });

langgraph 会把这些数据包装成事件的一部分。例如:

writer({ step: 1, message: "处理中" });

外部收到的是:

{
  type: "custom",
  data: { step: 1, message: "处理中" },
  // 其他 langgraph 元信息
  node: "longTask",
  step: 1,
  timestamp: "...",
  checkpoint_ns: "...",
}
// 任务节点
async function longTask() {
  const writer = getWriter();

  if (!writer) return;

  // 书写自定义的数据
  writer({ step: 1, message: "正在准备中..." });

  await sleep(3000);

  writer({ step: 2, message: "正在处理中..." });

  await sleep(3000);

  writer({ step: 3, message: "快要完成了..." });

  await sleep(3000);

  // 更新状态
  return {
    result: "任务完成",
  };
}

// 构建图
const graph = new StateGraph(StateSchema)
  .addNode("longTask", longTask)
  .addEdge(START, "longTask")
  .addEdge("longTask", END)
  .compile();

async function main() {
  console.log("===== custom 流式输出 =====\n");

  let input = {};

  const stream = await graph.stream(input, {
    streamMode: "custom",
  });

  for await (const item of stream) {
    console.log("custom自定义流每一次的输出");
    console.log(item);
    console.log("---------------------");
  }

  console.log("\n===== 执行结束 =====");
}
main();

流式其它细节

1. debug

1. debug模式是什么?

debug 是 langgraph 流式输出中的最详细、最底层的事件模式。当你启用:

streamMode: "debug";

langgraph 会在执行图时,把所有的底层事件(包括执行路径、节点生命周期等)实时推送给你。

它属于开发者调试模式,不是给用户看的。

2. debug模式输出什么内容?

当一个图执行时,每走一步,langgraph 都会产生一类事件。在 debug 模式里可以实时看到:

  • node_start:哪个节点开始执行,包含:
    • node 名称
    • 输入状态
    • 当前 superstep
    • 路径信息
  • node_end:节点执行结束,包含:
    • 输出状态更新(delta)
    • 执行耗时
    • next steps
  • task_scheduled:有哪些节点被调度执行(内部调度信息)
  • task_completed:某个任务完成执行
  • graph_start / graph_end:整个流程开始 / 结束(可用于全局监听)
  • superstep信息:显示每次迭代的 superstep 信息

2. 子图输出

在 langgraph 中,一个节点不仅可以是函数,还能是子图:

ParentGraph
 ├─ node1(普通节点)
 └─ node2(这里是一个完整的subgraph)
      ├─ subgraphNode1
      └─ subgraphNode2

如果对 graph.stream() 进行流式监听:

  • 默认情况下,只会看到父图每个节点执行后的状态更新。
  • 子图内部节点(subgraphNode1、subgraphNode2)的执行过程不会被流式输出。

但是如果开发者要做诸如:

  • 可视化执行路径
  • 监控复杂嵌套工作流
  • 前端演示“图执行动画”

那就必须知道:每个子图内部的节点什么时候执行了、执行结果是什么。

因此 LangGraph 提供了一个配置:

subgraphs: true;

这样不仅流式输出父图的更新,还流式输出所有子图内部节点的更新。

开启子图流之后,每条流式输出是一个数组:

[namespace, update];

例如:

[[], { node1: { foo: "hi! foo" } }][
  (["node2:<task_id>"], { subgraphNode1: { bar: "bar" } })
][(["node2:<task_id>"], { subgraphNode2: { foo: "hi! foobar" } })][
  ([], { node2: { foo: "hi! foobar" } })
];

这两部分分别表示:

  1. namespace
  2. update

1. namespace

用于标记当前输出来自哪个图。结构是一个数组:

["nodeName:taskId", "childNode:taskId", ...]

它就是一条“执行路径”。举例:

["node2:dfddc4...", "subgraphNode1:ab3de..."];

含义:

  • 当前输出来自父图的节点node2
  • node2是一个子图
  • 子图里正在执行subgraphNode1

你可以理解成类似 URL 路径:

/node2/subgraphNode1

2. update

该节点执行后的 state 变化。例如:

{'subgraphNode1': {'bar': 'bar'}}

或:

{'node1': {'foo': 'hi! foo'}}

流式输出可以逐步看到:

  • 哪个节点执行了?
  • 它对 state 做了什么更新?
  • 执行顺序是什么?

示例代码:

// 演示子图的输出
import { StateGraph, START } from "@langchain/langgraph";
import { z } from "zod/v4";

// 定义子图状态
const SubState = z.object({
  foo: z.string(),
  bar: z.string(),
});

// 构建子图
const subgraph = new StateGraph(SubState)
  .addNode("subgraphNode1", () => ({ bar: "bar" })) // 更新bar的值
  .addNode("subgraphNode2", (state) => ({ foo: state.foo + state.bar })) // 更新foo的值
  .addEdge(START, "subgraphNode1")
  .addEdge("subgraphNode1", "subgraphNode2")
  .compile();

// 定义父图的状态
const ParentState = z.object({
  foo: z.string(),
  bar: z.string(),
});

const graph = new StateGraph(ParentState)
  .addNode("node1", (state) => ({ foo: "hi! " + state.foo })) // 对foo字段进行更新
  .addNode("node2", subgraph) // 子图节点
  .addEdge(START, "node1")
  .addEdge("node1", "node2")
  .compile();

async function main() {
  const stream = await graph.stream(
    { foo: "这是一个测试" },
    {
      streamMode: "updates",
      subgraph: true, // show the sub graph updates information
    },
  );

  for await (const item of stream) {
    console.log("updates流输出的每一项");
    console.log(item);
    console.log("---------------------");
  }
}
main();