跳转至

P6-6.2 在长回答前输出判断值的理解中心任务

Section ID: P6-6.2 Version: v2026.07.26

如果把 BERT 家族读成基于 Transformer encoder 的表示模型,还需要区分这些表示会通向哪些任务组。理解中心任务会读取整个输入,并判断它是什么或它匹配得多好,例如分类、相关性判断、搜索和嵌入。这些任务很适合 BERT 家族表示模型。

理解中心任务的输出形式

理解中心输出从这些问题开始。

  • understanding-centered task 可以如何解释?
  • BERT 家族在哪些任务中特别有用?
  • 分类、搜索、句子对比较和嵌入如何绑成一个流程?

最安全的做法,是把理解中心任务抓成读取输入,并输出标签、分数或向量的任务组。这样,这条流程为什么适合 BERT 家族也会更清楚。

以后阅读 P6-13.1 向量数据库和 P6-13.2 索引与搜索质量,看到搜索流水线内部如何拆分结构时,也可以重新恢复这个比较标准。

与其列出很多任务名称,更重要的是理解读取输入并判断的流程。如果上一节把 BERT 家族的位置设为比较标准,本节就把这个比较缩窄到实际任务组,并先区分为什么标签、分数、排名、向量属于同一个输出家族。

所以,我们要看的是读取并输出判断值的结构和生成长文本的结构之间的差异,而不是背任务名称。理解中心任务不需要被学成新的清单;只要抓住分类、相关性判断、搜索和嵌入都在读取输入并输出判断值这一共同点就足够。

区分长回答生成和判断值输出

  • 你可以在入门层面解释理解中心任务。
  • 你可以把分类、相关性判断、搜索和嵌入归为同一类工作。
  • 你可以说明为什么这些任务适合 BERT 家族。
  • 你可以更清楚地阅读它们和 GPT 家族的对比。

什么是理解中心任务?

这里的理解中心任务并不是哲学意义上的类人理解。它指的是下面这些任务组。

  • 这个输入属于什么标签?
  • 这两个句子的意义是否接近?
  • 这个问题和这份文档有多相关?
  • 哪个向量能表示这个句子?

所以,输出不是通向一段长生成句子,而是通向:

  • 标签,
  • 分数,
  • 相关性,或
  • 代表性表达。

理解中心任务的输入和输出

这条流程更容易通过问这个输入会产生什么判断结果?来理解,而不是问模型是否继续写下一个句子?

任务 输入 输出
分类 一个句子 标签
句子对判断 两个句子 related / not related 等关系标签或分数
搜索排序 问题和候选文档 相关性分数、排序顺序
嵌入 句子或文档 向量表示

理解中心任务的输出通常不是下一个句子,而是用于判断的产物。

代表任务 1. 文档和情感分类

最熟悉的例子是分类。

例如:

  • 垃圾邮件 / 正常邮件分类,
  • 咨询类别分类,
  • 情感分类(positive / negative / neutral)。

这些任务读取整个句子,并判断它属于哪个类别。

因为 BERT 家族会创建反映整个输入上下文的表示,所以很适合这些任务。

代表任务 2. 句子对判断

判断两个句子之间关系的任务也很重要。

例如:

  • 两个句子的意义是否接近?
  • 问题和答案是否彼此匹配?
  • 句子 A 是否蕴含句子 B?

这些任务比单句分类更进一步,询问两个输入之间的关系。

可以这样理解:

句子对判断不是读取一个输入,而是比较两个输入之间的关系,并输出分数或标签。

代表任务 3. 搜索和排序

搜索也可以读成理解中心任务。

给定一个问题作为输入,系统判断:

  • 哪份文档相关;
  • 多个候选中哪份文档应该排得更高。

这里,BERT 家族可以通过两种方式连接。

  • 一起读取问题和文档,并输出相关性分数。
  • 分别把问题和文档转成表示向量,再进行比较。

后者会直接连接到嵌入搜索。

代表任务 4. 嵌入和表示复用

BERT 家族以及后来的 encoder-centered 模型,也常被用来把句子转成嵌入。

