🎯 目标:掌握大模型应用开发的核心技能——LLaMA架构、提示词工程、LangChain框架、RAG系统构建、OpenAI API,能够独立开发AI应用。 📋 前置要求:阶段三(Transformer架构、BERT/GPT原理、HuggingFace使用)
本阶段知识依赖图
阶段三基础(Transformer + BERT/GPT + HuggingFace)
│
├──→ LLaMA架构(理解现代大模型)
│ ├── RMSNorm ──→ 归一化改进
│ ├── RoPE ──→ 旋转位置编码
│ ├── SwiGLU ──→ 激活函数改进
│ ├── KV Cache ──→ 推理加速
│ └── GQA ──→ 注意力优化
│
├──→ 提示词工程(高效使用大模型)
│ ├── 基础技巧 ──→ 输出格式、指令设计
│ ├── 高级技巧 ──→ CoT、ToT、Few-shot
│ └── 参数调优 ──→ Temperature、Top-P
│
├──→ LangChain框架(应用开发框架)
│ ├── LLM调用 ──→ 统一接口
│ ├── 提示模板 ──→ 结构化提示
│ ├── 链(Chain) ──→ 组合多个组件
│ └── Agent ──→ 自主决策
│
├──→ RAG系统(检索增强生成)⭐
│ ├── 向量数据库 ──→ Milvus/FAISS/Chroma
│ ├── 文档加载 ──→ PDF/Markdown解析
│ ├── 文本切片 ──→ 语义切分
│ ├── Embedding ──→ BGE-Large/BM25
│ └── 检索策略 ──→ 高级检索/自适应RAG
│
└──→ OpenAI API(模型调用接口)
├── Embedding ──→ 文本向量化
├── Chat Completion ──→ 对话接口
└── Function Calling ──→ 工具调用
模块一:LLaMA架构深入——理解现代大模型
LLaMA为什么重要?
类比:如果Transformer是"汽车的发明",那LLaMA就是"现代汽车的标准设计"。
LLaMA(Large Language Model Meta AI)是Meta发布的开源大模型系列。
它是目前大多数开源大模型的基础架构——几乎所有主流开源模型都是基于LLaMA微调而来:
LLaMA (原始)
├── Alpaca(斯坦福微调)──→ 用指令数据微调
├── Vicuna(LMSYS微调)──→ 用对话数据微调
├── Chinese-LLaMA(中文适配)──→ 加入中文词表
├── CodeLLaMA(代码能力)──→ 用代码数据微调
└── LLaMA 2/3(Meta官方迭代)──→ 更大、更强
理解LLaMA = 理解当前大模型的核心设计思想
它在Transformer基础上做了5个关键改进,每个改进都解决了特定问题。
RMSNorm——改进的归一化
归一化为什么重要?
类比:考试成绩标准化
假设两个班级的考试:
- A班:平均分90分,最高95,最低85(分数集中在90附近)
- B班:平均分60分,最高100,最低20(分数很分散)
如果直接用原始分数比较两个班的学生,分布差异会干扰判断。
归一化的作用:把两个班的分数都"拉"到同一个范围(比如均值0,标准差1)
→ 现在可以公平比较了
神经网络中也一样:如果每层的输入分布差异很大,网络很难学习。
归一化让每层的输入分布稳定,训练更高效。
LayerNorm vs RMSNorm——详细对比
传统LayerNorm(4步):
1. 计算均值 μ = (1/d)Σx_i
2. 计算方差 σ² = (1/d)Σ(x_i - μ)²
3. 归一化 x̂ = (x - μ) / √(σ² + ε)
4. 缩放 y = γ · x̂ + β
RMSNorm(3步):
1. 计算均方根 RMS = √((1/d)Σx_i²)
2. 归一化 x̂ = x / RMS
3. 缩放 y = γ · x̂
核心区别:RMSNorm去掉了"减均值"(re-centering)和"偏置β"
为什么去掉"减均值"没问题?
类比:你在调节收音机的音量
- LayerNorm = 先把音量归零(减均值),再调到合适大小(缩放)
- RMSNorm = 直接调到合适大小(只缩放,不归零)
实验发现:先归零再调,和直接调,效果差别很小。
但省去"归零"这一步,计算量减少了约15%。
在大模型中(几十亿参数),15%的计算节省 = 巨大的成本节约!
哪些模型使用RMSNorm?
✅ LLaMA / LLaMA 2 / LLaMA 3
✅ Qwen / Qwen2
✅ Mistral / Mixtral
✅ Gemma
✅ DeepSeek
RMSNorm已经成为现代大模型的标配。
RoPE——旋转位置编码
为什么需要新的位置编码?
正弦位置编码的三个局限:
1. 固定不变:正弦编码是预先计算好的,不参与训练
→ 模型无法根据任务自适应调整位置表示
2. 只编码绝对位置:PE(3)只告诉你"这是第3个词"
→ 不能直接知道"第3个词和第7个词之间隔了4个词"
→ 但语言理解往往更依赖相对位置("主语在谓语前面2个词")
3. 外推能力有限:训练时最长512个词,推理时超过512效果急剧下降
→ 无法处理更长的文档
RoPE的核心思想——把位置编码变成"旋转"
类比:时钟的指针
想象一个时钟:
- 1点钟:指针转了30°
- 2点钟:指针转了60°
- 3点钟:指针转了90°
每个时刻的位置 = 指针旋转的角度
两个时刻之间的"距离" = 角度之差
RoPE做的是同样的事:
- 把词向量的每两个维度看作一个二维平面上的点
- 位置m的词向量旋转 m×θ 角度
- θ 是一个预设的旋转速度(不同维度不同)
RoPE的数学本质:
对于位置m的query向量 q 和位置n的key向量 k:
应用RoPE后的注意力分数:
q_m^T · k_n = (R_m · q)^T · (R_n · k) = q^T · R_(n-m) · k
关键性质:注意力分数只依赖于相对位置 (n-m),而不是绝对位置 m 和 n!
这意味着:
- 模型天然理解"距离"(相隔几个词)
- 不管句子从哪个位置开始,相对关系不变
- 可以外推到更长的序列(因为只依赖相对距离)
RoPE vs 正弦位置编码:
特性 正弦位置编码 RoPE
─────────────────────────────────────────────────
编码方式 加到输入上 乘到Q/K上
位置类型 绝对位置 相对位置
是否可训练 固定不变 可通过缩放因子调整
外推能力 有限 较好
使用模型 原始Transformer LLaMA、Qwen、Mistral
SwiGLU——改进的激活函数
从ReLU到SwiGLU的进化
传统FFN(Transformer原版):
FFN(x) = ReLU(x·W₁ + b₁)·W₂ + b₂
维度变化:d_model → 4×d_model → d_model
SwiGLU FFN(LLaMA使用):
FFN(x) = (Swish(x·W₁) ⊙ x·W₃)·W₂
维度变化:d_model → (8/3)×d_model → d_model
其中:
- Swish(x) = x · σ(x)(σ是Sigmoid函数)
直觉:Swish是一个"平滑的ReLU"——在x<0时不完全关闭,而是留一点"缝隙"
- ⊙ 是逐元素相乘(门控机制)
直觉:W₃产生的值像一个"阀门",控制W₁的信息通过多少
GLU(Gated Linear Unit)的核心思想——门控:
SwiGLU = Swish + GLU(门控线性单元)
门控的意思是:不是简单地"全部通过"或"全部阻断",
而是对信息的每个维度独立地"调节流量"
类比:
ReLU = 一个水龙头,要么全开(x>0),要么全关(x≤0)
SwiGLU = 一个可调节的阀门,可以控制每个出水孔的流量
效果:SwiGLU在多个基准测试上优于ReLU,训练更稳定
代价:多了一个权重矩阵W₃,参数量增加约50%
(所以LLaMA把隐藏维度从4d降到8/3d来补偿)
KV Cache——推理加速的关键
为什么自回归生成很慢?
类比:翻译一本书
翻译第1个词时:需要读完整本书(完整前向传播)
翻译第2个词时:又要读完整本书(但大部分内容和上次一样!)
翻译第3个词时:又要读完整本书...
翻译第1000个词时:还是要读完整本书!
问题:每次都重新计算所有位置的K和V,但之前计算的结果完全可以复用!
KV Cache的解决方案
KV Cache = "笔记本":把之前算过的K和V记下来,下次直接用
生成第1个词(Prefill阶段):
计算所有位置的K和V,全部存入Cache
输出第1个词
生成第2个词(Decode阶段):
只计算新位置的K₂, V₂(1次计算)
从Cache读取K₁, V₁(直接读取,不需要计算)
拼接后计算注意力
输出第2个词
生成第3个词:
只计算K₃, V₃
从Cache读取[K₁,K₂], [V₁,V₂]
拼接后计算注意力
输出第3个词
效果:每个新词只需要1次前向传播(而不是seq_len次)
→ 推理速度提升 seq_len 倍!
KV Cache的显存开销:
KV Cache大小 = 2 × num_layers × num_heads × d_head × seq_len × batch_size × dtype_size
示例(LLaMA-7B,float16,单条序列):
2 × 32层 × 32头 × 128维 × 2048长度 × 2 bytes ≈ 1GB
示例(LLaMA-70B,float16,单条序列):
2 × 80层 × 64头 × 128维 × 4096长度 × 2 bytes ≈ 20GB
→ 大模型的KV Cache可以占到模型本身显存的30-50%!
→ 这就是为什么长上下文(100K+token)需要大量显存
→ GQA的出现就是为了减少KV Cache的大小
GQA——Grouped Query Attention
为什么要优化注意力的KV?
问题:KV Cache太大了!
标准Multi-Head Attention (MHA):
Q: 32个头,每个头有独立的W_Q
K: 32个头,每个头有独立的W_K ← 32组KV
V: 32个头,每个头有独立的W_V
KV Cache大小 ∝ num_heads(头数越多,Cache越大)
如何减小KV Cache?→ 减少KV的"头数"
三种方案的对比:
MHA(标准多头注意力):
Q: 32头 K: 32头 V: 32头
→ 每个Q头有自己的KV,互不共享
→ 质量最好,但KV Cache最大
MQA(多查询注意力)——极端方案:
Q: 32头 K: 1头 V: 1头
→ 所有Q头共享同一个KV
→ KV Cache最小,但质量下降明显
GQA(分组查询注意力)——折中方案:
Q: 32头 K: 8组 V: 8组
→ 每4个Q头共享一组KV
→ 质量接近MHA,速度接近MQA
类比:
MHA = 每个人都有自己的参考资料(32份)
MQA = 所有人共用一份参考资料(1份)
GQA = 每4人一组,每组一份参考资料(8份)
GQA的效果:
LLaMA 1:使用MHA(标准多头注意力)
LLaMA 2:使用GQA(分组查询注意力,8个KV组)
LLaMA 3:使用GQA(进一步优化)
GQA让KV Cache减小了约4倍,同时模型质量几乎不变。
→ 可以在相同显存下处理更长的序列
→ 可以用更大的batch_size,提高吞吐量
LLaMA推理策略
Temperature——控制输出的"随机性"
类比:选择餐厅
Temperature = 0(极度保守):
每次都去评分最高的餐厅 → 确定性最高,但可能无聊
Temperature = 0.7(平衡):
大概率去评分高的,偶尔尝试新餐厅 → 既有质量又有惊喜
Temperature = 1.0(随机):
随机选一家 → 完全不可预测
Temperature的数学原理:
原始logits: [2.0, 1.0, 0.1]
Temperature = logits / T
T=0.5时:[4.0, 2.0, 0.2] → softmax后 [0.84, 0.12, 0.04] → 非常确定
T=1.0时:[2.0, 1.0, 0.1] → softmax后 [0.66, 0.24, 0.10] → 原始分布
T=2.0时:[1.0, 0.5, 0.05] → softmax后 [0.50, 0.30, 0.20] → 更均匀
T越小 → 分布越尖锐 → 输出越确定
T越大 → 分布越平坦 → 输出越随机
Top-K和Top-P采样
Top-K采样:只从概率最高的K个词中采样
K=1:等价于贪心搜索(永远选概率最高的词)
K=50:从50个候选词中随机选
问题:K是固定的,简单问题和复杂问题用同一个K
Top-P(Nucleus Sampling):动态选择候选集
按概率从高到低排序,累加直到概率之和超过P
简单问题:可能只需要前3个词就超过P=0.9 → 候选集小
复杂问题:可能需要前50个词才超过P=0.9 → 候选集大
实际使用推荐:
代码生成:Temperature=0, Top-P=1.0(确定性输出)
一般对话:Temperature=0.7, Top-P=0.9(平衡)
创意写作:Temperature=1.0, Top-P=0.95(多样输出)
模块二:提示词工程——高效使用大模型
为什么提示词工程重要?
类比:和外国人交流
你说:"帮我写个东西"
外国人(大模型):写什么?写给谁?什么风格?多长?
→ 输出:一段泛泛而谈的文字
你说:"你是一位资深电商文案专家。请为一款售价299元的蓝牙耳机写一段小红书种草文案。要求:标题吸引眼球,突出降噪功能,使用emoji,200字以内。"
外国人(大模型):明白了!
→ 输出:一篇精准、专业的文案
提示词工程 = 学会如何清晰、精确地表达你的需求
基础技巧
结构化提示词的四要素
1. 角色(Role):告诉模型"你是谁"
→ 设定专业背景,让模型调用相关知识
例:"你是一位有10年经验的Python高级工程师"
2. 任务(Task):告诉模型"做什么"
→ 明确、具体的任务描述
例:"请帮我写一个FastAPI接口,实现用户登录功能"
3. 格式(Format):告诉模型"怎么输出"
→ 指定输出的格式和结构
例:"输出为完整的Python代码,包含注释和错误处理"
4. 约束(Constraint):告诉模型"边界在哪"
→ 限制条件、质量要求
例:"使用异步函数,返回JSON格式,包含token过期时间"
四要素的组合效果:
只有任务:"帮我写个API" → 输出不确定
任务+格式:"帮我写个API,输出Python代码" → 稍好
任务+格式+约束:"帮我写个FastAPI用户登录API,用Python代码输出,包含JWT认证" → 更好
全部四个:"你是一位Python高级工程师。请帮我写一个FastAPI用户登录接口。输出完整的Python代码,包含注释。要求:使用异步函数,实现JWT认证,添加输入验证和错误处理,返回标准JSON响应。" → 最好
高级技巧
零样本思维链(Zero-shot CoT)——激活推理能力
普通提示:
"小明有5个苹果,给了小红2个,又买了3个,现在有几个?"
→ 模型可能直接猜"6"(可能错)
加一句魔法咒语"让我们一步一步思考":
"小明有5个苹果,给了小红2个,又买了3个,现在有几个?让我们一步一步思考。"
→ 模型会展示:
"1. 小明开始有5个苹果
2. 给了小红2个,剩下5-2=3个
3. 又买了3个,变成3+3=6个
答案:6个"
为什么加一句话就能提升准确率?
→ "让我们一步一步思考"激活了模型的"慢思考"模式
→ 模型被迫展示中间步骤,减少了"跳步"导致的错误
→ 类似于人类"打草稿"比"心算"更准确
少样本提示(Few-shot)——用示例教会模型
零样本(Zero-shot):直接让模型做任务
"请判断情感:这家餐厅很好吃 → ?"
→ 模型可能理解你要什么,也可能不理解
少样本(Few-shot):给几个示例,让模型学会模式
"请判断以下评论的情感:
评论:这家餐厅太好吃! → 正面
评论:服务态度很差 → 负面
评论:菜品种类丰富 → 正面
评论:等了一个小时才上菜 → ?"
→ 模型学会了"判断情感"的模式,准确率大幅提升
关键:示例的质量和多样性很重要
- 至少2-3个正例和2-3个反例
- 示例要覆盖典型情况
- 示例的格式要和目标任务一致
思维树(Tree of Thought)——多路径推理
思维链(CoT):一条推理路径(线性)
A → B → C → D → 答案
问题:如果B就是错的,后面全错
思维树(ToT):多条推理路径(树状)
A → B₁ → C₁ → D₁ → 答案₁
A → B₂ → C₂ → D₂ → 答案₂
A → B₃ → C₃ → D₃ → 答案₃
→ 评估每条路径的可行性,选择最优
适用场景:需要多步推理的复杂问题(数学证明、策略规划、代码调试)
参数调优——调参的艺术
Temperature(温度):
0.0 → 完全确定性,每次输出相同(适合代码、数学)
0.3 → 高度确定性,偶尔有小变化(适合翻译、摘要)
0.7 → 平衡模式(适合对话、写作)
1.0 → 高随机性(适合创意、头脑风暴)
Top-P(核采样):
0.1 → 只从最可能的几个词中选(极度保守)
0.9 → 从累积概率90%的词中选(推荐默认值)
1.0 → 从所有词中选(最多样)
Frequency Penalty(频率惩罚):
0.0 → 不惩罚重复(默认)
0.5 → 轻微减少重复
1.0 → 强烈避免重复(适合长文本生成)
Presence Penalty(存在惩罚):
0.0 → 不鼓励新话题(默认)
0.5 → 轻微鼓励新话题
1.0 → 强烈鼓励新话题(适合头脑风暴)
模块三:LangChain框架——AI应用开发的标准工具
LangChain是什么?
类比:Spring Boot之于Java Web开发,LangChain之于LLM应用开发
没有LangChain时,开发一个RAG应用需要:
1. 手动调用OpenAI API
2. 手动加载和切分文档
3. 手动实现向量搜索
4. 手动组装提示词
5. 手动处理输出解析
6. 手动管理对话历史
→ 每个项目都要重复造轮子
有了LangChain:
1. 统一的LLM调用接口(支持OpenAI、本地模型、各种API)
2. 内置的文档加载和切分工具
3. 内置的向量存储和检索
4. 标准化的提示模板
5. 自动化的输出解析
6. 内置的记忆管理
→ 专注于业务逻辑,不用重复造轮子
核心组件1:LLM调用——统一接口
from langchain_openai import ChatOpenAI
from langchain_community.llms import Ollama
# OpenAI API
llm = ChatOpenAI(model="gpt-4", temperature=0.7)
# 本地模型(Ollama部署的Qwen3)
llm = Ollama(model="qwen3:8b")
# 智谱AI
from langchain_community.chat_models import ChatZhipuAI
llm = ChatZhipuAI(model="glm-4")
# 统一接口:不管底层是什么模型,调用方式完全一样
response = llm.invoke("什么是RAG?")
print(response.content)
# 这就是LangChain的核心价值之一:
# 一行代码切换模型,不需要修改业务逻辑
核心组件2:提示模板——结构化提示词
from langchain_core.prompts import ChatPromptTemplate
# 创建模板(类似填空题)
template = ChatPromptTemplate.from_messages([
("system", "你是一位{role},请用{style}的方式回答问题。"),
("user", "{question}")
])
# 填充模板(把空填上)
prompt = template.invoke({
"role": "AI专家",
"style": "简洁明了",
"question": "什么是Transformer?"
})
# 发送给模型
response = llm.invoke(prompt)
为什么需要模板?
类比:邮件模板
- 没有模板:每次写邮件都要从头写
- 有模板:填入姓名、日期等变量,自动生成完整邮件
提示模板同理:
- 定义一次模板,多次复用
- 变量可以在运行时动态填充
- 保证提示词的一致性
核心组件3:链(Chain)——LCEL管道语法
from langchain_core.output_parsers import StrOutputParser
# LCEL(LangChain Expression Language)管道语法
chain = template | llm | StrOutputParser()
# 等价于:
# 1. template处理输入 → 生成提示词
# 2. llm处理提示词 → 生成回答
# 3. StrOutputParser处理回答 → 提取纯文本
# 执行链
result = chain.invoke({
"role": "AI专家",
"style": "简洁明了",
"question": "什么是Transformer?"
})
# 管道语法的魔力:可以用 | 把任意组件串联起来
# 就像Unix的管道:cat file | grep "error" | sort
核心组件4:文档加载与向量存储
from langchain_community.document_loaders import PyPDFLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_openai import OpenAIEmbeddings
from langchain_community.vectorstores import FAISS
# 1. 加载文档(把PDF变成可处理的文本)
loader = PyPDFLoader("document.pdf")
documents = loader.load()
# 2. 文本切分(把长文档切成小块)
splitter = RecursiveCharacterTextSplitter(
chunk_size=500, # 每块500个字符
chunk_overlap=50 # 相邻块重叠50个字符
)
chunks = splitter.split_documents(documents)
# 3. 向量化(把文本变成数字向量)
embeddings = OpenAIEmbeddings()
vectorstore = FAISS.from_documents(chunks, embeddings)
# 4. 检索(找到最相关的文档块)
retriever = vectorstore.as_retriever(search_kwargs={"k": 3})
docs = retriever.invoke("什么是机器学习?")
为什么要切分?为什么要有overlap?
为什么切分:
- 大模型有上下文长度限制(如4K、8K、128K tokens)
- 一次塞入整本文档不现实
- 检索时需要精确匹配,整本文档太粗糙
为什么overlap(重叠):
- 如果在句子中间切断,前后两块的语义都不完整
- overlap让相邻块有重叠部分,确保信息的连续性
- 类比:看书时翻页,上一页的最后一行和下一页的第一行能衔接上
核心组件5:RAG链——检索增强生成
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnablePassthrough
# RAG提示模板
rag_prompt = ChatPromptTemplate.from_template("""
请根据以下参考信息回答用户的问题。
如果参考信息中没有相关内容,请说明你不确定。
参考信息:
{context}
用户问题:
{question}
""")
# RAG链的构建(LCEL管道语法)
rag_chain = (
{"context": retriever, "question": RunnablePassthrough()}
| rag_prompt
| llm
| StrOutputParser()
)
# 使用
answer = rag_chain.invoke("什么是注意力机制?")
# 流程:
# 1. "什么是注意力机制?" → retriever检索相关文档
# 2. 检索到的文档 + 问题 → 填入提示模板
# 3. 填充后的提示 → 发送给LLM
# 4. LLM的回答 → 提取纯文本 → 返回
核心组件6:Agent——让模型自主决策
from langchain.agents import create_tool_calling_agent, AgentExecutor
from langchain_core.tools import tool
# 定义工具(给模型"武器")
@tool
def search_web(query: str) -> str:
"""搜索网页获取最新信息"""
return "搜索结果..."
@tool
def calculate(expression: str) -> str:
"""计算数学表达式"""
return str(eval(expression))
# 创建Agent(给模型"大脑")
tools = [search_web, calculate]
agent = create_tool_calling_agent(llm, tools, prompt)
agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True)
# 使用
result = agent_executor.invoke({"input": "今天北京的天气怎么样?"})
# Agent会自动:
# 1. 分析问题 → "需要查天气"
# 2. 选择工具 → search_web
# 3. 调用工具 → search_web("北京今天天气")
# 4. 整合结果 → 生成自然语言回答
Chain vs Agent的区别:
Chain(链):固定的流程,A → B → C
类比:工厂流水线——每一步都是预设的
适合:流程明确的任务(RAG问答、文本翻译)
Agent(智能体):动态决策,根据情况选择下一步
类比:真人客服——根据问题类型灵活应对
适合:需要判断和选择的任务(多工具调用、复杂查询)
模块四:OpenAI API与Embedding
Embedding——把文本变成"语义坐标"
类比:给每个词/句子在"语义地图"上定位
Embedding = 把文本转换为一个固定长度的数字向量
"猫" → [0.2, 0.8, -0.1, 0.5, ...] (1536维)
"狗" → [0.3, 0.7, -0.2, 0.4, ...] (和"猫"接近)
"汽车" → [-0.5, 0.1, 0.9, -0.3, ...](和"猫"很远)
语义相似的文本 → 向量接近(余弦相似度接近1)
语义不同的文本 → 向量远离(余弦相似度接近0)
from openai import OpenAI
import numpy as np
client = OpenAI()
# 获取Embedding
response = client.embeddings.create(
model="text-embedding-3-small",
input="什么是机器学习?"
)
embedding = response.data[0].embedding # 1536维向量
# 计算相似度
def cosine_similarity(a, b):
return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))
# "机器学习是什么" 和 "什么是机器学习" → 几乎相同的问题 → 高相似度
sim1 = cosine_similarity(embedding1, embedding2) # ≈ 0.95
# "什么是机器学习" 和 "今天天气怎么样" → 完全不同的话题 → 低相似度
sim2 = cosine_similarity(embedding1, embedding3) # ≈ 0.1
Embedding是RAG的基础:
RAG的检索步骤 = 把用户问题和所有文档都转为Embedding,然后找最相似的
用户问题:"什么是注意力机制?"
→ Embedding → [0.1, 0.5, -0.3, ...]
文档1:"注意力机制是一种让模型关注重要信息的技术"
→ Embedding → [0.1, 0.5, -0.2, ...] ← 高相似度!
文档2:"今天天气很好"
→ Embedding → [-0.4, 0.1, 0.8, ...] ← 低相似度
→ 返回文档1给模型作为参考
Chat Completion API
from openai import OpenAI
client = OpenAI()
response = client.chat.completions.create(
model="gpt-4",
messages=[
{"role": "system", "content": "你是一位AI助手"},
{"role": "user", "content": "解释什么是RAG"}
],
temperature=0.7,
max_tokens=500,
stream=True # 流式输出(逐字显示,而不是等全部生成完)
)
# 流式输出——提升用户体验
for chunk in response:
if chunk.choices[0].delta.content:
print(chunk.choices[0].delta.content, end="")
# 输出:RAG(Retrieval-Augmented Generation)是一种...
模块五:RAG系统深入——检索增强生成
为什么需要RAG?——大模型的三大硬伤
硬伤1:知识截止日期
GPT-4的知识截止到2024年4月
问它"今天的新闻" → 完全不知道
硬伤2:幻觉(Hallucination)
大模型会"一本正经地胡说八道"
问它一个它不知道的事实 → 它可能编造一个看起来合理但错误的答案
硬伤3:企业私有数据
大模型从未见过你公司的内部文档、产品手册、客户数据
问它"我们公司的退货政策是什么" → 完全不知道
RAG的解决方案:
不要让大模型"凭记忆回答",而是先帮它"查资料",再让它"基于资料回答"
→ 知识可以实时更新(查最新的资料)
→ 减少幻觉(有据可依)
→ 可以访问私有数据(先检索企业文档)
RAG的完整流程——每一步详解
Step 1: 文档处理(离线,一次性完成)
PDF/Word/Markdown → 文本提取 → 文本切分 → Embedding → 存入向量数据库
Step 2: 检索(在线,每次查询时执行)
用户问题 → 问题Embedding → 向量数据库中搜索最相似的文档块
Step 3: 生成(在线,每次查询时执行)
检索到的文档块 + 用户问题 → 组装提示词 → 发送给LLM → 生成回答
类比:
Step 1 = 把图书馆的书整理好,建立索引
Step 2 = 根据读者的问题,从图书馆找出相关的书
Step 3 = 让AI助手阅读这些书,然后回答读者的问题
数据加载与切片——决定RAG质量的关键
# PDF解析
from langchain_community.document_loaders import PyPDFLoader
loader = PyPDFLoader("enterprise_doc.pdf")
docs = loader.load()
# Markdown解析
from langchain_community.document_loaders import UnstructuredMarkdownLoader
loader = UnstructuredMarkdownLoader("readme.md")
# 语义切分(推荐)
from langchain_experimental.text_splitter import SemanticChunker
splitter = SemanticChunker(OpenAIEmbeddings())
chunks = splitter.split_documents(docs)
切分方式对比:
固定长度切分:每500个字符切一刀
优点:简单
缺点:可能在句子中间切断,语义不完整
递归切分(RecursiveCharacterTextSplitter):按段落→句子→词的优先级切
优点:尽量在自然边界处切分
缺点:块大小不均匀
语义切分(SemanticChunker):根据语义相似度自动找到切分点
优点:每个块语义完整
缺点:计算量较大(需要调用Embedding模型)
实际推荐:
- 快速原型:用递归切分
- 生产环境:用语义切分
- 特定格式:用专门的解析器(如Markdown用MarkdownHeaderTextSplitter)
向量数据库——存储和检索的"仓库"
数据库 类型 适用场景 特点
────────────────────────────────────────────────────
FAISS 内存型 快速原型/小数据 速度快,不能持久化(关机就没了)
Chroma 嵌入型 本地开发/小项目 简单易用,自动持久化到磁盘
Milvus 服务型 生产环境/大数据 分布式、高性能、企业级
Pinecone 云服务 无需运维 托管服务,按量付费
类比:
FAISS = 纸质便签——快速方便,但容易丢
Chroma = 笔记本——持久保存,但容量有限
Milvus = 数据中心——高性能、高可靠,但需要运维
Pinecone = 云存储——不用操心基础设施,但要付费
高级检索策略——提升RAG效果的关键
# 1. 混合检索(Hybrid Search):语义 + 关键词
# 类比:搜索时同时考虑"意思相近"和"关键词匹配"
from langchain.retrievers import EnsembleRetriever
from langchain_community.retrievers import BM25Retriever
bm25_retriever = BM25Retriever.from_documents(chunks) # 关键词检索
vector_retriever = vectorstore.as_retriever() # 语义检索
# 70%语义 + 30%关键词
ensemble_retriever = EnsembleRetriever(
retrievers=[vector_retriever, bm25_retriever],
weights=[0.7, 0.3]
)
# 为什么需要混合检索?
# 纯语义检索的问题:可能检索到语义相似但关键词不匹配的文档
# 纯关键词检索的问题:可能漏掉语义相关但用词不同的文档
# 混合检索取长补短
# 2. 多查询检索(Multi-Query):一个问题,多种表述
# 类比:搜索时同时用多个关键词
from langchain.retrievers import MultiQueryRetriever
retriever = MultiQueryRetriever.from_llm(
retriever=vectorstore.as_retriever(),
llm=llm
)
# 原始问题:"什么是RAG?"
# 自动生成多个变体:
# "解释检索增强生成技术"
# "RAG的定义和原理"
# "描述RAG的工作流程"
# 合并所有查询的检索结果 → 召回率大幅提升
# 3. 上下文压缩(Contextual Compression):只保留相关内容
# 类比:从一本书中只摘抄和问题相关的段落
from langchain.retrievers import ContextualCompressionRetriever
from langchain_cohere import CohereRerank
compressor = CohereRerank()
compression_retriever = ContextualCompressionRetriever(
base_compressor=compressor,
base_retriever=vectorstore.as_retriever()
)
# 检索到的文档块可能包含大量无关信息
# 压缩后只保留与问题最相关的部分 → 减少噪音,提高回答质量
Corrective RAG——自我纠正
标准RAG的问题:
检索到的文档可能不相关,但模型仍会基于这些文档生成回答
→ "垃圾进,垃圾出"
Corrective RAG的改进流程:
1. 检索文档
2. 评估文档与问题的相关性(用LLM判断)
3. 如果相关性低 → 用网络搜索补充更相关的信息
4. 如果相关性高 → 直接使用
5. 生成回答后,再评估回答的质量
6. 如果质量不达标 → 重新检索或重新生成
类比:一个负责任的研究员
普通RAG = 随便找几本书就回答
Corrective RAG = 先检查找的书是否相关,不相关就换一批,回答后还要检查质量
Adaptive RAG——自适应检索
核心思想:不同类型的问题需要不同的处理方式
简单事实问题:"法国的首都是哪?"
→ 大模型自己就知道,不需要检索
→ 直接回答,省时省力
复杂知识问题:"比较RAG和微调的优劣"
→ 需要检索相关文档
→ 基于检索结果回答
推理问题:"如果A>B,B>C,那么A和C的关系?"
→ 不需要检索(这不是知识问题,是推理问题)
→ 让模型用思维链推理
Adaptive RAG = 先判断问题类型 → 再选择最合适的处理方式
→ 效率最高,效果最好
模块六:向量数据库深入
向量搜索的底层原理
类比:在图书馆找"最相似的书"
传统数据库(MySQL):精确匹配
"SELECT * FROM books WHERE title = '深度学习'"
→ 只能找到标题完全匹配的书
向量数据库:语义相似度搜索
"找到和'深度学习'含义最接近的书"
→ 能找到"神经网络"、"机器学习入门"、"TensorFlow实战"等相关书籍
向量搜索的核心算法:
暴力搜索(Brute Force):
计算查询向量和所有向量的距离 → 取最近的k个
精度:100%(不会漏掉任何结果)
速度:O(n×d),n=向量数量,d=维度
问题:数据量大时极慢(100万个1536维向量需要计算15亿次)
近似最近邻(ANN):
用一些"聪明的策略"加速搜索,牺牲少量精度换取大量速度
HNSW——最常用的ANN算法
HNSW(Hierarchical Navigable Small World)= 分层可导航小世界图
类比:找人的社交网络
第1层(稀疏层):只有"超级节点"(明星、大V)→ 快速定位大致区域
第2层(中等层):普通节点 → 缩小范围
第3层(密集层):所有节点 → 精确定位
搜索过程:
1. 从最顶层的某个节点开始
2. 在当前层找到最近的邻居
3. 跳到下一层,继续找最近的邻居
4. 重复直到最底层 → 找到最近的向量
精度:95-99%(偶尔会漏掉,但几乎不影响结果)
速度:O(log n),比暴力搜索快几个数量级
IVF——另一种常用算法
IVF(Inverted File Index)= 倒排文件索引
类比:图书馆的分区
把所有书按主题分成100个区域
找书时先确定在哪个区域,再在区域内搜索
→ 不需要翻遍整个图书馆
实现:
1. 训练时:用K-Means把向量分成若干个聚类(Voronoi cells)
2. 搜索时:先找到查询向量最近的几个聚类,只在这些聚类中搜索
3. 参数nprobe:搜索多少个聚类(越大越精确,越慢)
量化压缩
PQ(Product Quantization)= 乘积量化
类比:用"代表色"代替所有颜色
一张图片有1600万种颜色
用256种"代表色"来近似 → 存储空间大幅减少
实现:
1. 把1536维向量切成多段(如8段,每段192维)
2. 每段用一个"码本"(codebook)中的最近码字代替
3. 原始向量:1536 × 4 bytes = 6KB
4. 量化后:8 × 1 byte = 8 bytes → 压缩768倍!
向量数据库选型深入
数据库 索引算法 持久化 分布式 适用场景
─────────────────────────────────────────────────────────
FAISS HNSW/IVF/PQ ❌内存 ❌单机 快速原型、小数据
Chroma HNSW ✅磁盘 ❌单机 本地开发、小项目
Milvus HNSW/IVF/DiskANN ✅ ✅分布式 生产环境、大数据
Qdrant HNSW ✅磁盘 ✅分布式 新兴选择,API友好
Weaviate HNSW ✅磁盘 ✅分布式 GraphQL接口
Pinecone 托管 ✅云 ✅云 无需运维,按量付费
选型建议:
- 学习和原型:FAISS(最快上手)
- 小型生产:Chroma或Qdrant
- 大型生产:Milvus(最成熟)或Qdrant
- 不想运维:Pinecone
模块七:GraphRAG
什么是GraphRAG?
类比:从"图书馆"到"知识图谱"
传统RAG = 图书馆找书
把文档切成片段,用向量搜索找最相关的片段
问题:片段之间没有"关系"——不知道A片段和B片段有什么联系
GraphRAG = 知识图谱 + 向量搜索
不仅找到相关片段,还找到片段之间的"关系"
→ 能回答需要"综合多个信息源"的复杂问题
GraphRAG的核心思想
Step 1:从文档中提取实体和关系
文档:"张三是百度的CTO,百度是李彦宏创办的公司"
→ 实体:张三、百度、李彦宏
→ 关系:(张三, 是CTO, 百度), (百度, 创办者, 李彦宏)
Step 2:构建知识图谱
张三 ──CTO──→ 百度 ←──创办者── 李彦宏
Step 3:社区检测
把紧密相关的实体聚成"社区"
→ {张三, 百度, 李彦宏} 是一个社区
Step 4:为每个社区生成摘要
→ "百度是一家由李彦宏创办的公司,张三担任CTO"
Step 5:检索时同时搜索向量和图谱
问题:"张三在哪家公司工作?"
→ 向量搜索找到相关文档片段
→ 图谱搜索找到张三→百度的关系
→ 综合回答:"张三在百度担任CTO"
GraphRAG vs 传统RAG
特性 传统RAG GraphRAG
─────────────────────────────────────────────────
数据结构 文档片段(扁平) 知识图谱(结构化)
检索方式 向量相似度 向量 + 图遍历
多跳推理 困难 自然支持
全局摘要 不支持 支持(社区摘要)
适用场景 单文档问答 多文档、复杂关系推理
复杂度 低 高(需要构建图谱)
GraphRAG的实现
Microsoft的GraphRAG实现:
1. 使用LLM从文档中提取实体和关系
2. 构建知识图谱
3. 使用Leiden算法进行社区检测
4. 为每个社区生成LLM摘要
5. 检索时:局部搜索(向量+图谱)+ 全局搜索(社区摘要)
适用场景:
- "这个公司的组织架构是什么?" → 需要从多份文档中综合信息
- "这些产品的共同竞争对手是谁?" → 需要跨文档推理
- "总结一下这个领域的最新进展" → 需要全局视角
模块八:Advanced RAG工程实践
为什么需要"高级"RAG?
基础RAG的问题:
1. 检索质量不稳定——有时找到的文档不相关
2. 生成质量参差——有时回答不准确或"幻觉"
3. 系统鲁棒性差——输入格式变化就可能出错
4. 评估困难——不知道效果好不好,怎么改进
Advanced RAG = 在每个环节都做优化
检索优化策略
1. 查询改写(Query Rewriting)
原始问题:"这个东西怎么用?"
改写后:"XX产品的使用方法和操作步骤"
→ 让问题更明确,检索更精准
2. 查询扩展(Query Expansion)
原始问题:"RAG"
扩展为:["RAG", "检索增强生成", "Retrieval Augmented Generation"]
→ 多角度检索,提高召回率
3. 假设文档嵌入(HyDE)
先让LLM生成一个"假设的回答"
用这个假设回答去检索(而不是用问题检索)
→ 假设回答和真实文档的语义更接近
4. 上下文压缩(Contextual Compression)
检索到的文档块可能很长,只有部分与问题相关
用LLM提取出与问题最相关的部分
→ 减少噪音,提高回答质量
生成优化策略
1. 提示词优化
- 明确告诉模型"只基于提供的资料回答"
- 要求模型"如果不确定就说不知道"
- 指定输出格式(如"先总结再详细说明")
2. 引用追溯
要求模型在回答中标注信息来源
→ "根据文档A第3段..."、"根据文档B..."
→ 用户可以验证回答的准确性
3. 多轮验证
生成回答后,用另一个LLM调用检查回答是否与原文一致
→ 类似于"编辑审查"流程
RAG/Agent评估体系
面试必问:“你怎么衡量RAG的效果?”
RAG评估的三个维度:
1. 检索质量(Retrieval Quality)
- Recall@k:前k个检索结果中,包含正确答案的比例
- Precision@k:前k个检索结果中,真正相关的比例
- MRR(Mean Reciprocal Rank):正确答案排第几(越靠前越好)
- NDCG:考虑排名位置的评估指标
2. 生成质量(Generation Quality)
- Faithfulness(忠实度):回答是否基于检索到的文档(没有编造)
- Relevance(相关性):回答是否和问题相关
- Correctness(正确性):回答是否正确
- Completeness(完整性):回答是否完整
3. 端到端质量(End-to-End)
- Answer Correctness:最终回答是否正确
- Answer Relevance:最终回答是否和问题相关
评估工具:
RAGAS:最流行的RAG评估框架
- 自动生成评估数据集
- 计算Faithfulness、Relevance、Context Precision等指标
- 代码示例:
from ragas import evaluate
result = evaluate(dataset, metrics=[faithfulness, answer_relevancy])
Agent评估:
Agent评估比RAG更复杂,因为Agent涉及多步决策:
1. 任务完成率:Agent是否完成了用户交给它的任务?
2. 工具调用准确率:Agent是否选择了正确的工具?
3. 参数正确率:Agent传给工具的参数是否正确?
4. 步骤效率:Agent用了多少步完成任务?(越少越好)
5. 错误恢复:Agent遇到错误时能否自动修正?
评估方法:
- 人工评估:找人来判断Agent的表现(金标准,但成本高)
- 自动评估:用另一个LLM来评判Agent的表现(成本低,但可能不准)
- 基准测试:用标准化的测试集来评估(可比较,但覆盖有限)
📝 自测题
- LLaMA架构:解释RMSNorm与LayerNorm的区别,为什么LLaMA选择RMSNorm?用"收音机音量调节"的类比说明
- RoPE:用"时钟指针"的类比解释旋转位置编码,为什么它天然支持相对位置?
- KV Cache:解释KV Cache如何加速推理,用"翻译一本书"的类比说明
- GQA:用"参考资料共享"的类比解释MHA、MQA、GQA的区别
- 提示词工程:设计一个完整的结构化提示词,包含角色、任务、格式、约束四要素
- 思维链:解释Zero-shot CoT的原理,为什么"让我们一步一步思考"能提升效果?
- LangChain:用LCEL管道语法构建一个完整的RAG链(写代码)
- RAG:画出RAG的完整流程图(从文档处理到回答生成),标注每一步的作用
- 向量检索:解释混合检索(语义+关键词)为什么比单一检索效果好
- 综合:设计一个企业知识库问答系统的技术方案,包括文档处理、检索策略、生成策略
...