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

5.3 KiB
Raw Blame History

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
rag
提示词
引用
拒答
幻觉
Recall@k
MRR
评测集
intermediate
ai.rag.hybrid-rerank
3
ai.rag.scenario.permission-aware-kb
ai.prompt-engineering.index
ai.agent-ops.index
ai.rag.interview
6
3

生成与评测:带引用的提示词、拒答、检索评测和答案评测

解释说明

提示词的关键要素

  1. 角色和任务:“你是公司制度问答助手”;
  2. 只依据资料回答,资料中没有就明确说没有(减少幻觉);
  3. 资料编号 + 要求用 [编号] 标注依据(可追溯、可校验);
  4. 输出格式和长度要求;
  5. 资料放在问题前面,并用清晰的分隔符包起来。

拒答:检索结果的最高相似度低于阈值、或者重排分数太低时,不调用大模型,直接回答“知识库中没有找到相关内容”,并引导到人工渠道。宁可不答,不要编。阈值需要用评测数据调。

引用校验:检查答案中的 [编号] 是否都在给出的资料范围内;可以进一步检查被引用的资料里是否包含答案中的关键数字。

评测——没有评测就无法判断改动是变好还是变坏:

层面 指标 怎么做
检索 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、拒答怎么做、线上效果怎么监控。