例如:

  • 寻找相似句子,
  • 寻找重复 FAQ 问题,
  • 聚类文档,
  • 为搜索生成 dense vector。

这些任务更接近表示复用,而不是生成。

所以,BERT 家族不只可以被读成分类模型,也可以被读成许多判断任务的通用表示引擎。

为什么这些任务属于同一条流程

这些任务表面上不同,但中心问题相似。

  • 它是什么?
  • 它们有多相似?
  • 它有多相关?
  • 它属于哪个类别?

它们更接近读取输入并判断,而不是长篇生成下一个句子。

可以把它们归成下面这条流程。

flowchart TD
  A["输入文本或文本对"]
  B["基于 encoder 的上下文表示"]
  C["分类 / 相关性 / 嵌入输出"]

  A --> B
  B --> C

这个图用最简单的方式归纳 BERT 家族的实践使用直觉。需要确认的结果是,首先需要读取、区分、连接的工作是否和长回答生成分开出现。

区分输出形式

首先要保留的一句话是:

与生成长回答相比,BERT 家族更自然地用于读取输入,并创建标签、分数、相关性和嵌入。

一旦固定这句话,就不需要背下每个细节任务名称,也能阅读 P6-5.1、P6-7.1 中的 GPT 和 next-token prediction 说明,或 P6-12.1、P6-12.2 中的 RAG 说明。

案例和例子

案例 1. 客户咨询分类

把客户咨询分到 shipping、account、payment、error 是典型的理解中心任务。即使在这个场景中,也很容易认为好服务就是模型写出一段长而亲切的说明。但在真实运营中,决定这应该由哪个处理流程接收?比长回答更重要。

例如,The payment went through, but I cannot see the order 这样的句子同时显示付款和订单,但实际运营需要知道哪个团队应先查看。先送到 payment confirmation 还是 order synchronization check,比写出一段长回答更直接地影响处理速度。

如果请求被送到错误队列,即使回答写得很好,也会拖慢实际解决。重要工作不是写长回复,而是读取进来的句子并决定它应进入哪个处理流程。需要纠正的误解是好的说明应该先帮助用户。实际上,只有先关闭谁应该先处理?,下一步说明才有意义。这个案例中要确认的结果是,请求是否先进入正确处理队列,以及仅凭队列选择是否就能让下一步运营立即继续,而不是看回答句子的质量。

案例 2. FAQ 搜索

把用户问题和已有 FAQ 比较,找出最接近的项目,是相关性判断和嵌入搜索一起使用的场景。在这个场景中,很容易想:如果模型重新写一段漂亮说明,不是更好吗? 但即使是人,通常也会先选择哪条已有答案最合适,而不是先写一段新说明。

例如,I forgot my password 和 How do I reset my login password? 虽然表面表达不同,但更应该连接到同一篇帮助文章。如果已有 FAQ 已经包含分步骤截图,准确连接到那个项目通常比生成新答案更安全。

相反,如果系统选了无关 FAQ,再加上一句自然的新句子,用户可能会被送到错误路径。这里的核心不是创建新句子,而是选择最匹配的文档。需要纠正的误解是生成式回答总是比搜索看起来更聪明。实际上,准确连接已有正确文档往往实用得多。这个案例中要确认的结果是,最接近的 FAQ 项目是否在生成新答案前先被连接,以及这个连接本身是否就能让用户采取下一步行动。

案例 3. 文档重复检测

判断两份文档的标题和正文是否几乎相同,可以读成句子对比较和相似度判断流程。在这个任务中,也很容易先想到让模型摘要或合并文档。但人们通常会先检查它们有多相似?,再决定是否重写两者。

例如,如果两条公告只是句子顺序不同,核心内容相同,把它们归为重复文档在运营上可能比创建新回答更重要。即使一个标题是 maintenance guide,另一个是 service maintenance notice,只要正文说明的是同一事件,重复判断就更重要。

如果漏掉重复文档,相似文档会不断堆积,搜索结果也会混乱。这个案例也属于同一家族,因为它是读取、比较、打分。这里的转换,是从问是否应该生成新说明?转向问这两份文档实际上是否是一组?。需要纠正的误解是文本工作首先就是生成。这个案例中要确认的结果是,重复文档是否真的被整理成一组,而不是作为新文档留下,以及判断值是否能直接用于后续搜索清理。

