为了便于理解代码实现部分,这篇笔记把 第3周 Day 4:RAG 实战——用 Embedding 实现语义搜索昨天的 RAG 入门用的是关键词匹配,有个问题: - 掘金 里的代码拆开讲,帮助理解每个部分。
一、导入的库是什么意思?
import requests # 发 HTTP 请求,调用 API
import json # 处理 JSON 数据
import os # 处理文件路径
import numpy as np # 数值计算,用来算向量相似度
1.1 requests —— 发网络请求
作用:让 Python 能像浏览器一样发请求,调用 API。
类比:
| requests | JavaScript |
|---|---|
requests.post(url, json=data) | fetch(url, {method: 'POST', body: JSON.stringify(data)}) |
response.json() | response.json() |
response.status_code | response.status |
在代码里的用法:
# 调用 Embedding API
response = requests.post(EMBEDDING_URL, headers=EMBEDDING_HEADERS, json=data)
1.2 json —— 处理 JSON 数据
作用:把 Python 对象转成 JSON 字符串,或把 JSON 字符串转成 Python 对象。
类比:
| Python json | JavaScript |
|---|---|
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 numpy | JavaScript |
|---|---|
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-v2 | 1024 |
| OpenAI text-embedding-3-small | 1536 |
| OpenAI text-embedding-3-large | 3072 |
向量越长,能表达的语义信息越丰富,但计算量也越大。
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(阿里云 DashScope)
EMBEDDING_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(智谱 GLM)
CHAT_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,把代码拆开讲,方便理解