目录
[1. 下载医学答问库](#1. 下载医学答问库)
[📊 主要中文医疗数据集对比](#📊 主要中文医疗数据集对比)
[2. 安装ollama](#2. 安装ollama)
[💡 安装时留意一下](#💡 安装时留意一下)
[2.1 ollama + 关键字搜索 + 整理](#2.1 ollama + 关键字搜索 + 整理)
[3. 下载向量计算模型](#3. 下载向量计算模型)
[下载脚本(推荐用 ModelScope)](#下载脚本(推荐用 ModelScope))
[备选:用 Hugging Face 镜像站下载](#备选:用 Hugging Face 镜像站下载)
[4. 下载Qdrant](#4. 下载Qdrant)
[🛠️ 使用方法(解压即用)](#🛠️ 使用方法(解压即用))
[5. 导入数据](#5. 导入数据)
[6. 测试问答](#6. 测试问答)
[7. 总结](#7. 总结)
[RAG 的标准骨架](#RAG 的标准骨架)
[RAG 各环节的通用叫法](#RAG 各环节的通用叫法)
[但 RAG 不止这一种形态](#但 RAG 不止这一种形态)
[为什么推荐 BGE-Reranker](#为什么推荐 BGE-Reranker)
1. 下载医学答问库
📊 主要中文医疗数据集对比
下面的表格整理了几个当前主流且各有侧重的数据集,可以根据自己的需求来判断。
| 数据集 | 规模 | 特点与适用场景 | 数据来源/质量 |
|---|---|---|---|
| Huatuo-26M | 2600万 QA对 | 规模最大 的中文医疗问答数据集。适合用于预训练或需要海量知识注入的场景。 | 整合了在线问诊记录、医学百科和知识库,覆盖面广但质量可能参差不齐。 |
| MedicalQA (MedArk-KI) | 约146万 | 高质量、结构化的SFT数据集 。适合监督微调(SFT) 或作为RAG的检索库,数据经过清洗和审核。 | 包含多个子集,其中"DX"子集来自丁香医生,由医生撰写并交叉审核,质量很高。 |
| TCM-Ladder | 5.2万+ | 首个中医多模态评估基准 。适合评估中医大模型的能力,覆盖文本、图像、视频。 | 涵盖中医核心学科,包含选择题、填空、诊断对话等多种任务类型,用于评测而非训练。 |
| JMED | 1千个问题 | 高难度、贴近真实的临床推理数据集 。每个问题有21个回答选项 ,适合评估模型的鉴别诊断和复杂推理能力。 | 源自京东大医院的真实会诊数据,更能反映患者描述的模糊性和临床诊断的动态过程。 |
| CMB / CMExam | 28万+/6.8万 | 中文医学考试基准。适合评估模型在标准化医学知识上的掌握程度。 | 基于中国执业医师/护士等资格考试题目构建,是衡量模型基础医学知识的权威标准之一。 |
-
如果想构建RAG的检索库 :MedicalQA 是一个很好的起点。它的数据质量高,结构清晰,非常适合作为知识库来检索。
-
如果目标是微调一个医疗对话模型 :Huatuo-26M 提供了海量数据用于预训练,之后可以再用 MedicalQA 的高质量数据进行指令微调,这是一个常见的"预训练+微调"组合。
-
如果想专门评估中医或临床推理能力 :那么 TCM-Ladder 或 JMED 这类专门设计的基准测试,比通用问答数据集更有针对性。
这里使用一个脚本来下载模型:
from huggingface_hub import snapshot_download
snapshot_download(
repo_id="Bolin97/MedicalQA",
repo_type="dataset",
local_dir="./MedicalQA",
endpoint="https://hf-mirror.com"
)
下载后,里面是一组JSON文件。
2. 安装ollama
Ollama 的官方下载地址是:Download Ollama on macOS 。
打开后它会自动识别你的系统,直接下载 Windows 版的 OllamaSetup.exe 即可 。
💡 安装时留意一下
安装包默认会装在 C 盘用户目录下,软件本身不大(约 4GB 空间),但后续下载的模型文件会很大(几 GB 到几十 GB)。
如果你 C 盘空间紧张,可以在安装时用命令行指定路径,比如装在 D 盘:
OllamaSetup.exe /DIR="D:\Ollama"
或者装完后,把模型存储位置改到别的盘(通过设置 OLLAMA_MODELS 环境变量)。
在chat页面输入
qwen2.5:7b
下载模型后就能聊天了。
2.1 ollama + 关键字搜索 + 整理
最简单的方式是让模型针对用户的提问转为关键字,用关键字搜索,喂给模型,让模型给出合适的答案:
import json
import pandas as pd
import requests
import os
# ============ 配置 ============
OLLAMA_URL = "http://localhost:11434/api/chat"
MODEL = "qwen2.5:7b"
DATA_DIR = "./MedicalQA"
# 选择加载哪些子集,HT 太大,默认先关掉
LOAD_DX = True
LOAD_TCM = True
LOAD_MB = True
LOAD_HT = False # 想吃 HT 就改 True,但很占内存
MAX_ANSWER_LEN = 1500
MAX_CONTEXT_ROWS = 20 # 喂给模型的上限,防止上下文爆掉
MAX_KEYWORDS = 8 # 关键词最多几个
# ============ 1. 加载数据 ============
frames = []
if LOAD_DX:
with open(os.path.join(DATA_DIR, "DX.json"), "r", encoding="utf-8") as f:
frames.append(pd.DataFrame(json.load(f)))
if LOAD_TCM:
with open(os.path.join(DATA_DIR, "TCM.json"), "r", encoding="utf-8") as f:
frames.append(pd.DataFrame(json.load(f)))
if LOAD_MB:
with open(os.path.join(DATA_DIR, "MB.json"), "r", encoding="utf-8") as f:
frames.append(pd.DataFrame(json.load(f)))
if LOAD_HT:
with open(os.path.join(DATA_DIR, "HT.json"), "r", encoding="utf-8") as f:
frames.append(pd.DataFrame(json.load(f)))
df = pd.concat(frames, ignore_index=True)
print(f"共加载 {len(df)} 条记录")
# 统一列名(HT 的字段可能不同,需要时再适配)
print("列名:", df.columns.tolist())
# ============ 2. 调用 Ollama ============
def call_ollama(system_prompt, user_prompt):
payload = {
"model": MODEL,
"messages": [
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt},
],
"stream": False,
}
resp = requests.post(OLLAMA_URL, json=payload)
resp.raise_for_status()
return resp.json()["message"]["content"]
# ============ 3. 抽取关键词 ============
KEYWORD_SYSTEM = """你是一个医疗检索助手。用户会用口语描述症状,你需要把它转成适合在医学问答库里检索的关键词。
要求:
1. 提取出最可能的疾病名、症状名、部位、诱因等关键词。
2. 输出 3-8 个关键词,用中文顿号「、」分隔。
3. 只输出关键词本身,不要解释,不要加任何其他文字。
4. 如果涉及多种可能,把可能的病名也列出来。
示例:
输入:我被猫抓了,长了一个疙瘩,医生说不是HPV,是瘢痕
输出:猫抓、疙瘩、瘢痕、疤痕、HPV"""
def extract_keywords(question):
raw = call_ollama(KEYWORD_SYSTEM, question).strip()
keywords = [k.strip() for k in raw.replace(",", "、").split("、") if k.strip()]
return keywords[:MAX_KEYWORDS]
# ============ 4. 多关键词检索(返回全部命中) ============
# def search_by_keywords(keywords):
# hits = []
# seen_ids = set()
# for kw in keywords:
# mask = (
# df["question"].str.contains(kw, na=False, regex=False)
# | df["answer"].str.contains(kw, na=False, regex=False)
# | df["name"].str.contains(kw, na=False, regex=False)
# )
# for _, row in df[mask].iterrows():
# if row["id"] not in seen_ids:
# seen_ids.add(row["id"])
# hits.append(row)
# return pd.DataFrame(hits)
PER_KEYWORD_TOP = 10 # 每个关键词取前几条
def search_by_keywords(keywords, per_keyword=PER_KEYWORD_TOP):
all_rows = []
seen_ids = set()
for kw in keywords:
mask = (
df["question"].str.contains(kw, na=False, regex=False)
#| df["answer"].str.contains(kw, na=False, regex=False)
#| df["name"].str.contains(kw, na=False, regex=False)
)
matched = df[mask]
print(f" 「{kw}」命中 {len(matched)} 条")
# 每个关键词只取前 N 条
for _, row in matched.head(per_keyword).iterrows():
if row["id"] not in seen_ids:
seen_ids.add(row["id"])
all_rows.append(row)
return pd.DataFrame(all_rows)
# ============ 5. 生成回答 ============
ANSWER_SYSTEM = """你是一个严谨的医疗信息助手。请基于提供的「参考资料」回答用户问题。
规则:
1. 只使用参考资料中的信息,不要引入资料之外的知识。
2. 如果资料不足以回答,明确说明「根据现有资料无法回答」,不要猜测。
3. 如果用户的描述和资料中的疾病不一致,可以指出差异,但不要下诊断结论。
4. 不要给具体用药剂量,涉及治疗提醒用户咨询医生。
5. 最后注明:以上信息仅供参考,不能替代专业医疗诊断。"""
def build_context(rows, limit=MAX_CONTEXT_ROWS):
rows = rows.head(limit)
blocks = []
for _, row in rows.iterrows():
ans = str(row["answer"])[:MAX_ANSWER_LEN]
blocks.append(
f"【病名】{row.get('name', '')} 【科室】{row.get('department', '')}\n"
f"问:{row['question']}\n"
f"答:{ans}"
)
return "\n\n".join(blocks)
def answer_question(question, rows):
context = build_context(rows)
user_prompt = f"""参考资料:
{context}
用户问题:{question}"""
return call_ollama(ANSWER_SYSTEM, user_prompt)
# ============ 6. 主流程 ============
if __name__ == "__main__":
question = input("请输入你的问题:")
print("\n[1] 抽取关键词...")
keywords = extract_keywords(question)
print("关键词:", "、".join(keywords))
print("\n[2] 检索...")
rows = search_by_keywords(keywords)
print(f"命中 {len(rows)} 条")
if not rows.empty:
print(rows[["id", "name", "question"]].head(30).to_string(index=False))
if rows.empty:
print("\n没检索到相关资料。")
else:
print(f"\n[3] 从 {len(rows)} 条中取前 {MAX_CONTEXT_ROWS} 条喂给模型...\n")
print("=" * 50)
print(answer_question(question, rows))
print("=" * 50)
但是经常会关键字会搜索到不相关的东西,主要是关键字无法代表语义。
那么我需要向量方式识别用户输入的含义。
3. 下载向量计算模型
pip install modelscope
国内网络连不上 Hugging Face,最直接的解决办法是走国内镜像站下载。
下载脚本(推荐用 ModelScope)
ModelScope 是阿里达摩院的国内模型社区,CDN 速度很稳定,而且 BAAI/bge-small-zh-v1.5 在 ModelScope 上也有官方同步的版本。
# download_model.py
import os
os.environ["HF_ENDPOINT"] = "https://hf-mirror.com" # 关键:必须在 import 之前设置
from modelscope import snapshot_download
model_dir = snapshot_download(
"BAAI/bge-small-zh-v1.5",
cache_dir="./models", # 下载到这里
)
print("模型已下载到:", model_dir)
先安装依赖:
pip install modelscope
然后运行 python download_model.py。下载完成后,模型文件会出现在 ./models/BAAI/bge-small-zh-v1.5 目录下。
备选:用 Hugging Face 镜像站下载
如果更习惯 Hugging Face 生态,也可以用 hf-mirror.com。
关键是环境变量必须在 sentence_transformers 被导入之前设置,否则库在导入阶段就已经初始化了官方地址,后面再改就晚了。
# download_model_hf.py
import os
os.environ["HF_ENDPOINT"] = "https://hf-mirror.com" # 必须在所有 HF 相关 import 之前
from huggingface_hub import snapshot_download
model_dir = snapshot_download(
repo_id="BAAI/bge-small-zh-v1.5",
local_dir="./models/bge-small-zh-v1.5",
local_dir_use_symlinks=False,
)
print("模型已下载到:", model_dir)
4. 下载Qdrant
Qdrant 官方为 Windows 提供了预编译的压缩包,在 GitHub Releases 页面下载即可。
-
官方发布页 :
https://github.com/qdrant/qdrant/releases -
具体文件 :在该页面的 Assets 区域,找到并下载
qdrant-x86_64-pc-windows-msvc.zip。这就是 Windows 64 位版本的免安装包。
🛠️ 使用方法(解压即用)
下载后不需要安装,解压就能跑。
1. 解压与启动 将下载的 zip 压缩包解压到一个你喜欢的目录(比如 D:\ProgramData\Qdrant)。然后在这个文件夹里打开命令行(PowerShell 或 CMD),运行:
./qdrant.exe
程序启动后,会默认在当前目录下创建一个 storage 文件夹来存放你的向量数据。
2. 验证与访问 启动成功后,打开浏览器访问 http://localhost:6333/dashboard,就能看到 Qdrant 的管理界面了。它的 HTTP API 默认监听 6333 端口,gRPC API 监听 6334 端口。
5. 导入数据
qdrant是可以本地使用的,这里使用服务方式,更接近真实的用法:
import json
import os
import pandas as pd
from sentence_transformers import SentenceTransformer
from qdrant_client import QdrantClient
from qdrant_client.models import Distance, VectorParams, PointStruct
# ============ 配置 ============
DATA_DIR = "./MedicalQA"
COLLECTION = "medical_qa"
VECTOR_DIM = 512
BATCH_SIZE = 64 # CPU 上 batch 太大反而慢,64 比较合适
# EMBED_MODEL = "BAAI/bge-small-zh-v1.5"
EMBED_MODEL = r"E:\2026work\models\BAAI--bge-small-zh-v1.5\snapshots\master"
FILES = {
"DX.json": True, # 53554 条
"TCM.json": True, # 38294 条
# "MB.json": True, # 14543 条
# "HT.json": True, # 1358093 条 # 1.9GB,内存够再开
}
# ============ 1. 加载数据 ============
frames = []
for fname, enabled in FILES.items():
if not enabled:
continue
path = os.path.join(DATA_DIR, fname)
if not os.path.exists(path):
print(f"跳过(不存在): {fname}")
continue
with open(path, "r", encoding="utf-8") as f:
data = json.load(f)
df_part = pd.DataFrame(data)
df_part["source_file"] = fname
frames.append(df_part)
print(f"已加载 {fname}: {len(df_part)} 条")
df = pd.concat(frames, ignore_index=True)
print(f"\n合计 {len(df)} 条")
print("列名:", df.columns.tolist())
# ============ 2. 加载 embedding 模型(CPU) ============
# model = SentenceTransformer(EMBED_MODEL, device="cpu")
model = SentenceTransformer(EMBED_MODEL, device="cpu")
print(f"embedding 模型已加载: {EMBED_MODEL}, 维度 {VECTOR_DIM}, 设备: CPU")
# ============ 3. 初始化 Qdrant(连接本机服务) ============
client = QdrantClient(host="localhost", port=6333)
try:
client.get_collections()
print("已连接本机 Qdrant")
except Exception as e:
print(f"无法连接 Qdrant,请确认 qdrant.exe 已启动: {e}")
raise
existing = [c.name for c in client.get_collections().collections]
if COLLECTION in existing:
print(f"集合 {COLLECTION} 已存在,删除重建...")
client.delete_collection(COLLECTION)
client.create_collection(
collection_name=COLLECTION,
vectors_config=VectorParams(size=VECTOR_DIM, distance=Distance.COSINE),
)
print(f"集合 {COLLECTION} 已创建")
# ============ 4. 拼接用于 embed 的文本 ============
def build_text(row):
return f"{row['question']}"
texts = df.apply(build_text, axis=1).tolist()
# ============ 5. 分批 embed + 写入 ============
total = len(texts)
point_id = 0
for start in range(0, total, BATCH_SIZE):
end = min(start + BATCH_SIZE, total)
batch_texts = texts[start:end]
batch_rows = df.iloc[start:end]
embeddings = model.encode(
batch_texts,
normalize_embeddings=True,
show_progress_bar=False,
batch_size=BATCH_SIZE,
)
points = []
for i, (_, row) in enumerate(batch_rows.iterrows()):
points.append(
PointStruct(
id=point_id,
vector=embeddings[i].tolist(),
payload={
"qa_id": str(row.get("id", "")),
"name": str(row.get("name", "")),
"department": str(row.get("department", "")),
"question": str(row["question"]),
"answer": str(row["answer"]),
"source_file": str(row.get("source_file", "")),
},
)
)
point_id += 1
client.upsert(collection_name=COLLECTION, points=points)
print(f"已写入 {end}/{total}")
print("\n全部完成")
print(f"集合 {COLLECTION} 共 {client.count(COLLECTION).count} 条")
用CPU计算比较慢
全部完成
集合 medical_qa 共 91848 条
(qlib312) PS E:\2026work> & C:\ProgramData\miniconda3\envs\qlib312\python.exe e:/2026work/query_qdrant.py
Loading weights: 100%|██████████████████████████████████████████████████████████████████████████████████████████████████████| 71/71 [00:00<00:00, 2354.45it/s]
集合 medical_qa 共 91848 条
6. 测试问答
写一个问答测试,这里不使用ollama了
# query_qdrant.py
from sentence_transformers import SentenceTransformer
from qdrant_client import QdrantClient
# ============ 配置 ============
EMBED_MODEL = r"E:\2026work\models\BAAI--bge-small-zh-v1.5\snapshots\master"
COLLECTION = "medical_qa"
TOP_K = 10
# ============ 加载模型和客户端 ============
model = SentenceTransformer(EMBED_MODEL, device="cpu")
client = QdrantClient(host="localhost", port=6333)
print(f"集合 {COLLECTION} 共 {client.count(COLLECTION).count} 条")
print("=" * 60)
# ============ 循环查询 ============
while True:
question = input("\n请输入问题(输入 q 退出):").strip()
if question.lower() == "q":
break
if not question:
continue
# 生成查询向量
q_vec = model.encode(
[question],
normalize_embeddings=True,
)[0].tolist()
# 检索
hits = client.query_points(
collection_name=COLLECTION,
query=q_vec,
limit=TOP_K,
).points
print(f"\n检索到 {len(hits)} 条:\n")
for i, h in enumerate(hits, 1):
p = h.payload
print(f"[{i}] 相似度 {h.score:.4f} | 病名:{p.get('name', '')} | 科室:{p.get('department', '')}")
print(f" 问:{p.get('question', '')}")
print(f" 答:{p.get('answer', '')[:150]}...")
print()
# ============ 可选:把检索结果拼成上下文喂给 Ollama ============
def build_context(hits, max_rows=5, max_len=1500):
blocks = []
for h in hits[:max_rows]:
p = h.payload
ans = str(p.get("answer", ""))[:max_len]
blocks.append(
f"【病名】{p.get('name', '')} 【科室】{p.get('department', '')}\n"
f"问:{p.get('question', '')}\n"
f"答:{ans}"
)
return "\n\n".join(blocks)
测试一下:
请输入问题(输入 q 退出):我被猫抓了,长了一个疙瘩,医生说不是HPV,是瘢痕
检索到 10 条:
[1] 相似度 0.6848 | 病名:瘢痕疙瘩 | 科室:皮肤科
问:瘢痕疙瘩怎么诊断?需要做检查吗?
答:根据疾病典型皮疹的发展特点及临床表现即可诊断,若有明确创伤及其他炎症病史对诊断更有帮助,一般不需做特殊检查。对难以确诊的病例、怀疑有恶变可能性的病例可 以做皮肤组织病理检查。在早期,肥大性瘢痕与瘢痕疙瘩无法区别,2~3 个月后疙瘩的进一步表现即可说明其今后的病程,若是瘢痕疙瘩,则容易受激惹且过度敏感,...
[2] 相似度 0.6848 | 病名:HPV感染 | 科室:妇科
问:HPV 感染时,能彻底根除 HPV 吗?
答:目前没有药物可以清除 HPV。但只要感染者没有免疫系统缺陷,在感染 HPV 的 2~3 年内即可自行清除 HPV。...
[3] 相似度 0.6826 | 病名:猫癣 | 科室:皮肤科
问:人得了猫癣后会留疤痕吗?
答:一般来说不会留下疤痕,具体还是要根据病情分析。绝大多数能在早期积极治疗的人群不会留下疤痕,因为导致猫癣的致病真菌通常都是寄生在皮肤的角质层,不会侵犯到 真皮层,因此通过早期干预,留疤的可能性较小。但若是长期不进行治疗,在瘙痒时抓破了皮肤可能会继发细菌感染,侵犯到真皮层后就有可能留疤了。...
[4] 相似度 0.6812 | 病名:瘢痕 | 科室:美容整形科
问:瘢痕和瘢痕疙瘩是同一种病吗?
答:不完全一样。瘢痕有生理性瘢痕和病理性瘢痕之分,瘢痕疙瘩是继发于皮肤外伤或自发形成和过度生长的病理性瘢痕。可见瘢痕和瘢痕疙瘩不完全是一回事,瘢痕是一个更 宽泛的称谓。...
[5] 相似度 0.6811 | 病名:瘢痕疙瘩 | 科室:皮肤科
问:瘢痕疙瘩就说明是疤痕体质吗?
答:是的。只有「瘢痕体质」的人,在受到外伤或某些刺激下,在特定部位,如胸、背、脖子等处,皮肤过度自我修复、愈合形成瘢痕疙瘩。...
[6] 相似度 0.6766 | 病名:HPV感染 | 科室:妇科
问:HPV 是什么?
答:HPV 又称「人乳头瘤病毒」,是一种双链 DNA 病毒,可致人类皮肤和黏膜异常增生。感染该病毒后可出现寻常疣、尖锐湿疣等良性皮肤病损,也可能引起恶性肿瘤,如宫颈癌、外阴癌、肛门癌等,为涉及到多学科的疾病。但也有部分人感染了该病毒,没有出现任何症状和不适的感觉。目前发现,HPV 有 200 多种类型(...
[7] 相似度 0.6735 | 病名:猫癣 | 科室:皮肤科
问:怀疑得了猫癣,需要做检查确诊吗?
答:需要。无论是人还是猫,由于猫癣的症状和其他较多的皮肤病相类似,所以还需要依靠相应的检查手段才能确诊。...
[8] 相似度 0.6703 | 病名:HPV感染 | 科室:妇科
问:HPV 感染是怎么发生的?
答:HPV感染部位为人的皮肤和黏膜,病毒通过皮肤黏膜的接触感染人体,其中性接触传播为 HPV 的主要传播方式。HPV 不入血,所以血液传播不是 HPV 的传播途径。...
[9] 相似度 0.6675 | 病名:HPV感染 | 科室:妇科
问:HPV 感染是不是另一半做了对不起自己的事?
答:不一定。HPV 感染后,不同的亚型会导致不同的疾病,如尖锐湿疣、扁平疣等。我们通常更关注的是以尖锐湿疣为主的性传播疾病,性生活复杂的人(比如同时有多个性伴 侣)感染 HPV 的概率会大大增加。但仅凭检查出 HPV 阳性不足以证明对方隐瞒了复杂的性生活史,因为除性生活外,直接与感染者皮肤进行密切接触、或...
[10] 相似度 0.6663 | 病名:HPV感染 | 科室:妇科
问:HPV感染如何诊断?
答:医生根据患者的临床症状,询问患者是否存在高危性生活史、有无与 HPV 感染者的直接、间接接触途径,结合相关检查(细胞学检查、阴道镜检查、组织病理学检查)结果以诊断是否感染 HPV。...
数据没有做清洗,就直接用了。
这里看到搜素的准确性还可以;
直接喂给AI就可以得到结果了。
7. 总结
典型的 RAG(Retrieval-Augmented Generation,检索增强生成)。
RAG 的标准骨架
用户问题
→ embedding 模型编码成向量
→ 向量数据库检索 top-k 相关文档
→ 把检索结果拼成上下文,连同问题一起喂给大模型
→ 大模型基于上下文生成回答
这四个步骤,就是 RAG 的核心。
今天要做的和"教科书版 RAG"略有不同的地方是:就是用本地 Ollama 代替了 OpenAI API,用本地 Qdrant 代替了云端向量库------但流程本身完全一样。
RAG 各环节的通用叫法
| 我们做的 | 术语 | 作用 |
|---|---|---|
| 把库里的问答转成向量存进 Qdrant | Indexing(索引构建) | 离线阶段,把知识库变成可检索的向量 |
| 用户问题转成向量去查 | Retrieval(检索) | 在线阶段,找到最相关的 top-k 文档 |
| 把检索结果拼进提示词 | Augmentation(增强) | 给大模型提供"外部知识" |
| Ollama 基于上下文生成回答 | Generation(生成) | 让模型基于证据回答,而非仅靠记忆 |
但 RAG 不止这一种形态
目前是最基础、最经典的 Naive RAG。实际生产中的 RAG 会在这个骨架上加东西:
-
Query Rewriting:用户问题太口语化时,先让模型改写成更规范的检索语句
-
Reranking:先召回 50 条,再用一个更精细的模型重排,取前 5 条
-
Hybrid Search:向量检索 + 关键词检索(BM25)结合,兼顾语义和字面,Qdrant单机版本不支持反向索引;
-
Multi-hop Retrieval:复杂的一个问题需要多轮检索才能凑齐答案
-
Agentic RAG:用 LangGraph 这类框架编排"检索→判断→重试→再检索"的循环
这些都是在这套基础流程上叠加的优化层,不是必须的。
关于重排
结论:用 BGE-Reranker 系列,这是中文 RAG 场景最成熟、最常用的选择。考虑到我的 3050 只有 8GB 显存,适合 bge-reranker-base 或 bge-reranker-v2-m3。
可选方案对比
| 模型 | 参数量 | 显存需求 | 特点 |
|---|---|---|---|
| bge-reranker-base | 278M | 约 4GB | 轻量快,中英双语,适合你当前的硬件 |
| bge-reranker-v2-m3 | 约 568M | 约 8GB | BAAI 最新多语言重排模型,精度更好 |
| bge-reranker-large | 560M | 约 8GB | 精度优于 base,但显存占用更高 |
| Qwen3-Reranker-0.6B | 0.6B | 约 1.2GB | 轻量,但社区反馈有显存管理问题 |
| qwen3-rerank | 闭源 API | 无需本地 | 阿里云百炼提供,不适合你本地部署的定位 |
为什么推荐 BGE-Reranker
中文医疗场景的匹配度最高。 BGE-Reranker 系列由北京智源研究院开源,训练数据包含大量中文语料,在中文语义匹配任务上表现稳定 。有学术研究直接测试了 bge-reranker-base 在专业领域检索中的效果,NDCG 从 0.39 提升到 0.44 。
和当前用的 embedding 模型同源。 bge-small-zh-v1.5 和 bge-reranker 来自同一个系列,搭配使用不会有兼容性问题。
社区验证充分。 在 FastGPT、Dify 等主流 RAG 框架中,BGE-Reranker 都是默认推荐的重排模型 。
重排的实际工作方式
重排模型是 Cross-Encoder 结构:它把「用户问题」和「候选文档」拼在一起输入模型,直接计算两者的相关性分数 。这和 embedding 的 Bi-Encoder (分别编码、算余弦相似度)不同,Cross-Encoder 能看到问题与文档之间的词级交互,所以排序更准,但计算量也更大,只适合对少量候选(50 条)做精排,不适合全库扫描。