1. 引言
检索增强生成(Retrieval-Augmented Generation,RAG)是当前大语言模型应用中最实用的范式之一。它通过先检索外部知识库中的相关文档片段,再将这些片段作为上下文注入 Prompt,引导大模型基于给定资料生成回答,从而有效缓解模型幻觉、知识过时等问题。
本文将以一个完整的本地化 RAG 系统为例,基于 LangChain 框架,从文档加载、文本切分、向量化存储到检索问答,逐步拆解整个构建流程。项目全部使用本地模型,无需调用云端 API,完全离线运行,适合在数据敏感、网络受限或追求低成本部署的场景下使用。
2. 系统架构与核心组件
一个典型的 RAG 系统由以下核心组件构成:
- 文档加载器(Loader):读取本地文档(如 Word、PDF、TXT),将其转换为统一的 Document 对象。
- 文本切分器(Splitter):将长文档按语义或固定长度切分为适合检索的片段。
- 嵌入模型(Embedding Model):将文本片段转换为稠密向量,用于语义相似度计算。
- 向量数据库(Vector Store):存储文档向量,并提供高效的相似度检索能力。
- 大语言模型(LLM):根据检索到的上下文和用户问题,生成最终回答。
- 提示词模板(Prompt Template):将检索结果与用户问题组装成结构化的模型输入。
整体流程如下图所示:
#mermaid-svg-hFPS7KZxbNyMEFR5{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-hFPS7KZxbNyMEFR5 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-hFPS7KZxbNyMEFR5 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-hFPS7KZxbNyMEFR5 .error-icon{fill:#552222;}#mermaid-svg-hFPS7KZxbNyMEFR5 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-hFPS7KZxbNyMEFR5 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-hFPS7KZxbNyMEFR5 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-hFPS7KZxbNyMEFR5 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-hFPS7KZxbNyMEFR5 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-hFPS7KZxbNyMEFR5 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-hFPS7KZxbNyMEFR5 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-hFPS7KZxbNyMEFR5 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-hFPS7KZxbNyMEFR5 .marker.cross{stroke:#333333;}#mermaid-svg-hFPS7KZxbNyMEFR5 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-hFPS7KZxbNyMEFR5 p{margin:0;}#mermaid-svg-hFPS7KZxbNyMEFR5 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-hFPS7KZxbNyMEFR5 .cluster-label text{fill:#333;}#mermaid-svg-hFPS7KZxbNyMEFR5 .cluster-label span{color:#333;}#mermaid-svg-hFPS7KZxbNyMEFR5 .cluster-label span p{background-color:transparent;}#mermaid-svg-hFPS7KZxbNyMEFR5 .label text,#mermaid-svg-hFPS7KZxbNyMEFR5 span{fill:#333;color:#333;}#mermaid-svg-hFPS7KZxbNyMEFR5 .node rect,#mermaid-svg-hFPS7KZxbNyMEFR5 .node circle,#mermaid-svg-hFPS7KZxbNyMEFR5 .node ellipse,#mermaid-svg-hFPS7KZxbNyMEFR5 .node polygon,#mermaid-svg-hFPS7KZxbNyMEFR5 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-hFPS7KZxbNyMEFR5 .rough-node .label text,#mermaid-svg-hFPS7KZxbNyMEFR5 .node .label text,#mermaid-svg-hFPS7KZxbNyMEFR5 .image-shape .label,#mermaid-svg-hFPS7KZxbNyMEFR5 .icon-shape .label{text-anchor:middle;}#mermaid-svg-hFPS7KZxbNyMEFR5 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-hFPS7KZxbNyMEFR5 .rough-node .label,#mermaid-svg-hFPS7KZxbNyMEFR5 .node .label,#mermaid-svg-hFPS7KZxbNyMEFR5 .image-shape .label,#mermaid-svg-hFPS7KZxbNyMEFR5 .icon-shape .label{text-align:center;}#mermaid-svg-hFPS7KZxbNyMEFR5 .node.clickable{cursor:pointer;}#mermaid-svg-hFPS7KZxbNyMEFR5 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-hFPS7KZxbNyMEFR5 .arrowheadPath{fill:#333333;}#mermaid-svg-hFPS7KZxbNyMEFR5 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-hFPS7KZxbNyMEFR5 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-hFPS7KZxbNyMEFR5 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-hFPS7KZxbNyMEFR5 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-hFPS7KZxbNyMEFR5 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-hFPS7KZxbNyMEFR5 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-hFPS7KZxbNyMEFR5 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-hFPS7KZxbNyMEFR5 .cluster text{fill:#333;}#mermaid-svg-hFPS7KZxbNyMEFR5 .cluster span{color:#333;}#mermaid-svg-hFPS7KZxbNyMEFR5 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-hFPS7KZxbNyMEFR5 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-hFPS7KZxbNyMEFR5 rect.text{fill:none;stroke-width:0;}#mermaid-svg-hFPS7KZxbNyMEFR5 .icon-shape,#mermaid-svg-hFPS7KZxbNyMEFR5 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-hFPS7KZxbNyMEFR5 .icon-shape p,#mermaid-svg-hFPS7KZxbNyMEFR5 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-hFPS7KZxbNyMEFR5 .icon-shape .label rect,#mermaid-svg-hFPS7KZxbNyMEFR5 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-hFPS7KZxbNyMEFR5 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-hFPS7KZxbNyMEFR5 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-hFPS7KZxbNyMEFR5 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 本地文档加载
文本切分
嵌入向量化
向量数据库存储
用户提问
问题向量化
相似度检索
组装 Prompt
大模型生成回答
3. 环境准备与依赖安装
本项目基于 Python 与 LangChain 生态,建议使用 Python 3.9 及以上版本。所有依赖均为开源库,模型文件全部存放在本地,全程不调用任何云端 API。核心依赖如下:
bash
pip install langchain langchain-community langchain-chroma langchain-text-splitters
pip install chromadb transformers torch python-docx
其中:
langchain:框架核心,提供链式编排能力。langchain-community:包含各类文档加载器与第三方集成。langchain-chroma:Chroma 向量数据库的 LangChain 封装。transformers与torch:用于加载本地大模型与嵌入模型。python-docx:用于解析 Word 文档。
4. 文档加载与文本切分
4.1 加载本地 Word 文档
使用 Docx2txtLoader 可以方便地读取 .docx 格式的文档。通过 os.path 拼接文件路径,可以确保脚本在不同目录下运行时都能正确定位文件。
python
import os
from langchain_community.document_loaders import Docx2txtLoader
current_folder = os.path.dirname(os.path.abspath(__file__))
docx_path = os.path.join(current_folder, "agent_doc.docx")
loader = Docx2txtLoader(docx_path)
documents = loader.load()
print(f"文档包含 {len(documents)} 个片段")
4.2 文本切分策略
原始文档往往较长,直接向量化会导致检索精度下降。这里使用 RecursiveCharacterTextSplitter,它按优先级依次尝试分隔符(\n\n、\n、空格),尽可能在语义完整的位置切分。
python
from langchain_text_splitters import RecursiveCharacterTextSplitter
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=800,
chunk_overlap=10,
separators=["\n\n", "\n", " ", ""]
)
documents = text_splitter.split_documents(documents)
print(f"文档切分为 {len(documents)} 个片段")
chunk_size 控制每个片段的字符数,chunk_overlap 让相邻片段保留少量重叠,避免关键信息被截断在边界处。
5. 嵌入模型与向量数据库
5.1 加载本地嵌入模型
为了完全离线运行,这里使用 HuggingFace 的 MiniLM-L6-v2 模型。该模型体积小、速度快,适合作为中文与英文文本的嵌入模型。
python
from langchain_community.embeddings import HuggingFaceEmbeddings
local_model_path = r"F:\agent_project\MiniLM-L6-v2"
miniLMEmbeddings = HuggingFaceEmbeddings(
model_name=local_model_path,
model_kwargs={"device": "cuda"},
encode_kwargs={"normalize_embeddings": True}
)
normalize_embeddings=True 会对向量做 L2 归一化,使余弦相似度计算更加稳定。若显存不足,可将 device 改为 "cpu"。
5.2 构建向量数据库
使用 Chroma 作为向量存储,将切分后的文档片段批量写入数据库,并持久化到本地目录。
python
from langchain_chroma import Chroma
db = Chroma.from_documents(
collection_name="demo",
documents=documents,
embedding=miniLMEmbeddings,
persist_directory="chroma_db"
)
print(f"向量数据库构建完成,包含 {db._collection.count()} 个向量")
首次运行会执行全量向量化,之后可复用 persist_directory 中的持久化数据,避免重复计算。
6. 本地大模型加载
6.1 加载模型与分词器
本项目使用 LiteLlama-460M-1T 作为生成模型,这是一个轻量级 Llama 架构模型,适合在消费级显卡上运行。加载时需指定 local_files_only=True,确保完全离线、不访问云端。
python
import torch
from transformers import AutoTokenizer, AutoModelForCausalLM
local_model_path = "F:/agent_project/LiteLlama-460M-1T"
tokenizer = AutoTokenizer.from_pretrained(
local_model_path,
local_files_only=True,
trust_remote_code=False
)
model = AutoModelForCausalLM.from_pretrained(
local_model_path,
dtype=torch.float16,
local_files_only=True,
trust_remote_code=False
).cuda()
使用 torch.float16 半精度加载可以显著降低显存占用。若显存不足,可移除 .cuda() 并改用 CPU 推理。
6.2 模型推理验证
在正式接入 RAG 流程前,建议先做一次简单的生成测试,确认模型与分词器工作正常:
python
prompt = "Q: what is your name ?\nA:"
inputs = tokenizer(prompt, return_tensors="pt").to("cuda")
outputs = model.generate(
**inputs, max_new_tokens=100, do_sample=True, temperature=1
)
print(tokenizer.decode(outputs[0], skip_special_tokens=True))
7. RAG 检索与问答流程
7.1 用户问题向量化与检索
当用户提出问题时,首先将问题转换为向量,然后在向量数据库中执行相似度检索,返回最相关的 k 个文档片段。
python
query = "can you tell me what is the name of the author?"
print(f"用户问题:{query}")
query_embedding = miniLMEmbeddings.embed_query(query)
retrieved_docs = db.similarity_search_by_vector(
embedding=query_embedding,
k=2
)
print(f"检索到 {len(retrieved_docs)} 个相关文档片段")
k=2 表示返回最相关的 2 个片段。实际应用中可根据文档长度与问题复杂度调整该参数。
7.2 组装 Prompt
将检索到的文档片段拼接为上下文,与用户问题一起填入提示词模板。这里使用 ChatPromptTemplate 构建结构化的对话消息。
python
from langchain_core.prompts import ChatPromptTemplate
context_text = ""
for i, doc in enumerate(retrieved_docs):
context_text += f"Document excerpts {i+1}:{doc.page_content}\n"
prompt_template = ChatPromptTemplate.from_messages([
("system", "You are an assistant. Respond with A: followed by your answer. After finishing your answer after A:, stop generating completely."),
("user", "Here are some document contents:\n{context}\n\nPlease answer the user's question based on these contents: {query}")
])
formatted_prompt = prompt_template.format_prompt(
context=context_text,
query=query
)
系统提示词中明确要求模型以 A: 开头作答,并在回答结束后停止生成,这有助于后续解析模型输出。
7.3 模型生成与结果解析
将组装好的 Prompt 分词后送入模型生成,最后只解码模型新生成的部分,避免重复输出输入内容。
python
model_inputs = tokenizer(formatted_prompt.to_string(), return_tensors="pt").to("cuda")
output_tokens = model.generate(
**model_inputs,
max_new_tokens=400
)
input_len = model_inputs["input_ids"].shape[1]
result = tokenizer.decode(output_tokens[0][input_len:])
print("模型回答:\n", result.split("Q:")[0])
通过 input_len 截断输入部分,确保只保留模型真正生成的回答内容。
8. 完整代码整合
将上述模块整合为三个文件,便于维护与复用:
embeddings.py:嵌入模型初始化。models.py:大模型与分词器加载。final_release.py:RAG 主流程。
8.1 embeddings.py
python
from langchain_community.embeddings import HuggingFaceEmbeddings
# 填写本地模型绝对路径,加r防止路径转义
local_model_path = r"F:\agent_project\MiniLM-L6-v2"
miniLMEmbeddings = HuggingFaceEmbeddings(
model_name=local_model_path,
# 有显卡用cuda,显存不够改成 device="cpu"
model_kwargs={"device": "cuda"},
encode_kwargs={"normalize_embeddings": True}
)
8.2 models.py
python
import torch
from transformers import AutoTokenizer, AutoModelForCausalLM
local_model_path = "F:/agent_project/LiteLlama-460M-1T"
tokenizer = AutoTokenizer.from_pretrained(
local_model_path,
local_files_only=True,
trust_remote_code=False
)
model = AutoModelForCausalLM.from_pretrained(
local_model_path,
dtype=torch.float16,
local_files_only=True,
trust_remote_code=False
).cuda()
8.3 final_release.py
python
import os
import time
from langchain_chroma import Chroma
from langchain_community.document_loaders import Docx2txtLoader
from langchain_core.prompts import ChatPromptTemplate
from langchain_text_splitters import RecursiveCharacterTextSplitter
from models import model, tokenizer
from embeddings import miniLMEmbeddings
current_folder = os.path.dirname(os.path.abspath(__file__))
docx_path = os.path.join(current_folder, "agent_doc.docx")
# 加载文档
loader = Docx2txtLoader(docx_path)
documents = loader.load()
print(f"文档包含 {len(documents)} 个片段")
# 文档切分
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=800,
chunk_overlap=10,
separators=["\n\n", "\n", " ", ""]
)
documents = text_splitter.split_documents(documents)
print(f"文档切分为 {len(documents)} 个片段")
time.sleep(1)
# 加载嵌入模型
embedding = miniLMEmbeddings
demo_vector = embedding.embed_query("Hello world")
print(f"嵌入向量维度:{len(demo_vector)}")
# 向量数据库构建
db = Chroma.from_documents(
collection_name="demo",
documents=documents,
embedding=embedding,
persist_directory="chroma_db"
)
print(f"向量数据库构建完成,包含 {db._collection.count()} 个向量")
time.sleep(1)
# RAG检索与问答流程
query = "can you tell me what is the name of the author?"
print(f"用户问题:{query}")
time.sleep(1)
query_embedding = embedding.embed_query(query)
time.sleep(1)
retrieved_docs = db.similarity_search_by_vector(
embedding=query_embedding,
k=2
)
print(f"检索到 {len(retrieved_docs)} 个相关文档片段")
# 拼接检索到的文档片段
context_text = ""
for i, doc in enumerate(retrieved_docs):
context_text += f"Document excerpts {i+1}:{doc.page_content}\n"
time.sleep(1)
# 组装Prompt
prompt_template = ChatPromptTemplate.from_messages([
("system", "You are an assistant. Respond with A: followed by your answer. After finishing your answer after A:, stop generating completely."),
("user", "Here are some document contents:\n{context}\n\nPlease answer the user's question based on these contents: {query}")
])
formatted_prompt = prompt_template.format_prompt(
context=context_text,
query=query
)
print(f"组装后的Prompt内容:\n{formatted_prompt.to_string()}")
print("正在调用大模型,请稍候...")
# 分词并生成
model_inputs = tokenizer(formatted_prompt.to_string(), return_tensors="pt").to("cuda")
output_tokens = model.generate(
**model_inputs,
max_new_tokens=400
)
# 解码,只保留模型新输出内容
input_len = model_inputs["input_ids"].shape[1]
result = tokenizer.decode(output_tokens[0][input_len:])
print("模型回答:\n", result.split("Q:")[0])
9. 运行效果与调优建议
9.1 预期输出
运行 final_release.py 后,控制台会依次输出文档加载信息、切分结果、向量维度、数据库构建状态,以及最终的模型回答。整个过程完全离线,无需调用任何云端 API,数据自始至终不出本地机器。
9.2 调优方向
- 检索精度 :适当增大
chunk_size可保留更多上下文,但过大会引入噪声;k值越大,模型可参考的信息越多,但也会增加 Prompt 长度与推理时间。 - 生成质量 :可通过调整
temperature、top_p等采样参数控制回答的多样性与稳定性。 - 向量数据库 :当文档规模增大时,可考虑使用
FAISS或Milvus等更高效的向量存储方案。 - 模型选择:若追求更强生成能力,可替换为更大的本地模型(如 Qwen、ChatGLM 系列),但需注意显存占用。
10. 总结
本文基于 LangChain 框架,完整实现了一个本地化部署、无需调用云端 API 的 RAG 系统。从文档加载、文本切分、向量化存储,到检索与生成,每一步都给出了可运行的代码示例。该方案完全离线运行,兼顾了数据隐私与部署成本,适合作为企业知识库问答、私有文档检索等场景的基础框架。
后续可以在此基础上扩展多轮对话、文档增量更新、混合检索(关键词 + 向量)等能力,进一步提升系统的实用性与鲁棒性。