惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Jina AI
Jina AI
MyScale Blog
MyScale Blog
量子位
月光博客
月光博客
J
Java Code Geeks
A
About on SuperTechFans
H
Hackread – Cybersecurity News, Data Breaches, AI and More
U
Unit 42
WordPress大学
WordPress大学
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
腾讯CDC
G
Google Developers Blog
博客园 - 【当耐特】
Engineering at Meta
Engineering at Meta
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
宝玉的分享
宝玉的分享
IT之家
IT之家
N
Netflix TechBlog - Medium
Microsoft Security Blog
Microsoft Security Blog
博客园 - 叶小钗
B
Blog
Martin Fowler
Martin Fowler
P
Proofpoint News Feed
B
Blog RSS Feed

博客园 - zhang-yd

今日开源[第57期]beautiful-water源码解读 今日开源[第56期]human-atlas源码解读 今日开源[第55期]model-x-studio源码解读 今日开源[第54期]Spark(@sparkjsdev/spark)源码解读 今日开源[第53期]PlayCanvas Engine(playcanvas/engine)源码解读 今日开源[第52期]SPEEDBALL GI WebGPU Showcase(speedball-gi)源码解读 今日开源[第51期]Pi Agent Harness(pi)源码解读 今日开源[第50期]World Monitor(worldmonitor)游戏项目解读 今日开源[第49期]竹知了(zhuzhiliao)小玩具项目解读 今日开源[第48期]Operation Ironhold 游戏项目解读 今日开源[第47期]Academic Research Skills for Claude Code(ARS)项目skill解读 今日开源[第46期]AI-Research-SKILLs项目skill解读 今日开源[第45期]nature-research-skills项目skill解读 今日开源[第44期]findskills项目skill解读 今日开源[第43期]last30days-skill 今日开源[第42期]Cangjie Skill 今日开源[第41期]MoneyPrinterTurbo 今日开源[第39期]img2threejs 今日开源[第39期]Kronos 今日开源[第38期]Open Code Review (OCR) 今日开源[第37期]Openship 今日开源[第36期]croc 今日开源[第35期] Code-Review-Graph 今日开源[第34期] 《深入理解 AI Agent:设计原理与工程实践》 今日开源[第33期] Home Assistant Core 今日开源[第32期] Vibe-Trading 今日开源[第31期]RuView 今日开源[第30期]Chrome DevTools MCP 今日开源[第29期]RomM (ROM Manager) 今日开源[第28期]Page Agent
今日开源[第58期]NanoJev源码解读
zhang-yd · 2026-09-20 · via 博客园 - zhang-yd

项目源码解读 —《NanoJev》

仓库: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)


一、项目简介、背景、作用与作者

1.1 一句话概括

NanoJev 是 TypeSafe AI 商用模型 Jev 的"纳米级"开源复刻:一个 0.6B 参数的并行决策模型。输入「状态 + 问题 + 候选集」,一次前向直接输出完整概率分布,全程零输出 token 自回归解码。

1.2 背景:什么是 Jev / System One Models

要理解 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 节)。

1.3 项目作用:它到底解决了什么问题

NanoJev 不是又一个小聊天模型,它瞄准的是一个很具体的工程痛点——把 AI 做成软件可以直接调用的"模糊 if 语句"

  1. 给出现成的可运行替代品:Jev 处于 early access 且闭源,绝大多数开发者拿不到。NanoJev 提供 0.6B 权重 + 训练代码 + 推理服务 + 浏览器演示,可以在自己机器上跑。
  2. 提供一个可审计的最小实现:整个决策头只有几十行 PyTorch(train_toy_decisions.py 第 86–141 行),任何人可以逐行读完再决定要不要用。
  3. 提供一个严格的数据方法论样板:这可能是全项目最有价值的隐性产出——从环境 pitfalls、split 泄漏防护、truncation 冒充失败的禁止、到教师 API 回执逐条比对,形成了一套"如何诚实地做概率监督学习"的工程规范。

1.4 作者介绍

内容
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)。

1.5 时间线:三天做出了什么

仓库创建于 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   ← 当前状态

1.6 项目当前成绩(README 官方数字)

一个 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

几个值得注意的读数:

  • ViZDoom Basic 128/128,比商用 Jev 的 56/128 高出整整一倍多——这是靠 APPO 专家监督(见 §3.7)堆出来的,不是靠蒸馏 Jev。
  • Predict Position 27/128 从基线 11/128 提升了一倍半,但绝对值仍然很低,说明长时程移动目标预判仍未解决
  • Maze 4/10 低于 Jev 的 7/10,仓库没有回避这一点。
  • 展示视频里 50×50 大迷宫 NanoJev 用 225 次尝试到达出口,Jev 用 2738 次,Untuned Qwen 用 4726 次——但这使用的是"局部安全问题 + 外部探索代码"的混合形态,不能直接等同于上表的纯模型策略成绩,README 也标注了这一点。

二、部署与使用,以及部署条件

2.1 部署条件清单

类别 要求 说明
操作系统 Linux / macOS / WSL(Windows 未记录) 全部路径与脚本按 POSIX 编写
Python 3.14.4(记录版本) requirements-toy.txt 注释行
核心依赖 torch==2.14.0transformers==5.17.0safetensors==0.8.0numpy==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/NanoJevC-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)固化了源码指纹来防这类漂移。

2.2 标准安装与模型拉取

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 查训练配置、日志、对比实验时用

2.3 启动推理服务

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 级服务,不是生产网关。

2.4 浏览器演示(两条路线)

路线 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

2.5 完整训练复现管线(进阶)

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 只是宿主环境的兼容回退,不是所有环境必需。
  • 只在 dev 上选 checkpoint;calibration 不参与参数训练;test/OOD 不用于选超参。
  • --head-steps 是"先只训决策头再放开通骨干"的阶段开关。
  • 已有实验目录是冻结产物,重跑必须换 --output-dir,不能覆盖

2.6 离线自检(无需模型/网络)

python3 -m unittest discover -s scripts -p test_question_contract.py -v

这套测试用可逆字符 tokenizer + 真实 prepare_examples,覆盖:传输 ID 不变性、无关问题/状态隔离、Boolean 边界 criterion、Choice 名称/描述、Score 索引/邻居排除、期望分数读出。不加载 checkpoint、不 import PyTorch、不联网——非常适合 CI。


三、功能与核心/特色功能源码解析

3.1 功能全景

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"):

  1. 并行决策 — 把状态、问题、候选路径批处理进一次骨干前向
  2. 动态候选 — Choice 支持运行时给定 2–255 个候选,共享打分头。
  3. 布尔与有序分数 — Boolean 输出命题概率;Score 输出 2–10 级分布及其期望。
  4. 直接概率 — 不生成任何答案 token,直接给分布供外部代码排序/选择/采样。
  5. 单一小骨干 — Qwen3-0.6B + 决策头,四款游戏 + 持久推理服务共用。

3.2 【核心】一次前向打完全部候选 —— 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

读懂这段的关键几点:

  • "并行"的真正含义:不是"多个样本并行"(那是普通 batching),而是同一批里不同 state、不同 question、不同候选数量的混合体,全部摊平后共享一次 backbone forward。一个含 3 个状态、每个状态 2 个问题、每题 4 个候选的请求 = 24 条路径 = 一次前向。
  • lengths - 1 取值技巧:因为每条路径尾部都拼了 EOS(见 §3.3),最后一个 token 的隐状态天然是"读完整个 (state, question, candidate) 之后的总结位"。这就是"零解码输出"的实现基础——用位置代替生成的 token。
  • use_cache=False:明确放弃 KV cache,因为这是打分任务而非生成任务。
  • masked_fill(~valid, -1e9):不同题的候选数不同,padding 位必须被 logits mask 掉,保证 softmax 分母只覆盖真实候选。

3.3 【核心】输入编码:prepare_examples

scripts/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_idqid 都不出现在 leaf_tokensTYPESAFE_CONTRACT.md 第 8 行确认)。这意味着重命名问题 ID 不会改变任何候选路径的输入——保证了"传输标识不影响语义"的不变性,同时也避免了模型走捷径记忆 ID。

