在构建专业问答系统时,模型对特定事实的掌握往往面临挑战。例如,关于“免费额度15GB”的信息,大模型经过多次微调仍可能错误回答为“10GB”。针对这类精准事实召回问题,除了持续微调,采用RAG(检索增强生成)技术是更为高效的解决方案。RAG与微调分工明确:微调(Fine-Tuning)旨在改变模型的表达或推理方式;而RAG的核心是为模型提供一个可实时检索参考的外部知识库。
本文将摒弃如Dify、Ragflow等现成平台,聚焦于使用Python原生代码,从零实现一个RAG系统,并深入探讨其开发流程与关键调优点。
一、RAG 文档准备与向量化 (Knowledge Ingestion)
首先,准备知识文件以验证流程。在 ./data/knowledge_base.txt 中存入一条简单知识:
怎么安装 SDK?`pip install feiyue-sdk` 即可完成安装。
随后,编写生成向量知识库的Python脚本:
import os
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_huggingface import HuggingFaceEmbeddings
from langchain_chroma import Chroma
from langchain_community.document_loaders import DirectoryLoader, TextLoader
# 配置向量存储目录
VECTOR_DB_PATH = "./vector_store"
DATA_PATH = "./data"
def ingest_local_data(vector_db_path=VECTOR_DB_PATH, data_path=DATA_PATH):
if not os.path.exists(data_path):
print(f"❌ Error: {data_path} not found.")
return
# 1️⃣ 初始化 Embeddings 模型
embeddings = HuggingFaceEmbeddings(
model_name="BAAI/bge-small-zh-v1.5",
model_kwargs={'device': 'cpu'} # 可改为 'cuda' 或 'mps'
)
# 2️⃣ 初始化向量数据库
vector_db = Chroma(
persist_directory=vector_db_path,
embedding_function=embeddings
)
# 3️⃣ 加载文档
loader = DirectoryLoader(data_path, glob="**/*.txt", loader_cls=TextLoader)
docs = loader.load()
# 4️⃣ 文档智能切分
splitter = RecursiveCharacterTextSplitter(chunk_size=600, chunk_overlap=100)
final_docs = splitter.split_documents(docs)
# 5️⃣ 向量化并入库
vector_db.add_documents(final_docs)
print(f"✅ Ingested {len(final_docs)} chunks from {data_path}")
if __name__ == "__main__":
ingest_local_data()
此阶段完成了RAG的知识库构建:
- 读取
./data/ 目录下的 .txt 文件。
- 使用
RecursiveCharacterTextSplitter 将文档切分为块(chunk),设置最大块大小为600字符,相邻块重叠100字符以保持语义连贯。
- 选用
BAAI/bge-small-zh-v1.5 作为Embedding模型,将文本块转化为向量。
- 使用Chroma向量数据库进行持久化存储。
代码中未显式定义分隔符,但 RecursiveCharacterTextSplitter 默认使用 ["\n\n", "\n", " ", ""] 作为层级分隔符,智能切分文本。
运行脚本后,将在 ./vector_store 目录下生成 chroma.sqlite3 等文件。通过数据库工具查看,可发现其中存储了向量数据及元数据。至此,RAG的知识库准备阶段完成。
二、RAG 检索与问答实现 (Retrieval & Generation)
接下来,实现检索与生成的核心逻辑:
import os
import torch
from modelscope import snapshot_download
from transformers import AutoModelForCausalLM, AutoTokenizer, pipeline
from langchain_core.prompts import ChatPromptTemplate
from langchain_huggingface import HuggingFacePipeline
from langchain_huggingface import HuggingFaceEmbeddings
from langchain_chroma import Chroma
from langchain_classic.chains.combine_documents import create_stuff_documents_chain
from langchain_classic.chains import create_retrieval_chain
VECTOR_DB_PATH = "./vector_store"
BASE_MODEL_ID = "Qwen/Qwen2.5-0.5B-Instruct"
class RAGService:
def __init__(self, vector_db_path=VECTOR_DB_PATH):
self.vector_db_path = vector_db_path
# 自动选择计算设备
if torch.cuda.is_available():
self.device = "cuda"
elif torch.backends.mps.is_available():
self.device = "mps"
else:
self.device = "cpu"
print(f"🖥️ Using device: {self.device}")
# 初始化 Embeddings
self.embeddings = HuggingFaceEmbeddings(
model_name="BAAI/bge-small-zh-v1.5",
model_kwargs={'device': self.device}
)
# 加载大语言模型 (LLM)
self.llm = self._load_model()
# 加载向量数据库
self.vector_db = Chroma(
persist_directory=self.vector_db_path,
embedding_function=self.embeddings
)
def _load_model(self):
print("📥 Downloading/Loading Model...")
model_dir = snapshot_download(BASE_MODEL_ID)
tokenizer = AutoTokenizer.from_pretrained(model_dir, trust_remote_code=True)
model = AutoModelForCausalLM.from_pretrained(
model_dir,
torch_dtype="auto",
device_map="auto",
trust_remote_code=True
)
pipe = pipeline(
"text-generation",
model=model,
tokenizer=tokenizer,
max_new_tokens=512,
temperature=0.1,
return_full_text=False
)
return HuggingFacePipeline(pipeline=pipe)
def get_chain(self):
system_prompt = (
"你是一个专业的助手。请仅根据提供的上下文(Context)回答问题。"
"如果你在上下文中找不到答案,请诚实告知。回答请简明扼要。"
"\n\n上下文: {context}"
)
prompt = ChatPromptTemplate.from_messages([
("system", system_prompt),
("human", "{input}"),
])
question_answer_chain = create_stuff_documents_chain(self.llm, prompt)
return create_retrieval_chain(
self.vector_db.as_retriever(search_kwargs={"k": 3}),
question_answer_chain
)
if __name__ == "__main__":
service = RAGService()
rag_chain = service.get_chain()
user_input = "安装飞跃云SDK的指令是什么?"
response = rag_chain.invoke({"input": user_input})
print("\n🤖 AI Answer:\n", response["answer"])
print("\n📄 Sources used:", [d.metadata.get('source') for d in response["context"]])
该流程的关键步骤如下:
- 问题向量化:用户问题通过相同的Embedding模型转换为查询向量。
- 向量相似度检索:在向量数据库中查找与查询向量最相似的Top-K个文档块(本例中K=3),常用余弦相似度等算法。
- 上下文增强生成:将检索到的文档块作为上下文,与问题一同提交给大语言模型,生成最终答案。
运行代码,成功输出答案 pip install feiyue-sdk 并溯源至源文件,标志着RAG检索与生成流程的贯通。
三、实战:构建个人知识库
在基础流程验证通过后,进行大规模知识库构建实战。将本地大量的 .md 文档进行切分与向量化,并将Embedding计算设备改为 mps 以利用Mac GPU加速。
执行后,成功将文档切分为5433个知识块(chunk)并存入向量库。随后进行检索测试,针对问题“我的低成本 LLM 微调用了哪个模型?秩用了多少?”,系统基本能给出正确回答并准确溯源,证明了 BAAI/bge-small-zh-v1.5 小Embedding模型与 Qwen2.5-0.5B-Instruct 小语言模型组合的有效性。
四、调优:修复检索信息偏移
在测试中,发现对于“秩(r)的值是多少?”这一问题,模型错误回答了“8”,而原文实际为“4”。为定位问题,我们深入探究向量数据库内部。
通过查询 embedding_metadata 表,可以观察到每个chunk存储了两条元数据:source(源文件路径)和 chroma:document(块文本内容)。这证实了 chunk_size=600 和 chunk_overlap=100 的参数生效。
通过修改代码打印出每次检索所命中的 embedding_id,并反查数据库,发现召回的三个chunk中,仅有一个包含了相关描述:
“而且,r (秩)也可能有关系,秩从 8 改为 4 可能会比较容易训练...”
该句表述存在一定歧义,对AI模型的理解能力构成了挑战。最初的回答错误可能源于生成温度(temperature)设置过低(0.1),导致模型过于“保守”而未能正确解读上下文。
将 temperature 参数调整至0.3后重新提问,模型成功给出了“秩为4”的正确答案。这表明适当提高温度可以增强模型在模糊上下文下的推理能力。
然而,调优也引入了新问题:
- 幻觉(Hallucination):回答中开始出现知识库外的噪声信息。
- 过拟合风险:当前的参数(如温度、top-k)可能仅对当前测试问题表现良好,对其他问题的泛化能力有待验证。
此外,还有两个潜在优化点:
- 检索返回的文档数量(
k值)需要权衡。k值过小可能导致信息不足;k值过大则可能超出小模型的有效上下文窗口。
- RAG系统在回答“知识库中不包含什么”(反向查询)时能力较弱,其答案质量严重依赖于检索环节召回的相关材料。