← 返回文章列表

LLM在贷前材料审核中的落地

LLM 在贷前材料审核中的落地

贷前材料审核是信贷流程中最依赖人工的环节之一。借款人上传身份证、营业执照、银行流水、经营合同等材料后,审核员要逐份核对信息一致性、识别 PS 痕迹、提取关键字段、判断材料是否合规。一套中型信贷机构的日均进件量在几百到上千单,每单平均涉及 5-10 份材料,纯人工审核的成本和时效都很难持续。

大模型(LLM)在这个场景里能做三件事:信息抽取一致性校验合规判断。这篇文章不讲"LLM 能不能用"的论证,直接讲工程落地的四个关键决策点。


一、整体架构:LLM 不是替代 OCR,是 OCR 的下游

一个常见的误解是把 LLM 直接怼到图片上做端到端审核。实际工程中,LLM 处理的是 OCR 产出的结构化/半结构化文本,而不是原始图片。链路如下:

用户上传材料(图片/PDF)
        │
        ▼
   ┌─────────┐
   │ OCR 引擎 │  ← 传统的文档 OCR(阿里云/腾讯云/自建)
   └────┬────┘
        │ 输出:结构化文本(姓名、身份证号、金额、日期等字段)
        ▼
   ┌─────────┐
   │ LLM 层   │  ← 信息抽取 + 交叉校验 + 合规判断
   └────┬────┘
        │ 输出:审核结果(通过/驳回/转人工)+ 驳回原因
        ▼
   ┌─────────────┐
   │ 规则引擎兜底  │  ← 硬规则(金额上限、黑名单等)最终决策
   └─────────────┘

OCR 和 LLM 的分工很清晰:OCR 负责"看清楚字",LLM 负责"理解意思"。不要把 LLM 放在 OCR 前面——LLM 的多模态能力虽然能看图,但在高精度文字识别上远不如专用 OCR 引擎,且延迟和成本高出几个数量级。


二、Prompt 设计:结构化输出是关键

贷前审核的 LLM 调用不是聊天场景,必须返回结构化数据,供下游的规则引擎和决策系统消费。JSON 格式的强制输出是基本要求。

一个营业执照信息抽取的 Prompt 模板:

EXTRACT_BIZ_LICENSE_PROMPT = """
你是一个信贷材料审核助手。从以下 OCR 识别结果中提取营业执照的关键字段。

## OCR 文本
{ocr_text}

## 输出要求
严格按以下 JSON 格式输出,不要包含任何其他文字:

{{
  "company_name": "企业全称",
  "unified_code": "统一社会信用代码(18位)",
  "legal_person": "法定代表人",
  "registered_capital": "注册资本(含单位,如'500万元人民币')",
  "establish_date": "成立日期(YYYY-MM-DD格式)",
  "business_scope": "经营范围(取前200字)",
  "registration_authority": "登记机关",
  "extraction_confidence": "high|medium|low",
  "missing_fields": ["未提取到的字段名列表"],
  "ocr_quality_issues": ["OCR质量问题描述,如'文字模糊''水印遮挡'等"]
}}

## 注意事项
1. unified_code 必须是18位,如果识别结果不足18位,标注为缺失
2. establish_date 统一转为 YYYY-MM-DD 格式,无法转换的保持原文
3. extraction_confidence 按以下标准判断:
   - high: 所有关键字段清晰可读,无歧义
   - medium: 部分字段模糊或不确定
   - low: 多数字段无法识别或 OCR 质量差
"""

def extract_biz_license(llm_client, ocr_text: str) -> dict:
    """调用 LLM 提取营业执照信息"""
    prompt = EXTRACT_BIZ_LICENSE_PROMPT.format(ocr_text=ocr_text)
    response = llm_client.chat(
        messages=[{"role": "user", "content": prompt}],
        temperature=0.0,       # 零温度,要求确定性输出
        response_format="json"  # 强制 JSON 模式
    )
    return json.loads(response.content)

关键设计点:

  • temperature=0:信息抽取任务不需要创造性,零温度保证每次相同输入得到相同输出。
  • response_format="json":用 API 原生的 JSON 模式约束,比在 prompt 里写"请输出 JSON"可靠得多。
  • extraction_confidence 字段:让 LLM 自己对抽取质量打分,下游可以据此决定走自动审核还是转人工。

