1 背景

围绕 响应速度健壮性准确度与灵活性 这三点展开。
1. 相关知识-RAG的优化:
https://fairy-study.blog.csdn.net/article/details/158778345?spm=1001.2014.3001.5502

2. RAG 基础知识(检索策略):
https://fairy-study.blog.csdn.net/article/details/158744941?spm=1001.2014.3001.5502

2 响应速度

响应速度 这一块主要依赖于 嵌入模型 的选型。大体分为开源 / 闭源两种,如果要用开源模型的话,Sentence-Transformer 无论是科研还是落地都是常用的选项。

2.1 Sentence-Transformer

Sentence-Transformer 的核心作用是将非结构化的文本(句子/段落)转化为稠密的、富含语义信息的向量,从而使得计算机可以“理解”并计算文本之间的语义关系。

2.1.1 特点

稠密检索:
不再依赖关键词匹配,而是根据语义(方向)搜索。
响应速度:
作为粗排阶段,从百万级文档中 快速 (毫秒级)召回 Top-K 个候选,然后再用更精细的 Cross-Encoder 模型进行重排序。
Bi-Encoder 架构:
先独立计算出所有句子的向量并缓存。之后计算相似度只需做简单的矩阵乘法(余弦相似度)。在最耗时间的向量化这块,时间复杂度为 0(n)。
在这里插入图片描述
查询时间复杂度:
在实际工程(如 FAISS, Milvus, Annoy)中,我们不会做 O(n) 的暴力扫描。我们会建立索引(如 HNSW, IVF-PQ)利用树结构或图结构快速剪枝,只搜索最可能的一部分区域。过程如下:

  • 预计算(建图):在有了所有句子的“坐标”(向量),你可以把这 100 万个向量全部画在一张图上。而对于 Bi-Encoder 架构 ,一般会建立索引,即在这些点之间连线,如果向量 A 和 B 很近,就在它们之间建立一条“高速公路”。
  • 查询(寻路):当新来一个查询句子 Q,你先算出它的向量,然后放到图上,沿着“高速公路”快速跳跃,很快就能找到离它最近的点。
  • 特点:时间复杂度为 0(logn)
2.1.2 传统嵌入方法 Bert
相比原始 BERT (Cross-Encoder 模式)

痛点:
原始 BERT 计算两个句子相似度时,必须将 (Sentence A, Sentence B) 一起输入模型。如果要计算 1 万个句子两两之间的相似度,需要计算 10000×9999/2≈5000 万次推理。这在大规模场景下是不可行的。时间复杂度在嵌入的时候,为 0(n2)。
特点:

  1. 输入为两两配对的形式,导致时间复杂度高;
  2. 输出为一个标量分数(0-1),不生成句子 A 或句子 B 的独立向量。它利用 Transformer 的 Self-Attention 机制,让 A 中的词和 B 中的词互相“看”对方(Cross-Attention)。因此无法建立索引。
2.1.3 使用

Sentence-Transformer 在保留了 BERT 强大的语义理解能力的同时,通过架构创新解决了其计算效率低下的问题,使得在大规模数据集上进行实时的语义搜索和聚类成为可能。

本质上,类似于 vLLMs 通过 PagedAttention 加速,因为 KV-Cache 那块本身就是 0(n2) 的时间复杂度,因此可以进行优化。

另外 Sentence-Transformer 不需要自己从头训练,只需一行代码即可切换不同的基座模型和微调策略。它支持的模型种类极其丰富:

  1. 多语言支持:例如 paraphrase-multilingual-MiniLM-L12-v2,支持 50+ 种语言,可以将中文、英文、法文映射到同一向量空间。
  2. 不同大小的模型(权衡速度与精度):轻量级/高速:all-MiniLM-L6-v2(目前最流行,速度极快,精度不错,适合实时系统)。
  3. 中等:all-mpnet-base-v2(精度更高,速度适中)。大型/高精度:基于 RoBERTa-Large 或 DeBERTa 的模型。
  4. 特定领域微调:生物医学:biobert 变体。法律:legal-bert 变体。
  5. 代码搜索:codebert 或 graphcodebert 变体(用于搜索代码片段)。
  6. 多模态模型:最新的版本甚至支持图像 - 文本匹配(如 CLIP 的封装),不仅限于纯文本。

