
9 月 21 日央视网讨论「人工智能+教育,如何加得更好」:AI 进课堂的速度很快,但家长要的不是孩子多一个查答案的入口,而是工具能回答那个真正的问题——孩子粗心错题多,根因是什么1。「粗心」在辅导语境里是最高频、也最无用的标签:它笼统、不可执行、无法转化成练习动作。对工程侧来说,这是一个可以严格定义的命题:给定多道被标注为「粗心」的错题,输出它们共同指向的具体知识漏洞。下文是一套在生产环境验证过的完整实现。
输入是聚类管线给出的错题簇:k 道题(实践中 k≈3~30),每题带有初步错因标注——「粗心」「看错题」「算错」这类口语标签;输出是一个图谱节点形式的知识漏洞,外加支撑证据题列表。
把这些口语标签直接拿来做交集,行不通,原因有二:
层级化记忆研究里有个反复被验证的现象:概念在语义网络中越靠上层,提取越轻松,但能带出来的细节也越少(Collins & Quillian, 1969)2。「共同根因」因此天然存在多个层级的候选,必须挑一个信息量适中的层级输出。所以第一步不是算交集,而是把口语标签翻译成图谱上的具体节点——这一步做完,问题就从 NLP 难题变成了图上定位问题。
知识组织沿用「章节 → 知识点 → 条件/性质」的骨架加前驱边,整体必须是 DAG 而非树:同一条法则可以从两条章节路径到达——「绝对值比较法则」既通向「有理数的大小比较」,也通向「绝对值」:
(:Chapter {name: "有理数"})
└─(:KnowledgeNode {name: "有理数的大小比较"})
└─(:Condition {name: "绝对值比较法则"})
(:Chapter {name: "数轴与绝对值"})
└─(:KnowledgeNode {name: "绝对值"})
└─(:Condition {name: "绝对值比较法则"})翻译层的要害是归因等级与图谱层级的对齐。三层归因体系里,Level 1 概念核心对应知识点层的法则类节点(「负数比大小要颠倒绝对值关系」),Level 2 知识断层对应条件/性质层的连接类节点(「绝对值」与「数轴上的距离」之间的连接),执行层问题则落到习惯与规范标签——层级对齐后,后续定位结果才可直接转译成辅导动作。
CAUSE_NODE_MAP = {
'粗心': None, # 禁止直接映射——口语标签必须先经过归因细分
'算错': '运算规则类节点',
'没掌握': '知识点层节点',
'看错题': '审题习惯标签',
}
def map_cause_to_node(cause: str, attribution: dict) -> str:
"""口语错因 → 图谱节点:优先用归因等级,拒绝透传口语标签。"""
if level := attribution.get('level'):
return attribution['node'] # Level 1/2 已由归因管线挂到具体节点
raise ValueError(f'口语标签 [{cause}] 未经过归因细分,拒绝入库')这道防线的价值在于:「粗心」永远不允许作为节点进入图谱。它只能是被拆解前的症状,不能是归因的终点。错题与节点是多对多关系,一张倒排表 (question_id, node, source) 承载,source 区分归因管线与人工复核,供支持度加权。
多道错题的共同根因,在图上的严格定义是:被全体(或近全体)错题的标签闭包覆盖、且信息量最大的最近公共祖先。DAG 上公共祖先是一个集合而非单点,需要自定义选择标准,工程实现三步:
第 1 步,铺闭包:把每道错题映射到的节点连同其全部祖先摊开成集合,一道题一个集合;
第 2 步,数票数:k 个集合逐节点计票,得票率不低于 τ=0.8 的节点才有资格当选——留两成余量吸收偶发的误打标;
第 3 步,比信息量:得票过线的候选里必然混着「数的运算」这类什么都能解释的泛节点——借鉴 Resnik 的信息量度量 4:IC(node) = -log p(node),p 用节点在历史标注路径中的出现频率估计,谁的 IC 高谁当选。
import math
from collections import deque
def build_closures(parent_of: dict[str, list[str]]) -> dict[str, frozenset]:
"""全图祖先闭包:逐节点迭代展开,命中缓存即合并,无递归栈。"""
cache: dict[str, frozenset] = {}
def closure_of(start: str) -> frozenset:
seen: set[str] = set()
queue = deque(parent_of.get(start, ()))
while queue:
cur = queue.popleft()
if cur in seen:
continue
seen.add(cur)
up = cache.get(cur)
if up is not None:
seen |= up # 缓存命中,整段祖先直接并入
else:
queue.extend(parent_of.get(cur, ()))
return frozenset(seen)
for node in parent_of:
cache[node] = closure_of(node)
return cache
def pick_root(closure_sets, ic_table, floor: float = 0.8) -> dict:
"""k 道错题的闭包集合逐节点计票,票数过线者按信息量取最大。"""
votes: dict[str, int] = {}
for cs in closure_sets:
for n in cs:
votes[n] = votes.get(n, 0) + 1
total = len(closure_sets)
pool = {n: c / total for n, c in votes.items() if c / total >= floor}
if not pool:
return {'verdict': 'unsolved', 'shortlist': []}
root = max(pool, key=lambda n: (ic_table.get(n, 0.0), pool[n]))
return {'verdict': 'solved', 'root': root, 'vote': pool[root]}用一组贴近真实结构的示例数据跑一遍(有理数错题簇,混入一道噪声题):
run_log = {
'cluster': 'CL-20260918-11', # 上游聚类送来的错题簇
'members': [ # 各题经映射层落到的图谱节点
{'id': 'T01', 'nodes': ['负数比较大小', '绝对值与符号']},
{'id': 'T02', 'nodes': ['数轴上的大小比较']},
{'id': 'T03', 'nodes': ['一元一次方程']}, # 误聚入簇的噪声题
{'id': 'T04', 'nodes': ['负数比较大小', '数轴上的大小比较']},
{'id': 'T05', 'nodes': ['绝对值与符号']},
],
'scored': [ # 票数过线后的打分候选
{'node': '有理数的大小比较', 'coverage': 0.8, 'ic': 1.97, 'depth': 2, 'ok': True},
{'node': '数的认识', 'coverage': 1.0, 'ic': 0.29, 'depth': 1, 'ok': False,
'why': '信息量过低,结论过泛'},
{'node': '一元一次方程', 'coverage': 0.2, 'ic': 2.55, 'depth': 3, 'ok': False,
'why': '覆盖率不足 0.8'},
],
'verdict': {'root': '有理数的大小比较', 'evidence': ['T01', 'T02', 'T04', 'T05']},
}输出里的 evidence 字段是给下游的绳索:共同根因必须能拽回到具体题目。家长看到的因此不是一句「孩子粗心」,而是「这几道题共同指向有理数的大小比较,证据如下」。
方案 | 核心原理 | 在线复杂度 | 主要失效场景 | ||
|---|---|---|---|---|---|
口语标签计数 | 「粗心」出现几次就报几次 | O(k) | 只能产出表象复述,无辅导价值 | ||
向量质心 + ANN | 簇向量求均值找最近考点 | O(log V) 检索 | 质心落入无考点区域,结果不可解释 | ||
图谱 LCA + IC(本方案) | 闭包计票 + 信息量排序 | O(k· | 闭包 | ),亚毫秒 | 断链/多父需治理(见下节) |
开销大头可以全部挪到离线:万级节点的闭包缓存一次预计算,Python 单进程百毫秒量级即可建完;图谱更新时只重算受影响节点的后代闭包,增量代价很小。线上要做的只剩集合运算——k 道题、每题平均 20 个闭包节点,单次合并微秒到亚毫秒级,比一趟向量检索还轻。树上 LCA 的 O(1) 经典解法(Bender & Farach-Colton, 2000)3 在多父 DAG 上并不适用,但这套计票方案的在线开销已经足够低,不值得为它引入树约束。
1. 归因等级挂错层,辅导动作跟着错。Level 1 的法则漏洞被挂到 Level 2 的连接节点上,输出会变成「补概念连接」,而孩子真正缺的是法则本身。治理:归因管线输出必须携带层级校验位,入库时节点 depth 与归因等级强一致校验,不一致进人工队列。
2. 多父节点让排序结果抖动。同一个知识点同时挂两条父链时,候选排名会在两次运行间互换。处理方式是把排序键写死——信息量第一、覆盖率第二、深度第三——并列候选全部透传给下游做二次排序,绝不在静默状态下替用户拍板。
3. 断边放大过泛结论。新知识点录入时漏挂前驱边,闭包会一路顶到「初中数学」这种根节点,输出一句永远正确的废话。防线是监控:候选深度小于 2 即告警,并降级为输出证据清单,不轻易给出单点结论。
4. 样本太小不硬算。簇内只有 2 道题时,覆盖率只有 0.5 和 1.0 两档,阈值形同虚设。这样的簇不做硬归因:给出前两名候选连同各自证据,解释文案并列呈现,把最终裁决留给人。
多道错题的共同根因定位,落地时就是三个工程决策:口语标签挡在图谱门外、知识组织 DAG 化、信息量压制过泛结论。归因定级、LCA 定点,整条链路每一步可回溯——这是把错题直接丢给大模型「总结共同考点」的生成式方案给不了的确定性,后者答案每次都变、无法取证,本质上是用幻觉换省事。
这套逻辑我们已经落地进错题透镜的多题归因链路:家长导入 3 道错题,系统输出的不是三份订正清单,而是一个收敛的知识漏洞和对应练习方向——「多题一起看」由此成为系统默认动作,而不是要求家长自己有耐心翻错题本。对开发者读者,本文闭包加打分的实现不过百余行代码,建议先在自己业务里跑通再谈优化;下一步我们计划把 IC 的频率估计改成按学期滚动重估,学期初样本少,p 值失真会系统性拉高过泛节点的排名。
1 央视网. 《人工智能进课堂 记者观察:人工智能+教育 如何加得更好》. 央视网, 2026-09-21
2 Collins, A. M., & Quillian, M. R. (1969). Retrieval Time from Semantic Memory. Journal of Verbal Learning and Verbal Behavior, 8(2), 240-247. DOI:10.1016/S0022-5371(69)80069-1
3 Bender, M. A., & Farach-Colton, M. (2000). The LCA Problem Revisited. LATIN 2000: Theoretical Informatics, LNCS 1776, 88-94. DOI:10.1007/10719839_27
4 Resnik, P. (1995). Using Information Content to Evaluate Semantic Similarity in a Taxonomy. Proceedings of IJCAI-95, 448-453.
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。