学习笔记 · Obsidian
TypeScript 流式、HITL、子图与前端 SDK
两层 streaming API
LangGraph TypeScript 同时存在两层流式接口:
| 层级 | 入口 | 适用场景 |
|---|---|---|
| Event streaming | streamEvents,v3 protocol | 新应用、类型化消息/State/子图/interrupt 投影 |
| Stream-mode API | stream + 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 |
| updates | node 级 State delta |
| messages | content block 与 token delta |
| tools | 工具 start/progress/finish/error |
| lifecycle | run、subgraph、subagent 状态 |
| checkpoints | time travel 所需轻量 checkpoint 事件 |
| input | HITL 请求与响应 |
| tasks | Pregel task 创建与结果 |
| custom | node/tool 自定义数据 |
| custom:name | transformer 命名扩展 |
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 过滤;
nostreamtag 可让一次模型调用运行但不进入 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 必须具备:
- checkpointer;
- 稳定 thread_id;
- JSON 可序列化 payload;
- 对 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 循环
交互式客户端应循环执行:
- streamEvents(input/Command, same config);
- 消费 messages、values 或 subgraphs;
- 检查 stream.interrupted;
- 若暂停,渲染 payload 并收集用户输入;
- 以 new Command({ resume }) 再次 streamEvents;
- 直到 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。它们说明概念,但不能作为可编译代码的唯一依据。
落地时以三层证据为准:
- 当前 TypeScript reference 和导出;
- 本项目锁定版本的类型检查;
- 本地 Agent Server 与目标浏览器的真实端到端测试。