3397 字
17 分钟
Embedding、向量检索与 RAG 系统

大语言模型拥有通用知识和推理能力,但并不天然知道企业内部文档、私有项目方案、用户文件、实时业务数据,以及训练完成后才产生的新资料。

假设公司内部规范写着:

当支付服务出现 E10023 错误码时,首先检查 Redis 分布式锁。
如果锁记录持续超过 30 秒,需要检查 payment-worker 是否异常退出。

如果文档很短,可以直接把它和用户问题一起交给模型。但当知识库包含数千份 PDF、Wiki 和产品手册时,不可能在每次请求中发送全部资料。更合理的方法是先找到与问题最相关的少量内容,再将其放入模型上下文。

这就是 RAG(Retrieval-Augmented Generation,检索增强生成):在生成回答前动态检索外部知识,而不是把所有知识永久训练进模型。

RAG 解决什么问题#

检索与生成#

RAG 可以拆成知识库构建和在线问答两个阶段:

RAG 检索增强生成流程

知识库构建通常在文档新增或更新时执行;在线问答则在每次用户请求到来时执行。将两者分开,才能独立扩展索引任务和查询服务。

RAG 更适合文档、规范、FAQ 和 Wiki。订单状态、账户余额等实时且结构化的数据,更适合通过 Function Calling 查询业务系统。真实应用常常同时使用两种能力。

关键词搜索为什么不够#

知识库可能写着“服务端无法建立数据库连接”,用户却搜索“MySQL 连不上怎么办”。两者语义相关,但词面并不完全相同。

关键词搜索擅长错误码、类名、版本号和 API Path 等精确匹配;语义搜索则能发现用词不同但含义接近的内容。企业检索通常会组合两者形成 Hybrid Search,而不是只保留其中一种。

Embedding 与语义相似度#

从文本到向量#

Embedding 会把文本转换成浮点数向量:

Redis 是一种内存数据存储系统

[0.0214, -0.1842, 0.7311, 0.0048, ...]

这些数字不是摘要,也不需要人直接理解。它们把语义映射到高维空间,使程序能够计算文本之间的相关程度。

Embedding 可以用于搜索、聚类、推荐、分类和异常检测,RAG 只是其中一个重要应用。它与生成模型承担不同职责:

模型主要职责
Embedding Model把文本转换为向量
Generation Model理解上下文、推理并生成回答

使用 OpenAI API 生成向量:

from openai import OpenAI

client = OpenAI()

response = client.embeddings.create(
    model='text-embedding-3-small',
    input='Redis 是一种高性能的内存数据存储系统。',
)

embedding = response.data[0].embedding

print(len(embedding))
print(embedding[:10])

text-embedding-3-small 与 text-embedding-3-large 还支持通过 dimensions 调整向量维度,在检索效果、存储空间和计算成本之间取舍。同一个索引中的文档和查询必须使用兼容的模型与维度。

余弦相似度#

文本检索经常使用余弦相似度比较两个向量的方向:

similarity(A, B) = (A · B) / (|A| × |B|)

可以用 NumPy 实现:

import numpy as np


def cosine_similarity(vector_a, vector_b) -> float:
    a = np.array(vector_a)
    b = np.array(vector_b)

    return float(
        np.dot(a, b)
        / (np.linalg.norm(a) * np.linalg.norm(b))
    )

一个最小语义搜索会为文档预先生成向量,再把查询转换为向量,计算相似度并按分数排序:

def search(query: str, knowledge_base: list[dict], top_k: int = 3):
    query_response = client.embeddings.create(
        model='text-embedding-3-small',
        input=query,
    )
    query_vector = query_response.data[0].embedding

    results = [
        {
            'text': item['text'],
            'score': cosine_similarity(
                query_vector,
                item['embedding'],
            ),
        }
        for item in knowledge_base
    ]

    return sorted(
        results,
        key=lambda item: item['score'],
        reverse=True,
    )[:top_k]

少量数据可以在内存中遍历。面对数十万或数百万个向量时,则需要 FAISS、pgvector、Qdrant、Milvus、OpenSearch 或托管 Vector Store 等索引能力来快速查找近邻。

文档处理与 Chunk#

为什么需要切片#

如果把一本 500 页的手册生成一个向量,它只能表达非常粗略的整体主题。用户询问第 378 页的某个故障步骤时,检索粒度会过大。

Chunk 是从原始文档中切出的、可以独立建立索引和检索的一段内容。每个 Chunk 通常保存:

{
  "id": "chunk_10001",
  "text": "Redis AOF 会记录服务器执行的写命令。",
  "embedding": [0.123, -0.423, 0.831],
  "metadata": {
    "source": "redis-guide.pdf",
    "page": 42,
    "section": "AOF",
    "version": "3.2"
  }
}

