










仓库:https://github.com/TianyuCodings/NanoJev
解读日期:2026-09-20(对应仓库main分支最新状态)
License:MIT | Stars 1282 | Forks 158 | Open Issues 7
语言构成:Python ~2.0 MB(142 个 .py)| JavaScript ~351 KB(20 个 .mjs + 8 个 .js)| HTML/CSS ~49 KB
仓库总 blob 体积约 245.8 MB(其中research/下的 JSON 实验探针占了约 230 MB)
NanoJev 是 TypeSafe AI 商用模型 Jev 的"纳米级"开源复刻:一个 0.6B 参数的并行决策模型。输入「状态 + 问题 + 候选集」,一次前向直接输出完整概率分布,全程零输出 token 自回归解码。
要理解 NanoJev,必须先理解它的复刻对象。Jev 是 TypeSafe AI(创始人 Diogo Almeida,前 OpenAI 指令微调方向研究者)于 2026 年 9 月 15 日发布的第一代 System One Model,核心主张是:
"Think of Jev as a frontier-intelligence function call: unstructured state in, typed probabilistic decisions out."
它与传统 LLM 的差异可以归结为一张表:
| 维度 | 传统 LLM | Jev / System One Model |
|---|---|---|
| 输出 | 字符串,需解析+校验,存在幻觉与类型错误 | 类型安全的结构化值,数学上不可能类型错误 |
| 采样 | 串行逐 token | 并行,单次查询输出全部概率分布 |
| 训练方法 | RLHF / RLVR | RLCD(Reinforcement Learning for Calibrated Decisions) |
| 速度 | 3–329 秒 | 70–500 ms(快 40–200 倍) |
| 定价 | 输入 $0.2–10/MTok,输出约 5 倍 | 输入 $0.042/MTok,输出免费 |
| 置信度 | 自报 confidence 往往过度自信 | 每次输出自带校准概率 |
Jev 对外暴露三个判断原语:Noul(命题真假概率)、Choice(2–255 个候选上的分类分布)、Score(2–10 个有序等级的加权期望)。它的两个招牌演示恰好是 ViZDoom 射击 和 Wikiracing(维基百科跳转竞速)——这两个任务的共同点是"候选数量多、要求实时、不能幻觉"。
NanoJev 正是沿着这条路线做的复刻:它把 Frontier 级闭源模型换成了开源的 Qwen3-0.6B 骨干,把闭源的 RLCD 换成了可验证的分布监督与程序监督,并且诚实地在文档里写明"我们没有把普通交叉熵改名叫 RLCD"(research/pipeline_runbook_zh.md 第 6 节)。
NanoJev 不是又一个小聊天模型,它瞄准的是一个很具体的工程痛点——把 AI 做成软件可以直接调用的"模糊 if 语句":
train_toy_decisions.py 第 86–141 行),任何人可以逐行读完再决定要不要用。| 项 | 内容 |
|---|---|
| GitHub 账号 | TianyuCodings(用户名 Tianyu,ID 48460209) |
| 主页 | https://tianyucodings.github.io/ |
| 账号创建时间 | 2019-03-12 |
| 公开仓库数 | 11 | Followers 39 |
| Hugging Face | C-Tianyu(发布 NanoJev 模型与数据集) |
| 演示站点 | nanojev-dev.tianyuchen99.chatgpt.site(需授权访问) |
从仓库内容判断,作者具备三个明显特征:一是极强的实验严谨性偏好(全仓库到处是 SHA256 摘要、对冲写作、显式声明"这不是什么");二是中文思维 + 英文发布的双语工作流(README 出英文/简体中文双版,research/ 下有 30+ 篇中文研究笔记 _zh.md);三是有 GPU 服务器资源(所有 package subscript 为 capyubara-0:/home/rwang/openjev_codex_20260917/,A100 80GB)。
仓库创建于 2026-09-17,截至解读时仅 3 天,已经迭代出 20 次提交,节奏非常密集:
2026-09-17 仓库创建
2026-09-20 00:23 Add APPO expert supervision and unified game benchmarks
2026-09-20 03:46 Add verified Predict Position expert data and mixed SFT pipeline
2026-09-20 04:52 Record mixed expert SFT results and verified game evaluation
2026-09-20 05:46 Record verified Predict Position development deployment
2026-09-20 08:42 Record verified hard-task development deployment
2026-09-20 09:31 Clarify release tags and training experiment names
2026-09-20 10:22 Publish verified public model and dataset release ← 当前状态
一个 checkpoint 打四款游戏,274 例完整 held-out 测试集结果:
| 模型 | Maze | Snake | Basic | Predict Position |
|---|---|---|---|---|
| NanoJev(step-400, hard_lr1e5) | 4/10 | 8/8 | 128/128 | 27/128 |
| Jev(TypeSafe 商用) | 7/10 | 8/8 | 56/128 | 11/128 |
| Untuned Qwen3-0.6B | 2/10 | 0/8 | 56/128 | 11/128 |
几个值得注意的读数:
| 类别 | 要求 | 说明 |
|---|---|---|
| 操作系统 | Linux / macOS / WSL(Windows 未记录) | 全部路径与脚本按 POSIX 编写 |
| Python | 3.14.4(记录版本) | 见 requirements-toy.txt 注释行 |
| 核心依赖 | torch==2.14.0、transformers==5.17.0、safetensors==0.8.0、numpy==2.5.3 |
这是当时的记录版本,实际需按本机环境调整 |
| GPU(推理) | NVIDIA CUDA,DevicePredictor 默认 device_name="cuda:0" |
默认 BF16 精度;--precision fp32 可降级 |
| GPU(全参训练) | 单张 A100 80GB 足够训练 0.6B | README 明确"0.6B 在一张 80GB 卡上全参训练,8 卡用于独立对照" |
| 可选:ViZDoom | vizdoom==1.3.0 + gymnasium==1.3.0 |
只在需要跑射击环境/重采数据时装 |
| 可选:CPU 回放 | vizdoom==1.3.0 + Pillow==12.3.0 |
requirements-shooting-demo.txt |
| 可选:教师标注 | Node.js ≥ 22 + npm ci + AI SDK 7(ai@^7.0.105) |
仅当需要用付费 Jev API 采集软标签时;.env 需提供 AI_GATEWAY_API_KEY |
| 磁盘 | ≥ 3 GB | 检查点 bundle 约 4.5 GB(+ initialization 另一份);数据集约 880 MB |
| 网络 | 下载 Hugging Face 快照;C-Tianyu/NanoJev 与 C-Tianyu/NanoJev-Data 均公开免登录 |
带 unified-games-v1 tag |
⚠️ 重要提醒:
requirements-toy.txt里锁的是 torch 2.14 / transformers 5.17 这类远超当前常识补丁线的版本。这是作者在 2026 年实验机器上的真实冻结记录,不代表你必须一模一样。实际部署建议按本机驱动重写 torch 版本,但要意识到:换 tokenizers/transformers 主版本可能改变 subword 切分,从而改变 checkpoint 的输入分布。仓库自己也用SOURCE_MANIFEST.json(114 KB)固化了源码指纹来防这类漂移。
git clone https://github.com/TianyuCodings/NanoJev.git
cd NanoJev
python -m pip install -r requirements-toy.txt huggingface_hub
from huggingface_hub import snapshot_download
snapshot_download(
repo_id="C-Tianyu/NanoJev",
revision="unified-games-v1",
local_dir="checkpoints/NanoJev-unified",
allow_patterns=["best.safetensors", "config.json", "tokenizer/*", "backbone_config/*"],
)
snapshot_download(
repo_id="C-Tianyu/NanoJev-Data",
repo_type="dataset",
revision="unified-games-v1",
local_dir="data/NanoJev-unified",
)
这里有两个版本标签概念,务必区分清楚:
| 名字 | 含义 | 用途 |
|---|---|---|
unified-games-v1 |
Hugging Face release tag | 下载时用 revision="unified-games-v1" |
hard_lr1e5 |
训练实验名:hard(one-hot)动作目标 + 骨干 lr 1e-5 + 决策头 lr 1e-4 |
查训练配置、日志、对比实验时用 |
python scripts/serve_decisions.py \
--checkpoint-dir checkpoints/NanoJev-unified \
--web-root web --port 8765 --disable-native-triton
服务启动时会一次性加载权重,之后所有请求复用同一个 DecisionPredictor 实例。HTTP 接口:
GET /api/health → {"ready": true, "model_loaded_once": true, "provider_calls": 0}GET /* → 静态站点(web/ 目录,含 index.html、三 Panel 对照、Arcade)POST /api/evaluate → 核心推理端点请求体必须是严格形状:
{
"states": [
{
"id": "唯一非空字符串",
"state": "字符串 / JSON 对象 / 数组(不得为空)",
"questions": {
"qid1": {"type": "choice",
"instructions": "完整问题文本",
"criteria": {"opt_a": "语义描述", "opt_b": "语义描述"}},
"qid2": {"type": "boolean", "instructions": "..."},
"qid3": {"type": "score",
"instructions": "...",
"criteria": ["等级0描述", "等级1描述", "等级2描述"]}
}
}
]
}
服务端的硬性限流(serve_decisions.py 第 44 行):
if len(states) > 32 or len(questions) > 96 or paths > 256:
raise ValueError('Local demo limit:32 states,96 questions,256 candidate paths per request')
即:每请求 ≤ 32 个状态、≤ 96 个问题、≤ 256 条候选路径、body ≤ 2 MB,且禁止跨域(校验 Origin 的 netloc 必须与 Host 一致)。这些限制写成得很直白——它是 demo 级服务,不是生产网关。
路线 A:静态回放(无需 GPU、无需模型)
python3 -m http.server 8080 --bind 127.0.0.1 --directory web
# ViZDoom Basic: http://127.0.0.1:8080/dev/?autoplay=1
# Maze: http://127.0.0.1:8080/dev/side-by-side.html?autoplay=1#maze
这些播放的是仓库已录好的轨迹 JSON。README 特意强调 "This can also be played locally",因为托管的 development site 目前需要授权访问。
路线 B:实时推理:走上面的 serve_decisions.py,由前端 JS(web/app.js 34 KB、web/arcade.js 24 KB、web/side-by-side.js 20 KB、web/dev/*.js)调用 /api/evaluate。
research/pipeline_runbook_zh.md 给出了依赖顺序:
# ① 生成题目(纯 Python,无 API)
python3 scripts/build_toy_decisions.py --output-dir research/private_toy --seed 17
python3 scripts/build_workflow_decisions.py --output-dir research/private_workflows_v2 --seed 20260917
python3 scripts/build_game_decisions.py --output-dir research/private_games_v2 --seed 17
# ②-A 纯程序监督(零 API 成本,开源可复现)
python3 -c 'from pathlib import Path; Path("research/empty_teacher.jsonl").write_text("")'
python3 scripts/assemble_pipeline_dataset.py \
--workflows research/private_workflows_v2/all.jsonl \
--games-raw research/private_games_v2/all.jsonl \
--games-labels research/empty_teacher.jsonl \
--toy research/private_toy/all.jsonl \
--output-dir research/private_pipeline_gold_rebuild --freeze
# ②-B Jev 教师路径(需 Node ≥22 + 付费 API)
npm ci
node --env-file=.env scripts/label_decision_dataset.mjs \
--input-dir research/private_workflows_v2 --budget-usd 3 --concurrency 8 --max-failures 20
python3 scripts/assemble_pipeline_dataset.py \
--workflows research/private_workflows_v2/labeled.jsonl ... --output-dir research/private_pipeline_v2 --freeze
# ③ 训练(CUDA 机器)
python scripts/train_pipeline_decisions.py \
--input research/private_pipeline_v2/merged.jsonl \
--output-dir runs/v2_teacher_seed17 \
--model Qwen/Qwen3-0.6B --revision c1899de289a04d12100db370d81485cdf75e47ca \
--objective teacher --seed 17 --head-steps 12 --steps 600 \
--batch-questions 12 --microbatch-questions 4 --max-microbatch-tokens 6000 \
--max-length 512 --eval-every 50 --precision bf16 --disable-native-triton
# ④ 离线评测
python scripts/evaluate_pipeline_decisions.py \
--input runs/.../predictions_test.jsonl --output runs/.../test_metrics.json
python scripts/evaluate_navigation_v3.py --data-dir ... --checkpoint-dir ... --policy greedy
# ⑤ 把真实对局写入回放页
python3 scripts/build_demo_artifact.py --models <结果json...> --output web/demo_results.json
关键约束(作者反复强调):
--disable-native-triton 只是宿主环境的兼容回退,不是所有环境必需。--head-steps 是"先只训决策头再放开通骨干"的阶段开关。--output-dir,不能覆盖。python3 -m unittest discover -s scripts -p test_question_contract.py -v
这套测试用可逆字符 tokenizer + 真实 prepare_examples,覆盖:传输 ID 不变性、无关问题/状态隔离、Boolean 边界 criterion、Choice 名称/描述、Score 索引/邻居排除、期望分数读出。不加载 checkpoint、不 import PyTorch、不联网——非常适合 CI。
NanoJev
├── 输入契约层 predict_toy_decisions.py 三种原语 + 严格 JSON 校验
├── 模型层 train_toy_decisions.py DecisionModel(骨干 + 两种决策头)
├── 训练层 train_pipeline_decisions.py / train_unified_games.py
├── 环境层 unified_grid_envs.py(Maze/Snake) unified_doom_env.py(ViZDoom)
├── 数据采集闭环 unified_game_pipeline.py cases → rollout → dataset
├── 时序差分目标 unified_td.py n-step bootstrap(γ=1)
├── 专家蒸馏 run_appo_supervision.py / sonic_predict_*.py(APPO 教师)
├── 契约对齐教学脚本 scripts/teachers.mjs Jev 原生概率 / LLM 硬标签两条路线
├── 服务层 serve_decisions.py stdlib HTTP + 静态站
├── 前端 web/ 4 套交互演示
└── 审计资产 research/ + results/ 大量 JSON 回执与符号 Dockerfile
核心能力可归纳为五条(README "What NanoJev does"):
DecisionModel.forward这是整个项目最精华的 40 行,位于 scripts/train_toy_decisions.py:104-141:
def forward(self, examples, pad_token):
# ① 把所有 example 的所有候选路径摊平成一张大 batch
paths = [ids for ex in examples for ids in ex['leaf_tokens']]
lengths = torch.tensor([len(ids) for ids in paths], device=device)
width = int(lengths.max())
tokens = torch.full((len(paths), width), pad_token, dtype=torch.long, device=device)
for i, ids in enumerate(paths):
tokens[i, :len(ids)] = torch.tensor(ids, device=device)
attention = torch.arange(width, device=device)[None, :] < lengths[:, None]
# ② 唯一一次骨干前向
hidden = self.backbone(input_ids=tokens, attention_mask=attention,
use_cache=False).last_hidden_state
# ③ 取每条路径最后一个有效 token 的隐状态作为该候选的表示
leaves = hidden[torch.arange(len(paths), device=device), lengths - 1]
# ④ 重新 scatter 回 [B, kmax, H]
kmax = max(len(ex['candidate_ids']) for ex in examples)
h = leaves.new_zeros((len(examples), kmax, leaves.shape[-1]))
valid = torch.zeros((len(examples), kmax), dtype=torch.bool, device=device)
offset = 0
for i, ex in enumerate(examples):
n = len(ex['leaf_tokens'])
h[i, :n] = leaves[offset:offset+n]
valid[i, :len(ex['candidate_ids'])] = True
offset += n
# ⑤ LayerNorm → 共享标量头 → 每个候选一个 logit
h = self.norm(h)
z = self.scalar(h).squeeze(-1).float()
...
return torch.stack(out).masked_fill(~valid, -1e9), valid
读懂这段的关键几点:
lengths - 1 取值技巧:因为每条路径尾部都拼了 EOS(见 §3.3),最后一个 token 的隐状态天然是"读完整个 (state, question, candidate) 之后的总结位"。这就是"零解码输出"的实现基础——用位置代替生成的 token。use_cache=False:明确放弃 KV cache,因为这是打分任务而非生成任务。masked_fill(~valid, -1e9):不同题的候选数不同,padding 位必须被 logits mask 掉,保证 softmax 分母只覆盖真实候选。prepare_examplesscripts/predict_toy_decisions.py:85-119。训练与推理共用同一份 token 拼写规则(注释原话:"逐段 encode、候选文本和 EOS 均精确遵循 train_toy_decisions.load_examples"),这点非常重要——训练和推理的 token 不一致是这类项目的头号杀手。
segments = [f"State:\n{row['state']}\n",
f"Question type: {typ}\nQuestion:\n{q['instructions']}\n"]
if typ == "boolean" and "criteria" in q:
for key, label in (("false", "False"), ("true", "True")):
if key in q["criteria"]:
segments[1] += f"{label} criterion: {q['criteria'][key]}\n"
prefix = sum([tokenizer.encode(t, add_special_tokens=False) for t in segments], [])
leaves = [prefix + tokenizer.encode(f"Candidate:\n{t}\nDecision:", add_special_tokens=False)
+ [tokenizer.eos_token_id] for t in texts]
于是每条候选路径长这样:
State:
<状态文本>
Question type: choice
Question:
<指令文本>
Candidate:
<候选key>: <候选语义描述>
Decision:<EOS>
三种题型的展开策略各不相同:
| 题型 | 候选 ID | 候选叶子文本 | 备注 |
|---|---|---|---|
boolean |
["false","true"] |
"The proposition is true." |
固定 false→true 顺序,可选 true/false criterion 文本 |
choice |
criteria 的 key(2–255) |
f"{key}: {description}" |
名称与描述都是语义输入 |
score |
["0","1",...,"k-1"](2–10) |
仅该等级自己的描述 | 绝不注入序号或相邻等级——这是刻意为之 |
最后一行是值得单独拎出来的设计。docs/TYPESAFE_CONTRACT.md 明确写道:Score criteria 必须是 {isOrdered Array},每一项被独立评估,数值只来自数组位置。如果 Leo 在叶子文本里写"等级 2:XXX",模型就会学到位置先验而不是语义。NanoJev 严格遵守,并且为此写了专门的单元测试(Score index/neighbor exclusion)。
还有一个容易被忽略的性质:state_id 和 qid 都不出现在 leaf_tokens 里(TYPESAFE_CONTRACT.md 第 8 行确认)。这意味着重命名问题 ID 不会改变任何候选路径的输入——保证了"传输标识不影响语义"的不变性,同时也避免了模型走捷径记忆 ID。
train_toy_decisions.py:86-102 定义了两种打分方式:
class DecisionModel(nn.Module):
def __init__(self, backbone, set_head):
self.backbone = backbone
hidden = backbone.config.hidden_size
self.norm = nn.LayerNorm(hidden)
self.scalar = nn.Linear(hidden, 1) # 共享标量头
nn.init.normal_(self.scalar.weight, std=0.02) # 非零初始化避免第一步 dead gradient
nn.init.zeros_(self.scalar.bias)
self.set_head = set_head
if set_head == 'attention':
self.set_project = nn.Linear(hidden + 1, 128)
self.set_attention = nn.MultiheadAttention(128, 4, dropout=0.0, batch_first=True)
self.set_output = nn.Linear(128, 1)
nn.init.zeros_(self.set_output.weight) # 仅最后残差投影零初始化
nn.init.zeros_(self.set_output.bias)
(a) Scale 路径(所有题型都走):每条候选路径独立编码 → LayerNorm → 共享 Linear(hidden,1) → 一个标量 logit。候选之间互不 attention,纯独立打分 + 共享 softmax 归一化。
(b) Set-Attention 路径(仅 choice 题型,可选):
if self.set_head == 'attention' and len(choice):
log_k = valid[choice].sum(-1).float().log()[:, None, None].expand(-1, kmax, 1)
u = self.set_project(torch.cat([h[choice], log_k.to(h.dtype)], dim=-1))
mixed, _ = self.set_attention(u, u, u, key_padding_mask=~valid[choice], need_weights=False)
delta = self.set_output(torch.tanh(u + mixed)).squeeze(-1).float()
z = z.index_add(0, choice, delta)
这是全项目最有技术品味的一处,四个细节值得学习:
log_k 条件化:把候选数 log k 拼进每个候选的向量。这让同一个集合层能感知"当前是 4 选 1 还是 255 选 1"——候选数不同时 softmax 的温度压力天差地别,不加这个条件,模型无法区分。self_attention 没有位置编码,因此是真正的集合(置换等变)操作——候选顺序变化不改变结果(配合 peer,这正好满足 Choice 的置换不变要求)。set_output 的 weight/bias 全零 → 训练刚开始时 delta ≡ 0 → 模型严格退化为纯 Scale 模型。这是一个标准的"warm-start from a simpler model"技巧,保证集合层只能渐进增加表达能力,不会在第一步就把已训好的表示搅乱。上游 set_project / set_attention 仍是非零初始化,避免 dead gradient。index_add 而非直接赋值:delta 只作用于 choice 行,boolean/score 行不受影响。注意 Boolean 被刻意排除在集合注意力之外——因为 Boolean 只有"一条语义路径 + 一个标量",没什么可集合交互的。Boolean 的输出尤为巧妙:
if ex['type'] == 'boolean':
out.append(F.pad(torch.stack([z[i, 0] * 0, z[i, 0]]), (0, kmax-2)))
它复用同一个标量头,构造 logits [0, z]。softmax([0, z]) = [1/(1+e^z), e^z/(1+e^z)] = [1-σ(z), σ(z)],等价于 sigmoid。这种"用统一的 softmax 归一化覆盖所有题型"的做法,让三种原语共享同一套损失与反向传播路径,代价是 Boolean 浪费了一条候选路径的计算。
train_pipeline_decisions.py:223-236:
def grouped_target_loss(logits, examples, objective):
losses = []
for z, ex in zip(logits, examples):
k = len(ex["candidate_ids"])
target = target_for(ex, objective)
if target is None:
raise ValueError(f"Missing/quarantined {objective} target entered training: {ex['id']}")
t = torch.tensor(target, dtype=torch.float32, device=z.device)
# 只在这道题内归一化;padding 候选的 logits 绝不进入损失
losses.append(-(t * z[:k].float().log_softmax(-1)).sum())
return torch.stack(losses)
文档里用公式强调:
p = softmax(z)
L(q, p) = -Σ_i q_i · log p_i
∂L/∂z_i = p_i - q_i
关键在于注释那句:"完整候选集合一起计算分母,不能把 20 个候选拆成 20 个独立二分类 loss"。这是一个非常容易写错的地方——很多人会下意识写成 k 个 binary cross-entropy,那样每个候选的概率不再互相竞争,输出也就不再是合法分布,破坏了模型的核心价值(可直接采样/排序的完整分布)。
配套的 microbatch 打包函数也贯彻了这一原则(train_pipeline_decisions.py:237-256):
def pack_complete_questions(examples, max_questions, max_tokens):
"""预算按候选路径数 × 最长 padding 路径计,绝不拆分候选"""
if max_tokens and n * size > max_tokens:
raise ValueError(f"Complete question {ex['id']} requires {n*size} padded tokens, over budget {max_tokens}; \
increase explicit budget or use a separately implemented gradient-cache path, never split its softmax or truncate candidates")
microbatch 只能按整题切分,梯度累积后再更新。 宁可报错也不静默截断或拆分 softmax。同一精神也体现在 predict_toy_decisions.py:113-115:
if largest > max_length:
raise ValueError(f"{row['id']}:{qid} 候选路径为 {largest} token,超过 max_length={max_length};未截断输入")
target_for(train_pipeline_decisions.py:207-220)支持四类监督目标,并且清晰地区分它们的语义地位:
| objective | 含义 | 语义地位 |
|---|---|---|
observed_outcome |
真实观测到的最终成败 | 唯一的事实,硬 one-hot |
gold_distribution |
程序/解析真值(如井字棋 minimax、BFS 最优策略) | 约定真值,不是观测结果 |
teacher |
Jev API 返回的舍入概率分布 | 代理标签,可能失效 |
gold |
argmax 硬标签 | 便于对齐 accuracy 的兼容argmax |
仓库有一条很重要的自律:"Jev 数值记录为舍入概率,不叫 logits"(runbook 第 90 行)。它不会把 API 给的 4 位小数概率伪装成 logits。
Quarantine(隔离)机制是这套数据管线的最后防线。以 train_toy_decisions.py:60-73 为例:
observed = row['teacher']['native_probs'][qid]
if set(observed) != set(ids): raise ValueError(...) # 候选必须完全匹配
if any(not math.isfinite(v) or not 0 <= v <= 1 for v in raw): raise ValueError(...)
decimals = row['teacher'].get('rounding', {}).get('probabilityDecimals')
scale = 10 ** decimals if isinstance(decimals, int) else None
if scale and any(abs(p*scale-round(p*scale)) > 1e-6 for p in raw):
raise ValueError('Teacher probabilities violate declared decimal precision')
unit_sum = sum(round(p * scale) for p in raw) == scale if scale else abs(sum(raw)-1) < 1e-6
teacher_ok = unit_sum and sum(raw) > 0
target = raw if teacher_ok else None # ← 不合格就置 None,不猜、不修补
不合格的目标不是报错中断,也不是用 fallback 修复,而是标记 target_transform = 'quarantined' 并置空,target_for 返回 None,最终在进入 loss 前由 grouped_target_loss 抛错拦截(如果它误闯进来)。README 提到 10,898 条训练题中 10,893 条通过目标有效性过滤,那 5 条被隔离且原记录保留。
unified_game_pipeline.py 是整个数据质量体系的中枢,三个子命令构成闭环:
cases → rollout → dataset
(1) cases:防 split 泄漏 —— 含 D4 对称性归一
def environment_group(case):
if spec["task"] == "maze":
from scaled_maze import make_maze, source_group_id
initial = spec.get("initial_state") or make_maze(...)
return source_group_id(initial) # ← 把 8 个 D4 对称/旋转归一化成同一个 group id
source_group_id 的实现在 scripts/game_tasks.py:
def source_group_id(state):
"""把所有棋盘对称性归为一组;网格组还忽略起点"""
maps = []
for i in range(8):
transformed = transform_state(state, i)
maps.append((tuple(transformed['goal']), tuple(map(tuple, transformed['walls']))))
goal, walls = min(maps) # ← 取字典序最小的规范形式
return f'grid:{state["size"]}:goal={goal}:walls={walls}'
然后在 validate_cases:
group = environment_group(case)
if group in groups and groups[group] != case["split"]:
raise ValueError("An underlying environment group crosses splits")
这一防护是全项目最值得借鉴的设计之一。 大多数 RL / 决策数据集做"同分布划分"时只去重精确相同的样本,而这里识别出了旋转/镜像/同图不同起点本质是同一个环境:如果测试集的图只是把训练图转了 90°,那测出的泛化是假的。明确处理这一点,是作者画了大量务实代价换来的诚实。
(2) rollout:冻结策略 + 明确的"外部截断不能冒充失败"
if transition["truncated"]:
raise RuntimeError("External truncation cannot be labeled as failure")
这句话直接切断了 RL 里一个极常见的造假路径:把"跑到超时"伪装成"任务失败"会系统性低估当前状态的价值,导致学到的 Q 函数 pessimistic 且不可比。NanoJev 要求环境的"截止时间"必须是任务自身的终止条件(finite_task_deadline_v1 契约),而不是数据采集器强行截断。
策略身份被完全冻结成一个 sha256:
def policy_identity(args):
result = {"engine": args.engine, "controller": args.controller, "epsilon": args.epsilon,
"sampling_seed": args.seed, "temperature": 1.0,
"source_sha256": source_hashes(),
"tie_break": "lexicographic_first",
"environment_contract": "finite_task_deadline_v1"}
连同源码本身的 SHA256 一起哈希——这意味着任何与控制环节相关的代码改动都会导致 policy id 变化,从而不可能把两个不同采集策略产生的数据混进同一个 dataset(make_dataset 会检查 episode["continuation_policy_id"] != policy_id 并报错)。
行为策略也是一个整洁的实现(behavior_distribution,第 183-197 行):ε-greedy 混合 + 确定性字典序打破平局(注释写明"确定性 tie breaking 是冻结控制器的一部分"——避免 *argmax 依赖实现细节)。
(3) dataset:观测结果的严格校验
if type(episode["success"]) is not bool or episode.get("final_info", {}).get("success") != episode["success"]:
raise ValueError("Episode success must be a Boolean matching terminal environment information")
if steps:
terminal = steps[-1]
if not terminal["terminated"] or terminal["truncated"] or terminal["info"].get("success") != episode["success"]:
raise ValueError("Observed outcomes require a consistent genuine terminal transition")
if any(step["terminated"] or step["truncated"] for step in steps[:-1]):
raise ValueError("An episode cannot continue after termination or external truncation")
以及一个非常敏锐的防偏差条款:
if args.role == "outcome" and state_limit != 0:
raise ValueError("Outcome data must retain every executed transition: \
thinning by final episode length biases future-success labels. Use --max-states-per-episode 0.")
对 outcome 数据做"按回合长度稀疏化"会引入幸存者偏差——保留的都是最终成功/失败的那条链上的某些点,破坏"未来成功"标签的因果一致性。policy 角色默认可以抽取 24 个状态,outcome 角色必须保留全部。这类"写下为什么出错"的注释在工程上极有价值。
还要注意:policy 数据用标签前会拿 API 日志逐条对账(make_dataset 第 491-499 行),校验 input_sha256、state、questions、native_probs 四项全部一致,否则报错。教师标签必须能追溯到某次真实成功的 API 调用回执——这直接杜绝了"随手编一笔 API 概率"的可能。
(4) 环境本身的"只给可见信息"纪律
get_table_of_contents:unified_doom_env.py 文档开头就写明:
Only visible label bounding boxes and player health/ammo/pose enter
state. Kill/damage counters are evaluation information, not policy observations.
同理 unified_grid_envs.py 开头:
hidden geometry and route oracles never enter observations
BFS / minimax 只用于生成标签和离线评分,绝不混入学生输入。这是很多"用专家数据蒸馏"项目会犯的错——把最优动作隐式编码进观测,学生学到的是抄答案。
另外 unified_grid_envs.py 里的 Maze 有一个有趣设计:候选动作是"当前格尚未尝试过的方向"(允许撞墙),并且提供基于已验证边的自动重定位 macro,但 macro 消耗真实物理步数。这样"探索见闻"(visited / open-edge masks) 自动进入观测,而不需要外部作弊。
teachers.mjsimport { experimental_evaluate, generateText } from 'ai';
jev 路由:调用 experimental_evaluate,保留完整原生概率映射与声明的舍入精度(probabilityDecimals),这是唯一能拿到真正"原生概率"的路径。注意它的类型拼写是 SDK 的 boolean,而非直接 TypeSafe API 的 noul——TYPESAFE_CONTRACT.md 明确标注了这个差异。llm 路由:把 {state, questions} JSON 丢给生成式模型,校验 hard JSON 标签。文档坦率指出这条路会把 query id、全部问题和全部 Score 等级同时暴露给模型,因此不满足本地推理的输入隔离性——它只是另一个标签接口,不是契约测试。unified_td.py 与 APPO 专家监督unified_td.py 为冻结的行为策略生成有限的 n-step 目标(γ=1),核心是:
def weighted_bootstrap(action_values, behavior_probs):
"""Return sum_a recorded_pi(a|s) Q_target(s,a); never maximize or refit pi."""
注意 never maximize——它对记录的 ε-greedy 行为分布做加权,而不是取 max。这是 off-policy 评估/学习里最基本的正确性要求:如果你的目标是 max-Q,那就隐含假设了行为策略会跟着改变,破坏 importance weighting 的一致性。
APPO 专家监督(docs/APPO_SUPERVISION.md)是 Basic 从 56/128 冲到 128/128 的原因:
(README 中的流程图,此处文字化) 同一 seed + 同一执行动作 ├─→ 原生 RGB 环境 + APPO(edbeeching/doom_basic_1111,含归一化与循环记忆) └─→ 标准环境(320×240 无 HUD 的可见状态文本) ↓ APPO 的动作分布与 argmax(第八步时使用预训练前的分布) ↓ 配对的 Basic 监督记录(hard / soft 两种 view,state_id 与候选集完全相同) ↓ 合并保留的 Maze / Snake / Predict Position 原样记录 ↓ 统一 SFT(hard CE 与 soft CE 对照) ↓ 配对回合基准 + 动作策略拟合度检查
关键细节:loss weight 设计得很讲究——每个任务占 1/3,射击内部 Basic:PredictPosition 保持 453:517 的原始有效样本比,所以扩大 Basic 语料不会挤掉其他任务的权重。结果:hard 目标 128/128(8-tick 版 126/128),soft 目标作为对照。
serve_decisions.py 虽然只有 74 行,但几个细节体现了"防守 rtlawk"的习惯:
if origin and urlparse(origin).netloc != self.headers.get('Host'):
raise ValueError('Cross-origin requests are disabled')
...
except (ValueError, TypeError, KeyError) as exc:
self.send_json(400, {'error': str(exc)})
except Exception:
self.send_json(500, {'error': 'Local model inference failed; inspect the server process. \
No teacher fallback was used.'}) # ← 明确声明"没有 teacher fallback",防止静默拿别的答案凑数
raise
以及日志:
def log_message(self, fmt, *args):
# 只记录 HTTP method/path/status;不记录请求状态与凭证
① 架构极简、可逐行审计。 决策模型本体只有 DecisionModel.__init__(16 行)+ forward(38 行)。没有 custom CUDA kernel、没有 MoE、没有复杂 loss 组合。0.6B 的参数让它在单张消费级/数据中心 GPU 上可与 cacheble 全参训练。
② 工程诚实度远超同类项目。 这是我最想强调的一点,具体体现在:
SOURCE_MANIFEST.json 114 KB;每次 rollout 不仅记录 상품 policy id 还记录源码本身的哈希)source_hashes(),源码不一致直接拒绝(unified_game_pipeline.py:111)TYPESAFE_CONTRACT.md 列出自己与官方不符的 9 处并标注"遗留工作"scripts/test_*.py,且 test_question_contract.py 可在无 GPU / 无网络 / 无 checkpoint 环境下跑(非常适合 CI 门禁)③ 零解码输出,形态正确。 一次前向拿到完整分布,rank/select/sample 全部由外部代码完成。这天然消除了幻觉与类型错误的可能——模型的输出空间在请求里就被 criteria 完全限定。
④ proper scoring rule 贯彻到底。 整题 softmax、禁止拆成 k 个二分类、microbatch 只切整题、宁可报错也不静默截断。这让输出的概率具有可比较性。
⑤ 完整的可复现资产。 模型 / 数据集 / 初始化权重 / # training recipe 全部上 Hugging Face 并拨 unified-games-v1 tag,每个文件通过远程 LFS SHA256 或 Git blob 校验,13 个代表文件做匿名下载字节校验,checkpoint 还做 byte-range 下载校验。docs/UNIFIED_DEVELOPMENT_RELEASE.md 记录了全部达到这一状态的得到,包括丑陋但重要的部分("早先 release 通过 legacy-before-unified-games-v1 tag 保留")。
⑥ 三方对照协议公平。 NanoJev / Jev / Untuned Qwen 使用相同的 case spec、seed、可见状态历史、候选动作集、ε 与采样种子(configs/sonic_unified_sft_v1.json 的 paired_comparison 字段)。这是极少见的、能让人信服的模型对比。
① 与官方 TypeSafe 契约仍有 9 处差距(作者自列,我归纳最关键的几个):
type: "boolean",官方是 type: "noul",本地 noul 目前会直接校验失败legend 与 confidence,Choice 缺 confidencestr(...),带单引号、True/None、且依赖插入顺序最后一条尤其危险。TYPESAFE_CONTRACT.md 明确警告:"Current object encoding uses single quotes, True/None, and insertion order; it is not canonical JSON. Changing it also changes checkpoint inputs." 也就是说数据结构顺序一变,模型行为就变,且这种变化不可解释。
② flat 路径重复计算前缀,未实现共享前缀内核。 forward 里每条候选路径都从 State: 重新拼起。对于 255 个候选的 Choice,前缀(state + question)会被算 255 遍。文档坦诚:"Inference has no output token decoding; the current flat path recomputes prefixes, and the production shared-prefix kernel is not yet implemented." README roadmap 也把 "Shared-prefix inference and larger candidate batches" 列为未打勾项。这是当前最大的性能瓶颈,也是它与商用 Jev 效率差距的主要来源。
③ 长时程能力明显不足。 Predict Position 27/128、Maze 4/10 (< Jev)。它本质是一个单步判别模型:擅长原子性的判断题,不具备真正的规划能力。"何时转向、何时等待、何时开火"这类需要跨步信用分配的能力是靠 SFT 硬塞进去的,没有真正的 RL 迭代。
④ 没有真正实现 RLCD。 作者在 pipeline_runbook_zh.md 第 6 节明确:
本轮一手核查没有取得其 reward、网络或训练数据配方。现在实现的是可验证的分布监督和程序监督;没有把普通 CE 改名为 RLCD。
research/rlcd_theory_zh.md(25 KB)与 core_features_rlcd_zh.md(19 KB)是理论分析,不是实现。README roadmap 里 "RLCD post-training for broader long-horizon tasks" 未打勾。这意味着 NanoJev 复刻的是 Jev 的形态,不是 Jev 的训练方法。
⑤ 依赖付费 API 的血统问题。 虽然存在纯程序监督的零 API 路径,但 README 主推的最佳成绩(128/128 Basic)来自 APPO 专家监督(开源),而 Jev 对比视频使用的却是 v3_teacher_coords_multi_seed17——一个用 Jev 分布监督训练出来的学生(pipeline_runbook_zh.md 第 7 节)。更微妙的是:
V3 五组都从同一个 V2 teacher checkpoint 继续训练,优化器重新初始化。因此这里的 gold 指 V3 更新目标,不表示权重谱系从未用过教师。
文档虽然说得很清楚,但这也意味着:你今天完全从零复现出来的模型,不会等同于 README 里宣传的那个模型。这是一个任何想用这个项目的人都需要知道的诚实边界。
⑥ 服务层是 demo 级。 Python 标准库 HTTPServer 单线程、无任何鉴权、无批处理队列、无 GPU 利用率优化、硬限 32/96/256。放进生产需要重写整整一层。
⑦ 任务域极其有限。 四款自写/包装的小游戏 + (研究阶段的)井字棋与网格导航。没有图像输入(Doom 走的是"可见物体的文字化 bounding box",不是像素),也没有真正的多模态。Wikiracing 这类官方演示的高基数用例完全没有。
⑧ 仓库信息组织混乱。 66 个 Markdown、186 个 JSON(其中 research/nanojev_comparison_public.json 单独 21 MB)、142 个 Python。所有测试都以 scripts/test_*.py 形式散落在 scripts/ 而没有独立 tests/ 目录;docs/ 和 research/ 的界限模糊;存在 README.md / README.en.md(内容完全相同的 9058 字节)/ README.zh-CN.md(8715 字节)三份,且 zh-CN 版与英文版内容并不完全同步。新人要找到"我该从哪开始"需要不少时间。
⑨ 概率校准性未经验证。 TYPESAFE_CONTRACT.md 最后一句很重要:"A normalized distribution alone does not establish empirical probability calibration." 输出加起来等于 1 是一回事,这个 0.7 是否真的意味着 70% 会成真是另一回事。官方 Jev 用 RLCD 专门解决这件事,而 NanoJev 目前尚未对这一维度做 calibration 评估(ECE / 可靠性图)。
⑩ 版本号超前于常识。 torch==2.14.0 / transformers==5.17.0 / Python 3.14.4 在当下偏先进。这本身不是错(作者诚实记录了冻结环境),但会给实操者带来困惑。
结合仓内思想的原始定位("AI 驱动的 workflows / 智能 if 语句")和 NanoJev 的实际能力边界,我给出分层判断:
✅ 现在就能用(形态匹配、规模匹配)
| 场景 | 为什么适合 | 落地形态 |
|---|---|---|
| 工作流 / 智能分支 | 官方定位就是"手写逻辑过于脆弱时的模糊判别器":分类、路由、打分、抽取、分支 | Choice/Boolean 原语替换 if-else 串,配合阈值门控 |
| 告警降噪 / 工单分流 | 低延迟 + 有置信度输出,天然支持"概率 > 0.8 才自动处理,否则转人工" | Boolean + 阈值路由 |
| LLM 输出的校验与护栏 | Jev 官方首推用例之一;0.6B 模型成本极低,可做第二道卡 | Score 打分 + Boolean 越狱/幻觉检测 |
| 批量数据的 map-reduce 判别 | 并行 batching 是它的本质,天然适合离线批量打标 | pack_complete_questions 大 batch 直接吞吐 |
| 教育与算法教学 | 全项目代码可读、0.6B 可本地训练、且公开了完整的失败案例与负面结果 | 讲授 proper scoring rule、off-policy 陷阱、split 泄漏防护的最佳教材 |
⚠️ 有潜力但需要补 work(能力尚缺)
| 场景 | 缺口 | 需要做什么 |
|---|---|---|
| 实时出价 / 推荐重排 | 缺 calibration;单线程服务扛不住 RTB 的 QPS | 先做 ECE 校正 + Platt/isotonic scaling,重写服务层为异步批处理网关 |
| 机器人 / 工业调度的决策层 | 长时程能力弱、无多模态 | 需要真正的 RLCD 或 MT-RL 补强,接入 proprioceptive/图像编码器 |
| RAG 的检索路由与重排 | Wikiracing 式高基数 Choice(255)尚未做到 | 需要先实现共享前缀内核,否则 255 候选 × 重复前缀的开销不可接受 |
| 代码评审 / 合规审查辅助 | 0.6B 的知识容量有限 | 换更大骨干(如 Qwen3-4B/8B)复用同一套 head 即可,架构是骨干无关的 |
| 医疗 / 司法等高风险决策 | calibration 是硬门槛 | 必须先建立 calibration 评估体系;当前框架还缺这一步 |
❌ 不适合
NanoJev 的价值不在它打败了 Jev(它在多数指标上并没有),而在于它用不到 2000 行 Python 证明了:一个"输入状态+问题+候选,输出校准分布"的决策模型,其架构、训练目标与数据管线是可以被完全开源、完全审计、并且带着自省地把自己每一处缺陷写进文档的。
对做工程的人来说,
unified_game_pipeline.py里的防 pits 条款和TYPESAFE_CONTRACT.md里的自曝清单,比 128/128 那个数字值钱得多。
| 文件 | 行数/大小 | 职责 |
|---|---|---|
scripts/train_toy_decisions.py |
20.9 KB | DecisionModel 定义(第 86–141 行);Scale/Set-Attention 双头 |
scripts/predict_toy_decisions.py |
18.0 KB | 输入契约校验、prepare_examples、DecisionPredictor、服务端输出成型 |
scripts/train_pipeline_decisions.py |
34.0 KB | 数据加载、target_for、grouped_target_loss、pack_complete_questions、评测 |
scripts/train_unified_games.py |
52.5 KB | 四任务混合 SFT 主训练器(任务权重 1/3, 1/3, 1/6, 1/6) |
scripts/unified_game_pipeline.py |
28.7 KB | cases / rollout / dataset 三命令闭环;本地+SSH 远端环境 RPC |
scripts/unified_grid_envs.py |
15.4 KB | UnifiedMazeEnv / UnifiedSnakeEnv(有限时程、可见 bool info) |
scripts/unified_doom_env.py |
16.6 KB | 无头 ViZDoom 适配器(basic / predict_position) |
scripts/unified_td.py |
16.2 KB | n-step 冻结策略 TD 目标(γ=1,weighted_bootstrap) |
scripts/serve_decisions.py |
4.2 KB | 74 行 stdlib HTTP 推理服务 + 静态站 |
scripts/game_tasks.py |
10.5 KB | 井字棋/网格导航求解器、source_group_id(D4 对称归一) |
scripts/teachers.mjs |
11.4 KB | Jev 原生概率路由 / LLM 硬标签路由 |
scripts/unified_jev_worker.mjs |
7.0 KB | Jev API 有界重试 worker(Node ≥22) |
docs/TYPESAFE_CONTRACT.md |
7.3 KB | 官方契约 vs 本地实现的逐条对照(荐读) |
research/pipeline_runbook_zh.md |
13.7 KB | 中文执行手册,含全部可运行命令 |
configs/sonic_unified_sft_v1.json |
1.9 KB | 四臂 AUC 对照实验配置 + 人口权重 + 评测协议 |
docs/APPO_SUPERVISION.md |
11.1 KB | APPO 专家监督(128/128 的来源) |
docs/UNIFIED_DEVELOPMENT_RELEASE.md |
5.3 KB | 发布清单与校验记录 |
外部资源
unified-games-v1)此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。