嘿,各位技术同学!今天,我们来聊一个大家在使用大语言模型(LLM)时都会遇到的痛点:知识过时。
无论是像我一样,用 Gemini Pro 学习日新月异的以太坊,还是希望它能精确掌握某个特定工具的最新用法,我们都可能被它"记忆"里的旧知识所困扰。比如,我让它生成 PlantUML 图时,它偶尔会"穿越"回过去,使用一些已经被废弃的旧语法,这无疑增加了我们的学习和开发成本。
为了解决这个问题,我动手开发了一个小项目:一个基于**检索增强生成(RAG)**的智能问答系统。它能读取最新的官方文档(比如 PlantUML 的语言参考指南),并让 Gemini 模型在回答问题前,先查阅这些最新资料。今天,我就和大家分享这个项目的技术实现,希望能给大家带来一些启发。
项目核心理念:RAG如何解决问题?
在我们深入代码之前,先花一分钟理解一下 RAG 的工作原理。
想象一下,我们让一位非常聪明的实习生(LLM)写一份报告,但他对某个特定领域的最新进展不太了解。我们会怎么做?我们不会让他凭空想象,而是会把最新的行业报告、研究论文(外部知识库)丢给他,说:"看,根据这些资料来写。"
RAG 就是这个过程的自动化版本。它通过一个"检索"步骤,从我们的知识库(PDF、文档、网站等)中找到与用户问题最相关的信息,然后将这些信息连同原始问题一起打包,交给 LLM,让它基于这些"新鲜出炉"的材料来"生成"答案。
这个流程可以用下面的 PlantUML 图清晰地表示:
技术实现深度解析
现在,让我们深入 main.py
文件,看看这个 RAG 系统是如何一步步构建起来的。
第一步:知识库的建立与"保鲜"
万丈高楼平地起,我们的第一步是建立一个可靠的知识库。在这个项目中,知识来源于一份PDF文档。但我们做了一个非常实用的优化:自动检测文档更新。
代码通过 get_pdf_hash
函数计算 PDF 文件的 SHA256 哈希值。
python
def get_pdf_hash(file_path):
"""计算文件的 SHA256 哈希值"""
sha256_hash = hashlib.sha256()
with open(file_path, "rb") as f:
for byte_block in iter(lambda: f.read(4096), b""):
sha256_hash.update(byte_block)
return sha256_hash.hexdigest()
在 load_or_create_vectorstore
函数中,系统启动时会检查持久化目录 chroma_db
中存储的旧哈希值与当前 PDF 文件的哈希值是否一致。
- 如果一致:太棒了!说明文档没变,直接从磁盘加载已经处理好的向量数据库,秒级启动。
- 如果不一致或首次运行 :说明知识需要更新!系统会自动删除旧的数据库,然后使用
PyPDFLoader
加载新的 PDF,并用RecursiveCharacterTextSplitter
将其分割成更小的、易于检索的文本块。
这个设计非常关键,它确保了我们的"外脑"永远存储的是最新信息,同时避免了每次运行都重复处理文档的昂贵开销。
第二步:将知识向量化 (Embedding)
计算机不理解文字,只理解数字。为了让机器能够"理解"并比较文本片段的相似度,我们需要将它们转换成向量,这个过程叫做嵌入(Embedding)。
我们使用了 HuggingFace 的 sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2
模型。这个模型能将文本块转换成一个包含几百个数字的向量,神奇的是,语义上相似的文本块,其对应的向量在多维空间中的距离也更近。
python
def setup_embeddings():
"""初始化并返回嵌入模型"""
print("正在初始化嵌入模型...")
embeddings = HuggingFaceEmbeddings(
model_name=EMBEDDING_MODEL_NAME,
model_kwargs={'device': 'cpu'},
encode_kwargs={'normalize_embeddings': False}
)
return embeddings
所有从 PDF 中分割出来的文本块都会经过这个模型处理,然后连同它们的向量表示一起存入 Chroma
向量数据库中。
第三步:搭建检索与生成的核心链
现在,我们有了存储知识的数据库和负责回答问题的 LLM,是时候把它们"链接"起来了。这里我们借助了 LangChain 框架的 RetrievalQA
链。
python
def create_qa_chain(llm, vectorstore):
"""基于LLM和向量数据库创建并返回问答链"""
retriever = vectorstore.as_retriever(search_kwargs={"k": 2})
qa_chain = RetrievalQA.from_chain_type(
llm=llm,
chain_type="stuff",
retriever=retriever,
return_source_documents=True
)
return qa_chain
这个链条的工作流程如下:
- 设置检索器(Retriever) :
vectorstore.as_retriever(search_kwargs={"k": 2})
这行代码创建了一个检索器,当用户提问时,它会去 Chroma 数据库中检索出最相似的k=2
个文档片段。 - 选择链类型(Chain Type) :
chain_type="stuff"
是一种简单直接的策略。它会把检索到的所有文档片段(stuff them)一股脑地塞进一个提示词模板中,和用户的原始问题一起发送给 LLM。 - 返回源文档 :
return_source_documents=True
是一个非常有用的调试和验证工具。它让 LLM 在给出答案的同时,也告诉我们它参考了哪些原文片段。这大大增强了结果的透明度和可信度。
实践与效果
一切准备就绪后,main
函数将所有部分串联起来,并用一个具体问题来测试系统:
python
def main():
"""主执行函数,编排整个RAG流程"""
embeddings = setup_embeddings()
vectorstore = load_or_create_vectorstore(PDF_FILE_PATH, PERSIST_DIRECTORY, embeddings)
llm = setup_llm()
qa_chain = create_qa_chain(llm, vectorstore)
query = "如何绘制 JSON 数据图?"
ask_question(qa_chain, query)
当运行这个脚本时,它会:
- 初始化模型和向量数据库。
- 接收问题
如何绘制 JSON 数据图?
。 - 在数据库中找到关于 JSON 数据可视化的相关片段。
- 将这些片段和问题一起发给 Gemini。
- Gemini 基于最新的 PlantUML 语法(来自PDF)生成答案,并附上参考来源。
这样,我们就得到了一个既有 Gemini 强大推理能力,又有最新、最准确领域知识的 AI 助手。
实用建议与未来展望
这个项目虽然简单,但扩展性很强:
- 更换知识源 :我们可以轻易地将
PDF_FILE_PATH
换成任何我们想要的 PDF 文档,比如以太坊的白皮书、某个开源库的API文档,甚至是我们的学习笔记。 - 支持更多格式 :通过使用 LangChain 提供的其他
DocumentLoader
,我们可以让系统读取.txt
,.md
文件,甚至直接爬取网站内容。 - 部署为服务:使用 FastAPI 或 Flask 将这个问答系统包装成一个 API 服务,让其他人也能方便地使用。
- 优化检索 :对于非常大的文档,可以探索更高级的检索策略,如
Parent Document Retriever
,以获得更相关的上下文。
结论
通过 RAG,我们为大语言模型安装了一个可随时更新、可定制的"外脑"。这个方法不仅有效解决了 LLM 知识滞后的问题,还通过引入可验证的信源,大大提高了生成内容的可信度。最棒的是,实现这一切的技术门槛并不高。
希望这篇文章能激励大家动手尝试,为自己的学习和工作场景打造一个专属的、永远不会"过时"的AI伙伴。