学习笔记 · Obsidian

TypeScript 流式、HITL、子图与前端 SDK

LangChainLangGraphTypeScript

两层 streaming API

LangGraph TypeScript 同时存在两层流式接口:

层级入口适用场景
Event streamingstreamEvents,v3 protocol新应用、类型化消息/State/子图/interrupt 投影
Stream-mode APIstream + streamMode低层 graph runtime 事件和特定 mode

官方建议新应用优先 event streaming。它在 v1.2 引入类型化投影,让不同消费者独立读取同一底层事件流;低层 API 仍适合协议诊断、自定义运行时工具或必须精确处理 raw mode 的应用。

对远程 Agent Server 的流式消费使用 LangSmith/Agent Server Streaming API 或 @langchain/langgraph-sdk,不能把进程内 graph 方法等同于网络协议。

Event streaming

基本形态:

const stream = await graph.streamEvents(input, { version: "v3" });

for await (const message of stream.messages) { for await (const token of message.text) { render(token); } }

const finalState = await stream.output;

一个 run stream 提供:

  • stream:所有 ProtocolEvent;
  • stream.messages:消息与 token;
  • stream.values:每步完整 State;
  • stream.output:最终输出 Promise;
  • stream.subgraphs:嵌套 graph run;
  • stream.interrupts:HITL payload;
  • stream.interrupted:是否因人工输入暂停;
  • stream.extensions:自定义 transformer 投影。

多个 projection 可以并发消费。读取 messages 不会耗掉 values、subgraphs 或 output 的事件;在 JavaScript 中可用 Promise.all 并行启动多个 consumer。

ProtocolEvent

raw event 的关键结构:

  • seq:同一 run 内严格递增,排序应依据它;
  • method:channel 名;
  • params.namespace:从根图到当前 scope 的路径;
  • params.timestamp:墙钟时间,可能漂移,不用于严格排序;
  • params.node:可选 node 名;
  • params.data:channel-specific payload。

根 namespace 是空数组;子 scope 追加 name:runtime_id。稳定逻辑应使用 name/路径结构,不把 runtime_id 当业务主键。

channels

channel内容
values完整 State snapshot
updatesnode 级 State delta
messagescontent block 与 token delta
tools工具 start/progress/finish/error
lifecyclerun、subgraph、subagent 状态
checkpointstime travel 所需轻量 checkpoint 事件
inputHITL 请求与响应
tasksPregel task 创建与结果
customnode/tool 自定义数据
custom:nametransformer 命名扩展

messages 以 message-start、content-block-start/delta/finish、message-finish 表达,能统一 text、reasoning、tool-call 与多模态 block。tool 事件用 tool call ID 与消息中的 tool call 关联。

Stream transformer

Transformer 观察 ProtocolEvent、维护自己的状态,并把派生视图发布到 StreamChannel。接口包含 init、process、可选 finalize/fail。

required stream modes 决定底层 Pregel 实际启用哪些 mode;多个 transformer 的需求取并集。声明某个 mode 只是启用上游事件,process 仍会收到已启用的全部 event,需自行按 method 过滤。

两种 StreamChannel:

  • unnamed:只存在于进程内 stream.extensions,可承载 promise、async iterable、class instance 等不可序列化对象;
  • named:除 extensions 外,还会作为 custom:<name> 进入主协议;payload 必须可序列化。

Transformer 可以在调用 streamEvents 时注册,也可以 compile 时固定到 graph。只有所有 run 都需要的协议扩展才适合 compile-time 注册。

低层 streamMode

常用 mode:

  • updates:每个 node 的 State 更新;同一步多个更新分别发出;
  • values:每步完整 State;
  • messages:[messageChunk, metadata],即使模型用 invoke 调用也可产生 token;
  • custom:node/tool 通过 writer 发出的任意数据;
  • tools:工具生命周期事件;
  • debug:最大量运行时信息。

一次传多个 mode 时,chunk 形态是 [mode, data]。启用 subgraphs: true 后,会再带 namespace;解析代码必须按所选 mode 和 subgraphs 选项处理实际 tuple 形态。

messages 过滤

  • 给模型调用加 tags,可按 metadata.tags 过滤;
  • nostream tag 可让一次模型调用运行但不进入 messages;
  • 按 metadata.langgraph_node 过滤具体 node;
  • 不支持 LangChain chat interface 的模型可通过 custom mode 转发自己的 token。

