从零搭建本地 RAG 系统:LangChain + LM Studio 完整实战指南

本文带你用 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 并下载模型

  1. 前往 LM Studio 官网 下载安装
  2. 搜索并下载以下模型:
    • EmbeddingBAAI/bge-m3(多语言嵌入首选)
    • RerankerBAAI/bge-reranker-v2-m3(精排模型)
    • ChatQwen/Qwen3-0.6B(或其他你喜欢的对话模型)
  3. 启动本地服务器,确保 "Enable Server" 已勾选
  4. 默认 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-openaiOpenAIEmbeddings,但有两个关键配置需要注意:

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 → 可关闭或替换为其他 Reranker
  • VectorStoreManager → 可换为 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=Falsecheck_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 原型,如果要用于生产环境,还可以考虑:

  1. 混合检索:结合 BM25 稀疏检索 + 稠密向量检索,提升召回率
  2. 查询改写:用 LLM 对用户查询进行扩展或改写,提高检索命中率
  3. 流式输出 :利用 LCEL 的 .stream() 方法实现打字机效果
  4. 对话记忆 :引入 ConversationBufferMemory 管理多轮对话上下文
  5. HyDE 策略:先让 LLM 生成假设性答案,再用答案去检索(Hypothetical Document Embeddings)
  6. 评估体系:使用 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 相关的项目,欢迎交流讨论!

相关推荐
weixin_436182421 小时前
一站式 ECAD 模型 AI 查询 专业设计辅助工具
人工智能
ting94520001 小时前
Fere AI 技术深度解析:面向加密货币与预测市场的自主交易智能体架构
人工智能·架构
生成论实验室1 小时前
通用人工智能完整技术方案:一个基于字序生命模型(WOLM)认知决策层实时、安全、可交互的数字生命体
人工智能·机器人·自动驾驶·agi·安全架构
罗超驿1 小时前
20.MySQL事务隔离级别示例详解(脏读、不可重复读、幻读)
java·数据库·mysql·面试
钓了猫的鱼儿1 小时前
基于深度学习+AI的玩手机行为目标检测与预警系统(Python源码+数据集+UI可视化界面+YOLOv11训练结果)
人工智能·深度学习·智能手机
搞科研的小刘选手1 小时前
【人工智能专题研讨会】第五届人工智能与智能信息处理国际学术会议(AIIIP 2026)
人工智能·神经网络·机器学习·网络安全·数据挖掘·人机交互·信息处理
凌冰_1 小时前
Claude Code + 智谱 BigModel 实战
人工智能
传说故事1 小时前
【论文阅读】RLDX-1
论文阅读·人工智能·具身智能·vla
vx153027823621 小时前
CDGA|企业数据治理中,AI权限该如何拿捏分寸
大数据·人工智能·cdga·数据治理