深入 RAG 与向量数据库:generative-ai-for-beginners 第 15 课检索增强生成的完整实现
【免费下载链接】generative-ai-for-beginners21 Lessons, Get Started Building with Generative AI项目地址: https://gitcode.com/GitHub_Trending/ge/generative-ai-for-beginners
本篇基于 generative-ai-for-beginners 课程第 15 课《Retrieval Augmented Generation (RAG) and Vector Databases》,系统讲解检索增强生成(RAG)的工作原理、向量数据库的选型与搭建、文本分块与向量化(embedding)、向量检索与重排序,并给出从知识库构建到 LLM 问答闭环的完整可运行代码,以及配套的 Jupyter Notebook 与评估方法。读完后你将能够独立搭建一个基于自有数据做“事实锚定”(grounding)的 RAG 应用,并理解每个环节在课程配套代码中的真实实现。
课程概述与学习目标
在课程第 8 课(搜索应用)中,我们初步了解了如何把自有数据接入大语言模型(LLM)。第 15 课在此基础上深入三个主题:
- RAG 简介:它是什么,为什么在 AI 应用中需要使用它;
- 向量数据库:理解其原理,并为应用创建一个向量数据库;
- 实战示例:如何把 RAG 集成到一个真实应用中。
学完本课,你将能够:
- 解释 RAG 在数据检索与处理中的意义;
- 搭建 RAG 应用,并把自己的数据锚定(ground)到 LLM 上;
- 在 LLM 应用中高效集成 RAG 与向量数据库。
场景设定:用自有数据增强 LLM
本课的假设场景是一家教育创业公司:把学员的学习笔记接入聊天机器人,让机器人能够基于笔记回答各个学科的问题。借助这些笔记,学员可以更好地理解各主题内容,从而更高效地备考。用户可以基于笔记生成练习题(quiz)、复习闪卡(flash card),并把内容总结成简洁的综述。
实现该场景用到以下组件:
Azure OpenAI:用于构建聊天机器人的 LLM;AI for Beginners 课程中的“神经网络”章节:作为锚定 LLM 的数据源;Azure AI Search与Azure Cosmos DB:用于存储数据并创建搜索索引的向量数据库。
课程配套的示例笔记本 notebook-rag-vector-databases.ipynb 中,知识源正是仓库内的三份 Markdown 文件:data/perceptron.md(感知器入门)、data/frameworks.md(神经网络框架)与 data/own_framework.md(多层感知器)。笔记本把它们作为“私有知识库”来构建索引,验证问题“what is a perceptron?”。
什么是 RAG(检索增强生成)
由 LLM 驱动的聊天机器人会处理用户提示词并生成回答。它被设计为交互式系统,可以就广泛的话题与用户交流。但它的回答受限于两方面:所提供的上下文,以及其基础训练数据。例如 GPT-4 的知识截止于 2021 年 9 月,它缺乏该时期之后事件的知识;此外,用于训练 LLM 的数据不包含机密信息,比如个人笔记或公司的产品手册。
RAG 是如何工作的
如果你要部署一个“从笔记生成测验”的聊天机器人,就需要连接到一个知识库——这正是 RAG 的价值所在。RAG 的运行流程分为四个阶段:
- 知识库(Knowledge base):检索之前,文档需要被摄取并做预处理,通常包括:把大文档切分为更小的块(chunk)、把文本转换为向量嵌入(embedding)、再存储到数据库中。
- 用户提问(User Query):用户提出一个问题。
- 检索(Retrieval):当用户提问时,嵌入模型从知识库中检索相关信息,以提供将被拼入提示词的额外上下文。
- 增强生成(Augmented Generation):LLM 基于检索到的数据来增强自己的回答,使生成的响应不仅基于预训练数据,还基于所加入上下文中的相关信息;随后 LLM 向用户返回答案。
RAG 的架构:编码器-解码器
RAG 的架构使用由两部分组成的 Transformer 实现:编码器(encoder)与解码器(decoder)。以用户提问为例:输入文本先被“编码”为捕捉词语语义的向量,这些向量再被“解码”去匹配文档索引,并基于用户查询生成新文本。LLM 使用编码器-解码器两类模型协同生成输出。
按提出 RAG 的经典论文《Retrieval-Augmented Generation for Knowledge Intensive NLP Tasks》所述,实现 RAG 有两种方式:
- RAG-Sequence:使用检索到的文档来预测对用户查询的最佳答案(一次性对整个回答序列做似然比较);
- RAG-Token:使用文档逐个生成下一个 token,再结合检索结果回答用户查询。
为什么要使用 RAG
- 信息丰富度:确保文本响应是最新、准确的,通过访问内部知识库来提升领域特定任务上的表现;
- 减少捏造(幻觉):利用知识库中可验证的数据为用户问题提供上下文,降低模型“编造”的概率;
- 成本效益高:相比对 LLM 做微调(fine-tuning),RAG 在经济上更划算。
构建知识库
本课的应用基于个人数据,即 AI For Beginners 课程中的神经网络章节内容(对应仓库中的data/三份 Markdown 文档)。
向量数据库
与传统数据库不同,向量数据库是一种专门设计用于存储、管理与检索嵌入向量的数据库,它保存文档的数值化表示。把数据拆解为数值嵌入,能让 AI 系统更容易理解和处理这些数据。
为什么必须存向量数据库而不是直接塞给 LLM?因为 LLM 有输入 token 数量的上限,无法把全部嵌入一次性传入。因此需要把文档切分成块(chunk):当用户提问时,只有与问题最相似的嵌入连同提示词一起返回。分块还能显著降低流经 LLM 的 token 数量,从而降低成本。
文档列出的常用向量数据库包括:Azure Cosmos DB、Clarifyai、Pinecone、ChromaDB、ScaNN、Qdrant 和 DeepLake。使用 Azure CLI 创建 Azure Cosmos DB 资源的命令如下:
az login az group create -n <resource-group-name> -l <location> az cosmosdb create -n <cosmos-db-name> -r <resource-group-name> az cosmosdb list-keys -n <cosmos-db-name> -g <resource-group-name>配套笔记本 notebook-rag-vector-databases.ipynb 中展示了与 Cosmos DB 的对接方式:创建资源后,在数据资源管理器(Data Explorer)中新建数据库与容器,然后安装并初始化azure-cosmos客户端:
from azure.cosmos import CosmosClient # Initialize Cosmos Client(凭据来自环境变量) url = os.getenv('COSMOS_DB_ENDPOINT') key = os.getenv('COSMOS_DB_KEY') client = CosmosClient(url, credential=key) # Select database database_name = 'rag-cosmos-db' database = client.get_database_client(database_name) # Select container container_name = 'data' container = database.get_container_client(container_name)也就是说,运行时需要准备COSMOS_DB_ENDPOINT与COSMOS_DB_KEY两个环境变量,分别对应 CLI 创建资源后拿到的端点与密钥。
从文本到嵌入(Embeddings)
存储数据之前,需要先把它转换成向量嵌入。如果处理的是大文档或长文本,可以基于预期查询来切块。切块可以按句子级,也可以按段落级。由于块的含义依赖于周围词,还可以为块追加额外上下文,例如加上文档标题,或包含块前后的一些文本。课程给出的切块实现如下:
def split_text(text, max_length, min_length): words = text.split() chunks = [] current_chunk = [] for word in words: current_chunk.append(word) if len(' '.join(current_chunk)) < max_length and len(' '.join(current_chunk)) > min_length: chunks.append(' '.join(current_chunk)) current_chunk = [] # 如果最后一个块未达到最小长度,也照样加入 if current_chunk: chunks.append(' '.join(current_chunk)) return chunks切块之后,可以用不同的嵌入模型对文本做向量化。可选模型包括:word2vec、OpenAI 的 ada-002、Azure Computer Vision 等等。模型选择取决于:所使用的语言、被编码内容的类型(文本/图片/音频)、可编码的输入规模,以及嵌入输出的长度。
用 OpenAItext-embedding-ada-002模型嵌入单词 “cat” 的结果示意如下(笔记本中对 “cat” 调用create_embeddings("cat")即为同样的演示):
笔记本中嵌入调用的真实实现值得留意——它基于 Azure OpenAI 的 v1 端点(Responses API 底座)配置客户端,并从环境变量读取端点、密钥与部署名:
from openai import OpenAI endpoint = os.getenv("AZURE_OPENAI_ENDPOINT") client = OpenAI( api_key=os.getenv("AZURE_OPENAI_API_KEY"), base_url=f"{endpoint.rstrip('/')}/openai/v1/", ) embeddings_deployment = os.getenv("AZURE_OPENAI_EMBEDDINGS_DEPLOYMENT") chat_deployment = os.getenv("AZURE_OPENAI_DEPLOYMENT")def create_embeddings(text, model=None): # 为每个文档块创建嵌入 model = model or embeddings_deployment embeddings = client.embeddings.create(input=text, model=model).data[0].embedding return embeddings从源码结构看,整个笔记本依赖五个关键环境变量:AZURE_OPENAI_ENDPOINT、AZURE_OPENAI_API_KEY、AZURE_OPENAI_EMBEDDINGS_DEPLOYMENT、AZURE_OPENAI_DEPLOYMENT,以及 Cosmos DB 侧的COSMOS_DB_ENDPOINT、COSMOS_DB_KEY。运行前需先在 Azure 门户部署好嵌入模型与聊天模型,再把这些值配置到环境中。
笔记本中对数据的实际处理流程是:先把data/下三份文档读入 pandas DataFrame(path+text两列),再以split_text(x, 400, 300)为每个文档生成块列表(最大 400 字符、最小 300 字符),随后用df.explode('chunks')把块列表展开为独立行,最后逐块调用create_embeddings把embeddings列回填进 DataFrame。这正是上文split_text函数在真实项目中的参数取值与数据流。
检索与向量搜索
当用户提问时,检索器(retriever)会先用查询编码器把问题转换成向量,然后在文档搜索索引中查找与输入相关的相关向量;完成后,它把输入向量与文档向量都还原为文本,并交给 LLM 处理。
检索(Retrieval)
检索发生在一个索引中快速查找满足搜索条件的文档。检索器的目标是拿到能为 LLM 提供上下文、把模型锚定到你数据上的文档。数据库中的搜索方式主要有三种:
- 关键词搜索(Keyword search):用于文本字面匹配;
- 向量搜索(Vector search):用嵌入模型把文档从文本转成向量表示,实现基于词义的语义搜索——检索时查询那些向量表示与用户问题最接近的文档;
- 混合搜索(Hybrid):关键词搜索与向量搜索的组合。
检索的一个难点是:当数据库中没有与查询相似的记录时,系统只能返回它能拿到的“最好”的信息。缓解手段包括设定相关性的最大距离阈值,或使用混合搜索。本课采用混合搜索,即向量搜索与关键词搜索的组合,并把数据存成包含chunks(文本块)与embeddings(向量)等列的数据框。
向量相似度
检索器会在知识库中寻找彼此接近(最近邻)的嵌入,因为接近意味着文本相似。用户提出查询时,查询先被嵌入,再与相似嵌入匹配。衡量向量相似度的常用度量是余弦相似度(cosine similarity),它基于两个向量之间的夹角。
其他可选度量包括:
- 欧氏距离(Euclidean distance):向量端点之间的直线距离;
- 点积(Dot product):度量两个向量对应元素乘积之和。
搜索索引
执行检索前,需要为知识库构建搜索索引。索引保存嵌入向量,即使在大型数据库中也能快速取出最相似的块。课程给出的本地索引实现如下:
from sklearn.neighbors import NearestNeighbors embeddings = flattened_df['embeddings'].to_list() # 创建搜索索引 nbrs = NearestNeighbors(n_neighbors=5, algorithm='ball_tree').fit(embeddings) # 查询索引可使用 kneighbors 方法 distances, indices = nbrs.kneighbors(embeddings)其中n_neighbors=5表示每次查询返回 5 个最近邻,algorithm='ball_tree'指定使用球树算法加速近邻查找。笔记本在构建索引后,还会把indices与distances回写到 DataFrame 中,方便后续逐行查看每个块命中了哪些邻居。
重排序(Re-ranking)
查询数据库之后,可能需要按相关性对结果重新排序。重排序模型利用机器学习把搜索结果按相关性从高到低排列。使用 Azure AI Search 时,重排序由语义重排序器(semantic reranker)自动完成。基于最近邻的重排序示例:
# 找到最相似的文档 distances, indices = nbrs.kneighbors([query_vector]) index = [] # 打印最相似的文档 for i in range(3): index = indices[0][i] for index in indices[0]: print(flattened_df['chunks'].iloc[index]) print(flattened_df['path'].iloc[index]) print(flattened_df['distances'].iloc[index]) else: print(f"Index {index} not found in DataFrame")这段代码的意图是:对查询向量做一次kneighbors检索,取前几名邻居,打印出对应的文本块(chunks)、来源文件(path)与距离(distances),从而直观验证“哪些文档块与查询最相关”。
把所有环节串起来:RAG 聊天机器人
最后一步是把 LLM 加进来,让回答锚定在自有数据上。课程文档给出的完整实现如下:
user_input = "what is a perceptron?" def chatbot(user_input): # 把问题转换为查询向量 query_vector = create_embeddings(user_input) # 找到最相似的文档 distances, indices = nbrs.kneighbors([query_vector]) # 把检索到的文档加入查询以提供上下文 history = [] for index in indices[0]: history.append(flattened_df['chunks'].iloc[index]) # 合并历史上下文与用户输入 history.append(user_input) # 构造消息对象 messages=[ {"role": "system", "content": "You are an AI assistant that helps with AI questions."}, {"role": "user", "content": "\n\n".join(history) } ] # 使用响应式 API 生成回答 response = client.responses.create( model="gpt-4o-mini", temperature=0.7, max_output_tokens=800, input=messages, store=False, ) return response.output_text chatbot(user_input)调用链可以概括为:create_embeddings(user_input)→nbrs.kneighbors([query_vector])→ 用命中的块拼上下文 → 组装messages→ 调用 LLM 返回response.output_text。
这里有一个值得注意的源码级细节:如果对照 notebook-rag-vector-databases.ipynb 的最后一个聊天单元,它同样构建了history,但组装消息时user消息的内容只传入了history[-1](即用户问题本身),从源码结构看,检索到的上下文块并没有真正拼进最终 prompt——这是一个容易踩的坑。正确做法应如上文文档版本,用"\n\n".join(history)把检索片段与问题一起送入模型,否则 RAG 的“增强”就名存实亡了。
此外,笔记本中 LLM 调用使用的是 Responses API(client.responses.create),参数为model=chat_deployment(来自AZURE_OPENAI_DEPLOYMENT环境变量)、temperature=0.7、max_output_tokens=800、store=False,返回结果为response.output_text字符串,与旧版 Chat Completions 接口(choices[0].message)在取值方式上不同。
应用评估
评估指标
课程给出四个定性评估维度:
- 回答质量:确保输出自然、流畅、接近人类表达;
- 数据锚定性(Groundedness):评估回答是否确实来自所提供的文档;
- 相关性(Relevance):评估回答是否与所提问题匹配且相关;
- 流畅性(Fluency):回答在语法上是否通顺合理。
基于平均精度均值(MAP)的量化评估
配套笔记本在“Testing and evaluation”一节补充了量化手段:用 scikit-learn 的average_precision_score计算 Mean Average Precision(MAP)来度量检索/回答的相关性。其做法是为每个查询准备“相关回答”与“不相关回答”两组,把 RAG 应用生成的回答与候选列表比对,构造二值标签与预测得分,再对多个测试用例(如“What is a perceptron?”“What is machine learning?”“What is deep learning?”“What is a neural network?”)的平均精度求均值:
from sklearn.metrics import average_precision_score test_cases = [ { "query": "What is a perceptron?", "relevant_responses": ["A perceptron is a type of artificial neuron.", ...], "irrelevant_responses": ["A perceptron is a type of fruit.", ...] }, # ... 更多测试用例 ] total_average_precision = 0 for test_case in test_cases: query = test_case["query"] response = chatbot(query) all_responses = test_case["relevant_responses"] + test_case["irrelevant_responses"] true_labels = [1] * len(test_case["relevant_responses"]) + [0] * len(test_case["irrelevant_responses"]) predicted_scores = [1 if resp == response else 0 for resp in all_responses] average_precision = average_precision_score(true_labels, predicted_scores) total_average_precision += average_precision mean_average_precision = total_average_precision / len(test_cases)这意味着:一个 RAG 应用不仅要做定性检查,还可以为每个查询建立“相关/不相关”测试集,用 MAP 持续追踪检索质量的变化。
RAG 与向量数据库的典型用例
- 问答系统(Q&A):把公司数据锚定到一个聊天应用中,供员工提问;
- 推荐系统:构建匹配最相似项的系统,例如电影、餐厅等;
- 聊天机器人服务:存储对话历史,并基于用户数据个性化会话;
- 基于向量嵌入的图片搜索:在图像识别与异常检测场景中很有用。
总结与课后任务
本课覆盖了 RAG 的基础领域:从把数据加入应用、用户查询,到最终输出。如果想简化 RAG 的搭建,可以使用 Semantic Kernel、LangChain 或 Autogen 等框架。
课后任务(Assignment):
- 用你选择的框架为应用构建一个前端界面;
- 使用 LangChain 或 Semantic Kernel 等框架,重新实现一遍你的 RAG 应用。
延伸学习
- 课程英文原文:15-rag-and-vector-databases/README.md
- 完整可运行笔记本:15-rag-and-vector-databases/notebook-rag-vector-databases.ipynb
- 知识库样例数据:data/perceptron.md、data/frameworks.md、data/own_framework.md
- 本课为“搜索应用”(第 8 课)的进阶篇,可结合 08-building-search-applications/README.md 一起阅读,理解从“把数据接入 LLM”到“完整 RAG 流水线”的演进路径。
【免费下载链接】generative-ai-for-beginners21 Lessons, Get Started Building with Generative AI项目地址: https://gitcode.com/GitHub_Trending/ge/generative-ai-for-beginners
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考