如果你使用过ChatGPT,你就知道它有多神奇了。你提出一个问题,它就能立刻生成一个表达清晰、逻辑严密的答案。
但你也应该知道它的最大缺陷:如果你问它关于你公司的内部代码、你的私人Notion工作空间,或者昨天发生的事情,它就会无法给出正确的回答。
通常情况下,它会做出两种反应之一:要么道歉说它没有这些信息,要么更糟糕的是,它会信口编造一些内容来回应。
之所以会出现这种情况,是因为大型语言模型就像是被关在房间里且无法上网的超级聪明学生。它们只知道在被关进来之前记住的内容;如果你问的问题超出了它们已掌握的知识范围,它们就只能靠猜测来回答。
那么,我们该如何解决这个问题呢?如何让人工智能能够在不重新训练整个模型的情况下,回答关于我们私人数据的问题呢?
答案就是RAG,也就是“检索增强生成”技术。
几乎所有涉及处理私人数据的现代人工智能应用,其背后都采用了RAG架构。如果你曾经使用过“与PDF文件聊天”的应用程序,或者那些真正了解公司政策的客户服务机器人,那么你就已经接触过RAG技术了。
在这篇文章中,我们将从基础原理入手,详细解释RAG的工作原理。然后,我们会使用Python从头开始构建一个能够实际运行的RAG应用。
我们将会讨论以下内容:
什么是RAG?
RAG代表的是检索增强生成。让我们来详细解释一下这三个词的含义。
-
检索:从数据库中查找相关信息。
-
增强:将找到的信息添加到用户最初提出的问题中。
-
生成:让大型语言模型仅使用这些新增的信息来生成答案。
类比开卷考试
为了更好地理解这一概念,可以把传统的大型语言模型想象成参加闭卷考试的学生。这些学生之前确实阅读过大量的资料,但此刻他们必须完全依靠记忆来回答问题。有时候他们会忘记某些知识点,有时则会编造答案以避免让试卷留白……说实话,在我上大学的那些年里,我也经常用这种办法应付考试。
RAG将这种学习方式变成了一种“开卷考试”。当你提出一个问题时,系统会首先访问庞大的数据库,找到其中包含答案的具体内容,然后将这些信息提供给学生。学生只需阅读这些特定内容,就能写出完美的答案。
我们并没有依赖人工智能的记忆能力,而是依靠它的阅读理解能力。
为什么传统的大型语言模型会失败
在探讨如何构建RAG之前,我们首先需要明白:为什么仅仅让AI自主学习是远远不够的。
-
训练时限的限制:训练一个大型语言模型需要花费数月时间,并耗费数百万美元。因此,这些模型的训练数据仅涵盖到特定日期之前的内容。如果一个模型是在2025年训练出来的,那么它就完全不了解2026年发生的事情。
-
无法访问私有数据:你公司的Jira工单、内部维基文档以及Slack聊天记录都属于私有信息,OpenAI、Google和Anthropic等机构并没有将这些数据纳入他们的训练集。
-
“幻觉”现象:大型语言模型本质上就是高级的自动完成工具。它们会根据现有模式来预测下一个最有可能出现的词语。如果它们不知道某个事实,就会拼凑出一些听起来合理但实际上完全错误的答案。我们把这种现象称为“幻觉”。
-
上下文窗口的限制:你可能会想:“为什么不把整个公司的维基文档直接复制粘贴到ChatGPT的输入框里呢?”但实际上,每个大型语言模型都有一个“上下文窗口”,即它一次能够处理的最大文本量。即使是用那些具有较大上下文窗口的现代模型,将数千份文件全部粘贴进去也会导致处理速度极慢且成本高昂。此外,当输入的文本量过大时,模型也容易丢失相关信息。
-
重新训练的高昂成本:理论上来说,你可以使用自己的私有数据来对大型语言模型进行微调。但这种微调过程既复杂又昂贵。更重要的是,知识总是在不断变化——如果你更新了公司的政策,就必须再次对模型进行微调,以便让它掌握新的规则。
RAG解决了所有这些问题。它能够让大型语言模型直接访问实时的私有数据,而无需重新进行训练。
RAG的内部工作原理
为了让RAG能够正常运行,我们需要一套特定的技术架构。下面让我们来详细了解一下RAG的各个组成部分。
文档
一切都是从原始数据开始的——这些数据可能是PDF文件、数据库记录、文本文件,或者是从网站上抓取的信息。在人工智能领域,我们将所有这类原始资料统称为“文档”。
分块处理
对于一个简单的问题来说,直接将一本500页的书的全部内容输入到AI系统中是效率极低的。因此,我们需要将这些文档分解成更小、更易于处理的片段,这些片段被称为“分块”。一个分块可能只包含一段文字或几句话而已。
这一点非常重要,因为当用户提出问题时,我们只需要检索出包含答案的具体段落,而不是整本书的内容。如果跳过文本分块处理,系统就会检索到大量文本信息,从而导致大语言模型的上下文窗口超负荷运行。
嵌入技术
对于初学者来说,“嵌入技术”这个术语可能听起来令人望而生畏,但它的原理其实非常巧妙。计算机虽然不理解人类的语言,但却非常擅长处理数学运算。嵌入技术就是将人类语言转化为数字列表(即向量),从而捕捉文本的实际含义。
想象一下一个二维坐标系:我们可以把“狗”这个词标在坐标[2, 3]处,把“小狗”这个词标在[2.1, 3.1]处。尽管这两个词的含义不同,但因为在坐标系中它们的位置很接近,所以计算机能够理解它们表示相似的概念;而“汽车”这个词则可能位于[10, 10]这样的位置。
在真正的人工智能系统中,嵌入模型使用的维度远不止两个。它会将句子映射到数千个维度上,从而捕捉其中深层的语义信息。
向量数据库
一旦我们将所有的文本片段转换成了数字坐标(即嵌入向量),我们就需要一个地方来存储这些数据。传统的SQL数据库在查找精确的关键词匹配结果方面表现很好,但在识别“相似的含义”时却力不从心。
向量数据库正是为存储这类数字列表并快速计算它们之间的距离而专门设计的。常见的向量数据库包括ChromaDB、Pinecone、Weaviate、FAISS和Milvus等。
语义搜索与相似性匹配
当用户向我们的聊天机器人提出问题时,我们会将该问题输入到相同的嵌入模型中,这样问题也会被转换成一系列数字。
随后,我们会让向量数据库执行相似性搜索。数据库会查看用户问题的数字表示,然后找出在数学空间中与之位置最接近的存储文本片段。由于距离越近,这些文本片段所包含的信息就越能与用户的问题相关联,因此它们也就更有可能成为回答问题的关键依据。
提示语优化
现在我们已经得到了用户的原始问题以及从数据库中检索到的相关文本片段。接下来我们需要对这些提示语进行进一步优化。在后台,我们会创建一个如下所示的隐藏模板:
“你是一个乐于助人的助手。请仅使用以下提供的背景信息来回答用户的问题。
背景信息:
[在此处插入检索到的文本片段]
问题:
[在此处插入用户提出的问题]
最终的大语言模型响应
我们将这个经过优化的提示语发送给大语言模型。模型会读取背景信息,处理用户的问题,并完全根据这些数据生成相应的回答。
简要总结
一般来说,RAG流程就是这样的:

