feat: 重构报销单AI预审流程并添加平台风险规则引擎

- 将AI验审改为AI预审,高风险不再拦截而是随单流转给审批人复核
- 新增平台风险规则评估引擎,支持事由过短、票据异常、重复发票等多种评估器
- 用户上下文增加部门信息(department_name),认证流程同步关联组织架构
- 规则scenario_json改为中文标签(差旅/费用科目),统一场景分类
- 新增orchestrator审核流程测试用例
- 前端更新审计视图、差旅报销等相关页面
This commit is contained in:
caoxiaozhu
2026-05-20 09:36:01 +08:00
parent 2574bc81d1
commit 57957d11a0
23 changed files with 2109 additions and 553 deletions

View File

@@ -120,11 +120,13 @@ EXPLAIN_KEYWORDS = ("为什么", "依据", "原因", "怎么处理", "是否可
COMPARE_KEYWORDS = ("对比", "比较", "相比", "差异", "变化")
RISK_KEYWORDS = ("风险", "异常", "重复", "超标", "超预算", "逾期", "验真", "巡检")
DRAFT_KEYWORDS = ("生成", "草稿", "起草", "拟一份", "创建", "发起", "准备")
DRAFT_FOLLOW_UP_KEYWORDS = (
"继续",
"补充",
"补一下",
"修改",
DRAFT_FOLLOW_UP_KEYWORDS = (
"继续",
"下一步",
"核对",
"补充",
"补一下",
"修改",
"改成",
"改为",
"换成",
@@ -136,9 +138,16 @@ DRAFT_FOLLOW_UP_KEYWORDS = (
"地点是",
"金额是",
"日期是",
"时间是",
)
OPERATE_KEYWORDS = (
"时间是",
)
EXPENSE_REVIEW_ACTIONS = {
"save_draft",
"next_step",
"edit_review",
"link_to_existing_draft",
"create_new_claim_from_documents",
}
OPERATE_KEYWORDS = (
"直接付款",
"帮我付款",
"安排付款",
@@ -636,12 +645,17 @@ class SemanticOntologyService:
def _compact(text: str) -> str:
return re.sub(r"\s+", "", text).lower()
@staticmethod
def _resolve_context_scenario(context_json: dict[str, Any]) -> str | None:
value = str(context_json.get("conversation_scenario") or "").strip()
if value in CONTEXTUAL_SCENARIOS:
return value
return None
@staticmethod
def _resolve_context_scenario(context_json: dict[str, Any]) -> str | None:
value = str(context_json.get("conversation_scenario") or "").strip()
if value in CONTEXTUAL_SCENARIOS:
return value
review_action = str(context_json.get("review_action") or "").strip()
if review_action in EXPENSE_REVIEW_ACTIONS:
return "expense"
if str(context_json.get("draft_claim_id") or "").strip():
return "expense"
return None
@staticmethod
def _resolve_session_type_scenario(context_json: dict[str, Any]) -> str | None:
@@ -728,19 +742,22 @@ class SemanticOntologyService:
)
return len(compact_query) <= 12 and not has_domain_keyword
def _should_inherit_expense_draft(
self,
compact_query: str,
def _should_inherit_expense_draft(
self,
compact_query: str,
*,
scenario: str,
entities: list[OntologyEntity],
time_range: OntologyTimeRange,
context_json: dict[str, Any],
) -> bool:
context_scenario = self._resolve_context_scenario(context_json)
draft_claim_id = str(context_json.get("draft_claim_id") or "").strip()
if context_scenario != "expense" and not draft_claim_id:
return False
context_json: dict[str, Any],
) -> bool:
context_scenario = self._resolve_context_scenario(context_json)
draft_claim_id = str(context_json.get("draft_claim_id") or "").strip()
review_action = str(context_json.get("review_action") or "").strip()
if review_action in EXPENSE_REVIEW_ACTIONS:
return True
if context_scenario != "expense" and not draft_claim_id:
return False
if any(keyword in compact_query for keyword in DRAFT_FOLLOW_UP_KEYWORDS):
return True
@@ -1674,15 +1691,16 @@ class SemanticOntologyService:
return False, None
@staticmethod
def _allow_incomplete_draft(
context_json: dict[str, Any],
*,
scenario: str,
intent: str,
def _allow_incomplete_draft(
context_json: dict[str, Any],
*,
scenario: str,
intent: str,
) -> bool:
if scenario != "expense" or intent != "draft":
return False
return str(context_json.get("review_action") or "").strip() == "save_draft"
if scenario != "expense" or intent != "draft":
return False
review_action = str(context_json.get("review_action") or "").strip()
return review_action in EXPENSE_REVIEW_ACTIONS
@staticmethod
def _display_slot_label(slot: str) -> str: