feat(steward): 拦截业务无关输入返回 off_topic 计划
- schemas/steward.py:StewardPlanResponse 新增 suggested_prompts 字段 - steward_planner.py:新增 STEWARD_BUSINESS_SIGNAL_KEYWORDS 与 _is_business_irrelevant_input 守卫,在 build_plan 入口前置; 新增 _build_off_topic_plan 构造 plan_status=off_topic 的引导计划 - steward_intent_agent.py:system prompt 追加业务无关约束 - test_steward_planner.py:覆盖 123/你好/纯标点走 off_topic, 并验证正常业务输入不受守卫影响
This commit is contained in:
@@ -128,6 +128,10 @@ class StewardPlanResponse(BaseModel):
|
||||
)
|
||||
candidate_flows: list[StewardCandidateFlow] = Field(default_factory=list, description="等待用户确认的候选流程快捷列表。")
|
||||
model_call_traces: list[dict[str, Any]] = Field(default_factory=list, description="模型工具调用轨迹。")
|
||||
suggested_prompts: list[str] = Field(
|
||||
default_factory=list,
|
||||
description="当 plan_status 为 off_topic 等场景时,给用户的推荐话术示例。",
|
||||
)
|
||||
|
||||
|
||||
class StewardSlotOption(BaseModel):
|
||||
|
||||
@@ -115,6 +115,10 @@ class StewardIntentAgent:
|
||||
"如果输入里出现 occurred_date、transport_type、reason_value 等别名,必须映射为 canonical 字段。"
|
||||
"相对日期必须以 base_date 为准转换为明确日期。"
|
||||
"thinking_events 只能是面向用户的过程摘要,不能暴露内部推理链。"
|
||||
"如果用户输入与出差、费用、报销、申请等财务事项完全无关"
|
||||
"(例如纯数字、问候、闲聊、无意义字符、单字符重复),"
|
||||
"必须让 tasks 返回空数组,并在 thinking_events 中明确说明“未识别到财务事项”,"
|
||||
"不要强行把无关输入识别为 expense_application 或 reimbursement 任务。"
|
||||
),
|
||||
},
|
||||
{
|
||||
|
||||
@@ -49,6 +49,27 @@ CITY_NAMES = (
|
||||
"无锡",
|
||||
)
|
||||
|
||||
# 业务信号关键词:用于判定输入是否与小财管家支持的财务事项相关。
|
||||
# 只要清洗后的消息命中其中任意一个关键词,就视为业务相关;否则进入 off_topic 拦截。
|
||||
STEWARD_BUSINESS_SIGNAL_KEYWORDS: tuple[str, ...] = (
|
||||
# 动作词
|
||||
"申请", "报销", "草稿", "提交", "审批", "保存", "发起", "创建", "核对", "归集",
|
||||
# 差旅场景
|
||||
"出差", "差旅", "费用", "交通", "住宿", "招待", "酒店", "机票", "航班", "高铁",
|
||||
"动车", "火车", "出租车", "的士", "网约车", "打车", "地铁", "公交", "用餐", "餐饮", "宴请",
|
||||
# 票据/凭证
|
||||
"票据", "发票", "凭证", "行程单", "付款截图", "付款", "小票", "收据",
|
||||
# 业务对象
|
||||
"客户", "项目", "拜访", "会议", "培训", "部署", "实施", "支撑", "支持", "协助",
|
||||
"调研", "驻场", "上线", "验收", "审核",
|
||||
# 时间信号
|
||||
"昨天", "前天", "明天", "后天", "下周", "下月", "近期", "月底", "今天", "上周", "上月",
|
||||
# 金额/数量("天"用于"出差3天"等表达)
|
||||
"金额", "元", "块", "万", "千", "天",
|
||||
# 复用城市名信号
|
||||
*CITY_NAMES,
|
||||
)
|
||||
|
||||
APPLICATION_SPLIT_PATTERN = re.compile(r"(?:^|[,,。;;])[^,,。;;]*?(?:申请|出差申请|差旅申请)[^,,。;;]*")
|
||||
REIMBURSEMENT_PATTERN = re.compile(r"(?:我要报销|还需要报销|需要报销|报销)([^,,。;;!??!\n]+)")
|
||||
MONTH_DAY_PATTERN = re.compile(r"(?P<month>\d{1,2})\s*月\s*(?P<day>\d{1,2})\s*(?:日|号)?")
|
||||
@@ -107,6 +128,9 @@ class StewardPlannerService:
|
||||
raise ValueError("小财管家需要一段任务描述。")
|
||||
|
||||
base_date = self._resolve_base_date(request.client_now_iso, request.context_json)
|
||||
# 业务无关输入拦截(纯数字、问候、闲聊、乱码等):在进入 LLM/规则兜底之前直接返回 off_topic 计划。
|
||||
if self._is_business_irrelevant_input(message, request):
|
||||
return self._build_off_topic_plan(request)
|
||||
model_call_traces: list[dict[str, Any]] = []
|
||||
fallback_reason = ""
|
||||
if self.intent_agent is not None and self._should_use_model_intent_recognition(message, base_date, request):
|
||||
@@ -159,6 +183,51 @@ class StewardPlannerService:
|
||||
return False
|
||||
return self._has_multiple_financial_demands(message)
|
||||
|
||||
@staticmethod
|
||||
def _is_business_irrelevant_input(message: str, request: StewardPlanRequest) -> bool:
|
||||
"""判断输入是否与小财管家支持的财务事项完全无关。
|
||||
|
||||
判定规则:消息去除所有空白后不含任何业务信号关键词,且没有上传附件,
|
||||
即视为业务无关输入(如纯数字、问候、闲聊、乱码)。
|
||||
"""
|
||||
if request.attachments:
|
||||
return False
|
||||
compact = re.sub(r"\s+", "", message)
|
||||
if not compact:
|
||||
return False
|
||||
return not any(keyword in compact for keyword in STEWARD_BUSINESS_SIGNAL_KEYWORDS)
|
||||
|
||||
def _build_off_topic_plan(self, request: StewardPlanRequest) -> StewardPlanResponse:
|
||||
"""业务无关输入的兜底计划:明确告知用户未识别到财务事项,并给出话术示例。"""
|
||||
return StewardPlanResponse(
|
||||
plan_id=f"steward_plan_{uuid.uuid4().hex[:12]}",
|
||||
plan_status="off_topic",
|
||||
planning_source="rule_fallback",
|
||||
next_action="none",
|
||||
summary="这看起来跟财务任务没什么关系,小财管家没识别到费用申请或费用报销的意图。",
|
||||
thinking_events=[
|
||||
StewardThinkingEvent(
|
||||
event_id="intent_agent_off_topic",
|
||||
stage="off_topic",
|
||||
title="未识别到财务事项",
|
||||
content=(
|
||||
"我检查了这句话,没有发现费用申请、报销、出差、交通、招待等财务线索。"
|
||||
"如果你确实是要处理财务任务,可以参考下面的示例换一种说法。"
|
||||
),
|
||||
)
|
||||
],
|
||||
tasks=[],
|
||||
attachment_groups=[],
|
||||
confirmation_groups=[],
|
||||
candidate_flows=[],
|
||||
suggested_prompts=[
|
||||
"我想要申请明天去北京出差3天,支撑客户现场实施",
|
||||
"我要报销昨天的交通费",
|
||||
"报销上周出差上海的费用",
|
||||
],
|
||||
model_call_traces=[],
|
||||
)
|
||||
|
||||
def _build_rule_fallback_plan(
|
||||
self,
|
||||
request: StewardPlanRequest,
|
||||
|
||||
Reference in New Issue
Block a user