返回 法律问答系统(llm-wiki-v1)课程讲义

评估与指标 —— 如何证明系统"够好"

所属课程:《法律问答系统(llm-wiki)》八讲课件 · 第 8/8 讲 定位:前面 7 讲把系统造出来了,但”造出来”不等于”好用”。本讲讲怎么量化证明——检索质量用 recall@k,回答质量用 ragas,对应 eval/ 目录。

本讲目标:

  1. 看懂 30 条测试用例的三种类型(single_hop / multi_hop / complex)设计意图;
  2. 理解自定义 recall@k 的算法与 k=1/3/5/8 各自的含义;
  3. 认识 ragas 五指标与 langchain 版本兼容红线;
  4. 解读实测基线,能说出系统当前的强项与薄弱项、以及改进方向。

1. 评估数据:eval/qa_test_cases.jsonl

JSONL 每行一条用例,共 30 条劳动法测试用例,覆盖试用期、竞业限制、经济补偿、加班、非全日制、劳务派遣、经济性裁员、孕期女职工保护、劳动争议处理、劳动监察等。

字段:

{
  "id": "eval_001",
  "type": "single_hop",          // single_hop(10) / multi_hop(10) / complex(10)
  "question": "…",               // 用户问题
  "answer": "…",                 // 标准答案 → 作为 ragas 的 ground_truth
  "expected_knowledge": ["KP_…"],// 期望命中的知识点 id → 用于 recall@k 比对
  "source_sections": ["…"],      // 答案来源章节
  "sub_questions": ["…"]         // complex 类型的子问题拆分
}

三种类型回答不同能力

类型数量考察什么
single_hop10检索能否一步命中对应条文“竞业限制的期限法律怎么规定?” → 命中第二十四条
multi_hop10图谱多跳能否把被引用的相关条文带进来一条引用 39 条,要带出 39/46/47 条
complex10大而泛的”责任/措施”题,需组合多法多条文 + 子问题拆分“拖欠工资怎么维权?” 牵涉劳动监察、责令支付、仲裁…

expected_knowledge 与知识图谱的 KP_xxx 一一对应(脚本逐条校验过无缺失、无重复),所以 recall@k 能精确到” 该召回的那几条有没有被召回到”。


2. 检索质量指标:自定义 recall@k

ragas 没有内置”该命中的知识点有没有被检索到”这种面向知识点的召回指标,所以自己写一个(eval/metrics.py):

def recall_at_k(retrieved_ids, expected_ids, k):
    """Top-k 检索召回率 = 期望命中的知识点里,有多少出现在 Top-k 内。"""
    if not expected_ids:
        return None
    hit = sum(1 for eid in expected_ids if eid in set(retrieved_ids[:k]))
    return hit / len(expected_ids)


def recall_report(retrieved_ids, expected_ids, ks=(1, 3, 5)):
    return {f"recall@{k}": recall_at_k(retrieved_ids, expected_ids, k) for k in ks}

为什么用 1/3/5/8 四个 k?

k含义
recall@1排第一的知识点是不是期望的那条——最严格,几乎决定能不能”一步答对”
recall@3 / @5前几名内有没有期望条文——反映检索排名质量
recall@8检索返回 5 种子 + 3 扩展 = 8 块全用给生成器,所以 @8 = 生成器真实可见上下文里有没有期望条文——是最贴近”答得对不对”的指标

一个容易忽略的点:evaluate.pykb.search(question, top_k),检索返回 ≤8 块,生成与 recall 都用这 8 块,@8 不是随便选的大数,是生成上下文的真实容量(EXPAND_K=3 + TOP_K=5)。


3. 回答质量指标:ragas 0.3.0

回答好不好,不只”检索到没有”,还要”上下文相不相关、答案忠不忠实”。用 Ragas 0.3.0 评估框架 + DashScope:

指标问的是什么
context_precision检索来的上下文里,有用的占多少(相关块排得靠前吗)
context_recall标准答案需要的信息,上下文覆盖了多少
ContextRelevance检索上下文和问题的相关程度
answer_relevancy回答是否切题、没跑偏
faithfulness回答是否忠于给定上下文、有没有凭空编造
def build_llm():
    from langchain_openai import ChatOpenAI
    from ragas.llms import LangchainLLMWrapper
    return LangchainLLMWrapper(ChatOpenAI(model=LLM_MODEL, base_url=DASHSCOPE_BASE_URL,
                                          api_key=DASHSCOPE_API_KEY, temperature=0))


def build_embeddings():
    from langchain_community.embeddings import DashScopeEmbeddings
    from ragas.embeddings import LangchainEmbeddingsWrapper
    return LangchainEmbeddingsWrapper(
        DashScopeEmbeddings(model="text-embedding-v3",
                            dashscope_api_key=DASHSCOPE_API_KEY))

⚠️ 两个必踩的坑(都在代码注释里写明了)

  1. Embedding 必须用 langchain_community.embeddings.DashScopeEmbeddingstext-embedding-v3),不要用 langchain 的 OpenAIEmbeddings 去接 DashScope——会报 input.contents is neither str nor list of str(请求格式不兼容)。
  2. 版本铁三角ragas==0.3.0 + langchain-community==0.4.0 + langchain 1.x。ragas 0.4.x 会因 vertexai 模块被移除而与 langchain-community≥0.4.2 冲突。改这三者版本前务必验证兼容。

运行评估

uv run python -m eval.evaluate                 # 全量 30 条(生成 + ragas,较慢,要 API_KEY)
uv run python -m eval.evaluate --limit 3       # 快速试跑前 3 条
uv run python -m eval.evaluate --skip-ragas    # 只算 recall@k(不调 LLM,可离线)

结果落盘 eval/results/evaluation_<时间戳>.json:recall@k 汇总 + ragas 各指标均值/最值/按类型分组 + 每用例明细。


4. 实测基线解读(2026-08-12,30 条,—skip-ragas)

指标数值解读
recall@10.40平均每条用例 Top1 就命中期望条文的比例不到一半——检索排序还有空间
recall@30.69前 3 名基本能框住约七成期望条文
recall@50.845 个种子内已框住绝大多数
recall@80.92加上图谱扩展后,生成器上下文里已包含 92% 的期望条文

再拆一层看为什么能到 0.92

  • 单跳用例:10 条全部 recall@3=1.0(其中 7 条 recall@1=1.0)——“一步定位”对这种题几乎没短板;
  • 多跳/复杂用例的依赖条文,主要靠 crossref 图谱扩展带进上下文(46→38、65→39、42→45、41→46 等),证实第 6 讲的设计是有效的。

0.40 → 0.92 的爬升曲线本身就是一张很好的”设计证据图”:单靠字面检索 top1 只能答好一半题;加上向量融合 + 图谱多跳,@8 能到九成以上——这正是混合检索与知识图谱存在的意义。


5. 已知薄弱项与改进方向(面试要会聊”不足”)

薄弱点:宽泛的”措施/责任”类问题——如 eval_026 拖欠工资维权、eval_030 维权途径、eval_025 孕期违法解除——期望条文* 未必全部落入 top-8*。成因:这类问题同时涉及两部法多个条文,检索与扩展的分数容易被”泛主题”稀释,部分期望条文掉出上下文。

缓解观察:即便如此,生成器在缺失部分条文时,仍能基于已召回的相关条文给出合法合理的回答(冒烟测试验证过)——因为 prompt 允许组合相近条文 + 如实说明。

后续方向(交接文档列了几个,供讨论):

  • 查询分解:用 LLM 把宽泛问题拆成子问题分别检索再合并(complex 用例的 sub_questions 字段已为此预留);
  • 实体链接 + 图传播:从 query 抽实体 → 映射到图谱节点 → 影响力传播召回,把”跨两法的责任/措施”题兜住;
  • 按预期条文补 crossref 或概念映射:数据层补救,让”维权/责任”等触发词命中更多相关条文;
  • 评估完善:测试集随机抽样、多版本结果对比、CI 里加评估回归。

6. 全课回顾:八讲串成一张图

