Files
ai-agent-dev/11_检索增强.ipynb
2026-07-08 10:09:42 +08:00

506 lines
17 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# 11 检索增强\n",
"\n",
"## 学习目标\n",
"1. 理解 RAGRetrieval-Augmented Generation检索增强生成的核心原理\n",
"2. 掌握文档加载、文本分割、向量化的基本流程\n",
"3. 学会使用 LangChain 构建简单的 RAG 问答链\n",
"4. 理解检索器Retriever在智能体中的作用\n",
"5. 能够处理本地文本数据并基于其回答问题"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 1. 为什么需要 RAG\n",
"\n",
"大语言模型有两个明显局限:\n",
"\n",
"- **知识过时**:模型训练数据有截止日期,不知道最新信息\n",
"- **容易产生幻觉**:对未训练过的问题可能编造答案\n",
"\n",
"**RAG** 通过在回答前先从外部知识库中检索相关信息,把检索到的内容作为上下文喂给模型,从而:\n",
"\n",
"- 让模型基于最新、准确的资料回答\n",
"- 减少幻觉\n",
"- 实现私有知识库问答(如公司文档、个人笔记)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 2. RAG 的核心流程\n",
"\n",
"一个完整的 RAG 系统通常包含以下步骤:\n",
"\n",
"```\n",
"1. 加载文档 -> DocumentLoader\n",
"2. 分割文档 -> TextSplitter\n",
"3. 文本向量化 -> Embeddings\n",
"4. 存入向量库 -> VectorStore\n",
"5. 用户提问 -> Query\n",
"6. 检索相关片段 -> Retriever\n",
"7. 拼接上下文 -> Context\n",
"8. 生成回答 -> LLM\n",
"```\n",
"\n",
"其中第 1-4 步是「离线」的索引阶段,第 5-8 步是「在线」的查询阶段。"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 3. 环境准备\n",
"\n",
"本节课会用到文档加载、文本分割组件。如果你的虚拟环境中还没有安装,请先执行:\n",
"\n",
"```powershell\n",
"pip install langchain-community langchain-text-splitters -i https://pypi.tuna.tsinghua.edu.cn/simple\n",
"```\n",
"\n",
"> 如果暂时不想安装,本课件也提供了用 Python 原生代码实现的备用方案。"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 4. 准备示例文档\n",
"\n",
"先创建一个示例文本文件,后面会用它来演示 RAG 流程。"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"sample_text = \"\"\"\n",
"人工智能Artificial Intelligence简称 AI是计算机科学的一个分支旨在创建能够执行通常需要人类智能才能完成的任务的系统。\n",
"\n",
"机器学习是人工智能的一个重要子领域。它通过数据训练模型,使计算机能够从经验中学习,而无需进行明确的编程。\n",
"\n",
"深度学习是机器学习的一个分支,使用多层神经网络来模拟人脑的工作方式。深度学习在图像识别、自然语言处理和语音识别等领域取得了显著成果。\n",
"\n",
"自然语言处理Natural Language Processing简称 NLP是人工智能和语言学的交叉领域研究如何让计算机理解、解释和生成人类语言。\n",
"\n",
"大语言模型Large Language Model简称 LLM是基于深度学习的自然语言处理模型。它们通过在海量文本数据上进行预训练能够生成连贯的文本、回答问题并完成多种语言任务。\n",
"\"\"\"\n",
"\n",
"with open('sample_rag.txt', 'w', encoding='utf-8') as f:\n",
" f.write(sample_text.strip())\n",
"\n",
"print('示例文档已创建sample_rag.txt')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### 5.1 使用 LangChain 的 TextLoader推荐\n",
"\n",
"`TextLoader` 可以自动处理编码、元数据等问题。\n",
"\n",
"> 注意:`langchain-community` 在 LangChain 1.x 中已被标记为弃用,但仍然可用。代码中用 `warnings.filterwarnings('ignore', category=DeprecationWarning)` 过滤了警告。"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import warnings\n",
"\n",
"# langchain-community 在 LangChain 1.x 中已被标记为弃用,但仍然可用\n",
"# 这里过滤掉 DeprecationWarning避免输出中显示警告信息\n",
"warnings.filterwarnings('ignore', category=DeprecationWarning)\n",
"\n",
"from langchain_community.document_loaders import TextLoader\n",
"\n",
"loader = TextLoader('sample_rag.txt', encoding='utf-8')\n",
"documents = loader.load()\n",
"\n",
"print(f'加载了 {len(documents)} 个文档')\n",
"print(f'第一个文档长度:{len(documents[0].page_content)} 字符')\n",
"print(f'元数据:{documents[0].metadata}')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### 5.2 原生 Python 加载方式(备用)\n",
"\n",
"如果暂时不想安装 `langchain-community`,可以直接用 Python 读取文件。"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from langchain_core.documents import Document\n",
"\n",
"with open('sample_rag.txt', 'r', encoding='utf-8') as f:\n",
" text = f.read()\n",
"\n",
"documents = [Document(page_content=text, metadata={'source': 'sample_rag.txt'})]\n",
"print(f'加载了 {len(documents)} 个文档,长度:{len(documents[0].page_content)} 字符')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 6. 文档分割\n",
"\n",
"大模型一次能处理的文本长度有限,而且过长的文档会影响检索精度。因此需要把长文档切分成小块。"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from langchain_text_splitters import RecursiveCharacterTextSplitter\n",
"\n",
"# 创建一个文本分割器\n",
"# chunk_size: 每个块的最大字符数\n",
"# chunk_overlap: 相邻块之间的重叠字符数,用于保持上下文连贯\n",
"splitter = RecursiveCharacterTextSplitter(\n",
" chunk_size=100,\n",
" chunk_overlap=20,\n",
" separators=['\\n\\n', '\\n', '。', '', ' ', '']\n",
")\n",
"\n",
"chunks = splitter.split_documents(documents)\n",
"\n",
"print(f'分割成 {len(chunks)} 个文本块')\n",
"for i, chunk in enumerate(chunks[:3]):\n",
" print(f'\\n--- 第 {i+1} 块 ---')\n",
" print(chunk.page_content[:80] + '...')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### 代码解释\n",
"\n",
"- `chunk_size`:每个文本块的最大长度\n",
"- `chunk_overlap`:相邻块重叠的字符数,避免关键信息被切分断掉\n",
"- `separators`:切分优先级,先按段落切,再按句子切,最后按字符切\n",
"- `RecursiveCharacterTextSplitter` 会尽量在语义边界处切分"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 7. 文本向量化\n",
"\n",
"向量化是把文本转换成数值向量的过程。语义相近的文本,向量距离也更近。\n",
"\n",
"本课件使用 `OpenAIEmbeddings`。如果你的 API 不支持 `qwen3-embedding`,请把 `model` 参数改成你实际可用的 embedding 模型名。"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from langchain_openai import OpenAIEmbeddings\n",
"from dotenv import load_dotenv\n",
"\n",
"load_dotenv()\n",
"\n",
"\n",
"# 创建 embedding 模型\n",
"# 如果你的 API 提供的是其他 embedding 模型名,请修改 model 参数\n",
"embeddings = OpenAIEmbeddings(model='qwen3-embedding')\n",
"\n",
"# 测试向量化\n",
"test_text = '人工智能是计算机科学的分支'\n",
"vector = embeddings.embed_query(test_text)\n",
"\n",
"print(f'文本:{test_text}')\n",
"print(f'向量维度:{len(vector)}')\n",
"print(f'向量前5个值{vector[:5]}')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### 备用方案:使用 FakeEmbeddings 测试流程\n",
"\n",
"如果你暂时没有可用的 embedding 接口,可以用 `FakeEmbeddings` 仅测试 RAG 流程。注意FakeEmbeddings 生成的向量是随机的,检索结果没有实际语义意义。"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from langchain_core.embeddings import FakeEmbeddings\n",
"\n",
"# 仅用于测试流程,向量维度设为 384\n",
"fake_embeddings = FakeEmbeddings(size=384)\n",
"\n",
"fake_vector = fake_embeddings.embed_query('测试文本')\n",
"print(f'Fake 向量维度:{len(fake_vector)}')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 8. 向量存储与检索\n",
"\n",
"把分割后的文本块存入向量数据库。这里使用 `InMemoryVectorStore`,它是一个内存型向量存储,适合学习和测试。"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from langchain_core.vectorstores import InMemoryVectorStore\n",
"\n",
"# 创建内存向量库\n",
"vectorstore = InMemoryVectorStore(embeddings)\n",
"\n",
"# 把文本块加入向量库\n",
"vectorstore.add_documents(chunks)\n",
"\n",
"# 创建检索器k=2 表示返回最相关的 2 个片段\n",
"retriever = vectorstore.as_retriever(search_kwargs={'k': 2})\n",
"\n",
"# 测试检索\n",
"query = '什么是深度学习?'\n",
"retrieved_docs = retriever.invoke(query)\n",
"\n",
"print(f'检索到 {len(retrieved_docs)} 个相关片段:\\n')\n",
"for i, doc in enumerate(retrieved_docs):\n",
" print(f'--- 片段 {i+1} ---')\n",
" print(doc.page_content)\n",
" print(f'来源:{doc.metadata}\\n')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### 代码解释\n",
"\n",
"- `InMemoryVectorStore(embeddings)`:创建内存向量库,使用指定的 embedding 模型\n",
"- `add_documents(chunks)`:把文本块存入向量库\n",
"- `as_retriever(search_kwargs={'k': 2})`:把向量库转成检索器,每次返回前 2 个最相关结果\n",
"- `retriever.invoke(query)`:根据用户问题检索相关文本块"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 9. 构建 RAG 问答链\n",
"\n",
"把检索器和 LLM 组合起来,构建一个完整的 RAG 链。流程是:\n",
"\n",
"1. 接收用户问题\n",
"2. 用检索器找到相关文档\n",
"3. 把文档内容拼接到 Prompt 中\n",
"4. 让 LLM 基于上下文回答"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from langchain_openai import ChatOpenAI\n",
"from langchain_core.prompts import ChatPromptTemplate\n",
"from langchain_core.output_parsers import StrOutputParser\n",
"from langchain_core.runnables import RunnablePassthrough\n",
"\n",
"llm = ChatOpenAI(model='qwen3.6-35b-A3b', temperature=0.1)\n",
"\n",
"# 定义 RAG Prompt 模板\n",
"rag_prompt = ChatPromptTemplate.from_messages([\n",
" ('system', '你是一个知识库问答助手。请根据下面的上下文回答问题,如果上下文没有相关信息,请说「我不知道」。'),\n",
" ('user', '''上下文:\\n{context}\\n\\n问题{question}''')\n",
"])\n",
"\n",
"# 把检索到的文档拼接成字符串\n",
"def format_docs(docs):\n",
" return '\\n\\n'.join([doc.page_content for doc in docs])\n",
"\n",
"# 构建 RAG 链\n",
"rag_chain = (\n",
" {'context': retriever | format_docs, 'question': RunnablePassthrough()}\n",
" | rag_prompt\n",
" | llm\n",
" | StrOutputParser()\n",
")\n",
"\n",
"# 测试\n",
"question = '深度学习和大语言模型有什么关系?'\n",
"answer = rag_chain.invoke(question)\n",
"print('问题:', question)\n",
"print('\\n回答', answer)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### 代码解释\n",
"\n",
"- `{'context': retriever | format_docs, 'question': RunnablePassthrough()}`\n",
" - `context` 分支:用检索器找到相关文档,再用 `format_docs` 拼接成字符串\n",
" - `question` 分支:用 `RunnablePassthrough()` 把原始问题传递下去\n",
"- `rag_prompt`:把 context 和 question 填充到模板中\n",
"- `llm`:让模型基于上下文生成回答\n",
"- `StrOutputParser()`:把 AIMessage 转成字符串"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 10. 完整示例:基于私有文档的问答系统\n",
"\n",
"下面把前面的步骤整合成一个完整的 RAG 系统。"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from langchain_community.document_loaders import TextLoader\n",
"from langchain_text_splitters import RecursiveCharacterTextSplitter\n",
"from langchain_core.vectorstores import InMemoryVectorStore\n",
"from langchain_openai import ChatOpenAI, OpenAIEmbeddings\n",
"from langchain_core.prompts import ChatPromptTemplate\n",
"from langchain_core.output_parsers import StrOutputParser\n",
"from langchain_core.runnables import RunnablePassthrough\n",
"\n",
"# 1. 加载文档\n",
"loader = TextLoader('sample_rag.txt', encoding='utf-8')\n",
"documents = loader.load()\n",
"\n",
"# 2. 分割文档\n",
"splitter = RecursiveCharacterTextSplitter(chunk_size=100, chunk_overlap=20)\n",
"chunks = splitter.split_documents(documents)\n",
"\n",
"# 3. 创建 embedding 和向量库\n",
"embeddings = OpenAIEmbeddings(model='qwen3-embedding')\n",
"vectorstore = InMemoryVectorStore(embeddings)\n",
"vectorstore.add_documents(chunks)\n",
"\n",
"# 4. 创建检索器\n",
"retriever = vectorstore.as_retriever(search_kwargs={'k': 2})\n",
"\n",
"# 5. 创建 LLM 和 Prompt\n",
"llm = ChatOpenAI(model='qwen3.6-35b-A3b', temperature=0.1)\n",
"prompt = ChatPromptTemplate.from_messages([\n",
" ('system', '你是知识库问答助手,请严格根据上下文回答。'),\n",
" ('user', '''上下文:\\n{context}\\n\\n问题{question}''')\n",
"])\n",
"\n",
"def format_docs(docs):\n",
" return '\\n\\n'.join([doc.page_content for doc in docs])\n",
"\n",
"# 6. 构建 RAG 链\n",
"qa_chain = (\n",
" {'context': retriever | format_docs, 'question': RunnablePassthrough()}\n",
" | prompt\n",
" | llm\n",
" | StrOutputParser()\n",
")\n",
"\n",
"# 7. 提问\n",
"questions = [\n",
" '机器学习是什么?',\n",
" '自然语言处理和大语言模型有什么关系?',\n",
" '苹果是什么颜色的?' # 这个问题不在文档中,观察模型是否说不知道\n",
"]\n",
"\n",
"for q in questions:\n",
" print(f'Q: {q}')\n",
" print(f'A: {qa_chain.invoke(q)}\\n')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 11. RAG 系统的优化方向\n",
"\n",
"实际应用中RAG 系统还有很多优化空间:\n",
"\n",
"| 优化方向 | 说明 |\n",
"| --- | --- |\n",
"| **更好的分块策略** | 按语义、按段落、按标题切分,避免切断关键信息 |\n",
"| **更优的 Embedding 模型** | 使用针对中文优化的模型,如 BGE、M3E 等 |\n",
"| **重排序Rerank** | 先检索多个候选,再用更精确的模型排序 |\n",
"| **查询改写** | 把用户问题改写成更适合检索的形式 |\n",
"| **持久化向量库** | 使用 Chroma、FAISS、Milvus 等持久化存储 |\n",
"| **元数据过滤** | 根据来源、时间、类别等元数据进行筛选 |"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 12. 本节课练习\n",
"\n",
"1. 创建一个新的文本文件,写入关于你感兴趣的主题(如 Python、篮球、电影用 TextLoader 加载并分割\n",
"2. 调整 `chunk_size` 和 `chunk_overlap`,观察分割结果的变化\n",
"3. 用 `OpenAIEmbeddings` 或 `FakeEmbeddings` 把文本块存入 `InMemoryVectorStore`\n",
"4. 针对你的文档提出 3 个问题,观察 RAG 链的回答质量\n",
"5. 尝试问一个文档中没有的问题,观察模型是否按 Prompt 要求回答「我不知道」"
]
}
],
"metadata": {
"kernelspec": {
"display_name": ".venv",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.3"
}
},
"nbformat": 4,
"nbformat_minor": 4
}