你的RAG系统,是“搭积木”还是“盖房子”?Langchain与纯手搓的抉择,决定了你的AI是“玩具”还是“神器”,细思极恐!
你是否曾为大模型“一本正经地胡说八道”而困扰?或者希望它能回答关于你个人笔记或公司内部文档的问题?今天,我们就来一起动手搭建一个简单 RAG 系统,让你的大模型从“闭卷考试”变成“开卷考试”,大幅提升回答的准确性和实用性。
了解了 RAG 的基本原理后,我们来看看如何动手实现它。

文章目录
一、什么是 RAG?——让大模型“开卷考试”
1.1 RAG 的核心思想

想象一下,你正在参加一场考试。如果只能靠记忆答题,遇到不熟悉的知识点就容易答错甚至瞎编——这就像没有 RAG 的传统大语言模型(LLM)。它们的知识被“固化”在训练数据中,无法获取新信息,也容易产生“幻觉”。
而 RAG(Retrieval-Augmented Generation,检索增强生成)技术,就是给大模型发了一本“参考书”。当用户提问时,系统会先从这本“参考书”(你的知识库)里查找相关信息,再把找到的内容和问题一起交给大模型去生成答案。
这种“开卷考试”的方式带来了三大优势:
- ✅ 事实更准确:答案基于真实文档,大幅减少胡编乱造。
- ✅ 知识可更新:只需更新你的知识库文件,就能让大模型掌握最新信息。
- ✅ 支持私有数据:你可以让它读取你的 PDF 报告、Word 文档、网页内容,解答专属问题。
1.2 RAG 的工作流程

RAG 系统的工作可以分为两个阶段:离线准备和在线查询。
在你开始使用之前,需要先完成“离线准备”阶段,也就是把你的知识文档处理好,方便后续快速查找。
这个过程包含五个核心步骤:
- 用户提问:“阿司匹林的禁忌症是什么?”
- 查询向量化:将你的问题转换成一串数字(向量),这是计算机理解语义的方式。
- 向量检索:在存储着无数文档片段向量的数据库中,找出与问题向量最相似的几个片段。
- 提示增强:把检索到的相关片段和原始问题拼接起来,形成一个新的、信息更丰富的提示词。
- 大模型生成:把这个增强后的提示词交给大模型,它就能结合这些参考资料,生成一个有据可依的答案。
整个过程通常在几秒内完成,为你提供精准可靠的信息。
二、两种实现方式对比:Langchain vs 纯手搓