Chunk Size 通常以 Token 衡量。过大的 Chunk 会混入多个主题、增加输入成本;过小的 Chunk 则可能切断上下文,让“整个过程需要 3~5 分钟”失去指代对象。

合理的切片策略通常是:

  • 优先按标题、章节、段落、列表和代码块等语义结构切分;
  • 超过长度预算时再按 Token 拆分;
  • 使用适量 Overlap 缓解边界信息丢失;
  • 保留标题和父级章节等上下文信息。

Overlap 能减少关键段落被切断的问题,但也会增加向量数量、存储空间和重复检索结果。它不是越大越好。

Metadata 与引用#

Metadata 不只是展示信息,还参与过滤、权限和引用。常见字段包括:

document_id
source
page
section
version
language
product
tenant_id
access_level

例如用户使用产品 V3 时,检索前就应过滤掉 V1 和 V2 文档。答案中还可以根据 source、page 和 section 展示引用,让用户能够回到原始资料验证结论。

文档更新后,应删除或停用旧 Chunk、重新生成受影响的 Embedding 并刷新索引。知识库还要处理重复文档、多个版本和来源优先级。RAG 是持续运行的数据系统,不是一次性导入 PDF 的脚本。

检索质量#

Top-K 与 Score Threshold#

向量搜索通常返回最相近的 K 个 Chunk。例如 Top-K = 5 表示返回前五个候选结果。但“最接近”不一定代表“真正相关”。如果知识库只有 Java 文档,用户询问航空发动机,系统仍然可以勉强排出五个结果。

因此检索层应该同时考虑:

  • Top-K:最多召回多少候选;
  • Score Threshold:最低相关度要求;
  • Context Budget:最终允许加入多少 Token;
  • 无结果状态:资料不足时允许明确拒答。

Top-K 太小可能漏掉正确资料,太大则会增加 Token 和噪声。生产系统应通过评估集选择参数,而不是凭感觉固定一个数字。

向量检索适合快速召回,但距离最近的结果未必最能回答当前问题。常见做法是先召回较多候选,再使用更精确的模型或 Ranker 重新排序,最终只保留少量 Chunk。

关键词检索则适合 E10023、ClassLoader、IP 地址和版本号等精确内容。Hybrid Search 将语义结果与关键词结果融合,可以兼顾自然语言表达和精确匹配。

当 Chunk 很长、真正相关的信息很少时,还可以执行 Context Compression,只提取与问题有关的部分。但压缩本身可能丢失限定条件,因此关键数据仍需保留引用和原文访问能力。

Query Rewrite#

用户原话不一定是最佳检索 Query。例如“刚才那个错误怎么解决”需要结合会话状态重写为具体错误;“数据库连接不上”也可以扩展为连接超时、拒绝连接等同义表达。

Query Rewrite 和 Query Expansion 能改善召回,但必须避免把用户意图改错。重写后的 Query、原始问题和最终命中结果都应该进入日志,便于评估和排查。

构造 RAG 回答#

最小完整示例#

检索完成后,将相关内容与问题清楚分隔,再调用生成模型:

def answer_with_context(query: str, results: list[dict]) -> str:
    context = '\n\n'.join(
        (
            f"[来源: {item['source']}]\n"
            f"{item['text']}"
        )
        for item in results
    )

    response = client.responses.create(
        model='gpt-5.6',
        instructions=(
            '你是企业知识库助手。只能根据参考资料回答。'
            '如果资料不足,明确说明无法从知识库确认。'
            '回答时标注使用的来源。'
        ),
        input=f'''
REFERENCE MATERIAL

{context}

USER QUESTION

{query}
''',
    )

    return response.output_text

显式区分参考资料和用户问题,可以让上下文边界更清楚。但 Prompt 中写“只能根据资料回答”并不能保证模型绝不出错。重要场景还需要引用核验、Structured Outputs、业务规则和人工审核。

RAG、数据库查询与 Fine-tuning#

需求更适合的方案
查询技术文档、FAQ、产品手册RAG
查询实时订单、余额或库存Function Calling
改变固定风格和任务行为Fine-tuning
返回稳定的数据结构Structured Outputs

RAG 在运行时检索可更新的知识,不会把资料写入模型参数。Fine-tuning 更偏向调整行为模式,也不适合每天重新训练以同步业务文档。

托管检索#

如果不希望自行维护 Chunk、Embedding 和向量索引,可以使用 OpenAI Vector Store。文件加入 Vector Store 后,会自动完成解析、切片、Embedding 和索引。

