大家好,我是飞哥!👋

欢迎来到吴恩达《LangChain LLM 应用开发》系列课程的第五讲。这一讲,我们要聊一个非常火的话题:如何让 AI 基于你自己的文档来回答问题?

也就是大家常说的 “RAG” (Retrieval-Augmented Generation,检索增强生成)。


1. 为什么:给 AI 配一本“参考书” 📖

Harrison 在视频中提到:

“One of the most common complex applications… is a system that can answer questions on top of a document.”

“最常见也是最复杂的应用场景之一… 就是一个能够基于文档回答问题的系统。”

1.1 场景锚定 ⚓️

你有没有发现,直接问 ChatGPT 一些公司内部的问题(比如“我们公司去年的报销政策是啥?”),它肯定答不上来。
或者问它“昨天的最新新闻”,它也可能不知道。

因为 LLM(大语言模型)有三个主要局限:

  1. 不知道私有数据:它没看过你公司的内网文档。
  2. 知识过时:它的训练数据截止到某个时间点。
  3. 容易产生幻觉:不知道的时候它可能会一本正经地胡说八道。

1.2 生动类比 🍊

  • LLM 就像一个“博学的博士”,但他是个“书呆子”,只知道书本上的旧知识,对你公司的情况一无所知。
  • RAG 就像是“给博士配了一本参考书”(你的文档)。
  • 当你问问题时,博士先去“翻书”(检索),找到相关的那几页,然后结合书里的内容“回答”你。

1.3 核心骨架 🦴

所以,“RAG” (检索增强生成) 的本质,就是:

先检索 (Retrieval),后生成 (Generation)。

我们来看一下这个过程的流程图:

向量数据库

查找最相似

查找最相似

Context + Question

用户提问

检索 Retrieval

文档切片 1

文档切片 2

文档切片 3

构建 Prompt

LLM 大模型

最终回答


2. 是什么:LangChain 的 RAG 组件 🧩

要实现这个流程,我们需要两个关键技术组件:

2.1 Embeddings (嵌入/向量化)

  • 通俗解释:把文字变成一串数字(向量)。
  • 神奇之处:意思相近的句子,变出来的数字也靠得很近。比如“狗”和“宠物”在数学空间里距离很近,但和“汽车”很远。
  • 作用:让计算机能理解“语义”。

💡 飞哥小贴士:数值越大越相似,还是越小越相似?

这取决于你用什么“尺子”去量:

  • 余弦相似度 (Cosine Similarity):这是最常用的(如 OpenAI Embeddings)。它衡量的是“方向一致性”。数值越大 (越接近 1),相似度越高
  • 欧几里得距离 (L2 Distance):它衡量的是“空间距离”。数值越小 (越接近 0),相似度越高

在 LangChain 和大多数 RAG 应用中,默认通常使用余弦相似度,所以记住:分数越高,越相关!

2.2 Vector Database (向量数据库)

  • 通俗解释:专门存这些数字向量的仓库。
  • 作用:让我们能在一瞬间(毫秒级)从海量文档中找到和“用户问题”最相似的片段。

3. 怎么用:实战演练 (DeepSeek 版) 🛠️

我们要用代码来验证一下。为了方便大家通过 DeepSeek 学习,我们对代码做了适配,并将所有 Prompt 翻译成了中文。

3.1 环境准备

请确保你的项目目录中有一个 .env 文件,内容如下(把 Key 换成你自己的):

OPENAI_API_KEY=sk-你的DeepSeek密钥
OPENAI_API_BASE=https://api.deepseek.com
OPENAI_MODEL_NAME=deepseek-chat

此外,本节代码需要安装以下依赖:

pip install docarray langchain-openai sentence-transformers

3.2 代码实战

(1) 准备工作:初始化模型与数据 🏗️

首先,我们需要加载环境变量,并配置 DeepSeek 模型和 Embeddings。

import os
from langchain_community.document_loaders import CSVLoader
from langchain_community.vectorstores import DocArrayInMemorySearch
from langchain.indexes import VectorstoreIndexCreator
from langchain_openai import OpenAIEmbeddings
from langchain_openai import ChatOpenAI
from langchain.chains import RetrievalQA
from dotenv import load_dotenv, find_dotenv

# 加载环境变量
_ = load_dotenv(find_dotenv())

# 配置 OpenAI API Key 和 Base URL (适配 DeepSeek)
api_key = os.getenv("OPENAI_API_KEY")
base_url = os.getenv("OPENAI_API_BASE")
model_name = os.getenv("OPENAI_MODEL_NAME")

