feat(server): 会话上下文保留(LLM 历史 + 确定性兜底双保险)

解决用户删除草稿后说'再提交'丢失上下文的问题:

- steward.py 新增 _inject_recent_conversation_history:build_plan 前读最近 10 条对话注入 context_json
- steward_intent_agent.py 的 _build_messages 把 recent_history 暴露给模型,system prompt 加确认类话术引导
- 新建 steward_context_resume.py:should_resume_recent_task 检测'再提交'类话术 + state 有可恢复 flow,attach_resumed_task 从 state 恢复 task
- 两个 plan 入口(/plans 和 /plans/stream)都已接入双保险
- 后端 67 passed,端到端验证'上海出差→再提交'成功恢复 task
This commit is contained in:
caoxiaozhu
2026-06-25 15:08:56 +08:00
parent 2ebc2756bf
commit e9d7c56d5b
5 changed files with 440 additions and 0 deletions

View File

@@ -29,6 +29,10 @@ from app.services.agent_conversations import AgentConversationService
from app.services.expense_claim_draft_flow import APPROVED_APPLICATION_LINK_STATUSES
from app.services.expense_claims import ExpenseClaimService
from app.services.runtime_chat import RuntimeChatService
from app.services.steward_context_resume import (
attach_resumed_task,
should_resume_recent_task,
)
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
@@ -61,7 +65,9 @@ def create_steward_plan(payload: StewardPlanRequest, db: DbSession) -> StewardPl
try:
planner = _build_steward_planner(db)
hydrated_payload = _hydrate_required_application_gate(db, payload, planner)
hydrated_payload = _inject_recent_conversation_history(db, hydrated_payload)
plan = planner.build_plan(hydrated_payload)
plan = _apply_context_resume(db, hydrated_payload, plan)
return _attach_conversation_state(db, hydrated_payload, plan)
except ValueError as exc:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) from exc
@@ -143,7 +149,9 @@ async def _iter_steward_plan_events(
try:
hydrated_payload = _hydrate_required_application_gate(db, payload, planner)
hydrated_payload = _inject_recent_conversation_history(db, hydrated_payload)
plan = planner.build_plan(hydrated_payload)
plan = _apply_context_resume(db, hydrated_payload, plan)
plan = _attach_conversation_state(db, hydrated_payload, plan)
except ValueError as exc:
yield _encode_stream_event("error", {"message": str(exc)})
@@ -495,3 +503,66 @@ def _resolve_current_steward_state(
return stored_state
incoming_state = context_json.get("steward_state") or context_json.get("stewardState")
return incoming_state if isinstance(incoming_state, dict) else {}
def _inject_recent_conversation_history(
db: Session,
payload: StewardPlanRequest,
) -> StewardPlanRequest:
"""读取会话最近 10 条对话历史,注入 context_json.recent_history 供 LLM 关联上下文。
历史只给模型用,不返回前端。在 get_or_create_conversation 之前读取,
使用前端传入的 conversation_id避免把本轮消息算进历史。
"""
context_json = dict(payload.context_json or {})
conversation_id = _resolve_conversation_id(context_json)
if not conversation_id:
return payload
try:
recent_history = AgentConversationService(db).list_message_history(
conversation_id,
limit=10,
)
except Exception:
recent_history = []
if not recent_history:
return payload
return payload.model_copy(
update={
"context_json": {
**context_json,
"recent_history": recent_history,
}
}
)
def _apply_context_resume(
db: Session,
payload: StewardPlanRequest,
plan: StewardPlanResponse,
) -> StewardPlanResponse:
"""确定性兜底:若 plan 无 task 且用户说"再提交"类话术,从会话状态恢复最近 task。
不依赖 LLM 理解力100% 可靠地恢复上下文。LLM 注入历史(保险②)覆盖更模糊话术。
"""
if plan.tasks or plan.candidate_flows:
return plan
context_json = dict(payload.context_json or {})
conversation_id = _resolve_conversation_id(context_json)
if not conversation_id:
return plan
try:
conversation = AgentConversationService(db).get_conversation(conversation_id)
except Exception:
conversation = None
if conversation is None:
return plan
current_state = _resolve_current_steward_state(
conversation.state_json if isinstance(conversation.state_json, dict) else {},
context_json,
)
resume_flow_id = should_resume_recent_task(payload.message, current_state)
if not resume_flow_id:
return plan
return attach_resumed_task(plan, current_state, resume_flow_id)