内部结构化模型输出、审查模型等通常应加 nostream,避免把内部推理或重复内容发给用户。

tools mode

普通 Promise 工具自动产生 start/end;async generator 工具的每次 yield 产生 progress event,return 成为最终 ToolMessage 内容。事件包括:

  • on_tool_start;
  • on_tool_event;
  • on_tool_end;
  • on_tool_error。

前端 useStream 在包含 tools mode 时可维护 toolProgress,记录 starting/running/completed/error、input、data、result 和 error。

tools mode 适合结构化工具生命周期;custom mode 适合不属于工具生命周期的自由业务事件。

interrupt 的执行语义

interrupt 在 node 中抛出运行时控制异常,checkpointer 保存当前状态,run 返回 __interrupt__ payload,并无限等待应用用同一 thread_id 恢复。

恢复方式:

await graph.invoke( new Command({ resume: humanValue }), sameConfig, );

恢复值成为 interrupt 调用的返回值。但 node 不是从那一行继续,而是从函数开头重新执行。

使用 interrupt 必须具备:

  1. checkpointer;
  2. 稳定 thread_id;
  3. JSON 可序列化 payload;
  4. 对 node replay 和副作用的明确设计。

interrupt 的硬规则

不用裸 try/catch 包 interrupt

interrupt 通过特殊异常暂停。裸 catch 会吞掉控制异常。需要捕获其他错误时,把 interrupt 移出 try/catch,或只处理明确业务异常并重新抛出未知异常。

不改变同一 node 内 interrupt 顺序

resume value 按任务内调用位置匹配。不能在旧 run 未完成时:

  • 插入、删除或重排之前的 interrupt;
  • 根据非确定性数据跳过某个 interrupt;
  • 用长度会变化的循环动态调用 interrupt。

输入校验应让 node 返回 State,再通过 conditional edge 回到同一个单 interrupt 节点,而不是在 node 内用动态 while 循环不断 interrupt。

payload 保持可序列化

只传 string、number、boolean、array 和普通 object。不要传函数、数据库连接、class instance 或 framework 对象。

interrupt 前的副作用必须幂等

恢复会重跑 node 开头。优先:

  • interrupt 后再执行真实写操作;
  • 把审批与动作拆成两个 node;
  • 前置操作采用 upsert 和稳定幂等键;
  • 不在 interrupt 前 append、创建无唯一约束记录或发送通知。

多个并行 interrupt

并行分支可能一次返回多个 interrupt。应用要读取每个 interrupt ID,构造 { [interruptId]: resumeValue } 映射,一次 Command.resume 正确配对;不能只按数组顺序猜测。

流式 HITL 循环

交互式客户端应循环执行:

  1. streamEvents(input/Command, same config);
  2. 消费 messages、values 或 subgraphs;
  3. 检查 stream.interrupted;
  4. 若暂停,渲染 payload 并收集用户输入;
  5. 以 new Command({ resume }) 再次 streamEvents;
  6. 直到 interrupted 为 false,再 await output。

resume 是 invoke/stream 输入中唯一常规 Command 模式。update/goto/graph 主要由 node 返回,不用于普通前端 turn。

静态 interruptBefore/interruptAfter 是调试 breakpoint,不是生产 HITL 替代品。

子图的两种通信方式

父子 State 不同

在父 node 函数内调用 subgraph.invoke,由 wrapper 显式完成:

  • parent State -> subgraph input;
  • subgraph output -> parent update。

这适合独立 agent 私有消息历史或不同团队组件,但 wrapper 本身是边界,需要校验与错误映射。

父子共享 State key

把 compiled subgraph 直接 addNode。共享 key 自动作为输入输出 channel;父图不需要知道子图内部节点。

若子图写父图未声明或 output schema 隐藏的字段,不能假设父图一定可观察;接口应明确共享 key、reducer 和 ownership。

子图持久化、并发与可观察性

  • 默认 per-invocation:继承父 checkpointer,每次调用独立,支持当前调用 interrupt;
  • checkpointer: true:per-thread,跨调用积累 State,但同一子图不能并行多次调用;
  • checkpointer: false:stateless,无 interrupt、恢复和 state inspection。