现在,你已经知道了 RAG 是怎么工作的,接下来就要选择用什么方式来搭建它。主要有两种路径:使用成熟的框架 Langchain,或是从零开始 纯手搓 实现。
2.1 开发效率对比
- Langchain:这就像使用一套高级乐高积木。它已经为你预制好了“文档加载器”、“文本切分器”、“向量数据库接口”等模块。你只需要像搭积木一样把它们组装起来,几分钟内就能跑通整个流程,非常适合快速验证想法。
- 纯手搓:这就像是自己烧砖、砍木头,再盖房子。你需要手动调用底层库(如
sentence-transformers、faiss)来实现每一个环节。虽然耗时较长,但你能完全掌控每一个细节。
2.2 代码复杂度与维护成本
| 维度 | Langchain 实现 | 纯手搓实现 |
|---|---|---|
| 开发时间 | ~25 分钟 | ~180 分钟 |
| 代码行数 | 少(高层 API) | 多(底层集成) |
| 调试难度 | 低 | 高 |
| 典型应用场景 | 快速原型、企业系统 | 教学演示、边缘设备 |
总的来说,Langchain 抽象程度高,学习曲线稍陡,但后期扩展和维护非常方便。而纯手搓代码透明可控,适合教学理解底层机制,但在实际项目中维护成本较高。
2.3 适用场景总结表
| 维度 | Langchain 实现 | 纯手搓实现 |
|---|---|---|
| 开发时间 | ~25 分钟 | ~180 分钟 |
| 代码行数 | 少(高层 API) | 多(底层集成) |
| 调试难度 | 低 | 高 |
| 典型应用场景 | 快速原型、企业系统、复杂 Agent 编排 | 教学演示、资源受限环境、极致性能优化 |
通过这个对比,你应该能初步判断哪种方式更适合你当前的需求。接下来,我们将分别用这两种方式,带你一步步实现一个完整的 RAG 系统。
三、Langchain 实现:高效构建你的第一个 RAG 系统
如果你的目标是快速上手并看到效果,那么 Langchain 绝对是首选。让我们开始吧!
3.1 环境准备
首先,你需要安装必要的 Python 包。打开你的命令行工具,执行以下命令:
pip install langchain langchain-openai langchain-text-splitters chromadb faiss-cpu openai python-dotenv 同时,你需要一个 OpenAI API 密钥来调用 GPT 模型。为了安全起见,建议你创建一个 .env 文件来管理它:
# .env 文件内容 OPENAI_API_KEY=your_actual_api_key_here 这样,你的密钥就不会硬编码在 Python 脚本里了。
3.2 步骤详解与代码实现
步骤 1:加载文档
from langchain_community.document_loaders import TextLoader # 创建TextLoader实例,加载名为"knowledge.txt"的文本文件 loader = TextLoader("knowledge.txt", encoding="utf-8") documents = loader.load()- 功能说明:这一步负责读取你的原始文档。
TextLoader支持 TXT 格式,如果是 PDF,你可以换成PyPDFLoader。
步骤 2:文本切分
from langchain.text_splitter import RecursiveCharacterTextSplitter # 初始化递归字符切分器 text_splitter = RecursiveCharacterTextSplitter( chunk_size=500,# 每个文本块约500个字符 chunk_overlap=50,# 块之间重叠50个字符 separators=["\n\n","\n","。",".","!","?"]# 分割符优先级) chunks = text_splitter.split_documents(documents)- 功能说明:将长文档切成小块,便于后续处理。
chunk_overlap的作用就像你读书时前后翻页保持连贯,防止一句话被切断。 - 💡 小贴士:对于中文文档,确保分割符列表包含了中文句号“。”。
步骤 3:向量化并存入数据库