第1讲  为什么不做 RAG:llm-wiki 把知识"编译"成资产
第2讲  系统总览:ingest 编译 + query 定位/综合,双阶段架构
─────── 摄入(编译知识,可读可审计)───────────────
第3讲  PDF → 205 知识点:清洗 / 康熙部首 / 章-节-条状态机
第4讲  概念增强(口语→法条) + 图谱(4类边) + Wiki 落盘
─────── 查询(只做"定位 + 综合")─────────────────
第5讲  BM25 + bge-m3 双路召回 → RRF 融合 → 5 种子
第6讲  图谱 BFS ≤3 跳扩展 → qwen-max 依据知识块综合生成
第7讲  短期记忆 / CLI / 结构化日志:能多轮、可观测、可回归
─────── 证明它够好 ─────────────────────────────
第8讲  recall@k(0.40→0.92)+ ragas:强项、薄弱、改进方向

一句话总纲:把结构规整的领域文档在摄入期”编译”成可读可审计的知识点 + 图谱(wiki + JSON),查询期用混合检索定位、图谱多跳补全、大模型综合生成,并用 recall@k 与 ragas 持续证明与改进——这就是 llm-wiki 问答系统。


企业常见面试问答

Q1:recall@1/3/5/8 各代表什么?为什么 @8 有意义?

它衡量”期望命中的知识点有没有被检索到 Top-k 内”。@1 看排第一的准不准,@3/@5 看排序质量;@8 特殊——系统检索给生成器的是 5 个种子 + 3 个图谱扩展 = 恰好 8 块,所以 recall@8 反映的是”生成器真实看到的上下文里有没有期望条文”,是最贴近答案质量的一个检索指标。基线是 0.40 / 0.69 / 0.84 / 0.92:单靠 top1 只能答对约四成,加上向量与图谱扩展到 @8 能达九成以上,恰好论证了混合检索 + 多跳扩展的设计价值。

Q2:你们的测试集是怎么设计的?三种类型分别验证什么?

30 条劳动法用例,分三类各 10 条。single_hop 验证”一步定位”——一个概念查一条条文能否命中;multi_hop 验证多跳召回——一条条文引用了另一条,扩展能否把被引条文(如 39 条带出 46/47 条)带进上下文;complex 覆盖大而泛的” 责任/措施/维权”题,需要组合多法多条文,每条例出子问题。每条都有 expected_knowledge(对应图谱 KP 号)用于 recall 比对、有标准答案用于 ragas 的 ground_truth。类型化的好处是:指标一掉,能立刻定位是单跳、多跳还是复杂题在退化。

Q3:回答质量怎么评估?ragas 在这套里有什么坑?

用 ragas 0.3.0

的五个指标:context_precision(上下文有用率)、context_recall(上下文对答案的覆盖)、ContextRelevance、answer_relevancy、faithfulness(是否忠于给定上下文、没编)。两个坑务必记住:①

embedding 接 DashScope 必须用 langchain_community.embeddings.DashScopeEmbeddings,用 OpenAIEmbeddings 会因请求格式不兼容报 input.contents 错误;② 版本铁三角 ragas==0.3.0 + langchain-community==0.4.0 + langchain 1.x,ragas 0.4.x 与 community≥0.4.2 因 vertexai 移除而冲突。

Q4:现在系统最弱的地方在哪?如果让你改进,先做哪个?

最弱是宽泛的”措施/责任”题:例如拖欠工资怎么维权、维权途径,这类同时牵涉两部法多个条文,期望条文未必全部落进 top-8(recall@8=0.92 里丢的就是它们)。改进我会先上查询分解:复杂用例已经带了 sub_questions,可以让 LLM 把大问题拆成子问题分别检索再合并,能直接缓解”泛主题稀释分数”。其次是数据层补救(给维权/责任类触发词补概念映射或 crossref),最后再考虑实体链接 + 图传播这种更重的召回升级。优先级依据很简单:哪类用例 recall 掉得最多,先补哪类。


上一讲:第 7 讲 短期记忆、CLI 与结构化日志