三个案例可以再次从理解中心任务的视角分组。

情况 生成前需要的判断 首先作为实际输出留下什么
客户咨询分类 应由哪个处理队列接收? 标签
FAQ 搜索 哪个已有项目最接近? 相关性排名
文档重复检测 两份文档是否在说同一件事? 相似度分数或重复判断

首先需要判断值的场景

第一次阅读理解中心任务时,一个常见误解是认为AI 应该先生成长回答。但首先要检查的是,需要的输出是否是标签、分数或排名,而不是长篇生成。转换成实践问题,可以这样读。

如果出现这种疑问 首先要问的问题
这应该写回答,还是先分类? 需要的输出是句子,还是标签?
已有文档不是更好吗? 是否应在新生成前先给出相关性排序?
它们看起来相似,但是否是同一处理流程? 比较结果是否应先作为分数或判断留下?

先学会的标准很简单。理解中心任务更接近 read -> label/score/rank/vector 的判断结构,而不是长回答生成。因此,BERT 家族应被读成读取和区分的前端结构,而不是生成的竞争者。

练习和例子

这个例子的目标,是用一个小型向量表示实验确认:理解中心任务实际输出的是 labels、relation scores、search ranks 等判断结果。

与生成式回答不同,下面的例子检查的是理解中心任务读取并输出判断值的结构。输入 CSV p6-20-understanding-task-cases-zh.csv 分别包含分类、句子对判断、搜索排序各 12 个案例。一行是一个判断案例。task_type 表示输出形式,scenario_pattern 表示观察角色,例如直接信号、边界信号或不同意图。

关键是确认:理解中心任务会在长回答之前输出标签、分数和排名。这里不直接下载并运行 BERT,而是使用本地可复现的 TF-IDF 向量作为小型替代表示。在真实 BERT 家族模型中,这些表示会变成更丰富的上下文表示,但输出流程仍然是把输入转成表示,再输出判断值。

代码中可以尝试改变的值是 relation_threshold。如果提高它,句子对判断会更保守,一些边界案例会从 related 移到 not_related。这个变化显示,理解中心任务的输出更接近从表示之间的分数产生哪个判断值,而不是一段长句子。

# 这个例子从 CSV 读取分类、句子对关系、文档排序案例,
# 检查输入表示如何变成标签、关系分数和文档排名。
import csv
from pathlib import Path

from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity

case_path = Path("docs/assets/part-06/chapter-20/p6-20-understanding-task-cases-zh.csv")

domain_terms = {
    "shipping": ["配送", "包裹", "发到", "地址", "到达", "箱子", "换货"],
    "account": ["账户", "登录", "密码", "验证", "验证码", "锁定", "重置", "邮件"],
    "payment": ["付款", "退款", "取消", "收据", "账单", "钱", "记录", "订单"],
    "document": ["FAQ", "公告", "重复", "文档", "维护", "指南"],
    "equipment": ["离职", "设备", "资产", "归还", "回收"],
}

queue_prototypes = {
    "shipping": "配送 延迟 包裹 地址 箱子 换货 配送 查询 旧地址",
    "account": "登录 密码 账户 验证码 锁定 认证 邮件 重置",
    "payment": "付款 退款 取消 收据 账单 钱 记录 订单 状态",
}

def enrich(text):
    if text == "-":
        return ""
    lowered = text.lower()
    tags = []
    for tag, terms in domain_terms.items():
        if any(term.lower() in lowered for term in terms):
            tags.extend([tag, tag])
    return text + " " + " ".join(tags)

def cosine_scores(left_texts, right_texts):
    vectorizer = TfidfVectorizer(analyzer="char_wb", ngram_range=(2, 4))
    matrix = vectorizer.fit_transform([enrich(text) for text in left_texts + right_texts])
    left_matrix = matrix[:len(left_texts)]
    right_matrix = matrix[len(left_texts):]
    return cosine_similarity(left_matrix, right_matrix)

with case_path.open(encoding="utf-8", newline="") as file:
    cases = list(csv.DictReader(file))

classification_rows = [row for row in cases if row["task_type"] == "classification"]
pair_rows = [row for row in cases if row["task_type"] == "pair_relation"]
ranking_rows = [row for row in cases if row["task_type"] == "ranking"]

queue_names = list(queue_prototypes)
queue_scores = cosine_scores(
    [row["text_a"] for row in classification_rows],
    list(queue_prototypes.values()),
)
classification_outputs = []
for row, scores in zip(classification_rows, queue_scores):
    best_index = scores.argmax()
    classification_outputs.append(
        {
            "case_id": row["case_id"],
            "pattern": row["scenario_pattern"],
            "output": queue_names[best_index],
            "score": round(float(scores[best_index]), 2),
        }
    )

relation_threshold = 0.24
strict_relation_threshold = 0.50
pair_scores = cosine_scores(
    [row["text_a"] for row in pair_rows],
    [row["text_b"] for row in pair_rows],
)
pair_outputs = []
for index, row in enumerate(pair_rows):
    similarity = float(pair_scores[index][index])
    pair_outputs.append(
        {
            "case_id": row["case_id"],
            "pattern": row["scenario_pattern"],
            "similarity": round(similarity, 2),
            "output": "related" if similarity >= relation_threshold else "not_related",
            "strict_output": "related" if similarity >= strict_relation_threshold else "not_related",
        }
    )

ranking_outputs = []
for row in ranking_rows:
    candidates = [row["candidate_1"], row["candidate_2"], row["candidate_3"]]
    scores = cosine_scores([row["text_a"]], candidates)[0]
    ranked = sorted(zip(candidates, scores), key=lambda item: item[1], reverse=True)
    ranking_outputs.append(
        {
            "case_id": row["case_id"],
            "pattern": row["scenario_pattern"],
            "top_document": ranked[0][0],
            "top_score": round(float(ranked[0][1]), 2),
        }
    )

by_task = {
    "classification": classification_outputs,
    "pair_relation": pair_outputs,
    "ranking": ranking_outputs,
}

print("[dataset]")
print("case_count =", len(cases))
print("task_counts =", {task: len(items) for task, items in by_task.items()})
print("representation = char_wb 2-4 gram TF-IDF + domain terms")
print("relation_threshold =", relation_threshold)
print("strict_relation_threshold =", strict_relation_threshold)
print()

for task_type in ["classification", "pair_relation", "ranking"]:
    print(f"[{task_type} preview]")
    for item in by_task[task_type][:3]:
        print(item)
    print("---")

changed = [item for item in pair_outputs if item["output"] != item["strict_output"]]
print("[threshold sensitivity]")
print("changed_pair_cases =", changed[:5])

一次示例运行可以这样阅读。representation 这一行表示,这个例子不是把文本只当作原始关键词来数,而是先把句子转成小型向量表示,再产生标签、关系分数和文档排名。

[dataset]
case_count = 36
task_counts = {'classification': 12, 'pair_relation': 12, 'ranking': 12}
representation = char_wb 2-4 gram TF-IDF + domain terms
relation_threshold = 0.24
strict_relation_threshold = 0.50

[classification preview]
{'case_id': 'C01', 'pattern': 'direct_label', 'output': 'shipping', 'score': 0.6}
{'case_id': 'C02', 'pattern': 'direct_label', 'output': 'account', 'score': 0.52}
{'case_id': 'C03', 'pattern': 'direct_label', 'output': 'payment', 'score': 0.45}
---
[pair_relation preview]
{'case_id': 'P01', 'pattern': 'same_intent', 'similarity': 0.68, 'output': 'related', 'strict_output': 'related'}
{'case_id': 'P02', 'pattern': 'different_intent', 'similarity': 0.0, 'output': 'not_related', 'strict_output': 'not_related'}
{'case_id': 'P03', 'pattern': 'same_intent', 'similarity': 0.73, 'output': 'related', 'strict_output': 'related'}
---
[ranking preview]
{'case_id': 'R01', 'pattern': 'semantic_match', 'top_document': '离职设备归还指南', 'top_score': 0.61}
{'case_id': 'R02', 'pattern': 'semantic_match', 'top_document': '登录密码重置指南', 'top_score': 0.6}
{'case_id': 'R03', 'pattern': 'semantic_match', 'top_document': '取消后退款申请流程', 'top_score': 0.78}
---
[threshold sensitivity]
changed_pair_cases = [{'case_id': 'P05', 'pattern': 'same_intent', 'similarity': 0.43, 'output': 'related', 'strict_output': 'not_related'}, {'case_id': 'P09', 'pattern': 'same_intent', 'similarity': 0.46, 'output': 'related', 'strict_output': 'not_related'}, {'case_id': 'P10', 'pattern': 'near_boundary', 'similarity': 0.46, 'output': 'related', 'strict_output': 'not_related'}]