多个不同 per-thread 子图要使用唯一稳定 node 名生成 namespace。同一 per-thread 子图作为工具时,应禁用模型对该工具的 parallel tool calling,或用调用限制中间件序列化。

getState(config, { subgraphs: true }) 只能检查运行时可静态发现的子图:直接加为 node,或在已知 node 内调用。隐藏在任意 tool/其他间接层中的子图通常无法这样发现,但 interrupt 仍会向顶层传播。

观察嵌套输出优先 event streaming 的 stream.subgraphs;低层 stream 也可 subgraphs: true,但调用者要解析 namespace tuple。

前端架构

LangGraph 前端不是只能显示一个聊天气泡。named node、State key、checkpoint、interrupt、subgraph 和 stream metadata 都可以映射为 UI:

  • node -> 卡片、时间线步骤、状态 badge;
  • State key -> typed result panel、table、chart;
  • interrupt -> 审批/编辑表单;
  • checkpoint -> 历史和 fork;
  • subgraph -> 可折叠的嵌套执行;
  • tool lifecycle -> 进度卡和错误状态。

v1 前端包覆盖 React、Vue、Svelte、Angular:

  • React:useStream;
  • Vue/Svelte:同名 useStream,按各自响应式模型读取;
  • Angular:injectStream;
  • 当前包名使用 @langchain/react、@langchain/vue、@langchain/svelte、@langchain/angular。

不要混用旧示例中的 @langchain/langgraph-sdk/react 与 v1 包名;升级时按各框架 migration guide 和当前 reference 核对。

图执行卡

useStream 的 stream.subgraphs 会发现实际执行的 node,而不是要求前端硬编码全部步骤。SubgraphDiscoverySnapshot 提供 nodeName 与 pending/running/complete/error 状态。

推荐:

  • 从当前 run 的 discovery map 渲染动态 pipeline;
  • 用 useMessages(stream, node) 读取 node-scoped token 与最终消息;
  • 只有确实需要 State 字段时才读 stream.values;
  • 完成节点自动折叠,错误按 node 展示;
  • partial Markdown 使用能容忍未闭合语法的 renderer;
  • 条件分支未执行的节点不会自动出现在 discovery map。

node 名和稳定 State key 是前后端合同。重命名它们既影响 checkpoint,也影响 UI。

Custom stream channel 到浏览器

服务端 transformer 可用命名 StreamChannel 发布 custom:<name>。前端有两个读取层次:

selector入参返回适用
useExtension/injectExtension裸 name最新 typed payload进度、状态、累计值
useChannel/injectChannel完整 custom:name有界 raw event buffer日志、审计、历史

useChannel 的 bufferSize 控制容量,replay 控制 selector mount 时是否回放已看到事件。raw payload 位于 event.params.data,需要应用自行解包。

服务端 StreamTransformer 与 StreamChannel 需要 @langchain/langgraph >= 1.3.1。远程 channel 应使用可序列化 payload。

custom channel 可用于进度、token 成本、引用、域事件与合规统计。原文示例还展示 transformer 在事件抵达浏览器前原地清理 PII,再发布脱敏计数;这是有用的 defense-in-depth,但不能替代服务端数据最小化、访问控制与日志脱敏。

Node 与浏览器安全边界

  • 模型 API key、LangSmith secret、数据库凭据只放服务端。
  • 浏览器连接 Agent Server 时必须有用户级认证与租户授权。
  • 不把 thread_id 当授权凭据;服务端仍需确认该用户可访问该 thread。
  • custom channel 也属于对外 API,需做 schema、大小限制和敏感字段审查。
  • raw values/debug stream 可能泄露内部 State,生产客户端只订阅必要 projection。
  • 断线重连要使用 SDK 的 resumable stream/thread 能力,不自行拼接不完整 token。
  • UI 发送 resume 前要校验 interrupt ID、当前 thread 状态与用户权限,防止重复审批。

文档实现边界

部分 TypeScript 页面仍出现 Python 风格的 stream_events、stream_mode、create_agent、checkpointer=True 或 snake_case reference 链接;一些长示例还有重复 import/code fence。它们说明概念,但不能作为可编译代码的唯一依据。

落地时以三层证据为准:

  1. 当前 TypeScript reference 和导出;
  2. 本项目锁定版本的类型检查;
  3. 本地 Agent Server 与目标浏览器的真实端到端测试。