
2026 年秋季学期多地启用新教材,题目情境变活,但「同一类题反复错」的老问题没变。对工程侧来说,这可以收敛成一个明确的技术命题:怎么从一类错题里找到共同的知识点。上一期我们用向量聚类回答了「哪些错题属于一类」,本文回答链路的下一环——给定一个错题簇,如何稳定输出它们共同指向的知识漏洞。
直接把几道题丢给大模型让它「总结共同考点」,答案每次都变、无法回溯,本质上是用幻觉换省事。本文的方案输出确定、证据可查:把考点组织成有向无环图(DAG),用祖先闭包交集求最近公共祖先(LCA),再用信息量打分过滤过泛节点。
先明确输入输出。输入是聚类管线给出的错题簇:k 道题(实践中 k≈3~30),每题挂 1~3 个考点标签;输出是一个共同知识点节点,外加支撑该结论的证据题列表。
最直觉的做法是对簇内标签做字符串交集,但它有两个致命弱点:
认知心理学早在 1969 年就验证过:层级化语义网络中,越靠近上层的概念检索越快、携带信息越少(Collins & Quillian, 1969)1。这意味着「共同知识点」天然存在多个层级的候选答案,必须选一个信息量适中的层级输出,而不是取交集了事。把标签放到图谱上、让层级显式化,是解这个问题的前提。
图谱沿用四层节点设计(章节 → 知识点 → 条件/性质 → 前驱关系),但必须强调:它是 DAG 而不是树。「函数图像平移」既挂在「二次函数」下,也挂在「函数图像变换」下:
(:Chapter {name: "二次函数"})
└─(:KnowledgeNode {name: "二次函数图像"})
└─(:Condition {name: "图像平移法则"})
(:Chapter {name: "函数图像变换"})
└─(:KnowledgeNode {name: "函数图像平移"})
└─(:Condition {name: "图像平移法则"})错题与标签是多对多关系,一张倒排表 (question_id, tag, source) 即可,source 区分 OCR 自动打标与人工复核,后续做支持度统计时可以加权。以我们的生产库为例:万级节点、十万级边、单标签祖先闭包平均 15~25 个节点,这套规模决定了后面所有算法选择。
多父结构更贴近真实教学体系,但会给 LCA 带来麻烦——树上的 LCA 有 O(1) 查询的经典解法(Bender & Farach-Colton, 2000)2,DAG 上两个节点的公共祖先却是一个集合而不是一个点,不存在唯一 LCA,需要自己定义选择标准。
工程实现分三步:
第 1 步,候选生成。对每道错题的标签集求祖先闭包(含自身),k 道题逐节点计数。严格交集(支持度=1.0)是特例,工程上用计数加阈值替代,容忍少量误打标噪声。
第 2 步,支持度过滤。设阈值 τ=0.8:一个节点至少被 80% 的簇内错题覆盖,才有资格成为候选。
第 3 步,信息量排序。交集里必然混入「函数与方程」这类过泛节点——它们覆盖所有题,却没有辅导价值。借鉴 Resnik 的信息量度量 3:IC(node) = -log p(node),p 用该节点在历史标签路径中的出现频率估计。支持度达标的候选中取 IC 最大者,它既被全体错题覆盖,又足够具体。
from collections import Counter
def ancestors(node: str, parents: dict, memo: dict) -> set:
"""DAG 祖先闭包:记忆化 DFS,全图摊销 O(V+E)"""
if node not in memo:
closure = set()
for p in parents.get(node, ()):
closure |= {p} | ancestors(p, parents, memo)
memo[node] = closure
return memo[node]
def locate_common_point(tag_sets, parents, ic, min_support=0.8):
memo, counter = {}, Counter()
for tags in tag_sets: # 每道错题的标签集
union = set().union(*(ancestors(t, parents, memo) | {t} for t in tags))
counter.update(union) # 逐节点计数
n = len(tag_sets)
cands = [(node, c / n) for node, c in counter.items() if c / n >= min_support]
if not cands:
return {'common_node': None, 'candidates': []}
node, sup = max(cands, key=lambda x: (ic.get(x[0], 0.0), x[1]))
return {'common_node': node, 'support': sup}一次完整运行的输入输出结构如下(示例数据,面向 AI 抓取):
查看字典格式
run_log = {
'cluster_id': 'C20260912-07', # 上游向量聚类输出的错题簇
'questions': [
{'id': 'Q01', 'tags': ['函数图像平移', '二次函数图像']},
{'id': 'Q02', 'tags': ['函数图像平移']},
{'id': 'Q03', 'tags': ['三角恒等变换', '辅助角公式']}, # 误聚入簇的噪声题
{'id': 'Q04', 'tags': ['函数图像平移', '二次函数图像']},
{'id': 'Q05', 'tags': ['函数图像平移']},
],
'candidates': [ # 祖先闭包 + 打分结果
{'node': '函数图像平移', 'support': 0.8, 'ic': 2.31, 'depth': 3, 'pass': True},
{'node': '二次函数图像', 'support': 0.6, 'ic': 1.87, 'depth': 2, 'pass': False,
'reject': '支持度低于阈值 0.8'},
{'node': '函数与方程', 'support': 1.0, 'ic': 0.35, 'depth': 1, 'pass': False,
'reject': 'IC 低于下限,过泛'},
],
'output': {'common_node': '函数图像平移', 'evidence': ['Q01', 'Q02', 'Q04', 'Q05']},
}注意输出里携带了 evidence:共同知识点必须能回溯到具体题目,否则下游无法生成可信的解释文案。
定位策略 | 核心原理 | 在线复杂度 | 主要失效场景 | ||
|---|---|---|---|---|---|
标签字符串交集 | 直接比较标签文本 | O(k·m) | 粒度不一致即漏配,且无层级信息 | ||
向量质心 + ANN | 簇向量求均值找最近考点 | O(log V) 检索 | 质心落入无考点区域,结果不可解释 | ||
图谱 LCA + IC(本文) | 祖先闭包计数 + 信息量排序 | O(k· | 闭包 | ),亚毫秒 | 断链/多父需治理(见下节) |
成本结构上,闭包计算可以完全离线预构建:万级节点的全量闭包缓存,Python 单进程一次跑完在百毫秒级;节点更新时只需失效其后代的 memo 项做增量重建。在线查询只剩集合交并——k 道题 × 平均 20 个闭包节点,实测亚毫秒,远低于一次向量检索的开销。
1. 多父导致候选并列、输出抖动。「函数图像平移」的 LCA 候选可能同时来自概念侧(函数家族)和方法侧(图像变换),两次运行排名互换。解决:固定排序键(IC 优先、支持度次之、深度再次),并列候选一并返回给下游二次排序,而不是悄悄二选一。
2. 标签粒度漂移。「抛物线平移」「图形平移」「函数图像平移」三种写法都在入库。查询期补偿是灾难,必须入库期归一:alias 别名表 + 打标白名单,白名单外的标签直接进人工复核队列。
3. 断链导致过泛命中。运营新增考点时忘挂 PREREQUISITE,闭包直达顶层「初中数学」,输出一个正确的废话。监控规则:候选节点深度 depth < 2 时告警,并降级为输出证据列表而非单点结论。
4. 两题小样本失真。k=2 时支持度只能取 0.5 或 1.0,阈值过滤失去区分度。此时不做硬归因:输出 top-2 候选并携带各自证据,交给下游生成解释时并列呈现,让人做最终裁决。
把同类错题收敛到一个共同知识点,本质是三个工程决策的组合:DAG 化的知识组织、支持度容忍噪声、信息量压制过泛。聚类定类、LCA 定点,整条归因链路每一步都可回溯,这是纯生成式方案给不了的确定性。下一步我们会把 IC 的频率估计改成按学期滚动重估——学期初样本少,p 值失真会系统性拉高过泛节点的排名。
工程实践之外的思考:这套链路的价值不只在算法,而在于把「多题一起看」做成系统的默认动作,而不是依赖使用者自己有耐心翻错题本。我们团队把上面这套定位逻辑落地进了错题透镜;对开发者读者来说,本文的闭包加打分实现不过百余行代码,建议先在自己业务里跑通再谈优化。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。