这页负责解释数据流和实现,不负责统一定义概念。 如果你还没建立 embedding / 余弦的直觉,先回到 总站首页 后进入 概念手册
适合任务
已经理解基础概念,准备把 V1 和 V2 的输入、输出和关键决策看清楚。
前置阅读
概念手册,尤其是 embedding、向量和余弦相似度部分。
Page Summary
V1 的核心
把“文本 → 向量 → 排序 → Prompt 注入”这条最小闭环看清楚。
V2 的核心
理解 chunk_size 和 overlap 如何影响检索命中的完整性,而不是只看代码写法。
必须记住的结论
RAG 质量的上限先由检索决定,而检索上限先由分块质量决定。
适合阶段
已经理解基础概念,准备把 V1 和 V2 数据流跑通。
相关文档
对应代码
`code/01_v1_最小RAG循环.py` 与 `code/02_v2_文档分块策略.py`。
Reading Modes
讲解模式
适合第一次学习。顺着 step 读,理解每段代码在数据流里的职责。
速查模式
复习时只看 `code-filename`、`insight` 和 V2 对比表,快速定位关键函数和结论。
实验模式
重点观察输入输出变化:Top-K 排序、chunk 边界、最终回答是否变完整。
最小 RAG 循环 "三步走完,RAG 的本质就在这里"
Embedding cosine_similarity Top-K 检索 Prompt 注入上下文 有/无 RAG 对比
STEP 01 文档 → Embedding 向量 把文本变成可以计算"距离"的数字
LLM 不能直接比较两段文字"意思有多像",但计算机可以计算两个向量的夹角。
Embedding 就是把文本压缩成一个高维向量的过程——语义相近的文本,向量的方向会相近。
text-embedding-3-small 输出 1536 维向量,每个维度捕捉语言的某种特征。
01_v1_最小RAG循环.py · embed() 函数 Python
# 知识库:5 段文本(真实场景来自 PDF/Wiki/手册)
DOCUMENTS = [
    "RAG 在生成前检索外部文档,让回答有据可查,有效减少幻觉。",
    "向量数据库通过余弦相似度找到语义最接近的文档块。",
    "LangChain 封装了文档加载、分块、检索、生成的完整流程。",
    "Transformer 的核心是注意力机制,让模型动态关注输入。",
    "RAG 的瓶颈在检索而非生成:垃圾进,垃圾出。",
]

def embed(text: str) -> np.ndarray:
    """
    调用 OpenAI Embedding API。
    为什么用 API 而不是本地模型?
    → 快速验证概念。v4 会讨论模型选型和本地部署。
    """
    response = client.embeddings.create(
        input=text,
        model="text-embedding-3-small"  # 1536 维,中英文均可
    )
    return np.array(response.data[0].embedding)  # shape: (1536,)

# 为所有文档建立索引
doc_embeddings = np.array([embed(doc) for doc in DOCUMENTS])
# doc_embeddings.shape → (5, 1536)   [文档数 × 向量维度]
预期输出(节选)
STEP 1 / 为知识库建立 Embedding 索引 模型: text-embedding-3-small | 文档数: 5 Doc[0] RAG 在生成前检索外部文档,让回... 维度=1536 示例(前4维)=[ 0.02341 -0.01823 0.04512 -0.00834] Doc[1] 向量数据库通过余弦相似度找到语... 维度=1536 示例(前4维)=[ 0.01902 -0.02341 0.03819 0.01023] ... → 知识库矩阵形状: (5, 1536) [文档数 × 向量维度]
🧠 认知对齐:5 段文字现在变成了一个 5×1536 的数字矩阵。每行是一篇文档的"语义指纹"。这个矩阵就是索引——真实系统中它会被存进向量数据库(v3 实现持久化)。
STEP 02 余弦相似度 + Top-K 检索 找到语义最相近的文档,不是字面匹配
余弦相似度公式(衡量两向量的语义距离):
cos(θ) = (a · b) / (|a| × |b|)
值域:1.0 = 完全相同方向(语义极相似) | 0.0 = 垂直(语义无关) | -1.0 = 相反方向
RAG 实践中,得分 > 0.75 通常才算真正相关。
01_v1_最小RAG循环.py · cosine_sim() + retrieve() Python
def cosine_sim(a: np.ndarray, b: np.ndarray) -> float:
    return float(
        np.dot(a, b)                               # 点积
        / (np.linalg.norm(a) * np.linalg.norm(b))  # 除以两向量的模
    )

