今天在 GitHub Trending 上看到一个有意思的项目:ARC-AGI-1 Task Generator(arc-task-gen)。它解决了一个越来越尖锐的问题——当公开评测集本身可能已经被「喂」进了模型的训练语料,我们该怎么客观地衡量一个前沿模型的真实推理能力?这个工具给出的答案是:自动批量生成与官方评测集「分布一致、内容全新」的私有 ARC-AGI-1 风格任务。

一、项目概述

ARC-AGI(Abstract and Reasoning Corpus,抽象与推理语料库)是 François Chollet 设计的一套衡量「小样本规则归纳 / 流体智力」的基准,由一个个网格变换谜题组成。公开评测集共 400 题,但业内普遍怀疑它已出现在网页爬取的训练语料中——于是高分可能来自「背过答案」而非真正推理。

arc-task-gen 的核心价值,就是生成一批私有、分布匹配、规则全新的任务,让模型不可能提前见过,从而把「公开榜分数」与「全新任务分数」对照,拆出「真推理能力」与「训练期先验熟悉度」。

  • 由 Pathway 团队发布,伴随论文 BDH-CQ(一种带循环潜在推理的模型)。150M 参数的 BDH-CQ 在公开 ARC-AGI-1 上达到 29.5% pass@2,单题推理成本仅 $0.0007,比 GPT-5.6 Luna (Low) 便宜约 11 倍。
  • 结果由《Attention Is All You Need》合著者 Łukasz Kaiser 等人独立复现,强调评测与基准鲁棒性。

核心特性:

  • 严格对齐官方评测集分布(网格尺寸、颜色数、训练/测试对数)
  • 三维新颖性过滤:去重 + 与官方题相似度过滤 + 结构合法性校验
  • 可对接任意 OpenAI 兼容端点(OpenAI / vLLM / Ollama / LM Studio)
  • 输出标准 ARC 格式 {"train": [...], "test": [...]},可直接喂给现有 ARC 评测框架

二、技术原理

2.1 为什么需要「分布匹配」而非随机生成

不能把网格尺寸、颜色数、对数这些属性独立随机抽样——否则会出现「2×2 网格却要容纳 9 种颜色」「3×3 画布放 7 对样本」这种现实中不存在的组合。源码 sample_joint_slots 的做法是:从同一个真实 eval task 整体抽样一个 slot(rows / cols / colors / n_train / n_test / anchor),从而保住这些属性之间的自然协方差。

def sample_joint_slots(eval_tasks: dict, n: int) -> list:
    task_ids = list(eval_tasks.keys())
    slots = []
    for _ in range(n):
        anchor_id = random.choice(task_ids)
        task = eval_tasks[anchor_id]

        # 网格尺寸只从 INPUT 网格抽:slot 设的是 input 目标,
        # 输出尺寸是模型「发明」的规则的涌现结果,事先不可控。
        input_grids, colors = [], set()
        for pair in task["train"] + task["test"]:
            for field in ("input", "output"):
                g = pair.get(field)
                if not g:
                    continue
                if field == "input":
                    input_grids.append((len(g), len(g[0])))
                for row in g:
                    colors.update(v for v in row if v != 0)

        rows, cols = random.choice(input_grids)
        slots.append({
            "rows": rows,
            "cols": cols,
            "colors": len(colors),
            "n_train": len(task["train"]),
            "n_test": len(task["test"]),
            "anchor": anchor_id,
        })
    return slots

2.2 新颖性过滤:嵌入余弦相似度

生成后,用 text-embedding-3-small 给每个任务的自然语言规则描述做嵌入,L2 归一化后余弦相似度退化为一次矩阵乘法(相似度矩阵 = 归一化矩阵 @ 归一化矩阵.T),整张两两相似度矩阵一次 BLAS 算出,无需逐个重算范数。

阈值校准非常讲究,全部基于官方集自身结构的实证分布:

  • 去重阈值 DEDUP_THRESHOLD = 0.80:同一批生成任务之间,相似度超过即视为近重复,用并查集合并成簇。
  • 官方相似度阈值 EVAL_SIMILARITY_THRESHOLD = 0.92:与官方评测集对比,只有「近乎逐字一致」的规则才拦截——因为与官方题「主题重叠」是允许的,真正要防的是复制。

注释里给出的校准依据:官方评测集中「真正不同」的两题最近邻相似度中位数 0.760、p95 0.879、最大 0.912。若设 0.85 会误删合理的全新题;0.92 恰好高于观测最大值。

def _unit_rows(vectors) -> "np.ndarray":
    """L2 归一化;归一化后余弦相似度 = 点积,相似度矩阵一次 matmul 完成。"""
    m = np.asarray(vectors, dtype=np.float32)
    norms = np.linalg.norm(m, axis=1, keepdims=True)
    np.maximum(norms, 1e-12, out=norms)     # 防护零向量
    return m / norms

def _embed(texts: list) -> "np.ndarray":
    resp = client.embeddings.create(model=EMBEDDING_MODEL, input=texts)
    ordered = [e.embedding for e in sorted(resp.data, key=lambda x: x.index)]
    return _unit_rows(ordered)

def find_duplicate_clusters(descriptions: dict, threshold: float = DEDUP_THRESHOLD) -> list:
    task_ids = list(descriptions.keys())
    if len(task_ids) < 2:
        return []
    m = _embed([descriptions[tid] or "no description" for tid in task_ids])
    sim = m @ m.T                       # 全 pairwise 余弦矩阵
    pairs = np.argwhere(np.triu(sim, k=1) >= threshold)
    # 用并查集把 >= threshold 的 pair 并入同一簇,返回 size >= 2 的组
    ...

2.3 收敛循环(convergence loop)

以往「串行两阶段」过滤会互相破坏保证:去重替换出来的题没做官方相似度检查,官方相似度替换出来的题没去做重检查。arc-task-gen 把两种过滤合并到同一轮:每轮同时算去重簇 + 官方相似度,取其并集删除,再补生成。循环里有三个关键设计:

  • 槽位回收(slot recycling):被删任务的 slot 会被回收而非重新随机抽样。因为删除与 slot 属性相关(可聚类的主题常来自特定 anchor),重抽会让幸存者分布偏移、偏离目标分布;回收把 slot 分布钉死在首轮抽样上。
  • 避免列表(avoidance list):把每轮所有「已生成过(无论保留或删除)」的规则描述喂给生成模型,明确要求其发明的机制不能与列表中任何一条雷同。eval 官方描述绝不放进生成提示词,以免泄漏评测集。
  • 停滞检测(stall detection):移除数本应几何式衰减;若连续若干轮移除数不降,说明某 slot 反复生成进同一簇,提前停止以省 API 成本。

2.4 分布一致性校验(sanity check)

run_sanity_check 对比生成集与官方集的输入面积均值/中位、行列均值、训练对数众数、单测试对占比(≥80% 判 PASS)。整体只校验输入维度——因为输出尺寸是模型所发明规则的涌现属性,刻意不约束,否则会得到一个无法归因的复合指标。

三、安装与快速开始

环境要求:Python 3,依赖 httpx、numpy、openai、matplotlib;支持任意 OpenAI 兼容端点。

git clone https://github.com/pathwaycom/arc-task-gen.git
cd arc-task-gen
pip install httpx numpy openai matplotlib   # 按源码实际依赖安装

配置环境变量(生成模型与嵌入模型均可替换成本地端点):

export OPENAI_API_KEY=sk-xxx                  # 本地服务可填任意占位符
export OPENAI_BASE_URL=http://localhost:8000/v1   # 可选:vLLM / Ollama / LM Studio
export ARCGEN_MODEL=gpt-5.6                   # 生成模型(默认 gpt-5.6)
export ARCGEN_EMBED_MODEL=text-embedding-3-small   # 新颖性过滤用的 embedding 模型

四、使用方法与实战

4.1 生成私有任务集

python generate_tasks.py --n 32

首次运行会自动下载 400 题 ARC-AGI-1 官方评测集并缓存(Apache 2.0,来自 fchollet/ARC-AGI)。生成目录结构:

data/generations/gen_YYYYMMDD_HHMMSS/
├── tasks.json          # 全部任务(标准 ARC {"train":[],"test":[]} 结构)
├── sanity_check.json   # 分布对比 + 规则描述 + 每轮过滤历史
└── separated/          # 每题一个 JSON,可直接进 ARC 测试界面

4.2 构建官方题描述缓存(启用官方相似度过滤)

python describe_eval_tasks.py        # 全量 400 题,结果缓存到 data/arc_agi_eval_descriptions.json

这一步用 LLM 把官方每题规则写成 1–2 句自然语言并缓存,供 generate_tasks.py 做跨集相似度过滤。若缓存缺失,脚本会跳过该检查并打印 WARNING——所以想开启官方相似度过滤,需先跑这一步。

4.3 可视化任务

python render.py data/generations/gen_xxx/ --out images/

每题渲染成一张 PNG:每个 train / test pair 占一行,左 input 右 output,test pair 用红色标签区分,方便人工抽检生成质量。

4.4 评测你的模型

tasks.json 是标准 ARC 格式,任何 ARC harness 都能直接消费——给模型看 train pairs,让它推 test output 即可。项目刻意把「给模型打分」留在 scope 之外,因为 tasks.json 已能被现有框架无缝加载。

五、常见问题与解决方案

Q1:运行报 HTTPSConnectionPool ... Read timed out?

抓取流程里就出现过 github.com 超时、自动回退 OSS Insight 兜底的情况,说明网络不稳时工具链有降级逻辑。本地生成任务本身不依赖 GitHub,但若首次下载官方评测集(codeload.github.com)超时,可手动下载 fchollet/ARC-AGI 的 tar.gz 解压到 data/ 对应位置。

Q2:生成任务数少于目标 N?

可能 API 失败超过 MAX_RETRIES(默认 3)轮,或收敛循环因停滞检测提前停止。脚本会打印 WARNING 并给出实际数量,可增大 --max-rounds 或检查 API 限流配置。

Q3:官方相似度检查被跳过?

提示 WARNING: data/arc_agi_eval_descriptions.json not found — skipping eval similarity check. Run describe_eval_tasks.py first。先跑 describe_eval_tasks.py 生成描述缓存即可开启。

Q4:输出尺寸与目标不符 / sanity check 报 WARN?

这是正常的——输出尺寸是模型所发明规则的涌现属性,脚本刻意只对「输入维度」做分布校验。WARN 表示偏离官方集 >40%,可检查锚点覆盖率,或调大 N 让统计更稳。

六、总结

arc-task-gen 把「评测集污染」这个悬在 ARC-AGI、乃至所有公开基准头上的问题,做成了一套工程化、可复现、分布对齐的解法。它的亮点不只是「能生成题」,更在于背后那套严谨的统计校准(阈值不是拍脑袋,而是基于官方集自身结构的实证分布定出来的)和收敛设计(槽位回收、避免列表、停滞检测)。如果你正在做模型评测、基准构建,或关心「agent 到底能不能真正推理」,这个仓库的 generate_tasks.py 单文件就值得反复读。