from langchain_openai import OpenAIEmbeddings from langchain_community.vectorstores import Chroma # 使用OpenAI的嵌入模型将文本块转为向量 embeddings = OpenAIEmbeddings(model="text-embedding-3-large") vectorstore = Chroma.from_documents( chunks, embedding=embeddings, persist_directory="./chroma_db" # 将数据库持久化保存到本地 ) vectorstore.persist() # 确保数据已写入磁盘 - 功能说明:
embeddings模型是关键,它能让语义相近的句子(如“猫爱吃鱼”和“猫咪喜欢吃鱼”)拥有相似的向量表示。 - 替代方案:不想依赖 API?可以换用开源的
HuggingFaceEmbeddings和BAAI/bge-small-zh模型。
步骤 4:创建检索器
retriever = vectorstore.as_retriever(search_kwargs={"k":3})- 功能说明:定义了一个检索策略,每次查询都会返回最相关的 Top-3 结果。
- 高级选项:可以设置
search_type="mmr"来使用最大边际相关性算法,避免返回重复冗余的结果。
步骤 5:初始化语言模型
from langchain_openai import ChatOpenAI llm = ChatOpenAI(model_name="gpt-3.5-turbo", temperature=0)- 功能说明:接入 GPT-3.5 模型进行回答生成。
temperature=0表示输出尽可能确定和稳定。 - 替代建议:国内用户可以尝试接入通义千问、百川等国产大模型的 API。
步骤 6:构建 RAG 生成链
from langchain_core.prompts import ChatPromptTemplate from langchain.schema.runnable import RunnablePassthrough from langchain.schema.output_parser import StrOutputParser # 定义一个提示模板,明确告诉大模型如何利用上下文 prompt = ChatPromptTemplate.from_template("请基于以下上下文回答问题,若无法找到答案请回答‘我不知道’。\n""上下文:{context}\n\n问题:{question}") # 构建核心链条:接收问题 -> 检索相关文档 -> 填充提示模板 -> 调用大模型 -> 解析字符串输出 rag_chain =({"context": retriever,"question": RunnablePassthrough()}| prompt | llm | StrOutputParser()) # 执行查询 result = rag_chain.invoke("阿司匹林的禁忌症是什么?")print("AI回答:", result)- 功能说明:
RunnablePassthrough()的作用是保留用户的原始问题,并将其传递给下一个组件。 - ✅ 已完成:恭喜!你已经成功用 Langchain 搭建了一个功能完整的 RAG 系统。
四、纯手搓实现:深入理解 RAG 底层机制
现在,让我们放下高级框架,亲手实现一遍 RAG 的核心逻辑,这能帮助你彻底理解它的本质。
4.1 不依赖框架的意义
通过纯手搓实现,你将清晰地看到数据是如何在各个组件间流动的。这对于学习原理、在资源受限的设备上部署,或者进行极致的性能优化都至关重要。
4.2 步骤详解与代码实现
步骤 1:加载并切分文本
# 手动读取文本文件withopen("knowledge.txt","r", encoding="utf-8")as f: text = f.read()# 使用简单的滑动窗口进行切分 chunk_size =500 overlap =50 chunks =[text[i:i + chunk_size]for i inrange(0,len(text), chunk_size - overlap)]print(f"✅ 切分为 {len(chunks)} 个文本块")- 功能说明:这是一种最基础的切分方法,通过固定长度和重叠来生成文本块。
- 缺陷提醒:这种方法缺乏语义感知,可能会在句子中间切断,导致信息丢失。相比之下,Langchain 的递归切分器要智能得多。
步骤 2:调用嵌入模型生成向量
from sentence_transformers import SentenceTransformer import numpy as np # 加载一个轻量级的开源嵌入模型 embedding_model = SentenceTransformer('all-MiniLM-L6-v2')# 将所有文本块批量编码为向量 chunk_vectors = embedding_model.encode(chunks, show_progress_bar=True) dimension = chunk_vectors.shape[1]# 获取向量维度- 功能说明:
SentenceTransformer库让本地运行嵌入模型变得非常简单。 - 推荐模型:对于中文场景,强烈推荐使用
BAAI/bge-small-zh模型,效果远超通用模型。
步骤 3:构建 Faiss 向量索引

import faiss # 创建一个基于欧氏距离(L2)的索引 index = faiss.IndexFlatL2(dimension)# 将所有文本块的向量添加到索引中 index.add(np.array(chunk_vectors))# 将索引持久化保存,避免每次启动都重新计算 faiss.write_index(index,"faiss_index.bin")- 功能说明:Faiss 是一个高效的向量相似度搜索库,特别适合处理大规模向量数据。
- 性能优化:对于更大的数据集,可以改用
IndexHNSWFlat等近似最近邻索引,以换取更快的检索速度。
步骤 4:相似度检索
query ="阿司匹林的禁忌症是什么?"# 将查询问题也编码为向量 query_vector = embedding_model.encode([query])# 在向量库中搜索与查询向量最相似的3个结果 distances, indices = index.search(np.array(query_vector), k=3)# 根据检索到的索引,取出对应的文本块 retrieved_chunks =[chunks[i]for i in indices[0]] context ="\n".join(retrieved_chunks)# 将多个相关片段合并为一段上下文- 功能说明:
index.search()返回的是距离值(越小越相似)和对应的向量索引。 - 改进建议:在某些场景下,使用余弦相似度比欧氏距离更能反映语义相关性。
步骤 5:拼接 Prompt 并调用 LLM 生成
from transformers import pipeline # 创建一个文本生成管道,这里使用较小的gpt2模型作为示例 generator = pipeline("text-generation", model="gpt2")# 构造增强提示词 prompt =f"根据以下资料回答问题:\n{context}\n\n问题:{query}\n回答:"# 调用模型生成回答 output = generator(prompt, max_new_tokens=150, do_sample=False)# 提取出模型生成的回答部分,去掉前面的提示词 answer = output[0]['generated_text'][len(prompt):].strip()print("AI回答:", answer)- 功能说明:这是整个流程的最后一步,也是最关键的一步。大模型基于你提供的上下文来生成最终答案。
- 注意事项:
max_new_tokens参数控制生成的最大长度,避免模型无限制地生成下去。 - 步骤 1:加载并切分文本
- 步骤 2:调用嵌入模型生成向量
- 步骤 3:构建 Faiss 向量索引
- 步骤 4:相似度检索
- 步骤 5:拼接 Prompt 并调用 LLM 生成
✅ 纯手搓 RAG 系统已成功运行!
五、环境配置与新手避坑指南