三、交叉校验:多份材料之间的信息比对

单独抽取每份材料的信息只是第一步。真正有价值的是多份材料之间的交叉校验——比如营业执照上的企业名称和银行流水上的户名是否一致、身份证号和申请表上的身份证号是否匹配。

CROSS_VALIDATION_PROMPT = """
你是一个信贷材料一致性审核助手。以下是同一笔贷款申请的多份材料提取结果,
请逐项比对并判断是否存在不一致。

## 材料提取结果
{extracted_fields_json}

## 校验规则
1. 姓名一致性:申请表姓名 == 身份证姓名 == 银行流水户名
2. 证件号一致性:申请表证件号 == 身份证号
3. 企业信息一致性:营业执照公司名 == 银行流水户名 == 经营合同甲方/乙方
4. 金额合理性:申请金额 ≤ 银行流水月均入账 × {income_multiplier}
5. 日期逻辑:营业执照成立日期 < 申请日期,材料日期在合理范围内

## 输出格式
{{
  "overall_result": "pass|reject|manual_review",
  "checks": [
    {{
      "check_name": "校验项名称",
      "result": "pass|fail|uncertain",
      "source_a": {{"document": "材料A", "field": "字段名", "value": "值"}},
      "source_b": {{"document": "材料B", "field": "字段名", "value": "值"}},
      "detail": "不一致的具体描述",
      "severity": "critical|warning|info"
    }}
  ],
  "reject_reasons": ["驳回原因列表(仅 overall_result=reject 时填写)"],
  "manual_review_reasons": ["转人工原因(仅 overall_result=manual_review 时填写)"]
}}
"""

def cross_validate(llm_client, extracted_data: dict) -> dict:
    """多材料交叉校验"""
    prompt = CROSS_VALIDATION_PROMPT.format(
        extracted_fields_json=json.dumps(extracted_data, ensure_ascii=False, indent=2),
        income_multiplier=3.0
    )
    response = llm_client.chat(
        messages=[{"role": "user", "content": prompt}],
        temperature=0.0,
        response_format="json"
    )
    result = json.loads(response.content)

    # 硬规则兜底:LLM 的判断结果可以被人为覆盖
    # 比如 LLM 判 pass 但金额超过硬上限,直接 reject
    if extracted_data.get("apply_amount", 0) > HARD_AMOUNT_LIMIT:
        result["overall_result"] = "reject"
        result["checks"].append({
            "check_name": "硬规则_金额上限",
            "result": "fail",
            "detail": f"申请金额 {extracted_data['apply_amount']} 超过系统上限 {HARD_AMOUNT_LIMIT}",
            "severity": "critical"
        })

    return result

注意 severity 字段的分级设计:

  • critical:一旦触发直接拒绝(姓名不一致、证件号对不上、金额超限)
  • warning:标记但不阻断(成立日期与申请日期间隔 < 6 个月、流水月度波动 > 50%)
  • info:仅记录供人工参考(经营范围含敏感行业关键词)

四、驳回原因的生成:不只是"通过/不通过"

审核系统如果只返回"驳回",审核员不知道原因、借款人也看不懂,客服电话会被打爆。LLM 在这个场景里的另一个价值是生成可读的驳回原因

REJECT_REASON_PROMPT = """
你是信贷审核系统的客户沟通助手。根据以下审核结果,生成一段驳回原因说明。
要求:面向借款人,语言礼貌但清晰,明确指出需要补充或修正的材料,不超过150字。

## 审核结果
{reject_checks_json}

## 输出格式
{{
  "reason_summary": "一句话总结驳回原因",
  "detail_to_customer": "面向借款人的详细说明",
  "action_required": ["借款人需要执行的具体操作列表"]
}}
"""

这里有一个实践要点:驳回原因的 Prompt 和审核 Prompt 必须分开。审核 Prompt 要求严谨、技术化,驳回 Prompt 要求面向借款人、通俗易懂。混在一个 Prompt 里,LLM 会倾向于折中输出,两头不讨好。


五、三个工程踩坑实录

5.1 OCR 噪声是最大的不确定性来源

OCR 输出不是干净的文本。常见问题:身份证号里的 "0" 被识别成 "O"、"1" 被识别成 "l"、水印文字混入正文、表格错行导致字段错位。

