大多数 RAG 演示都会在生产环境中失败。它们在 10 个问题的演示文稿上运行良好,但用户一旦提出新问题就会崩溃。本指南介绍能够经受住实际使用的架构和模式:遵循语义边界的分块、同时捕获模糊查询和字面查询的混合检索、防止幻觉的提示词结构,以及在规模扩大时保持账单稳定的成本控制。
真正重要的五项决策
- 分块策略——对检索质量的影响大于模型
- 嵌入模型——决定召回率上限
- 检索策略——仅靠向量是不够的
- 提示词结构——决定模型是否会产生幻觉
- 生成模型 + 缓存——决定单位经济性
1. 分块——朴素胜过聪明
不要过度思考。使用每个分块 1000-1500 个字符、重叠 100-200 个字符的递归字符分割器。优先尝试按段落边界拆分,然后按句子,最后按单词拆分。它在不同领域都很稳定:
# Recursive character splitter with overlap — the boring choice that wins
from langchain_text_splitters import RecursiveCharacterTextSplitter
splitter = RecursiveCharacterTextSplitter(
chunk_size=1200,
chunk_overlap=200,
separators=["\n\n", "\n", ". ", " ", ""],
)
chunks = splitter.split_text(document)
# 1200 chars ≈ 300-400 tokens — fits 5+ chunks in a Sonnet context window
# with room for system prompt + question + answer看起来更聪明但很少真正有帮助的做法:具备句子感知能力的模型、具备 Markdown 感知能力的分割器、结合嵌入的滑动窗口。它们并不差——只是需要投入大量精力才能获得略微更好的效果。把这些精力投入检索。
2. 嵌入模型——text-embedding-3-large 是默认选择
Kunavo 不提供嵌入服务——此步骤请直接调用嵌入提供商。 /v1/embeddings 是已实现的线路格式,但其后没有启用任何模型,因此对它的请求会失败;GET /v1/models 始终是判断哪些内容可调用的权威来源。实际上,这对 RAG 管道来说除了多一把密钥外不会增加成本:嵌入调用和生成调用本来就是独立请求,因此可以使用 OpenAI、Voyage 或 Cohere 生成嵌入,并使用 Kunavo 进行生成。
text-embedding-3-large 在大多数生产环境用例中很好地平衡了多语言支持和准确性,价格以 OpenAI 自己公布的费率为准——请在其定价页面查看,而不是在此处查看,因为我们不转售该服务,也不应在此引用其价格。更改分块策略或模型本身时重新生成嵌入;内容更新时不需要重新生成(只需添加新的分块)。
适合使用较小嵌入模型的例外情况:纯英文短文本检索(FAQ、产品卡片)。对于这些场景,text-embedding-3-small便宜 4 倍,而质量损失可以忽略。对于多语言或技术内容,请继续使用 large。
3. 检索——纯向量不如混合检索
向量相似度非常擅长模糊语义匹配(“如何取消”→“订阅取消政策”)。但它非常不擅长处理字面术语(“SKU-A92837”或“订单 #4729”)。解决方案是混合检索:并行进行 BM25 关键词搜索,然后使用倒数排名融合:
# Hybrid retrieval: vector similarity + BM25 keyword scoring
# Pure vector misses literal keywords (product SKUs, IDs, dates)
def retrieve(question: str, k: int = 5) -> list[dict]:
q_embed = embed([question])[0]
vector_hits = vector_db.similarity_search(q_embed, k=k * 2)
keyword_hits = bm25_search(question, k=k * 2)
# Reciprocal rank fusion — simple, robust
scores: dict[str, float] = {}
for rank, hit in enumerate(vector_hits):
scores[hit["id"]] = scores.get(hit["id"], 0) + 1 / (rank + 60)
for rank, hit in enumerate(keyword_hits):
scores[hit["id"]] = scores.get(hit["id"], 0) + 1 / (rank + 60)
top_ids = sorted(scores, key=scores.get, reverse=True)[:k]
return [chunk_by_id[i] for i in top_ids]选定一个合格嵌入模型之后,这是最大的单项质量提升。在留出的 100 个问题评估集上跟踪 recall@5——如果加入 BM25 后从 70% 提升到 90%,就相当于消除了三分之一的“我不知道”失败。
4. 提示词结构——防止幻觉的三种模式
# Final RAG prompt structure — three sections, citations enforced
SYSTEM_PROMPT = """You answer based exclusively on the supplied Context.
- Cite the [doc_id] for each factual claim.
- If the context doesn't answer the question, say "I don't have that
information" — do not extrapolate or use general knowledge.
- Be concise. No throat-clearing."""
def answer(question: str) -> dict:
chunks = retrieve(question, k=5)
context = "\n\n---\n\n".join(
f"[doc:{c['id']}] {c['text']}" for c in chunks
)
resp = client.chat.completions.create(
model="claude-sonnet-4-6",
messages=[
{"role": "system", "content": [{
"type": "text",
"text": SYSTEM_PROMPT,
"cache_control": {"type": "ephemeral"},
}]},
{"role": "user", "content": f"# Context\n{context}\n\n# Question\n{question}"},
],
max_tokens=600,
)
return {
"text": resp.choices[0].message.content,
"sources": [c["id"] for c in chunks],
}系统提示词中有三项不可妥协的要求:
- 引用来源——让模型为每个陈述附加
[doc:42]。如果引用的 ID 不真实,就捕获到了一个幻觉 - 明确拒答——“如果上下文无法回答,就说我没有相关信息。”没有这条要求,模型会根据世界知识补全答案
- 简洁输出——答案越短,通常越准确。最多 600 个 token 是一个不错的生产默认值
5. 生成模型 + 缓存——成本层
Claude Sonnet 4.6 是默认选择。如果成本压力较大,Claude Haiku 4.5 是低价备用选项。始终将系统提示词包在 cache_control 中——系统提示词每次调用都相同,因此缓存可以将这部分成本降至输入费率的 10%。
生产规模下每次查询的实际成本(上下文 5K、输出 500、启用缓存):
| 模型 | 每次查询成本 | 每天 10,000 次查询时 | 相对于 Sonnet 的质量 |
|---|---|---|---|
claude-sonnet-4-6 | ~$0.007 | ~$70/天 | 基准 |
claude-haiku-4-5 | ~$0.001 | ~$10/天 | ~85%,便宜 4 倍 |
每天 10,000 次查询时 → Sonnet $70/天,Haiku $10/天。高风险用例(面向客户、法律、医疗)选择 Sonnet,内部或批量处理选择 Haiku。
生产环境中会出现什么问题(以及如何捕获)
- 分布漂移:训练语料不再匹配真实用户问题。每周抽样 100 个查询,手动检查 recall@5
- 嵌入过时:源文档已更新,但嵌入未刷新。每周跟踪索引大小与源数据大小
- 虚假引用:模型编造看似真实的文档 ID。渲染前验证每个
[doc:N]是否属于实际检索到的 ID 列表 - 延迟悬崖:向量数据库在超过一百万个向量后性能暴跌。使用 HNSW 索引;如果是多租户系统,则按租户分区
4 周生产路线图
- 第 1 周:使用 100 份文档和 10 个测试问题制作原型,进行人工评估
- 第 2 周:扩展到完整语料库,构建混合检索,编写 100 个问题的评估集
- 第 3 周:基于评估集迭代调优分块和系统提示词,发布给内部用户
- 第 4 周:监控、成本预算、带来源引用 UI 的公开发布
常见问题
生产环境中的 RAG 系统应使用多大的分块?
每个分块使用 1,000–1,500 个字符,重叠 100–200 个字符;使用递归字符分割器,优先按段落边界拆分,然后按句子,最后按单词拆分。具备句子感知能力的模型、具备 Markdown 感知能力的分割器和滑动窗口的效果略好,但所需工作量高得多——这些精力更值得投入到检索上。
RAG 管道应使用哪个嵌入模型?
对于多语言和技术内容,text-embedding-3-large 是默认选择;对于纯英文短文本检索,例如 FAQ 和产品卡片,text-embedding-3-small 的价格约便宜 4 倍,而质量损失可以忽略。请注意,Kunavo 不提供嵌入服务:/v1/embeddings 是已实现的数据传输格式,但其后没有启用任何模型,因此嵌入步骤直接调用 OpenAI、Voyage 或 Cohere,而生成步骤调用 Kunavo。由于两者无论如何都是独立请求,这对管道来说除了多一把密钥外不会增加成本。
仅靠向量搜索就足够完成 RAG 检索吗?
不够。向量相似度很擅长模糊语义匹配,但无法处理 SKU 或订单号等字面术语。并行运行 BM25 关键词搜索,再通过倒数排名融合进行合并,通常可以将 recall@5 从约 70% 提升到约 90%——这是选定一个合格嵌入模型之后最大的单项质量提升。
如何防止 RAG 系统产生幻觉?
在系统提示词中采用三种模式:要求每个事实性陈述都附带 [doc:N] 引用;指示模型在上下文无法回答时说明自己没有相关信息;并保持答案简短——生产环境中将输出限制为最多 600 个 token 是一个不错的默认值。然后,在渲染前验证每个被引用的 id 是否属于检索结果集;不存在的 id 就是已被捕获的幻觉。
生产环境中的一次 RAG 查询成本是多少?
在上下文 5K、输出 500 个 token 且启用提示词缓存的情况下:Claude Sonnet 4.6 每次查询约 $0.007,Claude Haiku 4.5 约 $0.001(便宜 4 倍,质量约为 85%)。每天 10,000 次查询时,Sonnet 约 $70/天,Haiku 约 $10/天。