查看全课大纲(第 63 / 69 节)
  1. 1.NLP 的研究领域、应用与学习路径
  2. 2.自然语言的基本术语
  3. 3.信息论基础:信息熵与交叉熵
  4. 4.语言的进化与语义变迁
  5. 5.主观概率、客观概率与条件概率
  6. 6.贝叶斯原理与推理
  7. 7.随机变量:二项式、期望方差与联合概率
  8. 8.常见概率分布
  9. 9.辛普森悖论与贝叶斯解题实例
  10. 10.语言模型:语言概率与词袋模型
  11. 11.n-gram 语言模型(二元 / 三元 / N-Gram)
  12. 12.语言模型评价:交叉熵、困惑度与插值
  13. 13.朴素贝叶斯文本分类:垃圾邮件
  14. 14.拼写检查
  15. 15.中文分词:构词法、最大匹配、最优路径与 Jieba
  16. 16.词表示问题与语义相似度
  17. 17.TF-IDF 权重处理
  18. 18.One-Hot 表达与词表示的局限
  19. 19.Word2Vec 核心思想
  20. 20.Word2Vec 训练过程
  21. 21.Skip-gram 模型详解
  22. 22.词性标注任务与标注方法(规则 + N-Gram)
  23. 23.EM 算法与混合模型
  24. 24.高斯混合模型 GMM
  25. 25.从混合模型到隐马尔可夫模型
  26. 26.隐马尔可夫模型(HMM)推导与维特比解码
  27. 27.线性链条件随机场 CRF
  28. 28.贝叶斯网络与图模型
  29. 29.隐马尔可夫链与 HMM 推导
  30. 30.PLSA 主题模型
  31. 31.LSI 与 SVD 向量表达
  32. 32.LDA 与狄利克雷分布
  33. 33.主题模型实验复现(困惑度 / 分类 / 相似度 / 延展)
  34. 34.上下文无关文法(CFG)与句法分析
  35. 35.概率上下文无关文法 PCFG
  36. 36.统计机器翻译:词对齐与翻译模型
  37. 37.统计机器翻译中的对齐模型与参数估计
  38. 38.从统计机器翻译到神经机器翻译
  39. 39.神经元与反向传播速览(指向深度学习课)
  40. 40.CNN 原理及其在 NLP 中的应用
  41. 41.循环神经网络的基本原理与 Elman / Jordan 网络
  42. 42.LSTM:长短期记忆网络的核心思想与分步实现
  43. 43.Encoder-Decoder 框架
  44. 44.Seq2Seq 与早期注意力机制
  45. 45.注意力机制的背景与动机
  46. 46.注意力模型实现原理与匹配函数
  47. 47.记忆网络:组成与计算
  48. 48.注意力机制的应用与历史延展
  49. 49.Transformer 结构:Encoder-Decoder 栈与 Self-Attention
  50. 50.Multi-Head Attention、FFNN 与位置编码
  51. 51.层归一化、损失函数与 Universal Transformer 延展
  52. 52.预训练范式:从 ELMo 到 BERT
  53. 53.BERT 与掩码语言模型
  54. 54.BERT 下游任务微调与 GLUE 口径
  55. 55.GPT 与自回归语言建模
  56. 56.GPT-1 到 GPT-4 的演进
  57. 57.指令微调与 RLHF
  58. 58.分词器:BPE、WordPiece 与中文词表
  59. 59.上下文窗口与长文本处理
  60. 60.能力边界、成本与适用场景
  61. 61.文本分类实战:从朴素贝叶斯到逻辑回归
  62. 62.命名实体识别实战
  63. 63.文本生成与摘要实战
  64. 64.语义检索与向量库实战
  65. 65.RAG 知识库问答
  66. 66.NLP 评估指标:BLEU / ROUGE / BERTScore 与人工评估
  67. 67.幻觉与事实性问题
  68. 68.文本与知识结合(知识图谱压缩)
  69. 69.跨媒体信息检索与未来展望

文本生成与摘要实战

小象实战讲义 · 自然语言处理

摘要看起来是个”算法题”,实际是个”取舍题”:抽出来的句子够不够覆盖原文、有没有重复、长度守不守预算、分数怎么算才算数。本节只做抽取式摘要,用一个 6 句话的微例走完整条链路——建图排序、按分取句、按原文顺序拼接、再用 ROUGE 打分。全程两个自包含代码块,你可以在本地直接复现每一个数字。