解法:在 OCR 和 LLM 之间加一层预处理。不是做复杂的 NLP,而是做简单的规则清洗:

def preprocess_ocr_text(text: str, doc_type: str) -> str:
    """OCR 文本预处理"""
    if doc_type == "id_card":
        # 身份证号常见纠错
        text = text.replace("O", "0").replace("o", "0")
        text = text.replace("I", "1").replace("l", "1")
        text = text.replace("S", "5").replace("s", "5")
        # 去除非数字字符(仅对身份证号行)
        import re
        text = re.sub(r'(\d{6})\s+(\d{8})\s+(\d{4})', r'\1\2\3', text)
    elif doc_type == "bank_statement":
        # 银行流水常见:去除水印行(通常全是英文大写 + 重复模式)
        lines = text.split('\n')
        text = '\n'.join([
            l for l in lines
            if not (l.isupper() and len(l) > 30 and l.count(l[0]) / len(l) > 0.3)
        ])
    return text

5.2 LLM 输出格式不稳定

即使设了 response_format="json",LLM 偶尔还是会输出带 markdown 代码块的 JSON(json ... ),或者 JSON 中包含非法的控制字符。

def safe_parse_llm_json(raw_response: str) -> dict:
    """鲁棒的 LLM JSON 解析"""
    import re

    # 去掉可能的 markdown 代码块包裹
    cleaned = re.sub(r'^```(?:json)?\s*', '', raw_response.strip())
    cleaned = re.sub(r'\s*```$', '', cleaned)

    # 去掉非法控制字符(ASCII 0-31 除了 \t \n \r)
    cleaned = re.sub(r'[\x00-\x08\x0b\x0c\x0e-\x1f]', '', cleaned)

    try:
        return json.loads(cleaned)
    except json.JSONDecodeError:
        # 兜底:尝试用正则提取最外层的 {}
        match = re.search(r'\{.*\}', cleaned, re.DOTALL)
        if match:
            return json.loads(match.group())
        raise

5.3 延迟和成本的平衡

一笔贷款申请的完整审核链路通常涉及 3-5 次 LLM 调用(每份材料一次抽取 + 一次交叉校验 + 可选的驳回原因生成),如果每次都用 GPT-4 级别的模型,单笔成本可能超过 0.1 元,日处理 1000 单就是 100 元,还不算 OCR 费用。

降本策略:

环节 模型选择 理由
信息抽取 GPT-4o-mini / Qwen-2.5-7B 抽取任务相对简单,小模型即可
交叉校验 GPT-4o / Qwen-2.5-72B 需要多材料联合推理,大模型更可靠
驳回原因生成 GPT-4o-mini 生成任务对逻辑要求低,小模型足够
OCR 置信度低时 全链路升级到 GPT-4o 输入质量差时用小模型容易出错

另外,交叉校验可以加一级缓存:同一用户短期内多次申请时,历史校验结果可复用。


六、什么时候不该用 LLM

不是所有审核场景都适合上 LLM。以下场景应该继续用传统方法:

  1. 精确数值比对(身份证号、金额、日期):正则 + 规则引擎比 LLM 更可靠、更快、更便宜。LLM 做数值比对偶尔会出错(比如把 "18位" 数成 17 位)。
  2. 黑名单匹配:ElasticSearch 或 Redis Set 的 O(1) 查询,不需要 LLM。
  3. 材料真伪鉴定(PS 检测、水印验证):这是图像处理领域的专长,不是 LLM 的能力边界。
  4. 监管合规的硬性规则:这些规则必须 100% 准确执行,不允许 LLM 的概率性判断。

LLM 最适合的是模糊语义判断——"这份经营合同的业务描述和营业执照的经营范围是否一致?""银行流水的交易摘要中是否有可疑关键词?"这类没有精确规则、依赖语境理解的任务。


贷前材料审核是 LLM 在风控领域落地最成熟的场景之一。信息抽取 + 交叉校验 + 驳回原因生成的三段式架构,配合 OCR 预处理和硬规则兜底,能把人工审核量压缩到 20%-30%,同时保证不放过关键的不一致。上线前重点关注两点:OCR 噪声的容错能力和 LLM 输出的格式稳定性——这两个问题不解决,自动化率再高也是假象。