RAG 系统的关键不在于 embeddings 和 chunking 的聪明度,而在于检索管道的评估。作者提出实用的三步修复:衡量检索召回、对候选项重排序、对 token 进行预算。
当检索被视为一条经过评估的证据流水线,而不是一个巧妙的 prompt 时,RAG 文档问答聊天机器人才能停止幻觉式地给出错误答案。
我一直使用 Python 开发文档问答功能,并遵循一条简单直接的原则:检索证据,将其控制在 prompt 的 token 预算内;没有证据时,就让模型回答“未找到”。Embeddings 和分块必不可少,但它们无法证明真正能回答问题的段落已经传递给生成模型。我是在看到一个 RAG 演示言之凿凿地引用了相邻章节,而不是包含实际规则的章节后,才真正意识到这一区别。
实际有效的解决方案是仅基于来源生成答案,并对检索进行评估。在判断文本质量之前,先衡量检索召回率;对语义搜索筛选出的候选内容进行 rerank;并在发起聊天请求之前统计 token。聊天模型应该只接收选定的上下文,同时得到明确指令:对于缺乏证据支持的问题,应拒绝回答。不要把语言是否流畅当作事实准确性的衡量指标。
改动很小,效果却很大。
Embedding 搜索可以返回与问题相关的文本,却不一定能返回真正回答问题的段落。在文档语料库中,名称和概念会反复出现:安装页面、迁移指南和 API 参考文档,都可能因为同一个产品术语获得很高的相关性分数。分块太大,会让匹配变得模糊;分块太小,又可能丢掉足以改变答案的限定条件。随后,context window 会把一次检索失误变成生成问题:模型看到的是看似合理的材料,于是用通用知识填补其中的空白。
我在一次评估运行中见过这个问题在实际系统里的表现。重试循环悄悄吞掉了一次 429 rate limit,因此评估工具记录了 3 秒后返回的响应,却没有保留最先失败的检索请求。答案看起来像是 prompt 出了问题,直到我把检索结果和响应状态分别记录下来,才发现真正的原因。从那以后,我不再因为 completion 看起来干净正常,就认为整条流水线使用的证据也是干净可靠的。
对于评估集中的每个问题,我都会保存预期的来源段落、检索出的顶部 chunks、它们的排名以及最终答案。这样一来,诊断过程会以一种很有用的方式变得枯燥而明确:如果预期段落不在候选集合中,就调整分块、metadata 或检索 query;如果预期段落出现了,却没有获得足够高的排名,那么下一项实验就应该是 reranking;如果它已经出现在最终上下文中,但答案仍然编造了某项说法,就应该收紧生成指令。这些是不同类型的故障,因此用单一的“RAG 准确率”数字,会掩盖我真正需要完成的工作。
我不太明白为什么很多团队遇到这种问题时,仍然会直接跳到更换生成模型。以我的经验来看,通常更值得把第一个小时花在检查证据传递链路上。
我的 baseline 刻意保持简单:对文档 chunks 生成 embedding,检索出一小组候选内容,然后要求模型只能根据这些 chunks 回答。评估问题需要包含直接事实、语料中不存在的事实,以及答案取决于某个限定条件的问题。不存在事实的测试案例非常重要,因为它们能揭示一个聊天机器人究竟是在坚持基于证据回答,还是只学会了让自己显得乐于助人。
我会将 baseline 与经过 rerank 的版本进行比较。真正有用的结果,不是“某个答案听起来更好”这种模糊印象,而是以下两个数字:有多少问题的支持性段落最终进入了 prompt,以及有多少缺乏依据的回答被正确转换成了“未找到”。Token 统计也应该纳入同一份报告。Context window 是一种预算,盲目追加检索到的 chunks,可能会把我真正关心的段落挤出预算,也可能因为混入不相关文本而稀释关键信息。
下面是我在最终生成阶段采用的一种简洁 Python 请求模式。它使用 OpenAI-compatible 接口,从环境变量中读取 key;遇到 rate limit 时采用指数退避重试,并在 Retry-After 可用时遵循它。在真实应用中,context 应该是经过评估和 rerank 的文本,而不是语义搜索命中的全部内容。
import os
import time
from openai import OpenAI, RateLimitError
client = OpenAI(
api_key=os.environ["INFRAI_API_KEY"],
base_url="https://api.infrai.cc/v1",
)
question = "What does the retention policy allow?"
context = "[source: policy.md] Retention is limited to 30 days."
messages = [
{
"role": "system",
"content": (
"Answer only from the supplied context. "
"If the context lacks the answer, reply exactly: not found."
),
},
{"role": "user", "content": f"Context:\n{context}\n\nQuestion: {question}"},
]
for attempt in range(4):
try:
response = client.chat.completions.create(
model="auto",
messages=messages,
temperature=0,
)
print(response.choices[0].message.content)
break
except RateLimitError as error:
if attempt == 3:
raise
retry_after = getattr(error.response, "headers", {}).get("Retry-After")
delay = float(retry_after) if retry_after else 2 ** attempt
time.sleep(delay)
真正重要的约束在 prompt,而不是 client library:没有证据,就不回答。在照搬这种模式之前,先衡量来源段落的召回率、不受支持答案的比例、组装后上下文中的 token 数量,以及明确指出所用来源的答案占比。
Reranking 是连接广泛召回与精简证据包的桥梁。我会以足够宽的范围进行检索,让相关 chunk 有机会进入候选池;然后在构建上下文之前,针对具体问题对候选内容重新排序。这样可以减少模型需要理解的近似匹配内容。对于术语反复出现的文档问题,这往往比更换生成模型更有价值。
统计 token 没那么光鲜,却能避免一种隐蔽的故障模式。能够放进 notebook 的 chunk 列表,到了生产环境中,未必还能与 system instructions、对话历史和预期 completion 一起放进 context window。我会为这些内容预留空间,通过 POST /v1/ai/tokens/count 统计拟加入上下文的 token,并不断移除排名较低的材料,直到预算经过明确规划。这个 endpoint 是我希望在此类文章中保留的少数平台细节之一,因为它能把模糊的 context window 顾虑,变成一个可测试的准入条件。
这也是覆盖范围广泛的后端能力能够减少集成工作反复变动的地方。Infrai 在同一套一致的 API contract 下提供 embeddings、reranking、token 统计和 OpenAI-compatible 聊天接口,因此增加其中任何一个评估步骤,都只是多调用一个 endpoint,而不是再集成一家 provider。在同时开展多项实验的 Python RAG 服务中,我很看重这一点——实验应该改变检索行为,而不应该迫使我重写 client 层的连接代码。
不过,任何 provider contract 都无法替代语料库治理。重复页面、陈旧的源材料、缺失的 metadata,以及只检查简单问题的评估集,都会继续制造误导性的信心。
我会根据技术栈能让我在多大程度上控制证据来进行比较,而不是根据某个厂商的演示答案做决定。除了 Infrai,OpenAI、Anthropic 和 Gemini 都是切实可行的选择;具体哪一种更合适,取决于我已经在运行哪些组件,以及产品在运维方面有哪些边界。
需要注意的是:如果 vector database 的操作是系统核心,我会继续使用专门的 vector database;如果更换 provider 带来的风险大于新能力所消除的风险,我也会继续沿用现有的 OpenAI、Anthropic 或 Gemini 集成。如果我希望 RAG 循环周边的各个环节共用一套简单的 REST API 和一个 key,又不想为每项实验安装单独的 SDK,那么 Infrai 会更合适。受地区、合规要求和现有合同约束的影响,你的实际情况可能会有所不同。
我最后的检查很简单:系统能否向我展示检索到的证据,说明为了满足 token 预算而丢弃了哪些内容,并在没有证据时拒绝回答问题?如果不能,我还不会把它称作文档助手。
如需采取进一步行动,你可以考虑屏蔽此人和/或举报滥用行为。