学习笔记 · Obsidian

Deep Agents Python:流式前端、HITL 与状态协议

LangChainLangGraphPython

流式输出的两代接口

Deep Agents 基于 LangGraph 流式机制,并增加对子代理的第一等投影。

  • 新应用优先 event streaming(Deep Agents 0.6+):按 messages、tool_calls、values、subagents、output 分成独立迭代器。
  • 旧式 streaming 以 stream_mode 分支处理 updates、messages、custom 等 chunk;仍可用,但 UI 代码更容易混杂。
  • LangGraph 1.1+ 的 v2 格式统一为 type、ns、data,避免单模式/多模式/子图之间出现不同的 tuple 解包。

产品 UI 应优先 stream.subagents,而不是 stream.subgraphs:前者表达“用户可理解的委派任务”,后者暴露内部图执行结构。

Event streaming 模型

根 stream 表示协调者。每个 task 委派生成一个轻量 subagent handle;只有访问该 handle 的 messages、tool_calls 或 values 时才真正打开对应投影,适合按需加载 UI。

投影/字段含义
stream.messages主协调者消息和最终综合
stream.tool_calls主协调者工具调用
stream.values主图状态,如 todos、自定义状态
stream.subagents发现子代理及生命周期
subagent.name配置中的 subagent_type 名称
subagent.path该子代理命名空间
subagent.statusstarted、completed、failed、interrupted 等
subagent.messages子代理消息
subagent.tool_calls子代理内部工具调用
subagent.subagents更深层委派
subagent.output最终子代理状态/完成信号

协调者和子代理事件会交错。异步服务端用 astream_events + asyncio.gather 并发消费;同步代码可使用 interleave。若必须保持跨所有层级的精确到达顺序,消费原始协议事件并根据 namespace 路由。

旧式子图 streaming 中:

  • 空 namespace 表示主 Agent。
  • tools:任务ID 等 namespace 表示 task 启动的子代理。
  • 更深 path 表示子代理内部节点。

LLM token、工具调用块和 custom updates 都可按 namespace/lc_agent_name 归属。自动摘要步骤也可能产生 token;面向用户展示时应过滤摘要中间件节点。

前端总体架构

前端 SDK 的 useStream 把 Deep Agents 展示为“协调者 + 可发现工作者 + 共享状态 + 中断”,而不是单一聊天气泡。

前端投影UI 用途
messages主对话与最终综合
subagents专家卡片、状态、委派树
values待办、计划、报告章节、沙箱元数据
tool-call state文件、搜索、浏览器、业务动作卡片
interrupts审批、补充输入、恢复

React、Vue、Svelte、Angular 使用同一 v1 前端 SDK 思路;应传 Agent 类型参数以获得状态推断。

子代理 UI

selector-based 设计让根消息保持干净:

  • stream.messages 只渲染协调者。
  • stream.subagents 只提供发现快照。
  • useMessages(stream, subagent) 和 useToolCalls(stream, subagent) 在卡片挂载时订阅该命名空间。
  • 可按启动它的 tool_call_id 把子代理卡片插回对应协调者消息下。

SubagentDiscoverySnapshot 只有身份、namespace、status 和任务元数据,不内嵌完整消息。这允许完成的卡片折叠、按需展开,而不让 5 个以上并行专家把页面和内存同时撑大。

建议:

  1. 主区域展示协调者高层计划和综合。
  2. 子代理卡片默认显示名称、状态、当前工具和简短输出。
  3. 进行中的保持展开,完成的自动折叠;侧栏可显示全局鸟瞰。
  4. 单个子代理失败只在该卡片显示,不应使其他卡片或根 UI 崩溃。
  5. selector 只在可见组件挂载,避免无意义订阅。
  6. 仅对异常深的自定义工作流提高 recursion_limit;Deep Agents 默认已经很高。

Todo UI

任务计划从 0.7 起默认关闭。只有 Agent 加入 TodoListMiddleware,才会有 write_todos 和 stream.values.todos。

状态流程:

pending → in_progress → completed

UI 直接由 todos state 派生完成数、总数和百分比,不轮询、不维护第二份业务状态。初始 todos 为空时不显示占位面板;长列表降低已完成项视觉权重;通常只突出第一个 in_progress 项。

Todo 是 Agent 自报的执行计划,不等于后端任务队列的权威状态。它适合解释进展,不应被用作结算、权限或幂等依据。

沙箱 IDE UI

编码 Agent 的典型三栏:

  1. 文件树。
  2. 代码/差异查看器。
  3. Agent 聊天与工具进度。

