Files
X-Financial/document/development/AI意图规划器/LANGGRAPH_RUNTIME_MIGRATION.md

421 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 小财管家 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等待外部输入后恢复。
参考:
- <https://docs.langchain.com/oss/python/langgraph/overview>
- <https://docs.langchain.com/oss/python/langgraph/graph-api>
- <https://docs.langchain.com/oss/python/langgraph/persistence>
- <https://docs.langchain.com/oss/python/langgraph/interrupts>
## 总体目标架构
```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 2slot/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 3action 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 4checkpoint 与 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 行先拆文件。