Skip to content

perf(api): state 查询改用零工具骨架图,秒级降到毫秒级 - #1013

Open
zgpnuaa wants to merge 3 commits into
xerrors:mainfrom
zgpnuaa:perf/state-reader-skeleton
Open

perf(api): state 查询改用零工具骨架图,秒级降到毫秒级#1013
zgpnuaa wants to merge 3 commits into
xerrors:mainfrom
zgpnuaa:perf/state-reader-skeleton

Conversation

@zgpnuaa

@zgpnuaa zgpnuaa commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

现象

前端 state 面板(查看状态)查询很慢,重型 Agent 要 60-80 秒,用户点「状态」后长时间空白。子智能体 workdir 各异,缓存更是全 miss。

机理

_read_checkpoint_state 为了拿 checkpoint,调用了 完整的 agent.get_graph(context=context)

graph = await agent.get_graph(context=context)   # 完整构图
return await graph.aget_state(langgraph_config)

问题在于:完整构图需要连接全部 MCP server 装配工具(重型 Agent 60-80s),而这整条链路的目的只是为了「读一个 checkpoint」——state 读取根本不需要工具。

改进方法

新增 state 查询专用骨架图

  • _build_state_reader_schema():用 _resolve_schemas 合并 middleware 注入字段(TodoListMiddleware/TokenUsageMiddleware/SkillsMiddleware + ChatBotState),得到与真实 agent graph 一致的 state schema
  • _get_state_reader_graph():全局单例的零工具 StateGraph(一个 noop 节点 + entry),毫秒级编译,复用同一个 checkpointer;
  • _read_checkpoint_state 改读骨架图。

schema 必须与真实 graph 一致:若只用裸 ChatBotStateaget_state 会按骨架图 schema 过滤 checkpoint,丢掉 todos/token_usage/activated_skills 等 middleware 注入字段,前端 state-panel 的待办和 token 用量会消失。骨架图用 _resolve_schemas 保证字段完整。

效果

state 面板查询从秒级(重型 Agent 60-80s)降到毫秒级(骨架图全局单例、编译一次永久复用)。子智能体 workdir 各异导致的缓存 miss 问题一并消失。新增单测断言骨架图 schema 含 todos/token_usage/activated_skills/subagent_runs/artifacts

@xerrors

xerrors commented Sep 10, 2026

Copy link
Copy Markdown
Owner

Codex Review:

仅供参考:本轮预 review 基于 9a018bd0b637,不是正式批准或合并结论。

[P1] 骨架图丢失 checkpoint 中的中断任务,审批/提问无法从状态接口恢复。 chat_service.py:1730–1740 只注册 state_reader_noop,但 aget_state 不只按 schema 读 values,还按当前图的节点/触发器重建 tasks。原图停在 tools 或中间件审批节点时,骨架图没有这些节点,返回空 tasks;本服务 _extract_interrupt_infostate.tasks[*].interrupts 提取审批,因此 get_agent_state_view 不再返回 interrupt。用户在等待工具批准/回答问题时刷新页面,将丢失继续操作的入口。

已执行最小复现:使用本 PR 的真实 _build_state_reader_schema、LangGraph StateGraphInMemorySaver,由 tools 节点调用 interrupt(...) 写入 checkpoint,再分别用原图与本 PR _get_state_reader_graph 读取同一份数据。原图返回 1 个 task 和非空 interrupt;骨架图返回 0 个 task,_extract_interrupt_info 为 None。这个差异不依赖模型或 MCP。

请保留原执行图的中断/待处理写入语义,或在 checkpoint 读取边界完整恢复它们;只断言 schema 包含字段不足以证明状态等价。应增加真实“审批中断 → 刷新状态接口 → 展示审批 → resume”的回归,同时覆盖普通已完成状态。

验证:该 head 快照执行 PYTHONPATH=package:server python -m pytest test/unit/services/test_chat_service_sync.py -q --disable-warnings --tb=short,25 passed;另执行上述真实 LangGraph 内存 checkpoint 对照复现。未运行 PostgreSQL/HTTP/浏览器 E2E;新测试只检查 type hints,修改后的接口测试 mock 掉了 _read_checkpoint_state,未覆盖缺陷。请补此行为变更的 tracked decision。