💡 核心导读

  • 抽取 vs 生成:抽取式的每句话都有原文出处,事实性天然有保障,代价是拼出来可能不连贯;生成式流畅,但要额外承担事实核查成本。这是项目选型的第一条分界线。
  • TextRank 的三个动作:句子相似度建图 → 行归一化成转移矩阵 → 幂迭代到收敛。本例拿到 0.2509 / 0.2407 两个高分句,差距只有 0.0102。
  • ROUGE 的口径:ROUGE-1 的 F1 是 0.4286,ROUGE-L 也是 0.4286,两个数相同不是巧合——当 LCS 长度恰好等于 unigram 命中数时它们就会重合,这正是”指标之间并不独立”的实例。

一、先分清:抽取还是生成

摘要的两种做法,差别不在”先进程度”,而在风险落在哪里。

抽取式从原文挑句子拼接。它的好处是硬性的:每一句都能在原文里指出来,不会凭空冒出一个原文没写的事实;实现成本低,可控性强(改 k 就改长度)。代价是连贯性——把第 1 句和第 3 句拼一起,指代可能断(第 3 句开头那个”它”指谁?),而且它只能”挑”,不能”概括”,原文啰嗦的地方它也跟着啰嗦。

生成式理解原文后重写。它流畅、凝练,能合并同义表达,但引入了幻觉风险:模型可能写出原文没有的数字、因果或结论,而这类错误在摘要场景里往往是致命的(比如把”预计下降 5%“写成”下降 15%”)。它还需要额外一步事实核查,成本随质量要求上升。

所以选型的问题应该这样问:这个场景能不能接受”摘要里出现原文没有的话”? 法律、医疗、财报这类不能接受的,从抽取式起步;营销文案、读书笔记这类能接受且愿意配核查的,可以上生成式。

本节只讲抽取式,但它的评估口径(下一节的 ROUGE)和工程约束(长度预算、冗余控制)对生成式同样适用,这也是先学它的另一个理由。

二、微例:用简化 TextRank 排出句子高低

2.1 数据:一段 6 句的产品说明

我们虚构一段智能咖啡机的说明,共 6 句、137 个字;再人工写一句参考摘要,作为后面算 ROUGE 的标准答案。参考摘要不是原文的任何一句,而是”人读完觉得该留下的信息”——这一点很关键,后面 4.2 会展开。

2.2 算法:建图、归一化、迭代

TextRank 的思路借自 PageRank:句子是节点,句子间的相似度是边的权重。一个句子如果和很多高分句子相似,它自己也该是高分。

本例的相似度用字符 bigram 的 Jaccard:把句子按相邻两字切成二元组集合,两个句子的交集大小除以并集大小。这个度量只看字面重合,天然抓不到同义改写——这是它的短板,也正是 4.1 要处理的问题。

转移矩阵的构造要小心两件事:自环置零(否则句子给自己投票,分数虚高)、行归一化(每行加起来为 1,保证迭代稳定)。全零行(本例句子 4 与任何句子都不重合)要特殊处理,否则除零。

import numpy as np

sentences = [
    "本款智能咖啡机支持连接手机APP进行远程控制。",
    "用户可以通过APP预约冲泡时间,设定咖啡浓度和杯量。",
    "机器内置智能称重系统,能精确控制咖啡粉用量。",
    "它采用15巴高压泵,确保萃取出的咖啡油脂丰富。",
    "清洁方面,机器具备一键自清洁和除垢提醒功能。",
    "产品包装内含主机、滤水壶、咖啡勺和说明书。",
]

def bigrams(text):
    chars = text.replace('。', '')
    return {chars[i:i + 2] for i in range(len(chars) - 1)}

bgrams = [bigrams(s) for s in sentences]
n = len(sentences)
sim = np.zeros((n, n))
for i in range(n):
    for j in range(n):
        if i != j:
            sim[i, j] = len(bgrams[i] & bgrams[j]) / len(bgrams[i] | bgrams[j])

print(f"句子数: {n}|平均 bigram 数: {sum(len(b) for b in bgrams) / n:.2f}")
print("相似度矩阵(保留 4 位):")
for i in range(n):
    print("  句子{0}: {1}".format(i, [round(float(x), 4) for x in sim[i]]))

