refactor(server): steward 决策链路改用 LangGraph 编排

- 新增 StewardGraphPlannerService,用 LangGraph 状态图编排意图识别→流程判断→模型/规则分支→兜底,替代原 planner 内线性调用
- 新增 StewardGraphRuntimeService 编排运行时决策与槽位决策;StewardActionContracts/Executor 统一动作合约与执行
- steward_intent_agent/application_fact_resolver/runtime_chat 适配图执行器,config 暴露图相关开关
- pyproject/uv.lock 新增 langgraph 依赖
- 新增 graph_planner/graph_runtime/action_executor 测试,更新 intent_agent/planner/fact_resolver/runtime_chat/reimbursement 测试
This commit is contained in:
caoxiaozhu
2026-06-24 21:58:35 +08:00
parent 545b31d32f
commit 5311c99d69
25 changed files with 3580 additions and 104 deletions

View File

@@ -10,10 +10,13 @@ from fastapi.responses import StreamingResponse
from sqlalchemy import select
from sqlalchemy.orm import Session
from app.api.deps import get_db
from app.api.deps import CurrentUserContext, get_current_user, get_db
from app.core.config import get_settings
from app.models.financial_record import ExpenseClaim
from app.schemas.common import ErrorResponse
from app.schemas.steward import (
StewardActionExecuteRequest,
StewardActionExecuteResponse,
StewardPlanRequest,
StewardPlanResponse,
StewardRuntimeDecisionRequest,
@@ -27,6 +30,9 @@ from app.services.expense_claim_draft_flow import APPROVED_APPLICATION_LINK_STAT
from app.services.expense_claims import ExpenseClaimService
from app.services.runtime_chat import RuntimeChatService
from app.services.steward_flow_state import StewardFlowStateService
from app.services.steward_graph_action_runtime import StewardGraphActionRuntime
from app.services.steward_graph_planner import StewardGraphPlannerService
from app.services.steward_graph_runtime import StewardGraphRuntime
from app.services.steward_intent_agent import StewardIntentAgent
from app.services.steward_off_topic_agent import StewardOffTopicAgent
from app.services.steward_planner import StewardPlannerService
@@ -35,6 +41,8 @@ from app.services.steward_slot_decision_agent import StewardSlotDecisionAgent
router = APIRouter(prefix="/steward")
DbSession = Annotated[Session, Depends(get_db)]
CurrentUser = Annotated[CurrentUserContext, Depends(get_current_user)]
StewardPlannerLike = StewardPlannerService | StewardGraphPlannerService
@router.post(
@@ -69,7 +77,7 @@ def create_steward_slot_decision(
payload: StewardSlotDecisionRequest,
db: DbSession,
) -> StewardSlotDecisionResponse:
return StewardSlotDecisionAgent(RuntimeChatService(db)).decide(payload)
return _decide_steward_slot(payload, RuntimeChatService(db))
@router.post(
@@ -83,10 +91,27 @@ def create_steward_runtime_decision(
db: DbSession,
) -> StewardRuntimeDecisionResponse:
hydrated_payload = _hydrate_runtime_decision_payload(db, payload)
decision = StewardRuntimeDecisionAgent(RuntimeChatService(db)).decide(hydrated_payload)
decision = _decide_steward_runtime(hydrated_payload, RuntimeChatService(db))
return _attach_runtime_conversation_state(db, hydrated_payload, decision)
@router.post(
"/actions/execute",
response_model=StewardActionExecuteResponse,
summary="执行小财管家白名单动作",
description=(
"按 LangGraph 规划出的 action step 执行确定性业务动作;"
"当 action 未知、缺字段、缺确认或预检查未通过时直接阻断并返回结构化兜底结果。"
),
)
def execute_steward_action(
payload: StewardActionExecuteRequest,
db: DbSession,
current_user: CurrentUser,
) -> StewardActionExecuteResponse:
return StewardGraphActionRuntime(db).execute(payload, current_user)
@router.post(
"/plans/stream",
summary="流式生成小财管家任务计划",
@@ -101,7 +126,7 @@ async def stream_steward_plan(payload: StewardPlanRequest, db: DbSession) -> Str
async def _iter_steward_plan_events(
payload: StewardPlanRequest,
planner: StewardPlannerService,
planner: StewardPlannerLike,
db: Session,
) -> AsyncIterator[str]:
yield _encode_stream_event(
@@ -135,18 +160,53 @@ def _encode_stream_event(event: str, data: dict[str, Any]) -> str:
return json.dumps({"event": event, "data": data}, ensure_ascii=False) + "\n"
def _build_steward_planner(db: Session) -> StewardPlannerService:
def _build_steward_planner(db: Session) -> StewardPlannerLike:
runtime_chat = RuntimeChatService(db)
if get_settings().steward_agent_runtime.strip().lower() == "langgraph":
return StewardGraphPlannerService(
intent_agent=StewardIntentAgent(runtime_chat),
off_topic_agent=StewardOffTopicAgent(runtime_chat),
)
return StewardPlannerService(
intent_agent=StewardIntentAgent(runtime_chat),
off_topic_agent=StewardOffTopicAgent(runtime_chat),
)
def _decide_steward_slot(
payload: StewardSlotDecisionRequest,
runtime_chat: Any,
) -> StewardSlotDecisionResponse:
legacy_agent = StewardSlotDecisionAgent(runtime_chat)
if not _should_use_langgraph_runtime():
return legacy_agent.decide(payload)
try:
return StewardGraphRuntime(runtime_chat).decide_slot(payload)
except Exception:
return legacy_agent.decide(payload)
def _decide_steward_runtime(
payload: StewardRuntimeDecisionRequest,
runtime_chat: Any,
) -> StewardRuntimeDecisionResponse:
legacy_agent = StewardRuntimeDecisionAgent(runtime_chat)
if not _should_use_langgraph_runtime():
return legacy_agent.decide(payload)
try:
return StewardGraphRuntime(runtime_chat).decide_runtime(payload)
except Exception:
return legacy_agent.decide(payload)
def _should_use_langgraph_runtime() -> bool:
return get_settings().steward_agent_runtime.strip().lower() == "langgraph"
def _hydrate_required_application_gate(
db: Session,
payload: StewardPlanRequest,
planner: StewardPlannerService,
planner: StewardPlannerLike,
) -> StewardPlanRequest:
context_json = dict(payload.context_json or {})
required_gate = context_json.get("required_application_gate")