第3周 Day 3:RAG 入门——给 Agent 一个"图书馆"

0 阅读12分钟

第3周 Day 2:RAG 入门——给 Agent 一个"图书馆"

上次用 Function Calling 让 Agent 能"自己动手",但有个问题:AI 只能回答"通用问题",不知道咱们用户的数据。

比如我问 Agent:"我上周学了哪些单词?"

Agent 说:"我不知道你的学习记录。"

因为它没有看过我的数据

这就要用到 RAG(检索增强生成)

今天要干啥:

  • ✅ 理解 RAG 是什么(让 AI 先翻书再回答)
  • ✅ 理解为什么需要 RAG(AI 不知道用户的私数据)
  • ✅ 动手实现简单 RAG(关键词检索 + Prompt 增强)
  • ✅ 理解 Embedding(把文字变数字,语义相似)

一、RAG 是什么?

场景对比

没有 RAG有 RAG
我问:"我学了哪些单词?" → AI:"我不知道"AI 先读取你的学习记录 → 回答:"你学了 cat、dog、bird"
我问:"公司今年的销售额是多少?" → AI:"我没有数据"AI 先读取公司报表 → 回答:"今年销售额 500 万"

RAG = Retrieval-Augmented Generation(检索增强生成)

简单理解:让 AI 先"翻书",再回答问题


二、为什么需要 RAG?

大模型有个天生的局限:它只知道训练时看过的内容,不知道用户的私数据

大模型知道什么大模型不知道什么
公开的互联网知识(百科、新闻)你公司的内部文档
通用的语言知识(语法、词汇)你的个人学习记录
代码、数学、科学知识你用户的订单数据

RAG 的作用:把用户的数据给 AI 看,让它能回答用户的问题。


三、RAG 实战:让 Agent 能查询学习记录

我们先准备好"图书馆"(数据),然后教 Agent 如何"翻书"。

第一步:准备数据

// documents/learning_records.json
[
    {"id": 1, "content": "2026-04-15 学习了 cat,意思是猫,造句:I have a cat."},
    {"id": 2, "content": "2026-04-16 学习了 dog,意思是狗,造句:The dog is running."},
    {"id": 3, "content": "2026-04-17 学习了 bird,意思是鸟,造句:A bird is flying."},
    {"id": 4, "content": "2026-04-17 测验答错了 fish,正确答案是 fish(鱼)"},
    {"id": 5, "content": "2026-04-18 学习了 apple,意思是苹果"}
]

这是咱们的"图书馆"——存放着用户的学习记录。

第二步:编写检索函数

def retrieve(query, documents, top_k=3):
    """从文档中找到相关内容"""
    results = []

    # 提取关键词(简单版)
    keywords = extract_keywords(query)

    # 遍历所有文档,找匹配的
    for doc in documents:
        content = doc["content"]
        match_count = sum(1 for kw in keywords if kw in content)

        if match_count > 0:
            results.append({
                "content": content,
                "score": match_count
            })

    # 按匹配度排序,取前3条
    results.sort(key=lambda x: x["score"], reverse=True)
    return results[:top_k]

运行效果

用户问:"我学了哪些单词?"
检索结果:
  - "2026-04-15 学习了 cat..." (匹配度: 2)
  - "2026-04-16 学习了 dog..." (匹配度: 2)
  - "2026-04-17 学习了 bird..." (匹配度: 2)

第三步:增强 Prompt

def augment_query(query, retrieved_docs):
    """把检索结果加到问题里"""

    if not retrieved_docs:
        return f"用户问题:{query}"

    # 把检索结果拼成上下文
    context = "\n".join([doc["content"] for doc in retrieved_docs])

    # 构建增强后的 prompt
    augmented_prompt = f"""
根据以下学习记录回答用户的问题:

学习记录:
{context}

用户问题:{query}

要求:根据学习记录回答,不要编造内容。
"""
    return augmented_prompt

增强后的 Prompt

根据以下学习记录回答用户的问题:

学习记录:
2026-04-15 学习了 cat,意思是猫...
2026-04-16 学习了 dog,意思是狗...
2026-04-17 学习了 bird,意思是鸟...

用户问题:我学了哪些单词?

要求:根据学习记录回答,不要编造内容。

第四步:调用 AI 生成回答

def generate(prompt):
    """调用 AI API 生成回答"""
    data = {
        "model": "glm-5",
        "messages": [{"role": "user", "content": prompt}],
        "temperature": 0.3
    }

    response = requests.post(API_URL, headers=API_HEADERS, json=data)
    result = response.json()
    return result["choices"][0]["message"]["content"]