M = np.zeros((n, n))
for i in range(n):
    row = sim[i].sum()
    M[i] = sim[i] / row if row > 0 else np.ones(n) / n

d = 0.85
scores = np.ones(n) / n
for _ in range(20):
    scores = (1 - d) / n + d * M.T.dot(scores)

print("最终得分(d=0.85,迭代 20 次):")
for i, s in enumerate(scores):
    print(f"  句子{i}: {round(float(s), 4)}")

k = 2
top = np.argsort(-scores)[:k]
order = sorted(top.tolist())
summary = "".join(sentences[i] for i in order)
print(f"Top-{k} 索引(按得分): {[int(i) for i in top]}|按原文顺序: {order}")
print(f"候选摘要: {summary}")
print(f"候选摘要字数: {len(summary)}|原文总字数: {sum(len(s) for s in sentences)}")
句子数: 6|平均 bigram 数: 20.67
相似度矩阵(保留 4 位):
  句子0: [0.0, 0.0714, 0.0789, 0.0244, 0.0, 0.0256]
  句子1: [0.0714, 0.0, 0.0233, 0.0227, 0.0, 0.0238]
  句子2: [0.0789, 0.0233, 0.0, 0.025, 0.0263, 0.0263]
  句子3: [0.0244, 0.0227, 0.025, 0.0, 0.0, 0.0256]
  句子4: [0.0, 0.0, 0.0263, 0.0, 0.0, 0.0]
  句子5: [0.0256, 0.0238, 0.0263, 0.0256, 0.0, 0.0]
最终得分(d=0.85,迭代 20 次):
  句子0: 0.2509
  句子1: 0.1815
  句子2: 0.2407
  句子3: 0.1339
  句子4: 0.0549
  句子5: 0.1381
Top-2 索引(按得分): [0, 2]|按原文顺序: [0, 2]
候选摘要: 本款智能咖啡机支持连接手机APP进行远程控制。机器内置智能称重系统,能精确控制咖啡粉用量。
候选摘要字数: 45|原文总字数: 137

2.3 三个必须看懂的读数

第一,高分句和第二名只差 0.0102(0.2509 对 0.2407)。这个差距小到意味着:换一种相似度度量,排名就可能翻转。所以 TextRank 的结果不该当成”唯一正确答案”,它只是”在当前度量下的排序”。工程上如果两个句子分数贴得很近,更稳妥的做法是两个都要,或者把 k 调大一点。

第二,句子 4 掉到 0.0549,因为它是孤岛。看相似度矩阵的第 5 行(句子 4):与其他所有句子的 Jaccard 全是 0,只有”清洁”这个主题,字面上和别处没有共同 bigram。它因此在图里没有边,只能拿到全局跳转概率 (1-d)=0.15 平摊到 6 个节点后的基础分 (1-d)/n = 0.15/6 = 0.025,加上从别处传不进来的权重。注意这不是说句子 4 不重要——“一键自清洁”对咖啡机买家是卖点。孤岛句被压低,是字面相似度建图的结构性偏差,不是内容判断。

第三,拼摘要时用的是”按原文顺序”,不是”按分数顺序”。top 是 [0, 2],order 也是 [0, 2],本例两者恰好一致,是因为句子 0 本来就排在句子 2 前面。如果 top 是 [2, 0](句子 2 分数更高),order 依然会排成 [0, 2]——这一步不能省,否则摘要读起来是”后半句在前、前半句在后”,连贯性直接崩掉。

还有一处细节:拼接用的是 ""(空串)而不是 "。".join()。因为每个句子自带句号,再加分隔符就会拼出 。。——这是很多摘要 demo 里能看到的低成本 bug(本例拼接后 45 字,占原文 137 字的 32.8%)。

三、评估:ROUGE 怎么算才不算错

3.1 指标定义:召回导向,P/R 分开看

ROUGE 是召回导向的一组指标,名字里的 R 就是 Recall。它比的是候选摘要与参考摘要的 n-gram 重合:

  • ROUGE-N 的召回 = 命中 n-gram 数 / 参考摘要的 n-gram 总数;精确率 = 命中数 / 候选摘要的 n-gram 总数;F1 由两者调和而来。
  • ROUGE-L 用最长公共子序列(LCS):不要求连续,但要求顺序一致,因此对”语序乱掉”比 n-gram 更敏感。