骨架图只有 state_reader_noop 节点,aget_state 无法按原图节点重建 tasks,
等待审批的 checkpoint 在骨架图下 tasks 为空、_extract_interrupt_info 取不到
中断,用户刷新状态接口会丢失审批/提问的继续入口。

改为直接从 CheckpointTuple.pending_writes 的 __interrupt__ channel 恢复,
不依赖执行图结构。仅在 Run 状态为 interrupted 时回退读取,普通状态查询
零额外开销。
@zgpnuaa

zgpnuaa commented Sep 10, 2026

Copy link
Copy Markdown
Contributor Author

感谢 review,复现准确,已修复(最新 head a7bfacac)。

[P1] 已修复:中断不再依赖图结构,从 checkpoint 原始写入恢复

确认根因:aget_state 不只按 schema 读 values,还按当前执行图的节点/触发器重建 tasks。骨架图只有 state_reader_noop,原图停在审批节点的 checkpoint 在骨架图下 tasks 为空,_extract_interrupt_info(首选 state.tasks[*].interrupts)因此取不到中断。

修复:新增 _read_pending_interrupt,直接用 checkpointer 的 aget_tupleCheckpointTuple.pending_writes,返回 __interrupt__ channel 里的中断值——不经过 get_state、不依赖执行图结构。调用点只在 get_agent_state_viewlatest_run.status == "interrupted" 时回退读取,普通状态查询不额外读一次 checkpoint,保留骨架图的毫秒级收益。

骨架图继续负责 values(待办/用量/消息),两条路径共同覆盖「真实图 in-flight 中断」与「历史/刷新后的中断」。

回归测试(真实 LangGraph checkpoint,非 mock)

新增 test_state_reader_interrupt_recovery.py,用 InMemorySaver 让 tools 节点 interrupt(...) 写入真实 checkpoint:

  • 记录骨架图固有限制:原图 tasks=1interrupts 非空,骨架图 tasks=0_extract_interrupt_info 返回 None(正是你复现的现象);
  • 修复点:_read_pending_interrupt 恢复出的中断 id/value 与真实图 tasks[0].interrupts[0] 完全一致;
  • 骨架图 values 与真实图一致(确认性能优化未被破坏);
  • 负向:已完成、无中断的 checkpoint 返回 None;不存在的 thread 返回 None

tracked decision:docs/develop-guides/decisions/implemented/2026-09-10-state-reader-preserves-interrupts.md

验证

  • 该文件 4 passed,且断言用真实 LangGraph checkpoint(非 mock _read_checkpoint_state);
  • 全量 unit 1934 passed / 52 skipped
  • ruff check + ruff format --check + verify_engineering_contracts.py 通过。

未运行 PostgreSQL/HTTP/浏览器 E2E;中断值恢复这一层用真实 LangGraph checkpoint 对照验证,完整「审批中断 → 刷新 → resume」的浏览器回归如需要我再补。

@xerrors

xerrors commented Sep 11, 2026

Copy link
Copy Markdown
Owner

建议完成真实场景的测试

补真实场景测试:用真实 PG 的 AsyncPostgresSaver 构造停在 interrupt 的 checkpoint,
验证 _read_pending_interrupt 从持久化 pending writes 的 __interrupt__ channel 恢复
中断;并覆盖已完成、无中断的 checkpoint 返回 None。这是 InMemorySaver 单测覆盖不到
的存储后端差异。
@zgpnuaa

zgpnuaa commented Sep 11, 2026

Copy link
Copy Markdown
Contributor Author

感谢 review,已补真实场景测试(最新 head 80141d0a)。

补真实 PostgreSQL 集成测试

之前只有 InMemorySaver 的单元测试,现补 backend/test/integration/services/test_state_reader_interrupt_integration.py,用真实 PostgreSQL 的 AsyncPostgresSaver 验证「持久化 pending writes 里的 __interrupt__ channel」在真实存储后端上同样可读——这是 InMemorySaver 覆盖不到的存储格式差异:

  • 停在 interrupt 的真实 checkpoint:_read_pending_interrupt 从持久化 pending writes 恢复中断,value 与写入一致;
  • 已完成、无中断的真实 checkpoint 返回 None

2 个用例真实 PG 通过(另附 InMemorySaver 的 4 个单元用例)。「审批中断 → 刷新状态接口 → 展示审批 → resume」的浏览器回归我可以再用 playwright 补一轮截图,如需要请说。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants