
10 月上中旬,各地学校的第一次月考陆续出分,错题集中暴露的窗口又到了。孩子错题反复错怎么办?归因分析能给出「负负得正没吃透」「数与形没连线」这类结论,但新的问题随之而来:这个结论本身可信吗?补漏一周后,凭什么判定「补上了」?靠家长重讲一遍靠不住,靠一次考试运气成分太大。工程上有一个现成的思路可以搬过来——随机属性测试:把待检验的主张写成机器能执行的形式,让它自己去找反例1。下文是一套可直接运行的实现。
归因管线的输出通常是一段自然语言结论,比如「学生对负负得正的规则在字母代数式里没有完成迁移」。这段话人能读懂,机器没法执行——它既不能被验证,也不能被复测。
要让归因结论进入验收闭环,第一步是把它翻译成可检验的代数命题。初中代数的错因结论有个天然优势:绝大多数都能还原成「在某某前提下,某某式子的符号/大小关系如何」的形式。比如:
三种场景共用同一台引擎:给定前提、表达式、期望符号,在采样空间里找反例。找到反例的,定位的是「主张错在哪」;找不到的,支撑的是「补漏是否生效」。

数据结构刻意保持最小:一条主张就是三个函数加一句给人看的原话描述。
import random
from fractions import Fraction
class Claim:
"""一条可检验的代数主张:premise 成立时,expr 的符号恒为 sign。"""
def __init__(self, label, premise, expr, sign):
self.label = label # 家长和孩子能看懂的原话
self.premise = premise # (a, b, c) -> bool
self.expr = expr # (a, b, c) -> Fraction
self.sign = sign # +1 / -1 / 0
CLAIMS = {
# 月考第 2 题的正确命题:负负得正,c-a-b 恒为正
'sign_migration': Claim(
label='a<0<b 时,c-a-b 一定是正数',
premise=lambda a, b, c: a < 0 < b,
expr=lambda a, b, c: c - a - b,
sign=+1,
),
# 一个典型的学生错误主张:异号相加"绝对值变大"
'abs_misconception': Claim(
label='a、b 异号时,a+b 的绝对值比 |a|、|b| 都大',
premise=lambda a, b, c: a * b < 0,
expr=lambda a, b, c: abs(a + b) - max(abs(a), abs(b)),
sign=+1,
),
}表达式统一走 Fraction,是有意为之:初中错题的反例经常卡在分数上——浮点数会丢掉「恰好为 0」这类边界情形,而边界恰恰是反例最爱藏身的地方。
朴素随机采样对「和为 0」「异号抵消」这类结构命中率很低,直接均匀撒点会漏掉大量真反例。实现里做两层处理:采样函数按概率混合整数与真分数,并随机翻转符号;搜索函数额外维护一个边界池,按比例把 0 附近、±1、简单分数这些高危值直接投进采样流。
BOUNDARY_POOL = (
Fraction(1), Fraction(-1), Fraction(2), Fraction(-2),
Fraction(1, 2), Fraction(-1, 2), Fraction(3, 2), Fraction(-5, 2),
)
def draw_one(rng):
"""单值采样:三分之一概率取真分数,其余取整数,符号对半翻转,0 直接重抽。"""
if rng.random() < 1 / 3:
val = Fraction(rng.randint(1, 9), rng.randint(2, 9))
else:
val = Fraction(rng.randint(1, 9))
return -val if rng.random() < 0.5 else val
def draw_triple(rng):
"""一组 (a, b, c):按 20% 概率整组走边界池,其余走普通采样。"""
if rng.random() < 0.2:
return tuple(rng.choice(BOUNDARY_POOL) for _ in range(3))
while True:
trio = (draw_one(rng), draw_one(rng), draw_one(rng))
if 0 not in trio:
return trio
def hunt(claim, trials=2000, seed=7):
"""反例搜索:命中返回具体数组与轮次;跑满无命中则报告存活。"""
rng = random.Random(seed)
for rnd in range(trials):
a, b, c = draw_triple(rng)
if not claim.premise(a, b, c):
continue
val = claim.expr(a, b, c)
actual = (val > 0) - (val < 0)
if actual != claim.sign:
return {'verdict': 'refuted', 'round': rnd,
'counterexample': (a, b, c), 'value': val}
return {'verdict': 'survived', 'round': trials, 'counterexample': None}命中判定只有一行值得注意:actual != claim.sign——把「恰好为 0」单独处理成符号 0,避免它被吞进正负两侧。对初中命题来说,0 恰恰是最有教学价值的反例类型之一。
把第二节注册的两条主张分别送进引擎:
for key in ('abs_misconception', 'sign_migration'):
r = hunt(CLAIMS[key])
print(key, '->', r)
# abs_misconception -> {'verdict': 'refuted', 'round': 0,
# 'counterexample': (3, -1, 2), 'value': -2}
# sign_migration -> {'verdict': 'survived', 'round': 2000,
# 'counterexample': None}两条路径,两种辅导含义。
一击证伪。「异号相加绝对值变大」这条典型误解,第 0 轮就被 a=3、b=-1 击穿:|3-1|=2,比 max(3,1)=3 小,差值为 -2。反例的价值在于它是一份给孩子看的一次性证据——不用争论、不用讲道理,代进去算一遍,主张当场作废。孩子此时暴露的不是态度问题,而是绝对值的定义只建立了一半:|a+b| 从来不会超过 |a| 与 |b| 的和,这是三角不等式最朴素的形态。
两千轮存活。正确命题「a<0<b 时 c-a-b 恒为正」跑满 2000 轮无一命中——这正是月考第 2 题 B 选项为错的机器佐证。存活的命题恰好对应归因结论里「负负得正未迁移」的缺口:归因说孩子没吃透,验收命题说正确结论站得住,两者合起来,补漏动作就有了明确靶子——练「字母代数式里写出 -a-b 的符号」这一类,而不是回到题海。
引擎的输出还能再往前走一步:反例本身的形态,携带归因信息。
反例形态 | 暴露的概念缺口 | 对应辅导动作 |
|---|---|---|
依赖 0 或 ±1 的边界反例 | 定义层缺口(绝对值、相反数、0 的归属) | 回到定义重讲,配数轴可视化 |
普通整数即可击穿的命题 | 规则迁移缺口(正数规则错搬到负数/字母域) | 专题练"数字世界→字母世界"的对照 |
大量真分数才命中的命题 | 运算习惯缺口(回避分数运算) | 分数运算专项,Fraction 可视化分数过程 |
2000 轮存活 | 主张在采样空间内成立 | 转入按类补漏验收,隔周复测 |
也就是说,反例搜索不只回答「对不对」,还在回答「错在哪一层」——它与三层归因的层级体系天然对得上:边界反例指向概念核心层,迁移反例指向知识断层层,存活命题对应执行层的验收。
最后把边界说清楚,三点:
Q1:为什么不直接用 SymPy 符号求解,而要采样?
A:符号求解对「恒成立/恒不成立」给严格答案,但有两个代价:一是表达式稍复杂就需要假设声明(assume a<0),建 设成本高;二是它给的是冷冰冰的真假,给不出「a=3、b=-1 这组数一击致命」的具象反例。对辅导场景,反例的教学价值往往高于判定结论本身——孩子需要一个能亲手代进去的数。
Q2:随机采样的种子固定为 7,结果可复现吗?
A:可复现是有意设计:同一主张、同一种子,在任何机器上跑出同一条反例,家长群里讨论时才有共同事实。追求覆盖广度时可以批量换种子跑多轮,把每轮命中的反例按形态分类统计,正好喂给第五节的归因层级映射。
本文把「孩子错题反复错怎么办」推进到验收层:归因结论翻译成「前提+表达式+期望符号」的命题,分层采样加边界池搜索反例,错误主张一击证伪、正确命题轮次存活,反例形态再映射回归因层级决定辅导动作。带走三件事:其一,归因结论必须能被检验,否则只是又一个「粗心」式标签;其二,反例是给孩子看的最短证据,比讲十遍道理省力;其三,采样无反例不等于证明,含分段结构的命题要按分支补采样。
1 Claessen, K., & Hughes, J. (2000). QuickCheck: A Lightweight Tool for Random Testing of Haskell Programs. Proceedings of ICFP 2000, 268-279. DOI:10.1145/351240.351266
2 Hamlet, D., & Taylor, R. (1990). Partition Testing Does Not Inspire Confidence. IEEE Transactions on Software Engineering, 16(12), 1402-1411. DOI:10.1109/32.62450
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。