观澜:一个 AI 驱动的不良资产尽职调查系统实践
文章目录
- 一、背景:一个被低估的 AI 落地场景
- 二、技术架构:不追新,只求稳
- 容器化部署
- 三、核心流程:两阶段流水线
- 数据采集:优雅降级策略
- AI 分析:三条并行的 LLM 推理线
- 四、几个值得一说的技术细节
- 4.1 后端异步任务的双模式调度
- 4.2 Token 黑名单的自动过期
- 4.3 GSXT 反爬对策
- 4.4 LLM 输出的鲁棒性处理
- 五、为什么选 DeepSeek-V3 而不是 GPT-4
- 六、教训与反思
- 6.1 过早抽象 = 过度设计
- 6.2 WeasyPrint 的 Windows 兼容性
- 6.3 Redis 多 DB 的隔离策略
- 6.4 对国内网络环境的适应
- 七、写在最后
一、背景:一个被低估的 AI 落地场景
不良资产处置是一个万亿级的市场。这个行业的起点和核心是尽职调查——你需要搞清楚每个债务人是否还有偿还能力、有没有隐匿资产、实际控制人是谁、有没有关联企业转移资产。
传统的尽调流程是这样的:
- 手工打开 GSXT(国家企业信用信息公示系统),逐个搜索债务人名称
- 逐页截图、记录股东、对外投资、行政处罚等信息
- 打开裁判文书网,查询涉诉记录
- 把以上信息粘贴到 Word 里,人工写分析结论
- 一个批次的 30-50 个债务人,一个项目经理要干 2-3 周
这个流程有两个核心痛点:
- 信息搜集极其繁琐:每个债务人有 5-10 个数据维度,每个维度都需要在不同网站上查询、记录
- 分析依赖经验:判断关联关系、识别风险信号、评估回收价值,高度依赖资深项目经理的个人经验
"观澜"这个项目的目标很明确:用 AI 自动化掉这个流程。
二、技术架构:不追新,只求稳
![系统架构]
┌─────────────────────────────────────────────────────────────┐ │ Vue 3 + TypeScript SPA │ │ Element Plus + ECharts │ └──────────────────────┬──────────────────────────────────────┘ │ REST API ┌──────────────────────▼──────────────────────────────────────┐ │ FastAPI (Python) │ │ JWT Auth / SQLAlchemy / Pydantic v2 │ └──────┬───────────────────────────────────┬──────────────────┘ │ │ ┌──────▼──────┐ ┌───────▼──────────────────┐ │ MySQL 8 │ │ Celery Worker │ │ (业务数据) │ │ (异步任务流水线) │ └─────────────┘ └───────┬──────────────────┘ │ ┌──────────────┐ ┌───────▼──────────────────┐ │ Redis 7 │◄─────────────────┤ Dify LLM Orchestration │ │ (缓存/MQ) │ │ DeepSeek-V3 / Qwen-Max │ └──────────────┘ └──────────────────────────┘技术上我没有选择最时髦的方案,而是做了务实的权衡:
| 层 | 选型 | 为什么 |
|---|---|---|
| 前端 | Vue 3 + Element Plus | 团队最熟,表单/表格/对话框开箱即用 |
| 后端 | FastAPI | Python 异步生态最成熟的选择,自动生成 OpenAPI 文档 |
| 数据库 | MySQL 8.0 | 业务数据 ACID 要求,关系复杂,没必要上 PG |
| 缓存/MQ | Redis 7 | 同时做缓存和 Celery Broker,减少运维组件 |
| 异步任务 | Celery | 尽调是长时间运行的批处理任务,必须异步 |
| LLM 编排 | Dify | 可视化编排 3 个并行的 LLM 节点,降低 prompt 维护成本 |
| LLM | DeepSeek-V3 | 中文语义理解强,性价比极高 |
容器化部署
全项目用 Docker Compose 编排了 6 个服务:
services:mysql# 业务数据库redis# 缓存 + Celery Broker + Session 存储backend# FastAPI 应用服务器backend-worker# Celery 异步工作节点frontend# Nginx 托管 Vue SPA没有上 k8s——项目初期,一台机器 + Docker Compose 足够。Elasticsearch 和 MinIO 排在了 P1,等用户量上来再加。
三、核心流程:两阶段流水线
整个尽调流程分为两个阶段,由 Celery 编排:
上传债务人列表 (Excel/CSV) │ ▼ ┌───────────────────┐ │ 第一阶段:数据采集 │ ← 串行执行,带限速和反爬 │ GSXT 公开数据 │ │ + Mock 降级 │ └─────────┬─────────┘ ▼ ┌───────────────────┐ │ 第二阶段:AI 分析 │ ← Dify 工作流,3 个 LLM 并 │ 关联关系穿透 │ 行执行 │ 风险识别与打标 │ │ 还款能力评估 │ └─────────┬─────────┘ ▼ ┌───────────────────┐ │ 报告生成 │ │ PDF (WeasyPrint) │ │ + ZIP 批量打包 │ └───────────────────┘数据采集:优雅降级策略
GSXT 是核心数据源,但它有严格的反爬机制。我们的策略不是"死磕",而是优雅降级:
流程: 1. 查 Redis 缓存 (24h 过期) 2. 未命中 → 请求 GSXT 3. 请求成功 → 解析 HTML (三层降级解析) 4. 请求失败 (验证码/403/超时) → Mock 数据生成Mock 数据生成是一个有意思的设计——它不是随机造假,而是确定性生成:
importhashlibdefgenerate_mock_data(company_name:str):seed=hashlib.md5(company_name.encode()).hexdigest()random.seed(seed)# ... 用种子生成注册资本、涉诉数量、股东结构等# 同样的公司名始终得到同样的 Mock 数据这样保证了:
- AI 分析管道永远有输入数据,不会因为数据源不可用而中断
- 测试时可复现,按公司名就能得到确定的结果
- 每个债权人都"看起来像个真实企业"
AI 分析:三条并行的 LLM 推理线
在 Dify 工作流中,三个 LLM 节点并行执行:
┌──────────────────────────┐ │ Hidden Relation │ │ Penetration │ │ DeepSeek-V3 temp=0.3 │ │ 隐藏关联关系穿透 │ └──────────┬───────────────┘ │ 输入数据 ────────────────────┼─────────┬─────────┐ │ │ │ ┌──────────▼──┐ ┌──▼────────┐ │ │ Risk Tagging │ │ Repayment │ │ │ DeepSeek-V3 │ │ Capacity │ │ │ temp=0.2 │ │ DeepSeek │ │ │ 风险标签 │ │ 还款能力 │ │ └──────────┬──┘ └──┬────────┘ │ │ │ │ └─────────┴─────────┘ │ ▼ ┌──────────────────┐ │ Jinja2 Merger │ │ 统一报告格式 │ └──────────────────┘1. 关联关系穿透(温度 0.3):分析股东树、人员交叉任职、投资链条,识别隐形控制人、代持、循环担保。这是法律尽调中最耗脑力的部分。
2. 风险语义理解与打标(温度 0.2):将企业经营状态、司法涉诉等数据转化为标准化的风险标签,三级风险等级(红/黄/绿),覆盖 16+ 种风险类型。
3. 还款能力评估(温度 0.2):多维度评估——经营状况、资产质量、负债水平、现金流、关联方支持、逃废债倾向,输出回收等级(高/中/低)和预估回收金额区间。
四、几个值得一说的技术细节
4.1 后端异步任务的双模式调度
Celery 在 Windows 开发环境下有个老问题——和 FastAPI 的事件循环冲突。我采取的策略是一个核心函数,两种调度方式:
# 核心业务逻辑(纯异步)asyncdefrun_due_diligence_task(...):# ... 与事件循环无关的业务代码# Celery 包装(生产环境)@celery_app.task(bind=True,max_retries=3)defprocess_due_diligence_task(self,...):asyncio.run(run_due_diligence_task(...))# 开发环境直接调用ifconfig.CELERY_TASK_ALWAYS_EAGER:asyncio.create_task(run_due_diligence_task(...))else:process_due_diligence_task.delay(...)通过CELERY_TASK_ALWAYS_EAGER这个开关,开发时直接用create_task,生产用 Celery 调度,一套代码两套部署模式。
4.2 Token 黑名单的自动过期
JWT 登出后 token 仍然有效直到过期,这是 JWT 的固有问题。通用的做法是维护一个黑名单,但需要定时清理。
用 Redis 的 TTL 特性,这个问题的解法变得很优雅:
asyncdeflogout(token:str):# 解析 token 获取 jti 和剩余过期时间payload=decode_token(token)jti=payload.get("jti")exp=payload.get("exp")remaining=exp-time.time()# 写入黑名单,自动过期awaitredis.setex(f"token_blacklist:{jti}",remaining,"1")不需要定时任务,不需要手动清理,Redis 在 TTL 归零后自动删 key。这个模式的寓意是——让基础设施为你做运维。
4.3 GSXT 反爬对策
如果说整个系统中有什么地方最像"工程实践"而非"业务开发",就是这个 GSXT 客户端的实现了:
- 8 个 User-Agent 轮换:覆盖 Chrome/Firefox/Edge/Safari,Windows/macOS 全平台
- 请求抖动:每次请求间隔
0.8s ± 40%随机化 - Token 桶限速:每分钟最多 20 次请求
- 验证码检测:响应体中关键词匹配,命中则立即切换策略
- 三层 HTML 解析回退:
dl/dt/dd结构 → 表格行解析 → 正则提取
4.4 LLM 输出的鲁棒性处理
LLM 的输出天然不稳定。我必须在生产环境中处理这些情况:
defsanitize_llm_output(raw_text:str)->dict:# 1. 替换 Python 布尔值text=re.sub(r'\bTrue\b','true',raw_text)text=re.sub(r'\bFalse\b','false',text)text=re.sub(r'\bNone\b','null',text)# 2. 剥离 Markdown 代码块标记text=re.sub(r'```(?:json)?','',text)# 3. 尝试 JSON 解析try:returnjson.loads(text)exceptjson.JSONDecodeError:# 4. 实在解析失败,返回原始文本return{"raw_output":raw_text}核心原则:LLM 会犯错,pipeline 不能崩溃。无论 LLM 输出的质量如何,管道都要能走完。质量问题是 AI 调优的事情,系统可用性是工程的事情。
五、为什么选 DeepSeek-V3 而不是 GPT-4
这是一个经常被问到的问题。理由很简单:
- 中文语义理解:在处理企业名称、法律文书、经营异常信息时,DeepSeek 的中文语料覆盖远好于 GPT-4
- 性价比:价格大约是 GPT-4 的 1/20,一次尽调任务可能要调用 50-100 次 LLM,成本差距是数量级的
- 数据合规:金融行业的数据合规要求严格,通过 Dify 可以灵活切换模型,未来可以无缝迁移到私有化部署的 Qwen-Max
- 效果足够:在结构化分析和文本分类场景上,DeepSeek-V3 与 GPT-4 的差距微乎其微
六、教训与反思
6.1 过早抽象 = 过度设计
最初的 collector 模块我设计了一个完整的策略模式(Strategy Pattern),每种数据源一个实现类。最后发现——GSXT 占了 95% 的数据采集量,其他数据源只有 5%。抽象了 6 个接口,实际只用到 1 个。
教训:等你有第二个实现的时候再抽象。
6.2 WeasyPrint 的 Windows 兼容性
WeasyPrint 在 Linux 上运行良好,但在 Windows 上需要 GTK3 运行时。这个坑在开发初期就发现了,解决方式是惰性加载:
# 不在导入时加载,在调用时加载defgenerate_pdf(...):fromweasyprintimportHTML# 延迟导入HTML(string=html).write_pdf(target)这样即使开发机上没有 GTK3,FastAPI 也能正常启动,不影响其他功能开发。
6.3 Redis 多 DB 的隔离策略
一个 Redis 实例承担了三个职责,通过 DB 编号隔离:
| DB | 用途 | 策略 |
|---|---|---|
| 0 | Celery Broker | 无需持久化 |
| 1 | 业务缓存 (GSXT 数据) | 24h TTL + LRU |
| 2 | Token 黑名单 | 按 TTL 自动过期 |
没有引入额外的消息队列,减少了运维复杂度。Redis 7 的单实例性能在这个量级上完全够用。
6.4 对国内网络环境的适应
Dify Plugin Daemon 的 Docker 镜像在 Docker Hub 上,国内拉取经常超时。解决方案——注释掉,用 Dify Cloud API 替代。这个决定是在第一次docker-compose up失败后 10 分钟做出的。
在国内做开源基础设施开发,先确认所有依赖能不能拉下来。
七、写在最后
“观澜"这个项目目前还处于 MVP 阶段的尾声。从我的角度看,这个项目的价值不在于技术有多"新”,而在于把一个非常传统的业务场景用 AI 重新做了一遍。
不良资产行业的信息化程度整体偏低,很多项目经理还在用 Excel + 微信来管理尽调流程。AI 的落地机会不在于取代人,而在于把专业人员的精力从"信息搬运工"中解放出来,让他们专注于真正需要判断力的事情——比如怎么设计处置方案、怎么和债务人谈判。
如果你对这个项目感兴趣,或者你在做类似的 AI + 传统行业落地的尝试,欢迎交流。
项目上线:http://118.178.232.83
