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:
@@ -93,7 +93,14 @@
|
|||||||
- 验证:后端全量 **76 passed**;端到端 4 场景全部通过——①出差申请(llm_function_call, 1 task)、②再提交(context_resume, 1 task, 5 fields)、③查差旅标准(scene_handler:query_travel_standard)、④闲聊(off_topic)。
|
- 验证:后端全量 **76 passed**;端到端 4 场景全部通过——①出差申请(llm_function_call, 1 task)、②再提交(context_resume, 1 task, 5 fields)、③查差旅标准(scene_handler:query_travel_standard)、④闲聊(off_topic)。
|
||||||
- 影响:LangGraph 图成为唯一编排者,门控收敛到 gate_classify 单一决策点。endpoint 仍保留 hydrate/persist 两个补丁,P3 会收敛。
|
- 影响:LangGraph 图成为唯一编排者,门控收敛到 gate_classify 单一决策点。endpoint 仍保留 hydrate/persist 两个补丁,P3 会收敛。
|
||||||
|
|
||||||
## TODO
|
- 02:30:我把意图识别从"规则驱动"改为"LLM 驱动 + 极轻量规则过滤",规则不再判断"这是哪个业务场景"。
|
||||||
|
- Git 提交检查:本地与 origin/main 同步。
|
||||||
|
- 背景:gate_classify 用 94 关键词 + scene_registry CHOICE 规则匹配判断业务相关性,每加场景要维护关键词,关键词冲突(出差既可能申请也可能查标准),还导致 off_topic 误杀("下周去上海"被拦)。但 LLM 已经完全具备多场景识别能力(schema enum + prompt_fragment 都从 registry 动态生成),规则匹配门是多余的。
|
||||||
|
- 修改①(简化 gate_classify):删掉规则匹配门(③)和 ambiguous 提前判断(④)。gate_classify 现在只做两件事:①闲聊拦截(极轻量:greeting+meaningless,省 LLM 成本)②resume 确定性兜底。其他全部默认走 model_intent。新增 `_is_lightweight_off_topic` 只调 `_looks_like_greeting`+`_looks_like_meaningless`,不调带 94 词的 `_classify_irrelevant_input`。
|
||||||
|
- 修改②(HANDLER_ONLY 改为 LLM 输出驱动):查差旅标准不再由 gate_classify 规则命中,改为 LLM 返回 task_type=query_travel_standard 后,`_route_after_model_intent` 检查 scene.route=HANDLER_ONLY → 转 execute_scene_handler。`execute_scene_handler` 优先使用 LLM 生成的 task(含 ontology_fields)。
|
||||||
|
- 修改③(图拓扑简化):gate_classify 只输出 off_topic|resume|model_intent 三种。删掉 handler_only/ambiguous 边。
|
||||||
|
- 验证:后端 **76 passed**;端到端 7 场景全通过——①你好→off_topic、②123→off_topic、③下周去上海→llm_function_call(之前被误杀!)、④P5武汉住宿标准→scene_handler:query_travel_standard、⑤再提交→context_resume、⑥出差并且报销招待费→llm_function_call 返回 2 task(expense_application+reimbursement)。
|
||||||
|
- 影响:意图识别全部交给 LLM,规则只保留闲聊拦截和 resume 兜底两个不可替代职责。加新场景只需注册 SceneDescriptor(prompt_fragment+ontology_fields+handler),LLM 的 system prompt 和 schema 自动包含,**不加任何关键词规则,不动门控代码**。
|
||||||
|
|
||||||
## TODO
|
## TODO
|
||||||
|
|
||||||
|
|||||||
@@ -158,36 +158,36 @@ class StewardGraphPlannerService(StewardPlannerFallbackMixin, StewardPlannerExtr
|
|||||||
message = str(state.get("message") or "").strip()
|
message = str(state.get("message") or "").strip()
|
||||||
steward_state = state.get("steward_state") or {}
|
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)
|
resume_scene = should_resume_recent_task(message, steward_state)
|
||||||
if resume_scene:
|
if resume_scene:
|
||||||
return {"gate_decision": "resume", "gate_scene_id": resume_scene}
|
return {"gate_decision": "resume", "gate_scene_id": resume_scene}
|
||||||
|
|
||||||
# ② off_topic 门:复用成熟的 _classify_irrelevant_input(含城市名/时间词/金额词等 94 词 + greeting/meaningless 细分)
|
# ③ 其他全部走 LLM(不再有规则匹配门;LLM function call 是唯一的意图识别者)
|
||||||
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}
|
|
||||||
|
|
||||||
return {"gate_decision": "model_intent", "gate_scene_id": None}
|
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]:
|
def _execute_scene_handler(self, state: StewardGraphState) -> dict[str, Any]:
|
||||||
"""HANDLER_ONLY 路由:不调 LLM,直接执行 scene 的 handler。
|
"""HANDLER_ONLY 路由:不调 LLM,直接执行 scene 的 handler。
|
||||||
|
|
||||||
@@ -205,7 +205,12 @@ class StewardGraphPlannerService(StewardPlannerFallbackMixin, StewardPlannerExtr
|
|||||||
)
|
)
|
||||||
import time
|
import time
|
||||||
|
|
||||||
|
# scene_id 优先从 gate_scene_id 取(兼容旧行为),否则从 plan.tasks[0].task_type 取(LLM 驱动)
|
||||||
scene_id = state.get("gate_scene_id")
|
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
|
scene = REGISTRY.get(scene_id or "") if scene_id else None
|
||||||
if scene is None or scene.handler is None:
|
if scene is None or scene.handler is None:
|
||||||
plan = self._build_rule_fallback_graph_plan(state)
|
plan = self._build_rule_fallback_graph_plan(state)
|
||||||
@@ -217,9 +222,15 @@ class StewardGraphPlannerService(StewardPlannerFallbackMixin, StewardPlannerExtr
|
|||||||
return {"plan": plan}
|
return {"plan": plan}
|
||||||
|
|
||||||
# 构造 handler 期望的 StewardActionExecuteRequest
|
# 构造 handler 期望的 StewardActionExecuteRequest
|
||||||
from app.services.steward_action_contracts import StewardActionPlanBuilder
|
# 优先使用 LLM 已生成的 task(含 ontology_fields),否则构造最小 task
|
||||||
builder = StewardActionPlanBuilder()
|
existing_plan = state.get("plan")
|
||||||
if scene.action_steps_builder is not None:
|
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 = StewardTask(
|
||||||
task_id=f"task_handler_{int(time.time() * 1000)}",
|
task_id=f"task_handler_{int(time.time() * 1000)}",
|
||||||
task_type=scene.scene_id,
|
task_type=scene.scene_id,
|
||||||
@@ -232,27 +243,43 @@ class StewardGraphPlannerService(StewardPlannerFallbackMixin, StewardPlannerExtr
|
|||||||
missing_fields=[],
|
missing_fields=[],
|
||||||
confirmation_required=False,
|
confirmation_required=False,
|
||||||
)
|
)
|
||||||
action_steps = scene.action_steps_builder(task)
|
|
||||||
else:
|
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:
|
if not action_steps:
|
||||||
action_steps = [StewardActionStep(
|
action_steps = [StewardActionStep(
|
||||||
step_id=f"handler_{int(time.time() * 1000)}",
|
step_id=f"handler_{int(time.time() * 1000)}",
|
||||||
action_type=scene.side_effect_actions[0] if scene.side_effect_actions else scene.scene_id,
|
action_type=scene.side_effect_actions[0] if scene.side_effect_actions else scene.scene_id,
|
||||||
label=scene.label,
|
label=scene.label,
|
||||||
target_task_id="",
|
target_task_id=task.task_id,
|
||||||
status="planned",
|
status="planned",
|
||||||
requires_confirmation=False,
|
requires_confirmation=False,
|
||||||
payload={},
|
payload={"task_id": task.task_id, "ontology_fields": task.ontology_fields},
|
||||||
)]
|
)]
|
||||||
step = action_steps[0]
|
step = action_steps[0]
|
||||||
|
|
||||||
action_request = StewardActionExecuteRequest(
|
action_request = StewardActionExecuteRequest(
|
||||||
action_type=step.action_type,
|
action_type=step.action_type,
|
||||||
message=str(request.message or "").strip(),
|
message=str(request.message or "").strip(),
|
||||||
task=task if scene.action_steps_builder else None,
|
task=task,
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
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("build_pending_flow_plan", self._build_pending_flow_fallback_graph_plan)
|
||||||
graph.add_node("attach_action_steps", self._attach_action_steps)
|
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(START, "load_context")
|
||||||
graph.add_edge("load_context", "gate_classify")
|
graph.add_edge("load_context", "gate_classify")
|
||||||
graph.add_conditional_edges(
|
graph.add_conditional_edges(
|
||||||
@@ -467,13 +494,10 @@ class StewardGraphPlannerService(StewardPlannerFallbackMixin, StewardPlannerExtr
|
|||||||
self._route_after_gate_classify,
|
self._route_after_gate_classify,
|
||||||
{
|
{
|
||||||
"off_topic": "build_off_topic_plan",
|
"off_topic": "build_off_topic_plan",
|
||||||
"handler_only": "execute_scene_handler",
|
|
||||||
"resume": "resume_recent_task",
|
"resume": "resume_recent_task",
|
||||||
"ambiguous": "build_pending_flow_plan",
|
|
||||||
"model_intent": "prepare_context",
|
"model_intent": "prepare_context",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
graph.add_edge("execute_scene_handler", "attach_action_steps")
|
|
||||||
graph.add_edge("resume_recent_task", "attach_action_steps")
|
graph.add_edge("resume_recent_task", "attach_action_steps")
|
||||||
graph.add_conditional_edges(
|
graph.add_conditional_edges(
|
||||||
"prepare_context",
|
"prepare_context",
|
||||||
@@ -484,18 +508,21 @@ class StewardGraphPlannerService(StewardPlannerFallbackMixin, StewardPlannerExtr
|
|||||||
"fallback": "build_rule_fallback_plan",
|
"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(
|
graph.add_conditional_edges(
|
||||||
"detect_model_intent",
|
"detect_model_intent",
|
||||||
self._route_after_model_intent,
|
self._route_after_model_intent,
|
||||||
{
|
{
|
||||||
"done": "attach_action_steps",
|
"done": "attach_action_steps",
|
||||||
|
"handler_only": "execute_scene_handler",
|
||||||
"off_topic": "build_off_topic_plan",
|
"off_topic": "build_off_topic_plan",
|
||||||
"fallback": "build_rule_fallback_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_off_topic_plan", "attach_action_steps")
|
||||||
graph.add_edge("build_rule_fallback_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)
|
graph.add_edge("attach_action_steps", END)
|
||||||
return graph.compile()
|
return graph.compile()
|
||||||
|
|
||||||
@@ -589,7 +616,17 @@ class StewardGraphPlannerService(StewardPlannerFallbackMixin, StewardPlannerExtr
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _route_after_model_intent(state: StewardGraphState) -> str:
|
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"
|
return "done"
|
||||||
if state.get("scenario") is not None:
|
if state.get("scenario") is not None:
|
||||||
return "off_topic"
|
return "off_topic"
|
||||||
|
|||||||
Reference in New Issue
Block a user