3.4 【特色】两种决策头:Scale Head 与 Set-Attention Head

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)

这是全项目最有技术品味的一处,四个细节值得学习:

  1. log_k 条件化:把候选数 log k 拼进每个候选的向量。这让同一个集合层能感知"当前是 4 选 1 还是 255 选 1"——候选数不同时 softmax 的温度压力天差地别,不加这个条件,模型无法区分。
  2. 自注意力做集合内交互:候选之间可以"看到彼此",从而支持相对偏好(排除支配/对比消除),而不只是绝对打分。注意 self_attention 没有位置编码,因此是真正的集合(置换等变)操作——候选顺序变化不改变结果(配合 peer,这正好满足 Choice 的置换不变要求)。
  3. 零初始化残差set_output 的 weight/bias 全零 → 训练刚开始时 delta ≡ 0模型严格退化为纯 Scale 模型。这是一个标准的"warm-start from a simpler model"技巧,保证集合层只能渐进增加表达能力,不会在第一步就把已训好的表示搅乱。上游 set_project / set_attention 仍是非零初始化,避免 dead gradient。
  4. 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 浪费了一条候选路径的计算。

3.5 【特色】严格 proper 的损失:整题归一,禁止拆成二分类

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};未截断输入")

3.6 【特色】目标来源的四种演绎 + Quarantine 机制

target_fortrain_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 条被隔离且原记录保留

3.7 【特色】环境与数据采集闭环:把"诚实"写进代码

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 变化,从而不可能把两个不同采集策略产生的数据混进同一个 datasetmake_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_contentsunified_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) 自动进入观测,而不需要外部作弊。

3.8 【特色】两种 JavaScript 教师路由:teachers.mjs

import { 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 等级同时暴露给模型,因此不满足本地推理的输入隔离性——它只是另一个标签接口,不是契约测试。

3.9 【特色】时序差分目标: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 目标作为对照。

3.10 服务层的安全小节

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;不记录请求状态与凭证

四、优点、不足与潜在应用领域

4.1 优点

① 架构极简、可逐行审计。 决策模型本体只有 DecisionModel.__init__(16 行)+ forward(38 行)。没有 custom CUDA kernel、没有 MoE、没有复杂 loss 组合。0.6B 的参数让它在单张消费级/数据中心 GPU 上可与 cacheble 全参训练。

② 工程诚实度远超同类项目。 这是我最想强调的一点,具体体现在:

  • 全链路 SHA256 固化(SOURCE_MANIFEST.json 114 KB;每次 rollout 不仅记录 상품 policy id 还记录源码本身的哈希
  • 远端 environment 握手时比对 source_hashes()源码不一致直接拒绝unified_game_pipeline.py:111
  • 明确区分"观测结果 / 程序真值 / 教师代理"三种目标语义,绝不互相冒充
  • 主动写 TYPESAFE_CONTRACT.md 列出自己与官方不符的 9 处并标注"遗留工作"
  • 主动在 README 里报告 Maze 4/10 < Jev 7/10
  • 41 个 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.jsonpaired_comparison 字段)。这是极少见的、能让人信服的模型对比。

4.2 不足

① 与官方 TypeSafe 契约仍有 9 处差距(作者自列,我归纳最关键的几个):

  • 本地用 type: "boolean",官方是 type: "noul"本地 noul 目前会直接校验失败
  • Score 缺 legendconfidence,Choice 缺 confidence
  • 不支持结构化/null 的 instruction 与 description(官方支持 object/array/null)
  • 状态序列化非规范 JSON:dict/list 状态会经过 f-string 变成 Python str(...),带单引号、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 在当下偏先进。这本身不是错(作者诚实记录了冻结环境),但会给实操者带来困惑。

4.3 潜在应用领域

结合仓内思想的原始定位("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 明确要求"依赖判断必须由外部程序发起下一轮")

4.4 一句话总结

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_examplesDecisionPredictor、服务端输出成型
scripts/train_pipeline_decisions.py 34.0 KB 数据加载、target_forgrouped_target_losspack_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 发布清单与校验记录

外部资源