例子:

from sentence_transformers import SentenceTransformer

# 1. 直接加载一个现成的模型(自动下载)
model = SentenceTransformer('all-MiniLM-L6-v2')

# 2. 一句话生成向量
sentences = ["一只猫坐在垫子上", "A cat is sitting on the mat"]
embeddings = model.encode(sentences)

# 3. 直接计算相似度
similarity = model.similarity(embeddings[0], embeddings[1])

2.2 选哪一个模型

2.2.1 闭源模型

介绍:
调用 API,比如 OpenAI Embeddingstext-embedding-3-smalltext-embedding-3-large,特点就是映射的向量维度很高(3072)维,适合精度要求较高的场景。
问题:

  1. 成本问题,成本难以控制,调用 api 要花钱的;
  2. 风险问题,api 容易泄露,必须通过环境变量的写法才更为安全(甚至还是不安全,因为环境变量也是能够通过调试工具去捕获的呀);
  3. 无法微调,无法适用于垂直领域;
  4. 并发量大的时候容易出现成本、系统崩溃等情况;
  5. 耗时高,API 调用涉及网络请求(尤其是海外 API,如 OpenAI),单次嵌入耗时可能达 100~500ms,高并发下延迟会放大;
2.2.2 开源模型

介绍:
Embedding 模型部署一般没有要求,CPU 也能很快进行处理,参数量一般在(几十~几百MB),非常适合垂直领域检索。
最常用的为 MiniLM-v12(384维)
特点:

  1. 响应速度很快,毫秒级别的响应速度,而且支持批处理;
  2. 支持微调,适合垂直领域;
  3. 成本低,且支持量化;
    用法(基于 sentence-transformers):
{"sentence1": "合同违约的赔偿标准", "sentence2": "民法典第584条违约责任", "label": 1.0}  # 高度相似
{"sentence1": "盗窃罪的量刑标准", "sentence2": "公司法股东权利", "label": 0.0}          # 不相似
{"sentence1": "医疗事故的举证责任", "sentence2": "侵权责任法医疗损害责任", "label": 0.9} # 较相似
from sentence_transformers import SentenceTransformer, InputExample, losses
from sentence_transformers.evaluation import EmbeddingSimilarityEvaluator
from torch.utils.data import DataLoader

# 1. 加载基础模型
model = SentenceTransformer('BAAI/bge-base-zh-v1.5')

# 2. 加载训练数据(自定义Dataset)
train_examples = []
with open("law_data.jsonl", "r", encoding="utf-8") as f:
    import json
    for line in f:
        data = json.loads(line.strip())
        train_examples.append(
            InputExample(
                texts=[data["sentence1"], data["sentence2"]],
                label=float(data["label"])
            )
        )

# 3. 配置训练参数
train_dataloader = DataLoader(train_examples, shuffle=True, batch_size=8)
# 损失函数:余弦相似度损失(适配相似性任务)
train_loss = losses.CosineSimilarityLoss(model=model)
# 评估器(验证微调效果)
evaluator = EmbeddingSimilarityEvaluator.from_input_examples(
    train_examples[:100],  # 取100条作为验证集
    name="law-eval"
)

# 4. 开始微调(核心!)
model.fit(
    train_objectives=[(train_dataloader, train_loss)],
    evaluator=evaluator,
    epochs=3,  # 训练轮数(垂直领域3~5轮足够)
    evaluation_steps=100,  # 每100步评估一次
    learning_rate=2e-5,    # 学习率(小模型用2e-5,大模型用1e-5)
    warmup_steps=100,      # 预热步数
    output_path="./bge-law-finetuned",  # 微调后模型保存路径
    show_progress_bar=True
)

# 5. 加载微调后的模型
fine_tuned_model = SentenceTransformer("./bge-law-finetuned")
# 测试微调效果
vec = fine_tuned_model.encode("合同违约的赔偿标准", normalize_embeddings=True)
print(f"微调后向量维度:{len(vec)}")

