Files
zhuangang/topics/ai/rag/03-generation-eval.md
T

110 lines
5.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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、拒答怎么做、线上效果怎么监控。