5.3 KiB
5.3 KiB
id, title, type, domain, stack, tags, level, prerequisites, est_hours, related, week, order
| id | title | type | domain | stack | tags | level | prerequisites | est_hours | related | week | order | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| ai.rag.generation-eval | 生成与评测:带引用的提示词、拒答、检索评测和答案评测 | lesson | ai |
|
|
intermediate |
|
3 |
|
|
3 |
生成与评测:带引用的提示词、拒答、检索评测和答案评测
解释说明
提示词的关键要素
- 角色和任务:“你是公司制度问答助手”;
- 只依据资料回答,资料中没有就明确说没有(减少幻觉);
- 资料编号 + 要求用
[编号]标注依据(可追溯、可校验); - 输出格式和长度要求;
- 资料放在问题前面,并用清晰的分隔符包起来。
拒答:检索结果的最高相似度低于阈值、或者重排分数太低时,不调用大模型,直接回答“知识库中没有找到相关内容”,并引导到人工渠道。宁可不答,不要编。阈值需要用评测数据调。
引用校验:检查答案中的 [编号] 是否都在给出的资料范围内;可以进一步检查被引用的资料里是否包含答案中的关键数字。
评测——没有评测就无法判断改动是变好还是变坏:
| 层面 | 指标 | 怎么做 |
|---|---|---|
| 检索 | Recall@k(正确资料是否在前 k 个里)、MRR(正确资料的排名倒数的平均值) | 人工标注“问题 → 应命中的文档 / 块”,几十到几百条 |
| 生成 | 忠实度(答案是否都有资料支撑)、相关性、正确性 | 人工抽查 + 用大模型按评分标准打分(LLM-as-judge) |
| 线上 | 点赞 / 点踩、转人工率、无答案率 | 埋点、定期分析差评样本 |
评测集要覆盖:常见问题、需要精确匹配的问题(编号、数字)、多轮追问、知识库里没有答案的问题(测试拒答)、无权限的问题。
工具:Ragas 等框架提供了忠实度、上下文召回等指标的实现,接口变化较快,以官方文档为准(https://docs.ragas.io/)。
案例
制度问答助手上线前建立了 9 条以上的检索评测集(examples/mini-rag 中的 EVAL,真实项目要几十到上百条),比较纯向量、纯 BM25、混合三种检索的 Recall@3 和 MRR;之后每次调整切分或检索参数都跑一遍,指标下降就不合并。
代码示例
检索评测(Recall@k、MRR):
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 题没命中
带引用的提示词和拒答、引用校验:
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 面试题:怎么减少幻觉、怎么评测 RAG、拒答怎么做、线上效果怎么监控。