# 小财管家 LangGraph Runtime 迁移计划 > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** 把小财管家的感知、规划、记忆、确认和行动逐步迁到 LangGraph runtime,减少自研流程编排里的隐藏状态和分支 bug。 **Architecture:** 保留现有 FastAPI API、Pydantic schema、`RuntimeChatService` 模型供应商抽象和业务服务;LangGraph 只接管 agent 编排。副作用动作必须通过白名单 action node 调用现有业务服务,保存草稿、提交和关联单据都不能由模型文本直接触发。 **Tech Stack:** Python 3.11、FastAPI、Pydantic、LangGraph 1.x、SQLAlchemy、现有 `AgentConversationService` / `StewardFlowStateService`。 --- ## 当前状态 已经完成第一阶段接入: - `server/src/app/services/steward_graph_planner.py` - 已新增 `StewardGraphPlannerService`。 - 已用 LangGraph `StateGraph` 编排 `prepare_context -> detect_model_intent -> done/off_topic/rule_fallback`。 - `server/src/app/services/steward_graph_runtime.py` - 已新增 `StewardGraphRuntime`。 - 槽位决策已编排为 `slot_prepare_context -> slot_tool_decision -> done/rule_fallback`。 - 运行时决策已编排为 `runtime_memory_context -> runtime_action_decision/runtime_tool_decision -> done/rule_fallback`。 - 图内失败会降级到原有规则兜底;端点层图运行异常会回退旧 Agent。 - `server/src/app/services/steward_action_contracts.py` - 已新增 `StewardActionPlanBuilder`。 - 已把申请、报销、保存草稿、直接提交规划成 `StewardActionStep` 白名单动作。 - `StewardGraphPlannerService` 已增加 `attach_action_steps` 节点,计划返回前统一补全动作列表。 - `server/src/app/services/steward_action_executor.py` - 已新增 `StewardActionExecutor`。 - 已接入 `/steward/actions/execute`,支持未知动作拒绝、缺字段阻断、提交确认门禁、重复申请 precheck、申请/报销草稿真实执行、关联申请单和附件关联。 - 申请动作复用 `UserAgentService` 的申请保存/提交能力,报销草稿复用 `ExpenseClaimService.save_or_submit_from_ontology()`。 - `server/src/app/services/steward_graph_action_runtime.py` - 已新增 `StewardGraphActionRuntime`。 - 已用 LangGraph `StateGraph` 编排 `action_checkpoint_load -> action_execute_node -> action_checkpoint_persist`。 - 已通过 `conversation_id + client_trace_id` 在 `AgentConversation.state_json` 中记录 checkpoint、pending interrupt 和幂等重放结果。 - `web/src/services/steward.js` / `web/src/views/scripts/stewardPlanModel.js` - 前端已新增 `executeStewardAction()` 服务方法。 - steward suggested action 已携带服务端可执行 action step,旧申请预览流仍保留兜底。 - `web/src/composables/workbenchAiMode/useWorkbenchAiActionRouter.js` - AI 工作台点击 `steward_execute_action` 时会调用 `/steward/actions/execute`。 - 直接提交会先执行 `run_duplicate_precheck`,precheck 通过后再提交申请。 - `server/src/app/api/v1/endpoints/steward.py` - `/steward/plans` 和 `/steward/plans/stream` 默认使用 `StewardGraphPlannerService`。 - `/steward/slot-decisions` 和 `/steward/runtime-decisions` 默认使用 `StewardGraphRuntime`。 - `STEWARD_AGENT_RUNTIME=legacy` 可回退旧 `StewardPlannerService`。 - `server/pyproject.toml` / `server/uv.lock` - 已加入 `langgraph>=1.2.0,<2.0.0`。 - `server/tests/test_steward_graph_planner.py` - 已覆盖默认 LangGraph、显式 LangGraph、legacy 回退、模型成功计划和模型失败后规则兜底。 - `server/tests/test_steward_graph_runtime.py` - 已覆盖槽位工具节点、图内规则兜底、运行时记忆合并、确定性行动选择和端点级 legacy 兜底。 - `server/tests/test_application_fact_resolver.py` - 已覆盖 `交通火车` 不污染申请事由,以及 `高铁往返` 这类业务描述仍保留。 当前已完成规划入口、slot decision、runtime decision、基础 action contract、第一版 action executor、前端点击执行闭环,以及 action runtime 的 checkpoint / interrupt / 幂等重放。 真实外部模型连通性仍需要用当前 MiniMax / backup 配置再回放确认。 ## 官方能力边界 LangGraph 官方文档强调它是低层 agent orchestration runtime,重点能力是 durable execution、streaming、human-in-the-loop 和 persistence。 本项目只采用这些底层编排能力: - Graph API:用 state、node、edge 表达流程,node 可以是 LLM 调用,也可以是普通业务代码。 - Persistence:用 checkpointer 保存 thread scoped state,用 store 保存跨线程长期信息。 - Interrupt:在需要用户确认时暂停 graph,等待外部输入后恢复。 参考: - - - - ## 总体目标架构 ```text 用户输入 / 前端上下文 / 附件 ↓ StewardGraphRuntime ├── prepare_context_node ├── model_intent_node ├── slot_decision_node ├── flow_guard_node ├── action_plan_node ├── human_review_node ├── action_execute_node ├── persist_state_node └── response_adapter_node ↓ 现有前端协议:StewardPlanResponse / StewardRuntimeDecisionResponse / StewardSlotDecisionResponse ``` 关键原则: - API 响应协议稳定,前端不因为 runtime 替换被迫大改。 - 模型只负责理解、拆步骤、给候选 action,不直接写库。 - 所有副作用节点必须调用现有业务服务,并保留确认和审计。 - `RuntimeChatService` 继续是唯一模型调用入口,LangGraph 不直接管理供应商密钥。 - `StewardFlowStateService` 和 `AgentConversationService` 是 checkpoint 落库的第一候选,不另起一套状态源。 ## Runtime State 草案 第一版 graph state 用 `TypedDict`,后续需要持久化时再评估 Pydantic schema。 ```python class StewardGraphState(TypedDict, total=False): request: StewardPlanRequest conversation_id: str thread_id: str message: str base_date: date steward_state: dict[str, Any] model_call_traces: list[dict[str, Any]] intent_result: StewardIntentAgentResult | None slot_decision: StewardSlotDecisionResponse | None runtime_decision: StewardRuntimeDecisionResponse | None action_plan: list[StewardActionStep] pending_interrupt: dict[str, Any] action_result: dict[str, Any] response: StewardPlanResponse | StewardRuntimeDecisionResponse | StewardSlotDecisionResponse fallback_reason: str ``` `thread_id` 推荐使用现有 `conversation_id`,这样 LangGraph checkpoint、业务会话和前端会话能对齐。 ## 节点边界 ### 感知节点 - `prepare_context_node` - 输入:`StewardPlanRequest` 或 runtime decision request。 - 输出:清洗后的消息、base date、当前 conversation state。 - 禁止:模型调用、数据库写入。 - `model_intent_node` - 调用:`StewardIntentAgent.detect()`。 - 约束:保留 `timeout_seconds=10`、`max_attempts=3`、`use_failure_cooldown=False`。 - 失败:只写入 `fallback_reason` 和 `model_call_traces`,不吞掉错误上下文。 ### 决策节点 - `slot_decision_node` - 迁移对象:`StewardSlotDecisionAgent.decide()`。 - 目标:让字段缺口判断进入同一个 graph state,不再由前端和后端多个入口各自推断。 - `runtime_decision_node` - 迁移对象:`StewardRuntimeDecisionAgent.decide()`。 - 目标:用户补字段、确认流程、取消当前动作、继续下一任务都走 graph routing。 - `flow_guard_node` - 负责申请/报销歧义、重复单据、时间重叠、低置信度、off-topic。 - 低置信度或强冲突必须进入确认或阻断,不允许继续 action 执行。 ### 行动节点 - `action_plan_node` - 输出白名单步骤,例如 `build_application_preview`、`save_application_draft`、`submit_application`。 - 禁止输出自由文本 action。 - `human_review_node` - 对保存草稿、提交审批、关联申请单等副作用动作生成 interrupt payload。 - 第一阶段可继续返回前端确认动作;第二阶段再接 LangGraph `interrupt()`。 - `action_execute_node` - 只能调用现有业务服务。 - 必须幂等,或在执行前检查动作是否已完成。 - 提交类动作必须检查 `confirmation_required` 和用户确认来源。 - `persist_state_node` - 合并 `StewardFlowStateService.merge_plan()` / `merge_state()`。 - 后续接入 LangGraph checkpointer 后,这里负责业务状态与 checkpoint 的一致性。 ### 响应节点 - `response_adapter_node` - 输出现有 Pydantic response。 - 必须保留 `model_call_traces`、`planning_source`、`steward_state`。 ## 分阶段计划 ### Phase 0:规划入口接入 LangGraph 状态:已完成。 完成标准: - [x] 默认 runtime 为 `langgraph`。 - [x] `STEWARD_AGENT_RUNTIME=legacy` 可回退。 - [x] `/steward/plans` 真实接口仍返回兼容的 `StewardPlanResponse`。 - [x] 模型失败后仍按 10s * 3 次再规则降级。 ### Phase 1:修通模型成功路径 目标:让真实保存草稿话术至少能走一次 `llm_function_call` 成功路径。 文件: - 修改:`server/src/app/services/runtime_chat.py` - 修改:`server/src/app/services/settings.py` - 修改:`server/tests/test_runtime_chat_service.py` - 测试:`server/tests/test_steward_intent_agent.py` 步骤: - [x] 写失败测试:模拟 main provider 超时但 backup provider 可用时,`complete_with_tool_call()` 返回 backup 的 tool call。 - [x] 确认 backup function calling 会在 main 失败后返回 tool call,不等待 main 重试结束。 - [ ] 写失败测试:MiniMax 返回非 OpenAI tool call 兼容格式时,服务端能记录清晰 trace 并降级。 - [x] 容器内运行: ```bash docker exec -w /app -e SERVER_VENV_DIR=/tmp/x-financial-server-venv x-financial-local-linux \ timeout 60s /tmp/x-financial-server-venv/bin/pytest -q \ server/tests/test_runtime_chat_service.py \ server/tests/test_steward_intent_agent.py ``` 验收: - [ ] 真实 `/api/v1/steward/plans` 对保存草稿原句返回 `planning_source=llm_function_call`。 - [x] `model_call_traces` 能区分 main 失败、backup 成功或全失败。 ### Phase 2:slot/runtime decision 迁入 graph 目标:把字段缺口和运行时动作判断从独立 agent 类迁进同一个 graph runtime。 状态:已完成。 文件: - 已创建:`server/src/app/services/steward_graph_runtime.py` - 已修改:`server/src/app/api/v1/endpoints/steward.py` - 已测试:`server/tests/test_steward_graph_runtime.py` - 保留测试:`server/tests/test_steward_slot_decision_agent.py` - 保留测试:`server/tests/test_steward_runtime_decision_agent.py` 步骤: - [x] 写失败测试:`StewardGraphRuntime.decide_slot()` 调用 slot node 后返回与现有 `StewardSlotDecisionAgent.decide()` 等价的 response。 - [x] 写失败测试:`StewardGraphRuntime.decide_runtime()` 在用户补充字段时合并 `steward_state`。 - [x] 把 `StewardSlotDecisionAgent` 包成 `slot_tool_decision` 节点。 - [x] 把 `StewardRuntimeDecisionAgent` 包成 `runtime_tool_decision` 节点。 - [x] 把已选流程确认和当前运行时记忆归一化拆成 `runtime_memory_context` / `runtime_action_decision` 节点。 - [x] endpoint 在 `STEWARD_AGENT_RUNTIME=langgraph` 时使用 graph runtime。 - [x] endpoint 在 graph runtime 异常时回退旧 Agent,避免框架层失效影响会话。 验收: - [x] `slot-decisions`、`runtime-decisions` 和 `plans` 三个入口都由 LangGraph runtime 路由。 - [x] legacy 回退仍可用。 - [x] 图内工具节点失败时仍有可执行规则兜底。 ### Phase 3:action node 标准化 目标:把保存草稿、提交审批、关联申请单这些副作用动作变成显式 action node。 状态:基础 action contract、第一版 action executor、前端点击执行闭环、LangGraph action node、checkpoint、pending interrupt 和幂等重放已完成;后续只剩 durable checkpointer 抽象与更完整 trace UI。 文件: - 已创建:`server/src/app/services/steward_action_executor.py` - 已创建:`server/src/app/services/steward_action_contracts.py` - 已创建:`server/src/app/services/steward_graph_action_runtime.py` - 已修改:`server/src/app/api/v1/endpoints/steward.py` - 已修改:`server/src/app/services/steward_graph_planner.py` - 已修改:`server/src/app/schemas/steward.py` - 已修改:`web/src/services/steward.js` - 已修改:`web/src/views/scripts/stewardPlanModel.js` - 已修改:`web/src/composables/workbenchAiMode/useWorkbenchAiActionRouter.js` - 已修改:`web/src/composables/workbenchAiMode/usePersonalWorkbenchAiMode.js` - 已测试:`server/tests/test_steward_action_executor.py` - 已测试:`server/tests/test_steward_graph_planner.py` - 已测试:`server/tests/test_steward_planner.py` - 已测试:`web/tests/workbench-ai-intent-planner-model.test.mjs` - 已测试:`web/tests/steward-actions-service.test.mjs` - 已测试:`web/tests/steward-plan-message-copy.test.mjs` - 已测试:`web/tests/workbench-ai-action-router.test.mjs` 白名单 action: - `build_application_preview` - `save_application_draft` - `submit_application` - `link_existing_application` - `create_reimbursement_draft` - `associate_attachments` 步骤: - [x] 写失败测试:申请直接提交计划必须输出 `fill_application_fields -> build_application_preview -> validate_required_fields -> run_duplicate_precheck -> submit_application`。 - [x] 写失败测试:保存草稿计划必须输出 `save_application_draft`,并在字段缺失时阻断后续副作用。 - [x] 写失败测试:报销计划必须输出 `fill_reimbursement_fields -> build_reimbursement_preview -> validate_required_fields -> create_reimbursement_draft`。 - [x] 在 `StewardTask` 和 `StewardPlanResponse` 中增加 `action_steps`。 - [x] 实现 `StewardActionPlanBuilder`,由服务端确定性生成白名单 action step。 - [x] 在 LangGraph planner 中新增 `attach_action_steps` 节点。 - [x] 前端 planner model 优先消费服务端 `task.action_steps`,旧响应才回退本地推导。 - [x] 写失败测试:未知 action step 被拒绝,不调用任何业务服务。 - [x] 写失败测试:`submit_application` 缺少确认来源时返回 `needs_confirmation`。 - [x] 写失败测试:`submit_application` 必须看到 precheck 通过后才允许真实提交。 - [x] 实现 action registry,只允许白名单 action。 - [x] 接入现有申请保存草稿、申请提交和报销草稿服务。 - [x] 前端新增 `executeStewardAction()` 并让 suggested action 携带服务端 action step。 - [x] AI 工作台点击 suggested action 时调用 `StewardActionExecutor`;直接提交先串行执行 precheck,再把结果交给 submit。 - [x] 接入 `link_existing_application` 和附件关联的真实执行。 - [x] 把 action executor 包成 LangGraph `action_execute_node`,并接入 checkpoint / interrupt 恢复。 验收: - [x] 模型不能通过自由文本触发数据库写入,服务端只输出白名单 `StewardActionStep`。 - [x] 申请提交动作没有确认时返回 `needs_confirmation`,不会写库。 - [x] 申请提交动作没有 precheck 通过证据时被阻断。 - [x] 保存申请草稿和创建报销草稿通过现有业务服务真实入库,测试覆盖。 - [x] 前端点击可执行 steward action 会真实调用 executor,并把结果回写到会话消息。 - [x] 基础副作用动作有 `client_trace_id` 幂等键、conversation checkpoint 和 pending interrupt 记录。 ### Phase 4:checkpoint 与 human-in-the-loop 目标:让 LangGraph 负责暂停、恢复和跨轮状态,而不是只靠前端消息状态。 文件: - 已创建:`server/src/app/services/steward_graph_action_runtime.py` - 已复用:`server/src/app/models/agent_conversation.py` - 已测试:`server/tests/test_steward_action_executor.py` 步骤: - [x] 设计 `conversation_id -> thread_id` 映射,当前直接以 `conversation_id` 作为 graph action thread。 - [x] 实现基于数据库的 checkpoint,先复用 `AgentConversation.state_json`。 - [x] 在提交审批缺确认时生成 pending interrupt payload。 - [x] 用户确认后用同一 `client_trace_id` / `conversation_id` 继续执行;终态请求会幂等重放。 验收: - [x] 刷新页面后,后端仍保留当前等待确认的 action checkpoint。 - [x] 同一个 action 不会因为重复点击或重试重复写库。 - [ ] 前端完整恢复 UI 仍需继续把 checkpoint 状态展示为可恢复按钮。 ### Phase 5:可观测性与 legacy 收敛 目标:让 agent 规划过程可解释、可测试、可回放,并逐步删除重复手写编排。 文件: - 修改:`server/src/app/services/agent_traces.py` - 修改:`server/src/app/models/agent_run.py` - 修改:`document/development/Agent链路追踪中心/CONCEPT.md` - 测试:`server/tests/test_agent_traces.py` 步骤: - [ ] 每个 graph node 写入 trace event:输入摘要、输出摘要、耗时、错误。 - [ ] 前端显示“模型规划中 / 字段判断中 / 等待确认 / 执行动作”阶段。 - [ ] 对 legacy planner 增加废弃标记和删除条件。 - [ ] 删除重复的手写分支前,先保留至少一轮灰度开关。 验收: - 任何一次用户请求都能从 trace 看出走过哪些 node。 - 删除 legacy 前,LangGraph 路径覆盖当前核心申请/报销链路测试。 ## 回退策略 - 环境变量: ```bash STEWARD_AGENT_RUNTIME=legacy ``` - 回退后: - `/steward/plans` 使用旧 `StewardPlannerService`。 - 新 action node 和 checkpoint 不应影响 legacy。 - 回退原因必须写入当天工作日志。 ## 验证矩阵 每个阶段至少跑: ```bash docker exec -w /app -e SERVER_VENV_DIR=/tmp/x-financial-server-venv x-financial-local-linux \ timeout 60s /tmp/x-financial-server-venv/bin/pytest -q \ server/tests/test_steward_graph_planner.py \ server/tests/test_steward_planner.py \ server/tests/test_steward_intent_agent.py \ server/tests/test_runtime_chat_service.py ``` 涉及前端执行器时补跑: ```bash node --test web/tests/workbench-ai-intent-planner-model.test.mjs \ web/tests/workbench-ai-application-gate-model.test.mjs \ web/tests/steward-actions-service.test.mjs \ web/tests/steward-plan-message-copy.test.mjs npm --prefix web run build ``` 每次完成后执行: ```bash git diff --check docker exec -w /app -e SERVER_VENV_DIR=/tmp/x-financial-server-venv x-financial-local-linux \ /tmp/x-financial-server-venv/bin/python -m pip check ``` ## 不做的事 - 不引入 LangChain 高层 Agent 来替代现有业务服务。 - 不让模型直接调用数据库写入、提交审批或绑定附件。 - 不为了接框架重写申请、报销、OCR、票据夹、审批规则。 - 不在没有测试覆盖时删除 legacy planner。 ## 风险与处理 - 模型连通性不稳定:先修 provider 和 backup,再扩大 graph 节点。 - checkpoint 与业务 state 双写不一致:先复用 `AgentConversation.state_json`,不要新增第二个业务状态源。 - action node 副作用重复执行:每个 action 必须有幂等键和执行前状态检查。 - LangGraph 传递依赖影响 WebSocket:当前 `pip check` 通过;后续若流式接口异常,先核对 `websockets` 版本。 - 节点内继续堆大块逻辑:每个 node 只做一类事,超过 300 行先拆文件。