EP06. "RAG 基础:切块、向量化与检索流程"
🔒 登录后可标记已读- 文档太长时,把全部内容塞进一个 prompt 会撞到处理上限、拖慢速度、推高成本,Claude 处理超长 prompt 时表现还会变差——RAG(Retrieval Augmented Generation)就是先把文档切成小块,每次问题只喂最相关的那几块
- 切块(chunking)是整条 RAG pipeline 里最关键的一步:切不好,问「工程部去年做了什么」可能捞出不相关部门刚好也提到类似字眼的段落,答非所问
- Vertex 上生成 embedding 用的不是 Anthropic 的模型,而是 Google 的
text-embedding-005,走的是google-genai这个另外的 SDK,不是anthropic包 - 完整 RAG 流程:切块 → 生成 embedding → 正规化成单位向量 → 存进向量数据库 → 把用户问题也转成 embedding → 用 cosine similarity 找出最像的块 → 把这些块塞进 prompt 给 Claude
- 前置知识:EP01 的基本 API 调用;这篇笔记不需要提前懂线性代数,cosine similarity 的部分会从头讲
重点内容
为什么要用 RAG:文档太长,一个 prompt 塞不下
处理大文档最直接的做法是把全文整段塞进一个 prompt,但这样做问题很明显:
- Claude 处理长度有上限,超长文档根本放不下
- Prompt 越长,Claude 的表现反而会下降
- Token 用量越多,成本越高
RAG 的做法是反过来:先把文档预处理、切成小段,每次提问时只搜出跟这个问题相关的那几段,让 Claude 只需要专注在真正有用的信息上。
📌 RAG 的好处:能处理非常大的文档集合、缩小 prompt 尺寸降低成本、处理速度更快、能同时支持多份文档。代价是:需要提前把文档处理好、需要一套可靠的检索机制去找出相关片段、切出来的片段可能丢失周边上下文、切块策略选不好会直接影响检索准确度。
一套 RAG 系统的三个必要组件:① 文档预处理与切块、② 找出相关片段的检索机制、③ 挑出合适片段塞进 prompt 的逻辑。
切块策略怎么选
切块切不好,会把不相关的上下文一起塞进去,导致 Claude 答错方向。目标是让每个切块在语意上保持完整、检索时能提供有意义的上下文。三种主要策略:
| 策略 | 做法 |
|---|---|
| 按大小(Size-based) | 切成等长的字符串 |
| 按结构(Structure-based) | 按文档结构切(标题、段落、章节) |
| 按语意(Semantic-based) | 用 NLP 技术把意思相关的句子/段落分到一组 |
按大小切块
优点:不管什么类型的文档都能直接套用,简单可靠。缺点:词句可能被从中间切断,切出来的块会丢失章节标题这类重要上下文。解法是让相邻切块之间保留一段重叠(overlap),避免生硬截断。
def chunk_by_char(text, chunk_size=150, chunk_overlap=20):
chunks = []
start_idx = 0
while start_idx < len(text):
end_idx = min(start_idx + chunk_size, len(text))
chunk_text = text[start_idx:end_idx]
chunks.append(chunk_text)
start_idx = (
end_idx - chunk_overlap if end_idx < len(text) else len(text)
)
return chunks
按结构切块
利用文档本身的组织方式(标题、段落)来切。Markdown 文档的例子——按 ## 二级标题切开:
def chunk_by_section(document_text):
pattern = r'\n## '
return re.split(pattern, document_text)
局限:很多文档没有统一一致的结构标记,这个方法不是每种文档都适用。
按语意切块
先把文字拆成句子,用 NLP 技术算句子之间的语意相似度,再把相关的内容分到同一组。切出来的质量最高,但计算成本和实现复杂度也最高。
折中方案:按句子切块
def chunk_by_sentence(text, max_sentences_per_chunk=5, overlap_sentences=1):
sentences = re.split(r'(?<=[.!?])\s+', text)
chunks = []
start_idx = 0
while start_idx < len(sentences):
end_idx = min(start_idx + max_sentences_per_chunk, len(sentences))
current_chunk = sentences[start_idx:end_idx]
chunks.append(' '.join(current_chunk))
start_idx += max_sentences_per_chunk - overlap_sentences
if start_idx < 0:
start_idx = 0
return chunks
方法怎么选
| 文档情况 | 建议策略 |
|---|---|
| 结构一致(标题层级清楚) | 按结构切块 |
| 文档类型混杂 | 按句子切块,效果不错的折中方案 |
| 代码/技术类内容 | 按字符切块最可靠 |
| 格式未知/不确定 | 按字符切块最安全 |
💡 切块是一个迭代过程——先用最简单的策略跑起来,拿真实数据测试效果,再根据结果调整,不用一开始就追求完美方案。
Text Embedding:把文字变成一串数字
Text embedding 是「一段文字所含意思」的数字化表示。流程:把文字丢进 embedding 模型 → 拿到一串数字(每个数字介在 -1 到 +1 之间)→ 每个数字对应文字的某种特征/性质。
📌 这些数字具体各自代表什么意思,没人知道——embedding 能有效捕捉语意、拿来做相似度比较,但单看某一个数字本身,对人类来说是不可解读的。
在 Vertex 上生成 embedding,用的不是 Anthropic 模型,而是 Google 的 text-embedding-005,要另外装一个 SDK:
pip install google-genai
from google import genai
client = genai.Client(
project="YOUR_PROJECT_ID",
location="global",
vertexai=True
)
def generate_embedding(text):
response = client.models.embed_content(
model="text-embedding-005",
contents=text
)
if not response.embeddings:
return []
return [e.values for e in response.embeddings]
📌 这里用的是 google-genai 这个 Google 官方 SDK 的 genai.Client,跟前几篇笔记发文字请求用的 AnthropicVertex client 是两个不同的库——在 Vertex 上做 RAG,切块/检索这一层通常用 Google 自己的 embedding 模型,生成回答这一层才交给 Claude,两边各司其职。
完整 RAG 流程:6 步
预处理阶段(Step 1-3):文档切成小节 → 用 embedding 模型转成数字向量 → 正规化成单位向量后存进向量数据库
查询阶段(Step 4-5):用户问题用同一个 embedding 模型转成向量、同样正规化 → 向量数据库用 cosine similarity 找出最相似的已存向量
生成阶段(Step 6):把检索到的相关文字片段和用户原始问题一起组进 prompt,交给 Claude 生成最终回答
正规化(Normalization):把 embedding 向量缩放成长度(magnitude)为 1.0,这样向量都能画在一个单位圆上比较。
Cosine Similarity(余弦相似度):量的是两个向量之间的夹角,分数介在 -1 到 1 之间——1 代表方向完全相同,0 代表两者互相垂直(没关系)。
Cosine Distance(余弦距离):是相似度的反面(1 - cosine similarity),数值越小代表越相似。
📌 课程用两份虚构文档(医学研究 vs 软件工程)搭配二维 embedding 示范:一个关于软件工程的查询,能透过相似度匹配,准确捞出真正相关的那份文档段落,而不是医学研究那份。
实操示例:五步实现 RAG Pipeline
- 切块:
chunk_by_section(text)把文档切成逻辑段落 - 生成 embedding:
generate_embedding(chunks)把每个切块转成语意向量 - 存进向量数据库:把 embedding 和对应的原始文字一起存进 VectorIndex——原始文字内容一定要跟向量一起存
- 查询也要转成向量:用户问题用同一套 embedding 空间转换
- 相似度检索:用 cosine distance 算出最相似的几个切块
💡 Embedding 本身只是一串对人类没有直接意义的数字——这正是为什么一定要把原始文字内容跟向量一起存进数据库:检索结果返回时,程序需要有可读的文字内容展示给用户,不能只给一串数字。
📌 示范查询「软件工程部门去年做了什么」,系统会按距离分数(越小越相似)排序返回相关段落——这就是向量数据库在高维空间里找出语意上下文匹配内容的实际运作方式。
常见错误
- ❌ 在 Vertex 上想用
AnthropicVertexclient 生成 embedding——生成 embedding 要另外装google-genai、用genai.Client,这是 Google 自己的模型和 SDK,跟发对话请求走的不是同一条路 - ❌ 切块只存 embedding 数字、不存对应的原始文字——检索结果返回时会没有可读内容能展示给用户
- ❌ 不管文档类型统一用同一种切块策略——结构一致的文档硬用按字符切块,会丢掉标题这类有意义的边界信息
- ❌ 混淆 cosine similarity 和 cosine distance 的大小方向——similarity 越大越相似,distance 越小越相似,两者刚好相反
Sources
官方文档:
- Claude with Google Cloud's Vertex AI(Claude Academy 课程)— https://academy.claude.com/courses/claude-with-google-cloud-s-vertex-ai