第3周 Day 5:代码详解:RAG 实战——每个库、每个方法是什么意思

4 阅读12分钟

为了便于理解代码实现部分,这篇笔记把 第3周 Day 4:RAG 实战——用 Embedding 实现语义搜索昨天的 RAG 入门用的是关键词匹配,有个问题: - 掘金 里的代码拆开讲,帮助理解每个部分。


一、导入的库是什么意思?

import requests   # 发 HTTP 请求,调用 API
import json       # 处理 JSON 数据
import os         # 处理文件路径
import numpy as np  # 数值计算,用来算向量相似度

1.1 requests —— 发网络请求

作用:让 Python 能像浏览器一样发请求,调用 API。

类比

requestsJavaScript
requests.post(url, json=data)fetch(url, {method: 'POST', body: JSON.stringify(data)})
response.json()response.json()
response.status_coderesponse.status

在代码里的用法

# 调用 Embedding API
response = requests.post(EMBEDDING_URL, headers=EMBEDDING_HEADERS, json=data)

1.2 json —— 处理 JSON 数据

作用:把 Python 对象转成 JSON 字符串,或把 JSON 字符串转成 Python 对象。

类比

Python jsonJavaScript
json.loads(str)JSON.parse(str)
json.dumps(obj)JSON.stringify(obj)

在代码里的用法

# 读取 JSON 文件
with open(DOCUMENTS_FILE, "r", encoding="utf-8") as f:
    return json.load(f)  # 把 JSON 文件内容转成 Python 列表/字典

1.3 os —— 处理文件路径

作用:处理文件和目录路径,让代码能在不同电脑上运行。

为什么需要

# 错误写法(在别的电脑上会找不到文件)
DOCUMENTS_FILE = "documents/learning_records.json"

# 正确写法(自动找到脚本所在目录)
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
DOCUMENTS_FILE = os.path.join(SCRIPT_DIR, "documents", "learning_records.json")

常用方法

方法意思
os.path.abspath(__file__)获取当前脚本的绝对路径
os.path.dirname(path)获取路径的目录部分
os.path.join(a, b)把两个路径拼起来
os.path.exists(path)检查文件是否存在

1.4 numpy —— 数值计算

作用:处理数组、矩阵,做数学运算。这里用来计算向量相似度。

类比

Python numpyJavaScript
np.array([1,2,3])[1,2,3](JS 数组)
np.dot(a, b)a.reduce((sum, x, i) => sum + x * b[i], 0)(点积)
np.linalg.norm(a)Math.sqrt(a.reduce((sum, x) => sum + x*x, 0))(向量长度)

在代码里的用法

a = np.array(vec1)  # 把列表转成 numpy 数组
b = np.array(vec2)

dot_product = np.dot(a, b)  # 点积:a[0]*b[0] + a[1]*b[1] + ...
norm_a = np.linalg.norm(a)  # 向量长度:sqrt(a[0]^2 + a[1]^2 + ...)

二、核心函数详解

2.1 get_embedding(text) —— 把文字转成向量

作用:调用 Embedding API,把一段文字变成一串数字(向量)。

参数

参数类型意思
text字符串要转换的文字,如 "猫"

返回

返回值类型意思
embedding列表一串数字,如 [0.23, 0.45, 0.12, ...],长度通常是 1024 或 1536

流程

输入:"猫"
    ↓
调用 Embedding API
    ↓
返回:[0.23, 0.45, 0.12, -0.08, ...](1024个数字)

代码解释

def get_embedding(text):
    # 1. 准备请求数据
    data = {
        "model": "text-embedding-v2",  # 用哪个模型
        "input": {"texts": [text]},    # 要转换的文字(放在列表里)
        "parameters": {"text_type": "query"}  # 参数:这是查询文本
    }

    # 2. 发请求
    response = requests.post(EMBEDDING_URL, headers=EMBEDDING_HEADERS, json=data)

    # 3. 解析返回结果
    result = response.json()
    # 返回格式:{"output": {"embeddings": [{"embedding": [0.23, 0.45, ...]}]}}

    # 4. 提取向量
    return result["output"]["embeddings"][0]["embedding"]

2.2 cosine_similarity(vec1, vec2) —— 计算相似度

作用:计算两个向量的相似程度,返回 0~1 的数字,越接近 1 表示越相似。

参数

参数类型意思
vec1列表第一个向量,如 [0.23, 0.45, ...]
vec2列表第二个向量

返回

返回值类型意思
similarity浮点数0~1,越接近 1 越相似

数学公式

相似度 = (a · b) / (|a| × |b|)

其中:
a · b = a[0]*b[0] + a[1]*b[1] + ... (点积)
|a| = sqrt(a[0]^2 + a[1]^2 + ...) (向量长度)

通俗理解

想象两个向量是两根箭头:

           ↗ a
          /
         /
        •  (角度小 = 相似度高 = 接近 1)
       /
      ↘ b

如果两根箭头指向同一个方向,相似度接近 1
如果两根箭头指向相反方向,相似度接近 -1
如果两根箭头垂直,相似度接近 0

代码解释

def cosine_similarity(vec1, vec2):
    # 1. 转成 numpy 数组(方便计算)
    a = np.array(vec1)
    b = np.array(vec2)

    # 2. 计算点积(对应位置相乘再相加)
    dot_product = np.dot(a, b)
    # 例如:[1,2] · [3,4] = 1*3 + 2*4 = 11

    # 3. 计算向量长度(各元素平方后相加,再开根号)
    norm_a = np.linalg.norm(a)
    # 例如:|[1,2]| = sqrt(1^2 + 2^2) = sqrt(5) ≈ 2.24

    norm_b = np.linalg.norm(b)

    # 4. 计算相似度
    similarity = dot_product / (norm_a * norm_b)
    # 例如:11 / (2.24 * 4.47) ≈ 0.98(很相似)

    return similarity

2.3 build_vector_index(documents) —— 构建向量索引

作用:给每个文档生成向量,存起来。这样检索时不用每次都调用 API。

为什么要提前生成

每次调用 Embedding API 需要:
    - 网络请求时间(0.1~0.5秒)
    - API 费用(按次数收费)

如果有 100 条文档,检索时调用 100 次 API:
    - 时间:10~50 秒
    - 费用:贵!

提前生成向量索引后:
    - 检索时只需要计算相似度(毫秒级)
    - 不需要再调用 API

代码解释

def build_vector_index(documents):
    # 遍历每个文档
    for doc in documents:
        # 给文档内容生成向量,存到 doc["embedding"] 里
        doc["embedding"] = get_embedding(doc["content"])

    return documents

# 结果:
# [
#   {"id": 1, "content": "学习了 cat...", "embedding": [0.23, 0.45, ...]},
#   {"id": 2, "content": "学习了 dog...", "embedding": [0.25, 0.44, ...]},
# ]

2.4 retrieve_by_embedding(query, documents) —— 向量检索

作用:用向量相似度找到最相关的文档。

流程

用户问:"我学了什么猫咪相关的单词?"1. 把问题转成向量:get_embedding("我学了什么猫咪相关的单词?")
    ↓
2. 计算和每个文档的相似度
    ↓
3. 按相似度排序
    ↓
返回最相关的几条文档

代码解释

def retrieve_by_embedding(query, documents, top_k=3):
    # 1. 把用户问题转成向量
    query_embedding = get_embedding(query)

    # 2. 计算和每个文档的相似度
    results = []
    for doc in documents:
        # 计算相似度
        similarity = cosine_similarity(query_embedding, doc["embedding"])

        # 把结果存起来
        results.append({
            "content": doc["content"],
            "score": similarity  # 相似度分数
        })

    # 3. 按相似度排序(从高到低)
    results.sort(key=lambda x: x["score"], reverse=True)

    # 4. 返回前 top_k 个
    return results[:top_k]

三、数据结构详解

3.1 documents 数据结构

原始数据

documents = [
    {"id": 1, "content": "2026-04-15 学习了 cat,意思是猫"},
    {"id": 2, "content": "2026-04-16 学习了 dog,意思是狗"},
]

加向量索引后

documents = [
    {
        "id": 1,
        "content": "2026-04-15 学习了 cat,意思是猫",
        "embedding": [0.23, 0.45, 0.12, -0.08, ...]  # 新增:1024个数字
    },
    {
        "id": 2,
        "content": "2026-04-16 学习了 dog,意思是狗",
        "embedding": [0.25, 0.44, 0.11, -0.07, ...]
    },
]

3.2 API 返回数据结构

Embedding API 返回

{
    "output": {
        "embeddings": [
            {
                "text_index": 0,
                "embedding": [0.23, 0.45, 0.12, ...]  // 1024个数字
            }
        ]
    },
    "usage": {
        "total_tokens": 5
    }
}

Chat API 返回

{
    "choices": [
        {
            "message": {
                "role": "assistant",
                "content": "根据你的学习记录,你学了 cat..."
            }
        }
    ]
}

四、完整流程图解

用户问题:"我学了什么猫咪相关的单词?"

【第一步:向量检索】
    ↓
问题转向量:get_embedding("猫咪相关单词") → [0.23, ...]
    ↓
计算相似度:
    - 和 doc1(cat)相似度:0.87 ✅
    - 和 doc2(dog)相似度:0.42
    - 和 doc3(bird)相似度:0.31
    ↓
返回相似度最高的:doc1(cat)

【第二步:增强 Prompt】
    ↓
