一、 核心概念修正(专门针对你的薄弱点)
1. Embedding(嵌入):是"翻译官",不是 API Key
-
❌ 误区:以为是把 API Key 嵌进去,或者是简单的插入操作。
-
✅ 正解 :Embedding 是将**人类语言(文字)转化为计算机语言(向量/数字列表)**的过程。
-
💡 实战理解:
-
计算机看不懂"苹果",但 Embedding 模型把它变成
[0.01, -0.5, ...]。 -
计算机通过计算两个向量的**距离(余弦相似度)**来判断意思像不像。
-
注意 :入库(Indexing)和查询(Query)必须使用同一个 Embedding 模型,否则就像一个说英语一个说日语,完全鸡同鸭讲。
-
2. 数据处理的不对称性:文档要切,问题不切
这是你之前容易混淆的地方,这个流程图一定要刻在脑子里:
-
📚 文档处理流程(Indexing):
Load (加载)->Split (切分)->Embed (向量化)->Store (入库)- 为什么要切? 为了突破 Token 限制,更为了语义聚焦(防止长文稀释了关键信息)。
-
❓ 用户提问流程(Retrieval):
User Query->No Split (不切分)->Embed (向量化)->Search (检索)- 为什么不切? 用户的问题通常很短(如"怎么报销?"),如果切成"怎么"、"报销",语义就碎了,根本搜不到准的东西。
二、 LangChain 代码核心逻辑(必背代码)
1. VectorStore vs. Retriever(仓库与接口)
-
VectorStore (Chroma/FAISS) :是底层的仓库。负责存数据、算数学距离。
-
Retriever :是上层的接口。LangChain 的 Chain 只认 Retriever,不认具体的库。
-
🔑 关键代码(必考):
# 1. 只是建库 vectorstore = Chroma.from_documents(docs, embedding) # 2. 必须转换!变成标准接口,并设置只取前 k 个 # .as_retriever() 是连接底层库和上层链的桥梁 retriever = vectorstore.as_retriever(search_kwargs={"k": 3}) # 3. 传入 Chain chain = RetrievalQA.from_chain_type(llm=llm, retriever=retriever)
2. RAG 的 Prompt 结构
RAG 实际上是在做一个**"填空题"**。
-
{context}:填入从retriever找回来的文档片段(LangChain 自动帮你拼成字符串)。 -
{question}:填入用户的提问。 -
防幻觉咒语:在 Prompt 里必须写"请严格基于 Context 回答,如果不知道就说不知道"。
三、 实战避坑与调优指南
1. 数据清洗(Garbage In, Garbage Out)
-
痛点 :检索出的内容包含
<div>、乱码。 -
解决:必须在**入库前(Indexing 阶段)**处理。
-
后果:不清洗会浪费 Token,且让 Embedding 向量变脏(搜"样式"搜出一堆 HTML 标签)。
2. 检索结果为"空"或"不准"的排查路径
当 LLM 说"我不知道"时,按这个顺序查:
-
查 Embedding:模型是不是选错了?中文用了英文模型?
-
查 K 值 :
k=3是不是太少?导致正确答案排在第 4 位被丢弃了? -
查切分 (Chunking):切分块是不是太小,导致语义不完整?
-
查距离阈值 (Threshold):是不是设置了必须相似度 > 0.8,但实际文档只有 0.75?
3. 上下文窗口与"中间丢失"
-
原理:大模型记性有限(Context Window)。
-
现象 :Lost in the Middle。如果你贪心塞入 20 个文档,模型往往只看头和尾,忽略中间的内容。
-
策略 :少即是多。精准的 Top-5 往往比 杂乱的 Top-20 效果好。
四、 面试/工作速查表(Cheat Sheet)
| 术语 | 核心作用 | 一句话解释 |
|---|---|---|
| Embedding | 向量化 | 把字变成数字,用来算距离。 |
| Cosine Similarity | 算距离 | 0度(1.0)=一样;90度(0.0)=无关。RAG 靠它找相似。 |
| Splitter | 切分器 | 把长书切成小段,为了语义聚焦和省 Token。 |
| VectorStore | 向量库 | 存向量的数据库(如 Chroma),支持语义搜索。 |
| Retriever | 检索器 | LangChain 的标准接口,负责去库里捞数据。 |
| Hallucination | 幻觉 | 模型胡说八道。RAG 通过提供 Context 来抑制它。 |
五、代码实战
在代码里模拟一个"公司内部文档"。你只需要打开你的 IDE(PyCharm 或 VS Code),新建一个 rag_demo.py 文件,然后跟着我一步步来。
第一步:环境准备(装包)
在你的终端(Terminal)里运行这行命令。这相当于把"厨具"都买齐:
pip install langchain langchain-openai chromadb
-
langchain: 框架本体。 -
langchain-openai: 用来调 OpenAI 的模型和 Embedding。 -
chromadb: 那个轻量级的向量数据库(Vector Store)。
第二步:编写代码(复制并理解)
rag_demo.py :
import os
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
from langchain_community.vectorstores import Chroma
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.chains import RetrievalQA
from langchain.docstore.document import Document
# ==========================================
# 0. 配置密钥 (把这里换成你的 OpenAI Key)
# ==========================================
os.environ["OPENAI_API_KEY"] = "sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
# ==========================================
# 1. 准备数据 (模拟加载数据的过程)
# ==========================================
# 假设这是我们公司的内部 Wiki,大模型绝对没见过
raw_text = """
【公司零食柜管理条例】
1. 每天下午 3 点开放零食柜。
2. 每个人每天限领 2 包辣条,严禁多拿。
3. 如果发现有人偷吃老板的奥利奥,罚款 50 元。
4. 只有周五才提供免费的可乐。
"""
# 把纯文本包装成 LangChain 的 Document 对象
docs = [Document(page_content=raw_text)]
# ==========================================
# 2. 切分数据 (Splitting)
# ==========================================
# 咱们刚才学的:为了防止语义稀释和超长,要切!
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=100, # 每块大概 100 个字符
chunk_overlap=10 #稍微重叠一点,防止切断了关键句子
)
split_docs = text_splitter.split_documents(docs)
print(f"✅ 数据切分完成,共切成了 {len(split_docs)} 个片段")
# ==========================================
# 3. 向量化与入库 (Embedding & VectorStore)
# ==========================================
print("🔄 正在将文本转化为向量并存入 Chroma 数据库...")
# 定义 Embedding 模型 (翻译官)
embedding_model = OpenAIEmbeddings()
# 创建向量库 (Chroma)
# 这一步会自动调用 embedding_model 把上面的 split_docs 变成向量存进去
vectorstore = Chroma.from_documents(
documents=split_docs,
embedding=embedding_model
)
# ==========================================
# 4. 构建检索器与 RAG 链 (The Chain)
# ==========================================
# 关键一步:把 仓库(VectorStore) 变成 接口(Retriever)
retriever = vectorstore.as_retriever(search_kwargs={"k": 2}) # 咱们只找最相关的2条
# 定义大模型 (大脑)
llm = ChatOpenAI(model_name="gpt-3.5-turbo")
# 组装 RAG 链
qa_chain = RetrievalQA.from_chain_type(
llm=llm,
retriever=retriever
)
# ==========================================
# 5. 提问测试 (Run)
# ==========================================
question = "我想喝可乐,什么时候去拿?"
print(f"\n❓ 用户提问: {question}")
# 启动链
result = qa_chain.invoke(question)
print(f"🤖 AI 回答: {result['result']}")
# 再问一个如果不查库绝对不知道的问题
question_2 = "偷吃奥利奥怎么罚?"
print(f"\n❓ 用户提问: {question_2}")
result_2 = qa_chain.invoke(question_2)
print(f"🤖 AI 回答: {result_2['result']}")
第三步:代码深度拆解(带你读懂每一行)
虽然代码跑起来了,但咱们得知道它是怎么对应理论的:
1. 那个 Document 对象是啥?
docs = [Document(page_content=raw_text)]
LangChain 不直接处理字符串,它喜欢把文本封装成 Document 类。你可以把这个想象成给你的文本穿了一层"制服",方便后续处理。
2. RecursiveCharacterTextSplitter 里的参数
chunk_size=100, chunk_overlap=10
-
这对应咱们考题里的**"切分"**。
-
overlap=10是个实战技巧:比如一句话是"严禁多拿辣条",如果不重叠,可能切成"严禁多拿"和"辣条",意思就断了。重叠能保留一点上下文。
3. 最神奇的 Chroma.from_documents
vectorstore = Chroma.from_documents(documents=split_docs, embedding=embedding_model)
这行代码背后干了三件累活:
-
调用 OpenAI 接口,把你切好的每一段话变成向量
[0.1, 0.2...]。 -
在内存里建一个简单的数据库。
-
把向量和原文都存进去,建立索引。
4. 那个 as_retriever
retriever = vectorstore.as_retriever(search_kwargs={"k": 2})
这就是你笔记里记的"接口转换"。如果不写这行,Chain 就不知道怎么跟数据库说话。k=2 意味着每次只给大模型看 2 条最相关的规定,不多给,防止它眼花。
第四步:运行预期结果
当你运行代码后,控制台应该会输出类似这样的内容:
✅ 数据切分完成,共切成了 4 个片段
🔄 正在将文本转化为向量并存入 Chroma 数据库...
❓ 用户提问: 我想喝可乐,什么时候去拿?
🤖 AI 回答: 根据规定,只有周五才提供免费的可乐。
❓ 用户提问: 偷吃奥利奥怎么罚?
🤖 AI 回答: 如果发现偷吃老板的奥利奥,需要罚款50元。
GPT-3.5 根本不知道你们公司的零食柜规定,但通过 RAG,它准确地说出了"周五"和"罚款50元"。这就是 RAG 的魔力。
💡 小作业
试着改两个地方,体验一下讲过的"坑":
-
修改数据 :把
raw_text里的"罚款 50 元"改成"奖励一朵小红花"。重新运行,看看 AI 会不会改口?(验证实效性) -
制造幻觉 :把
k=2改成k=1,然后问一个需要结合两条规则才能回答的问题(如果有的话),或者把数据里的关键词改得隐晦一点,看看能不能搜到。