LLM贷前材料审核从Demo到生产
LLM 贷前材料审核:从 Demo 到生产环境的落地踩坑全记录
用 GPT-4 写了个 Prompt 跑通 10 条测试数据,准确率 95%,产品经理说"下周上线"。三个月后你还在修边角——因为真实场景的材料比测试集脏十倍。
一、贷前材料审核到底在审什么
小微企业申请贷款时,需要上传一堆材料:营业执照、对公流水、纳税证明、经营合同、法人身份证。传统做法是人工审核——一个信审员一天看 30-50 份,看走眼是常态,效率低、标准不统一、高峰期扛不住。
LLM 入场后,典型任务变成了这样:
| 材料类型 | 审核任务 | LLM 能力需求 |
|---|---|---|
| 营业执照 | 识别统一社会信用代码、经营范围、注册资本是否匹配 | OCR + 结构化提取 |
| 对公流水 | 判断流水是否真实(有没有明显伪造痕迹)、月均入账是否达标 | 数值推理 + 异常检测 |
| 纳税证明 | 提取年度纳税额,与流水交叉验证 | 多文档交叉校验 |
| 经营合同 | 判断合同真实性、识别关联交易 | 语义理解 + 实体关系抽取 |
| 法人身份证 | OCR 提取 + 公安库比对(OCR 层,LLM 不直接做比对) | 结构化字段提取 |
说白了就是:LLM 替代的不是"信审员看材料"这五个字,是"看→提取关键字段→比对一致性→标记疑点→写审核意见"这一整条流水线。
二、Demo 怎么写——30 行 Python 跑通
先看一个最小可行版本,用 OpenAI API 做营业执照审核:
import openai
import base64
from pathlib import Path
client = openai.OpenAI()
def audit_business_license(image_path: str) -> dict:
"""审核营业执照,返回结构化结果"""
with open(image_path, "rb") as f:
image_b64 = base64.b64encode(f.read()).decode()
response = client.chat.completions.create(
model="gpt-4o",
messages=[{
"role": "user",
"content": [
{
"type": "text",
"text": """你是信贷审核专家。请从这张营业执照中提取以下信息,以 JSON 返回:
{
"company_name": "企业名称",
"credit_code": "统一社会信用代码(18位)",
"legal_person": "法定代表人",
"registered_capital": "注册资本(万元,纯数字)",
"business_scope": "经营范围(取前100字)",
"establish_date": "成立日期(YYYY-MM-DD)",
"is_blacklisted": "经营范围是否包含禁止类目(是/否/不确定)",
"confidence": "整体置信度(0-1)"
}
如果某项无法识别,填写 null。"""
},
{
"type": "image_url",
"image_url": {"url": f"data:image/jpeg;base64,{image_b64}"}
}
]
}],
response_format={"type": "json_object"},
temperature=0.0 # 审核场景必须确定性输出
)
return eval(response.choices[0].message.content)
result = audit_business_license("test_license.jpg")
print(f"企业: {result['company_name']}")
print(f"注册资本: {result['registered_capital']}万元")
print(f"置信度: {result['confidence']}")
Demo 阶段这 30 行就能让产品经理眼前一亮。但 Demo 和生产的差距,至少隔着下面这六个坑。
三、生产落地的六个坑
坑 1:幻觉——LLM 最致命的缺陷
真实案例:营业执照上写"壹仟万元整",LLM 返回 registered_capital: 10000——多了一个零。因为"壹仟万"被识别成了"一千万"。
# 幻觉防御策略:后置校验层
def validate_capital(ocr_text: str, llm_value: float) -> bool:
"""用传统 OCR(PaddleOCR/EasyOCR)的结果交叉校验 LLM 提取值"""
import re
# 传统 OCR 识别中文大写数字
cn_num_map = {'零': 0, '壹': 1, '贰': 2, '叁': 3, '肆': 4,
'伍': 5, '陆': 6, '柒': 7, '捌': 8, '玖': 9}
# 如果 OCR 识别出"壹仟万" → 1000万,但 LLM 给了 10000万,触发告警
ocr_value = extract_cn_number(ocr_text) # 传统正则提取
if ocr_value and abs(llm_value - ocr_value) / ocr_value > 0.1:
return False # 差异 > 10%,回退人工审核
return True
核心原则:LLM 输出必须经过规则校验层,金额类字段尤甚。不要把 LLM 的输出当数据库查询结果用。
坑 2:多页 PDF 材料——不是每页都有用
银行流水动辄 50 页 PDF,其中 20 页是广告页、封面、条款说明。全部扔给 LLM 既浪费 token 又容易干扰判断。
from pypdf import PdfReader
def extract_relevant_pages(pdf_path: str) -> list[str]:
"""只提取包含交易记录的有效页面"""
reader = PdfReader(pdf_path)
relevant_pages = []
for i, page in enumerate(reader.pages):
text = page.extract_text()
# 关键特征:包含日期 + 金额 + 交易摘要
has_date = bool(re.search(r'\d{4}[-/]\d{2}[-/]\d{2}', text))
has_amount = bool(re.search(r'[¥¥]\s*\d[\d,]+\.\d{2}', text))
if has_date and has_amount:
relevant_pages.append(text)
return relevant_pages
坑 3:多文档交叉校验——营业执照 vs 流水 vs 合同
三份材料分别审核完,还得交叉校验:
def cross_validate(license_info: dict, bank_stmt: dict, contract_info: dict) -> list[str]:
"""多文档交叉校验,返回疑点清单"""
flags = []
# 校验1:流水中的交易对手方是否包含营业执照上的公司名
if license_info['company_name'] not in str(bank_stmt.get('counterparties', [])):
flags.append(f"银行流水中未发现与[{license_info['company_name']}]的交易记录")
# 校验2:经营合同金额是否在流水中有对应入账
if contract_info.get('total_amount'):
expected = contract_info['total_amount']
total_inflow = bank_stmt.get('total_inflow', 0)
if total_inflow < expected * 0.8:
flags.append(
f"合同金额 {expected} 万,但流水入账仅 {total_inflow} 万(不足80%),疑似虚假合同"
)
# 校验3:营业执照成立日期 vs 合同签订日期(不能先签合同后注册公司)
if license_info.get('establish_date') and contract_info.get('sign_date'):
if contract_info['sign_date'] < license_info['establish_date']:
flags.append("合同签订日期早于公司成立日期,材料存疑")
return flags
坑 4:Token 成本——一份 50 页流水审下来要多少钱
GPT-4o 的 image token 按分辨率计费。一份 50 页 PDF 转图片,每页约 2000 tokens,50 页 = 10 万 tokens。按 $5/1M input tokens 算,一份材料审核成本约 $0.5。
# 成本控制策略:两阶段审核
# 阶段1:用便宜模型(GPT-4o-mini / Qwen-VL)做初筛
# 阶段2:只有初筛"可疑"的才送 GPT-4o 做深度审核
def two_stage_audit(image_path: str) -> dict:
# 阶段1:轻量模型初筛,成本 ~1/10
cheap_result = call_model("gpt-4o-mini", image_path, temperature=0.0)
if cheap_result.get('confidence', 0) > 0.9 and not cheap_result.get('flags'):
return cheap_result # 高置信度 + 无疑点,直接通过
# 阶段2:只有可疑的才送大模型深度审
return call_model("gpt-4o", image_path, temperature=0.0)
坑 5:审核结论的"可解释性"
你告诉信审员"模型判断这份材料高风险",信审员问"为什么"。你不能回答"模型的 hidden state 这么说了"。
# LLM 审核必须产出三段式结论
audit_output = {
"decision": "人工复核", # 通过 / 拒绝 / 人工复核
"risk_score": 0.72, # 0-1
"evidence": [ # 关键:必须有证据链
{
"source": "营业执照",
"field": "registered_capital",
"finding": "注册资本仅 10 万元,低于准入门槛 50 万元",
"severity": "high"
},
{
"source": "对公流水_第3页",
"field": "transaction",
"finding": "2025-11-15 有一笔 200 万元大额入账,摘要为'借款',非经营性收入",
"severity": "medium"
}
],
"summary": "注册资本不达标 + 流水存在大额非经营性入账,建议人工复核"
}
每条 evidence 必须指向具体的材料页和字段,信审员能直接点开原始图片验证。这是 LLM 审核能被业务方接受的前提。
坑 6:生产部署——不是 FastAPI 套个壳就完事
/**
* 贷前材料审核服务——生产级实现
*/
@Service
public class MaterialAuditService {
@Resource
private LLMClient llmClient; // OpenAI / 私有化模型网关
@Resource
private AuditRuleEngine ruleEngine; // 规则后置校验层
@Resource
private AuditResultMapper auditMapper; // MySQL 持久化
@Resource
private RedisTemplate<String, Object> redis;
/**
* 审核入口——支持同步 + 异步两种模式
*/
public AuditResponse audit(MaterialAuditRequest request) {
String taskId = request.getTaskId();
// 1. 去重——同一份材料不重复审
String cacheKey = "audit:" + hashMaterial(request.getFiles());
AuditResponse cached = (AuditResponse) redis.opsForValue().get(cacheKey);
if (cached != null) {
log.info("命中缓存, taskId={}", taskId);
return cached;
}
// 2. 材料预处理——压缩、分页过滤、格式归一化
List<ProcessedPage> pages = preprocessor.process(request.getFiles());
// 3. 两阶段审核
Stage1Result stage1 = llmClient.audit("gpt-4o-mini", pages, request.getAuditType());
AuditResponse result;
if (stage1.getConfidence() > 0.9 && stage1.getFlags().isEmpty()) {
result = buildResponse(stage1, "通过");
} else {
Stage2Result stage2 = llmClient.audit("gpt-4o", pages, request.getAuditType());
result = buildResponse(stage2, stage2.getDecision());
}
// 4. 规则引擎后置校验——LLM 不可信,必须过规则
List<RuleViolation> violations = ruleEngine.validate(result);
if (!violations.isEmpty()) {
result.setDecision("人工复核");
result.getFlags().addAll(violations);
}
// 5. 缓存 + 持久化
redis.opsForValue().set(cacheKey, result, 24, TimeUnit.HOURS);
auditMapper.insert(result, taskId);
return result;
}
}
四、哪些场景 LLM 审得了、哪些审不了
| 场景 | LLM 能做吗 | 说明 |
|---|---|---|
| 营业执照信息提取 | ✅ 很好 | 标准模板、字段固定、结构化输出 |
| 银行流水真伪判断 | ⚠️ 部分能做 | 能识别明显伪造(金额对不上、格式异常),但专业造假需要规则引擎 |
| 经营合同风险条款识别 | ✅ 能做 | 识别关联交易、异常条款是 LLM 强项 |
| 身份证 OCR + 公安库比对 | ❌ 做不了 | OCR 用传统模型(PaddleOCR),比对走公安接口,LLM 只做字段格式化 |
| 财务报表分析 | ⚠️ 审慎 | 数字计算容易出错,建议 LLM 只做异常标注,数值校验用规则引擎 |
五、总结
LLM 做贷前材料审核,Demo 只需要 30 行代码,生产需要六个防御层。记住五条:
- LLM 输出不可信——每个关键字段(金额、日期、信用代码)必须过规则校验层,用传统 OCR 做交叉验证
- 两阶段审核降成本——轻量模型初筛 + 大模型深度审,90% 的材料用便宜模型就够了
- 审核结论必须有证据链——每条 risk flag 必须指向具体的材料页和字段,否则信审员不会信任
- 多文档交叉校验是 LLM 的杀手锏——传统 OCR 做不到"营业执照注册资本 10 万但合同金额 500 万"这种跨文档矛盾检测
- LLM 是流水线的一环,不是整个流水线——OCR 预处理 → LLM 语义理解 → 规则引擎校验 → 人工复核,四个环节缺一不可