def retrieve(query: str, doc_embeddings: np.ndarray, top_k=2):
    q_emb = embed(query)                            # 问题也转成向量
    scores = [cosine_sim(q_emb, de) for de in doc_embeddings]
    ranked = sorted(
        enumerate(scores),
        key=lambda x: x[1], reverse=True
    )[:top_k]                                      # 取最高的 K 个
    return [{"text": DOCUMENTS[i], "score": s} for i, s in ranked]
预期输出
STEP 2 / 语义检索 用户问题: RAG 系统里最关键的环节是什么? 全库相似度得分(热力条): [0] 0.8234 ████████████████████████████████ RAG 在生成前检索外部文档,让回答有据可查... [1] 0.7103 ████████████████████████ 向量数据库通过余弦相似度找到语义最接近的... [2] 0.6281 ████████████████████ LangChain 封装了文档加载、分块、检索... [3] 0.5410 █████████████████ Transformer 的核心是注意力机制... [4] 0.8567 ██████████████████████████████████ RAG 的瓶颈在检索而非生成:垃圾进,垃圾出... → Top-2 命中文档: ✦ [0.8567] RAG 的瓶颈在检索而非生成:垃圾进,垃圾出。 ✦ [0.8234] RAG 在生成前检索外部文档,让回答有据可查...
🧠 认知对齐:注意 Doc[4](垃圾进垃圾出)得分最高,因为它和"最关键的环节"语义最接近。Doc[3](Transformer)得分最低。这是语义检索,不是关键词匹配——问题里没有"瓶颈"这个词,但语义一致。
STEP 03 Prompt 注入上下文(RAG 的"增强") 把检索结果塞进 Prompt,扩展 LLM 的视野
这里是 RAG 中 "A"(Augmented 增强)的字面含义: 把检索到的真实资料拼进 Prompt,让 LLM 不需要从训练数据"回忆", 而是从当前上下文"阅读后回答"。

关键提示词设计:明确要求 LLM 只用参考资料,不要猜测。 这不是银弹(LLM 仍可能忽视指令),但是控制幻觉的标准做法。
01_v1_最小RAG循环.py · build_prompt() Python
def build_prompt(query: str, contexts: list) -> str:
    ctx = "\n".join(
        f"  [{i+1}] {c['text']}"
        for i, c in enumerate(contexts)
    )
    return f"""你是一个严谨的知识助手。请仅根据以下参考资料回答问题。
若参考资料中没有相关信息,请明确说"资料中未提及",不要猜测。

参考资料:
{ctx}

问题:{query}
回答:"""

# 构建后的 Prompt 长这样:
# ┌──────────────────────────────────────────────┐
# │ 你是一个严谨的知识助手...(系统指令)           │
# │ 参考资料:                                    │
# │   [1] RAG 的瓶颈在检索而非生成...             │  ← 检索结果注入
# │   [2] RAG 在生成前检索外部文档...             │  ← 检索结果注入
# │ 问题:RAG 系统里最关键的环节是什么?            │  ← 用户原问题
# └──────────────────────────────────────────────┘
STEP 04 对比实验:有 RAG vs 无 RAG 这是最重要的一步——看见差异
01_v1_最小RAG循环.py · 对比实验 Python
# temperature=0:让输出稳定,便于对比,不是随机创作
def chat(prompt: str) -> str:
    resp = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[{"role": "user", "content": prompt}],
        temperature=0,
    )
    return resp.choices[0].message.content.strip()

