1. 环境准备:打造你的专属AI开发空间
想自己动手做一个能“读懂”你私人文档,并像专家一样回答问题的AI助手吗?听起来很酷,但第一步往往最让人头疼:环境配置。别担心,我带你一步步来,避开我当初踩过的那些坑。咱们的目标是搭建一个完全在本地运行的RAG系统,这意味着你的数据、你的模型、你的知识库,全都掌握在自己手里,安全又私密。
首先,你得有个“工作台”。我强烈推荐使用Conda来管理Python环境,这能让你在尝试不同项目时,避免各种依赖包版本打架的混乱局面。打开你的终端(Windows用户用Anaconda Prompt,Mac/Linux用户直接用终端),输入下面这行命令,创建一个名为llmrag、Python版本为3.10的独立环境。为什么是3.10?因为这个版本对后面要用的PyTorch、LangChain等库的兼容性最好,最稳定。
conda create -n llmrag python=3.10
创建完成后,激活它。这个环境就像你的专属实验室,所有操作都在这里面进行,不会影响电脑上其他项目。
conda activate llmrag
接下来是重头戏:安装PyTorch。这是运行很多AI模型的基础框架。如果你有NVIDIA的显卡并且想利用GPU来加速(强烈推荐,速度会快很多),安装命令需要根据你的CUDA版本(一个让GPU干AI活的驱动)来定。你可以通过nvidia-smi命令查看你的CUDA版本。假设你是比较新的CUDA 12.1,就用我下面这个命令。如果你的显卡不支持,或者你只想用CPU跑(会慢一些),可以去PyTorch官网生成对应的安装命令。
conda install pytorch==2.3.1 torchvision==0.18.1 torchaudio==2.3.1 pytorch-cuda=12.1 -c pytorch -c nvidia
基础框架搭好了,现在来安装这个项目的核心“零件”。我们不用OpenAI的在线API,主打一个本地化。所以核心是langchain-ollama,它能让LangChain框架和本地的Ollama模型对话。langchain-chroma和langchain-community则是为了使用Chroma向量数据库和一些社区工具。beautifulsoup4和langchain是处理网页和构建应用链所必需的。
pip install langchain-ollama langchain-chroma langchain-community beautifulsoup4 langchain
最后,安装文本嵌入模型所需的库sentence-transformers。我们后面会用它来加载开源的BGE模型,把文字转换成计算机能理解的“向量”。
pip install sentence-transformers
好了,至此,你的AI开发环境已经准备就绪。这个过程就像组装电脑,把CPU(PyTorch)、主板(LangChain框架)、内存条(各种工具库)都插好了。接下来,我们就要安装这台“电脑”的大脑——本地大模型。
1.1 安装与配置Ollama:请来你的本地“大脑”
Ollama可以说是本地运行大模型的“神器”。它把模型下载、加载、运行这些复杂过程都打包好了,你只需要一条简单的命令就能让各种开源大模型在你的电脑上跑起来。这比你自己去折腾模型文件、配置参数要省心一百倍。
安装Ollama非常简单。根据你的操作系统,去它的官网(https://ollama.com)下载对应的安装包,像安装普通软件一样完成安装。安装好后,打开终端(注意,要确保你已经退出了刚才的llmrag的Conda环境,因为Ollama是一个独立的系统服务),输入下面的命令来拉取一个模型。这里我推荐使用qwen2.5:7b,它是阿里通义千问的7B参数版本,在中文理解和生成上表现不错,对硬件要求也相对友好(16G内存的电脑基本就能跑)。
ollama pull qwen2.5:7b
这个下载过程取决于你的网速,模型大概有4-5个G。下载完成后,你可以先试运行一下,确保模型正常工作:
ollama run qwen2.5:7b
你会进入一个对话界面,直接输入问题,比如“你好”,看它是否能正常回复。按Ctrl+D可以退出这个对话界面。记住,Ollama服务会在后台运行。现在,你的“大脑”已经就位,并且可以通过http://localhost:11434这个地址来访问它。我们的Python程序之后就会通过这个地址和模型“交流”。
1.2 安装Chroma向量数据库:给知识安个家
我们的RAG系统需要一个地方来存储处理过的知识,这就是向量数据库。你可以把它理解成一个超级智能的图书馆。普通的图书馆按书名、作者找书,而这个图书馆能按“意思”找内容。你把一段段文本(比如文档的段落)转换成高维向量(可以理解为一串有意义的数字指纹)存进去。当你提问时,问题也会被转换成向量,然后数据库帮你找出“意思”最接近的那些文本段落。
Chroma是一个轻量级、易用的开源向量数据库,特别适合我们这种本地项目和初学者。它不需要你安装Docker或者配置复杂的服务,一个Python包就能搞定。我们在前面已经通过pip install langchain-chroma安装了它的LangChain集成包。为了更直观地管理和查看数据库,你还可以安装它的独立运行版本:
pip install chromadb
安装后,你可以启动一个本地的Chroma服务端(虽然我们的程序默认用内嵌模式,但独立服务在某些场景下更方便管理):
chroma run --path ./chroma_data
这行命令会在当前目录下的./chroma_data文件夹里运行一个Chroma服务。不过,在我们接下来的代码中,我们会采用更简单的“持久化目录”模式,让LangChain直接管理数据库文件,无需单独运行服务。这样整个项目结构更简洁,所有数据都在你的项目文件夹里。
2. 核心组件与原理浅析:RAG是如何工作的?
在开始敲代码前,我们花点时间搞明白RAG到底在干什么。这能让你后面调代码、解决问题时心里有底。RAG,检索增强生成,这个名字听起来高大上,其实拆开看就两步:“检索”和“增强生成”。
想象一下,你是一个刚入职的新员工,对公司业务一无所知。这时候老板问你一个专业问题,你肯定答不上来。但如果你手边有一份完备的公司知识库,你就可以先去快速查阅(检索)相关的制度文件和项目历史,然后结合你自身的语言能力(生成),组织出一个准确的回答。RAG里的语言模型(LLM)就是这个“新员工”,向量数据库就是那个“知识库”。
具体流程是这样的:首先,你把你的私人文档(比如产品手册、项目报告、学习笔记)进行“切片”,切成一段段有重叠的小文本块(防止把一句话拦腰截断)。然后,使用一个Embedding模型(比如我们选的BAAI/bge-small-zh-v1.5)把这些文本块转换成向量。这个转换过程,可以理解为把一段文字的含义映射到一个高维数学空间中的一个点。意思相近的文字,它们的向量点在空间里的距离也更近。所有这些向量连同原文,都被存进Chroma向量数据库。
当用户提出一个问题时,系统首先用同样的Embedding模型把问题也转换成向量。然后,拿着这个“问题向量”去向量数据库里搜索,找出和它最相似的几个文本块(这就是检索)。最后,把这些检索到的文本块作为“参考上下文”,和原始问题一起打包,提交给Ollama本地大模型。我们会给模型一个指令(Prompt模板):“请根据下面的上下文回答问题...”,模型就会基于这些确凿的参考资料来生成答案(这就是增强生成)。这样生成的答案,既利用了模型强大的语言能力,又规避了它可能胡编乱造(幻觉)的问题,因为答案的素材来源是确定的。
所以,我们项目的四大核心就是:文本切片器(Text Splitter)、嵌入模型(Embedding Model)、向量数据库(Vector Database) 和大语言模型(LLM)。LangChain框架就像胶水,把它们优雅地粘合在一起,组成一个完整的工作流水线。
3. 实战第一步:准备与处理你的知识库数据
理论说再多不如动手做。现在,我们开始用代码构建这个流水线的第一步:准备数据。数据是RAG系统的基石,质量决定上限。这里我给你演示两种最常用的数据来源:本地HTML文件和在线网页。
首先,在你的项目目录下创建一个新的Python文件,比如叫rag_pipeline.py。让我们先导入所有需要的“工具包”。
import os
import time
from bs4 import BeautifulSoup
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.prompts import ChatPromptTemplate
from langchain.schema.runnable import RunnablePassthrough
from langchain.schema.output_parser import StrOutputParser
from langchain_ollama.llms import OllamaLLM
from langchain_community.embeddings import HuggingFaceBgeEmbeddings
from langchain_chroma import Chroma
3.1 从本地HTML文件加载数据
我经常会把一些重要的网页“另存为”HTML文件到本地,作为知识库。比如,我们可以把百度百科上关于“RAG”的词条页面保存下来,命名为rag_baidu.html。下面这个函数就是用来读取并提取其中纯文本内容的。
# 设置一个用户代理,模拟浏览器访问,有些网站会检查这个
os.environ["USER_AGENT"] = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
def load_local_file(file_path):
"""加载本地HTML文件,并用BeautifulSoup提取纯净文本"""
with open(file_path, "r", encoding="utf-8") as f:
html_content = f.read()
# 使用BeautifulSoup解析HTML,并获取所有文本
soup = BeautifulSoup(html_content, "html.parser")
# 去除脚本、样式等无用标签
for script in soup(["script", "style"]):
script.decompose()
text = soup.get_text()
# 清理多余的空行和空白字符
lines = (line.strip() for line in text.splitlines())
chunks = (phrase.strip() for line in lines for phrase in line.split(" "))
text = ' '.join(chunk for chunk in chunks if chunk)
return text
拿到大段文本后,不能直接扔给模型。一方面有长度限制,另一方面也不利于精确检索。我们需要进行“切片”。这里用到RecursiveCharacterTextSplitter,它会智能地按照段落、句子、甚至标点进行递归切割,尽量保证语义的完整性。chunk_size=500表示每个文本块大约500个字符,chunk_overlap=50表示块与块之间重叠50个字符,防止一个完整的句子被切碎。
def prepare_data_from_local(file_path):
"""从本地文件准备数据块"""
raw_text = load_local_file(file_path)
# 初始化文本分割器
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=500,
chunk_overlap=50,
separators=["\n\n", "\n", "。", "!", "?", ";", ",", "、", " "]
)
# 创建文档对象,LangChain的后续处理需要这个格式
chunks = text_splitter.create_documents([raw_text])
print(f"共切分出 {len(chunks)} 个文本块。")
print("第一个文本块预览:", chunks[0].page_content[:200])
return chunks
3.2 从在线网页抓取数据
有时候我们需要直接抓取在线的产品文档、技术博客作为知识源。LangChain提供了WebBaseLoader这个便捷的工具。这里以讯飞开放平台的Spark PPT生成接口文档为例。
from langchain_community.document_loaders import WebBaseLoader
def prepare_data_from_web(url):
"""从网页URL加载并准备数据块"""
# WebBaseLoader会自动处理简单的网页内容抓取
loader = WebBaseLoader(url)
# load()方法返回Document对象列表
documents = loader.load()
# 使用同样的分割器进行切片
text_splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=50)
chunks = text_splitter.split_documents(documents)
print(f"从网页抓取并切分出 {len(chunks)} 个文本块。")
print("第一个文本块预览:", chunks[0].page_content[:200])
return chunks
你可以根据需求选择一种方式准备数据。我建议初学者先从本地文件开始,更容易控制和调试。数据准备好后,这些chunks就是我们要喂给向量数据库的“原料”了。
4. 构建知识库核心:嵌入模型与向量化存储
数据切片准备好了,下一步就是把这些文本变成向量,存进数据库。这个过程叫做“嵌入”(Embedding)。我们选择BAAI/bge-small-zh-v1.5这个开源模型,它由智源研究院发布,在中文文本表示上效果很好,而且模型体积小,在CPU上也能快速运行。
4.1 初始化嵌入模型与向量数据库
我们使用LangChain集成好的HuggingFaceBgeEmbeddings来加载这个模型。第一次运行时会自动从HuggingFace下载模型文件,需要一点时间。
def create_vector_store(chunks, persist_directory="./chroma_langchain_db"):
"""
将文本块嵌入为向量,并存储到Chroma数据库
Args:
chunks: 文本块列表
persist_directory: 向量数据库持久化保存的目录
"""
# 初始化嵌入模型,设置模型名称和设备('cpu' 或 'cuda')
model_name = "BAAI/bge-small-zh-v1.5"
model_kwargs = {'device': 'cpu'} # 如果你有GPU,可以改为 'cuda'
encode_kwargs = {'normalize_embeddings': True} # 标准化向量,有利于相似度计算
embeddings = HuggingFaceBgeEmbeddings(
model_name=model_name,
model_kwargs=model_kwargs,
encode_kwargs=encode_kwargs
)
print(f"嵌入模型 '{model_name}' 加载成功。")
# 创建Chroma向量存储,并持久化到本地目录
# from_documents方法会完成:嵌入文本 -> 存入数据库 -> 保存到磁盘 这一系列操作
vector_store = Chroma.from_documents(
documents=chunks,
embedding=embeddings,
persist_directory=persist_directory
)
print(f"向量数据库已创建并保存至:{persist_directory}")
# 创建一个检索器,用于后续查询
retriever = vector_store.as_retriever(search_kwargs={"k": 3}) # 检索最相似的3个片段
return vector_store, retriever
这段代码有几个关键点:
device: 如果你安装了GPU版本的PyTorch并且想加速,这里可以改成'cuda'。normalize_embeddings: 设为True,会将向量归一化,使得相似度计算(通常用余弦相似度)更准确。persist_directory: 指定一个目录,Chroma会把数据库文件保存在这里。下次运行程序时,如果目录已存在,可以直接加载,无需重新嵌入,节省大量时间。search_kwargs={"k": 3}: 定义检索器默认返回与问题最相似的3个文本块。这个数字可以根据需要调整,太多可能引入噪音,太少可能信息不足。
4.2 知识库的持久化与重复利用
一旦你的知识库构建好,除非源文档有更新,否则不需要每次都重新嵌入。你可以把创建和加载的逻辑分开。
def load_existing_vector_store(persist_directory="./chroma_langchain_db"):
"""加载已存在的向量数据库"""
embeddings = HuggingFaceBgeEmbeddings(model_name="BAAI/bge-small-zh-v1.5")
# 直接从一个持久化目录加载
vector_store = Chroma(
persist_directory=persist_directory,
embedding_function=embeddings
)
retriever = vector_store.as_retriever(search_kwargs={"k": 3})
print(f"已从 {persist_directory} 加载现有向量数据库。")
return vector_store, retriever
在实际项目中,我通常会写一个判断:如果持久化目录存在且里面有数据,就直接加载;否则,才执行从原始文档创建的全流程。这样,开发调试效率高很多。
5. 组装问答链:让系统“动”起来
最激动人心的部分来了!我们要把检索器(Retriever)和大语言模型(LLM)连接起来,形成一个完整的问答链条。LangChain的“链”(Chain)概念非常直观,就是把各个环节像管道一样接起来。
5.1 连接Ollama本地模型
首先,初始化我们通过Ollama部署的本地大模型。确保你的Ollama服务正在运行(ollama serve或模型已在后台运行)。
# 初始化Ollama LLM,指定我们之前拉取的模型
# base_url默认为 http://localhost:11434,如果你的Ollama运行在其他地方,需要修改
llm = OllamaLLM(model="qwen2.5:7b", base_url="http://localhost:11434", temperature=0.1)
# temperature参数控制生成答案的随机性,0表示最确定,1表示最有创造性。对于问答,可以设低一点。
5.2 设计Prompt模板
Prompt是指令,告诉模型应该扮演什么角色、按照什么规则来回答问题。一个好的Prompt能极大提升答案质量。这里我们设计一个简洁明确的模板:
# 定义我们的提示词模板
template = """你是一个专业的问答助手。
请严格根据以下提供的上下文信息来回答问题。上下文是你能获取的唯一信息。
如果上下文中的信息不足以回答问题,请直接说“根据提供的资料,我无法回答这个问题”。
请用中文,以清晰、简洁、有条理的方式组织答案,尽量使用上下文的原话。
上下文:
{context}
问题:
{question}
请根据上下文回答:"""
prompt = ChatPromptTemplate.from_template(template)
这个模板比原始文章里的更详细一些,它强调了“严格根据上下文”,并给出了无法回答时的标准回应,这能有效减少模型的“幻觉”。{context}和{question}是占位符,运行时会被实际的内容替换。
5.3 构建并运行RAG链
现在,用LangChain的表达式语言(LCEL)把各个组件“组装”起来。这种语法非常清晰,就像在描述一个流程图。
def setup_rag_chain(retriever):
"""组装RAG链"""
rag_chain = (
# 第一步:准备输入。这里“context”键的值通过retriever获取,它接收“question”作为输入。
{"context": retriever, "question": RunnablePassthrough()}
# 第二步:将上一步的输出(一个包含context和question的字典)传递给prompt模板进行格式化。
| prompt
# 第三步:将格式化后的完整提示词发送给LLM。
| llm
# 第四步:将LLM的输出解析为字符串。
| StrOutputParser()
)
return rag_chain
# 假设我们已经有了retriever
# rag_chain = setup_rag_chain(retriever)
这个rag_chain是一个可调用的对象。当你向它传入一个问题字符串时,它会自动执行以下步骤:
retriever根据问题检索出最相关的几个文本块,合并成context。prompt模板将context和question填充进去,生成完整的提示词。llm接收提示词,生成回答。StrOutputParser()将模型的输出解析为干净的字符串。
5.4 进行问答测试
让我们写一个主函数,把前面的所有步骤串起来,并进行测试。
def main():
# 方式一:从本地文件构建知识库(首次运行)
# chunks = prepare_data_from_local("rag_baidu.html")
# vector_store, retriever = create_vector_store(chunks)
# 方式二:从网页构建知识库(首次运行)
# chunks = prepare_data_from_web("https://www.xfyun.cn/doc/spark/PPTv2.html")
# vector_store, retriever = create_vector_store(chunks)
# 方式三:加载已存在的知识库(后续运行)
vector_store, retriever = load_existing_vector_store()
# 组装RAG链
rag_chain = setup_rag_chain(retriever)
# 测试问题
test_questions = [
"RAG技术的主要作用是什么?",
"在RAG系统中,向量数据库扮演什么角色?",
"请总结一下本文档的要点。"
]
for question in test_questions:
print(f"\n=== 问题:{question} ===")
start_time = time.time()
# 调用链并获取答案
answer = rag_chain.invoke(question)
elapsed_time = time.time() - start_time
print(f"答案:{answer}")
print(f"耗时:{elapsed_time:.2f}秒")
print("="*50)
if __name__ == "__main__":
main()
运行这个脚本,你就能看到你的本地RAG系统开始工作了!它会从向量数据库中检索相关信息,并指挥本地的Qwen2.5模型生成答案。第一次运行因为要下载嵌入模型和嵌入文本,会慢一些。后续运行直接加载数据库,速度会快很多。
6. 效果优化与踩坑指南
一个能跑的系统只是开始,一个“好用”的系统还需要调优。根据我的实战经验,以下几个地方的调整能显著提升效果:
1. 文本分块策略是灵魂
chunk_size和chunk_overlap没有黄金标准。对于技术文档,500-1000字可能合适;对于对话记录,可能200字更好。overlap设置50-150字能有效防止关键信息被割裂。你可以尝试不同组合,观察检索到的片段是否完整回答了测试问题。
2. 检索器的调参
retriever = vector_store.as_retriever(search_kwargs={"k": 4}) 这里的k值很重要。我一般从3开始试。有时,增加k值(比如到5)能让答案更全面,但也会增加无关信息。你还可以尝试不同的搜索类型,比如search_type="mmr"(最大边际相关性),它会在相关性和多样性之间做权衡,避免返回过于相似的片段。
3. Prompt工程的微调 Prompt直接指挥模型。你可以让它“先总结,再回答”,或者“以列表形式给出答案的关键点”。如果发现模型总忽略上下文,可以在Prompt里用更强烈的语气,比如“必须且只能根据以下上下文回答”。把“用户”换成“你是一个XX领域的专家”,也能激发模型不同的表现。
4. 处理模型“幻觉” 即使有上下文,模型有时还是会自己编造细节。除了强化Prompt,可以在后处理中加入验证逻辑。比如,让模型在生成答案后,为答案中的关键事实引用上下文的原文句段。更高级的做法是使用“链式验证”,让另一个流程去检查答案与上下文的一致性。
5. 性能瓶颈排查
如果感觉系统慢,一步步排查:嵌入模型在CPU还是GPU?Ollama模型加载是否用了GPU加速(可在Ollama运行时指定)?检索的k值是否过大?对于更大的知识库,可以考虑使用更高效的向量索引算法,Chroma默认的足够应对万级以下的数据量。
我最初搭建时,曾因为chunk_size设得太大,导致检索到的片段包含太多无关信息,答案变得冗长且偏离重点。后来调整为小尺寸、有重叠的分块,效果立竿见影。另一个坑是网页抓取的内容包含大量导航栏、广告文本,污染了知识库。所以,在load_local_file或使用WebBaseLoader时,一定要做好文本清洗,这是保证RAG答案质量的前提,往往比后面所有调参都重要。
551



被折叠的 条评论
为什么被折叠?