如何构建一个真正的RAG项目
让我们来构建一个实际应用的RAG系统吧。我们将开发一个能够读取并理解PDF文档的AI聊天机器人。
为了确保整个开发过程完全免费,我们将使用Python、LangChain(一个流行的AI框架)、谷歌的Gemini API(其为开发者提供了丰富的免费资源)以及ChromaDB(一种本地向量数据库)。
注意:在这里我们使用免费的Gemini版本,只是为了方便大家学习,让大家无需花费任何费用就能掌握相关技能。由于LangChain是模块化的,因此之后你只需修改一两行代码,就可以轻松更换为其他生产级别的模型。
项目设置
首先,打开终端或命令提示符,创建一个用于存放项目的新文件夹,然后进入该文件夹:
mkdir my-rag-project
cd my-rag-project
接下来,最好创建一个独立的虚拟环境。这样就能确保为这个项目安装的包不会与你电脑上其他Python项目发生冲突。
要创建并激活虚拟环境,请根据你的操作系统运行相应的命令:
对于macOS和Linux:
python3 -m venv venv
source venv/bin/activate
对于Windows(命令提示符):
python -m venv venv
venv\Scripts\activate
对于Windows(PowerShell):
python -m venv venv
.\venv\Scripts\Activate.ps1
激活虚拟环境后,你会看到终端界面中出现了(venv)这一提示。现在,可以在这个新的虚拟环境中安装所需的库:
python -m pip install --upgrade pip
pip install langchain langchain-google-genai langchain-community chromadb python-dotenv pypdf
你还需要一个谷歌Gemini API密钥。你可以从Google AI Studio免费获取这个密钥。
为了避免为不同的操作系统编写繁琐的终端配置命令,你可以在项目文件夹的根目录下创建一个名为.env的文件,然后将你的API密钥添加到其中,如下所示:
GOOGLE_API_KEY=你的实际API密钥
准备PDF文档
因为这是一个“与PDF文档进行交互”的项目,所以你需要一份样本PDF文件来进行测试。为了简化操作,请下载下面提供的这份现成的样本文档,并将其放入你的项目文件夹中。
之后,你可以在整个教程中使用这份PDF文件来测试上传、解析、嵌入以及聊天功能。
逐步编写RAG代码
在你的项目文件夹中创建一个名为rag_app.py的Python文件。我们不会复制一大段代码,而是会逐块构建这个应用程序,这样就能清楚地了解数据是如何在我们的处理流程中流动的。
步骤1:导入库并设置环境
在文件的顶部,添加必要的库导入语句,并初始化你的环境配置:
import os
from dotenv import load_dotenv
from langchain_community.document_loaders import PyPDFLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain.google_genai import GoogleGenerativeAIEmbeddings, ChatGoogleGenerativeAI
from langchaincommunity.vectorstores import Chroma
from langchain_core.prompts import PromptTemplate
from langchain_core.runnables import RunnablePassthrough
# 从.env文件中加载环境变量
load_dotenv()
我们引入了LangChain的相关模块,用于处理数据的加载、分割、嵌入、存储以及提示生成等功能。load_dotenv()这个函数是必不可少的,因为它会读取我们的.env文件,并将GOOGLE_API_KEY值设置到系统的环境变量中,这样我们的AI模型就可以在不需要手动输入密码的情况下顺利地进行身份验证了。
步骤2:加载PDF文档
接下来,让我们让脚本指向我们之前下载的PDF文件:
print("正在加载PDF文档...")
loader = PyPDFLoader("TechCorp_Official_Employee_Handbook.pdf")
document = loader.load()
print(document[0].page_content)
计算机无法像读取普通文本文件那样直接读取PDF文件,因为PDF文件中包含了复杂的布局信息。PyPDFLoader负责打开文件、去除其中的视觉布局格式,并将原始文本提取出来,转换成LangChain能够处理的格式。
当你运行这个脚本时,终端中应该会显示PDF文件第一页的文本内容。这是一种快速验证PDF文件是否成功被加载,以及PyPDFLoader是否正确地提取出了可读文本的方法。
步骤3:将文本分割成小段
现在原始文本已经存储在内存中了,我们需要将其分割成更小的部分:
print("正在分割文本...")
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=500,
chunk_overlap=50
)
chunks = text_splitter.split_documents(document)
print(chunks[0].page_content)
如果用户提出了一个简单的问题,那么将整整100页的文档发送给大语言模型会非常耗时且效率低下。RecursiveCharacterTextSplitter会将文本分割成大约500个字符长的小段。
chunk_overlap=50这一参数告诉文本分割工具,在相邻片段的开头重复使用前一个片段的最后50个字符。这样有助于保持片段之间的逻辑连贯性,从而避免句子或观点被突然截断。
如果没有设置重叠部分,位于片段边界附近的重要信息可能会被分开,从而导致检索结果的不准确。通过确保相邻片段之间存在一小段共享内容,模型就能更好地理解文档的连续性,进而获得更可靠的搜索结果和更高质量的响应。
当你运行脚本后,终端中应该会显示出第一个文本片段的内容。
步骤4:创建嵌入向量并初始化向量数据库
现在我们已经准备好了这些文本片段,接下来会将它们转换成向量坐标并保存到本地:
print("正在创建向量数据库...)")
embeddings = GoogleGenerativeAIEmbeddings(model="models/embedding-001")
vector_db = Chroma.from_documents(
documents=chunks,
embedding=embeddings,
persist_directory="./chroma_db"
)
这就是RAG技术背后的数学原理。GoogleGenerativeAIEmbeddings会将原始文本片段转换成代表其含义的数字列表,然后我们把这些数字和片段传递给Chroma,它会把它们存储在硬盘上的chroma_db文件夹中,这样后续进行数学运算时就能快速获取所需信息。
步骤5:配置检索器与提示模板
现在我们需要一个机制来查询数据库,并且还需要一个结构来存储我们的指令:
# 将数据库配置为文档检索器
retriever = vector_db.as_retriever(search_kwargs={"k": 2})
# 定义大语言模型的提示模板
template = """
使用以下检索到的上下文内容来回答问题。如果不知道答案,就直接说“不知道”。
请使用最多三句话进行回答,并保持答案简洁。
上下文:{context}
问题:{question}
答案:"""
prompt = PromptTemplate.from_template(template)
vector_db.as_retriever()将向量数据库转换成了一个检索器对象,该对象能够搜索存储在数据库中的嵌入向量,并返回与用户查询最相关的文本片段。我们将k=2作为参数设置给检索器,这样它就会只返回与问题最相关的前两个片段,从而保证查询结果的准确性和效率。
提示模板实际上为模型提供了隐藏的指令。当用户提出问题时,LangChain会自动将{context}替换为检索到的文本片段,将{question}替换成用户的实际提问内容。此外,这个模板还起到了一种“安全防护”的作用——通过明确要求模型在缺乏相关信息时回答“不知道”,我们可以有效抑制模型生成虚假答案的倾向。
步骤6:初始化大语言模型并构建RAG链
接下来,我们需要将我们的语言模型与执行流程连接起来:
# 初始化Gemini模型
llm = ChatGoogleGenerativeAI(model="gemini-1.5-flash", temperature=0)
# 辅助函数,用于将检索到的文档片段合并成一段连续的文本
def format_docs(docs):
return "\n\n".join(doc.page_content for doc in docs)
# 使用LangChain表达式语言将所有组件连接起来
rag_chain = (
{"context": retriever | format_docs, "question": RunnablePassthrough()}
| prompt
| llm
)
我们使用gemini-3.5-flash模型,并将temperature参数设置为0,这样模型就会只提供事实性信息和进行分析,而不会产生创造性内容。
检索器会以结构化对象的形式返回多份文档片段。format_docs函数通过将这些片段的page_content连接起来,将它们合并成一段连续的文本。这一步是必要的,因为提示系统需要的是一段清晰、易于阅读的文本,而不是一系列文档对象。
最后,我们使用LangChain表达式语言将所有组件连接起来。当有问题输入时,系统会先将其传递给检索器,然后将处理后的文本结果传递给提示处理模块,最终将生成的结果直接发送给大语言模型进行处理。
步骤7:使用问题来调用这个流程
现在,让我们执行这个流程,并将结果输出到控制台:
user_question = "我哪些天可以居家工作?"
print(f"\n问题:{user_question}")
response = rag_chain.invoke(user-question)
print(f"答案:{response.content}")
这就是神奇的地方。invoke命令会触发我们刚才构建的整个流程。运行这段代码后,控制台将会输出如下内容:
正在加载PDF文档…
对文本进行分割处理…
创建向量数据库…
问题:我哪些天可以居家工作?
答案: [{'type': 'text', 'text': '您可以在周二和周四居家工作。您的部门经理也可能批准您在其他时间采取远程办公方式。', 'extras': {'signature': 'Eo0JCooJAQw51seue7vZT7Vby90GMDLhtOBWLKm5UjfEro7f8dRoKC0KAIHxSqQSLXq0s3kf6yfzTsgaUMFiNd0fnwtNSNoApzcZ7huRD8iq+f+xomoXGhmFYClnLApHUKtOLykICluJnM1j6DfYGaVHKLqU0MF4+Fng9CdqXVqPgN9HcfJEvSpeMAc9vTYENj07s8N6MidlMvMt1w0fl4GCjxAZXyEngdU4kGfjUqaKyjjCQ9yLFeoXrV55pqZdkElLxXEK4ZWNnMGh5NDqGmt2b0kMG4KoCdunUltBr1ctV15rZ+724T0qnjDvI+pIgp/ZtKa423gaVXSkSmdvSePEog38blJ2dgjtZg72XF5xlh45Yv06fZVu7e60ZB1sTn4W8iWuYGQ61i/xCN6xCX/e3SuitjwQoHSlEe/iuoaNf5BXhdp87TUyQTawiY+qIZjgWz2AMLUbMcOvns/0iFt6jpUkXr/dO4eYF39UCosrbWC5TZQp2gllNQ6mlrczTAKqe8mPZwmBVuTJ3kx3q+SsVROln584EdD94IxXrgLXhuLkbR9ub0qyvjBfAmIfvUEK5pcaBCGydQvheH9wsIvAOG1kspMb/wqjAv/mpmii8J9vztSvM9PR9v7L3YLu8vcANol80w2PfeHhyWUJWit8R58kKd7HHor5GJhA436x+tCukIlBq2oTcob+ydxVJydA12pRsiuw4kYkEIU8nr5yCiIwjYCDtVm6Ws0RUnhyk5u+dRONPZ6g+mfBShKCnahcIMzzJpXznmPXvmP2C96uD64SGTI6L86EMlLEz06/cTJTabgqAYqe2AhERgnYc/4d0XabQOkzvDmBKMr5/LOAt3ZZg7X4PIuefEwxx0eB60gLROefcbbu8k+KPazqFsDP/YA/aPyAxyss/6V43EID0amJcDA81LKJzazL9KnclefQZrN9viIwteMaV04IIlx+Ynk1vZi/LVgWiFuDVWF3Ql2luY4KwFpfFDxQ728gkrhvUdTBrfUeKRSLV1W4ox6I7ogo0e9i7db2lkOQljctGs3Km3hWu4JOkH+YzLNmcDHMF3imfgQH5Ml99H9PXh1ScBjq47MXKzJPdHijkY5ZRSjceEIlKEGv8afQO60NB8lk1MQAGwd+CxqIwVg11N8q9EFSwdJmVVmoyM1nINGJERSKhKOrkqBsOELfpKDjv14tuNgDUy4wdtuxn8C4tJBKvN8t/hrW/Z65VoBGdMwA08sRSV6Fp5l/gSdYeB9yA/Lx/VGkgVqaP5tU73XrE/XO8ysJ/kgRDXiTvsg+2uayU1Q9PfKFAawopslwybCHtdOwaVgsRdA5R4f1NIkPoP/sX+iBxyR0kKg6v4RRAj851WifM2fQ8Vsw5dtFSeh/4TfYg1GCCCDNT4JwrtI8fqcF+qMQqUb+oUqoyzjzFqqSRxXcyqHXOLV9V9C6yWYmZ3TSY043WL9L4kGGJGxFHD5VWG77Quiy+rHWGO13LOc5EBKIO05sg1xnI88QQTUgkxwJeuntytIy3f3pfMVrFYFkvi8w5LzL4RK68+4HMg=='}}]
像谷歌的Gemini这样的现代大型语言模型是多模态的。这意味着它们不仅能读取和生成纯文本,还能同时处理图像、视频和音频数据。因此,当LangChain与谷歌集成时,它返回的并不总是简单的文本字符串,而是一个内容块列表。
在输出结果中,AI不仅会返回你输入的文本,还会附带一个包含签名的extras字典。这个签名是谷歌用于追踪AI安全性、验证元数据以及分析思考过程的一种内部数据。
如果你想得到一份清晰、便于人类阅读的文本,只需从该列表中提取text字段即可。你可以修改你的代码,让程序在检测到返回的是列表时自动提取文本:
# 如果Gemini返回的是内容块列表,就对输出结果进行处理
if isinstance(response.content, list):
clean_answer = response.content[0]['text']
else:
clean_answer = response.content
print(f"答案: {clean_answer}")
现在,你的输出结果将会是这样的:
问题:我可以在哪些日子在家工作?
答案:你可以在周二和周四在家工作。你的部门经理也可能批准你享受更多的远程办公灵活性。
步骤8:让脚本具备对话功能
目前,我们的脚本只是固定地输入一个问题、输出答案,然后立即结束运行。但在实际使用中,人们希望能够自然地与这些工具进行交流。让我们修改脚本,让它能够在终端中持续运行,这样你就可以随时提出问题而无需重新启动程序。
将代码的最后部分替换为一个简单的while循环:
# 实现连续对话功能
print("\n--- PDF聊天机器人已启动 ---")
print("输入'exit'或'quit'即可退出.")
while True:
# 1. 等待用户输入问题
user_question = input("\n你的问题是:")
# 2. 允许用户随时结束对话
if user_question.lower() in ['exit', 'quit']:
print("聊天机器人即将关闭。再见!")
break
# 3> 将问题传递给RAG模型进行处理
response = rag_chain.invoke(user-question)
# 4> 整理输出结果
if isinstance(response.content, list):
clean_answer = response.content[0]['text']
else:
clean_answer = response.content
# 5> 在控制台显示最终答案
print(f"答案: {clean_answer}")
通过使用Python的input()函数,并将其放在一个无限循环while True中,我们可以让脚本持续运行。PDF文件内容和向量数据库会一直保留在计算机内存中,这样你就可以随时连续提出问题。这样一来,你的脚本就从一个静态演示工具变成了一个完全交互式的AI辅助工具!
以下是一个示例运行过程:

完整代码
import os
from dotenv import load_dotenv
from langchain.community.document_loaders import PyPDFLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain.google_genai import GoogleGenerativeAIEmbeddings, ChatGoogleGenerativeAI
from langchain_community.vectorstores import Chroma
from langchain_core.prompts import PromptTemplate
from langchain_core.runnables import RunnablePassthrough
# 从.env文件中加载环境变量
load_dotenv()
print("正在加载PDF文档...")
loader = PyPDFLoader("TechCorp_Official_Employee_Handbook.pdf")
document = loader.load()
# print(document[0].page_content)
print("对文本进行分割...")
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=500,
chunk_overlap=50
)
chunks = text_splitter.split_documents(document)
# print(chunks[0].page_content)
print("正在创建向量数据库...")
embeddings = GoogleGenerativeAIEmbeddings(model="gemini-embedding-001")
vector_db = Chroma.fromdocuments(
documents=chunks,
embedding=embeddings,
persist_directory="./chroma_db"
)
# 配置数据库以作为文档检索器使用
retriever = vector_db.as_retriever(search_kwargs={"k": 2})
# 定义用于大型语言模型的隐藏提示结构
template = """
利用以下检索到的上下文来回答问题。如果不知道答案,就直接说明“不知道”。请使用最多三句话进行回答,并保持答案简洁。
上下文:{context}
问题:{question}
答案:
"""
prompt = PromptTemplate.from_template(template)
# 初始化Gemini模型
llm = ChatGoogleGenerativeAI(model="gemini-3.5-flash", temperature=0)
# 辅助函数,用于将分割后的文本块合并成一段连续的文字
def format_docsdocs):
return "\n\n".join(doc.page_content for doc in docs)
# 使用LangChain表达式语言将所有组件连接在一起
rag_chain = (
{"context": retriever | format_docs, "question": RunnablePassthrough()}
| prompt
| llm
)
"""
user_question = "我哪些天可以居家工作?"
print(f"\n问题:{user-question}")
response = rag_chain.invoke(user_question)
# print(f"答案:{response.content}")
# 如果Gemini返回的是一个包含多个文本块的列表,就需要对输出进行整理
if isinstance(response.content, list):
clean_answer = response.content[0]['text']
else:
clean_answer = response.content
print(f"答案:{clean_answer}")
"""
# 进入连续对话模式
print("\n--- PDF聊天机器人已启动 ---")
print("输入‘exit’或‘quit’即可退出.")
while True:
# 1. 等待用户输入问题
user_question = input("\n您的问题是:")
# 2. 允许用户退出循环并关闭程序
if user-question.lower() in ['exit', 'quit']:
print("聊天机器人即将关闭。再见!")
break
# 3. 将用户的问题传递给RAG链进行处理
response = rag_chain.invoke(user_question)
# 4. 整理输出结果
if isinstance(response.content, list):
clean_answer = response.content[0]['text']
else:
clean_answer = response.content
# 5. 在控制台显示最终答案
print(f"答案:{clean_answer}")
将聊天机器人从终端中分离出来
一旦你的终端聊天机器人能够正常运行,你可能就会想要为它提供一个直观的可视化界面。在Python中,实现这一目标最简单的方法就是使用一个名为Gradio的开源库。Gradio内置了ChatInterface功能,只需添加三行代码,就能将你现有的RAG代码封装起来,并自动在浏览器中生成一个美观的、类似ChatGPT风格的网页界面。强烈推荐你把这个项目作为你的下一个小项目来尝试。
完整的数据处理流程
为了真正加深对你的理解,让我们一起来梳理一下系统中单个用户问题从提出到得到处理的整个生命周期:

