引言
在医疗健康领域,快速准确地获取信息至关重要。传统的信息检索方式往往依赖关键词匹配,难以理解用户自然语言提问背后的语义。近年来,随着大语言模型(LLM)和向量数据库技术的成熟,构建一个能够理解复杂医学问题、并从海量文档中精准召回相关知识的智能问答系统成为可能。
本文将结合一段完整的Python代码,详细介绍如何利用FastAPI 、Milvus (向量数据库)、智谱Embedding模型 以及DeepSeek大语言模型,搭建一个具有语义检索、混合排序(RRF)和父子文档检索能力的RAG(检索增强生成)系统。我们将逐层剖析代码的设计思路、技术选型与实现细节,并探讨其在实际部署中的考量。
一、系统架构与技术选型
1.1 整体流程
系统接收用户问题(question),依次执行以下步骤:
- 语义召回:使用Milvus向量数据库,基于稠密向量和稀疏向量(BM25)进行多路召回。
- 重排序(Rerank):采用RRF(Reciprocal Rank Fusion)算法融合多路召回结果,输出前10个最相关文档片段。
- 父子文档检索:针对PDF等长文档,利用ParentDocumentRetriever策略,先检索子片段(chunk),再返回其父文档(完整段落),补充更丰富的上下文。
- 提示构建与生成:将检索到的上下文与用户问题组装成提示(Prompt),调用DeepSeek模型生成最终答案。
1.2 技术栈
| 组件 | 选型 | 说明 |
|---|---|---|
| Web框架 | FastAPI + Uvicorn | 轻量异步,支持高并发 |
| 向量数据库 | Milvus (Lite模式) | 支持稠密+稀疏混合检索,内置BM25函数 |
| 嵌入模型 | 智谱AI embedding-3 |
中文语义理解能力强 |
| 大语言模型 | DeepSeek API | 性价比高,推理能力强 |
| 文档分割 | LangChain RecursiveCharacterTextSplitter |
灵活控制chunk大小 |
| 检索策略 | LangChain ParentDocumentRetriever |
父子文档关联检索 |
| 跨域处理 | CORS中间件 | 支持前端直接调用 |
二、环境准备与关键初始化
2.1 依赖库导入
python
import os
import uvicorn
from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
import json
import datetime
from zai import ZhipuAiClient
from langchain_milvus import Milvus, BM25BuiltInFunction
from model import ZhipuAIEmbeddings, create_deepseek_client, generate_deepseek_answer
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_classic.storage import InMemoryStore
from langchain_classic.retrievers.parent_document_retriever import ParentDocumentRetriever
zai是智谱AI官方SDK,用于调用嵌入模型。model为自定义模块,封装了DeepSeek客户端创建和回答生成函数(在代码中未展示完整实现,但逻辑清晰)。
2.2 环境变量与API Key
python
os.environ["TOKENIZERS_PARALLELISM"] = "false"
os.environ['DEEPSEEK_API_KEY'] = '***'
deepseek_api_key = os.getenv("DEEPSEEK_API_KEY")
client_embedding = ZhipuAiClient(api_key="***")
- 关闭tokenizer并行警告,避免多进程冲突。
- 实际部署时应使用环境变量或密钥管理服务,避免硬编码。
2.3 CORS配置
python
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
允许所有域访问,便于前后端分离开发。
三、Milvus向量存储的构建
3.1 双路召回策略
代码中创建了两个Milvus实例:
milvus_vectorstore(URI =./milvus_agent.db):用于通用知识库检索。pdf_vectorstore(URI =./pdf_agent.db):专门用于PDF长文档的父子检索。
两者均采用混合向量设计:
- 稠密向量(dense) :由
ZhipuAIEmbeddings生成,用于语义相似度计算。 - 稀疏向量(sparse) :通过
BM25BuiltInFunction()内置函数生成,用于关键词匹配。
索引参数:
python
index_params=[
{"metric_type": "IP", "index_type": "IVF_FLAT"}, # 稠密向量用内积
{"metric_type": "BM25", "index_type": "SPARSE_INVERTED_INDEX"} # 稀疏向量用BM25
]
这种设计使得检索既能够捕捉语义相关性,又能保留精确关键词匹配能力,为后续RRF融合打下基础。
3.2 父子文档检索器
对于PDF等长文档,简单分块可能丢失上下文。ParentDocumentRetriever通过两个分割器解决:
child_splitter:chunk_size=200,负责生成细粒度子片段,用于检索。parent_splitter:chunk_size=1000,负责生成更大的父段落,用于提供丰富上下文。
检索时,系统根据query匹配到子片段,然后从InMemoryStore中取出对应的父文档完整内容,从而既保证检索精度,又避免上下文截断。
四、核心API接口实现
4.1 请求处理
python
@app.post("/")
async def chatbot(request: Request):
json_post_raw = await request.json()
json_post = json.dumps(json_post_raw)
json_post_list = json.loads(json_post)
query = json_post_list.get('question')
接收JSON格式的POST请求,提取question字段。
4.2 语义检索与重排序
python
recall_rerank_milvus = milvus_vectorstore.similarity_search(
query,
k=10,
ranker_type="rrf",
ranker_params={"k": 100}
)
similarity_search方法集成了多路召回和重排序。ranker_type="rrf":使用倒数排名融合算法,将稠密和稀疏检索结果合并排序,参数k=100控制平滑因子。- 返回前10个最相关文档。
4.3 PDF文档补充召回
python
retrieved_docs = parent_retriever.invoke(query)
if retrieved_docs and len(retrieved_docs) >= 1:
res = retrieved_docs[0].page_content
context = context + "\n" + res
从PDF向量库中检索,取第一个父文档内容追加到上下文,增加信息覆盖面。
4.4 构建提示词
python
SYSTEM_PROMPT = """
System: 你是一个非常得力的医学助手, 你可以通过从数据库中检索出的信息找到问题的答案.
"""
USER_PROMPT = f"""
User: 利用介于<context>和</context>之间的从数据库中检索出的信息来回答问题, 具体的问题介于<question>和</question>之间. 如果提供的信息为空, 则按照你的经验知识来给出尽可能严谨准确的回答, 不知道的时候坦诚的承认不了解, 不要编造不真实的信息.
<context>
{context}
</context>
<question>
{query}
</question>
"""
- 明确角色(医学助手)。
- 规定回答依据(检索信息优先,空则依赖自身知识,且禁止捏造)。
- 使用标记符包裹上下文和问题,便于模型理解结构。
4.5 调用DeepSeek生成答案
python
response = generate_deepseek_answer(
client_llm,
SYSTEM_PROMPT + USER_PROMPT.format(context, query)
)
generate_deepseek_answer为自定义函数,内部调用DeepSeek API(如deepseek-chat模型),返回文本回答。
4.6 返回结果
python
now = datetime.datetime.now()
time = now.strftime("%Y-%m-%d %H:%M:%S")
answer = {
"response": response,
"status": 200,
"time": time
}
return answer
附带状态码和时间戳,便于前端追踪。
五、关键设计亮点
5.1 混合检索与RRF融合
单靠稠密向量容易遗漏关键词信息,单靠BM25则无法理解同义词。两者结合并使用RRF算法,能够平衡召回率和精准度,尤其适合医学领域术语严谨、同义表达多样的特点。
5.2 父子文档策略
医学文档(如指南、文献)往往段落较长,直接分割成小chunk会丢失逻辑完整性。通过父子检索,检索到匹配的子句后,返回其所属的完整段落,让LLM拥有更充分的上下文,提升回答质量。
5.3 灵活的提示设计
提示中加入了**"若上下文为空则依靠自身知识"的指令,增强了系统对未知问题的容错性,同时通过"不知道时坦诚承认"**来抑制幻觉,符合医学场景对准确性的高要求。
5.4 异步与轻量化部署
使用FastAPI的异步特性,支持高并发请求。Milvus采用本地文件模式(./milvus_agent.db),无需单独部署服务,降低了运维复杂度,适合中小规模应用。
六、部署与运行
6.1 启动服务
python
if __name__ == '__main__':
uvicorn.run(app, host='0.0.0.0', port=8103, workers=1)
执行脚本后,服务监听8103端口,可通过http://localhost:8103访问。
6.2 数据准备
- 需提前将知识文档(如医学教材、指南)分割并嵌入到Milvus中(代码未展示写入逻辑,但需预先执行)。
- PDF文档需先解析并存入
pdf_vectorstore对应的集合。
6.3 测试示例
bash
curl -X POST http://localhost:8103/ -H "Content-Type: application/json" -d '{"question":"高血压的常见并发症有哪些?"}'
返回JSON包含response字段。
七、未来优化方向
- 多轮对话:当前为单轮问答,可引入记忆模块实现上下文连贯。
- 文档动态更新:结合Milvus的upsert能力,实现知识库的实时增量更新。
- 细粒度重排序:可引入Cross-Encoder模型对RRF后的top-k结果进行二次精排,进一步提高相关度。
- 监控与日志:接入日志系统,记录检索效果和用户反馈,用于持续调优。
- 安全性:API Key应通过环境变量或密钥服务管理,避免代码泄露。
结语
本文详细解析了一段基于FastAPI、Milvus和DeepSeek的医学问答系统代码。通过混合检索、父子文档策略和精心设计的提示词,系统在准确性和上下文完整性上达到了较好的平衡。该架构不仅适用于医学领域,也可迁移到法律、金融等需要精确知识检索的场景。希望本文能为读者在构建自己的RAG系统时提供参考与启发。
如果你对代码中的某些部分有疑问,或希望了解更多实现细节,欢迎在评论区交流讨论!