# 完整流程
def rag_chat(query):
    # 1. 检索
    docs = retrieve(query, documents)

    # 2. 增强
    augmented_prompt = augment_query(query, docs)

    # 3. 生成
    answer = generate(augmented_prompt)

    return answer

最终效果

用户:我学了哪些单词?

AI:根据你的学习记录,你已经学习了以下单词:
- cat(猫)- 2026-04-15
- dog(狗)- 2026-04-16
- bird(鸟)- 2026-04-17
- apple(苹果)- 2026-04-18

四、完整代码结构

week3/02_rag/
├── app.py                           # 主程序
└── documents/
    └── learning_records.json        # 学习记录

app.py 核心逻辑

# 完整 RAG 流程
def rag_chat(query):
    print(f"\n用户问题:{query}")

    # 1. 检索(Retrieval)
    print("【第一步:检索】")
    retrieved_docs = retrieve(query, documents)
    print(f"找到 {len(retrieved_docs)} 条相关记录")

    # 2. 增强(Augmented)
    print("【第二步:增强】")
    augmented_prompt = augment_query(query, retrieved_docs)

    # 3. 生成(Generation)
    print("【第三步:生成】")
    answer = generate(augmented_prompt)
    print(f"AI 回答:{answer}")

    return answer

完整代码

# -*- coding: utf-8 -*-
"""
Week 3 Day 2: RAG 入门示例

让 Agent 能回答"我学了哪些单词"这类需要查询用户数据的问题。

RAG 流程:
    1. 检索(Retrieval):从文档中找到相关内容
    2. 增强(Augmented):把检索结果加到问题里
    3. 生成(Generation):调用 AI 回答

运行:
    python app.py

测试问题:
    - 我学了哪些单词?
    - 我答错了什么?
    - 我什么时候学了 cat?
"""

import requests
import json
import os

# ===== API 配置 =====
API_URL = "https://coding.dashscope.aliyuncs.com/v1/chat/completions"
API_HEADERS = {
    "Content-Type": "application/json",
    "Authorization": "Bearer sk-sp-xxx"
}
API_MODEL = "glm-5"

# ===== 加载文档数据 =====
# 使用脚本所在目录的相对路径
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
DOCUMENTS_FILE = os.path.join(SCRIPT_DIR, "documents", "learning_records.json")

def load_documents():
    """从文件加载学习记录"""
    if os.path.exists(DOCUMENTS_FILE):
        with open(DOCUMENTS_FILE, "r", encoding="utf-8") as f:
            return json.load(f)
    print(f"警告:文档文件不存在:{DOCUMENTS_FILE}")
    return []

# 全局文档数据
documents = load_documents()

# ===== 第一步:检索(Retrieval) =====

def retrieve(query, documents, top_k=3):
    """
    从文档中检索相关内容。

    入门版:使用关键词匹配。
    进阶版:会用 Embedding 进行语义搜索。
    """
    results = []

    # 简单关键词匹配
    keywords = extract_keywords(query)

    for doc in documents:
        content = doc["content"]
        # 计算匹配度:有多少关键词出现在文档中
        match_count = sum(1 for kw in keywords if kw in content)

        if match_count > 0:
            results.append({
                "content": content,
                "score": match_count
            })

    # 按匹配度排序,取前 top_k 个
    results.sort(key=lambda x: x["score"], reverse=True)
    return results[:top_k]

def extract_keywords(query):
    """从用户问题中提取关键词"""
    # 简单版:提取常见关键词
    keywords = []

    # 学习相关关键词
    if "学" in query or "学习" in query:
        keywords.append("学习")
    if "错" in query or "答错" in query:
        keywords.append("答错")
    if "测验" in query or "考试" in query:
        keywords.append("测验")
    if "正确" in query:
        keywords.append("正确")

    # 单词关键词(提取英文单词)
    for word in ["cat", "dog", "bird", "fish", "apple", "banana", "happy", "rabbit"]:
        if word in query.lower():
            keywords.append(word)

    # 如果没有提取到关键词,就用原始问题
    if not keywords:
        keywords = [query]

    return keywords

# ===== 第二步:增强(Augmented) =====

def augment_query(query, retrieved_docs):
    """
    把检索结果加到问题里,形成增强后的 prompt。
    """
    if not retrieved_docs:
        # 没有找到相关文档,直接返回原问题
        return f"用户问题:{query}\n\n注意:没有找到相关的学习记录,请根据已有知识回答。"

    # 把检索结果格式化
    context = "\n".join([doc["content"] for doc in retrieved_docs])

    # 构建增强后的 prompt
    augmented_prompt = f"""
根据以下学习记录回答用户的问题:

学习记录:
{context}

用户问题:{query}

要求:
1. 根据学习记录回答,不要编造内容
2. 如果学习记录中没有相关信息,就说"记录中没有找到"
3. 回答要简洁清晰
"""
    return augmented_prompt