为了让你的 RAG 之旅更加顺畅,这里有一份详细的避坑指南。
5.1 推荐开发环境
- Python 版本:强烈推荐使用 3.9 – 3.11 版本。避免使用 3.12 及以上版本,因为其对
typing模块的修改可能导致langchain等库出现ImportError: cannot import name 'AsyncGenerator'的错误。 - 内存要求:建议 ≥16GB。向量计算和模型加载会消耗大量内存。
- GPU 建议:如果有 NVIDIA 显卡(显存 ≥8GB),可以显著加速嵌入模型和大模型的推理速度。
5.2 虚拟环境与依赖管理
永远不要在全局环境中安装 Python 包!使用虚拟环境可以避免不同项目间的依赖冲突。
# 创建名为 rag_env 的虚拟环境 python -m venv rag_env # 激活虚拟环境 (Linux/Mac) source rag_env/bin/activate # 激活虚拟环境 (Windows) rag_env\Scripts\activate # 安装依赖 pip install langchain sentence-transformers faiss-cpu openai chromadb PyPDF2 5.3 新手常见问题与解决方案
| 问题类型 | 表现 | 解决方案 |
|---|---|---|
| Python 版本冲突 | ImportError: cannot import name ‘AsyncGenerator’ | 使用 Python 3.9–3.11 |
| API 密钥未设置 | AuthenticationError | 检查 .env 文件中的 OPENAI_API_KEY 是否正确 |
| 中文路径报错 | invalid utf-8 sequence | 将项目放在纯英文路径下,例如 C:\projects\rag_demo |
| 模型下载失败 | .model.part 残留 | 检查网络连接,或使用代理;也可尝试离线预载模型 |
5.4 最佳实践建议
- 使用
.env文件:管理 API 密钥等敏感信息,保障安全。 - 合理设置文本块大小:建议在 200–500 字符之间,并启用 50 字符左右的重叠,以保持语义完整。
- 约束大模型行为:在提示词中明确指令,如“仅基于以上资料回答,不知道就说‘我不知道’”,有效防止幻觉。
- 生产环境容器化:使用 Docker 打包应用,保证在任何服务器上都能一致运行。
六、总结与学习建议
通过本文的学习,相信你已经掌握了 RAG 系统的核心概念和两种主流实现方式。
- 初学者建议:优先使用 Langchain 快速搭建原型,体验 RAG 带来的强大能力。
- 进阶学习:再通过 纯手搓 的方式,亲手实现一遍,从而深刻理解向量检索、嵌入模型等底层机制。
- 未来探索:当你熟练掌握基础 RAG 后,可以进一步探索 GraphRAG(利用图结构进行复杂推理)或 多模态 RAG(支持图片、音频等非文本信息的检索)等更高级的形态。
七、转载声明
本文为原创文章,如需转载,请联系作者获得授权,并注明出处。