lc:152

今天来总结学习rag项目,先看config。
在开头配置好数据与索引的落盘,避免绝对路径写法。
主语言模型用ollama配置为Qwen2.5-7B,免去细节,反正RAG项目本质上是优化prompt模板和embedding及相似度计算,对主模型直接ollama部署完调接口也省事。
embedding 选择BAAI/bge-m3,默认支持多语言,不仅可以在同一种语言内部进行语义匹配,还能处理不同语言之间的语义对齐(如中文查询匹配英文文档,在此处就是中文疾病名称查询RAG信息的英文原始信息)。支持 100+ 语言,并具备跨语言语义匹配能力,适合作为RAG/检索系统的基础质检模型。输出维度1024必须与 Embedding 模型的输出向量维度一致否则 FAISS 建索引/检索会报错或结果错乱。batch size根据显存适当调整.

FAISS index 选择IVFFLAT,近似检索,需要训练聚类中心。FAISS_NLIST = 100 聚类中心数(粗分桶)。越大一般越准但训练更慢、索引更大。查询时探测多少个桶。越大召回越高但更慢。METADATA_PATH:通常存 doc_id -> 原文/标题/来源等映射,检索后回填文本用

数据量小一般用flat,大chunk用ivflat。NPROBE 是主要调参旋钮

chunking,文本切分配置 CHUNK_SIZE = 512
CHUNK_OVERLAP = 64 重叠,防止关键信息被切断
SEPARATORS:优先用更“自然”的边界分割(段落/句号/分号/逗号/空格)
医疗文本通常句子长、术语密:适当 overlap 很重要。

检索配置多管齐下,RETRIEVER_TOP_K = 5,RETRIEVER_SCORE_THRESHOLD = 0.5,取二者的min候选
控制“先粗搜、后精选”。
- RERANKER:二阶段重排(Cross-Encoder 类)更准但更慢
- RERANKER_TOP_K=3:重排后只保留 3 条给 LLM
BM25和向量检索加权融合(稀疏 + 稠密融合)

进行多路检索配置 BGE-M3 Multi-Route:Dense + Sparse + ColBERT
- Dense:句向量相似度
- Sparse:稀疏表示(可用 BM25 或 bge-m3 学出来的 sparse)
- ColBERT:token 级别的 late interaction,常在细粒度匹配上更强
三路权重归一化为 1.0

Prompt模板,即给出明确的输入端的系统约束和QA模板,比如
- 必须基于参考资料
- 没资料就明确说不能回答
- 要引用来源 [来源: 文档名称]
- 诊疗方案提醒咨询医生
- 条理化输出
- 当资料互相冲突时说明冲突并给出最保守表述(医疗场景常见)

gradio web地址配置 本地/服务器启动可视化界面。

API 配置(FastAPI) 提供程序化调用入口(比如前端/其他服务调用 RAG)

src/data_loader.py

RAG 的典型流水线:

原始文档 →(加载/清洗)→ Document 列表 →(切分)→ chunk 列表 →(向量化)→ 检索 → 生成

这个模块覆盖了
- MedicalDocumentLoader:把各种来源的内容变成 LangChain 的 Document(page_content, metadata) 标准格式
- ChineseTextSplitter:把长文本切成适合检索的 chunk,并保留必要的上下文(overlap)

load_json_documents
[
{"text": "...", "metadata": {...}},
...
]
每条 item → 一个 Document
metadata 缺省时给 {}(避免 KeyError)

当前规模尚可可以用json格式,更大规模适合jsonl(逐行读)

load_txt_documents(filepath)

读整个文件内容作为一个 Document - metadata 里加了:
- "source": 文件名
- "type": "text"
这很重要:后面检索结果展示或调试时,你能知道 chunk 从哪里来。
同样适用于 .md(在 load_directory 里就是这么复用的)。
.md 直接按纯文本读(不解析 Markdown 结构)

暂时不添加pdf支持,后续需要使用了再加。
load_cmedqa_data(cmedqa_dir=None) 用途:把问答对变成可检索的“知识文档”

