diff --git a/.gitignore b/.gitignore
index 554c174..d53c9d1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,6 @@
# WorkBuddy / local assistant state
.workbuddy/
+AGENTS.md
# Temporary research clone
research/
diff --git a/AGENTS.md b/AGENTS.md
deleted file mode 100644
index ac20bdd..0000000
--- a/AGENTS.md
+++ /dev/null
@@ -1,133 +0,0 @@
-# Hello-AI Agent 写作指南
-
-## 项目定位
-
-Hello-AI 是面向中文小白的 AI / LLM 入门知识库。仓库即唯一内容源,MkDocs Material 构建为静态站点。
-
-## 写作原则(必须遵守)
-
-1. **先讲问题,再讲概念**:不要一上来堆定义,先说明这个概念解决什么困惑。
-2. **先路径,后百科**:告诉读者先学什么、后学什么、学到什么程度即可。
-3. **先能用,后讲原理**:先做出结果,再解释底层机制。
-4. **面向小白,但不装傻**:讲清楚,但不啰嗦;有例子,但不堆术语;可以口语化,但不要油。
-5. **每个新术语首次出现时**:提供中文解释 + 英文原文 + 简短定义 + 一个例子。
-6. **涉及模型/产品/API/数据时**:必须标注来源;如果是推测,明确写"推测"或"待验证"。
-7. **安全边界要写清楚**:涉及 API Key、数据上传、模型输出、自动化调用时,要提醒风险。
-8. **避免制造焦虑**:目标是降低入门门槛,不是用概念堆叠劝退新手。
-
-## 内容结构模板
-
-每篇正文必须包含以下结构(根据内容适当调整):
-
-```markdown
-# 标题
-
-> 一句话概括这页讲什么。
-
-## 这章解决什么问题
-
-(1-2 段,说明读者为什么要花时间读这页)
-
-## 核心概念解释
-
-### 概念 A
-...
-
-### 概念 B
-...
-
-## 最小示例 / 动手试试
-
-(给出可复现的代码、Prompt 示例或操作步骤)
-
-## 常见误区
-
-(至少 3 个常见误解 + 纠正)
-
-## 延伸阅读
-
-(链接到本仓库其他相关页面)
-
-## 练习题 / 小实验
-
-(至少 1 道思考题或动手实验)
-```
-
-## 插图规范
-
-### Mermaid 图(优先使用)
-
-以下类型的图优先用 Mermaid 直接在 Markdown 中绘制:
-
-- 流程图 (`flowchart` / `graph`)
-- 时序图 (`sequenceDiagram`)
-- 类图 (`classDiagram`)
-- 状态图 (`stateDiagram-v2`)
-- 用户旅程图 (`journey`)
-- 甘特图 (`gantt`)
-- 饼图 (`pie`)
-- Git 图 (`gitGraph`)
-- 思维导图 (`mindmap`)
-- 时间线 (`timeline`)
-- 象限图 (`quadrantChart`)
-- 需求图 (`requirementDiagram`)
-- C4 图 (`C4Context` / `C4Container`)
-- 桑基图 (`sankey-beta`)
-- XY 图表 (`xychart-beta`)
-- 块图 (`block-beta`)
-- 数据包图 (`packet-beta`)
-- 架构图 (`architecture-beta`)
-
-Mermaid 示例:
-```markdown
-```mermaid
-flowchart LR
- A[输入] --> B{判断}
- B -->|条件1| C[结果1]
- B -->|条件2| D[结果2]
-```
-```
-
-### 外部图片(按需下载)
-
-当概念需要真实照片、产品截图、历史图片等时,可搜索并下载图片。
-
-**命名规范**:`assets/images/<所属章节>/<位置_内容描述>.png`
-
-例如:
-- `assets/images/basics/what-is-ai/dartmouth_conference_1956.png`
-- `assets/images/basics/deep-learning/alexnet_architecture.png`
-
-**引用方式**:
-```markdown
-
-
-
图X:图片描述
-```
-
-## 标签规范
-
-每篇新页面顶部可添加标签:
-
-```yaml
----
-tags:
- - AI 基础
----
-```
-
-可用标签见 `mkdocs.yml` 的 `material/tags` 配置。
-
-## 事实核验规则
-
-- 模型名称要写清版本(如 GPT-4、Claude 3.5 Sonnet)。
-- API 和工具要标明更新时间。
-- 关键页面保留参考链接。
-- 过期内容要有版本意识,注明"截至 XXXX 年 X 月"。
-
-## 格式约定
-
-- 中英文之间加空格。
-- 代码块标注语言类型。
-- 列表层级清晰,不要嵌套超过 3 层。
-- 每页长度建议 1500~4000 字,不要太短也不要太长。
diff --git a/docs/assets/mermaid/.cache-key b/docs/assets/mermaid/.cache-key
index c774b6c..eccee7c 100644
--- a/docs/assets/mermaid/.cache-key
+++ b/docs/assets/mermaid/.cache-key
@@ -1 +1 @@
-be1bcd46e3b02a4fa2bba5cb7ebd9f6159b6b4050fe3744688f277d889372423
+866dfc20ff03579b4ed9fe88ba6b151b5790e9c1c1aec829d93657714545660f
diff --git a/docs/assets/mermaid/chat-products_mermaid_0_d921ced9.svg b/docs/assets/mermaid/chat-products_mermaid_0_d921ced9.svg
index 386b359..927df65 100644
--- a/docs/assets/mermaid/chat-products_mermaid_0_d921ced9.svg
+++ b/docs/assets/mermaid/chat-products_mermaid_0_d921ced9.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/docs/assets/mermaid/failure-cases_mermaid_0_6cb86e51.svg b/docs/assets/mermaid/failure-cases_mermaid_0_6cb86e51.svg
index fc8065e..5f5390d 100644
--- a/docs/assets/mermaid/failure-cases_mermaid_0_6cb86e51.svg
+++ b/docs/assets/mermaid/failure-cases_mermaid_0_6cb86e51.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/docs/assets/mermaid/index_mermaid_0_4bfd489a.svg b/docs/assets/mermaid/index_mermaid_0_4bfd489a.svg
index 6fd8e78..de7c5bd 100644
--- a/docs/assets/mermaid/index_mermaid_0_4bfd489a.svg
+++ b/docs/assets/mermaid/index_mermaid_0_4bfd489a.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/docs/assets/mermaid/index_mermaid_1_4c49cd69.svg b/docs/assets/mermaid/index_mermaid_1_4c49cd69.svg
index fffba13..cdeb56d 100644
--- a/docs/assets/mermaid/index_mermaid_1_4c49cd69.svg
+++ b/docs/assets/mermaid/index_mermaid_1_4c49cd69.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/docs/assets/mermaid/machine-learning_mermaid_2_6c878947.svg b/docs/assets/mermaid/machine-learning_mermaid_2_6c878947.svg
index 3247c8d..d4f7c59 100644
--- a/docs/assets/mermaid/machine-learning_mermaid_2_6c878947.svg
+++ b/docs/assets/mermaid/machine-learning_mermaid_2_6c878947.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/docs/assets/mermaid/model-data-training_mermaid_0_66650f38.svg b/docs/assets/mermaid/model-data-training_mermaid_0_66650f38.svg
index 6c2bca3..f2860dd 100644
--- a/docs/assets/mermaid/model-data-training_mermaid_0_66650f38.svg
+++ b/docs/assets/mermaid/model-data-training_mermaid_0_66650f38.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/docs/assets/mermaid/model-data-training_mermaid_1_fdc2dfb9.svg b/docs/assets/mermaid/model-data-training_mermaid_1_fdc2dfb9.svg
index 6a5961e..f8237c6 100644
--- a/docs/assets/mermaid/model-data-training_mermaid_1_fdc2dfb9.svg
+++ b/docs/assets/mermaid/model-data-training_mermaid_1_fdc2dfb9.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/docs/assets/mermaid/prompt-basic_mermaid_0_72623ff4.svg b/docs/assets/mermaid/prompt-basic_mermaid_0_72623ff4.svg
index 2b15343..bf9e7b9 100644
--- a/docs/assets/mermaid/prompt-basic_mermaid_0_72623ff4.svg
+++ b/docs/assets/mermaid/prompt-basic_mermaid_0_72623ff4.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/docs/assets/mermaid/prompt-basic_mermaid_1_5d4e2cd9.svg b/docs/assets/mermaid/prompt-basic_mermaid_1_5d4e2cd9.svg
index f2ad57a..bef95df 100644
--- a/docs/assets/mermaid/prompt-basic_mermaid_1_5d4e2cd9.svg
+++ b/docs/assets/mermaid/prompt-basic_mermaid_1_5d4e2cd9.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/docs/assets/mermaid/prompt-basic_mermaid_2_4fa92616.svg b/docs/assets/mermaid/prompt-basic_mermaid_2_4fa92616.svg
index c7d171b..7a3a825 100644
--- a/docs/assets/mermaid/prompt-basic_mermaid_2_4fa92616.svg
+++ b/docs/assets/mermaid/prompt-basic_mermaid_2_4fa92616.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/docs/assets/mermaid/stable-output_mermaid_0_a4883ca0.svg b/docs/assets/mermaid/stable-output_mermaid_0_a4883ca0.svg
index 02ff056..c502593 100644
--- a/docs/assets/mermaid/stable-output_mermaid_0_a4883ca0.svg
+++ b/docs/assets/mermaid/stable-output_mermaid_0_a4883ca0.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/docs/assets/mermaid/stable-output_mermaid_1_b549843d.svg b/docs/assets/mermaid/stable-output_mermaid_1_b549843d.svg
index 86a4eb1..c82f2fe 100644
--- a/docs/assets/mermaid/stable-output_mermaid_1_b549843d.svg
+++ b/docs/assets/mermaid/stable-output_mermaid_1_b549843d.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/docs/assets/mermaid/structure_mermaid_0_39cfc0b3.svg b/docs/assets/mermaid/structure_mermaid_0_39cfc0b3.svg
index 49bf645..f6ea97c 100644
--- a/docs/assets/mermaid/structure_mermaid_0_39cfc0b3.svg
+++ b/docs/assets/mermaid/structure_mermaid_0_39cfc0b3.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/docs/assets/mermaid/tasks_mermaid_0_f5e00efb.svg b/docs/assets/mermaid/tasks_mermaid_0_f5e00efb.svg
index 0e7e471..5b9edfc 100644
--- a/docs/assets/mermaid/tasks_mermaid_0_f5e00efb.svg
+++ b/docs/assets/mermaid/tasks_mermaid_0_f5e00efb.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/docs/assets/mermaid/temperature-sampling_mermaid_0_1fb265a4.svg b/docs/assets/mermaid/temperature-sampling_mermaid_0_1fb265a4.svg
index 3232de5..83c1433 100644
--- a/docs/assets/mermaid/temperature-sampling_mermaid_0_1fb265a4.svg
+++ b/docs/assets/mermaid/temperature-sampling_mermaid_0_1fb265a4.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/docs/assets/mermaid/temperature-sampling_mermaid_1_87f42f11.svg b/docs/assets/mermaid/temperature-sampling_mermaid_1_87f42f11.svg
index c8b46aa..3a5d4f6 100644
--- a/docs/assets/mermaid/temperature-sampling_mermaid_1_87f42f11.svg
+++ b/docs/assets/mermaid/temperature-sampling_mermaid_1_87f42f11.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/docs/assets/mermaid/temperature-sampling_mermaid_2_505dced2.svg b/docs/assets/mermaid/temperature-sampling_mermaid_2_505dced2.svg
index d2e8558..1cc02b9 100644
--- a/docs/assets/mermaid/temperature-sampling_mermaid_2_505dced2.svg
+++ b/docs/assets/mermaid/temperature-sampling_mermaid_2_505dced2.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/docs/assets/mermaid/temperature-sampling_mermaid_3_b20d5792.svg b/docs/assets/mermaid/temperature-sampling_mermaid_3_b20d5792.svg
index c92d5bf..f4187cc 100644
--- a/docs/assets/mermaid/temperature-sampling_mermaid_3_b20d5792.svg
+++ b/docs/assets/mermaid/temperature-sampling_mermaid_3_b20d5792.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/docs/assets/mermaid/temperature-sampling_mermaid_4_bfb6189a.svg b/docs/assets/mermaid/temperature-sampling_mermaid_4_bfb6189a.svg
index 3641607..cbaf5ec 100644
--- a/docs/assets/mermaid/temperature-sampling_mermaid_4_bfb6189a.svg
+++ b/docs/assets/mermaid/temperature-sampling_mermaid_4_bfb6189a.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/docs/assets/mermaid/templates_mermaid_0_bf562375.svg b/docs/assets/mermaid/templates_mermaid_0_bf562375.svg
index fce3092..add9566 100644
--- a/docs/assets/mermaid/templates_mermaid_0_bf562375.svg
+++ b/docs/assets/mermaid/templates_mermaid_0_bf562375.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/docs/assets/mermaid/token-embedding-context_mermaid_0_8e1cc234.svg b/docs/assets/mermaid/token-embedding-context_mermaid_0_8e1cc234.svg
index 5a47ea5..01f2dfe 100644
--- a/docs/assets/mermaid/token-embedding-context_mermaid_0_8e1cc234.svg
+++ b/docs/assets/mermaid/token-embedding-context_mermaid_0_8e1cc234.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/docs/assets/mermaid/token-embedding-context_mermaid_1_898036d2.svg b/docs/assets/mermaid/token-embedding-context_mermaid_1_898036d2.svg
index 34f235e..22b2c7e 100644
--- a/docs/assets/mermaid/token-embedding-context_mermaid_1_898036d2.svg
+++ b/docs/assets/mermaid/token-embedding-context_mermaid_1_898036d2.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/docs/assets/mermaid/token-embedding-context_mermaid_2_790c3adb.svg b/docs/assets/mermaid/token-embedding-context_mermaid_2_790c3adb.svg
index fe6ff03..3827ee2 100644
--- a/docs/assets/mermaid/token-embedding-context_mermaid_2_790c3adb.svg
+++ b/docs/assets/mermaid/token-embedding-context_mermaid_2_790c3adb.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/docs/rag/chunking.md b/docs/rag/chunking.md
index 9321544..b7cce9d 100644
--- a/docs/rag/chunking.md
+++ b/docs/rag/chunking.md
@@ -1,10 +1,245 @@
+---
+tags:
+ - RAG
+---
+
# 文档切分
-> 占位页:本页内容待团队补充。
+> 文档切分(Chunking)是把长文本切成小片段的过程——切得好,检索才能找得准;切得不好,后面的所有优化都救不回来。
+
+## 这章解决什么问题
+
+想象你要从一本 500 页的说明书里找到「保修期限」那一段。如果说明书被撕成了几百张纸条,每张只有一句话——你可能找不到完整的上下文。如果每张纸条是一整章——那你要的信息又和其他内容混在一起,不够精确。
+
+**文档切分就是找这个「平衡点」**:让每个片段足够小,能精确匹配用户问题;又足够大,包含完整的上下文信息。
+
+这是 RAG 系统里最重要的优化点之一。原因很简单:后续的检索、重排、生成都是建立在这些片段之上的。**切分策略直接决定了检索质量的上限。**
+
+## 核心概念
+
+### 切分粒度
+
+切分的「粗细」直接影响检索效果:
+
+| 粒度 | 典型大小 | 优点 | 缺点 |
+|------|---------|------|------|
+| **粗粒度** | 按章节或段落 | 上下文完整,不会切断关键语句 | 可能包含无关信息,检索精度下降 |
+| **中粒度** | 固定字符数(如 300~500) | 统一,便于批量处理 | 可能从句子中间切断 |
+| **细粒度** | 按句子或语义段落 | 精确匹配用户问题 | 缺少上下文,信息可能不完整 |
+
+没有绝对正确的粒度,取决于你的文档类型和用户提问方式。一般建议从 300~500 个 token 开始,根据检索效果调整。
+
+### 常见的切分策略
+
+**策略 1:固定长度切分**
+
+这是最简单的方案,按字符数或 Token 数硬切:
+
+```python
+def fixed_length_chunk(text, chunk_size=500, overlap=50):
+ """按固定字符数切分,带重叠"""
+ chunks = []
+ start = 0
+ while start < len(text):
+ end = start + chunk_size
+ chunks.append(text[start:end])
+ start = end - overlap # 前一个片段的尾部与后一个片段的头部重叠
+ return chunks
+```
+
+- 优点:实现简单,性能好
+- 缺点:可能从句子中间切断,丢失语义
+
+**策略 2:递归字符切分(Recursive Character Splitter)**
+
+LangChain 的 `RecursiveCharacterTextSplitter` 是最常用的方案。它按优先级依次尝试分隔符:`\n\n`(段落) → `\n`(换行) → `。`(句号) → ` `(空格),尽可能在语义边界处切分:
+
+```python
+from langchain.text_splitter import RecursiveCharacterTextSplitter
+
+text = """
+# 第一章:RAG 简介
+
+RAG 是检索增强生成。它通过检索外部知识来辅助生成。
+
+## 核心组件
+
+RAG 包含三个核心组件:检索器、生成器、知识库。
+
+### 检索器
+
+检索器负责根据用户问题找到最相关的文档片段。
+"""
+
+splitter = RecursiveCharacterTextSplitter(
+ chunk_size=100,
+ chunk_overlap=20,
+ separators=["\n\n", "\n", "。", " ", ""],
+)
+
+chunks = splitter.split_text(text)
+for i, chunk in enumerate(chunks):
+ print(f"--- Chunk {i+1} ({len(chunk)} 字符) ---")
+ print(chunk)
+```
+
+**策略 3:语义切分(Semantic Chunking)**
+
+利用 Embedding 模型探测段落之间的语义边界。当相邻几个句子的向量相似度有明显下降时,说明话题切换了,在此处切分:
+
+```python
+from langchain_experimental.text_splitter import SemanticChunker
+from langchain_openai.embeddings import OpenAIEmbeddings
+
+splitter = SemanticChunker(
+ OpenAIEmbeddings(model="text-embedding-3-small"),
+ breakpoint_threshold_type="percentile"
+)
+chunks = splitter.split_text(document)
+```
+
+**策略 4:按文档结构切分**
+
+对于有固定结构的文档(Markdown、HTML、代码),按标题层级切分最自然:
+
+```python
+from langchain.text_splitter import MarkdownHeaderTextSplitter
+
+headers_to_split_on = [
+ ("#", "H1"),
+ ("##", "H2"),
+ ("###", "H3"),
+]
+splitter = MarkdownHeaderTextSplitter(headers_to_split_on)
+chunks = splitter.split_text(markdown_document)
+```
+
+这种方式保证每个 chunk 在语义上是一个完整的「小节」,最适合结构化文档。
+
+### 重叠策略(Overlap)
+
+切分时让相邻片段有一部分重叠,避免关键信息恰好在切分边界处丢失:
+
+```mermaid
+flowchart LR
+ A[原始文档] --> B["Chunk 1
500 token"]
+ A --> C["Chunk 2
500 token
(与前一段重叠 50)"]
+ A --> D["Chunk 3
500 token
(与前一段重叠 50)"]
+ B -.->|重叠 50| C
+ C -.->|重叠 50| D
+```
+
+- 重叠大小通常设为 chunk 大小的 **10%~20%**
+- 重叠太少:边界信息仍有丢失风险
+- 重叠太多:索引体积增大,检索时重复内容增多,浪费上下文窗口
+
+### Chunk 大小怎么定
+
+chunk 大小没有一个标准值,但有一些经验参考:
+
+| 文档类型 | 建议 chunk 大小(token) | 理由 |
+|---------|------------------------|------|
+| 新闻文章 | 200~400 | 每段信息密度高,不宜太大 |
+| 技术文档 | 300~500 | 需要保留完整的技术说明 |
+| 学术论文 | 500~1000 | 摘要/方法/结论各成独立 chunk |
+| 代码文件 | 按函数或类切 | 代码的逻辑单元是函数 |
+| 对话记录 | 按轮次切 | 一问一答是一个语义单元 |
+
+最可靠的方案是做 A/B 测试:准备一组典型问题,分别用不同 chunk 大小检索,对比召回率。
+
+## 最小示例
+
+以下代码演示如何评估不同切分策略的效果:
+
+```python
+import openai
+import numpy as np
+
+# 原始文档
+document = """
+# RAG 入门指南
+
+## 什么是 RAG
+
+RAG(检索增强生成)是一种让 LLM 先查资料再回答的技术。
+
+## 为什么需要 RAG
+
+LLM 不知道你的私有数据,RAG 通过检索来解决这个问题。
+
+## 核心组件
+
+一个 RAG 系统由检索器、知识库和生成器三部分组成。
+"""
+
+# 尝试不同的切分策略
+from langchain.text_splitter import (
+ RecursiveCharacterTextSplitter,
+ MarkdownHeaderTextSplitter,
+)
+
+# 策略 A:递归字符切分
+r_splitter = RecursiveCharacterTextSplitter(
+ chunk_size=50, chunk_overlap=10
+)
+chunks_a = r_splitter.split_text(document)
+
+# 策略 B:按标题切分
+h_splitter = MarkdownHeaderTextSplitter([
+ ("#", "H1"), ("##", "H2")
+])
+chunks_b = h_splitter.split_text(document)
+
+print("策略 A 结果:")
+for i, c in enumerate(chunks_a):
+ print(f" [{i}] {c[:40]}...")
+
+print("\n策略 B 结果:")
+for i, c in enumerate(chunks_b):
+ print(f" [{i}] {c.page_content[:40]}...")
+```
+
+## 常见误区
+
+!!! failure "误区 1:chunk 越大越好"
+ chunk 越大,语义越完整,但检索精度越低。太大的 chunk 导致很多不相关的内容一起被搜到,LLM 会被噪声干扰。建议从 300~500 token 开始测试。
+
+!!! failure "误区 2:所有文档用同一种切分策略"
+ Markdown 文档适合按标题切,法律合同适合按条款切,代码适合按函数切。一刀切的策略往往效果最差。
+
+!!! failure "误区 3:切完就不管了"
+ 切分策略需要持续调试。建议定期采样检查 chunk 是否语义完整,用典型问题测试检索效果,根据实际反馈调整参数。
+
+## 延伸阅读
+
+- [向量化](vectorization.md) —— 切分后的文本如何变成向量
+- [检索](retrieval.md) —— 切分质量直接影响检索效果
+- [RAG 常见问题](troubleshooting.md)
+
+## 练习题
+
+??? question "练习 1:评估不同切分策略"
+
+ 找一篇 2000 字以上的文章(比如维基百科条目),用以下三种方式各切一次,并回答后续问题:
+
+ ```
+ - 方式 A:每 200 字符固定长度,无重叠
+ - 方式 B:每 200 字符,50 字符重叠
+ - 方式 C:按段落(\n\n 分隔符)
+ ```
+
+ 对每种方式随机挑 3 个 chunk,判断:
+
+ 1. 这个 chunk 的语义是否完整?有没有被切断的句子?
+ 2. 如果拿这个 chunk 去回答相关问题,信息够用吗?
+ 3. 哪种方式产生的 chunks 整体质量最高?
+
+??? question "练习 2:给不同文档选策略"
+
+ 假设你需要为以下三类文档设计切分策略:
-## 建议内容
+ - 一本编程书籍(Markdown 格式,有标题层级)
+ - 一年的客服对话记录(每段对话以时间戳开头)
+ - 一份法律合同(没有标题,但有条款编号 1.1, 1.2...)
-- 切分粒度
-- 重叠策略
-- 常见错误
-- 调试方法
+ 对每类文档,你会选择哪种切分策略?为什么?
diff --git a/docs/rag/generation.md b/docs/rag/generation.md
index bd8609f..ef94dda 100644
--- a/docs/rag/generation.md
+++ b/docs/rag/generation.md
@@ -1,10 +1,227 @@
-# 生成
+---
+tags:
+ - RAG
+---
-> 占位页:本页内容待团队补充。
+# 生成:LLM 基于资料组织答案
-## 建议内容
+> 生成(Generation)是 RAG 的最后一步——把检索到的资料和用户问题拼成 Prompt,让 LLM 基于事实生成答案。
-- 上下文拼接
-- 引用来源
-- 答案格式
-- 幻觉控制
+## 这章解决什么问题
+
+假设你已经做好了所有前置工作:文档切分得漂亮、向量化精确、检索返回了最相关的 3 个片段。但如果 Prompt 写得不好,LLM 可能忽略资料、编造答案、或者回答得啰嗦没重点。
+
+**生成阶段的任务是**:把资料、问题和指令精确地编排在一起,让 LLM 知道自己的角色、知道哪些是事实依据、知道什么情况下该说「不知道」。
+
+## 核心概念
+
+### RAG Prompt 的三大要素
+
+一个标准的 RAG Prompt 包含三部分:
+
+1. **指令(Instruction)**:告诉 LLM 它的角色和任务
+2. **资料(Context)**:检索到的文档片段
+3. **问题(Question)**:用户的原问题
+
+基础模板示例:
+
+```python
+prompt_template = """
+你是一个基于资料的客服助手。请用以下资料回答用户问题。
+
+资料:
+{context}
+
+用户问题:{question}
+
+要求:
+- 只根据资料内容回答,不要自行编造
+- 如果资料中没有足够信息,请说「我找不到相关信息」
+- 如果可能,在回答末尾标注信息来源
+- 保持回答简洁、专业
+"""
+```
+
+### Prompt 设计的关键原则
+
+**原则 1:资料比问题更重要**
+
+研究表明,将资料放在问题之前,LLM 会更倾向于使用资料。反之,如果问题在前,LLM 可能更依赖自己的训练数据:
+
+```python
+# ✅ 推荐:资料在前
+prompt = f"资料:{context}\n\n基于以上资料,回答:{question}"
+
+# ❌ 不推荐:问题在前
+prompt = f"问题:{question}\n\n另外,这里有参考资料:{context}"
+```
+
+**原则 2:明确拒绝编造的边界**
+
+RAG 的一个核心目的是减少幻觉。但 LLM 的「续写」本能仍然存在。需要在 Prompt 里明确加一条边界:
+
+```
+- 如果资料中没有相关信息,请说「根据现有资料,我无法回答这个问题」
+- 不要推测或编造资料中不存在的信息
+```
+
+**原则 3:控制回答格式**
+
+不指定格式的话,同一个问题可能今天得到一个段落、明天拿到一个列表。常用的格式控制:
+
+```
+- 请用 Markdown 格式回答
+- 对于多个要点,用有序列表组织
+- 引用来源请用 [来源 X] 的格式标注
+```
+
+**原则 4:来源标注**
+
+让 LLM 标注回答中每一部分信息的来源,可以提高可信度并方便用户验证:
+
+```
+- 在回答末尾添加:\n\n(来源:{source_1}、{source_2})
+- 引用具体信息时标注对应的文档编号
+```
+
+### 幻觉(Hallucination)在 RAG 中如何产生
+
+即使有了 RAG,幻觉仍然可能发生。常见原因:
+
+| 原因 | 表现 | 解决办法 |
+|------|------|---------|
+| 检索到的资料与问题不相关 | LLM 忽略资料凭记忆回答 | 改善检索/重排 |
+| 资料正确但 LLM 没用它 | LLM 自己编了一个「更合理」的答案 | 强化 Prompt 中「只根据资料回答」的约束 |
+| 资料中存在矛盾信息 | LLM 尝试调和矛盾,产生不准确描述 | 确保切分不产生矛盾片段 |
+| 问题中包含 LLM 训练数据中的强关联 | LLM 用训练数据覆盖了资料信息 | 在 Prompt 中说「忽略你的训练数据,只使用以下资料」 |
+
+### 事实性校验(Groundedness Checking)
+
+在一些高风险场景(医疗、法律、金融),可以在生成后加一道校验步骤:
+
+```python
+check_prompt = f"""
+请判断以下 LLM 回答是否完全基于提供的资料。用 Yes/No 回答,并说明理由。
+
+资料:
+{context}
+
+LLM 回答:
+{answer}
+
+判断是否有内容在资料中没有依据:
+"""
+```
+
+只有在校验通过时才向用户展示回答。这虽然增加了额外成本,但在关键场景中是值得的。
+
+### 温度与生成参数
+
+| 参数 | 推荐值 | 理由 |
+|------|--------|------|
+| `temperature` | 0.1~0.3 | 低温度减少幻觉,让 LLM 更忠实于资料 |
+| `top_p` | 0.1~0.3 | 与低温度配合,进一步约束输出的随机性 |
+| `max_tokens` | 视需要 | 限制回答长度,避免漫无边际 |
+| `frequency_penalty` | 0 | RAG 本身已限制输入,不需要额外惩罚 |
+| `presence_penalty` | 0 | 同上 |
+
+!!! warning "不要用默认 temperature"
+ 很多 API 的默认 temperature 是 1.0。对于 RAG 场景,这个值偏高——LLM 会更「有创意」,但也更容易偏离资料。建议统一设为 0.3。
+
+## 最小示例
+
+完整的 RAG 生成流程,包含资料编排和校验:
+
+```python
+import openai
+
+# ── 1. 检索到的资料 ──
+context = """
+RAG(Retrieval-Augmented Generation)是一种检索增强生成技术。
+它由检索器、知识库和生成器三部分组成。
+RAG 的优势在于可以引入外部知识,减少模型幻觉。
+"""
+
+question = "RAG 由哪些部分组成?"
+
+# ── 2. 编排 Prompt ──
+prompt = f"""
+你是一个专业的 AI 知识助手。请严格根据以下资料回答问题。
+
+{context}
+
+用户问题:{question}
+
+要求:
+1. 只根据资料内容回答,不要添加资料中没有的信息
+2. 如果资料不足以回答问题,请说「我找不到相关信息」
+3. 回答要简洁、准确
+"""
+
+# ── 3. 生成 ──
+response = openai.chat.completions.create(
+ model="gpt-4o-mini",
+ messages=[{"role": "user", "content": prompt}],
+ temperature=0.3,
+)
+
+answer = response.choices[0].message.content
+print(answer)
+
+# ── 4. 事实性校验(可选)──
+check = openai.chat.completions.create(
+ model="gpt-4o-mini",
+ messages=[{
+ "role": "user",
+ "content": f"""判断以下回答是否完全基于资料。
+资料:{context}
+回答:{answer}
+请用 Yes/No 回答。"""
+ }],
+ temperature=0,
+)
+print(f"事实性校验通过:{check.choices[0].message.content}")
+```
+
+## 常见误区
+
+!!! failure "误区 1:所有 LLM 对同样的 Prompt 表现一样"
+ 不同模型在「忠实于资料」的能力上差异很大。小模型更容易忽略资料直接凭记忆回答。建议测试时对比至少 2~3 个模型的回答质量。
+
+!!! failure "误区 2:Prompt 写一次就够了"
+ RAG 的 Prompt 需要持续调整。当你更换知识库、LLM 或业务场景时,都需要重新评估 Prompt 的效果。
+
+!!! failure "误区 3:temperature=0 就能完全消除幻觉"
+ temperature=0 只是让输出确定,但不能保证输出正确。如果资料缺失或 Prompt 没有约束「必须以资料为准」,LLM 仍然可能自信地编造。
+
+## 延伸阅读
+
+- [为什么需要 RAG](why-rag.md) —— RAG 的动机和定位
+- [RAG 常见问题](troubleshooting.md) —— 从「回答不好」反向排查故障
+- [什么是 Prompt](../prompt/index.md) —— Prompt 工程的核心思想
+- [结构化的 Prompt](../prompt/structure.md) —— 如何系统化设计 Prompt
+
+## 练习题
+
+??? question "练习 1:对比 Prompt 设计的效果"
+
+ 设计三个不同的 RAG Prompt 模板来回答同一个问题:
+
+ - Prompt A:只有「请基于以下资料回答」这句话,没有其他约束
+ - Prompt B:加了「只根据资料回答,不要编造」的约束
+ - Prompt C:B 的基础上加了「标注来源」和「如果不知道就说不知道」
+
+ 用同一个资料和问题分别测试,对比三次回答的准确性和格式。
+
+??? question "练习 2:设计你自己的 RAG Prompt"
+
+ 选择一个你熟悉的业务场景(如产品客服、学习助手、文档摘要),写出一个完整的 RAG Prompt 模板,包含:
+
+ 1. 系统指令(角色和任务)
+ 2. 资料占位符
+ 3. 问题占位符
+ 4. 输出格式要求
+ 5. 边界条件(什么情况拒绝回答)
+
+ 提交之前找一个人review你的模板,让对方判断:
+ 「在不看资料的情况下,只看你的 Prompt,能不能清楚地知道模型应该做什么、不该做什么?」
diff --git a/docs/rag/index.md b/docs/rag/index.md
index 425d4b5..1c31151 100644
--- a/docs/rag/index.md
+++ b/docs/rag/index.md
@@ -1,45 +1,207 @@
+---
+tags:
+ - RAG
+---
+
# RAG 总览
-RAG 是 Retrieval-Augmented Generation 的缩写,中文常译为“检索增强生成”。
+> RAG(Retrieval-Augmented Generation,检索增强生成)是一种让大语言模型先查资料再回答的技术架构。它不是某个具体的产品,而是一套解决问题的思路。
+
+## 这章解决什么问题
+
+假设你是一家公司的技术支持,每天需要回答客户关于产品使用的问题。你的知识库里有完整的用户手册,但你不能把整本手册塞进聊天框。
+
+你试过直接问 ChatGPT,但它的回答要么太通用(不知道你的产品细节),要么直接编造。你也想过把所有文档喂给模型,但上下文窗口放不下,而且用户只想知道某一个功能怎么用,不需要整本手册。
+
+RAG 就是来解决这个矛盾的:**不让模型凭记忆回答,而是先从一个知识库里找到相关的几段资料,再让模型基于这些资料来组织答案。**
+
+```mermaid
+flowchart LR
+ A[用户提问] --> B[将问题转为向量]
+ B --> C[在知识库中搜索
最相似的片段]
+ C --> D[返回 Top-K
相关片段]
+ D --> E[将片段 + 问题
拼成 Prompt]
+ E --> F[LLM 基于资料
生成回答]
+ F --> G[带引用来源的答案]
+```
+
+这章是 RAG 模块的入口。读完这章,你会知道:
+
+- RAG 解决什么问题、不能解决什么问题
+- 一个完整 RAG 系统由哪些环节组成
+- 每个环节之间的关系和常见坑
+- 学完本模块后你应该能做什么
+
+## 核心概念
+
+### 什么是 RAG
+
+RAG 是 **Retrieval-Augmented Generation** 的缩写,中文叫**检索增强生成**。名字本身就是它的工作原理拆解:
+
+| 环节 | 英文 | 做什么 |
+|------|------|--------|
+| **检索** | Retrieval | 从知识库里找到与问题最相关的文档片段 |
+| **增强** | Augmented | 把检索到的资料作为额外信息拼入 Prompt |
+| **生成** | Generation | LLM 基于原始问题 + 检索资料生成最终答案 |
+
+这三个环节缺一不可。没有检索,模型只能凭训练数据回答(不知道你的私有数据);没有增强,检索结果的利用率低;没有生成,你得到的是原文片段而不是答案。
+
+### 一个 RAG 系统的五步流程
+
+!!! note "RAG 的标准流程"
+ 1. **文档切分(Chunking)**:把长文档切成合适大小的片段
+ 2. **向量化(Vectorization)**:用 Embedding 模型把文本片段转成向量,存入向量数据库
+ 3. **检索(Retrieval)**:用户提问时,把问题也转成向量,在数据库中找最相似的前 K 条
+ 4. **重排(Rerank)**:用更精确的模型对检索结果重新排序,把最相关的排前面
+ 5. **生成(Generation)**:把问题 + 精选后的资料拼成 Prompt,交给 LLM 生成答案
+
+每个步骤都有独立的优化空间。切分的粒度影响检索精度,Embedding 模型的选择影响语义匹配质量,重排决定最终给 LLM 的资料质量,Prompt 设计影响最终答案的可读性和准确性。
+
+### RAG vs 直接问 LLM vs 微调
-它解决的是:模型不一定知道你的私有资料,怎么让它先查资料,再组织答案。
+新手最容易混淆这三者的区别。我们用一个具体场景来说明:
-## 它在解决什么问题
+> 场景:你是某 SaaS 公司的客服,用户问「我们的年付套餐能退款吗?」
-LLM 有几个天然限制:
+| 方式 | 过程 | 结果 |
+|------|------|------|
+| **直接问 LLM** | 模型凭训练数据回答 | 可能说「一般可以退款」,但你们公司的政策是「年付超过 30 天不退」 |
+| **RAG** | 先从公司知识库检索退款政策 → 再把政策原文给 LLM → LLM 据此回答 | 「年付套餐购买后 30 天内可全额退款,超过 30 天按剩余天数比例折算」+ 来源标注 |
+| **微调(Fine-tuning)** | 用公司历史客服对话训练模型 | 模型学会了回复风格和专业术语,但新政策的更新需要重新训练 |
-- 不一定知道最新信息
-- 不一定知道你的私有文档
-- 直接回答时可能会“编”
+| 维度 | 直接问 LLM | RAG | 微调 |
+|------|-----------|-----|------|
+| 引入新知识 | ❌ 训练后才知 | ✅ 更新知识库即可 | ❌ 需重新训练 |
+| 知识透明度 | ❌ 黑盒 | ✅ 可追溯来源 | ❌ 知识嵌入参数 |
+| 维护成本 | 低 | 中(维护知识库) | 高(训练 + 部署) |
+| 适合场景 | 常识问答、翻译、创意 | 知识问答、客服、文档解读 | 格式转换、风格迁移、分类 |
+| 延迟 | 最低 | 中(多一次检索) | 低 |
-RAG 的思路就是先检索,再生成,把外部资料接进来。
+## RAG 模块的学习路径
-## 最小流程
+本模块共 8 页,建议按以下顺序阅读:
-1. 切分文档
-2. 向量化
-3. 检索
-4. 重排
-5. 生成
+```
+RAG 总览(当前页)
+ └→ 为什么需要 RAG —— 理解 RAG 的适用场景和边界
+ └→ 文档切分 —— 切得好,检索才能找得准
+ └→ 向量化 —— 把文本变成向量
+ └→ 检索 —— 在向量库中找最相关的片段
+ └→ 重排 —— 对初步结果做精细排序
+ └→ 生成 —— LLM 根据资料组织答案
+ └→ RAG 常见问题 —— 调试与故障排查
+```
-## 一个最小系统长什么样
+如果你时间有限,优先读:为什么需要 RAG → 文档切分 → 检索 → 生成。这四页覆盖了 RAG 的核心决策点。
-- 文档库:存你的资料
-- 检索器:找相关片段
-- 生成器:把片段整理成答案
+## 最小示例
-这三块缺一块,RAG 就不完整。
+以下代码演示了一个极简 RAG 流程。它不做重排、不连外部向量数据库,但能让你直观感受「检索 → 增强 → 生成」的完整链路:
-## 你先记住
+```python
+import openai
+import numpy as np
-- 文档不是直接喂给模型就完事
-- 检索质量会直接影响答案质量
-- 切分、召回、重排都很关键
+# ── 1. 准备知识库片段 ──
+chunks = [
+ "RAG 是 Retrieval-Augmented Generation 的缩写,中文叫检索增强生成。",
+ "文档切分(Chunking)是把长文本切成小片段的过程。",
+ "向量化(Vectorization)是把文本转换成数值向量的过程。",
+ "检索(Retrieval)是通过向量相似度找到相关文本片段。",
+ "重排(Rerank)是对检索结果做二次排序,提高精度。",
+ "生成(Generation)是 LLM 基于资料组织最终答案。",
+]
+
+# ── 2. 向量化:将文本转为向量 ──
+response = openai.embeddings.create(
+ model="text-embedding-3-small", input=chunks
+)
+chunk_vectors = np.array([d.embedding for d in response.data])
+
+# ── 3. 检索:找与问题最相似的片段 ──
+query = "RAG 是什么意思?"
+query_resp = openai.embeddings.create(
+ model="text-embedding-3-small", input=[query]
+)
+query_vec = np.array(query_resp.data[0].embedding)
+
+# 用余弦相似度找到最匹配的片段
+similarities = np.dot(chunk_vectors, query_vec) / (
+ np.linalg.norm(chunk_vectors, axis=1) * np.linalg.norm(query_vec)
+)
+best_idx = int(np.argmax(similarities))
+best_chunk = chunks[best_idx]
+
+print(f"检索到的片段:{best_chunk}")
+
+# ── 4. 生成:基于资料回答 ──
+prompt = f"基于以下资料回答问题:\n{best_chunk}\n\n问题:{query}"
+answer = openai.chat.completions.create(
+ model="gpt-4o-mini",
+ messages=[{"role": "user", "content": prompt}],
+ temperature=0.3,
+)
+print(f"LLM 回答:{answer.choices[0].message.content}")
+```
+
+!!! warning "API Key 安全提示"
+ 上面的代码使用了 OpenAI API。在生产环境中,API Key 应通过环境变量读取,不要硬编码在代码里。详见 [API 入门](../tools/api.md)。
## 学完这一章的目标
-你应该能回答:
+读完整个 RAG 模块,你应该能:
+
+1. **说清 RAG 是什么**:能用一句话解释 RAG 的原理,以及它解决了什么问题
+2. **画出一个 RAG 系统的流程图**:能说出完整的 5 个环节,并理解每个环节的作用
+3. **区分 RAG 与微调**:知道什么场景用 RAG、什么场景用微调
+4. **搭建一个最小 RAG 原型**:能用 LangChain 或直接调用 API 搭建一个简单的 RAG 系统
+5. **定位 RAG 故障**:当回答质量差时,能判断是切分、检索还是生成的问题
+
+## 常见误区
+
+!!! failure "误区 1:RAG 能解决所有「模型不知道」的问题"
+ 如果知识库里根本没有相关信息,检索结果为空,RAG 也无能为力。RAG 的前提是「库里有,但要能找到」。
+
+!!! failure "误区 2:RAG = 把文档全塞进 Prompt"
+ RAG 是先检索再生成,不是把所有文档拼进上下文。不分青红皂白地塞入大量无关内容,反而会引入噪声、降低回答质量。
+
+!!! failure "误区 3:有了 RAG 就不需要微调"
+ 两者解决不同的问题。RAG 解决「不知道私有知识」的问题(知识维度),微调解决「不擅长某种输出格式」的问题(能力维度)。格式转换、风格迁移等任务更适合微调。
+
+## 延伸阅读
+
+- [为什么需要 RAG](why-rag.md) —— 深入理解 RAG 的适用场景和边界
+- [什么是 LLM](../basics/what-is-llm.md) —— 理解 LLM 的能力边界是理解 RAG 的前提
+- [函数调用与工具调用](../tools/tool-calling.md) —— RAG 常与工具调用配合使用
+- [Chat、Copilot、Agent 与 Workflow](../tools/workflow.md) —— RAG 在更大工作流中的位置
+
+## 练习题
+
+??? question "练习 1:判断场景是否适合用 RAG"
+
+ 以下场景你会选择 RAG、直接问 LLM 还是微调?为什么?
+
+ 1. 做一个公司内部 HR 政策问答机器人
+ 2. 让模型用莎士比亚风格写产品描述
+ 3. 根据最新研究论文回答读者提问
+ 4. 把用户查到的天气信息用口语说出来
+
+ ??? done "参考答案"
+
+ | 场景 | 方案 | 原因 |
+ |------|------|------|
+ | 1 | RAG | HR 政策经常更新,放知识库方便维护,不改模型 |
+ | 2 | 微调或写好 few-shot Prompt | 风格问题,不需要外部知识 |
+ | 3 | RAG | 论文内容是 LLM 训练数据之外的,需要先检索 |
+ | 4 | 直接 LLM | 天气信息已经通过 API 或工具调用拿到了,LLM 只需重新组织语言 |
+
+??? question "练习 2:拆解 RAG 流程"
+
+ 找一个你日常遇到的「需要查资料才能回答」的问题(比如产品使用疑问),手动模拟 RAG 流程:
+
+ 1. 你的「知识库」是什么?(比如产品手册、帮助中心)
+ 2. 你会切成多大一段作为检索单元?
+ 3. 如果让你写代码实现,你会用什么 Embedding 模型和向量数据库?
+ 4. LLM 生成时,你会怎么写 Prompt 来约束它?
-- 为什么需要 RAG
-- 一个最小 RAG 系统长什么样
-- 哪些错误会让 RAG 变得很差
+ 不需要真的写代码,把思路写下来就行。
diff --git a/docs/rag/rerank.md b/docs/rag/rerank.md
index d56b946..6914eba 100644
--- a/docs/rag/rerank.md
+++ b/docs/rag/rerank.md
@@ -1,10 +1,193 @@
+---
+tags:
+ - RAG
+---
+
+# 重排:对检索结果做精细化排序
+
+> 重排(Rerank)是对初步检索结果进行二次排序的过程,用更精确的模型将最相关的文档片段排在前面。
+
+## 这章解决什么问题
+
+向量检索(第一阶段检索)速度快但精度有限。它的相似度是基于 Embedding 向量的整体语义匹配,可能把「苹果很好吃」和「苹果发布了新手机」也算得很近——它们都提到了苹果,但语义方向完全不同。
+
+而 LLM 的上下文窗口有限,只能接受 3~5 个片段。如果最相关的内容被排在倒数第一,LLM 可能漏掉关键信息。
+
+**重排就是解决这个矛盾的**——先用快速的向量检索从百万级候选中召回几十个候选(保召回率),再用精确的交叉编码器排序模型从中精挑细选出最有用的几个(保精确率)。
+
+## 核心概念
+
+### 两阶段检索架构
+
+```mermaid
+flowchart LR
+ subgraph 第一阶段_检索
+ A[用户问题] --> B[Embedding 模型
语义检索]
+ B --> C[Top-50 候选]
+ end
+ subgraph 第二阶段_重排
+ C --> D[交叉编码器
逐一计分]
+ D --> E[Top-3 精选结果]
+ end
+ E --> F[LLM 生成答案]
+```
+
+第一阶段(检索):用 Embedding 模型快速召回 Top-50 ~ Top-100
+
+第二阶段(重排):用交叉编码器(Cross-Encoder)对每个候选逐条打分,只保留 Top-3 ~ Top-5
+
+### 向量检索 vs 重排模型
+
+| 维度 | 向量检索(Bi-Encoder) | 重排模型(Cross-Encoder) |
+|------|----------------------|--------------------------|
+| 处理方式 | 问题和文档分别编码 | 问题和文档拼接后一起编码 |
+| 编码速度 | 快(可预先计算文档向量) | 慢(无法预计算,需实时编码) |
+| 精度 | 中,丢失交互信息 | 高,能捕捉 query-doc 的语义交互 |
+| 适用阶段 | 候选生成(速度快,处理海量数据) | 候选排序(精度高,处理少量数据) |
+
+重排的本质是用精度换时间:把需要高精度的运算限制在少量候选项上。
+
+### 重排的工作原理
+
+以 `BAAI/bge-reranker-v2-m3` 为例:
+
+```
+[CLS] 问题:什么是 RAG? [SEP] 资料:RAG 是检索增强生成 [SEP]
+ ↓
+ Cross-Encoder 模型
+ ↓
+ 0.92(相关度分数)
+```
+
+输入是问题和文档拼接后的文本对,输出是 0~1 之间的相关度分数。模型能同时看到问题中的每个词和文档中的每个词,捕捉它们之间的交互关系。这就是为什么交叉编码器比双编码器精度更高的原因。
+
+### 常见重排模型
+
+| 模型 | 语言 | 特点 |
+|------|------|------|
+| `BAAI/bge-reranker-v2-m3` | 中英多语言 | 轻量,适合中文场景 |
+| `BAAI/bge-reranker-v2-gemma` | 多语言 | 更强但更重 |
+| `Cohere Rerank` | 多语言 | 云端 API,无需本地部署 |
+| `cross-encoder/ms-marco-MiniLM-L6-v2` | 英文 | 经典的 MS MARCO 排名模型 |
+| `Jina Reranker` | 多语言 | 开箱即用,支持长文本 |
+
+### 重排策略
+
+**策略 1:重排后截断**
+
+最常见的方式。检索返回 Top-50 → 重排 → 取 Top-3:
+
+```python
+from langchain.retrievers import ContextualCompressionRetriever
+from langchain_community.cross_encoders import HuggingFaceCrossEncoder
+from langchain.retrievers.document_compressors import CrossEncoderReranker
+
+retriever = vectorstore.as_retriever(search_kwargs={"k": 20})
+
+reranker = CrossEncoderReranker(
+ model=HuggingFaceCrossEncoder(model_name="BAAI/bge-reranker-v2-m3"),
+ top_n=3,
+)
+compression_retriever = ContextualCompressionRetriever(
+ base_compressor=reranker,
+ base_retriever=retriever,
+)
+
+results = compression_retriever.invoke("什么是 RAG?")
+```
+
+**策略 2:用 LLM 做重排**
+
+你不一定要用专门的 Rerank 模型。LLM 也可以做排序——虽然慢一些,但理解能力更强:
+
+```python
+rerank_prompt = f"""
+请根据相关性对以下文档片段排序(从最相关到最不相关)。
+返回排序后的序号。只返回序号,用逗号分隔。
+
+问题:{query}
+
+文档:
+1: {doc_1}
+2: {doc_2}
+3: {doc_3}
+"""
+```
+
+这种方式的优点是零配置,缺点是成本高、延迟大。
+
+**策略 3:MMR(最大边际相关性)**
+
+MMR 不追求「最相关」,而是追求「相关信息最大覆盖」——即在相关性高的前提下,尽量选择候选之间差异大的:
+
+```python
+from langchain.vectorstores import Chroma
+
+retriever = vectorstore.as_retriever(
+ search_type="mmr",
+ search_kwargs={"k": 5, "fetch_k": 20, "lambda_mult": 0.7}
+)
+```
+
+`lambda_mult=0.7` 表示 70% 权重给相关性,30% 权重给多样性。你在处理多角度问题(如「介绍一下 RAG 的优缺点」)时,MMR 会让回答更全面。
+
+## 最小示例
+
+```python
+from sentence_transformers import CrossEncoder
+
+# 准备数据和模型
+query = "RAG 的主要优势是什么?"
+candidates = [
+ "RAG 通过检索外部知识来辅助生成,减少幻觉。",
+ "RAG 相比微调更容易更新知识。",
+ "RAG 由检索器、生成器和知识库三部分组成。",
+ "RAG 的缺点是延迟比直接生成稍高。",
+ "RAG 适合需要引用来源的场景。",
+]
+
+model = CrossEncoder("cross-encoder/ms-marco-MiniLM-L6-v2")
+
# 重排
+pairs = [(query, doc) for doc in candidates]
+scores = model.predict(pairs)
+
+# 按分数排序
+ranked = sorted(
+ zip(candidates, scores), key=lambda x: x[1], reverse=True
+)
+
+print("重排结果:")
+for doc, score in ranked:
+ print(f" [{score:.4f}] {doc}")
+```
+
+## 常见误区
+
+!!! failure "误区 1:重排是可选步骤,不做也行"
+ 如果你只用向量检索的 Top-K 结果直接给 LLM,效果可能差 5~10 个百分点。重排在绝大多数场景下都有显著改善。
+
+!!! failure "误区 2:重排模型越大越好"
+ 重排模型是每次查询都要调用的,不是索引阶段预先计算。模型越重,延迟越高。轻量模型(如 bge-reranker-v2-m3)在多数场景下已经足够。
+
+!!! failure "误区 3:重排只做一次就够"
+ 当知识库规模很大时,可以考虑**级联重排(Cascaded Rerank)**——先用轻量模型从 Top-100 筛到 Top-20,再用更重的模型从 Top-20 筛到 Top-3。既保证精度又控制延迟。
+
+## 延伸阅读
+
+- [检索](retrieval.md) —— 重排的上游步骤
+- [生成](generation.md) —— 重排后的结果如何输入 LLM
+- [BGE Reranker 论文](https://arxiv.org/abs/2310.11423)
+- [Sentence Transformers Cross-Encoder 文档](https://www.sbert.net/examples/applications/cross-encoder/README.html)
+
+## 练习题
+
+??? question "练习:对比重排效果"
-> 占位页:本页内容待团队补充。
+ 准备一个问题和 10 篇候选文档,其中 2~3 篇是最相关的。分别做以下实验:
-## 建议内容
+ 1. 只用向量检索的 Top-3(不重排)→ LLM 生成的答案准确率如何?
+ 2. 向量检索 Top-20 → Cross-Encoder 重排取 Top-3 → LLM 生成,答案改善了吗?
+ 3. 尝试不同的重排模型(bge-reranker-v2-m3 vs ms-marco-MiniLM),结果有差异吗?
-- 为什么需要重排
-- 什么时候使用
-- 常见方案
-- 效果评估
+ 用 5 个不同的查询重复上面的实验,你觉得重排至少能提升多少准确率?
diff --git a/docs/rag/retrieval.md b/docs/rag/retrieval.md
index 959d29a..d7440bb 100644
--- a/docs/rag/retrieval.md
+++ b/docs/rag/retrieval.md
@@ -1,10 +1,206 @@
-# 检索
+---
+tags:
+ - RAG
+---
-> 占位页:本页内容待团队补充。
+# 检索:从知识库中找到相关内容
-## 建议内容
+> 检索(Retrieval)是 RAG 系统的核心环节——在向量数据库中找到与用户问题最相似的那些文本片段。
-- 召回
-- 相似度
-- Top-K
-- 检索失败原因
+## 这章解决什么问题
+
+想象你在一个巨大的图书馆里找一本特定的书。如果图书馆没有索引,你得挨个书架找,效率极低。检索就是给知识库建一套高效的索引系统,让每次查询只扫描最可能相关的区域,而不是从头到尾翻一遍。
+
+## 核心概念
+
+### 检索的基本流程
+
+```mermaid
+flowchart LR
+ A[用户问题] --> B[Embedding 模型
转为向量]
+ B --> C[向量数据库
检索 Top-K]
+ C --> D[返回相似片段
及分数]
+ D --> E[送入重排或
直接给 LLM]
+```
+
+用户提问时,需要将问题也通过同样的 Embedding 模型转换成向量。这一步很容易被忽略但非常重要——检索是基于「问题向量」和「文档向量」之间的相似度,如果模型或参数不同,匹配效果会大幅下降。
+
+### 语义检索 vs 关键词检索
+
+| 维度 | 语义检索(Semantic / Dense) | 关键词检索(Lexical / Sparse) |
+|------|-----------------------------|-------------------------------|
+| 原理 | 用 Embedding 模型理解语义 | 用 BM25/TF-IDF 匹配关键词 |
+| 优势 | 理解同义词、近义表达 | 精确匹配专有名词(如产品型号) |
+| 劣势 | 对罕见词和专有名词敏感度低 | 不理解语义,「苹果」搜不到「iPhone」 |
+| 场景 | 开放域问答、多轮对话 | 搜索文档标题、代码片段 |
+| 工具 | 向量数据库 | Elasticsearch、Whoosh |
+
+实践中,很多系统采用**混合检索(Hybrid Search / Multi-Recall)**——同时用语义检索和关键词检索,分别取 Top-K,然后合并去重。混合检索往往比单一方式更鲁棒。
+
+```python
+from langchain.retrievers import EnsembleRetriever
+from langchain_community.retrievers import BM25Retriever
+from langchain_community.vectorstores import Chroma
+from langchain_openai import OpenAIEmbeddings
+
+# 语义检索
+vectorstore = Chroma.from_texts(chunks, OpenAIEmbeddings())
+semantic_retriever = vectorstore.as_retriever(search_kwargs={"k": 5})
+
+# 关键词检索
+keyword_retriever = BM25Retriever.from_texts(chunks, k=5)
+
+# 混合检索
+ensemble_retriever = EnsembleRetriever(
+ retrievers=[semantic_retriever, keyword_retriever],
+ weights=[0.6, 0.4], # 语义检索权重更高
+)
+
+results = ensemble_retriever.invoke("什么是 RAG?")
+```
+
+### Top-K 的选择
+
+K 值决定了送入 LLM 的片段数量:
+
+| K 值 | 优点 | 缺点 | 适用场景 |
+|------|------|------|---------|
+| 1~2 | 上下文纯净,LLM 注意力集中 | 单一来源,信息可能不够 | 简单事实查询 |
+| 3~5 | 信息充分,容错性高 | 可能包含无关信息 | 通用问答 |
+| 6~10 | 覆盖全面,减少遗漏 | LLM 可能被大量文本分散注意力 | 复杂推理、多角度分析 |
+
+K 值不是越大越好。送入过多的片段会稀释 LLM 对关键信息的注意力,而且增加了 token 成本和延迟。
+
+### 检索结果的质量指标
+
+评估检索有两个来自信息检索的经典指标:
+
+- **精确率(Precision,查准率)**:返回的 K 个结果中,有多少是真正相关的
+- **召回率(Recall,查全率)**:知识库中所有相关文档,被检索回来的占比
+
+用公式更好理解:
+
+```
+相关文档(在知识库中): ● ● ● ● ●(共 5 篇)
+检索返回的 Top-3: ● ■ ■ ← 只有第 1 篇相关
+
+精确率 = 1/3 ≈ 0.33 (返回的 3 篇中有 1 篇相关)
+召回率 = 1/5 = 0.20 (总共 5 篇相关只找回 1 篇)
+```
+
+两者存在矛盾:K 越大,召回率 ↑(多找回一些相关文档),但精确率 ↓(混进更多不相关的);K 越小则相反。重排环节的作用就是先用大 K 保召回,再用精确模型提高 Top 结果的精确率。
+
+### 检索策略进阶
+
+除了标准的向量检索,还有一些进阶策略:
+
+**Query 重写(Query Rewriting)**
+
+用户的问题往往很简短(如「那个多少钱?」),缺乏上下文。Query 重写在检索前先把问题扩展成更完整的查询:
+
+```python
+rewrite_prompt = "请将以下用户问题改写成更适合搜索的完整句子:\n{query}"
+query = llm.invoke(rewrite_prompt)
+results = vectorstore.similarity_search(query)
+```
+
+**HyDE(Hypothetical Document Embeddings)**
+
+HyDE 的思路很巧妙:如果一个真实文档和问题相似,那模型「假设」的回答文档应该和真实文档更相似。所以先让 LLM 生成一段假设回答,再用这段假设回答去检索,往往能找到更匹配的文档。
+
+**Multi-Query Retrieval(多查询检索)**
+
+同一个问题可能有多重解读视角。用 LLM 生成 3~5 个不同的搜索词,分别检索后合并结果:
+
+```python
+queries_prompt = f"针对问题 '{query}',给出 5 个不同角度的搜索查询:"
+queries = llm.invoke(queries_prompt).split("\n")
+
+all_results = []
+for q in queries:
+ all_results.extend(vectorstore.similarity_search(q, k=3))
+```
+
+## 最小示例
+
+一个完整的检索循环,包含 Query 重写和混合检索:
+
+```python
+import openai
+import numpy as np
+
+# ── 1. 初始化知识库(简化的内存向量库)──
+chunks = [
+ "RAG 是 Retrieval-Augmented Generation,检索增强生成。",
+ "文档切分(Chunking)是把长文本切成小片段的过程。",
+ "向量化(Vectorization)是把文本转换为数值向量。",
+]
+
+response = openai.embeddings.create(
+ model="text-embedding-3-small", input=chunks
+)
+chunk_vecs = np.array([d.embedding for d in response.data])
+
+def search(query, k=2):
+ """简单的向量检索函数"""
+ q_resp = openai.embeddings.create(
+ model="text-embedding-3-small", input=[query]
+ )
+ q_vec = np.array(q_resp.data[0].embedding)
+ scores = np.dot(chunk_vecs, q_vec) / (
+ np.linalg.norm(chunk_vecs, axis=1) * np.linalg.norm(q_vec)
+ )
+ top_k = np.argsort(scores)[::-1][:k]
+ return [(chunks[i], scores[i]) for i in top_k]
+
+# ── 2. 用户提问 ──
+raw_query = "RAG 是什么?"
+# 简易 Query 重写:用 gpt-4o-mini 扩展
+rewrite_response = openai.chat.completions.create(
+ model="gpt-4o-mini",
+ messages=[{
+ "role": "user",
+ "content": f"重写以下问题使其更适合搜索:{raw_query}"
+ }],
+)
+rewritten = rewrite_response.choices[0].message.content
+
+results = search(rewritten or raw_query, k=2)
+print("检索结果:")
+for text, score in results:
+ print(f" [{score:.4f}] {text}")
+```
+
+## 常见误区
+
+!!! failure "误区 1:「检索」只是查向量数据库"
+ 向量检索只是第一步。Query 重写、HyDE、混合检索等策略往往能显著提升最终的检索质量。很多 RAG 系统只实现了基础的向量检索,空间还很大。
+
+!!! failure "误区 2:K 值越大越好"
+ 过大的 K 值让 LLM 的上下文被非核心内容挤满,反而降低回答质量。建议从 K=3~5 开始,根据实际效果调整。
+
+!!! failure "误区 3:Top-1 就够用了"
+ Top-1 的结果可能错误或偏差。保留多个候选项,让后续的 Rerank 或 LLM 自己从多段文本中提取信息,容错性更高。
+
+## 延伸阅读
+
+- [向量化](vectorization.md) —— 检索的数据基础
+- [重排](rerank.md) —— 检索结果的二次排序
+- [生成](generation.md) —— 检索结果如何输入 LLM
+- [BM25 算法详解](https://en.wikipedia.org/wiki/Okapi_BM25)
+
+## 练习题
+
+??? question "练习:评估你的检索效果"
+
+ 准备 10 个问题及其标准答案。用你选择的检索方案(语义 / 关键词 / 混合)对整个知识库检索,计算:
+
+ 1. **Top-1 准确率**:第一个结果里有正确答案的比例
+ 2. **Top-5 召回率**:前 5 个结果里有正确答案的比例
+ 3. **平均检索时间**:每次检索的平均延迟
+
+ 然后尝试:
+
+ - 把 Embedding 模型换成另一个(如 small → large),Top-1 准确率有变化吗?
+ - 添加 Query 重写后,检索效果改善了吗?
+ - 如果加入混合检索(语义 + BM25),召回率上升还是下降?
diff --git a/docs/rag/troubleshooting.md b/docs/rag/troubleshooting.md
index 5b07b63..64d0385 100644
--- a/docs/rag/troubleshooting.md
+++ b/docs/rag/troubleshooting.md
@@ -1,10 +1,283 @@
-# RAG 常见问题
+---
+tags:
+ - RAG
+---
-> 占位页:本页内容待团队补充。
+# RAG 常见问题与调试
-## 建议内容
+> RAG 系统的失败模式是多样的——回答不好可能因为任何一个环节存在缺陷。这章帮你系统性定位问题。
-- 召回差
-- 切分差
-- 引用差
-- 幻觉
+## 这章解决什么问题
+
+你的 RAG 系统上线了。用户问了一个看起来很简单的问题,但 LLM 给出了一个离谱的答案。
+
+是检索没找到正确的资料?是 LLM 忽略了资料?是切分切断了关键信息?还是 Embedding 模型没理解用户的表达?
+
+RAG 的调试比单环节系统更复杂,因为**每一个环节都可能是瓶颈**。这章提供一套系统性的故障排查思路。
+
+## 核心排查方法论
+
+### RAG 调试的十一问
+
+!!! info "从回答不好到找到根因的排查路径"
+
+ 1. **资料库里有正确答案吗?**
+ - 先去知识库里手动搜一下。如果库里就没有答案,RAG 不可能给出正确答案
+ - 如果资料不全 → 补充知识库
+
+ 2. **资料被正确切分了吗?**
+ - 找到包含答案的原始片段,看它是否被切散到了多个 chunk 中
+ - 是否在 chunk 边界处被截断?
+ - 如果切分有问题 → 调整切分策略(用更大 chunk size 或语义切分)
+
+ 3. **切分后的 chunk 语义完整吗?**
+ - 逐个检查被你命中的 chunk,上下文是否自包含?
+ - chunk 包含的信息是否完整到能独立回答问题?
+ - 不完整?→ 增加 chunk size 或 chunk_overlap
+
+ 4. **用户提问和答案之间的语义鸿沟大吗?**
+ - 用户说「怎么取消」,答案叫「退款政策」——虽然相关但不完全匹配
+ - 语义鸿沟大?→ 在索引中加入同义词或别名,或者直接用 LLM 做 Query 重写
+
+ 5. **Embedding 模型能捕捉这种语义关系吗?**
+ - 手动算一下用户问题和 chunk 之间的相似度
+ - 相似度明显偏低?→ 换更合适的 Embedding 模型(如针对中文场景优化)
+
+ 6. **Top-K 够用吗?**
+ - K=3 只返回了 3 个 chunk,正确答案排在第四
+ - K 太小?→ 增加 K 值,然后配合重排提高精确率
+
+ 7. **重排模型有没有把正确答案排下去?**
+ - 检查正确答案在重排前后的排名变化
+ - 重排分数偏低?→ 检查重排模型是否适配当前语言/领域,或者重排的候选集是否足够大
+
+ 8. **LLM 用上了资料吗?**
+ - 把 LLM 的 Prompt 打印出来,看资料有没有正确传递到 LLM 的输入中
+ - 如果资料传过去了但 LLM 没用 → 强化 Prompt 约束
+ - 如果资料根本没传 → 检查你的代码
+
+ 9. **Prompt 约束足够强吗?**
+ - Prompt 里说了「只根据资料回答」吗?
+ - 说了「如果找不到相关信息就说不知道」吗?
+ - 约束不够 → 优化 Prompt
+
+ 10. **temperature 高不高?**
+ - temperature > 0.5 会增加 LLM 的生成随机性
+ - 对于 RAG 场景,0.1~0.3 更合适
+
+ 11. **是不是 token 限制被截断了?**
+ - 如果 Prompt 加上资料后超过了模型的上下文窗口,后边的资料会被截掉
+ - 截断了?→ 减少 K 值或 chunk size,或使用更大上下文窗口的模型
+
+### 快速诊断表
+
+```mermaid
+flowchart TD
+ A["回答质量差"] --> B{"资料库中
有正确答案吗?"}
+ B -->|没有| C["补充知识库"]
+ B -->|有| D{"检索到了
正确答案吗?"}
+ D -->|没有| E["检查切分策略"]
+ D -->|有| F{"LLM 正确使用
了资料吗?"}
+ E --> G["检查 Embedding
和检索参数"]
+ F -->|没有| H["优化 Prompt
和 temperature"]
+ F -->|有| I["检查格式、来源
等细粒度问题"]
+```
+
+### 生产环境的调试技巧
+
+**1. 记录检索日志**
+
+在每个查询中记录:用户问题、检索到的 Top-K 片段及分数、重排后的片段及分数、最终 Prompt、LLM 回答:
+
+```python
+import json
+
+def log_query(user_question, retrieval_results, final_prompt, llm_answer):
+ log = {
+ "question": user_question,
+ "retrieval_top_k": [
+ {"text": t, "score": float(s)}
+ for t, s in retrieval_results
+ ],
+ "final_prompt": final_prompt,
+ "llm_answer": llm_answer,
+ }
+ with open("query_log.jsonl", "a", encoding="utf-8") as f:
+ f.write(json.dumps(log, ensure_ascii=False) + "\n")
+```
+
+有了日志,你就可以离线分析失败模式,而不是在线上盲目调试。
+
+**2. 建立测试集(Golden Dataset)**
+
+收集 50~100 个典型问题和正确答案,自动化跑回归测试:
+
+```python
+# golden_dataset.json
+[
+ {
+ "question": "RAG 由哪些部分组成?",
+ "expected": ["检索器", "知识库", "生成器"],
+ "category": "概念理解"
+ },
+ {
+ "question": "年付套餐怎么退款?",
+ "expected": ["30 天内可全额退款", "超过 30 天按比例"],
+ "category": "业务政策"
+ }
+]
+```
+
+每次修改系统后,用数据集跑一遍,看**召回率(Retrieval Recall)** 和**回答准确率(Answer Accuracy)** 是否下降。
+
+**3. A/B 测试**
+
+要验证一个改动是否有效,在线上分流对比最可靠。以下变量值得做 A/B 测试:
+
+- 不同切分策略
+- 不同 Embedding 模型
+- 不同 K 值
+- 有/无重排
+- 不同 Prompt 模板
+- 不同 temperature
+
+每次只改一个变量,观察回答质量指标的变化。
+
+## 各环节常见问题速查
+
+### 切分相关问题
+
+| 症状 | 可能原因 | 解决方案 |
+|------|---------|---------|
+| 检索到的 chunk 信息不完整 | chunk size 太小或 overlap 不足 | 增加 chunk size 或 overlap |
+| 同一个话题被切成多个 chunk | 固定长度切分没考虑语义边界 | 改用语义切分或按标题切分 |
+| 检索结果很精确但上下文太少 | 切分太细,chunk 缺少上下文 | 适当增大 chunk size |
+
+### 检索相关问题
+
+| 症状 | 可能原因 | 解决方案 |
+|------|---------|---------|
+| 完全找不到匹配 | Embedding 模型不合适 | 换一个更适合语种的 Embedding 模型 |
+| 分数偏低 | 语义鸿沟大 | 加 Query 重写或用 HyDE |
+| 答案在 Top-4,K=3 找不到 | K 值太小 | 增大 K 值,配合重排 |
+| 检索结果看似相关但不解决问题 | 语义检索没捕捉到意图 | 加混合检索(语义 + BM25) |
+
+### 重排相关问题
+
+| 症状 | 可能原因 | 解决方案 |
+|------|---------|---------|
+| 正确答案重排后排到了后面 | Rerank 模型不匹配语言/领域 | 换个 Rerank 模型 |
+| 重排后 Top-1 和 Top-3 效果差异不大 | 候选集本身质量高 | 可考虑减少 K 值跳过重排 |
+| 重排严重影响延迟 | 模型太大或候选太多 | 换轻量模型,或减少候选数 |
+
+### 生成相关问题
+
+| 症状 | 可能原因 | 解决方案 |
+|------|---------|---------|
+| LLM 编造了资料中没有的信息 | Prompt 没约束「只根据资料回答」 | 添加约束,降低 temperature |
+| LLM 回答了「不知道」但资料里有 | Prompt 在资料前,模型更依赖训练数据 | 把资料放在 Prompt 开头 |
+| 回答太长或结构混乱 | 没指定输出格式 | 在 Prompt 中限定格式 |
+| 回答了「我不知道」但资料中有 | 上下文被截断 | 减少 K 值或 chunk size |
+| 回答自相矛盾 | 多个 chunk 信息冲突 | 检查切分是否产生矛盾片段 |
+
+## 最小示例
+
+```python
+import openai
+import numpy as np
+
+def diagnose_rag(question, chunks, answer):
+ """RAG 问题诊断助手"""
+ issues = []
+
+ # 1. 资料中有相关片段吗?
+ q_resp = openai.embeddings.create(
+ model="text-embedding-3-small", input=[question]
+ )
+ q_vec = np.array(q_resp.data[0].embedding)
+
+ chunk_resp = openai.embeddings.create(
+ model="text-embedding-3-small", input=chunks
+ )
+ chunk_vecs = np.array([d.embedding for d in chunk_resp.data])
+ scores = np.dot(chunk_vecs, q_vec) / (
+ np.linalg.norm(chunk_vecs, axis=1) * np.linalg.norm(q_vec)
+ )
+
+ best_score = float(np.max(scores))
+ if best_score < 0.3:
+ issues.append(f"低相似度:最高相似度仅 {best_score:.2f},可能检索没找到正确内容")
+ elif best_score < 0.6:
+ issues.append(f"中等相似度:最高 {best_score:.2f},需要确认结果是否真的相关")
+ else:
+ issues.append(f"相似度良好({best_score:.2f}),检索环节可能没问题")
+
+ # 2. 回答中有没有可能编造的内容?
+ check_prompt = f"""以下回答完全基于提供的资料吗?
+资料(前3个chunk):{chunks[:3]}
+回答:{answer}
+是否完全基于资料?回答 Yes 或 No 并说明理由。"""
+ check = openai.chat.completions.create(
+ model="gpt-4o-mini",
+ messages=[{"role": "user", "content": check_prompt}],
+ temperature=0,
+ )
+ issues.append(f"事实性检查结果:{check.choices[0].message.content}")
+
+ return issues
+
+# 使用示例
+issues = diagnose_rag(
+ question="RAG 是什么?",
+ chunks=["RAG 是一种文本生成方法。", "机器学习的应用很广泛。"],
+ answer="RAG 是检索增强生成技术。",
+)
+for issue in issues:
+ print(f"- {issue}")
+```
+
+## 常见误区
+
+!!! failure "误区 1:最后一个环节出问题就是 LLM 的错"
+ 多数 RAG 回答质量差的问题,根因在检索环节(没找到正确答案),而不是生成阶段。先检查检索。
+
+!!! failure "误区 2:上线之后就不需要关注了"
+ 文档更新、用户提问方式变化、Embedding 模型升级——任何一个变动都可能影响 RAG 质量。建议定期跑测试数据集。
+
+!!! failure "误区 3:调试靠直觉"
+ 不要猜。检索日志、测试集、A/B 测试——三个工具能解决 90% 的 RAG 调试问题。直觉不可靠,数据才可靠。
+
+## 延伸阅读
+
+- [生成](generation.md) —— 调试的常见终点
+- [检索](retrieval.md) —— 调试的常见起点
+- [为什么需要 RAG](why-rag.md) —— 回顾 RAG 的能力边界
+- [RAG 分类与评估](https://docs.llamaindex.ai/en/stable/module_guides/querying/retriever/retrievers/)
+
+## 练习题
+
+??? question "练习 1:一次完整的 RAG 调试"
+
+ 假设你的 RAG 系统遇到了以下问题:
+
+ > 用户问:「你们公司的退款政策是什么?」LLM 回答:「一般来说,大多数公司接受 30 天内退款。」
+
+ 这个回答明显不对——你们公司的政策是「软件产品一旦激活概不退款」。
+
+ 请按照「十一问」排查流程,写出你的排查思路和可能的解决方案。
+
+??? question "练习 2:搭建你的测试数据集"
+
+ 为你的 RAG 场景准备 20 个测试用例,每个包含:
+
+ - `question`:用户问题
+ - `expected`:期望的回答关键词(3~5 个)
+ - `category`:问题类型
+
+ 然后写一个脚本:
+
+ 1. 用你的 RAG 系统回答这 20 个问题
+ 2. 判断回答是否包含所有 `expected` 关键词
+ 3. 输出通过率
+
+ 这个测试集将成为你后续所有 RAG 改动的基本验证工具。
diff --git a/docs/rag/vectorization.md b/docs/rag/vectorization.md
index a73f2f1..c7b4337 100644
--- a/docs/rag/vectorization.md
+++ b/docs/rag/vectorization.md
@@ -1,10 +1,291 @@
-# 向量化
+---
+tags:
+ - RAG
+---
-> 占位页:本页内容待团队补充。
+# 向量化:把文本变成向量
-## 建议内容
+> 向量化(Vectorization / Embedding)是把文本转换成数值向量的过程,让计算机能在数学空间里衡量「两段文字有多像」。
-- Embedding
-- 向量数据库
-- 选择策略
-- 常见误区
+## 这章解决什么问题
+
+人类判断两段文字是否相关,靠的是语义理解。但计算机看不懂「意思」,只能处理数字。
+
+向量化就是造一座桥,把人类语言翻译成计算机能算的数字序列——**向量(Vector)**。有了这些数字,我们就可以用数学公式计算相似度,实现快速检索。
+
+## 核心概念
+
+### 什么是向量
+
+向量是一组浮点数,比如 `[0.023, -0.157, 0.892, ...]`(通常 256~3072 维)。每个维度代表文本在某个潜在「语义方向」上的分量。
+
+通俗理解:向量就像是给一段文字做的「语义指纹」。两段文字在语义上越接近,它们的向量在空间中就越靠近。
+
+```mermaid
+flowchart LR
+ A["文本 A:
什么是 RAG?"] --> E["Embedding
模型"]
+ B["文本 B:
RAG 是检索增强生成"] --> E
+ C["文本 C:
今天天气不错"] --> E
+ E --> VA["向量 A:[0.2, -0.1, ...]"]
+ E --> VB["向量 B:[0.3, -0.2, ...]"]
+ E --> VC["向量 C:[-0.5, 0.8, ...]"]
+ VA -.->|相似度高
cos ≈ 0.9| VB
+ VA -.->|相似度低
cos ≈ 0.1| VC
+```
+
+### 相似度计算
+
+有了向量之后,我们需要一个「尺子」来衡量两段文字的语义距离。不同尺子侧重点不同,选择哪个直接影响检索效果。
+
+#### 余弦相似度(Cosine Similarity)
+
+衡量两个向量在方向上的**夹角大小**,不考虑向量长度(模长)。
+
+```
+cos(a, b) = (a · b) / (|a| × |b|)
+```
+
+| 取值范围 | 含义 |
+|---------|------|
+| 1 | 方向完全一致(语义最接近) |
+| 0 | 正交,不相关 |
+| -1 | 方向完全相反(语义最远) |
+
+Python 实现:
+
+```python
+import numpy as np
+
+def cosine_similarity(a, b):
+ return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))
+
+vec_a = np.array([0.2, -0.1, 0.5])
+vec_b = np.array([0.3, -0.2, 0.4])
+print(f"余弦相似度:{cosine_similarity(vec_a, vec_b):.4f}")
+```
+
+**适用场景**:语义搜索、文本相似度判断、信息检索。这是 RAG 中最常用的指标,因为它只关心方向(语义倾向),不受文本长度影响。
+
+!!! tip "OpenAI embeddings 已经归一化"
+ OpenAI 的 `text-embedding-3-small` 和 `text-embedding-3-large` 输出默认是 L2 归一化的(模长为 1)。此时余弦相似度等价于点积,可以省去分母计算。
+
+#### 点积相似度(Dot Product / Inner Product)
+
+直接计算两个向量的内积:
+
+```
+dot(a, b) = a₁b₁ + a₂b₂ + ... + aₙbₙ
+```
+
+```python
+def dot_product(a, b):
+ return np.dot(a, b)
+
+print(f"点积:{dot_product(vec_a, vec_b):.4f}")
+```
+
+| 特点 | 说明 |
+|------|------|
+| 与余弦的关系 | 当向量已 L2 归一化时,点积 = 余弦相似度 |
+| 未归一化时 | 同时受方向和模长影响——模长长的向量更容易得高分 |
+| 取值范围 | 无固定边界,取决于向量维度和数值范围 |
+
+**适用场景**:
+- 向量已归一化时:直接替代余弦相似度,计算更快(省一次模长运算)
+- 向量未归一化时:适合对「高频/长文档」有偏好的场景(如长文本比短文本更值得被检索到)
+
+#### 欧氏距离(Euclidean Distance / L2)
+
+衡量两个向量在多维空间中的**直线距离**。距离越小越相似。
+
+```
+L2(a, b) = √[(a₁ - b₁)² + (a₂ - b₂)² + ... + (aₙ - bₙ)²]
+```
+
+```python
+def euclidean_distance(a, b):
+ return np.linalg.norm(a - b)
+
+def euclidean_similarity(a, b):
+ """将距离转为相似度分数,方便统一比较"""
+ return 1 / (1 + euclidean_distance(a, b))
+```
+
+**与余弦相似度的关键区别**:
+
+| 维度 | 余弦相似度 | 欧氏距离 |
+|------|-----------|---------|
+| 关注点 | 方向夹角 | 绝对距离 |
+| 受向量长度影响 | 否 | 是 |
+| 举例 | (1,0) 与 (10,0) 的 cos=1,完全相同 | (1,0) 与 (10,0) 的 L2=9,差异很大 |
+
+**适用场景**:当向量长度本身携带语义信息时(比如向量的模长代表「信息量」或「置信度」),欧氏距离能同时捕捉方向和大小的差异。
+
+#### 曼哈顿距离(Manhattan Distance / L1)
+
+计算两个向量在各维度上的**绝对差之和**:
+
+```
+L1(a, b) = |a₁ - b₁| + |a₂ - b₂| + ... + |aₙ - bₙ|
+```
+
+```python
+def manhattan_distance(a, b):
+ return np.sum(np.abs(a - b))
+```
+
+**与欧氏距离对比**:曼哈顿距离对单个维度的异常差异不那么敏感(不取平方)。在高维空间中,曼哈顿距离有时比欧氏距离更稳定(缓解「维度灾难」)。但 Embedding 检索中较少直接使用曼哈顿距离。
+
+#### 各指标对比总结
+
+```python
+import numpy as np
+
+def compare_metrics(a, b):
+ """一次性输出多种相似度指标"""
+ cos = np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))
+ dot = np.dot(a, b)
+ l2 = np.linalg.norm(a - b)
+ l1 = np.sum(np.abs(a - b))
+
+ return {
+ "余弦相似度": cos,
+ "点积": dot,
+ "欧氏距离": l2,
+ "欧氏相似度": 1 / (1 + l2),
+ "曼哈顿距离": l1,
+ }
+
+v1 = np.array([0.2, -0.1, 0.5])
+v2 = np.array([0.3, -0.2, 0.4])
+v3 = np.array([-0.5, 0.8, 0.1])
+
+for name, val in compare_metrics(v1, v2).items():
+ print(f"{name}: {val:.4f}")
+```
+
+在 RAG 实践中,**余弦相似度是默认选择**,其次是点积(向量归一化后等效于余弦)。欧氏距离作为补充,适合向量长度携带信息的场景。向量数据库(FAISS、Milvus 等)通常同时支持多种距离度量,可以在建索引时通过参数指定。
+
+### Embedding 模型的选择
+
+Embedding 模型(也叫向量化模型)的质量直接决定检索效果。常见选项:
+
+| 模型 | 维度 | 特点 | 适用场景 |
+|------|------|------|---------|
+| `text-embedding-3-small` | 1536 | OpenAI,性价比高,支持 256 维截断 | 通用场景 |
+| `text-embedding-3-large` | 3072 | OpenAI,精度更高,成本也更高 | 精度优先的场景 |
+| `BAAI/bge-large-zh-v1.5` | 1024 | 中文优化,开源 | 中文场景 |
+| `intfloat/multilingual-e5-large` | 1024 | 多语言,包含中文 | 多语言场景 |
+| `text2vec-large-chinese` | 1024 | 国产中文模型 | 中文专用场景 |
+
+选择建议:
+
+1. **优先用 Embedding 模型,不用通用 LLM**:LLM 最后一层的 hidden state 不是为相似度任务优化的,效果通常不如专门的 Embedding 模型
+2. **中文场景优先考虑中文优化的模型**:OpenAI 的模型也支持中文,但中文优化的模型(如 bge-large-zh)在纯中文场景可能更好
+3. **维度不是越高越好**:维度高意味着更多存储和计算。OpenAI 支持设置 `dimensions` 参数进行降维,从 1536 降低到 256 还能保持 95%+ 的性能
+
+### 向量数据库
+
+当知识库只有几十个片段时,你可以像上面的示例一样直接在内存中做余弦相似度计算。但当片段数量达到数万、数百万时,就需要一个专门的**向量数据库(Vector Database)**。
+
+向量数据库的核心能力是**近似最近邻搜索(ANN,Approximate Nearest Neighbor)**——不像暴力扫描那样精确但慢,而是用索引算法(HNSW、IVF 等)在可接受的精度损失下大幅提升检索速度。
+
+| 工具 | 类型 | 规模 | 特点 |
+|------|------|------|------|
+| **Chroma** | 本地嵌入式 | 小规模 | 简单易用,适合原型 |
+| **FAISS** | 本地库 | 中大规模 | Meta 开源,性能好 |
+| **Milvus** | 分布式 | 大规模 | 云原生,功能完整 |
+| **Weaviate** | 分布式 | 中大规模 | 原生支持多种 Embedding 模型 |
+| **Qdrant** | 分布式 | 中大规模 | Rust 实现,性能优异 |
+| **PostgreSQL + pgvector** | 扩展 | 中规模 | 如果你已经在用 Postgres |
+
+用 LangChain 集成向量数据库非常简单:
+
+```python
+from langchain_community.vectorstores import Chroma
+from langchain_openai import OpenAIEmbeddings
+
+# 生成向量并存入 Chroma
+embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
+vectorstore = Chroma.from_texts(
+ texts=chunks,
+ embedding=embeddings,
+ persist_directory="./chroma_db"
+)
+
+# 检索
+results = vectorstore.similarity_search("什么是 RAG?", k=3)
+```
+
+### Token 数量与成本控制
+
+向量化的成本跟文本长度直接相关。以 OpenAI 为例:
+
+- `text-embedding-3-small`:$0.02 / 1M token
+- `text-embedding-3-large`:$0.13 / 1M token
+
+如果知识库有 100 万 token(大约 3~4 本 300 页的书),small 模型的向量化成本约 $0.02,但存储为 1536 维浮点数后占用约 6MB。如果使用大型知识库需要考虑存储和内存占用。
+
+## 最小示例
+
+以下代码对比 OpenAI 和本地模型的向量化效果:
+
+```python
+import openai
+
+# ── 使用 OpenAI Embedding ──
+response = openai.embeddings.create(
+ model="text-embedding-3-small",
+ input=["什么是 RAG?", "RAG 是检索增强生成"],
+ dimensions=256 # 降维,节省存储
+)
+vectors = [d.embedding for d in response.data]
+print(f"向量维度:{len(vectors[0])}") # 256(通过 dimensions 降维)
+print(f"向量前 5 维:{vectors[0][:5]}")
+
+# ── 使用 Sentence Transformers(本地,适合离线)──
+from sentence_transformers import SentenceTransformer
+
+model = SentenceTransformer("BAAI/bge-large-zh-v1.5")
+embeddings = model.encode(["什么是 RAG?", "RAG 是检索增强生成"])
+print(f"本地模型向量维度:{len(embeddings[0])}") # 1024
+print(f"相似度矩阵形状:{embeddings.shape}")
+```
+
+## 常见误区
+
+!!! failure "误区 1:越贵的 Embedding 模型效果越好"
+ 不一定。OpenAI 的 large 模型比 small 模型贵 6.5 倍,但在 MTEB 基准上只高出不到 5 个点。对于大多数应用场景,small 模型已经足够。如果 budget 有限,先用 small。
+
+!!! failure "误区 2:Embedding 模型选好后就不用管了"
+ Embedding 模型持续在进化。建议每隔 6~12 个月重新评估一次(尤其是中文场景),看看更新的模型能否提升检索效果。Embedding 替换后需要重新索引全部知识库。
+
+!!! failure "误区 3:向量相似度 = 语义相似度"
+ 向量相似度是语义相似度的一种近似。它可能把同义词算得很近(好的),也可能把反义词算得很近(不好)。比如「喜欢」和「讨厌」的向量可能比较接近,因为它们在很多语境中互换出现。
+
+## 延伸阅读
+
+- [文档切分](chunking.md) —— 向量化之前,文本先要切好
+- [检索](retrieval.md) —— 向量化之后,如何用向量搜索
+- [什么是多模态 AI](../basics/multimodal-ai.md) —— Embedding 思想在图像领域的应用
+
+## 练习题
+
+??? question "练习 1:对比不同 Embedding 模型"
+
+ 准备 5 个中文句子(主题尽量不同),分别用以下方式计算两两之间的余弦相似度:
+
+ 1. 用 OpenAI `text-embedding-3-small`
+ 2. 用本地 `BAAI/bge-large-zh-v1.5`(或 `shibing624/text2vec-base-chinese`)
+ 3. 用 One-Hot 编码 + Jaccard 相似度(最简单的基线)
+
+ 对比三者的结果:哪些句子对在语义模型下相似度很高,但在 One-Hot 下很低?这说明什么?
+
+??? question "练习 2:计算你的知识库向量化成本"
+
+ 假设你有一个 10 万 token 的中文文档库,需要选择合适的 Embedding 模型和向量数据库。回答:
+
+ 1. 用 `text-embedding-3-small`($0.02/1M token)的成本是多少?
+ 2. 用 `text-embedding-3-large`($0.13/1M token)的成本是多少?
+ 3. 存储 1536 维 vs 256 维向量,存储空间差多少倍?
+ 4. 如果每天有 100 次查询,每次返回 Top-5,用 FAISS 和暴力扫描对延迟的影响大概差多少?
diff --git a/docs/rag/why-rag.md b/docs/rag/why-rag.md
index afeb054..815f342 100644
--- a/docs/rag/why-rag.md
+++ b/docs/rag/why-rag.md
@@ -1,10 +1,138 @@
+---
+tags:
+ - RAG
+---
+
# 为什么需要 RAG
-> 占位页:本页内容待团队补充。
+> RAG 解决的冲突是:LLM 什么都知道一点,但什么都不够精确——它不是你公司内部的活百科。
+
+## 这章解决什么问题
+
+你打开 ChatGPT,问了一个关于你们公司内部流程的问题。它回答得非常有条理、非常自信,但——完全是错的。
+
+这不是模型笨,而是它根本没见过你们公司的文档。**LLM 的知识来自训练数据,而训练数据不可能包含你的私有信息。**
+
+那么,把整本产品手册塞进 Prompt 行不行?有两个问题:
+
+1. **上下文窗口有限**——OpenAI 的上下文窗口从 8K 到 200K token 不等,但一本产品手册可能有几十万 token
+2. **信息密度不均**——用户只想知道「怎么重置密码」,但你需要从几万字的文档里找到那一行
+
+RAG 的思路就是换一个角度:**不把所有文档塞进 Prompt,而是先找到最相关的那几段,再把它们给模型看。**
+
+## 核心概念
+
+### LLM 的三个天然局限
+
+要理解为什么需要 RAG,先要理解 LLM 本身有哪些做不到的事:
+
+**局限 1:知识有截止日期**
+
+LLM 的训练数据有一个截止时间。GPT-4o 的知识截止于 2023 年 10 月左右。如果你问「2025 年发布的 iPhone 17 有什么新功能」,它不可能知道,因为训练时这台手机还没发布。
+
+**局限 2:看不到私有数据**
+
+你的公司文档、产品手册、用户协议、内部知识库——LLM 一概不知。它的训练数据来自互联网公开内容,不包含任何非公开信息。
+
+**局限 3:不知道的它会「编」**
+
+当 LLM 被问到不知道的问题时,它倾向于**自信地给出看起来合理的错误答案**。这种现象叫**幻觉(Hallucination)**。不是模型故意撒谎,而是它的工作机制就是「根据上下文预测最合理的下一个词」。如果没有相关信息,它会预测一个「听起来靠谱」的答案。
+
+RAG 通过提供真实的参考资料来缓解这三个问题:用检索获取最新信息、用私有知识库补充模型盲区、用原文减少编造空间。
+
+### RAG 解决什么、不解决什么
+
+!!! success "RAG 擅长解决的场景"
+ - **知识问答**:用户问什么,你正好有文档
+ - **客服支持**:基于产品手册回答用户问题
+ - **文档解读**:从长篇合同中提取关键条款
+ - **私有数据处理**:公司内部文档、研究报告、用户协议
+ - **知识库查询**:结合已有数据库进行问答
+
+!!! warning "RAG 不适合的场景"
+ - **知识库里没有的内容**:检索不到就是检索不到,RAG 变不出来
+ - **需要深度推理的问题**:RAG 提供素材,但多步推理靠模型自身推理能力
+ - **格式转换 / 风格迁移**:不需要外部知识,直接 LLM 即可
+ - **极低延迟场景**:RAG 多一次检索调用,增加几十到几百毫秒
+ - **高频更新的知识**:知识库更新不及时,RAG 反而给出过时答案
+
+### RAG vs 微调 vs 长上下文模型
+
+| 维度 | RAG | 微调 (Fine-tuning) | 长上下文模型 |
+|------|-----|-------------------|-------------|
+| 知识更新 | ✅ 改知识库即可 | ❌ 需重新训练 | ✅ 直接塞入新内容 |
+| 知识删除 | ✅ 删除知识库条目 | ❌ 无法「忘记」 | ✅ 不塞即可 |
+| 知识透明度 | ✅ 可追踪来源 | ❌ 知识嵌入参数中 | ✅ 上下文可见 |
+| 一次性成本 | 低(搭知识库) | 高(训练 + 标注数据) | 低 |
+| 运行成本 | 中(检索 + 生成) | 低(只需生成) | 高(越长越贵) |
+| 延迟 | 中 | 低 | 低 |
+| 适合 | 知识密集型场景 | 能力/风格调整 | 长文档一次性分析 |
+
+## 最小示例:一个具体场景的对比
+
+假设你是一家 SaaS 公司的客服,用户问:「我们的订阅套餐能随时取消吗?」
+
+**不用 RAG**:
+
+```
+LLM 凭训练数据回答:
+大多数订阅服务都可以随时取消。建议你登录账户查看设置页面。
+
+→ 错误!你们公司的政策是年付套餐只能 30 天内取消。
+```
+
+**用 RAG**:
+
+```
+用户问题:我们的订阅套餐能随时取消吗?
+
+检索到的资料片段:
+"月付套餐可以随时取消,取消后下个计费周期不再扣费。
+年付套餐在购买后 30 天内可以取消,超过 30 天需联系客服协商,
+按剩余天数比例退款。"
+
+LLM 生成的回答(基于资料):
+根据我们公司的订阅政策(来源:服务条款 - 取消政策):
+
+- 月付套餐:随时可取消,次月起不再扣费
+- 年付套餐:购买后 30 天内可取消,超过 30 天需联系客服处理
+
+需要我帮您操作取消吗?
+```
+
+## 常见误区
+
+!!! failure "误区 1:RAG 就必须用向量数据库"
+ 向量数据库是为大规模检索设计的工具。如果你的知识库只有几百条,用内存计算或 BM25 传统搜索也能实现 RAG。工具的选择取决于规模,不是定义。
+
+!!! failure "误区 2:有了 RAG 模型就不会产生幻觉"
+ RAG 能**降低**幻觉,但不能**根除**。如果检索回来的资料与问题无关,或者 LLM 忽略了资料里的信息,幻觉仍然可能出现。生成阶段的 Prompt 设计同样重要。
+
+!!! failure "误区 3:RAG 不需要专门设计 Prompt"
+ 资料怎么拼、要不要标来源、先写资料还是先写问题——这些 Prompt 编排细节直接影响回答质量。一句「基于以上资料回答」和详细的约束条件,效果差别很大。
+
+!!! failure "误区 4:既然有长上下文模型了,RAG 就不需要了"
+ 长上下文模型能一次性塞入更多内容,但「能塞」不等于「能找到」。研究表明,当上下文长度超过一定阈值后,模型对中间部分信息的关注度会显著下降。RAG 通过只保留最相关的片段来解决这个问题。
+
+## 延伸阅读
+
+- [RAG 总览](index.md) —— 回到模块入口
+- [什么是 LLM](../basics/what-is-llm.md) —— 理解 LLM 的能力和局限
+- [为什么模型会胡说](../basics/hallucination.md) —— 深入理解幻觉现象
+- [Chat、Copilot、Agent 与 Workflow](../tools/workflow.md) —— RAG 与工具调用的协作
+
+## 练习题
+
+??? question "练习:为你的工作场景设计 RAG 方案"
+
+ 想一个你自己工作或学习中「需要频繁查资料」的场景,回答以下问题:
-## 建议内容
+ 1. 你的知识库来源是什么?(文档、Wiki、数据库、聊天记录?)
+ 2. 这些资料多久更新一次?更新后是否需要立刻反映在回答中?
+ 3. 用户最常问的 3 类问题是什么?
+ 4. 如果这些问题的答案就在知识库里但模型回答错了,最可能是哪个环节出了问题?
-- 问题背景
-- 典型场景
-- 能解决什么
-- 不能解决什么
+ ??? done "参考思路"
+ - 如果知识库更新频繁(如产品手册每周更新),RAG 比微调更适合
+ - 如果用户的问题集中在少数几个主题上,可以针对性地优化切分策略
+ - 回答出错的常见原因:检索没找到正确片段(切分/Embedding 问题)> LLM 没正确使用资料(Prompt 问题)> 资料本身过时