有一个坑必须先说清:命中数要用”截断计数”(clipping)。候选里同一个 n-gram 出现 3 次、参考里只出现 1 次,最多只能算命中 1 次。若不截断,重复堆砌同一句话反而能把分数刷上去——这是 ROUGE 被”刷分”的经典路径。本节代码用 Counter 取两边计数的较小值,做的就是截断。

3.2 手算一遍:4 个词的小例子

先手算,再写代码,才知道代码里的每个数从哪来。

  • 候选 C:智能 咖啡机 控制 参数(4 词)
  • 参考 R:咖啡机 可以 控制 参数(4 词)

ROUGE-1:共有 unigram 是 咖啡机 / 控制 / 参数 三个(智能 只在候选里有,不算命中)。所以召回 = 3/4 = 0.7500,精确 = 3/4 = 0.7500,F1 = 0.7500。

ROUGE-L:C 与 R 的最长公共子序列是 咖啡机 控制 参数,长度 3。召回 = 3/4 = 0.7500,精确 = 3/4 = 0.7500,F1 = 0.7500。

这个例子说明一件容易被忽略的事:当两个摘要长度相同、且 LCS 恰好等于公共 unigram 数时,ROUGE-1 和 ROUGE-L 会给出完全一样的分数。所以看到两个数相同,不要以为”两个指标互相印证了”,它们可能只是在这个样本上退化成了同一个量。

3.3 在咖啡机例子上算真账

候选摘要来自 2.2 的输出(句子 0 + 句子 2),参考摘要来自 2.1 的人工撰写。中文要先分词才能算 n-gram,本例直接在代码里硬编码两份人工分词后的词表,避免引入分词库(分词本身的分歧会污染指标)。

from collections import Counter

candidate = ["本款", "智能", "咖啡机", "支持", "连接", "手机", "APP", "进行", "远程", "控制",
             "机器", "内置", "智能", "称重", "系统", "能", "精确", "控制", "咖啡粉", "用量"]
reference = ["智能", "咖啡机", "可用", "手机", "APP", "远程", "控制", "预约", "冲泡", "并",
             "设定", "参数", "机器", "具备", "智能", "称重", "和", "15巴", "高压泵", "确保",
             "咖啡", "品质"]

def ngrams(tokens, n):
    return [tuple(tokens[i:i + n]) for i in range(len(tokens) - n + 1)]

def rouge_n(cand, ref, n):
    """截断计数(clipped):候选里某个 n-gram 的计数不得超过参考里该 n-gram 的计数。"""
    cc, rc = Counter(ngrams(cand, n)), Counter(ngrams(ref, n))
    hit = sum(min(c, rc[g]) for g, c in cc.items())
    p = hit / sum(cc.values()) if cc else 0.0
    r = hit / sum(rc.values()) if rc else 0.0
    f = 2 * p * r / (p + r) if p + r else 0.0
    return hit, p, r, f

def lcs(a, b):
    dp = [[0] * (len(b) + 1) for _ in range(len(a) + 1)]
    for i in range(1, len(a) + 1):
        for j in range(1, len(b) + 1):
            dp[i][j] = dp[i - 1][j - 1] + 1 if a[i - 1] == b[j - 1] else max(dp[i - 1][j], dp[i][j - 1])
    return dp[len(a)][len(b)]

L = lcs(candidate, reference)
p_l = L / len(candidate)
r_l = L / len(reference)
f_l = 2 * p_l * r_l / (p_l + r_l)

print(f"候选词数: {len(candidate)}|参考词数: {len(reference)}")
for n in (1, 2):
    hit, p, r, f = rouge_n(candidate, reference, n)
    print(f"ROUGE-{n}: 命中 {hit} 个 n-gram|P = {p:.4f}|R = {r:.4f}|F1 = {f:.4f}")
print(f"ROUGE-L: LCS 长度 {L}|P = {p_l:.4f}|R = {r_l:.4f}|F1 = {f_l:.4f}")
候选词数: 20|参考词数: 22
ROUGE-1: 命中 9 个 n-gram|P = 0.4500|R = 0.4091|F1 = 0.4286
ROUGE-2: 命中 4 个 n-gram|P = 0.2105|R = 0.1905|F1 = 0.2000
ROUGE-L: LCS 长度 9|P = 0.4500|R = 0.4091|F1 = 0.4286

