🔍 Day 3 · RAG 向量检索

预计时间:2-3 小时 · 难度:⭐⭐⭐ · 目标:让 Agent 能"读懂"你的笔记 —— 这是知识助手项目的心脏
📖 今日路线

为什么需要 RAG?

Day 2 你写的 read_file 工具有个致命限制:你得知道文件名

真实场景是这样的:

三种"曾经的方案",都不行:

方案为什么不行
把所有笔记塞进 prompt200 篇 = 几百 KB,早爆 context 了;每次调用还要重发所有内容,token 爆炸
grep 关键词搜你问"记忆机制",笔记里写的是"短期记忆"、"conversation state" —— 关键词搜不到
让 LLM 逐个读文件200 次 API 调用,慢得要死

RAG(Retrieval-Augmented Generation) 是当下的标准答案:

  1. 把笔记切成小段(chunking)
  2. 每段变成一个向量(embedding,通常 512~1536 维的浮点数组)
  3. 存到向量数据库(ChromaDB / Milvus / Pinecone)
  4. 查询时:问题也变向量 → 找最相似的几段 → 塞给 LLM 作为参考

核心概念:向量 + 相似度

什么是 embedding?

把一段文字塞给 embedding 模型,它吐出一个浮点数数组(叫"向量")。

"Agent 是能自主决策的智能程序"
    ↓ embedding 模型
[0.023, -0.181, 0.442, 0.007, ..., -0.093]   ← 1024 个数字

关键性质语义相近的文本,向量在高维空间里挨得近

文本 A: "Agent 是智能程序"           → 向量 A
文本 B: "AI Agent 有自主决策能力"    → 向量 B(跟 A 很近)
文本 C: "今天午饭吃什么"             → 向量 C(跟 A/B 都很远)

余弦相似度(cosine similarity):
  A · B / (|A| × |B|)   ← 越接近 1 越像,接近 0 或负数就完全无关
💡 Java/Go 类比

什么是 ChromaDB?

向量数据库,专门存这些向量并高效搜索"最近邻"。

对比:

Chroma 的优点:纯 Python 库,本地文件存储,零部署pip install chromadb 就能用。


RAG 完整流水线

阶段 A:Ingest(一次性,或每次笔记更新时跑)

