--- id: ai.rag.generation-eval title: 生成与评测:带引用的提示词、拒答、检索评测和答案评测 type: lesson domain: ai stack: [rag] tags: [提示词, 引用, 拒答, 幻觉, Recall@k, MRR, 评测集] level: intermediate prerequisites: [ai.rag.hybrid-rerank] est_hours: 3 related: [ai.rag.scenario.permission-aware-kb, ai.prompt-engineering.index, ai.agent-ops.index, ai.rag.interview] week: [6] order: 3 --- # 生成与评测:带引用的提示词、拒答、检索评测和答案评测 ## 解释说明 **提示词的关键要素** 1. 角色和任务:“你是公司制度问答助手”; 2. **只依据资料回答**,资料中没有就明确说没有(减少幻觉); 3. 资料编号 + 要求用 `[编号]` 标注依据(可追溯、可校验); 4. 输出格式和长度要求; 5. 资料放在问题前面,并用清晰的分隔符包起来。 **拒答**:检索结果的最高相似度低于阈值、或者重排分数太低时,不调用大模型,直接回答“知识库中没有找到相关内容”,并引导到人工渠道。宁可不答,不要编。阈值需要用评测数据调。 **引用校验**:检查答案中的 `[编号]` 是否都在给出的资料范围内;可以进一步检查被引用的资料里是否包含答案中的关键数字。 **评测**——没有评测就无法判断改动是变好还是变坏: | 层面 | 指标 | 怎么做 | | --- | --- | --- | | 检索 | Recall@k(正确资料是否在前 k 个里)、MRR(正确资料的排名倒数的平均值) | 人工标注“问题 → 应命中的文档 / 块”,几十到几百条 | | 生成 | 忠实度(答案是否都有资料支撑)、相关性、正确性 | 人工抽查 + 用大模型按评分标准打分(LLM-as-judge) | | 线上 | 点赞 / 点踩、转人工率、无答案率 | 埋点、定期分析差评样本 | 评测集要覆盖:常见问题、需要精确匹配的问题(编号、数字)、多轮追问、**知识库里没有答案的问题**(测试拒答)、无权限的问题。 工具:Ragas 等框架提供了忠实度、上下文召回等指标的实现,接口变化较快,以官方文档为准(https://docs.ragas.io/)。 ## 案例 制度问答助手上线前建立了 9 条以上的检索评测集([examples/mini-rag](examples/mini-rag/) 中的 `EVAL`,真实项目要几十到上百条),比较纯向量、纯 BM25、混合三种检索的 Recall@3 和 MRR;之后每次调整切分或检索参数都跑一遍,指标下降就不合并。 ## 代码示例 检索评测(Recall@k、MRR): ```python def recall_and_mrr(results: dict[str, list[str]], expected: dict[str, str], k: int = 3) -> tuple[float, float]: """results:问题 → 检索出的文档 ID(已排序);expected:问题 → 应命中的文档 ID。""" hit, rr = 0, 0.0 for q, gold in expected.items(): top = results[q][:k] if gold in top: hit += 1 rr += 1 / (top.index(gold) + 1) n = len(expected) return hit / n, rr / n results = {"年假几天": ["hr-leave", "fin"], "VPN 连不上": ["hr-leave", "it-vpn"], "报销时限": ["it-vpn", "x", "y"]} expected = {"年假几天": "hr-leave", "VPN 连不上": "it-vpn", "报销时限": "fin"} print(recall_and_mrr(results, expected)) # (0.666..., 0.5):第 1 题排第 1,第 2 题排第 2,第 3 题没命中 ``` 带引用的提示词和拒答、引用校验: ```python import re def build_prompt(question: str, chunks: list[str]) -> str: context = "\n".join(f"[{i}] {c}" for i, c in enumerate(chunks, start=1)) return ("你是公司制度问答助手。只根据<资料>回答;资料中没有的内容,回答“资料中没有相关规定”。" "每个结论后用 [编号] 标注依据。\n" f"<资料>\n{context}\n\n问题:{question}") def answer_or_refuse(question: str, hits: list[tuple[str, float]], min_score: float = 0.3) -> str: if not hits or hits[0][1] < min_score: return "知识库中没有找到相关内容,建议咨询 HR 或提交工单。" return build_prompt(question, [text for text, _ in hits]) # 交给大模型 def citations_valid(answer: str, n_chunks: int) -> bool: cited = {int(x) for x in re.findall(r"\[(\d+)\]", answer)} return bool(cited) and cited <= set(range(1, n_chunks + 1)) print(answer_or_refuse("股票期权怎么行权", [("差旅住宿标准……", 0.12)])) print(citations_valid("满十年年假 10 天 [1]。", 3), citations_valid("答案 [5]", 3)) # True False ``` ## 易错点 - **没有评测集,靠几个问题“感觉效果不错”**:改了参数也不知道变好还是变坏。 - **评测集里没有“无答案”的问题**:拒答能力从未被测试。 - **拒答阈值拍脑袋**:要用评测数据画出“阈值 vs 准确率 / 拒答率”来选。 - **完全依赖 LLM 打分**:评分模型本身有偏差,要和人工标注对比校准。 - **提示词里资料和指令混在一起**:资料里的内容可能被模型当成指令(提示词注入),用分隔符包裹并在指令中说明。 - **引用编号不校验**:模型可能引用不存在的编号,或者引用了但内容对不上。 ## 面试常问 见 [RAG 面试题](interview.md):怎么减少幻觉、怎么评测 RAG、拒答怎么做、线上效果怎么监控。