RAG(知识库问答)最难受的不是“模型答错”,而是:
资料明明有,但模型总答非所问。
我先给一个工程结论:
RAG 失败 80% 在检索链路,不在生成。
所以这篇不讲玄学 prompt,直接给你一套“工程排错打法”:
- 固定失败样本
- 打印 top-k(看证据是否命中)
- 用 hit@k/cite_acc/ans_acc 把迭代量化
- 给一个最小评测脚手架(能跑起来就行)
0)TL;DR
- 先评检索,再评生成:hit@k 不行别调 prompt
- 打印 top-k 是第一生产力:没有 top-k 你只能猜
- 一次只改一个变量:关重排/调 k/换分块/改提示词,别全改
1)RAG 链路图(你要知道自己在调哪一段)
docs -> clean -> chunk -> embed -> index
-> retrieve(top-k) -> rerank(optional) -> build_context
-> prompt_guardrails -> generate -> citations/postprocess
2)最常见翻车点(按优先级)
2.1 top-k 没命中正确证据(hit@k 低)
优先检查:
- 文档是否真的入库?索引是否更新?
- chunk 是否切断了关键句?(太碎/边界断)
- top_k 是否太小?(先试 5→10→20)
- filter 是否误伤?(版本/部门/权限)
2.2 命中了但答错(context 污染/提示词带跑)
优先检查:
- context 是否太长、噪音太多?
- 资料顺序是否合理?(定义→例外→边界)
- 是否缺少“只基于资料/资料不足就拒答”的强约束?
2.3 引用错(看起来像,但不是证据)
优先检查:
- 是否需要 rerank(召回多但排序乱时)
- 引用格式是否强约束(必须引用片段编号)
- 是否要做引用校验(引用片段是否包含关键事实)
3)排错流程(照着做,最快定位)
Step 1:固定失败样本(四字段)
- query
- expected_keypoints(期望关键点)
- expected_source_ids(正确证据段落 id)
- observed(线上输出)
Step 2:打印 top-k(必须做)
看两件事:
- top-k 里有没有 expected_source_ids
- 如果有,排第几,上方噪音多不多
Step 3:消融实验(一次只改一个变量)
建议顺序:
- 关 rerank
- 调 top_k
- 调 chunk_size/overlap
- 调 prompt_guardrails
4)最小指标体系(让迭代可量化)
hit@k(检索命中率)
top-k 里是否命中正确证据段落。
这个指标低,你就别纠结生成。
cite_acc(引用正确率)
答案引用的片段是否真的支持结论(制度/合同类尤其重要)。
ans_acc(答案正确率)
关键点是否覆盖且无严重错误。
5)最小评测脚手架(Python)
5.1 评测集格式(jsonl)
{"id":"q001","query":"退款规则是什么?","expected_keypoints":["7天无理由","特殊商品除外"],"expected_source_ids":["refund_v2025#p12","refund_v2025#p13"]}
5.2 检索评测:hit@k
import json
from typing import List
def load_jsonl(path: str):
with open(path, "r", encoding="utf-8") as f:
for line in f:
yield json.loads(line)
def hit_at_k(retrieved_ids: List[str], expected_ids: List[str], k: int) -> int:
topk = set(retrieved_ids[:k])
return 1 if any(eid in topk for eid in expected_ids) else 0
def evaluate_retrieval(cases_path: str, retrieve):
ks = [3, 5, 10]
totals = {k: 0 for k in ks}
hits = {k: 0 for k in ks}
for c in load_jsonl(cases_path):
results = retrieve(c["query"]) # -> [{"id": "...", "text": "..."}]
retrieved_ids = [r["id"] for r in results]
for k in ks:
hits[k] += hit_at_k(retrieved_ids, c["expected_source_ids"], k)
totals[k] += 1
return {f"hit@{k}": hits[k] / max(1, totals[k]) for k in ks}
5.3 打印 top-k(排错必备)
def debug_topk(query: str, retrieve, k: int = 5):
results = retrieve(query)[:k]
for i, r in enumerate(results, 1):
print(f"#{i} id={r['id']}")
print(r["text"][:400])
print("-" * 40)
6)默认参数起点(先从可用开始)
| 参数 | 起点 | 什么时候调 |
|---|---|---|
| chunk_size | 300–800 字/Token 等价 | 缺上下文→调大;噪音大→调小 |
| overlap | 10%–20% | 关键句被切断→调大 |
| top_k | 5–10 | 找不到资料→调大;噪音污染→调小 |
| rerank | 可选 | 召回多但排序乱→开;成本敏感→先关 |
7)工程化建议(上线前一定要补)
- 版本化:文档/向量/embedding/prompt/rerank 都要可追溯
- 可观测:request_id、retrieved_ids、latency、token、retries
- 兜底:资料不足就拒答并说明缺什么;检索失败可返回资料列表/转人工
资源区
如果你要做多模型 A/B 或评测,建议先把接入层统一(OpenAI 兼容协议多数情况下只改 base_url/api_key)。
我自己用的是大模型中转平台 147ai 官网(参数以其文档与控制台为准)。