在这里插入图片描述

3 健壮性

3.1 API 健壮性问题

比如以以下代码为例,这是调用 API 时候实现的重试机制:

    def _with_retry(self, func, *args, **kwargs):
        """
        通用重试装饰器:指数退避 + 错误分类处理
        """
        delay = self.retry_delay
        
        for attempt in range(1, self.max_retries + 1):
            try:
                return func(*args, **kwargs)
            except Exception as e:
                # 🔧 兼容 openai 2.x:安全获取 Retry-After
                response = getattr(e, 'response', None)
                
                if isinstance(response, dict):
                    headers = response.get('headers', {})
                    retry_after = headers.get('Retry-After') if isinstance(headers, dict) else None
                else:
                    headers = getattr(response, 'headers', {})
                    retry_after = getattr(headers, 'get', lambda x: None)('Retry-After')
                
                wait = float(retry_after) if retry_after else delay
                
                logger.warning(f"⚠️ 请求失败 (尝试 {attempt}/{self.max_retries}),等待 {wait:.1f}s: {type(e).__name__}")
                time.sleep(wait)
                delay *= self.retry_backoff
                
        
        logger.error(f"❌ 重试 {self.max_retries} 次仍失败")
        return None

闭源 API 的三大痛点:

  • 版本兼容问题: OpenAI SDK 的版本迭代(如 1.x → 2.x)会导致 异常对象的结构变化(1.x 的 e.response 是字典,2.x 是对象),除此以外还有 头信息获取方式变化。这会导致代码要不断迭代,维护成本很高;
  • 服务不可靠问题: 服务降级 / 5xx 错误:需要多次重试才能成功;这些是闭源 API 的 “天然问题”,和版本无关,但必须靠重试逻辑兜底。

3.2 开源模型健壮性问题

开源模型部署在本地,确实不需要处理 API 相关的重试 / 版本兼容(因为没有 网络调用 、没有 第三方 SDK 版本 绑定),解决了 时间问题成本问题维护问题

  • 无需处理 Retry-After/ 限流:本地模型的并发仅受自身算力限制,没有第三方 QPS 限制;
  • 无需处理网络超时:推理在本地完成,无网络波动;
  • 无需兼容第三方 SDK 版本:直接调用模型库(如 transformers/sentence-transformers),版本由自己控制;
  • 无需 API 密钥 / 鉴权:本地运行,无密钥泄露 / 计费风险;

4 OpenAI 不同模型对应的分词标准是什么?

Question: 在 RAG 中,不同 OpenAI 模型要使用哪种 tiktoken 编码?
A: OpenAI 每个模型系列固定对应一种 BPE 分词规则,直接决定 token 计数和上下文长度。

  • gpt-4o / gpt-4o-mini / gpt-4-turbo
    o200k_base (200k 词表,新版多语言最优)
  • gpt-4 / gpt-3.5-turbo
    cl100k_base (100k 词表,最常用)
  • 旧版 davinci / codex 等
    → p50k_base / r50k_base

在 RAG 里的意义:

  • 必须用对应编码计算 chunk token,才能准确控制上下文窗口。
  • 本地计数必须和 API 计费一致,否则会超长度、报错、成本不准。

5. tiktoken 闭源分词在 RAG 中是什么角色?

Q: tiktoken 是开源还是闭源?在 RAG 里为什么必须用它?
A: tiktoken 是 OpenAI 闭源的、高度优化的 BPE 分词器。

  • 核心是:词表和合并规则是 OpenAI 预训练好的黑盒
  • 只提供 encode / decode / count 接口,不能重新训练
  • 用 Rust 实现,极快,适合大规模文档分块

RAG 中的作用:

  • 做 chunk 切分的精确 token 计数
  • 保证 chunk 大小不超模型上限
  • 唯一能和 OpenAI API 计费完全对齐的工具

一句话:用 OpenAI 模型做 RAG,tiktoken 就是标准尺子。

6 开源分词在 RAG 中怎么用?和 tiktoken 区别?

Q: 本地开源模型(Llama/Qwen/ChatGLM)做 RAG,用什么分词?
A: 用 HuggingFace AutoTokenizer / SentencePiece 等开源分词工具。