# ===== 第三步:生成(Generation) =====

def generate(prompt):
    """
    调用 AI API 生成回答。
    """
    data = {
        "model": API_MODEL,
        "messages": [{"role": "user", "content": prompt}],
        "temperature": 0.3  # 低温度,确保回答准确
    }

    try:
        response = requests.post(API_URL, headers=API_HEADERS, json=data, timeout=60)

        if response.status_code != 200:
            return f"API 错误:{response.status_code}"

        result = response.json()
        return result["choices"][0]["message"]["content"]

    except requests.exceptions.Timeout:
        return "API 请求超时,请稍后重试"
    except requests.exceptions.RequestException as e:
        return f"网络错误:{str(e)}"

# ===== 完整 RAG 流程 =====

def rag_chat(query):
    """
    完整的 RAG 流程:检索 → 增强 → 生成
    """
    print(f"\n用户问题:{query}")
    print("-" * 30)

    # 1. 检索
    print("【第一步:检索】")
    retrieved_docs = retrieve(query, documents)
    print(f"找到 {len(retrieved_docs)} 条相关记录:")
    for doc in retrieved_docs:
        print(f"  - {doc['content']}")
    print()

    # 2. 增强
    print("【第二步:增强】")
    augmented_prompt = augment_query(query, retrieved_docs)
    print("增强后的 prompt(前100字):")
    print(augmented_prompt[:100] + "...")
    print()

    # 3. 生成
    print("【第三步:生成】")
    answer = generate(augmented_prompt)
    print("AI 回答:")
    print(answer)

    return answer

# ===== 测试用例 =====

def test_rag():
    """测试 RAG 功能"""
    print("=" * 50)
    print("RAG 入门示例测试")
    print("=" * 50)

    test_questions = [
        "我学了哪些单词?",
        "我答错了什么?",
        "我什么时候学了 cat?",
        "我测验成绩怎么样?",
    ]

    for question in test_questions:
        rag_chat(question)
        print("\n" + "=" * 50 + "\n")

# ===== 命令行交互 =====

def main():
    print("=" * 50)
    print("RAG 入门示例 - 英语学习记录查询")
    print("=" * 50)
    print(f"已加载 {len(documents)} 条学习记录")
    print()
    print("试试这些问题:")
    print("  - 我学了哪些单词?")
    print("  - 我答错了什么?")
    print("  - 我什么时候学了 cat?")
    print("  - 我测验成绩怎么样?")
    print("=" * 50)
    print()

    # 先跑一遍测试
    print(">>> 运行测试用例 <<<")
    test_rag()

    print("\n>>> 进入交互模式 <<<")
    print("输入问题查询学习记录,输入 'exit' 退出\n")

    while True:
        query = input("你的问题:").strip()

        if query.lower() in ["exit", "quit", "退出"]:
            print("再见!")
            break

        if not query:
            continue

        rag_chat(query)
        print()

if __name__ == "__main__":
    main()

五、RAG 的核心技术:Embedding

上面的例子用的是关键词匹配,实际生产环境用 Embedding(向量搜索)

1. 什么是 Embedding?

"猫" → [0.23, 0.45, 0.12, ...]
"狗" → [0.25, 0.44, 0.11, ...]  (和"猫"的数字很像)
"汽车" → [0.01, 0.02, 0.88, ...] (和"猫""狗"差别很大)

原理:把文字变成数字向量,意思相近的向量距离也近。

2. Embedding 流程

1. 准备阶段:
   文档 → Embedding API → 向量 → 存到向量数据库

2. 查询阶段:
   用户问题 → Embedding API → 向量 → 在数据库找相似 → 返回相关文档

3. 为什么用 Embedding?

关键词匹配Embedding 匹配
"猫" ≠ "猫咪""猫" ≈ "猫咪" ≈ "小猫"
"狗" ≠ "犬""狗" ≈ "犬" ≈ "狗狗"
完全匹配语义相似

4. 为什么 RAG 比 Elasticsearch 快?

你可能好奇:ES 也是用来做搜索的,为什么数据量大时 ES 很慢,RAG 却很快?

核心区别:检索方式不同

ElasticsearchRAG 向量检索
原理倒排索引 + 文本匹配向量 Embedding + 近似最近邻(ANN)
查询时遍历索引,逐个匹配关键词计算向量距离,找最近邻
时间复杂度O(N),数据量越大越慢O(logN) 或 O(1),数据量影响小
相似度只能字面匹配语义相似即可匹配

