文本嵌入、向量数据库和 RAG(检索增强生成)架构详解。
什么是 Embedding?
Embedding(嵌入)是将文本转换为数值向量的技术。向量可以捕捉文本的语义信息,语义相似的文本在向量空间中距离更近。
为什么需要 Embedding?
大语言模型有两个核心限制:
- 知识截止:训练数据有截止日期,不知道最新信息
- 上下文限制:无法一次性处理海量文档
Embedding + 向量检索可以解决这些问题:将文档转换为向量存储,查询时检索相关内容,再交给 LLM 处理。
Embedding 的本质:
Embedding 模型将文本映射到高维向量空间。在这个空间中:
- 语义相似的文本距离近
- 语义不同的文本距离远
- 可以用数学方法计算相似度
"猫" → [0.2, 0.5, 0.1, ...]
"狗" → [0.3, 0.4, 0.2, ...] # 与"猫"接近
"汽车" → [0.8, 0.1, 0.9, ...] # 与"猫"较远
应用场景
flowchart LR
A[Embedding] --> B[语义搜索]
A --> C[文档问答 RAG]
A --> D[推荐系统]
A --> E[文本分类]
A --> F[聚类分析]| 场景 | 说明 | 示例 |
|---|---|---|
| 语义搜索 | 根据含义而非关键词匹配 | 搜索”如何减肥”也能找到”瘦身方法” |
| RAG | 检索相关文档增强 LLM 回答 | 企业知识库问答 |
| 相似度计算 | 判断两段文本是否相似 | 查重、去重 |
| 聚类 | 将相似文档分组 | 新闻分类、话题发现 |
| 推荐 | 找到相似内容 | “看过这个的人也看了” |
Embedding API 对比
| 厂商 | 模型 | 维度 | 最大 Token | 价格/百万Token | 特点 |
|---|---|---|---|---|---|
| OpenAI | text-embedding-3-small | 1536 | 8191 | $0.02 | 性价比高 |
| OpenAI | text-embedding-3-large | 3072 | 8191 | $0.13 | 精度最高 |
| Cohere | embed-english-v3.0 | 1024 | 512 | $0.10 | 多语言 |
| Voyage | voyage-2 | 1024 | 4000 | $0.10 | 代码优化 |
选择建议:
| 场景 | 推荐模型 |
|---|---|
| 通用场景 | text-embedding-3-small |
| 高精度需求 | text-embedding-3-large |
| 成本敏感 | 开源模型(如 BGE) |
| 中文优化 | BGE-M3、text2vec |
OpenAI Embedding
基础调用
from openai import OpenAI
client = OpenAI()
response = client.embeddings.create(
model="text-embedding-3-small",
input="你好世界"
)
vector = response.data[0].embedding # 1536 维向量
print(f"向量维度: {len(vector)}") # 1536
批量处理
批量处理比逐条调用更高效,建议一次处理多条文本:
texts = ["文本1", "文本2", "文本3"]
response = client.embeddings.create(
model="text-embedding-3-small",
input=texts
)
vectors = [d.embedding for d in response.data]
# vectors[i] 对应 texts[i]
批量处理建议:
- 单次最多 2048 条文本
- 总 Token 数不超过模型限制
- 大量数据分批处理,避免超时
降维(节省存储)
text-embedding-3 系列支持指定输出维度,可以在精度和存储之间权衡:
# 指定输出维度(仅 text-embedding-3 支持)
response = client.embeddings.create(
model="text-embedding-3-small",
input="你好世界",
dimensions=512 # 从 1536 降到 512
)
降维效果:
| 维度 | 存储大小 | 精度损失 |
|---|---|---|
| 1536 | 100% | 0% |
| 1024 | 67% | ~1% |
| 512 | 33% | ~3% |
| 256 | 17% | ~5% |
相似度计算
余弦相似度
余弦相似度是最常用的向量相似度计算方法,衡量两个向量的方向是否一致:
import numpy as np
def cosine_similarity(a, b):
return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))
# 使用
sim = cosine_similarity(vector1, vector2)
# 结果范围 [-1, 1],越接近 1 越相似
其他相似度方法:
| 方法 | 公式 | 特点 |
|---|---|---|
| 余弦相似度 | cos(θ) | 最常用,不受向量长度影响 |
| 欧氏距离 | L2 | 受向量长度影响 |
| 点积 | a·b | 需要归一化向量 |
相似度阈值参考
| 相似度 | 含义 | 应用建议 |
|---|---|---|
| > 0.9 | 几乎相同 | 去重、查重 |
| 0.7-0.9 | 高度相关 | 语义搜索 Top 结果 |
| 0.5-0.7 | 有一定关联 | 扩展搜索结果 |
| < 0.5 | 关联较弱 | 通常不相关 |
注意: 阈值因模型和场景而异,建议通过实验确定最佳阈值。
向量数据库
向量数据库专门用于存储和检索高维向量,支持快速的相似度搜索。
为什么需要向量数据库?
普通数据库不支持高效的向量相似度搜索。向量数据库使用专门的索引算法(如 HNSW、IVF),可以在毫秒级完成百万级向量的相似度搜索。
主流选择
| 数据库 | 类型 | 特点 | 适用场景 |
|---|---|---|---|
| Pinecone | 云服务 | 全托管,易用 | 快速上手、小团队 |
| Milvus | 开源 | 功能强大,分布式 | 大规模生产 |
| Chroma | 开源 | 轻量级,内嵌 | 本地开发、原型 |
| Qdrant | 开源 | Rust 实现,高性能 | 生产环境 |
| pgvector | 扩展 | PostgreSQL 插件 | 已有 PG 环境 |
| Weaviate | 开源 | 内置向量化 | 全栈方案 |
选择决策:
flowchart TD
A[选择向量数据库] --> B{数据规模?}
B -->|< 10万| C[Chroma/SQLite]
B -->|10万-1000万| D{运维能力?}
B -->|> 1000万| E[Milvus/Qdrant 集群]
D -->|强| F[Qdrant/Milvus]
D -->|弱| G[Pinecone 云服务]Chroma 示例
Chroma 是最简单的向量数据库,适合本地开发和原型:
import chromadb
# 创建客户端
client = chromadb.Client()
# 创建集合
collection = client.create_collection("docs")
# 添加文档
collection.add(
documents=["文档1内容", "文档2内容"],
ids=["doc1", "doc2"]
)
# 查询
results = collection.query(
query_texts=["查询内容"],
n_results=5
)
Pinecone 示例
from pinecone import Pinecone
pc = Pinecone(api_key="YOUR_KEY")
index = pc.Index("my-index")
# 插入向量
index.upsert(vectors=[
{"id": "doc1", "values": vector1, "metadata": {"text": "..."}},
{"id": "doc2", "values": vector2, "metadata": {"text": "..."}}
])
# 查询
results = index.query(vector=query_vector, top_k=5, include_metadata=True)
RAG 架构
RAG(Retrieval-Augmented Generation)通过检索相关文档来增强 LLM 的回答能力。
工作流程
flowchart TD
A[用户问题] --> B[Embedding]
B --> C[向量检索]
C --> D[获取相关文档]
D --> E[构建 Prompt]
E --> F[LLM 生成]
F --> G[返回答案]
H[(向量数据库)] --> C基础实现
def rag_query(question, collection, llm_client):
# 1. 检索相关文档
results = collection.query(query_texts=[question], n_results=3)
# 2. 构建上下文
context = "\n".join(results["documents"][0])
# 3. 生成回答
prompt = f"""基于以下信息回答问题:
{context}
问题:{question}
"""
response = llm_client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": prompt}]
)
return response.choices[0].message.content
文档处理
文本分块策略
长文档需要分块后再生成 Embedding。
flowchart LR
A[长文档] --> B{分块策略}
B --> C[固定长度]
B --> D[按段落]
B --> E[语义分块]| 策略 | 说明 | 适用场景 |
|---|---|---|
| 固定长度 | 每 N 个字符切分 | 简单场景 |
| 按段落 | 按自然段落切分 | 结构化文档 |
| 重叠分块 | 块之间有重叠 | 避免信息丢失 |
| 语义分块 | 按语义边界切分 | 高质量检索 |
分块代码
def chunk_text(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
优化技巧
1. 查询优化
# 使用 HyDE:先让 LLM 生成假设答案,再用假设答案检索
def hyde_search(question, collection, llm_client):
# 生成假设答案
hypo = llm_client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": f"简短回答:{question}"}]
).choices[0].message.content
# 用假设答案检索
return collection.query(query_texts=[hypo], n_results=5)
2. 重排序
# 检索后用 LLM 重排序
def rerank(question, docs, llm_client):
prompt = f"""对以下文档按与问题的相关性排序,返回排序后的编号:
问题:{question}
文档:
{chr(10).join([f'{i+1}. {d}' for i, d in enumerate(docs)])}
"""
# ... 解析 LLM 返回的排序
3. 混合检索
结合关键词搜索和向量搜索,取长补短:
flowchart TD
A[查询] --> B[关键词搜索 BM25]
A --> C[向量搜索]
B --> D[结果融合 RRF]
C --> D
D --> E[最终结果]为什么需要混合检索?
| 方法 | 优势 | 劣势 |
|---|---|---|
| 关键词搜索 | 精确匹配、专有名词 | 无法理解语义 |
| 向量搜索 | 语义理解、同义词 | 可能忽略关键词 |
| 混合检索 | 兼顾两者优势 | 实现复杂 |
def hybrid_search(query, collection, bm25_index, alpha=0.5):
"""混合检索:向量 + BM25"""
# 向量搜索
vector_results = collection.query(query_texts=[query], n_results=20)
# BM25 关键词搜索
bm25_results = bm25_index.search(query, top_k=20)
# RRF 融合(Reciprocal Rank Fusion)
scores = {}
k = 60 # RRF 参数
for rank, doc_id in enumerate(vector_results["ids"][0]):
scores[doc_id] = scores.get(doc_id, 0) + alpha / (k + rank + 1)
for rank, doc_id in enumerate(bm25_results):
scores[doc_id] = scores.get(doc_id, 0) + (1 - alpha) / (k + rank + 1)
# 按分数排序
sorted_docs = sorted(scores.items(), key=lambda x: x[1], reverse=True)
return sorted_docs[:10]
RAG 进阶技巧
1. 查询改写
用户查询可能不够清晰,先改写再检索:
def rewrite_query(query, llm_client):
"""改写查询以提高检索效果"""
response = llm_client.chat.completions.create(
model="gpt-4o-mini",
messages=[{
"role": "user",
"content": f"""将以下查询改写为更适合搜索的形式,生成 3 个变体:
原始查询:{query}
改写要求:
1. 保持原意
2. 使用不同的表达方式
3. 包含可能的同义词
输出格式:每行一个查询"""
}]
)
queries = response.choices[0].message.content.strip().split("\n")
return [query] + queries # 原始查询 + 改写查询
2. 上下文压缩
检索到的文档可能包含无关内容,压缩后再使用:
def compress_context(question, documents, llm_client):
"""压缩上下文,只保留相关部分"""
response = llm_client.chat.completions.create(
model="gpt-4o-mini",
messages=[{
"role": "user",
"content": f"""从以下文档中提取与问题相关的信息:
问题:{question}
文档:
{chr(10).join(documents)}
只输出与问题直接相关的内容,删除无关信息。"""
}]
)
return response.choices[0].message.content
3. 多步检索
复杂问题可能需要多步检索:
flowchart TD
A[复杂问题] --> B[分解子问题]
B --> C[检索子问题1]
B --> D[检索子问题2]
C --> E[合并结果]
D --> E
E --> F[生成最终答案]4. 引用来源
让 LLM 在回答中标注引用来源:
def rag_with_citations(question, documents, llm_client):
"""带引用的 RAG"""
# 给文档编号
numbered_docs = "\n".join([f"[{i+1}] {doc}" for i, doc in enumerate(documents)])
response = llm_client.chat.completions.create(
model="gpt-4o",
messages=[{
"role": "user",
"content": f"""基于以下文档回答问题,在回答中用 [编号] 标注引用来源。
文档:
{numbered_docs}
问题:{question}
要求:
1. 只使用文档中的信息
2. 每个观点都要标注来源
3. 如果文档中没有相关信息,说明"根据提供的文档无法回答"
"""
}]
)
return response.choices[0].message.content
常见问题
Q: 检索结果不相关怎么办?
| 问题 | 解决方案 |
|---|---|
| 分块太大 | 减小 chunk_size |
| 分块太小 | 增加 chunk_size 或使用重叠 |
| 查询太短 | 查询扩展/改写 |
| 领域词汇 | 使用领域专用 Embedding 模型 |
Q: 如何评估 RAG 效果?
| 指标 | 说明 |
|---|---|
| 召回率 | 相关文档是否被检索到 |
| 准确率 | 检索结果中相关文档的比例 |
| MRR | 第一个相关结果的排名 |
| 答案质量 | 人工评估最终答案 |
Q: 向量数据库如何选择?
flowchart TD
A[选择向量数据库] --> B{预算}
B -->|有预算| C{运维能力}
B -->|无预算| D[开源方案]
C -->|强| E[Milvus/Qdrant]
C -->|弱| F[Pinecone]
D --> G{数据规模}
G -->|小| H[Chroma/SQLite]
G -->|大| I[Qdrant/Milvus]成本估算
| 场景 | 文档数 | Token 数 | Embedding 成本 | 存储成本/月 |
|---|---|---|---|---|
| 小型知识库 | 1,000 | 50万 | ~$0.01 | ~$0 |
| 中型知识库 | 10,000 | 500万 | ~$0.10 | ~$5 |
| 大型知识库 | 100,000 | 5000万 | ~$1.00 | ~$50 |
| 企业级 | 1,000,000 | 5亿 | ~$10.00 | ~$500 |
使用 text-embedding-3-small,$0.02/百万 Token 存储成本基于 Pinecone 标准版估算