notes/*.md
    ↓ 读取
"Agent 是..." "记忆机制..." "工具调用..."
    ↓ 切分(chunking)
[chunk1, chunk2, chunk3, ...]        ← 每段 ~500 字
    ↓ embedding 模型
[vec1, vec2, vec3, ...]              ← 每段一个向量
    ↓ 存入 ChromaDB
{id: uuid, text: chunk, vector: vec, metadata: {file, chunk_idx}}

阶段 B:Query(每次用户提问时跑)

用户提问:"Agent 怎么记住上下文?"
    ↓ 同一个 embedding 模型
[0.021, -0.15, ...]                  ← 问题的向量
    ↓ ChromaDB 检索 top-k
[chunk7 (相似度 0.89), chunk12 (0.83), chunk3 (0.79)]
    ↓ 拼进 prompt
"根据以下笔记回答:\n[chunk7]\n[chunk12]\n[chunk3]\n\n问题:Agent 怎么记住上下文?"
    ↓ LLM
"根据你的笔记,Agent 通过 messages 数组累积历史..."
⚠️ 注意:Query 阶段用的 embedding 模型必须和 Ingest 阶段完全一致,否则向量不在同一个"空间"里,相似度无意义。

✅ 任务清单


Step 1 · 环境准备 + 开通 Embedding 接入点

1.1 装依赖

cd ~/Documents/practice/agents
mkdir -p day3
cd day3

python3 -m venv venv
source venv/bin/activate

# 新增:chromadb(向量库)
pip install openai python-dotenv chromadb
💡 首次装 chromadb 会顺带装 onnxruntime 等,稍等一分钟。

1.2 火山方舟开通 Embedding 接入点

  1. 访问 火山方舟控制台
  2. 左侧 "在线推理""创建推理接入点"
  3. 模型分类选 "向量化""Embedding"
  4. 选择 Doubao-embedding(有 text 版和多语言版,选 text 版即可)
  5. 拿到接入点 ID,形如 ep-2024xxxx-xxxxx

1.3 更新 .env

ARK_API_KEY=你的APIKey                              # 跟 Day 1/2 一样
ARK_ENDPOINT_ID=你的Chat接入点ID                    # 跟 Day 1/2 一样
ARK_EMBEDDING_ENDPOINT_ID=你新建的Embedding接入点ID  # 🆕 今天新增
ARK_BASE_URL=https://ark.cn-beijing.volces.com/api/v3

Step 2 · Demo 1 · 玩玩 embedding

先不做工程,玩一下 embedding,感受"向量相似度"是什么。

day3/demo1_embedding_basics.py(已经建好,直接跑):

python demo1_embedding_basics.py

你会看到什么

=== 3 段文本的 embedding ===

📝 文本 1: "Agent 是能自主决策的智能程序"
   向量前 5 维: [0.021, -0.153, 0.089, -0.007, 0.212, ...]
   向量长度: 2048

📝 文本 2: "AI 助手可以理解意图并调用工具"
   向量前 5 维: [0.019, -0.148, 0.091, ...]

📝 文本 3: "今天中午吃什么好"
   向量前 5 维: [-0.088, 0.234, -0.011, ...]

=== 两两相似度(余弦) ===
  文本1 vs 文本2: 0.783   ← 都在讲 AI Agent,很相似
  文本1 vs 文本3: 0.412   ← 无关,低
  文本2 vs 文本3: 0.398   ← 无关,低
💡 关键洞察:文本 1 和 2 用词完全不同("Agent" vs "AI 助手","自主决策" vs "理解意图"),但 embedding 依然把它们判定为"相似"—— 这就是 语义搜索 相比关键词搜索的强大之处。

Step 3 · Demo 2 · Ingest 流水线

notes/ 目录下所有 .md 文件切分 → embedding → 存 ChromaDB。

我们准备了 3 篇示例笔记在 day3/notes/,你也可以塞自己的笔记进去。

核心代码逻辑

for md_file in notes/*.md:
    text = read(md_file)
    chunks = split_by_paragraph(text, max_chars=500)   # 简单按段落切
    for i, chunk in enumerate(chunks):
        vector = embed(chunk)
        chroma.add(
            ids=[f"{md_file}#{i}"],
            documents=[chunk],
            embeddings=[vector],
            metadatas=[{"file": md_file, "chunk_idx": i}],
        )

跑起来

python demo2_ingest.py

预期输出:

📁 找到 3 个笔记文件
   ├─ notes/agent-basics.md    (2 chunks)
   ├─ notes/rag-notes.md       (3 chunks)
   └─ notes/go-tips.md         (2 chunks)

✅ 入库完成:共 7 个 chunk 存入 ChromaDB
📊 数据库路径: ./chroma_db/

关于 chunking 的取舍

切分是 RAG 的隐藏关键。我们今天用最简单的"按段落切+字数限制"策略。有几个原则:


Step 4 · Demo 3 · Query 检索

命令行 REPL,你输入问题 → 显示最相似的 3 段笔记。

python demo3_query.py

试试这些问题:

问题: Agent 是怎么记住上下文的?
   ↑ 你的笔记里可能写着"messages 数组累积历史" —— 关键词完全不匹配
   ↑ 但 embedding 应该能找到

问题: Go 里怎么处理错误
问题: 什么是向量数据库

预期输出

🔍 检索: "Agent 是怎么记住上下文的?"

  [1] 相似度 0.847  来源: notes/agent-basics.md#1
      内容: LLM 本身是无状态的,Agent 通过 messages 数组累积对话历史...

  [2] 相似度 0.732  来源: notes/agent-basics.md#0
      内容: Agent = LLM + 记忆 + 工具 + 规划。所谓记忆有两种...

  [3] 相似度 0.512  来源: notes/rag-notes.md#2
      内容: 长期记忆通常放向量库,短期放消息数组...
⚠️ 注意:这个 demo 只做检索,没接 LLM。看清检索质量后,Day 4 我们再把它包装成一个工具,让 Agent 用起来。

🎯 Step 5 · 小挑战(选做)

挑战 1:加 overlap

让相邻 chunk 重叠 50 字。测试:"段落被切断的信息还能否被检索到?"

挑战 2:加元数据过滤

让 query 支持 "只搜 notes/go-tips.md"—— ChromaDB 支持 where={"file": "..."}

挑战 3:加入你的真实笔记

notes/ 替换成你自己的 10 篇笔记,看看检索效果如何。


🕳️ 今日踩坑

解决
Chroma 每次跑重复入库入库前先 collection.delete();或用文件 hash 做 id,重复自动覆盖
相似度都很低(<0.3)可能笔记内容跟问题真的无关;或 chunk 切太碎;或用错了 embedding 模型
Chroma 版本报错Chroma 0.4 → 0.5 API 有变化。我们代码用 PersistentClient,兼容新版
中文 embedding 效果差确认用的是 doubao-embedding-text,支持中文;如果是 base 模型可能只对英文好

✅ 验收标准

  1. ✅ 能解释 embedding 是什么、余弦相似度含义
  2. ✅ 能画出 RAG 的两个阶段(Ingest + Query)
  3. ✅ 3 个 demo 都跑通
  4. ✅ 用 Demo 3 检索一个"关键词不匹配但语义相近"的问题,能找到相关 chunk

📦 收工提交

cd ~/Documents/practice/agents
git status                     # 确认 .env、chroma_db/ 都在 .gitignore 里
git add day3/
git commit -m "Day 3: RAG 向量检索(embedding + ChromaDB)"
git push

🎉 明天预告:Day 4

Day 4 是大合体:把 Day 2 的工具调用 + Day 3 的向量检索融合,做出知识助手 v1。你会看到:

那才是真正让人惊艳的 Agent 时刻 ✨