5. ES 为什么慢?

用户搜:"猫咪怎么养"
    ↓
ES 去倒排索引找包含"猫咪"的文档
    ↓
数据量 100万条 → 扫描 10万条包含"猫咪"的
    ↓
再排序、过滤、聚合...
    ↓
耗时:几百毫秒~几秒

ES 需要遍历文档,数据量越大,要扫描的文档越多。

6. RAG 为什么快?

用户搜:"猫咪怎么养" → 转成向量 [0.23, 0.45, ...]
    ↓
在向量空间里找"距离最近"的向量(用 ANN 算法)
    ↓
数据量 100万条 → 只需查几十次距离计算
    ↓
返回 Top-K 结果
    ↓
耗时:几毫秒~几十毫秒

关键:ANN 算法(如 HNSW)把向量空间组织成图结构,查找时像"跳台阶"一样快速定位,不需要遍历所有数据。

类比理解

ES 像RAG 向量检索像
图书馆的目录卡片图书馆的位置地图
找"猫" → 翻所有带"猫"的卡片找"猫" → 直接在地图上标出位置
卡片越多翻得越久地图大小不影响查找速度

实际应用场景

# ES 适合:精确匹配
"订单号 = 12345"
"用户名 = 张三"

# RAG 适合:语义搜索
"我想学动物单词" → 找到 "cat""dog""bird"
"猫怎么养" → 找到 "猫咪护理指南"(虽然没有"猫"字)

这段代码用的是关键词匹配(类似 ES),后面用 Embedding 后就变成真正的 RAG 向量检索了。


六、RAG vs 传统数据库(MySQL)

你可能还想问:RAG 和 MySQL 有什么区别?我什么时候用 MySQL,什么时候用 RAG?

核心区别:数据类型和查询方式

MySQLRAG
存储什么结构化数据(表、行、列)非结构化文本(文档、知识库)
查询方式SQL 精确匹配语义相似匹配
查询语言SELECT * WHERE id = 123"帮我找关于猫的资料"
结果特点精确、确定相似、相关
适合场景订单、用户、库存文档、FAQ、知识库

举个例子

场景:电商系统

-- MySQL:查询订单信息(精确匹配)
SELECT * FROM orders 
WHERE user_id = 12345 AND status = 'paid';
-- 结果:订单号、金额、时间、状态
# RAG:查询商品使用说明(语义搜索)
用户问:"这个充电宝能带上飞机吗?"
    ↓
RAG 检索:找到说明书里的 "航空携带规定" 段落
    ↓
回答:根据产品说明书,该充电宝容量为 10000mAh,
      符合民航规定,可以随身携带上飞机...

数据对比

MySQL 存储的结构化数据

+----+----------+--------+-----------+
| id | user_name| order  | amount    |
+----+----------+--------+-----------+
| 1  | 张三     | A001   | 199.00    |
| 2  | 李四     | A002   | 299.00    |
+----+----------+--------+-----------+

RAG 处理的非结构化数据

产品说明书.txt:
"本品采用锂离子电池,容量 10000mAh。
 符合民航局规定,可随身携带上飞机。
 请勿放入托运行李..."

用户常见问题.doc:
"Q: 充电需要多久?
 A: 约 3-4 小时可充满..."

简单对比

用 MySQL 当你需要用 RAG 当你需要
查订单、查用户、查库存查文档、查知识、回答问题
精确匹配(id = 123)语义匹配(相关问题)
结构化数据(表格)非结构化数据(文本)

实际项目中两者经常一起用

用户:"我上个月买的充电宝怎么充电?"

系统处理:
    1. MySQL:查用户上个月订单 → 找到充电宝商品
    2. RAG:检索该商品的说明书 → 找到充电说明
    3. AI:整合信息,生成回答

七、RAG 和 Function Calling 的区别

Function CallingRAG
AI 调用函数,执行动作AI 检索数据,回答问题
"动手"能力"翻书"能力
适合:查天气、发消息、存记录适合:查文档、问数据、知识问答

结合使用

用户:"帮我查一下我上周学了多少单词,然后发个总结到我的邮箱"

AI 处理流程:
    1. 用 RAG 检索学习记录(查数据)
    2. 用 Function Calling 调用邮件接口(发邮件)
    3. 生成总结内容

八、明天要干的

  • 用 Embedding API 实现真正的语义搜索
  • 让英语 Agent 能回答"我学了什么"

写于 2026-04-28 RAG入门

最近有点懈怠!!!!