1. 背景:风控规则引擎的"AI 化"卡在了哪里
我们团队负责某股份制银行信用卡中心的贷前审批辅助系统,日均处理进件约 20 万笔。原有规则引擎基于专家经验配置了 800 多条硬规则,覆盖反欺诈、额度测算、黑名单命中判断等场景。2025 年初,业务方提出一个诉求:把客户经理提交的"非结构化尽调备注"(平均每笔 300~500 字,含经营流水描述、抵押物情况、关联企业线索)自动解析成结构化特征,喂给下游评分卡模型,期望把人工录入特征的时间从平均 4.2 分钟/笔压缩到 1 分钟以内。
这个诉求的本质,是把"人读文本、人填字段"的重复劳动交给模型。我们第一版直接用 GPT-4o 做零样本信息抽取,Prompt 写得也算细致,但上线试运行两周后拿到一组很难看的数字:
- 特征抽取字段级准确率仅 87.3%,其中"关联企业识别"字段准确率跌到 71.6%;
- 因抽取错误导致的"人工复核退回率"从原来的 5% 飙到 11.4%,一线审核团队抱怨反而更忙了;
- 最致命的是出现 3 笔"幻觉型误判":模型在尽调备注里并不存在的情况下,自行"脑补"出"实际控制人对外担保余额 2000 万",直接触发人工复核,虽未造成实质损失,但暴露出严重合规隐患。
业务方的结论很直接:大模型抽取结果不可信,不敢用。这本质上就是大模型幻觉在金融风控场景的典型表现------不是模型不会抽取,而是我们缺少一套"幻觉治理 + 自动评测"的工程化机制。
2. 踩坑与现状:零样本抽取在风控场景的四个真实痛点
复盘第一版失败,我们踩的坑可以拆成四层,每一层都是真实约束:
第一层:评测缺失,上线前根本不知道准确率。 我们当时只做了 20 条人工样例的冒烟验证,没有建立带标注的评测集,更没有把"字段级准确率""幻觉率"作为可量化的门禁指标。模型表现如何全凭感觉,这是最根本的问题。
第二层:Prompt 约束力不足。 零样本 Prompt 里写"如备注中未提及,请输出'未知'",但模型在长文本、多实体、否定表达(如"无对外担保")场景下,指令遵循能力会显著衰减。事后抽检发现,71.6% 的关联企业错误里,有近一半是模型把"客户配偶名下企业"直接泛化成了"关联企业",另一半是纯粹的无中生有。
第三层:缺少结构化约束与外部校验。 纯 LLM 输出是自由文本,没有 JSON Schema 强约束,也没有与行内企业工商库、征信字段做交叉校验。模型"脑补"出的担保余额,如果接一个工商/征信接口做一致性校验,当场就能拦住。
第四层:上下文窗口与信息密度不匹配。 尽调备注里经常出现"其配偶张某某控制的企业曾于 2023 年对外担保 500 万,已结清"这类长句,模型需要做时序判断(已结清 vs 在保),零样本下极易把"曾担保"误抽成"当前在保",这属于语义理解层面的系统性偏差。
3. 方案:两套候选方案对比与选型
针对上述痛点,我们设计了两个候选方案,先做对比再定选型。
方案 A:纯 Prompt 工程 + 自洽性校验(Self-Consistency)
对同一段文本采样 5 次,对抽取结果做投票一致性校验,不一致的字段标记为"低置信度"转人工。实现成本低,不改模型链路。
- 优点:改动小,两天能上线;
- 缺点:投票只能暴露"模型自己都不稳定"的字段,拦不住"稳定地错"的幻觉(比如模型每次都把"曾担保"抽成"在保");且 5 倍采样把单笔推理成本从 0.03 元拉到 0.15 元,按日均 20 万笔算,仅此一项每天增加 2.4 万元成本,业务方无法接受。
方案 B:结构化抽取 + 外部知识校验 + 自动评测闭环(最终选型)
核心思路是"让模型只做它擅长的事,把确定性交给代码和外部数据"。具体拆成三层:
- 结构化约束层 :用函数调用(Function Calling)强制模型按 JSON Schema 输出,枚举字段(如担保状态)只允许输出
ACTIVE/CLEARED/UNKNOWN三选一,从输出格式上压缩幻觉空间; - 外部校验层 :抽取出的企业名称、担保余额等关键字段,与行内企业工商库(Oracle 19c 同步的镜像库)和征信接口做交叉比对,比对不一致直接置为
UNKNOWN并转人工,而不是把模型的"自信输出"直接落库; - 评测闭环层:建设一个带人工标注的评测集(首批 2000 条真实脱敏尽调备注),把"字段级准确率、幻觉率、转人工率"三个指标接入 CI,每次 Prompt 或模型版本变更必须跑评测,指标回退即阻断发布。
选型依据很直接:方案 A 解决不了"稳定地错",而金融风控场景最怕的就是稳定地错------它会让审核团队对系统产生虚假信任。方案 B 虽然前期建设成本高(约 3 人周),但把幻觉拦截从"概率手段"变成了"确定性手段"。
4. 实操步骤:环境版本与关键代码
4.1 环境与依赖版本
- 模型:Qwen2.5-72B-Instruct(私有化部署,vLLM 0.6.3 推理,满足数据不出行要求);
- 编排:Python 3.11 + LangChain 0.3.7;
- 结构化输出:LangChain 的
PydanticOutputFunctionsParser+ 自定义 JSON Schema; - 评测与向量检索:OpenAI
text-embedding-3-small做语义召回(仅用于评测集去重,不参与抽取),Redis 7.2 存评测结果缓存; - 外部校验:行内企业工商镜像库(Oracle 19c),通过 JDBC 查询。
4.2 结构化抽取核心代码
python
# extract_features.py
# Python 3.11 + LangChain 0.3.7 + Qwen2.5-72B-Instruct (vLLM 0.6.3)
from langchain_core.pydantic_v1 import BaseModel, Field
from langchain_openai import ChatOpenAI
from langchain_core.utils.function_calling import convert_to_openai_function
class GuaranteeStatus(str, Enum):
ACTIVE = "ACTIVE" # 在保
CLEARED = "CLEARED" # 已结清
UNKNOWN = "UNKNOWN" # 备注未提及或校验不一致
class GuaranteeInfo(BaseModel):
"""对外担保信息抽取结果"""
debtor_name: str = Field(description="被担保人/债务企业名称")
amount: float = Field(description="担保金额(万元),备注未提及则为 0")
status: GuaranteeStatus = Field(description="担保状态,严格三选一")
source_sentence: str = Field(description="抽取依据的原文句子,禁止编造")
class DueDiligenceFeatures(BaseModel):
"""尽调备注结构化特征"""
related_enterprises: list[str] = Field(description="关联企业名称列表")
guarantees: list[GuaranteeInfo] = Field(description="对外担保信息列表")
llm = ChatOpenAI(
model="qwen2.5-72b-instruct",
base_url="http://llm-internal:8000/v1", # vLLM 服务地址
temperature=0.0, # 抽取任务关闭随机性
)
# 关键:用函数调用强制 JSON Schema,而不是让模型自由输出
llm_with_tools = llm.bind_tools([convert_to_openai_function(DueDiligenceFeatures)])
4.3 外部校验与置信度标记
python
# validate_features.py
def cross_validate(features: DueDiligenceFeatures, remark_id: str) -> DueDiligenceFeatures:
"""与行内工商镜像库交叉校验,不一致字段置为 UNKNOWN"""
for g in features.guarantees:
# 1. 企业名称精确匹配工商库,匹配不到说明可能是幻觉
if not enterprise_db.exists(g.debtor_name):
g.status = GuaranteeStatus.UNKNOWN
g.source_sentence = "[校验未通过: 工商库无此企业]"
continue
# 2. 担保状态与征信接口比对(仅示例,实际走内部服务)
actual_status = credit_api.query_status(g.debtor_name)
if actual_status != g.status.value:
g.status = GuaranteeStatus.UNKNOWN # 宁可转人工,不落错误结论
return features
4.4 预期运行结果
在 2000 条标注评测集上,单条推理耗时约 1.8 秒(vLLM 批量推理),字段级准确率从 87.3% 提升到 96.8%,幻觉率(无中生有字段占比)从 5.2% 降到 0.4%,转人工率控制在 8.5% 以内。人工录入特征平均耗时从 4.2 分钟/笔降到 52 秒/笔。
5. 踩坑记录:三个真实报错与排查
5.1 坑一:Pydantic 枚举字段被模型"硬编码"绕过
上线第二天发现,部分返回里 status 字段出现了 "status": "已结清" 这种中文字符串,直接导致下游解析抛异常。排查发现是 LangChain 0.3.7 的 bind_tools 在部分请求下退化为纯文本补全,没有真正走函数调用分支。
解决 :在 ChatOpenAI 上显式加 model_kwargs={"tool_choice": "auto"} 不够,最终改为在 Prompt 里追加一行硬约束:"status 字段只允许输出 ACTIVE/CLEARED/UNKNOWN 三个枚举值之一,禁止输出中文或其他值",并在解析层加 Pydantic 校验兜底,解析失败整条转人工。
5.2 坑二:vLLM 长文本推理 OOM
评测集里有一条 1800 字的尽调备注,推理时 vLLM 直接报错:
torch.OutOfMemoryError: CUDA out of memory. Tried to allocate 512.00 MiB
(GPU 0; 80.00 GiB total capacity; 78.21 GiB already allocated; ...)
排查发现是 vLLM 0.6.3 的 max_model_len 默认值被设成了 32768,而 Qwen2.5-72B 的 KV Cache 在长序列下吃满显存。解决 :把 --max-model-len 降到 8192(尽调备注 99% 在 2000 字以内),并在代码侧对超长文本做截断预处理,OOM 消失。
5.3 坑三:评测集数据泄漏导致指标虚高
第一版评测集直接用了生产库近 3 个月的备注,结果发现模型在评测集上字段准确率 97.5%,但上线后回落到 94%。排查发现评测集与 Prompt 调试集有重叠------我们调试 Prompt 时反复用了同一批数据,模型"记住"了答案。
解决 :用 text-embedding-3-small 对全量备注做向量化,存入 Redis 7.2,按余弦相似度 > 0.85 去重,保证评测集与调试集零重叠,重新标注后准确率回落到真实的 96.8%。
6. 权衡:代价与边界,什么场景不建议这么干
这套方案不是免费的午餐,代价和边界必须说清楚。
代价一:外部校验依赖数据源质量。 我们的工商镜像库是 Oracle 19c 每日同步,若同步延迟超过 24 小时,新注册企业会查不到,导致误转人工率上升。实测在同步延迟 48 小时的情况下,转人工率从 8.5% 升到 12.3%,需要监控同步延迟并设置告警。
代价二:结构化约束牺牲了召回。 强制 JSON Schema 输出后,模型对"备注里确实有但表述极不规范"的信息(如"他老婆公司去年贷过款")会倾向于输出 UNKNOWN,导致部分真实特征被漏掉。我们在 2000 条评测集上对比,结构化约束的召回率比自由文本低约 3.2 个百分点,但换来的是幻觉率从 5.2% 降到 0.4%,这个交换在风控场景是值得的。
代价三:评测集建设是持续投入。 首批 2000 条标注花了 3 人周,后续每季度还要补充 500 条覆盖新业务形态的样本,否则评测集会逐渐与真实分布脱节,指标虚高。
不适用场景:纯开放式生成(如撰写尽调报告摘要、生成客户沟通话术),这类任务没有唯一正确答案,外部校验无从下手,结构化约束反而会牺牲生成质量。此时更该做的是 RAG 引用溯源 + 人工抽检,而不是追求"零幻觉"。
7. 总结:适用边界,哪些业务不要照搬
适用场景:信息抽取结果可被外部确定性数据源交叉校验的领域------金融风控(工商、征信)、供应链(发票、物流)、医疗(药品库、诊断编码)。这类场景的核心特征是"错了有代价,且存在权威第三方数据做锚点",幻觉治理的投入产出比最高。
边界条件要认清:我们的方案能拦住"无中生有",但拦不住"张冠李戴"------比如备注里同时出现 A、B 两家企业,模型把 A 的担保金额安到 B 头上,工商库校验发现 B 确实有担保记录,就会误放行。这类错误目前只能靠"抽取依据原文句子 + 审核人员快速定位"来缓解,是已知的未解决问题。
哪些业务不要照搬:如果你的业务没有权威外部数据源可做交叉校验(比如纯文本摘要、情感分析、开放式问答),这套"结构化约束 + 外部校验"的架构会显得笨重且收益有限。此时更该做的是 RAG 引用溯源 + 人工抽检,而不是追求"零幻觉"。
最后一条经验:大模型幻觉治理的本质不是把模型调得更准,而是设计一套让幻觉"无处藏身"的工程架构------结构化约束压缩幻觉空间,外部校验拦截幻觉落地,自动评测量化幻觉水位。三者缺一不可,且自动评测要建在最前面,否则后面的一切优化都是盲人摸象。