特点:

  • 每个模型自带专属词表,必须和模型训练时一致
  • 完全开源、可查看、可修改、可训练
  • 支持 BPE / WordPiece / Unigram 等

RAG 中的用法:

  • 开源 LLM + 开源 Embedding → 必须用模型自带 tokenizer
  • 切 chunk、算长度、构建 prompt 都要统一分词器
  • 否则会出现:本地算 512 token,模型实际 600+,直接超窗

对比 tiktoken:

  • tiktoken:闭源、固定、只适配 OpenAI
  • 开源 tokenizer:可定制、随模型走、适合本地 RAG

7 什么是感知 Chunk?现在 RAG 主流用静态还是感知?

Q: 什么叫语义感知 chunk(感知chunk)?和固定 token chunk 有什么区别?
A:

  • 静态固定 chunk:严格按 512/1024 token 切,不管语义,容易把一句话切断。
  • 语义感知 chunk(感知chunk):
    先按句号、逗号、换行、段落、标题切分,再把句子合并到接近目标 token 数。
    → 保证语义完整,不硬切句子。

但是 Overlap 的设置已经弥补了静态Chunk和语义感知Chunk之间的 gap。

Q:感知 chunk 是什么时候提出的?
A:
大约在 2022–2023 年 RAG 工业化落地阶段被广泛提出。
早期 RAG 简单粗暴用固定分块,后来发现语义断裂会大幅降低检索准确率。

Q:现在工业界主流用哪种?
A:
90% 真实 RAG 系统都用「语义感知分块」,不再用纯静态硬切。

常见策略:

  • 递归分块(先段落 → 再句子 → 再凑到目标 token)
  • 按标点/章节/结构分块
  • 保持标题和内容在同一块
  • chunk 大小在目标附近浮动,不强求完全相等

对你的实验意义:
你看到厂商日志里 token 数不固定,大概率就是用了语义感知分块,这是正常、合理、更高级的做法。

8 准确度:RAG 中为什么有的业务用 L2 不用余弦相似度?

背景:
大多数厂商 RAG 的实现一般都是用余弦相似度,因为能够将所有向量投影到同一空间上(即同一球面),解决了长度大小对语义方向造成的影响。但落地发现,L2 有时候更适合真实场景,比如,我们检索热门商品,发现商品的介绍都是很长的,而不是短短几个字,一般长的介绍文档更容易被推荐。

Q:向量检索里,余弦相似度和 L2 距离该怎么选?
A:两者关注的重点完全不同,直接影响 RAG 召回结果。

余弦相似度(最常用)

  • 只看向量方向,不看长度
  • 文本长短不影响分数
  • 适合:知识库、问答、纯语义匹配
    → 只要意思对就行,不管内容详不详细

L2 距离(欧氏距离)

  • 同时看方向 + 向量长度(模长)
  • 文本越长、信息越丰富 → 向量模长通常越大
  • L2 会天然偏向长文本、内容详细的文本

Q:为什么商品/评价类 RAG 喜欢用 L2?
A:因为这类业务里:

  • 热度高、质量高的评价/商品 → 内容更长、描述更详细
  • 简单一句“不错”的短评价 → 模长小
  • 用 L2 能自动把更详细、更有价值的内容排前面

RAG 选型总结:

  • 问答/知识库 → 余弦相似度
  • 商品/评论/内容质量排序 → L2 更贴合业务
  • 你观察到的“用 L2 更适合详细内容”是业界标准结论

整体极简总结

  1. OpenAI 模型对应固定分词,tiktoken 是官方闭源实现,RAG 中用于精准分块。
  2. 开源模型必须用自带 tokenizer,保证计数一致。
  3. 语义感知 chunk 是现在 RAG 主流,按语义边界切分,不再硬切固定 token。
  4. 余弦相似度重语义,L2 重内容长度/质量,商品/评价类 RAG 常用 L2。

9 准确度:ChunkSize 大小

https://fairy-study.blog.csdn.net/article/details/158778345?spm=1001.2014.3001.5502

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