从这个例子中要读出的关键点是:

  • 理解中心任务通常输出判断结果;
  • 中心不是像生成模型那样写长回答;
  • 分类、关系判断和搜索排序都可以绑成同一个读取并输出分数或标签流程;
  • 即使是小型 TF-IDF 向量,也能显示哪个输入如何变成哪个判断值,而真实 BERT 家族模型会在更丰富的上下文表示上执行这种评分;
  • 如果提高 relation_threshold,边界句子对更容易变成 not_related,显示判断标准会改变输出标签;
  • BERT 家族很适合这些判断任务。

下面的图按任务和输出形式汇总了同一 CSV 中的案例数。这里重要的不是条形值本身,而是分类、句子对判断和排序都会留下标签、分数、排名等判断值,而不是长回答。

理解中心任务的输出类型

通过运营判断重新连接

上面的三个案例显示了分类、相关性判断和相似度判断。如果从运营视角再次压缩同一想法,生成前必须检查的问题会更清楚。

场景 首先要做的判断 如果先生成会有什么问题
客户咨询分类 应由哪个处理队列接收? 即使回答礼貌,如果负责团队错了,也会延迟解决。
FAQ 搜索 哪个已有答案最接近? 增加一句新话,可能把用户连接到错误 FAQ。
文档重复检测 两份文档是否在说同一件事? 漏掉重复文档,会让搜索结果和文档管理持续混乱。

阅读这张表时的关键很简单。生成模型可以很擅长产出长而自然的句子,但在真实运营的第一阶段,必须先分类、比较、连接什么?往往更紧急。

例如,如果退款咨询被路由到账户锁定队列,即使回答句子很顺,处理也会更慢。相反,当路由和搜索判断先准确时,之后附上的生成回答会从更安全的位置开始。所以需要再次检查的结果,不是长回答草稿的自然程度,而是咨询是否先进入正确处理流程、相关文档是否先被准确连接。

BERT 重要不只是因为它是一种新结构。它强烈显示了基于 Transformer encoder 的预训练表示可以很好迁移到许多 NLP 任务。

通过这个时期,许多实践团队开始把:

  • 分类,
  • 搜索,
  • 排名,
  • 句子相似度,
  • 嵌入生成

看作同一个表示模型工作家族。

为什么要把它和生成中心结构分开?

到这里,比较的必要性会更清楚。

  • 与读取输入并判断的 BERT 家族不同,GPT 家族如何继续生成?
  • 为什么用户体验在 GPT 家族中变化得更明显?

这些问题让我们重新阅读 P6-5.1 The GPT Family as a Decoder-Based Cumulative Generation Structure。重点不是说生成结构不重要,而是服务前端常常还需要一个先分类、比较、连接的判断结构。有了这个标准,GPT 家族模型就不太容易被读成什么都做的结构,BERT 家族也可以被重新分离为负责阅读和判断的另一条轴。

检查清单

  • 你应该能够把理解中心任务解释为读取输入并输出标签、分数、相关性或嵌入的任务组。
  • 你应该能够说出,分类、搜索、句子对判断和嵌入名称不同,但属于同一条判断流程。
  • 你应该能够通过区分生成结构和判断结构这两种不同输出问题,解释 GPT 家族和 BERT 家族使用方式的任务级差异。

参考资料