# 对比实验
ans_raw = chat(QUERY)              # ❌ 无 RAG,直接问
ans_rag = chat(prompt_rag)         # ✅ 有 RAG,带上下文
预期输出对比
❌ 无 RAG(直接问 LLM): RAG 系统中最关键的环节因不同视角而异。从数据质量来看, 知识库的构建质量至关重要;从系统设计来看,检索策略和 生成模型的协调同样关键;从用户体验来看... (泛泛而谈,没有明确结论) ✅ 有 RAG(基于检索内容): 根据参考资料,RAG 系统中最关键的环节是检索而非生成。 资料明确指出"垃圾进,垃圾出"——即使生成能力再强, 如果召回了错误的文档,最终回答也无法保证质量。
🧠 认知对齐:没有 RAG 时,LLM 用训练时学到的"泛知识"回答,倾向于面面俱到却不具体。有 RAG 后,LLM 被约束在检索到的资料里回答,答案更具体、有出处、可溯源。这就是 RAG 的核心价值。
V1 完整数据流 知识库文档 × 5 ↓ embed() × 5(API 调用) 向量矩阵 [5 × 1536] ← 这是"索引" ↑ 用户问题: "RAG 最关键的环节是什么?" ↓ embed()(API 调用) query 向量 [1536] ↓ cosine_sim() × 5 → 排序 Top-2 文档 ↓ build_prompt() 注入上下文 增强 Prompt(问题 + 参考资料) ↓ chat()(API 调用) 最终回答 ✓
V2 · 文档分块策略
文档加载与分块策略 "分块决定上限,垃圾进垃圾出"
chunk_size chunk_overlap 固定大小分块 句子感知分块 边界信息保护 分块对比实验
V1 的知识库只有 5 句话。真实场景:一份产品手册 100 页,一个 Wiki 几千篇,一个 PDF 几万字。
你不能把整篇文档塞进 Prompt(Token 限制 + 注意力稀释)。
解决方案:把长文档切成小块(Chunk),每块独立存入向量库,检索时只取相关的几块。

分块是 RAG 的第一道工程决策,直接决定检索质量的上限。
策略 A 固定大小分块(无 overlap) 最简单,但边界信息会丢失
02_v2_文档分块策略.py · chunk_fixed_no_overlap() Python
def chunk_fixed_no_overlap(text: str, chunk_size: int = 150) -> list[str]:
    """
    按字符数切割,走到哪切到哪。
    
    问题:可能在句子中间切断。
    例:"推荐分块大小在 200 到 500 个字符之间,并设置重叠"
    → 块 A:...推荐分块大小在 200 到 500
    → 块 B:个字符之间,并设置重叠...
    两个块都是残缺信息。
    """
    text = text.strip()
    chunks, start = [], 0
    while start < len(text):
        chunk = text[start : start + chunk_size].strip()
        if chunk: chunks.append(chunk)
        start += chunk_size    # 每次直接跳 chunk_size,无回退
    return chunks
⚠ 当 chunk_size=120,文档里"推荐分块大小在200到500个字符,设置10%~20%重叠"这句关键信息极有可能被切成两半,任何单块都拿不到完整的工程建议。
策略 B 固定大小 + Overlap(推荐生产基线) overlap 是解决边界问题的最简单方案
Overlap 的核心机制:下一个块的起点 = 上一个块的终点 - overlap。
即相邻两块有 overlap 个字符是重叠的。
这保证了边界附近的信息一定会完整出现在某个块里。
02_v2_文档分块策略.py · chunk_fixed_with_overlap() ← 关键行高亮 Python
def chunk_fixed_with_overlap(text: str, chunk_size=200, overlap=40):
    text = text.strip()
    chunks, start = [], 0
    while start < len(text):
        end = start + chunk_size
        chunks.append(text[start:end].strip())
        next_start = end - overlap    # ← 核心:回退 overlap 个字符再开始下一块
        if next_start <= start: next_start = start + 1  # 防死循环
        start = next_start
        if start >= len(text): break
    return [c for c in chunks if len(c) > 20]

