refactor(server): 意图识别改 LLM 驱动,规则只做闲聊拦截+resume 兜底

规则不再判断'这是哪个业务场景'——那交给 LLM function call。
规则只保留两个不可替代职责:闲聊拦截(省 LLM 成本)、resume 确定性兜底。

- gate_classify 简化:删掉规则匹配门(94 词 CHOICE 匹配)和 ambiguous 提前判断
- 新增 _is_lightweight_off_topic:只拦 greeting+meaningless,不依赖业务关键词
- HANDLER_ONLY 改为 LLM 输出驱动:LLM 返回 query_travel_standard 后转 handler
- 图拓扑简化:gate_classify 只输出 off_topic|resume|model_intent
- 验证:76 passed;复合场景'出差并且报销招待费'→LLM 返回 2 task
This commit is contained in:
caoxiaozhu
2026-06-26 10:19:04 +08:00
parent 8a2ae6eb75
commit d139a63e64
2 changed files with 84 additions and 40 deletions

View File

@@ -158,36 +158,36 @@ class StewardGraphPlannerService(StewardPlannerFallbackMixin, StewardPlannerExtr
message = str(state.get("message") or "").strip()
steward_state = state.get("steward_state") or {}
# ① resume 门(优先:即使消息不命中业务关键词,只要是"再提交"类话术就尝试恢复)
# ① 闲聊拦截(极轻量:greeting + meaningless,省 LLM 成本,不依赖业务关键词)
# 不用 _classify_irrelevant_input(那套带 94 词判断,会误杀"下周去上海"等正常业务输入)
if self._is_lightweight_off_topic(message, request):
return {"gate_decision": "off_topic", "gate_scene_id": None}
# ② resume 门(用户说"再提交" + state 有可恢复 flow → 确定性恢复)
resume_scene = should_resume_recent_task(message, steward_state)
if resume_scene:
return {"gate_decision": "resume", "gate_scene_id": resume_scene}
# ② off_topic 门:复用成熟的 _classify_irrelevant_input(含城市名/时间词/金额词等 94 词 + greeting/meaningless 细分)
scenario = self._classify_irrelevant_input(message, request)
if scenario is not None:
return {"gate_decision": "off_topic", "gate_scene_id": None}
# ③ 规则匹配门(按 priority 遍历,命中 CHOICE 规则的)
for scene in REGISTRY.scenes_sorted_by_priority():
if scene.gate != GateRule.CHOICE:
continue
if not scene.signal_keywords:
continue
compact = _compact_text(message)
if any(kw in compact for kw in scene.signal_keywords):
route = _scene_route_to_gate_decision(scene.route)
return {"gate_decision": route, "gate_scene_id": scene.scene_id}
# ④ LLM 门(规则未命中)
# 走 model_intent 时,如 state 已有 active_flow 且 LLM 准备 fallback,可优先做 candidate_flow
if self._looks_like_ambiguous_travel_flow(
message, self._resolve_base_date_from_request(request), request
):
return {"gate_decision": "ambiguous", "gate_scene_id": None}
# ③ 其他全部走 LLM(不再有规则匹配门;LLM function call 是唯一的意图识别者)
return {"gate_decision": "model_intent", "gate_scene_id": None}
def _is_lightweight_off_topic(self, message: str, request: StewardPlanRequest) -> bool:
"""极轻量闲聊拦截:只拦 greeting 和 meaningless,不做业务相关性判断。
有附件时一定不是闲聊(附件意味着用户有业务诉求)。
业务相关性交给 LLM 判断,规则只挡掉绝对无关的输入省 LLM 成本。
"""
if request.attachments:
return False
compact = _compact_text(message)
if not compact:
return True
if self._looks_like_greeting(compact):
return True
if self._looks_like_meaningless(compact):
return True
return False
def _execute_scene_handler(self, state: StewardGraphState) -> dict[str, Any]:
"""HANDLER_ONLY 路由:不调 LLM,直接执行 scene 的 handler。
@@ -205,7 +205,12 @@ class StewardGraphPlannerService(StewardPlannerFallbackMixin, StewardPlannerExtr
)
import time
# scene_id 优先从 gate_scene_id 取(兼容旧行为),否则从 plan.tasks[0].task_type 取(LLM 驱动)
scene_id = state.get("gate_scene_id")
if not scene_id:
plan = state.get("plan")
if isinstance(plan, StewardPlanResponse) and plan.tasks:
scene_id = str(plan.tasks[0].task_type or "").strip()
scene = REGISTRY.get(scene_id or "") if scene_id else None
if scene is None or scene.handler is None:
plan = self._build_rule_fallback_graph_plan(state)
@@ -217,9 +222,15 @@ class StewardGraphPlannerService(StewardPlannerFallbackMixin, StewardPlannerExtr
return {"plan": plan}
# 构造 handler 期望的 StewardActionExecuteRequest
from app.services.steward_action_contracts import StewardActionPlanBuilder
builder = StewardActionPlanBuilder()
if scene.action_steps_builder is not None:
# 优先使用 LLM 已生成的 task(含 ontology_fields),否则构造最小 task
existing_plan = state.get("plan")
llm_task = None
if isinstance(existing_plan, StewardPlanResponse) and existing_plan.tasks:
llm_task = existing_plan.tasks[0]
if llm_task is not None:
task = llm_task
elif scene.action_steps_builder is not None:
task = StewardTask(
task_id=f"task_handler_{int(time.time() * 1000)}",
task_type=scene.scene_id,
@@ -232,27 +243,43 @@ class StewardGraphPlannerService(StewardPlannerFallbackMixin, StewardPlannerExtr
missing_fields=[],
confirmation_required=False,
)
action_steps = scene.action_steps_builder(task)
else:
action_steps = []
task = StewardTask(
task_id=f"task_handler_{int(time.time() * 1000)}",
task_type=scene.scene_id,
assigned_agent=scene.assigned_agent or "policy_query_assistant",
title=scene.label,
summary=str(request.message or "").strip(),
status="delegated",
requested_action="preview",
ontology_fields={},
missing_fields=[],
confirmation_required=False,
)
# 构造一个最小 action step 用于构造 handler request
# 构造 action steps(优先用 scene 的 builder,否则最小 step)
action_steps: list[StewardActionStep] = []
if scene.action_steps_builder is not None:
try:
action_steps = list(scene.action_steps_builder(task) or [])
except Exception:
action_steps = []
if not action_steps:
action_steps = [StewardActionStep(
step_id=f"handler_{int(time.time() * 1000)}",
action_type=scene.side_effect_actions[0] if scene.side_effect_actions else scene.scene_id,
label=scene.label,
target_task_id="",
target_task_id=task.task_id,
status="planned",
requires_confirmation=False,
payload={},
payload={"task_id": task.task_id, "ontology_fields": task.ontology_fields},
)]
step = action_steps[0]
action_request = StewardActionExecuteRequest(
action_type=step.action_type,
message=str(request.message or "").strip(),
task=task if scene.action_steps_builder else None,
task=task,
)
try:
@@ -459,7 +486,7 @@ class StewardGraphPlannerService(StewardPlannerFallbackMixin, StewardPlannerExtr
graph.add_node("build_pending_flow_plan", self._build_pending_flow_fallback_graph_plan)
graph.add_node("attach_action_steps", self._attach_action_steps)
# 拓扑
# 拓扑(P2:LLM 驱动,gate_classify 只输出 off_topic|resume|model_intent)
graph.add_edge(START, "load_context")
graph.add_edge("load_context", "gate_classify")
graph.add_conditional_edges(
@@ -467,13 +494,10 @@ class StewardGraphPlannerService(StewardPlannerFallbackMixin, StewardPlannerExtr
self._route_after_gate_classify,
{
"off_topic": "build_off_topic_plan",
"handler_only": "execute_scene_handler",
"resume": "resume_recent_task",
"ambiguous": "build_pending_flow_plan",
"model_intent": "prepare_context",
},
)
graph.add_edge("execute_scene_handler", "attach_action_steps")
graph.add_edge("resume_recent_task", "attach_action_steps")
graph.add_conditional_edges(
"prepare_context",
@@ -484,18 +508,21 @@ class StewardGraphPlannerService(StewardPlannerFallbackMixin, StewardPlannerExtr
"fallback": "build_rule_fallback_plan",
},
)
# detect_model_intent 成功后:HANDLER_ONLY 类(task_type 对应 scene.route=HANDLER_ONLY)
# 转 execute_scene_handler;其他转 attach_action_steps
graph.add_conditional_edges(
"detect_model_intent",
self._route_after_model_intent,
{
"done": "attach_action_steps",
"handler_only": "execute_scene_handler",
"off_topic": "build_off_topic_plan",
"fallback": "build_rule_fallback_plan",
},
)
graph.add_edge("execute_scene_handler", "attach_action_steps")
graph.add_edge("build_off_topic_plan", "attach_action_steps")
graph.add_edge("build_rule_fallback_plan", "attach_action_steps")
graph.add_edge("build_pending_flow_plan", "attach_action_steps")
graph.add_edge("attach_action_steps", END)
return graph.compile()
@@ -589,7 +616,17 @@ class StewardGraphPlannerService(StewardPlannerFallbackMixin, StewardPlannerExtr
@staticmethod
def _route_after_model_intent(state: StewardGraphState) -> str:
if isinstance(state.get("plan"), StewardPlanResponse):
plan = state.get("plan")
if isinstance(plan, StewardPlanResponse):
# LLM 成功:检查第一个 task 是否对应 HANDLER_ONLY 场景(查询类,直接执行 handler)
if plan.tasks:
from app.services.scenes import REGISTRY
from app.services.scenes.gate_rules import SceneRoute
first_task_type = str(plan.tasks[0].task_type or "").strip()
scene = REGISTRY.get(first_task_type)
if scene is not None and scene.route == SceneRoute.HANDLER_ONLY:
return "handler_only"
return "done"
if state.get("scenario") is not None:
return "off_topic"