后端有三部分:

  • Deep Agent 以 sandbox backend 获得 read/write/edit/delete/execute。
  • FastAPI 自定义路由供前端直接浏览沙箱文件。
  • Agent graph 与 API 路由共用同一个“按 thread 解析 sandbox”函数,sandbox_id 存在线程 metadata,不能依赖进程内 cache 作为唯一事实源。

Thread 与页面恢复

  • 默认 thread-scoped sandbox。
  • 页面首次加载创建 thread,把 thread_id 存 sessionStorage。
  • 刷新页面继续同一 thread 和 sandbox。
  • “新会话”清除旧 thread_id,下一次创建新环境。
  • 多租户改用经过认证的用户/assistant scope,并在服务端验证 thread 所有权。

实时文件同步

每次运行前保存原始文件快照。流中看到以下 ToolMessage 后更新:

  • write_file、edit_file、delete:刷新目标文件/目录。
  • execute:命令可能修改任意文件,刷新整个文件树。

当前内容与运行前快照比较得到 changed files;用户打开改动文件时默认展示 diff,并显示增加/删除行统计。只读工具在聊天里显示紧凑摘要,避免把 read_file 全文复制一遍。

过滤 node_modules 等大目录;自定义 http.app 路由会优先于默认 LangGraph 路由,命名时避免意外遮蔽 /threads、/runs。

Human-in-the-loop

interrupt_on 为工具名配置审批:

  • True:默认允许 approve、edit、reject、respond。
  • False:不拦截。
  • InterruptOnConfig:限定 allowed_decisions,可加 when 条件谓词。
决策语义注意
approve原参数执行适合已确认动作
edit修改参数再执行大幅修改可能使模型重新规划或重复动作
reject不执行,返回拒绝反馈副作用工具的正确拒绝方式
respond人直接作为工具结果仅适合 ask_user 类工具,不应用于拒绝副作用

reject 消息应明确“工具未执行”和下一步:放弃、补充信息或选择更安全方案。respond 会被模型当作工具成功结果,错误使用会造成状态误判。

条件审批

when 谓词接收 ToolCallRequest,可根据参数只拦截高金额、外部地址或敏感路径。未命中条件的调用不会进入审批 batch。条件中断需要 langchain 1.3.3+。

文件权限 mode=interrupt 会对匹配的 write_file、edit_file、delete 产生同类中断,并与 interrupt_on 合并。

恢复协议

HITL 的硬性条件:

  1. Agent 必须有 checkpointer。
  2. 首次调用与恢复必须用相同 thread_id。
  3. 读取 result 中的 interrupts/action_requests。
  4. 多个被拦截调用会批量返回;decisions 必须与 action_requests 同序。
  5. 用 Command(resume=...) 恢复,而不是重新发送原消息。

如果运行在工具返回前取消或中断,PatchToolCallsMiddleware 会修复消息历史中的悬空工具调用,减少恢复后的协议错误。

子代理中断

声明式子代理可有自己的 interrupt_on;也可以在子代理工具内部直接调用 interrupt。父/子恢复都依赖同一持久状态语义。

必须特别注意两条旁路:

  • PTC 从解释器桥接工具,不走常规工具调用路径,不会逐次触发父 Agent interrupt_on。
  • 动态子代理从 eval 内 task() 调度,也不会逐次走父 task 审批。

需要覆盖这两种能力时,对 eval 本身做审批,并缩小解释器 allowlist。

状态一致性原则

前端看到的是运行投影,不应自创第二个事实源:

  • 对话连续性:thread_id。
  • 沙箱归属:服务端 thread metadata。
  • Agent 共享状态:stream.values / checkpoint。
  • 子代理身份:namespace + lc_agent_name + tool_call_id。
  • 异步后台任务:专用 async_tasks channel,不靠历史消息。
  • 审批恢复:interrupt action 顺序 + 同一 thread。

UI 可缓存用于体验,但任何刷新、重连和授权判断都要回到服务端 thread/run 状态。

上线检查

  • 断网重连后能继续同一 run/thread,不丢进度。
  • 同一协调者消息下能正确关联多个并行 subagent 卡片。
  • 子代理失败、取消、中断均有独立状态。
  • 自动摘要 token 不作为用户最终回复展示。
  • Todo 未启用时 UI 不假定 todos 存在。
  • 审批 batch 的顺序、拒绝反馈、同 thread 恢复均有测试。
  • execute 后全量文件刷新;delete 后文件树同步。
  • thread_id 不能由用户越权访问;所有自定义文件 API 做服务端授权。
  • 差异与沙箱产物按不可信内容渲染,防止 HTML/script 注入。

延伸阅读