← 返回资讯

RAG 怎么控制 token 成本:检索精度与上下文长度的平衡之道

2026-07-15

RAG(检索增强生成)是目前最主流的大模型落地架构之一,但很多团队在上线后发现成本远超预期。原因在于 RAG 的成本不只是”生成”的那一次 API 调用,检索阶段的 Embedding 调用和注入上下文的长度同样是重要成本来源。

我见过一个真实案例:某团队的客服 RAG 系统上线两周后,账单从预估的每天几十块涨到了小四位数,运维半夜被告警吵醒还以为是被刷单了。排查了两天才发现,问题根本不在生成模型本身——他们图省事把 top-k 设成了 20,每个文档块又切得很大(接近 1,000 token 一块),一次问答光是塞进 prompt 的检索结果就有近 2 万 token,其中真正跟用户问题相关的内容可能不到十分之一,剩下的全是陪跑的噪声,而这些噪声一样要按 token 计费。这也是为什么很多团队一上 RAG,成本曲线就会跟着检索配置一起失控:生成模型只是照单全收,账单涨在了你根本没盯着看的检索环节。

RAG 系统的三层成本

阶段产生成本的操作频率
索引阶段文档 Embedding(离线批量)一次性 + 增量更新
检索阶段查询 Embedding(在线)每次用户请求 × 1
生成阶段检索到的文档块作为 context 注入 LLM每次用户请求 × top-k

其中生成阶段的注入上下文通常是最大的成本来源:如果每次检索返回 top-5 个 512-token 的文档块,每次请求就多出约 2,500 token 的输入。这笔账放大看更直观:如果你的应用日均有 5 万次问答请求,光这一项,一天就要多吃掉 1.25 亿 token 的输入量——这还没算生成模型自己产出回答要花的 token。很多团队盯着”生成模型单价贵不贵”去比价,却漏算了这部分隐藏在检索配置里的输入量,等账单出来才后知后觉。

控制 Embedding 成本

索引阶段:批量 + 一次性

  • 文档 Embedding 属于一次性离线任务,用 Batch API 处理可享约 50% 折扣
  • Embedding 模型单价远低于生成模型(通常低 10–100 倍),不是成本重点
  • 只对更新的文档做增量 Embedding,避免全量重算

Batch API 便宜的原因也值得说一句:它把你的请求丢进一个非实时的处理队列,服务商可以在算力空闲的时段统一调度、攒批处理,所以能给折扣,代价是通常要等到一个完成窗口(常见是提交后 24 小时内完成,具体以官方文档为准)才能拿到结果。这就决定了它只适合离线的索引构建、全量重建这类不着急的任务——如果你的业务是”用户上传文档后马上要能被检索到”,Batch API 的延迟会直接拖垮体验,这种场景老老实实用同步 Embedding 接口,成本上多花点但换来即时可用。

检索阶段:查询 Embedding 复用

from functools import lru_cache

@lru_cache(maxsize=1000)  # 缓存最近 1000 个查询的 Embedding
def get_query_embedding(query: str) -> list[float]:
    return embedding_client.embed(query)

同一用户在同一会话中重复问类似问题时,缓存命中率可达 30–50%。

这段代码看着简单,上生产前有两个坑得先踩明白:

  1. lru_cache 是进程内存缓存,只在当前 Python 进程里生效。如果你的服务是用 Gunicorn/Uvicorn 起了 4 个 worker 进程,实际上是 4 份互不相通的缓存各自为战,真实命中率会比单进程测试时低不少。想要跨进程共享,得换成 Redis 之类的外部缓存,用归一化后的查询文本(去掉首尾空格、统一大小写、去掉标点差异)做 key,否则”什么是RAG”和”什么是 RAG?“会被当成两个不同的查询,缓存直接失效。
  2. 缓存没有过期机制,maxsize=1000 满了之后会按最近最少使用(LRU)规则淘汰旧条目,这对查询场景一般没问题,但如果你的文档库频繁更新、同一个查询在不同时间点应该检索到不同内容,就不能无脑缓存查询 Embedding——这时候该加的是缓存失效逻辑,而不是任由旧结果一直命中。

控制生成阶段的上下文成本

这是 RAG 成本优化的重点。

策略一:精确控制 top-k

不要默认用 top-5 或 top-10,找到你的最小有效 k:

# 从 top-3 开始测试,逐步增加,观察回答质量何时不再提升
for k in [1, 2, 3, 5, 8]:
    results = vector_db.search(query, top_k=k)
    answer = llm.generate(context=results, question=query)
    quality_score = evaluate(answer)  # 用你的评估逻辑
    print(f"k={k}: 质量={quality_score}, context_tokens={count_tokens(results)}")

多数场景下 top-3 到 top-5 已经足够,超过之后质量边际提升极小,但成本线性增加。