拼接上下文:
    "根据以下学习记录回答:
     2026-04-15 学习了 cat,意思是猫

     问题:我学了什么猫咪相关的单词?"

【第三步:生成回答】
    ↓
调用 Chat API
    ↓
返回:"你学了 cat(猫),学习时间是 2026-04-15"

五、常见问题

Q1:向量长度为什么是 1024?

回答:这是模型决定的。不同的 Embedding 模型产生不同长度:

模型向量长度
text-embedding-v21024
OpenAI text-embedding-3-small1536
OpenAI text-embedding-3-large3072

向量越长,能表达的语义信息越丰富,但计算量也越大。

Q2:相似度多少算"相似"?

回答:一般标准:

相似度意思
> 0.8非常相似(几乎一样)
0.5~0.8相关(可以算匹配)
< 0.5不太相关

实际使用时可以根据场景调整阈值。

Q3:为什么要用 numpy 而不是普通列表?

回答

# 用普通列表计算点积(慢)
def dot_product_list(a, b):
    result = 0
    for i in range(len(a)):
        result += a[i] * b[i]
    return result

# 用 numpy 计算点积(快100倍)
def dot_product_numpy(a, b):
    return np.dot(a, b)

numpy 是专门做数值计算的,底层用 C 语言实现,处理 1024 个数字的计算比普通 Python 循环快很多。


六、总结

库/方法作用
requests发 HTTP 请求,调用 API
json处理 JSON 数据
os处理文件路径
numpy数值计算,算向量相似度
get_embedding()把文字转成向量
cosine_similarity()计算两个向量的相似度
build_vector_index()给文档生成向量索引
retrieve_by_embedding()用向量相似度检索

核心概念

  • 向量:把文字变成一串数字,意思相近的数字也相近
  • 相似度:两个向量的"角度",越接近 1 表示越相似
  • 语义搜索:用户说"猫咪"能找到"猫",不依赖字面匹配

完整代码:

# -*- coding: utf-8 -*-
"""
Week 3 Day 3: RAG 实战 - Embedding 版

用向量相似度替换关键词匹配,实现真正的语义搜索。
用户搜"猫咪"能找到"",搜"小狗"能找到""。

运行:
    python app_embedding.py

测试问题:
    - 我学了什么猫咪相关的单词?
    - 我学了什么小狗相关的单词?
    - 我学了什么宠物相关的单词?
"""

import requests
import json
import os
import numpy as np

# ===== API 配置 =====
# Embedding API(阿里云 DashScopeEMBEDDING_URL = "https://dashscope.aliyuncs.com/api/v1/services/embeddings/text-embedding/text-embedding"
EMBEDDING_HEADERS = {
    "Authorization": "Bearer sk-sp-xxxx",
    "Content-Type": "application/json"
}
EMBEDDING_MODEL = "text-embedding-v2"

# Chat API(智谱 GLMCHAT_URL = "https://coding.dashscope.aliyuncs.com/v1/chat/completions"
CHAT_HEADERS = {
    "Content-Type": "application/json",
    "Authorization": "Bearer sk-sp-xxxx"
}
CHAT_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()

# ===== Embedding 函数 =====

def get_embedding(text):
    """
    调用 Embedding API,把文字转成向量。

    返回:一个向量(一串数字),通常长度为 1024 或 1536
    """
    data = {
        "model": EMBEDDING_MODEL,
        "input": {"texts": [text]},
        "parameters": {"text_type": "query"}
    }

    try:
        response = requests.post(EMBEDDING_URL, headers=EMBEDDING_HEADERS, json=data, timeout=30)

        if response.status_code != 200:
            print(f"Embedding API 错误:{response.status_code}")
            # 返回一个假的向量(用于演示)
            return [0.1] * 1024

        result = response.json()
        return result["output"]["embeddings"][0]["embedding"]

    except Exception as e:
        print(f"Embedding API 调用失败:{e}")
        # 返回一个假的向量(用于演示)
        return [0.1] * 1024

def cosine_similarity(vec1, vec2):
    """
    计算两个向量的余弦相似度。

    返回:0 到 1 之间的数字,越接近 1 表示越相似
    """
    a = np.array(vec1)
    b = np.array(vec2)

    # 计算点积
    dot_product = np.dot(a, b)

    # 计算向量长度
    norm_a = np.linalg.norm(a)
    norm_b = np.linalg.norm(b)

    # 防止除以 0
    if norm_a == 0 or norm_b == 0:
        return 0

    return dot_product / (norm_a * norm_b)

# ===== 构建向量索引 =====

def build_vector_index(documents):
    """
    把所有文档转成向量,存到 documents 里。
    这样检索时就不需要每次都调用 Embedding API。
    """
    print(f"正在为 {len(documents)} 条文档生成向量...")

    for i, doc in enumerate(documents):
        doc["embedding"] = get_embedding(doc["content"])
        print(f"  {i+1}/{len(documents)}: {doc['content'][:30]}...")

    print("向量索引构建完成!")
    return documents

# ===== 检索函数 =====

def retrieve_by_embedding(query, documents, top_k=3):
    """
    用向量相似度检索相关文档。

    流程:
    1. 把用户问题转成向量
    2. 计算和每个文档的相似度
    3. 按相似度排序,返回最相关的几条
    """
    # 把用户问题转成向量
    query_embedding = get_embedding(query)

    # 计算每个文档的相似度
    results = []
    for doc in documents:
        if "embedding" not in doc:
            continue

        similarity = cosine_similarity(query_embedding, doc["embedding"])
        results.append({
            "id": doc["id"],
            "content": doc["content"],
            "score": similarity
        })

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

# ===== 增强函数 =====

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])

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

学习记录:
{context}

用户问题:{query}

要求:
1. 根据学习记录回答,不要编造内容
2. 回答要简洁清晰
"""
    return augmented_prompt

# ===== 生成函数 =====

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

    try:
        response = requests.post(CHAT_URL, headers=CHAT_HEADERS, json=data, timeout=60)

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

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

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

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

def rag_with_embedding(query, documents):
    """
    完整的 Embedding 版 RAG 流程。

    流程:向量检索 → 增强 Prompt → 调用 AI
    """
    print(f"\n用户问题:{query}")
    print("-" * 40)

    # 1. 检索:用向量相似度找相关文档
    print("【第一步:向量检索】")
    docs = retrieve_by_embedding(query, documents)
    print(f"找到 {len(docs)} 条相关文档:")
    for doc in docs:
        print(f"  相似度 {doc['score']:.3f}: {doc['content']}")

    # 2. 增强:把检索结果加到问题里
    print("\n【第二步:增强 Prompt】")
    augmented_prompt = augment_query(query, docs)
    print("增强后的 prompt(前100字):")
    print(augmented_prompt[:100] + "...")

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

    return answer

# ===== 测试对比 =====

def compare_search_methods():
    """
    对比关键词匹配和向量匹配的区别。
    """
    print("=" * 50)
    print("关键词匹配 vs 向量匹配 对比测试")
    print("=" * 50)

    test_queries = [
        "我学了什么猫咪相关的单词?",
        "我学了什么小狗相关的单词?",
        "我学了什么宠物相关的单词?",
    ]

    # 简单关键词匹配(Day 2 的方法)
    def keyword_retrieve(query, docs):
        results = []
        keywords = ["猫", "狗", "鸟", "鱼", "苹果", "香蕉"]
        for doc in docs:
            for kw in keywords:
                if kw in query and kw in doc["content"]:
                    results.append(doc)
        return results

    print("\n>>> 关键词匹配结果:")
    for query in test_queries:
        print(f"\n用户问:{query}")
        results = keyword_retrieve(query, documents)
        if results:
            for doc in results:
                print(f"  找到:{doc['content']}")
        else:
            print(f"  ❌ 没找到")

    print("\n>>> 向量匹配结果:")
    for query in test_queries:
        print(f"\n用户问:{query}")
        results = retrieve_by_embedding(query, documents, top_k=2)
        for doc in results:
            print(f"  相似度 {doc['score']:.3f}: {doc['content']}")

# ===== 主程序 =====

def main():
    print("=" * 50)
    print("RAG 实战 - Embedding 版(语义搜索)")
    print("=" * 50)

    if not documents:
        print("没有加载到文档数据,请检查 documents/learning_records.json")
        return

    # 构建向量索引
    indexed_docs = build_vector_index(documents)

    print("\n" + "=" * 50)
    print("对比测试:关键词匹配 vs 向量匹配")
    print("=" * 50)

    # 先对比两种检索方式
    compare_search_methods()

    print("\n" + "=" * 50)
    print("完整 RAG 流程测试")
    print("=" * 50)

    print("\n试试这些语义搜索问题:")
    print("  - 我学了什么猫咪相关的单词?(能找到 cat)")
    print("  - 我学了什么小狗相关的单词?(能找到 dog)")
    print("  - 我学了什么宠物相关的单词?(能找到 cat/dog)")
    print("=" * 50)
    print()

    # 测试完整 RAG
    test_questions = [
        "我学了什么猫咪相关的单词?",
        "我学了什么宠物相关的单词?",
    ]

    for question in test_questions:
        rag_with_embedding(question, indexed_docs)
        print("\n" + "-" * 40 + "\n")

    # 交互模式
    print(">>> 进入交互模式 <<<")
    print("输入问题进行语义搜索,输入 'exit' 退出\n")

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

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

        if not query:
            continue

        rag_with_embedding(query, indexed_docs)
        print()

if __name__ == "__main__":
    main()

写于 2026-04-30,把代码拆开讲,方便理解