vector_store = client.vector_stores.create(
    name='Technical Knowledge Base',
)

随后可以直接执行自然语言搜索:

results = client.vector_stores.search(
    vector_store_id=vector_store.id,
    query='Redis AOF 的工作原理是什么?',
)

也可以让 Responses API 使用托管 File Search 工具:

response = client.responses.create(
    model='gpt-5.6',
    input='根据知识库解释 Redis AOF 的工作原理。',
    tools=[
        {
            'type': 'file_search',
            'vector_store_ids': [vector_store.id],
        }
    ],
)

print(response.output_text)

File Search 会在知识库中执行语义和关键词检索,并把相关内容提供给模型,适合快速验证和常规文档问答。

自建还是托管#

方案优势更适合
OpenAI File Search接入快、基础设施少原型、小型和常规知识库
自建 Retrieval Layer检索和数据控制能力高复杂权限、自定义 Rerank、多数据源
本地 FAISS简单、无需独立服务实验和小型离线数据
pgvector 等数据库方案可复用现有数据体系已有数据库和过滤需求的系统

数据规模很小时,不必为了“做 RAG”搭建复杂分布式向量数据库;需要供应商无关架构、精细权限或特殊索引策略时,自建检索层通常更合适。

权限与多租户#

权限必须在检索前生效#

不能先检索所有文档,再通过 Prompt 要求模型不要泄露。内容一旦进入 Context,模型就已经获得数据。

正确的安全边界是先验证用户身份和会话归属,再将权限条件加入 Retrieval Filter,只允许有权访问的 Chunk 进入候选集合。

filters = {
    'tenant_id': current_user.tenant_id,
    'department': current_user.department,
    'access_level': {'$lte': current_user.access_level},
}

多租户系统至少要确保每次查询都包含 tenant_id。高安全场景还可以为不同租户建立独立索引或 Vector Store,从架构上降低跨租户数据泄漏风险。

检索日志也可能包含用户问题、文件名和敏感片段,需要执行访问控制、脱敏与生命周期管理。

评估与生产实践#

分层定位失败#

RAG 回答错误不一定是生成模型的问题。常见故障可以分为:

层级典型问题
数据文档错误、过期、重复或解析失败
Retrieval正确资料存在但没有召回
Ranking正确资料被召回但排名过低
Context内容重复、顺序混乱或超过预算
Generation模型拿到正确资料仍理解或表达错误

检索层可以关注 Recall@K、Precision@K、MRR 和 NDCG;生成层则关注答案正确性、Faithfulness、Citation Accuracy 和 Answer Relevance。分开评估才能知道应该调整 Chunk、Retriever、Reranker 还是 Prompt。

Token Budget 与可观测性#

RAG 是上下文预算的重要消费者。Retriever 不只要考虑相关性,还要计算 Chunk 的 Token 成本,为历史对话、当前问题、模型回答和推理留出空间。

生产环境建议记录:

原始 Query 与重写 Query
命中的 document_id 和 chunk_id
召回分数与 Rerank 分数
过滤条件与权限范围
进入 Context 的 Token 数
最终引用
回答延迟与用户反馈

这些数据既用于质量评估,也用于排查权限、版本污染和成本问题。日志中不应保存不必要的敏感正文。

推荐的模块边界#

一个可维护的 RAG 服务通常包含文档解析、Chunk、Embedding、向量存储、Retriever、Reranker、Context Builder、LLM Client 和 Evaluation 等独立模块。

真正决定效果上限的往往不是最后几行模型调用,而是文档质量、切片策略、Metadata、查询改写、检索和上下文选择。Vector Database 只是检索基础设施,Embedding 只是文本表示方式,两者都不等于完整 RAG。

总结#

Embedding 将文本转换成可计算语义距离的向量;Vector Search 从大量数据中找到相关内容;RAG 再把这些内容加入 Context,让模型基于外部知识生成答案。

可靠的 RAG 系统需要同时处理数据质量、Chunk、Metadata、Hybrid Search、Rerank、Token Budget、引用、权限和评估。它更像信息检索系统与生成系统的组合,而不是简单地给模型接入一个向量数据库。

当应用已经具备多轮对话、Structured Outputs、Function Calling 和 RAG 后,模型就能维护状态、获取知识并调用外部能力。继续加入任务规划、工具循环和结束条件,就会逐渐进入 Agent 系统的范畴。

本文 API 行为参考:

Embedding、向量检索与 RAG 系统
https://lllirunze.cn/posts/embedding-vector-search-rag/
作者
柒月の風灬
发布于
2026-09-24
许可协议
CC BY-NC-SA 4.0