本文带你用 Python 从零构建一套完整的中文 RAG(检索增强生成)系统,涵盖文档分块、向量化、重排序和 LLM 问答全流程。所有模型通过 LM Studio 本地部署,零 API 费用,数据不出本地。
一、为什么要做这个项目?
大语言模型(LLM)虽然强大,但有一个致命缺陷------幻觉。它会一本正经地胡说八道,尤其在回答专业领域知识时。
RAG(Retrieval-Augmented Generation) 正是为了解决这个问题而生的:
用户提问 → 从知识库检索相关文档 → 将检索结果作为上下文喂给 LLM → 生成有据可查的回答
相比微调(Fine-tuning),RAG 有几个显著优势:
| 特性 | RAG | 微调 |
|---|---|---|
| 知识更新 | 实时更新,改文档即可 | 需重新训练 |
| 成本 | 低 | 高 |
| 可解释性 | 高,可追溯来源 | 低 |
| 适用场景 | 知识库问答 | 风格迁移/任务适配 |
本文的目标是:用尽可能少的代码,搭建一个生产级的 RAG 原型系统。
二、技术选型
| 组件 | 选型 | 说明 |
|---|---|---|
| 框架 | LangChain 1.3.x | 最新稳定版,LCEL 链式调用 |
| Embedding | BGE-M3(LM Studio 部署) | 1024 维,支持 100+ 语言 |
| Reranker | BGE-Reranker-v2-m3 (LM Studio 部署) | Cross-Encoder 精排 |
| Chat LLM | Qwen3(LM Studio 部署) | 本地对话模型 |
| 向量库 | ChromaDB | 轻量级,本地持久化 |
| 语言 | Python 3.13+ |
核心设计思路 :所有模型都通过 LM Studio 本地运行,对外暴露 OpenAI 兼容的 API,代码层面用 langchain-openai 的 SDK 即可无缝对接。
三、环境准备
3.1 安装 LM Studio 并下载模型
- 前往 LM Studio 官网 下载安装
- 搜索并下载以下模型:
- Embedding :
BAAI/bge-m3(多语言嵌入首选) - Reranker :
BAAI/bge-reranker-v2-m3(精排模型) - Chat :
Qwen/Qwen3-0.6B(或其他你喜欢的对话模型)
- Embedding :
- 启动本地服务器,确保 "Enable Server" 已勾选
- 默认 API 地址为
http://localhost:1234/v1
3.2 安装 Python 依赖
项目使用 uv 管理依赖:
toml
[project]
dependencies = [
"langchain>=1.3.1",
"langchain-community>=0.4.2",
"langchain-openai>=1.2.2",
"langchain-text-splitters>=1.1.2",
"chromadb>=1.0.0",
"langchain-core>=1.4.0",
]
安装并运行:
bash
uv sync
python rag_demo.py
四、核心代码解析
整个系统分为 6 大模块,下面逐一拆解。
4.1 Embedding 封装:对接 LM Studio
LM Studio 提供 OpenAI 兼容 API,因此可以复用 langchain-openai 的 OpenAIEmbeddings,但有两个关键配置需要注意:
python
class LMStudioEmbeddings(Embeddings):
def __init__(self, model, base_url, **kwargs):
self._client = OpenAIEmbeddings(
model=model,
api_key="lm-studio", # LM Studio 不需要真实 Key
base_url=base_url,
tiktoken_enabled=False, # ⚠️ 关键配置 1
check_embedding_ctx_length=False, # ⚠️ 关键配置 2
**kwargs
)
def embed_documents(self, texts):
return self._client.embed_documents(texts)
def embed_query(self, text):
return self._client.embed_query(text)
为什么要关闭这两个选项? 这是我在开发中踩过的坑:
-
tiktoken_enabled=False:默认情况下,OpenAIEmbeddings会用 tiktoken 将文本转为整数 ID 数组(如[[82805]])。但 LM Studio 期望收到的是字符串数组 (如["测试"]),否则会报错 "input field must be a string or an array of strings"。 -
check_embedding_ctx_length=False:该安全检查会走_get_len_safe_embeddings路径,将 token ID 直接传给 API,LM Studio 不支持这种格式。
4.2 文本分块:中文友好的切分策略
分块(Chunking)是 RAG 系统中至关重要的一环,直接影响检索精度。
python
class TextChunker:
def __init__(self, chunk_size=500, chunk_overlap=100):
self.separators = [
"\n\n", # 优先按段落分割
"\n", # 其次按换行
"。", "!", "?", # 中文句末标点
". ", "! ", "? ", # 英文句末标点
",", ",", # 逗号级别
" ", "" # 最后强制切分
]
分块策略的设计思路:
- 语义完整性:优先按段落和句子切分,避免把一句话从中间截断
- 上下文连贯:设置 100 字的重叠(overlap),保证相邻块之间的信息连续性
- 适度大小:500 字/块在检索精度和上下文丰富度之间取得平衡
对于 Markdown 格式的文档,还支持按标题层级分块:
python
def split_markdown(self, text, metadata=None):
headers_to_split_on = [
("#", "level_1"),
("##", "level_2"),
("###", "level_3"),
]
splitter = MarkdownHeaderTextSplitter(headers_to_split_on=headers_to_split_on)
return splitter.split_text(text)
这样每个 chunk 会自带标题层级的元数据,方便后续追溯信息来源。
4.3 向量库:ChromaDB 持久化存储
python
class VectorStoreManager:
def create_or_load(self, chunks, embeddings):
if Path(self.persist_path).exists():
# 加载已有向量库,避免重复构建
return Chroma(
persist_directory=self.persist_path,
embedding_function=embeddings
)
# 首次运行:从文档构建向量库
return Chroma.from_documents(
documents=chunks,
embedding=embeddings,
persist_directory=self.persist_path
)
首次运行会将所有文档块向量化并持久化到磁盘,后续运行直接加载,无需重复计算。
4.4 Reranker:Cross-Encoder 精排
向量检索速度快但精度有限,Cross-Encoder 精度高但速度慢。两者结合是 RAG 系统的经典范式:先用向量检索快速召回候选集,再用 Cross-Encoder 精细排序。
python
class LMStudioReranker:
def rerank(self, query, docs, top_n=None):
# 构造 "query: ... doc: ..." 格式(BGE-Reranker 期望的输入)
passages = [
f"query: {query} doc: {doc.page_content[:512]}"
for doc in docs
]
# 通过 /v1/embeddings 端点调用 reranker 模型
response = requests.post(
f"{self.base_url}/embeddings",
json={"model": self.model, "input": passages},
headers={"Authorization": f"Bearer lm-studio"},
timeout=60
)
# 解析结果:reranker 返回的 embedding 第一个值即为相关性得分
results = []
for i, item in enumerate(data.get("data", [])):
score = item["embedding"][0]
results.append((docs[i], float(score)))
# 按得分降序排列
results.sort(key=lambda x: x[1], reverse=True)
return results[:top_n]
这里有一个巧妙的设计:在 LM Studio 的配置中,reranker 模型也通过 /v1/embeddings 端点调用,只是 model 参数不同。返回的 embedding 向量的第一个元素就是 query-document 的相关性得分。
4.5 检索器:串联全流程
python
class RAGRetriever:
def retrieve(self, query, k=3):
# Step 1: 向量检索(粗筛)------ 召回 Top-10
docs = self.vectorstore.similarity_search(query, k=SEARCH_K)
# Step 2: Reranker 精排 ------ 选出 Top-3
if self.use_reranker and self.reranker:
scored_docs = self.reranker.rerank(query, docs, top_n=k)
docs = [doc for doc, _ in scored_docs]
return docs
4.6 LCEL 链式调用:优雅的 RAG Pipeline
LangChain 1.4.x 推荐使用 LCEL(LangChain Expression Language)来编排 RAG 流程:
python
def create_rag_chain(retriever, llm):
prompt = ChatPromptTemplate.from_template(
"你是一个专业的中文问答助手。请根据以下检索到的上下文回答问题。\n"
"如果上下文中没有相关信息,请如实告知,不要编造答案。\n\n"
"【检索到的上下文】:\n{context}\n\n"
"【问题】:\n{question}\n\n"
"【回答】:"
)
chain = (
{"context": retriever, "question": RunnablePassthrough()}
| prompt
| llm
| StrOutputParser()
)
return chain
LCEL 的管道语法 | 让整个流程一目了然:检索 → 填充 Prompt → LLM 推理 → 解析输出。
此外,项目还支持带对话历史的 RAG 链,适合多轮问答场景:
python
def create_conversational_rag_chain(retriever, llm):
# 在 Prompt 中加入 chat_history 占位符
prompt = ChatPromptTemplate.from_template(
"...【对话历史】:\n{chat_history}\n\n【检索到的上下文】:\n{context}..."
)
五、运行效果
5.1 基础检索测试
程序内置了 5 个测试查询,覆盖文档的不同章节:
csharp
❓ 查询: RAG 系统有哪些优势?
[1] 重排序得分: 0.8921 块 ID: 2/15
内容: RAG 的优势在于:1. 减少幻觉:模型基于真实检索内容生成...
[2] 重排序得分: 0.8534 块 ID: 5/15
内容: RAG 系统主要由三个模块组成:索引模块、检索模块、生成模块...
可以看到,Reranker 成功地将最相关的文档块排在了前面。
5.2 LLM 问答
erlang
❓ 问题: RAG 系统的主要优势是什么?
💡 回答: RAG 系统的主要优势包括三个方面:第一,减少幻觉,模型基于真实
检索内容生成回答,降低编造信息的概率;第二,知识更新方便,无需重新
训练模型,只需更新向量数据库即可;第三,可解释性高,可以追溯回答的
信息来源...
5.3 交互式问答
程序最后会进入交互模式,支持多轮对话:
erlang
🔍 请输入问题: 中文分块有什么注意事项?
💡 回答: 中文没有天然的空格分隔,建议优先按段落(\n\n)分割,其次按
句子结束符(。!?)分割,最后按字符数强制切分...
🔍 请输入问题: 那英文分块呢?
💡 回答: 英文有天然的空格分隔,通常按空格和标点符号切分即可...
六、架构设计亮点
6.1 模块化设计
每个组件都封装为独立的类,可以单独替换和测试:
LMStudioEmbeddings→ 可换为其他 Embedding 服务TextChunker→ 可调整分块策略LMStudioReranker→ 可关闭或替换为其他 RerankerVectorStoreManager→ 可换为 FAISS、Milvus 等
6.2 智能模型发现
Chat 模型支持自动发现机制:
python
def resolve_chat_model():
"""优先使用环境变量指定,否则从 LM Studio 自动选取"""
if CHAT_MODEL_NAME:
return CHAT_MODEL_NAME
available = get_available_chat_models()
if available:
return available[0]
return "local-model"
程序会自动过滤掉 embedding 和 reranker 模型,从剩余模型中选取对话模型。
6.3 优雅降级
当 Reranker 不可用时,自动降级为按向量相似度排序,不会因为单点故障导致整个系统崩溃。
七、踩坑记录
在开发过程中,我遇到了几个典型问题:
坑 1:OpenAIEmbeddings 的 tiktoken 兼容性
现象 :调用 LM Studio 的 /v1/embeddings 接口时报错 "input field must be a string or an array of strings"。
原因 :OpenAIEmbeddings 默认使用 tiktoken 将文本转为 token ID 整数数组,而 LM Studio 只接受原始字符串。
解决 :设置 tiktoken_enabled=False 和 check_embedding_ctx_length=False。
坑 2:LM Studio 需要显式加载 Chat 模型
现象:Embedding 接口正常,但 Chat 请求失败。
原因:LM Studio 中 Embedding 模型和 Chat 模型需要分别加载。加载了 Embedding 模型不代表 Chat 模型也已加载。
解决 :在 LM Studio 界面中显式加载 Chat 模型,或使用 resolve_chat_model() 自动检测。
坑 3:Reranker 的输入格式
现象:Reranker 得分不合理。
原因 :BGE-Reranker 期望输入格式为 "query: xxx doc: yyy",需要显式拼接。
解决 :构造 passages 时遵循 query: ... doc: ... 格式,并截断文档长度([:512])避免超出模型上下文窗口。
八、可优化方向
当前版本是一个功能完整的 RAG 原型,如果要用于生产环境,还可以考虑:
- 混合检索:结合 BM25 稀疏检索 + 稠密向量检索,提升召回率
- 查询改写:用 LLM 对用户查询进行扩展或改写,提高检索命中率
- 流式输出 :利用 LCEL 的
.stream()方法实现打字机效果 - 对话记忆 :引入
ConversationBufferMemory管理多轮对话上下文 - HyDE 策略:先让 LLM 生成假设性答案,再用答案去检索(Hypothetical Document Embeddings)
- 评估体系:使用 RAGAS 框架评估 Faithfulness、Answer Relevancy 等指标
九、总结
本文搭建的 RAG 系统覆盖了从文档处理到问答生成的完整链路:
css
文档输入 → 中文分块 → BGE-M3 向量化 → ChromaDB 存储
│
用户查询 → 向量检索 Top-10 → Reranker 精排 Top-3 → LLM 生成回答
核心收获:
- 理解了 RAG 系统的完整工作流程
- 掌握了 LM Studio 本地部署 Embedding/Reranker/Chat 模型的方法
- 学会了 LangChain LCEL 链式调用的优雅写法
- 积累了
OpenAIEmbeddings与 LM Studio 兼容性的实战踩坑经验
所有代码已开源在 RagGuide 仓库 中,如果你也在做 RAG 相关的项目,欢迎交流讨论!