中文切分器 ChineseTextSplitter:核心机制拆解
- length_function=len:中文按字符长度算很直观(不是 token 数)
- is_separator_regex=False:分隔符按字面匹配,不当正则用(更安全)

chunks = self.splitter.split_documents(documents) LangChain 会把每个 chunk 仍然包装成 Document,并继承原 metadata(非常关键!)
- chunk_size:便于统计和调参
- 打印 avg chunk length,能快速判断“切太碎/切太粗”

便捷函数 load_and_split_documents:一键跑通流程

无overlap切分:Chunk1:
糖尿病的典型症状包括多饮、多尿、多食

Chunk2:
和体重减轻。早期可能没有明显症状。

加overlap:
Chunk1:
糖尿病的典型症状包括多饮、多尿、多食

Chunk2:
多尿、多食和体重减轻。早期可能没有明显症状。

直观来看就是:
[ A B C ]
[ C D E ]
[ E F G ]

回头来看一下build_index.py,把处理过的数据转换成向量。
把医疗文档:
- 切成 chunk(小段)
- 用嵌入模型编码成向量(dense,必要时还会拿到 sparse/colbert)
load_and_split_documents():读取“已预处理好的医疗文档”,并切 chunk
get_embeddings():根据参数返回不同嵌入模型对象(bge-m3 或 simple)
FAISSVectorStore:你们封装过的向量库(内部会创建/保存/搜索 FAISS)

主函数 build_index
- embedding_type:决定用哪种 embedding(bge-m3 支持三路;simple 多半只支持 dense)
- index_type:FAISS 索引类型(Flat / IVFFlat / HNSW)
- routes:决定构建哪几路索引:
- "dense":只建 FAISS(dense)
- "dense+sparse":FAISS + sparse
- "all":FAISS + sparse + colbert
基础步骤至少有 4 个(加载切分、初始化模型、编码、建 FAISS、保存等)

FAISS 的聚类实质是“空间分区”,让搜索变成局部搜索,而不是全局搜索。

chunk

embeddings = get_embeddings(embedding_type)
is_flag_model = getattr(embeddings, "_is_flag_model", False)
use_multi = (build_sparse or build_colbert) and is_flag_model
- getattr(..., "_is_flag_model", False):用一个“暗号属性”判断当前模型是否支持“三路编码”
- 如果是 BGE-M3 那类 FlagEmbedding 封装,可能提供 encode_multi / encode_query_multi
- use_multi:只有当你要建 sparse/colbert 且模型支持 multi 才走“三路编码”分支 否则就只做 dense 编码。

Dense-only and Multi-route

构建 FAISS Dense 索引(核心检索)

import faiss
vector_store = FAISSVectorStore(embeddings=embeddings, index_type=index_type)

vector_store.dimension = dense_vecs.shape[1]
faiss.normalize_L2(dense_vecs)
vector_store.index = vector_store._create_index(num_vectors=len(chunks))

if index_type == "IVFFlat":
    vector_store.index.train(dense_vecs)

vector_store.index.add(dense_vecs)
vector_store.documents = chunks
vector_store.doc_count = len(chunks)

IVFFlat:需要训练(train),更适合大数据

sparse检索 colbert检索(二者可选)

`SparseVectorStore.build_from_weights
- 建倒排结构 / 稀疏向量矩阵
- 支持 query 的稀疏权重打分

if build_colbert and all_colbert is not None:
    from src.colbert_store import ColBERTStore
    colbert_store = ColBERTStore()
    colbert_store.build_from_vecs(all_colbert)

ColBERT 检索不是全量暴力匹配,而是:
- 先用 sparse 或 dense 召回候选
- 再对候选用 token-level 相似度做 rerank(更准)

保存

构建后验证模块(Sanity Check)
做一次小规模试检索,检查数据
- 能正常加载
- 能正常检索
- 各路(dense / sparse / colbert)都能跑通
- 返回结果看起来“像是对的”