from __future__ import annotations import json from typing import Any def build_risk_rule_compiler_messages( *, domain: str, domain_label: str, expense_category: str | None, expense_category_label: str, natural_language: str, available_fields: list[dict[str, Any]], ) -> list[dict[str, str]]: """构造自然语言规则编译提示词。 大模型只负责把业务语言拆成“语义计划”,后端会校验字段、操作符和模板。 """ schema = { "name": "规则名称,短句", "description": "面向业务和审核人员的说明,不要写实现细节", "template_key": "field_required_v1 | field_compare_v1 | keyword_match_v1 | composite_rule_v1", "semantic_type": ( "可选。可用稳定英文短语描述语义类型;" "已知差旅票据城市/路线一致性可使用 travel_route_city_consistency,其他规则按业务含义命名" ), "field_keys": ["只能选择 available_fields.key"], "condition_summary": "用公式化语言描述判断依据,不要写'是否出现风险关键词'", "rule_ir": { "facts": "事实变量数组,例如 A=票据事实、B=业务申报事实、E=例外说明", "conditions": "条件数组,必须能被人解释", "hit_logic": "命中逻辑,例如 D AND ((A NOT_IN B) OR DATE_OUTSIDE(T,R)) AND NOT EXCEPTION(E)", }, "conditions": [ { "id": "稳定英文标识", "operator": ( "exists_any | exists_all | in_scope | not_in_scope | overlap | " "not_overlap | date_outside_range | contains_any | not_contains_any" ), "fields": ["exists/contains 类操作使用"], "left_fields": ["集合比较左侧字段"], "right_fields": ["集合比较右侧字段"], "date_fields": ["日期字段"], "range_start_fields": ["日期范围开始字段"], "range_end_fields": ["日期范围结束字段"], "keywords": ["例外或风险词"], } ], "hit_logic": {"all": ["condition_id", {"any": ["condition_id"]}]}, "formula": "可执行逻辑公式,字段使用事实变量表达", "message_template": "命中后的业务提示", "unsupported_fields": ["用户规则提到但 available_fields 中暂时没有的字段"], "keywords": "仅 keyword_match_v1 使用,且必须是真正风险词,不得把例外说明词当风险词", "exception_keywords": "例外说明词,例如绕行、跨城办事、临时改签", "risk_scoring_evidence": { "impact_level": "low | medium | high | critical", "violation_certainty": "low | medium | high | critical", "evidence_strength": "low | medium | high | critical", "exception_dependence": "low | medium | high | critical", "control_action": "remind | supplement | manual_review | return | block", "business_sensitivity": "low | medium | high | critical", "reason": "用一句话说明这些评分证据来自哪些业务语义", }, "flow": { "start": "流程起点", "evidence": "读取哪些事实", "decision": "判断公式或分支条件", "pass": "未命中时说明", "fail": "命中时说明", }, } guardrails = [ "只能输出 JSON 对象,不能输出 Markdown 或解释。", "字段必须来自 available_fields,不能编造字段。", "多步骤规则要使用 composite_rule_v1:先抽取事实变量,再写 conditions 和 hit_logic,不要压扁成单个关键词判断。", "城市/地点/路线一致性必须用 field_compare_v1 或 semantic_type=travel_route_city_consistency。", "涉及多个字段、日期范围、金额范围、集合关系、例外说明的规则必须使用 composite_rule_v1。", "日期字段必须区分事实日期、票据日期和业务期间;如果只能拿到替代字段,要在 rule_ir 中说明这是 fallback evidence。", "composite_rule_v1 只能使用受控 operator:exists_any、exists_all、in_scope、not_in_scope、overlap、not_overlap、date_outside_range、contains_any、not_contains_any。", "差旅路线规则中,交通票行程城市和住宿发票城市属于附件城市集合。", "申报目的地和明细发生地点属于申报行程城市集合。", "员工常驻地/出发地如可用,属于合理起终点集合,不等同于申报目的地。", "绕行、跨城办事、临时改签是例外说明证据,不是风险命中关键词。", "如果票据路线出现申报目的地和常驻地之外的额外城市,应描述为中途周转/绕行异常。", "keyword_match_v1 只用于品名、摘要、票据全文中出现明确风险词的规则。", "不要直接指定 risk_level 或 risk_score;只输出 risk_scoring_evidence,后端会按固定评分模型计算 0-100 分和风险等级。", "评分证据必须围绕六个指标:业务影响、违规确定性、证据强度、例外/规避空间、处置强度、场景敏感度。", ] examples = [ { "user_rule": ( "差旅报销时,交通票或住宿票据中的城市均无法与申报目的地、" "明细地点形成一致关系,且事由未说明绕行或改签原因,则高风险。" ), "expected": { "template_key": "field_compare_v1", "semantic_type": "travel_route_city_consistency", "field_keys": [ "attachment.route_cities", "attachment.hotel_city", "claim.location", "item.item_location", "employee.location", "claim.reason", "item.item_reason", ], "condition_summary": ( "A=交通票行程城市∪住宿发票城市,B=申报目的地∪明细发生地点," "C=员工常驻地/合理起终点;A与B无交集且无合理说明,或A中出现B∪C之外城市时命中。" ), "keywords": [], "exception_keywords": ["绕行", "跨城办事", "临时改签"], }, } ] return [ { "role": "system", "content": "\n".join( [ "你是 X-Financial 风险规则语义编译器。", "你的任务是把自然语言规则转换成可校验 JSON 语义计划。", "后端执行器只接受受控模板和受控字段,所以你必须严格遵守以下约束:", *[f"- {item}" for item in guardrails], ] ), }, { "role": "user", "content": json.dumps( { "business_domain": domain, "business_domain_label": domain_label, "expense_category": expense_category, "expense_category_label": expense_category_label, "natural_language": natural_language, "available_fields": available_fields, "required_json_shape": schema, "examples": examples, }, ensure_ascii=False, ), }, ]