从零构建企业级"中文法律智能问答系统":FastAPI+phpStudy+Milvus+真流式RAG实战

🌟 引言
在法律咨询领域,大语言模型(LLM)的"幻觉"是致命的。一个虚构的法条可能会导致用户做出错误的判断。此外,法律文本极长,传统的向量检索极易丢失上下文;而在高并发场景下,大模型的流式输出又容易因资源抢占导致阻塞。
为了解决这些痛点,我开发了一套基于 FastAPI + phpStudy (MySQL) + Milvus + Redis + 通义千问 的双引擎中文法律问答系统 。项目采用 "BM25拦截 + 意图识别 + RAG深度检索 + 真流式生成" 的架构。本文将带你深入剖析该系统的核心设计与工程化部署避坑指南。
🏗️ 一、 系统整体架构设计
系统的核心逻辑是:先"快"后"准",兜底"稳"。
- 快速路由拦截:用户提问后,先经过本地 BERT 意图分类与 BM25 检索。如果命中高频 FAQ 且通过 IDF 覆盖率校验,直接查 Redis/MySQL 毫秒级返回。
- RAG 深度检索:未命中 FAQ 或属于专业法律咨询,进入 RAG 引擎。动态选择检索策略(直接检索、HyDE、子查询、回溯),使用 BGE-M3 进行稠密+稀疏混合检索,再用 BGE-Reranker 重排。
- 真流式生成:通过异步队列包装大模型生成器,生成一段推流一段。
- 持久化 :多轮对话记录落库 MySQL,问答对缓存至 Redis。

🐳 二、 基建先行:环境搭建与数据库设计
1. 混合部署方案:phpStudy_pro + Docker Milvus
- 关系型数据 :本项目使用 phpStudy_pro 内置的 MySQL 数据库。对于结构化极强的法律问答对(FAQ)以及多轮对话历史,MySQL 是最稳的选择。开启 phpStudy_pro 一键启动 MySQL,结合代码中的
pymysql和线程锁,轻松搞定数据持久化。 - 向量数据 :Milvus 是目前主流的向量数据库,但它依赖 etcd 和 MinIO。我选择使用 Docker Compose 进行容器化部署,一键拉起所有组件,保证了环境的一致性。

2. 优雅的配置与并发安全
在 config.py 中,我们利用 configparser 和 os.getenv 实现了配置的优雅映射,并支持环境变量兜底。
在 mysql_client.py 中,针对多并发,我们引入了 全局线程锁 db_lock,防止多线程下共享同一个 MySQL 连接导致的游标冲突:
python
db_lock = threading.Lock()
def get_cursor(self):
try:
self.connection.ping(reconnect=True) # 断线重连机制
except Exception:
self._connect()
return self.connection.cursor()
🔥 三、 核心亮点与代码解析
1. 巧妙的防误判:BM25 + IDF 加权覆盖率校验
纯靠 BM25 打分很容易翻车。例如:"嫖娼怎么处罚?"和"醉驾怎么处罚?",BM25 可能会因为"怎么处罚"这个泛化词的高频匹配,错误地命中另一条 FAQ。
在 bm25_search.py 中,我们设计了 _key_tokens_covered 方法,通过计算 IDF 加权的关键词覆盖率,给予特定法律主体(未登录词)最高权重。如果最高分超阈值但关键词覆盖不足,强制转 RAG,彻底解决"答非所问"。
2. 法律长文本利器:Parent-Child 父子块切分
法律法条往往上下文关联极强。我们在 document_processor.py 实现了父子块切分(父块 1200 字符保留语境,子块 300 字符用于向量召回)。并在 init_milvus.py 初始化时,将完整的"法律依据+解答"放入 parent_content,召回后喂给大模型,保证大模型不"断章取义"。
3. 极致并发:FastAPI 真流式与 PyTorch 锁分离
- 锁的下沉 :PyTorch 推理(Embedding/Rerank/BERT)是 CPU/GPU 密集型,如果不加锁会崩溃;但 LLM 生成是 I/O 密集的。我们在
rag_system.py中只在检索阶段使用threading.RLock,生成阶段完全释放锁。 - 异步解耦 :在
app.py中,使用asyncio.Queue和loop.run_in_executor将同步生成器解耦为真流式 WebSocket 推送,完美支撑高并发。
4. 全栈体验:丝滑的前端流式交互
在 static/index.html 中,前端原生使用 WebSocket 接收流式数据。利用 marked.js 配合 WebSocket 逐字接收,实现打字机效果。同时,用户消息按纯文本渲染(textContent),机器人回复才解析 Markdown(innerHTML),防止 XSS 注入。

💣 四、 部署实战与踩坑经验分享
1. Docker 部署 Milvus(避坑指南)
Milvus 官方推荐使用 Docker Compose 部署。在服务器上准备好 docker-compose.yml 后,执行 docker-compose up -d,它就会自动拉起 etcd、minio 和 milvus-standalone 三个容器。
踩坑点:Milvus 对内存要求较高,如果服务器内存小于 16GB,极容易发生 OOM(内存溢出)导致容器自动重启。建议增加 swap 空间或在配置文件中调低 Milvus 的缓存参数。
2. Windows/Linux 下的 OpenMP 冲突
在使用 PyTorch 和某些 C++ 扩展库时,经常会报 OMP: Error #15。解决方案是在 app.py 顶部(import 之前)强制设置:
python
os.environ["KMP_DUPLICATE_LIB_OK"] = "TRUE"
os.environ["OMP_NUM_THREADS"] = "1"
3. FastAPI 多进程与模型加载
不要把模型加载写在模块顶层,而是使用懒加载单例模式 (get_qa_system)。在 uvicorn 启动时设置 workers=1(单卡 GPU 场景),否则每个 worker 都会加载一份模型,显存瞬间爆炸。
4. Redis 数据序列化踩坑
在 redis_client.py 中,set_data 存的是 json.dumps 后的字符串。如果直接从 Redis 取出就返回,前端接收到的答案会带引号(如 '"试用期6个月"')。必须通过 json.loads 还原成 Python 对象。
🎯 总结与展望
本项目成功将 传统检索(BM25) 、向量检索(Milvus) 、模型微调(BERT) 与 大模型(LLM) 深度融合。无论是防误判的 IDF 加权,还是提升吞吐的流式并发改造,以及 Docker 化的工程部署,都可以无缝复用到医疗、金融等垂直领域的 AI 助手开发中。