读数:

  • ROUGE-1 命中 9 个,召回 0.4091——意思是参考摘要里 22 个词,候选只覆盖了 9 个。漏掉的主要是参考摘要里的”可用/预约/冲泡/参数/具备/高压泵/品质”这些同义或换词的表达:候选说的是”支持连接…进行远程控制”,参考说的是”可用…远程控制”,字面不同,n-gram 就抓不到。这不是模型差,是字面指标的天花板。
  • ROUGE-2 掉到 0.2000,比 ROUGE-1 掉了一半多。二元组要求”连续两个词一起对上”,同义换写只要有一个词不同就整段落空。所以 ROUGE-2 更严,也更不宽容改写。
  • ROUGE-L 又回到 0.4286,且 LCS 长度 9 恰好等于 ROUGE-1 的命中数——这正好印证 3.2 手算时的结论:LCS 把”一个个命中词”按顺序串起来,本例里串起来的结果和 unigram 命中集合的大小一致。

四、工程化:算法之外的六件事

微例走通了流程,但真实项目里决定成败的往往不是算法,而是下面六件。

4.1 TextRank 的字面缺陷与 MMR 补救

本节用 bigram Jaccard,抓不到”远程控制”和”用手机操控”这种同义改写。更严重的问题是冗余:高度相似的一簇句子会互相传递高分,一起挤进 top-k,摘要里反复说同一件事。

补救思路是 MMR(最大边际相关):选下一句时不只看它自己的分,还要扣掉它与已选句子的最大相似度。落到代码上就是每选一句更新一次候选分:score'(i) = λ·score(i) - (1-λ)·max_sim(i, 已选)。这一步不需要改 TextRank 的主体,是纯后处理。

4.2 参考摘要的口径,决定分数的可比性

ROUGE 的分母来自参考摘要。参考摘要是人工撰写的流畅摘要,还是从原文标出的关键句集合?本例属于前者(“可用手机APP远程控制”这类改写原文里根本没有),所以候选摘要天然吃亏——它只能整句搬原文。

同一份候选,换一份”关键句集合”式参考,ROUGE-1 会明显上升。所以报告里必须写清参考摘要的构建口径、几份、谁写的,否则跨项目比分数毫无意义。多份参考取平均通常更稳。

4.3 长度预算是硬约束,不是事后裁剪

摘要一般有字数上限。常见错误是”先取 top-k,再超长就截断”——截断会把句子切成半句,可读性直接归零。

正确做法是在选择过程中就守预算:按分数降序遍历,装得下就装、装不下就跳过继续看下一句。本例 k=2 得到 45 字(占原文 32.8%),若预算只有 30 字,句子 0(23 字)能进、句子 2(22 字)进不去,最终只剩一句——这个结果虽然短,但每句都是完整的。

4.4 冗余率与覆盖率:ROUGE 看不到的两件事

ROUGE 只和参考摘要比,不看候选自己。所以它能给出 0.42 的高分,却对”摘要里两句话在说同一件事”毫无反应。需要补两个自评指标:

  • 冗余率:候选内所有句子对的平均相似度。高于某个阈值(比如 0.3)说明在自我重复。
  • 覆盖率:原文的关键实体/关键句有多少落进了摘要。可以用实体集合比对,也可以人工抽检 10 条。

这两个指标不需要参考摘要,在没有标准答案的线上环境里是唯一可用的监控手段。

4.5 生成式摘要的额外纪律

如果换成生成式模型,有三条必须加:生成长度上限(否则容易截在半句)、结尾完整性检查(最后一个字是不是句号或终止符)、事实核验(把摘要里的数字、专名、因果词抽出来,回原文比对)。第三条最贵,但也最关键——摘要场景里”编了一个数”比”少说一句”严重得多。

4.6 从单文档到多文档

多篇报道同一事件时,第一问题不是排序而是去重:不同来源的句子会大量重复。常见流程是先把所有句子聚类(按相似度),每个簇只留一句代表,再在代表句上跑排序。否则 top-k 会被同一事件的几种说法占满。

五、逻辑映射:微例到真实项目