# 直觉理解 overlap=40 的效果:
#
#  位置:  0        200      360      520  ...
#          ├────────┤         ├────────┤
#  块 A:  [0      :200]
#  块 B:          [160     :360]   ← 从 200-40=160 开始
#  块 C:                  [320   :520]   ← 从 360-40=320 开始
#
#  160~200 这 40 个字符,同时出现在块 A 和块 B 里。
#  任何跨越 200 这个边界的句子,都会完整保存在某个块中。
overlap 效果验证
STEP 2 / Overlap 的作用:保护边界信息 目标关键句:「推荐分块大小在200到500个字符之间,并设置约10%到20%的重叠区域」 策略A(120字无overlap):关键句完整存在? ❌ 被切断了 策略B(200字有overlap):关键句完整存在? ✅ 是 策略C(句子感知): 关键句完整存在? ✅ 是 策略A中被切断的情况: Chunk[X]: "...分块太大(如2000个字符):召回的块包含太多无关内容,LLM在 长上下文中丢失焦点,答案精度降低。工程实践建议:推荐分块大小在200到500" Chunk[X+1]: "个字符之间,并设置约10%到20%的重叠区域(overlap)防止边界信息..." → 看到了吗?数字建议被切成两截,两块都是残缺信息。
策略 C 句子感知分块 先分句,再合并,不在句子中间切断
02_v2_文档分块策略.py · chunk_by_sentence() Python
def chunk_by_sentence(text: str, max_chars: int = 300) -> list[str]:
    # 按中文标点分句,保留标点(lookahead 不消耗字符)
    sentences = re.split(r'(?<=[。!?;\n])', text)
    sentences = [s.strip() for s in sentences if s.strip() and len(s) > 5]

    chunks, current = [], ""
    for sent in sentences:
        if len(current) + len(sent) <= max_chars:
            current += sent           # 累积句子,不超限就继续加
        else:
            if current: chunks.append(current.strip())
            current = sent             # 超限,保存旧块,开新块
    if current.strip():
        chunks.append(current.strip()) # 最后一块别忘了
    return [c for c in chunks if len(c) > 20]
关键实验 分块策略 → 检索质量 → 回答质量因果链 这是 v2 最重要的实验
三种策略对同一问题的回答对比
问题: RAG 的分块策略有哪些工程建议?推荐用多大的分块和 overlap? ── 策略 A|固定 120 字,无 overlap(块太小)── [0.7821] "...推荐分块大小在200到500" ← 句子被切断了 [0.7640] "个字符之间,并设置约10%到20%..." ← 上一句的后半段 回答: 根据资料,分块大小建议在200到500... (注意:两个残缺的块凑在一起,LLM 可能拼出答案, 但也可能混乱,且没有"overlap"建议的完整上下文) ── 策略 B|固定 200 字 + 40 字 overlap── [0.8234] "...推荐分块大小在200到500个字符之间, 并设置约10%到20%的重叠区域(overlap)防止边界信息丢失。" 回答: 根据参考资料,RAG 的工程建议:分块大小推荐 200~500 字符, 并设置 chunk_size 10%~20% 的 overlap。 ── 策略 C|句子感知,max=300── [0.8401] 完整的"分块策略"段落(句子完整,语义最好) 回答: 根据资料,分块建议:①大小 200-500 字符; ②overlap 比例 10%-20%;③不同文档类型有不同最优策略。
🧠 认知对齐:分块太小,关键信息被切断,检索命中的是残缺内容;合理分块,关键信息完整保留,回答更准确。这就是"分块决定上限"——不管你的检索算法多好、Reranker 多精准,如果 chunk 里根本没有完整的答案,LLM 就算再聪明也无法凭空生成正确答案。
策略 chunk_size overlap 语义完整性 块数量 推荐场景
A 固定无重叠 任意 0 低(边界易断) 最少 快速原型
B 固定+overlap 200~500 10%~20% 中(推荐基线) 略多 通用文档
C 句子感知 max_chars 高(不切句子) 不固定 技术规范/手册
收尾 V2 的边界在哪里 → V3 要解决什么
现在的问题(V2 的局限) chunks 只存在内存里 ↓ 程序退出 → 全部消失 每次运行都要重新计算所有 chunk 的 embedding ↓ 100 块文档 = 100 次 API 调用(慢 + 贵) 没有持久化,不支持增量更新 ↓ 添加一篇新文档 = 重算所有文档(不可接受) V3 要解决:把 chunks + embeddings 存入 ChromaDB 新文档只算新文档的 embedding → 写入 → 不影响旧数据 程序重启 → 从磁盘加载 → 毫秒级恢复 支持 metadata 过滤:按来源、日期、部门筛选文档
下一步与回查
回到概念
如果你还在问“为什么文本能变成向量”,回到 概念手册