执行流程的详细分解
-
请求开始:用户通过我们的控制台提出一个文本问题:“我应该享受多少天的假期?”就在这一刻,我们的应用程序代码开始掌控程序的执行流程。
-
文本到向量的转换:计算机无法直接利用原始的文本字符来计算相似度。因此,我们的应用程序会快速发起网络请求,将原始问题数据传递给Google Embedding模型。该模型会将这些文本转换为一系列数字,这些数字在数学上能够代表用户的真实意图。
-
数据库中的相似度检测:应用程序会将这些数字直接传送到ChromaDB系统中。ChromaDB会扫描本地硬盘中存储的PDF文件内容,通过数学计算来找出与用户问题最匹配的文本片段。在这个例子中,系统找到了其中提到“20天带薪假期”的那段文字,因为它的数值与用户问题的数据最为接近。
-
提示语的生成:ChromaDB会将这些相关文本片段返回给我们的应用程序代码。代码会自动填充提示语模板,将提取出的文本片段放入{context}字段中,而用户的原始问题则会被放在{question}字段中。
-
最终结果的生成:最后,我们的应用程序会将这些处理后的数据发送给Gemini LLM模型。由于配置中设置了温度参数为0,因此该模型会纯粹地作为阅读理解工具来使用。它会根据提供的上下文信息生成一段简洁的回答,并将其呈现给用户。
常见的RAG开发问题
构建一个简单的RAG应用程序其实很容易,但要让它在实际环境中完美运行却非常困难。以下是一些工程师们经常遇到的问题以及相应的解决方法。
1. 不恰当的分块处理
如果分块的大小过大,其中会包含一些无关信息,这些信息会令大语言模型感到困惑;而如果分块太小,则会丢失重要的上下文信息。工程师可以通过尝试不同的分块大小,或采用语义分块的方法(即按照整句话或整个段落来划分内容,而不是严格依据字符数量进行分割),来解决这个问题。
2. 无关信息的检索
有时,语义搜索也会出现失败的情况。例如,如果用户搜索“Apple”,期望得到关于水果的信息,但数据库中却只有这家科技公司的相关数据,那么系统就会返回与科技公司相关的文档。工程师可以通过调整嵌入模型或添加关键词搜索规则来解决这个问题。
3. 信息错觉
即使使用了RAG技术,大语言模型也可能会忽略检索到的上下文信息,而仅依赖其训练过程中积累的记忆。为了解决这个问题,工程师会为提示模板制定严格的规则,比如“只能使用提供的文本”。
4. 延迟问题
RAG技术需要执行嵌入网络的调用、数据库搜索以及大语言模型的处理,这些操作都会耗费一定的时间。工程师可以通过使用速度更快的本地部署的嵌入模型,或对常见查询结果进行缓存,来优化这一过程。
5>数据过时问题
即使人力资源部门更新了公司政策的PDF文件,向量数据库中仍然会保留旧的数据。这样一来,人工智能系统就会给出过时的答案。为了解决这个问题,工程师会建立更新流程,每当源文件发生变化时,就自动删除旧的向量数据并嵌入新的数据。
高级RAG技术概念
一旦掌握了基本的RAG技术,人工智能工程领域就会展现出更多高级的应用方法。
混合搜索
向量数据库在理解文本含义方面表现优异,但在查找具体的ID编号或名称时却不够高效。混合搜索结合了传统的关键词搜索方式(类似于在SQL数据库中查询)与语义向量搜索技术,从而充分利用了两者的优势。
重新排序
有时,向量数据库会返回10个分块结果,但其中最好的答案却意外地被排在列表的最后。重新排序功能会使用第二个专门设计的AI模型来阅读检索到的这些分块内容,并根据它们之间的相关性对它们进行排序,然后再将排序后的结果发送给大语言模型进行处理。
智能决策型RAG
智能决策型RAG技术不会强制系统每次都执行检索操作,而是让一个AI“代理”来决定是否真的需要进行搜索。例如,当你说“Hello”时,这个代理会直接回应“Hi”,而当你提出一个复杂的问题时,它才会决定去查询数据库。
图谱驱动的RAG
与将文本简单分割成独立的分块不同,图谱驱动的RAG技术会提取出文本中的实体(如人物、地点、概念等),并绘制出这些实体在知识图谱中的相互关系。对于那些包含复杂关联关系的数据集来说,这种技术显得尤为强大。
多模态RAG
传统的检索增强生成技术仅能处理文本数据,而多模态检索增强生成技术则能够处理图像、图表和音频文件,这使得用户可以提出诸如“第4页上的那张图表表示什么意义?”这样的问题。
结语
检索增强生成技术正是将强大的推理引擎与可靠的事实性知识联系起来的一座桥梁。
对于软件工程师来说,理解检索增强生成技术已不再是可选项。如今,几乎每一款企业级软件产品都包含了这一技术成分。通过了解分块处理、嵌入技术、向量数据库以及提示信息优化机制之间的协同作用,你就能够揭开现代人工智能技术的神秘面纱。
你的下一步应该是基于我们今天编写的代码进行进一步开发。试着将PDF文件加载器应用于你的简历、学校教材或财务报告上。当你亲眼看到自己编写的代码能够针对个人数据提出问题并给出答案时,你就会真正领会到人工智能工程技术的强大威力了。