print(f"🚀 正在启动...")
print(f"🤖 模型名称: {model_name}")
print(f"🔗 API Base: {base_url}")

# 初始化 LLM (大语言模型)
# 注意:tiktoken_model_name="gpt-3.5-turbo" 是为了解决 DeepSeek 等非 OpenAI 官方模型
# 在 LangChain 中计算 Token 时可能出现的 "model not found" 错误。
llm = ChatOpenAI(
    temperature=0.0,
    model=model_name,
    base_url=base_url,
    api_key=api_key,
    tiktoken_model_name="gpt-3.5-turbo" 
)

# 初始化 Embeddings (嵌入模型)
# 策略:自动选择最佳 Embeddings 模型
try:
    if "deepseek" in (model_name or "").lower():
        print("💡 检测到 DeepSeek 模型,自动切换为本地 HuggingFaceEmbeddings 以节省成本并避免 API 兼容问题...")
        from langchain_community.embeddings import HuggingFaceEmbeddings
        # 使用更轻量级的模型,下载更快
        embeddings = HuggingFaceEmbeddings(model_name="sentence-transformers/all-MiniLM-L6-v2")
    else:
        embeddings = OpenAIEmbeddings(api_key=api_key)
except Exception as e:
    print(f"⚠️ Embeddings 初始化失败,正在尝试 fallback 到 OpenAIEmbeddings... 错误: {e}")
    embeddings = OpenAIEmbeddings(api_key=api_key, base_url="https://api.openai.com/v1")

# 模拟一个 CSV 文件 (如果没有本地文件)
csv_content = """name,description
男士热带格纹短袖衬衫,"评级:防晒。由100%聚酯纤维制成,这款衬衫抗皱并具有UPF 50+的防晒功能。"
男士格纹热带衬衫,"评级:防晒。UPF 50+防晒。100%聚酯纤维。抗皱。"
女士防晒衬衫,"评级:防晒。吸湿排汗。UPF 50+。100%聚酯纤维。"
男士羊毛衫,"温暖舒适。100%羊毛。仅干洗。"
"""

csv_file_path = "OutdoorClothingCatalog_1000.csv"
if not os.path.exists(csv_file_path):
    with open(csv_file_path, "w") as f:
        f.write(csv_content)
    print(f"✅ 已创建模拟数据文件: {csv_file_path}")

loader = CSVLoader(file_path=csv_file_path)
(2) 快速上手:一行代码创建问答系统 (High Level) ⚡️

LangChain 提供了一个极简的封装 VectorstoreIndexCreator,就像魔法一样,把所有复杂的 RAG 步骤都藏在了一行代码里。

print("\n--- 1. 一行代码创建索引与问答 (High Level) ---")
print("说明:LangChain 提供了极简的封装 (VectorstoreIndexCreator),像魔法一样一行代码搞定 RAG。")

# 创建索引 (需要安装 docarray: pip install docarray)
# 这一步内部做了三件事:
# 1. Loader 加载数据
# 2. Embeddings 将数据向量化
# 3. VectorStore 存储向量
try:
    index = VectorstoreIndexCreator(
        vectorstore_cls=DocArrayInMemorySearch,
        embedding=embeddings,
    ).from_loaders([loader])

    query = "请用 Markdown 表格列出所有具有防晒功能的衬衫,并为每个衬衫写一个简短的中文摘要。"
    
    # query 方法内部自动完成了:检索(Retrieval) -> 拼接Prompt -> 调用LLM(Generation)
    response = index.query(query, llm=llm)
    
    print(f"User Query: {query}")
    print(f"\nAI Response:\n{response}")

    # --- 运行结果示例 ---
    # User Query: 请用 Markdown 表格列出所有具有防晒功能的衬衫,并为每个衬衫写一个简短的中文摘要。
    # 
    # AI Response:
    # | 产品名称 | 防晒功能 | 材质 | 特点摘要 |
    # | :--- | :--- | :--- | :--- |
    # | 女士防晒衬衫 | UPF 50+ | 100%聚酯纤维 | 具有防晒、吸湿排汗功能的女士衬衫。 |
    # | 男士热带格纹短袖衬衫 | UPF 50+ | 100%聚酯纤维 | 防晒、抗皱的男士短袖格纹衬衫。 |
    # | 男士格纹热带衬衫 | UPF 50+ | 100%聚酯纤维 | 防晒、抗皱的男士格纹衬衫。 |