不变的四条

  1. 先定策略:抽取还是生成,取决于能不能接受”原文没有的话”。这条不因数据量变化而变化。
  2. 预算先行:长度上限是产品给的硬约束,必须进算法,不能做事后裁剪。
  3. 评估口径写清楚:参考摘要怎么来的、用 ROUGE 几、截断计数有没有做——这三条不写,分数不可比。
  4. 盯住指标盲区:ROUGE 看不见冗余和覆盖率,这两项要单独监控。

要变的四处

  1. 规模:句子从 6 条到几百条,两两算 Jaccard 是 O(n²),要换 MinHash/SimHash 近似,或者先粗筛再精算。
  2. 相似度:从 bigram Jaccard 升级到词向量/BERT 余弦。图的结构不变,边的质量变了——这正是 4.1 说的”同义改写抓不到”的解法。
  3. 分块:长文档不能整篇建图,要先按章节分段,段内摘要、段间再摘要(层次化)。
  4. 多文档:增加跨文档去重与信息融合,流程从”排序”变成”聚类 + 排序”。

一句话结论:摘要系统的质量,一半在”选句与去重”是否稳,另一半在”评估口径是否和业务对齐、以及有没有监控 ROUGE 看不见的那两项”。

📝 动手练一练

  1. 口径辨析:本例 ROUGE-1 与 ROUGE-L 都是 0.4286。请说明这个”相同”是巧合还是有条件成立,并给出一个让两者明显分岔的候选摘要。

  2. 改代码:把 2.2 的相似度从 bigram Jaccard 换成 unigram Jaccard(按单字集合算),观察 top-2 的索引和分数如何变化,并解释为什么会出现这个变化。

👉 点击查看参考答案

1. 口径辨析

不是巧合,但有条件:当 LCS 长度恰好等于 ROUGE-1 的截断命中数时,两者会重合。本例 LCS = 9,ROUGE-1 命中也是 9。

要让两者分岔,构造一份”词都对、顺序全乱”的候选即可。例如候选写成 控制 智能 咖啡机 手机 APP 远程:ROUGE-1 依然能命中这些 unigram(召回仍然不低),但 LCS 只能取到 智能 咖啡机 这样的短片段,ROUGE-L 会明显低于 ROUGE-1。这正是 ROUGE-L 存在的意义:它对语序敏感。

2. 改成 unigram Jaccard

把 bigrams() 改成返回单字集合后重跑,会看到:句子间的相似度普遍上升(单字重合比双字容易命中),句子 4(孤岛句)不再完全孤立——“清洁""机器”里的”机”字会与别句的单字重合,它的分数会从 0.0549 上升。同时前两名的差距会缩小,甚至可能换位。

原因:bigram 要求”两个字连续相同”,是更严的匹配;退到单字后,匹配门槛降低、噪声变大,图的边变密,权重分布被摊平——度量越松,排序越钝。这也是为什么真实项目里宁可用语义向量,而不是继续往更松的字面度量退。

本章小结

本节用两个自包含代码块走完了抽取式摘要的全链路,结论有三条:

  1. TextRank 给的是”排序”,不是”答案”。本例前两名只差 0.0102,换度量就可能换位;孤岛句(句子 4)被压到 0.0549,是字面相似度的结构性偏差,不代表内容不重要。拼接必须按原文顺序,否则连贯性崩掉。
  2. ROUGE 必须做截断计数。不做截断,重复堆砌就能刷分;做了截断,本例 ROUGE-1 F1 = 0.4286、ROUGE-2 = 0.2000、ROUGE-L = 0.4286。ROUGE-L 与 ROUGE-1 相同是”LCS 长度 = unigram 命中数”的结果,不是互相印证。
  3. 分数之外还有两件事:长度预算要在选句过程中守(本例 45 字 / 原文 137 字,占 32.8%),冗余率与覆盖率要单独监控——这是 ROUGE 永远看不见的盲区。

📋 行动清单

  • 跑通本节两个代码块,复现 0.2509 / 0.2407、0.4286 / 0.2000 这几个数,并说清每个分母是什么。
  • 给 rouge_n 加一条注释或断言:确认它用的是 min(候选计数, 参考计数) 的截断计数,而不是简单集合交集。
  • 给自己的摘要流程加长度守门:按分数降序装句子、装不下就跳过,禁止”先取 top-k 再截断”。
  • 补两个自评指标:冗余率(句对平均相似度)与覆盖率(关键实体命中比例),在没有参考摘要的环境里用它们做线上监控。

—— 小象教研组