学习笔记 · Obsidian
Deep Agents Python:流式前端、HITL 与状态协议
流式输出的两代接口
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.status | started、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 个以上并行专家把页面和内存同时撑大。
建议:
- 主区域展示协调者高层计划和综合。
- 子代理卡片默认显示名称、状态、当前工具和简短输出。
- 进行中的保持展开,完成的自动折叠;侧栏可显示全局鸟瞰。
- 单个子代理失败只在该卡片显示,不应使其他卡片或根 UI 崩溃。
- selector 只在可见组件挂载,避免无意义订阅。
- 仅对异常深的自定义工作流提高 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 的典型三栏:
- 文件树。
- 代码/差异查看器。
- 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 的硬性条件:
- Agent 必须有 checkpointer。
- 首次调用与恢复必须用相同 thread_id。
- 读取 result 中的 interrupts/action_requests。
- 多个被拦截调用会批量返回;decisions 必须与 action_requests 同序。
- 用 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 注入。