except Exception as e:
    print(f"❌ Error in Index Creator: {e}")
(3) 深度拆解:Step-by-Step 理解 RAG 原理 🔍

虽然“一行代码”很爽,但作为开发者,我们必须理解黑盒里发生了什么。让我们把 RAG 的四个步骤拆开来看。

Step 1: Embeddings (向量化)
把文本变成计算机能理解的数字。

print("\n--- 2. 深度拆解 (Step-by-Step RAG) ---")
print("\n[Step 1] Embeddings (向量化)...")

test_text = "你好,我叫飞哥"
embed = embeddings.embed_query(test_text)
print(f"文本: '{test_text}'")
print(f"向量维度: {len(embed)}")
print(f"向量前5位: {embed[:5]}... (计算机看到的语义)")

# --- 运行结果示例 ---
# 文本: '你好,我叫飞哥'
# 向量维度: 384
# 向量前5位: [-0.03247414156794548, 0.10191547125577927, 0.04291674867272377, 0.03600271791219711, 0.036942895501852036]... (计算机看到的语义)

Step 2: Vector Store (存入向量库)
将所有文档切片并向量化,存入数据库。

print("\n[Step 2] Vector Store (存入向量库)...")
docs = loader.load()
# 将所有文档转换成向量,并存入内存中的向量数据库 (DocArrayInMemorySearch)
db = DocArrayInMemorySearch.from_documents(docs, embeddings)
print("✅ 文档已存入向量数据库。")

Step 3: Retrieval (检索)
根据用户的问题,在数据库中找出最相似的文档片段。

print("\n[Step 3] Retrieval (检索)...")
query = "请列出所有具有防晒功能的衬衫"
# 找出与 query 语义最相似的文档
docs = db.similarity_search(query)
print(f"用户问题: {query}")
print(f"🔍 检索到 {len(docs)} 个相关文档:")
print(f"   > 文档 1: {docs[0].page_content}")

# --- 运行结果示例 ---
# 用户问题: 请列出所有具有防晒功能的衬衫
# 🔍 检索到 4 个相关文档:
#    > 文档 1: name: 男士热带格纹短袖衬衫
# description: 评级:防晒。由100%聚酯纤维制成,这款衬衫抗皱并具有UPF 50+的防晒功能。

Step 4: Chain (构建问答链)
将检索到的文档片段(Context)和用户问题(Question)一起发给 LLM,生成最终回答。

print("\n[Step 4] Chain (构建问答链)...")
# 创建检索器接口
retriever = db.as_retriever()

# 创建 RetrievalQA 链
# chain_type="stuff" 的意思是:
# 把检索到的所有文档片段(Stuffing),一股脑填充到 Prompt 中发给 LLM。
qa_stuff = RetrievalQA.from_chain_type(
    llm=llm, 
    chain_type="stuff", 
    retriever=retriever, 
    verbose=True
)

print("🚀 开始运行问答链...")
response = qa_stuff.run(query)
print(f"\n🤖 AI 最终回答:\n{response}")

# --- 运行结果示例 ---
# 🚀 开始运行问答链...
# 
# 🤖 AI 最终回答:
# 根据提供的产品信息,具有防晒功能的衬衫如下:
# 
# 1. **男士热带格纹短袖衬衫**
#    - **评级**:防晒
#    - **关键功能**:具有UPF 50+的防晒功能。
# 
# 2. **女士防晒衬衫**
#    - **评级**:防晒
#    - **关键功能**:UPF 50+。
# 
# 3. **男士格纹热带衬衫**
#    - **评级**:防晒
#    - **关键功能**:UPF 50+防晒。
# 
# **请注意**:另一款“男士羊毛衫”的描述中未提及任何防晒功能,因此不在上述列表中。

4. 飞哥总结 📝

今天我们学习了 RAG 的核心流程。

核心三要点

  1. Embeddings 是灵魂:让机器理解语义相似度,把“词”变成“数”。
  2. VectorStore 是仓库:高效存取这些语义向量,实现快速检索。
  3. RetrievalQA 是工人:负责“搬运文档”并让 LLM 结合文档回答问题。

一句话记住它

RAG 就是给 LLM 买了本参考书,遇到不会的问题,先查书,再回答。

下一讲,我们来聊聊一个很现实的问题:怎么知道 AI 回答得对不对?—— Evaluation (评估)。🚀

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