注意这段循环是一次性的线下标定脚本,不是拿到生产环境里跑的东西——它对同一个 query 调用了 5 次 llm.generate(),本身就是一笔不小的测试成本,只应该在你搭建评估集、确定生产环境最终 k 值的阶段跑一次。evaluate() 这个函数怎么实现也有讲究:如果你手头有人工标注过”标准答案”的 golden set,可以用字符串匹配或者语义相似度打分;如果没有,很多团队会退而求其次用”LLM 当裁判”(把生成答案和参考答案一起丢给另一个模型打分),但这个裁判本身也要花 token,评估阶段的成本记得单独算,不要和线上成本混在一起看。

策略二:相关性分数过滤

不仅限制数量,还过滤质量差的结果:

results = vector_db.search(query, top_k=10)
# 只保留相关性分数高于阈值的结果
filtered = [r for r in results if r.score > 0.75]
# 最多保留 top-3,避免过滤后仍然太多
context_chunks = filtered[:3]

0.75 这个阈值不是万能常数,千万别从某篇博客抄来直接用在自己项目上。不同 Embedding 模型算出来的相似度分数分布差异很大——有的模型分数普遍集中在 0.60.9,有的模型同样相关的内容却只能打到 0.40.5,阈值必须针对你实际用的 Embedding 模型重新标定,方法很简单:拿几十条已知”相关”和”不相关”的问答对,看看两类分数分别落在什么区间,取中间值。

还有一个更容易被忽略的坑:如果 filtered 过滤完是空的怎么办?很多实现直接把空列表传给 llm.generate(),模型拿不到任何 context,但它不会老实说”我不知道”,而是会开始凭训练数据里的记忆编答案——这就是 RAG 场景下典型的幻觉来源,而且比不用 RAG 更隐蔽,因为团队会误以为”检索到的内容”支撑了这个回答。生产环境一定要加保底逻辑:过滤后为空就适当放宽阈值重试一次,或者直接让系统回答”没有找到相关内容”,而不是让模型硬编。

策略三:分块大小优化

分块策略token 消耗适用场景
大块(1,024 token)高,但单次检索就够内容连贯性强的文档
小块(256 token)低,但可能需要更多块FAQ、结构化条目
父子分块中等,精准检索后扩展上下文通用推荐方案

父子分块方案: 用小块做检索(高精度),命中后取其父块(完整上下文)注入 LLM,在精度和 token 消耗之间取得平衡。

实现上这个方案容易被低估工作量。它不是简单调个参数就完事,你需要:切分文档时用递归字符切分器先切出较大的父块,再对每个父块二次切分成检索用的子块,并给子块打上 parent_id 元数据一起存进向量库;命中子块之后,拿这个 parent_id 去另一个存储(一张 SQLite 表或者一个 KV 存储都行)里查出父块的完整文本再注入 LLM。也就是说向量库本身通常只适合存”用来检索”的小块,完整的父块内容得单独维护一份文档存储——这是很多团队第一次实现父子分块时漏掉的一环,误以为向量库能顺带存好所有层级的内容。

策略四:重排序(Reranking)后再截断

用轻量 Reranker 模型对检索结果重新打分,只取重排后前 N 名:

raw_results = vector_db.search(query, top_k=20)   # 粗检索多取一些
reranked = reranker.rerank(query, raw_results)      # 重排,更准确
final_context = reranked[:3]                         # 只注入前 3 名

Reranker 的成本极低(通常是本地模型或极便宜的 API),但能显著提高检索精度,让每个注入的块都真正有用。

本地模型和 API 两种部署方式怎么选,看你团队的实际情况:

方案延迟成本部署门槛适合谁
本地跑开源 cross-encoder低(毫秒级,同机房)只有算力成本,无调用费需要自己管模型和推理服务有 GPU/CPU 资源、请求量大到 API 调用费划不来的团队
调用托管 Reranker API中(含网络往返)按调用量计费,通常很便宜几乎零门槛,接个接口就行快速验证效果、请求量不大、不想自建推理服务的团队

用 API 版本的话有一点要提前防:Reranker 调用也是你请求链路里的一环,如果它本身触发了服务商的限速返回 429,你的整个问答请求也会跟着卡住甚至失败。生产环境里给这类内部调用也配上超时和重试退避,不要因为”它只是个辅助步骤”就掉以轻心。

策略五:上下文压缩

不注入整个文档块,而是先提取与问题相关的句子:

def extract_relevant_sentences(chunk: str, question: str, llm) -> str:
    prompt = f"从以下文本中提取与问题最相关的 1-3 句话。\n问题:{question}\n文本:{chunk}"
    return llm.generate(prompt, model="gpt-4o-mini", max_tokens=100)  # 用轻量模型

# 对每个检索到的块先压缩,再注入
compressed_context = [extract_relevant_sentences(c, query, llm) for c in raw_chunks]

这一步本身是要花钱的:每个 chunk 都单独调一次 LLM,哪怕用的是便宜的轻量模型,请求数量和延迟也会跟着涨上去——如果你的 top-k 是 3,这里就凭空多了 3 次串行(或并行)的额外调用。什么时候值得用这招给出个简单判断:只有当原始 chunk 明显偏大(比如超过 400~500 token)、且压缩后能省下的 token 明显多于这次额外调用花掉的 token 时才划算;如果你的分块本来就控制在 200 token 左右,这一步的调用成本很可能反而超过它省下来的部分,属于典型的”为了优化而优化”,这种情况不如把预算花在把 top-k 调低或者上 Reranker 上。

典型优化效果对比

优化配置每次请求 context token(估算)
默认 top-10 × 512 token 块~5,120 token
优化后 top-3 × 256 token 块 + 相关性过滤~600–800 token
父子分块 + Reranker top-3~768 token(更准确)

成本可降至默认配置的 15–20%,且回答质量通常不下降甚至提升(因为噪声更少)。

进阶:批量导入时的并发与限流

前面说的都是单次问答的成本优化,但如果你要做的是批量导入历史文档(比如一次性把 10 万篇文章都建好索引),会遇到另一类完全不同的坑。

真实现象是这样的:批量导入跑起来前几百个请求都很正常,但从某个时间点开始,日志里持续报错,embedding_client.embed() 抛出 RateLimitError,HTTP 状态码是 429 Too Many Requests。根因很直接——你为了赶进度开了很高的并发(比如用多线程或者 asyncio.gather 一次性发出几百个请求),超过了 Embedding 服务商对你的账号设定的 TPM(每分钟 token 数)或 RPM(每分钟请求数)限制,服务商用 429 把你挡回来了。

修法也不复杂,分两步:一是加一个令牌桶或者信号量控制并发数,别让并发无限往上冲;二是给每次请求配上指数退避重试——第一次失败等 1 秒,连续失败就翻倍等待(2 秒、4 秒、8 秒……),设一个上限(比如最多等到 30 秒),超过重试次数上限再放弃并记录下来人工处理。这套组合拳做好之后,批量导入 10 万篇文档从”跑到一半就报错卡死”变成”稳定跑完,只是慢一点”,本质上是用时间换稳定性,而不是死磕更高的并发数硬冲限速红线。

另外,索引阶段的并发优化和线上问答阶段是两回事,不要混为一谈:批量导入追求的是”在限速范围内尽快跑完”,而线上问答请求追求的是”单次响应尽量快”,两者的限流策略和重试策略应该分开配置,用同一套参数很容易顾此失彼。

常见问题

减少 top-k 会导致”检索不到”吗?
如果检索质量好(Embedding 模型合适、分块合理),top-3 命中率通常已经足够。可以通过评估集(golden set)测量 Recall@k,确认降低 k 后召回率是否可接受。

Embedding 模型本身影响成本吗?
影响不大,Embedding API 单价很低。但 Embedding 质量直接影响检索精度,进而影响需要多少 top-k 才能找到答案——选好 Embedding 模型反而间接降低生成成本。

RAG 成本和纯长上下文方案哪个便宜?
多数场景下 RAG 更便宜:把 100 万 token 的文档库全部塞入长上下文模型费用极高,RAG 只取相关的几百 token 注入。但对于需要全局推理的任务(如”分析整本书的主题”),长上下文有其不可替代性。

Reranker 一定要加吗,能不能跳过这一步直接上生产?
小规模、文档库结构简单(比如都是短 FAQ)的场景可以先跳过,直接用向量检索的相似度排序,把 top-k 控制小一点就够用。但如果你的文档库量级上到几万篇以上、内容领域交叉多,向量检索本身的排序质量会明显下降,这时候 Reranker 带来的精度提升往往能让你把 top-k 从 8~10 压到 3,成本和质量是同时改善的,不是纯粹的额外开销。建议先跑一版没有 Reranker 的基线,测出质量分和成本,再加上 Reranker 对比一次,用数据决定要不要留着它,而不是凭感觉。

批量导入时具体该怎么设并发数?
没有放之四海而皆准的数字,因为每个账号的限速额度不同(新账号、低等级账号的限速通常比用了一段时间、消费达标后自动升级的账号低不少,具体额度以服务商后台面板为准)。实操上建议从一个保守的并发数(比如 5~10)开始跑,观察是否触发 429,如果全程没有报错再逐步往上调,找到你账号当前额度下的稳定并发上限,而不是一上来就猜一个很大的数字硬冲。


延伸阅读: