[{"content":" 一句话版本：OPD（on-policy distillation，在线策略蒸馏）用「学生自己采样的数据」加「教师逐 token 的密集信号」，同时绕开离线蒸馏的分布错位和纯 RL 的信号稀疏。我们基于 TRL 做了一次最小复现，得到三个可量化的结论：数据筛选把 GSM8K 准确率从 11.52% 抬到 43.06%；换更强的教师做离线蒸馏几乎不动（43.06% → 43.21%）；只有「强教师 + 完整生成预算」的组合下，on-policy 的价值才兑现（54% \u0026gt; 42% \u0026gt; 39%）。\n本文面向已经接触过 RL 和蒸馏、想理解 OPD 来龙去脉并动手复现的读者。主线：OPD 本质是什么 → TRL 怎么实现 → 我们的实验证明了什么 → 结论能否被外部证据验证。\n开篇：OPD 的本质 为什么现在都在谈 OPD OPD 最近在大模型后训练里升温：Qwen3、GLM-5、DeepSeek 的后训练实践都涉及「在线蒸馏」思路，社区里也有不错的入门视频：从 RL 到 OPD：Qwen3、GLM-5、DeepSeek 都在关注什么？。核心主张一句话：保留 RL 里「学生自己探索」的 on-policy 采样，同时引入教师对每个 token 的密集反馈，缓解传统蒸馏的分布错位，也降低纯 RL 里信号稀疏、成本高、训练不稳定的压力。\n三范式对比 维度 离线蒸馏 offline KD OPD 在线策略蒸馏 RL（PPO/GRPO） 采样分布 教师或固定数据（off-policy） 学生当前策略（on-policy） 学生当前策略（on-policy） 学习信号 教师逐 token 分布 教师逐 token 分布 奖励（通常稀疏） 是否要奖励模型 否 否 是 分布错位 有 无 无 信号密度 高 高 低 不稳定风险 低 中（依赖采样质量） 高 本质是两个维度的交集：「谁的数据」决定分布是否错位——离线蒸馏用教师生成的数据，学生学的是「自己不太会产生的输入」上的分布，且固定数据不会随学生进步自适应；「谁的信号」决定密度——教师逐 token 的 KL 目标密集，RL 的奖励通常只出现在序列末尾。OPD 取交集：学生数据 × 教师信号。\n公式：广义 JSD OPD 的损失函数来自 GKD（Generalized Knowledge Distillation for Auto-regressive Language Models，Agarwal et al., ICLR 2024），用 β 把两种 KL 插值：\n$$L = \\beta \\cdot KL(P \\| M_\\beta) + (1-\\beta) \\cdot KL(Q_\\theta \\| M_\\beta), \\quad M_\\beta = \\beta P + (1-\\beta) Q_\\theta$$其中 $P$ 是教师分布、$Q_\\theta$ 是学生分布、$M_\\beta$ 是两者的几何混合。β 的两个端点对应两种 KL：\nβ → 0：forward KL $KL(P\\|Q_\\theta)$，mass-covering，学生尽量覆盖教师的分布，鼓励多样性； β → 1：reverse KL $KL(Q_\\theta\\|P)$，mode-seeking，学生收紧到教师的高概率区域，分布更锐利。 TRL 的实现和论文公式一致：对两个分布取 logsumexp 混合，再按 β 加权求和（β=0/1 走 forward/reverse KL 特判分支，DistillationTrainer 文档）。注意方向：蒸馏的 KL 是「学生向教师靠拢」，RLHF 里的 KL 正则方向相反——「策略不偏离参考模型」，这两个方向很容易混。\n需要优化的点：四个旋钮 β 调度：固定 0.5 还是退火（0 → 1，先覆盖后收紧）。TRL 没有内置退火，一个 callback 就能加（见第一节）。 采样完整性（max_completion_length）：学生生成被截断时，损失只算在截断前缀上。对 GSM8K 这种长推理任务，这是生死参数，第四节有量化证据。 教师质量：教师弱时逐 token 目标本身就是错的，学生会收敛到高置信的错误模式。 数据筛选：蒸馏数据里混入「答案不可提取/错误」的样本，会直接拖垮效果（第四节实验 1）。 需要新增的资源 相对 SFT，OPD 每步要：学生采样一个完整 rollout（生成整条推理链）、学生和教师各一次前向（教师冻结但显存双份）。教师越大推理成本越高——我们强教师跑 150 步花了 105 分钟，弱教师 250 步不到 30 分钟。纯 RL 还需要奖励模型/评测器，OPD 不需要。\n训练时大概是什么样子 一轮循环四步：取 prompt 批 → 学生按当前策略采样完整回答（temperature/top_p）→ 学生与教师分别前向，取 completion 位置逐 token logits → 算逐 token JSD 损失并只更新学生（LoRA）梯度，教师冻结。\n训练曲线看起来和普通训练没有区别：loss 平滑下降、JSD 缓慢收敛。真正有信息量的是行为指标——学生熵、completion 触顶率、终止率。我们会在第四节展示：弱教师那组熵从 0.276 塌缩到 0.248，loss 却还在降，光看 loss 完全发现不了问题。\n第一节：代码实践——TRL 的 DistillationTrainer (a) 怎么用 TRL 从 0.x 时代的 GKDTrainer 演进到现在的 DistillationTrainer。我们环境是 trl 1.10.0 + transformers 5.15.1，核心用法：\nfrom trl import DistillationConfig, DistillationTrainer cfg = DistillationConfig( output_dir=\u0026#34;outputs/opd\u0026#34;, max_steps=250, per_device_train_batch_size=8, learning_rate=1e-4, bf16=True, beta=0.5, # JSD 插值：0=forward KL，1=reverse KL max_completion_length=384, # 学生采样上限（关键旋钮） temperature=0.7, top_p=0.9, disable_dropout=True, ) trainer = DistillationTrainer( model=\u0026#34;Qwen/Qwen3-0.6B\u0026#34;, # 学生（可挂 peft LoRA） teacher_model=\u0026#34;Qwen/Qwen3-4B-Instruct-2507\u0026#34;, args=cfg, train_dataset=ds, peft_config=lora, ) trainer.train() 关键参数：\n参数 默认 作用 beta 1.0（合法域 [0,1]） JSD 插值：0=forward KL，1=reverse KL，中间为广义 JSD max_completion_length 512 学生采样上限，决定损失覆盖多长 temperature / top_p 1.0 / 1.0 学生采样分布，越接近 1 探索越多 disable_dropout False 训练时关 dropout，稳定采样 (b) 核心部分：它和之前的算法差在哪 三种方法的数据流对比：\nSFT: 固定数据 -\u0026gt; 学生前向 -\u0026gt; CE 损失 PPO: 学生采样 -\u0026gt; 奖励打分 -\u0026gt; 稀疏 advantage -\u0026gt; 策略梯度 OPD: 学生采样 -\u0026gt; 教师逐 token 打分 -\u0026gt; 密集 JSD -\u0026gt; 学生梯度 三个主要区别：\n数据谁产生：SFT 用固定数据；OPD 的学生每步自己采样（on-policy），输入分布随学生当前能力自适应，这是解决分布错位的机制。 信号是什么：OPD 是教师对每个 token 的分布目标，密度和 SFT 一样高；PPO/GRPO 的奖励通常只在序列末尾，难任务上容易出现 advantage 全 0 或全 1——我们主线里叫「二元陷阱」。 谁的梯度：只有学生更新，教师冻结但占显存。TRL 用 chunked 前向（核心函数 _chunked_divergence_loss 和 _chunk）逐块算 JSD，把峰值显存从 2×batch×seq×vocab 降到 chunk 级别，否则双模型前向根本放不下。 源码里还有一道硬校验：学生和教师词表必须一致（报错是 The student model has vocab_size X but the teacher model has vocab_size Y）。这直接决定模型选型——Qwen2.5 系列里 0.5B（151936）和 7B（152064）词表不一致，蒸馏直接报错；我们因此换成同词表族的 Qwen3-0.6B + Qwen3-4B/8B。\n一个 TRL 1.10 的 API 断层值得记录：旧 GKDTrainer 的 lmbda（on-policy 数据比例）参数在 DistillationTrainer 里没有了，只剩 beta 一个插值旋钮；GKDConfig 也不存在（旧版 GKD Trainer 文档是历史 API）。如果要把这套东西移植到别的框架（比如 veRL），这两个旋钮要自己接。\nβ 退火：6 行 callback TRL 没有内置退火，但 beta 只是 trainer 上的一个属性，写个 callback 即可：\nclass BetaAnnealCallback(TrainerCallback): def on_train_begin(self, args, state, control, **kwargs): self.total = max(1, state.max_steps) def on_step_begin(self, args, state, control, **kwargs): trainer = kwargs.get(\u0026#34;trainer\u0026#34;) if trainer is not None: frac = min(1.0, state.global_step / self.total) trainer.beta = 0.0 + (1.0 - 0.0) * frac # 0 -\u0026gt; 1 我们在实验里用固定 β=0.5 和这个退火各跑了一组。\n第二节：最小实现 (a) TRL 的最小实现 最小可跑版本（实验脚本 train_opd.py 的骨架）：\nimport json, random, re from datasets import load_dataset from peft import LoraConfig from trl import DistillationConfig, DistillationTrainer # 1) 数据：只需要 prompt，学生自己采 completion，不需要教师预生成答案 ds = load_dataset(\u0026#34;json\u0026#34;, data_files=\u0026#34;train_prompts.jsonl\u0026#34;, split=\u0026#34;train\u0026#34;) # 2) LoRA 学生 lora = LoraConfig(r=16, lora_alpha=32, lora_dropout=0.05, bias=\u0026#34;none\u0026#34;, task_type=\u0026#34;CAUSAL_LM\u0026#34;) # 3) 训练 cfg = DistillationConfig( output_dir=\u0026#34;outputs/opd\u0026#34;, max_steps=250, per_device_train_batch_size=8, learning_rate=1e-4, bf16=True, beta=0.5, max_completion_length=384, temperature=0.7, top_p=0.9, disable_dropout=True, ) trainer = DistillationTrainer( model=\u0026#34;Qwen/Qwen3-0.6B\u0026#34;, teacher_model=\u0026#34;Qwen/Qwen3-4B-Instruct-2507\u0026#34;, args=cfg, train_dataset=ds, peft_config=lora, ) trainer.train() trainer.save_model(\u0026#34;outputs/opd/final\u0026#34;) 加上第一节的 β 退火 callback、再挂一个 SwanLab callback 记曲线，就是我们实验的实际代码。OPD 和离线蒸馏在数据准备上最大的差别：离线蒸馏要先拿教师把整份训练集生成一遍（我们弱教师、强教师各生成 1000 条），OPD 完全不需要预生成，数据侧只有 prompt。\n(b) 外部库 库 作用 trl 1.10.0 DistillationTrainer 本体 transformers 5.15.1 模型、分词器与训练基础设施 peft LoRA 学生 datasets 数据加载（GSM8K、JSONL） torch 后端 swanlab（可选） 训练曲线记录（SwanLab offline 模式，之后 sync 回传） (c) 整体逻辑与三个细节 train_prompts.jsonl（1000 条 GSM8K question） -\u0026gt; 每步：学生采样 completion（max_completion_length=384） -\u0026gt; 学生 + 教师前向，completion 位置逐 token JSD -\u0026gt; 更新 LoRA 学生 -\u0026gt; 每 10 步记录 loss / 熵 / completion 统计 三个容易踩的细节：\nleft padding：生成端必须 left padding，否则右 padding 会污染采样起点（很多 RL 框架踩过同一个坑）。 max_completion_length 是采样完整性参数：GSM8K 推理链长，第一版用 384，completion 100% 触顶、损失只算在截断前缀上——这是弱教师组崩塌的直接诱因之一。 筛选标准 = 评测标准：筛选蒸馏数据用的 extract_answer 和评测是同一份逻辑（同时处理 #### 和 \\boxed{}），保证「数据契约」和「验收标准」一致，否则筛选实验的结论不可信。 第三节：实验与效果 (a) 实验设计 配置 值 学生 Qwen3-0.6B（LoRA r16/alpha32/dropout0.05，lr 1e-4，batch 8，bf16，seed 42） 弱教师 Qwen3-4B-Instruct-2507（greedy 0-shot 43%，200 子集测得） 强教师 Qwen3-8B（greedy 0-shot 66.5%，200 子集、2048 上限 + 双格式提取测得） 数据 GSM8K train 1000（seed 42 打乱），test 全量 1319 评测 greedy、0-shot、双格式提取（#### / \\boxed{}），max_new 2048；强教师对比用 4096 九个臂，一次只改一个变量：\nsft-baseline：SFT 金标答案（250 步） offline-kd：SFT 在弱教师生成的原始 1000 条上（不筛选） kd-hint：offline-kd 模型 + 评测时加「请以 #### 结尾」的格式提示 kd-random：弱教师数据随机抽 256 条 kd-filtered：弱教师数据筛出 256 条「答案正确且可提取」 opd：弱教师 on-policy，β=0.5，384 上限 opd-anneal：弱教师 on-policy，β 退火 0→1 kd8b-v2：强教师筛 699 条（69.9% 正确率）离线蒸馏 opd8b：强教师 on-policy，β=0.5，1024 上限，150 步 (b) 实验效果 弱教师全量 1319 评测：\n臂 准确率 正确/总数 SFT 金标 39.12% 516/1319 offline-KD 原始 1000 条 11.52% 152/1319 offline-KD + 格式提示 24.94% 329/1319 offline-KD 随机 256 条 3.33% 10/300（300 子集） offline-KD 筛选 256 条 43.06% 568/1319 OPD β=0.5 10.08% 133/1319 OPD β 退火 0→1 9.55% 126/1319 强教师（100 子集、4096 上限）：\n臂 准确率 口径 SFT 金标 39.12% 全量参考 offline-KD 筛选 699 条 42% 100 子集 OPD 150 步 54% 100 子集 三层结论：\n数据质量是决定性变量：同样 256 条数据，随机抽 3.33%，筛出正确样本后 43.06%——打平并略超 SFT 金标（39.12%），还超过了教师自己（43%）。学生学的不是「教师风格」，而是「正确且可提取」的答案格式。 离线蒸馏有硬天花板：换 66.5% 的强教师、筛 699 条，43.21%——几乎不动。offline 数据再干净也补不上「教师路径 ≠ 学生路径」的分布错位。 on-policy 的价值要教师够强 + 生成够完整才兑现：强教师 + 150 步 OPD 54% \u0026gt; offline-KD 42% \u0026gt; SFT 39%；弱教师下 OPD 反而崩到 10%，根因见下一节。 第四节：实验分析与验证 (a) 溯源：提升到底来自哪里 实验 1：数据筛选（+31.5pp）。offline-KD 在原始 1000 条上只有 11.52%——弱教师 43% 的准确率意味着过半样本的答案是错的，模型在学错误答案。筛出 256 条正确样本后直接到 43.06%，反超教师 43% 的上限。再补一个对照：随机 256 条只有 3.33%，说明提升来自「正确样本」这个属性，不是样本量或随机性。kd-hint（24.94%）说明格式提示只能救一半——答案错了就是错了。\n实验 2：弱教师 OPD 为什么崩。看训练曲线：\n弱教师组（灰线）250 步：熵从 0.276 一路塌缩到 0.248，completion 100% 触顶 384（损失只算在截断前缀上），JSD loss 从 0.078 降到 0.053 还在降——loss 全程正常，行为指标先坏了。叠加教师本身只有 43% 准确率，逐 token 目标大量是错的，学生收敛到一个高置信的错误模式。我们做了 sanity check：让这个模型翻译句子、写诗，它输出大段中文思考后被截断，回答质量明显退化——不是完全坍塌，但行为已经坏了。\n实验 3：强教师为什么能行。强教师组（黑线）150 步：熵从 0.232 升到 0.273（保持），1024 上限虽然也 100% 触顶（terminated 率为 0），但教师目标质量高，学生分布没有塌缩。代价是训练成本：150 步 105 分钟，弱教师 250 步不到 30 分钟。\n实验 4：评测上限也是口径陷阱。强教师 OPD 用默认 2048 上限全量评测只有 0.08%（1/1319）——学生学到了更长的推理链，2048 截断后提取不到答案；同一模型在 4096 上限的 100 子集上是 54%。具体案例：强教师 8B 有一条生成在 1529 token 处被截断，答案提取为 None（金标 3）。测「变长」的模型，评测预算必须跟着涨。\n(b) 边界与口径 边界集中说明：子集口径（100/200/300）、单一数据集 GSM8K、LoRA 非全参、β 只试了 0.5 与退火、GPU 为共享环境。这些是后续要补的验证面，不改变上面三个结论的相对方向。\n结尾：结论 回到开篇的问题——OPD 的 on-policy 机制到底有没有用：\n成立有两个前提：教师足够强、学生采样足够完整。两个前提都不满足时（弱教师 + 384 截断），它比离线蒸馏更容易崩——错误目标和高置信塌缩是叠加的。 数据筛选是所有方法里性价比最高的杠杆：+31.5pp、零额外算力，本质是把「评测标准」前移到数据契约。 ","permalink":"https://letsgetai.github.io/posts/opd-on-policy-distillation-trl-repro/","summary":"\u003cblockquote\u003e\n\u003cp\u003e一句话版本：OPD（on-policy distillation，在线策略蒸馏）用「学生自己采样的数据」加「教师逐 token 的密集信号」，同时绕开离线蒸馏的分布错位和纯 RL 的信号稀疏。我们基于 TRL 做了一次最小复现，得到三个可量化的结论：数据筛选把 GSM8K 准确率从 11.52% 抬到 43.06%；换更强的教师做离线蒸馏几乎不动（43.06% → 43.21%）；只有「强教师 + 完整生成预算」的组合下，on-policy 的价值才兑现（54% \u0026gt; 42% \u0026gt; 39%）。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e本文面向已经接触过 RL 和蒸馏、想理解 OPD 来龙去脉并动手复现的读者。主线：OPD 本质是什么 → TRL 怎么实现 → 我们的实验证明了什么 → 结论能否被外部证据验证。\u003c/p\u003e\n\u003ch2 id=\"开篇opd-的本质\"\u003e开篇：OPD 的本质\u003c/h2\u003e\n\u003ch3 id=\"为什么现在都在谈-opd\"\u003e为什么现在都在谈 OPD\u003c/h3\u003e\n\u003cp\u003eOPD 最近在大模型后训练里升温：Qwen3、GLM-5、DeepSeek 的后训练实践都涉及「在线蒸馏」思路，社区里也有不错的入门视频：\u003ca href=\"https://www.bilibili.com/video/BV1bNGz6xEQf/\"\u003e从 RL 到 OPD：Qwen3、GLM-5、DeepSeek 都在关注什么？\u003c/a\u003e。核心主张一句话：保留 RL 里「学生自己探索」的 on-policy 采样，同时引入教师对每个 token 的密集反馈，缓解传统蒸馏的分布错位，也降低纯 RL 里信号稀疏、成本高、训练不稳定的压力。\u003c/p\u003e\n\u003ch3 id=\"三范式对比\"\u003e三范式对比\u003c/h3\u003e\n\u003ctable\u003e\n\t\u003cthead\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003cth\u003e维度\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003e离线蒸馏 offline KD\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eOPD 在线策略蒸馏\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eRL（PPO/GRPO）\u003c/th\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/thead\u003e\n\t\u003ctbody\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e采样分布\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e教师或固定数据（off-policy）\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e学生当前策略（on-policy）\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e学生当前策略（on-policy）\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e学习信号\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e教师逐 token 分布\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e教师逐 token 分布\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e奖励（通常稀疏）\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e是否要奖励模型\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e否\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e否\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e是\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e分布错位\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e有\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e无\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e无\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e信号密度\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e高\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e高\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e低\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e不稳定风险\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e低\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e中（依赖采样质量）\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e高\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003e本质是两个维度的交集：「谁的数据」决定分布是否错位——离线蒸馏用教师生成的数据，学生学的是「自己不太会产生的输入」上的分布，且固定数据不会随学生进步自适应；「谁的信号」决定密度——教师逐 token 的 KL 目标密集，RL 的奖励通常只出现在序列末尾。OPD 取交集：学生数据 × 教师信号。\u003c/p\u003e","title":"OPD 本质分析：从在线蒸馏公式、TRL 最小复现到 GSM8K 验证"},{"content":"场景：10 周，13,147 条 commit AI 辅助开发把代码的累积速度提高了一个量级，随之而来的问题是项目越写越“堆”：功能在快速扩张，结构在失去秩序。最直觉的应对是在 prompt 里加约束——“做最小实现”“不要过度抽象”。本文的结论是：这类事前限制控制不了膨胀，起作用的是事后治理——用固定门禁拦住可机械检测的结构问题（坏味道，如重复代码与死代码），用定期重构和去重抽象控制长期累积。核心证据来自 deepseek-harness 的公开 commit 史。\ndeepseek-harness（下文简称 DSH）是 DeepSeek 在 GitHub 上公开的仓库，在 2026-06-10 至 2026-08-21 的 10 周里积累了 13,147 条 commit，峰值一天 889 条。commit 主题与分支名中留有大量 AI 编码代理参与的痕迹（codex/ 分支前缀、.agents/ 目录）。这些数字全部来自公开数据，附录给出了可复现的统计命令，读者可以自行验证。\ncommit 史数据：三个反直觉的分布 提交类型按 commit 主题首词归类统计（命令见附录）：\n类型 数量 占比 Merge 5,891 45% fix 2,490 19% docs 1,435 11% test 1,028 8% feat 745 6% refactor 494 4% revert 36 0.3% 其他 1,028 8% 三个反直觉的发现：\n第一，45% 的 commit 是 Merge，不是功能提交。合并是最大宗，说明开发不是单线程追加，而是大量并行分支持续汇入主干。一个仓库的形态是众多并行流合并出来的——这正是“单点约束难以控制全局结构”的机制原因。\n第二，revert 只有 36 条，占 0.3%。靠“写错就回滚”纠错的项目，回滚不会这么少。36 条说明纠错不依赖回滚：提案在裁决阶段被否掉（archived / rejected），不合规的改动被门禁挡在合并之前，剩下的问题由后续小步修改向前消化。\n第三，docs 占 11%，约每 9 条 commit 就有 1 条涉及文档。文档是日常流程的一部分，跟着代码同步演进，而不是收尾阶段的事后补写。这与“AI 写太快，文档必然滞后”的常见预期相反。\n删除是制度化动作：净删重构与 26 条简化分支 只看类型分布容易得到“只增不删”的印象。删除的证据在 refactor 里，而且是净删除主导：\nrefactor(image): remove region reads —— +132 / -1,040 行，54 个文件 refactor(attachment) —— +612 / -748 行，68 个文件 两条都是单主题重构：移除一组区域读操作、重构 attachment 模块，净效果都是行数大幅下降。\n更系统的证据是 26 条以 codex/simp- 开头的简化分支。这些分支按顺序堆叠成链：\nsimp-hide-concrete-agent-loop → simp-hide-subagent-internals → simp-unify-agent-session-id → simp-compaction-surface → simp-trim-hook-snapshot-noise 从名字看，每一步是同一个动作：把具体实现藏到接口后面（hide-concrete-agent-loop、hide-subagent-internals），统一重复的会话语义（unify-agent-session-id），压缩暴露面（compaction-surface、trim-hook-snapshot-noise）。分支名本身就是意图声明——“这周删掉什么”写进了分支名。26 条分支在 10 周内出现，平均每周 2~3 条：删除是有节奏的常规动作，不是偶尔的整理。\n三层机制：机器门禁、Agent 提案、决策留痕 删除能在 13,000 条 commit 的规模下不发生混乱，依赖三层机制，每层解决一个问题。\n第一层解决“能不能合”：机器门禁。检测重复代码块的工具 jscpd、扫描死代码的 knip、检查包发布配置的 publint，加上按文件 100% 的测试覆盖率门槛（test:coverage 脚本），全部挂进 pnpm run 与 CI。不合规的改动无法变绿。这一层不区分代码来源，AI 写的和人写的走同一个门。\n第二层解决“删什么、影响多大”：Agent 提案。仓库内有一个把简化流程化的编码代理技能 dsh-find-simplifications：先给出判定标准，再实测影响面，输出提案。代理负责找候选与测量，不直接改主干。\n第三层解决“谁说了算”：决策留痕。.agents/notes/ 目录下有 2,250 条决策记录，状态机为 proposed → implemented → archived / rejected，且强制填写 Alternatives（备选方案）。\n三层合起来回答了人的注意力分配问题：在 13,000 条 commit 的规模下，人不需要逐条看。人只出现在两个位置——定义门禁规则与简化判定标准；对提案做最终裁决（archived / rejected 需要人拍板）。扫描、测量、提案、实施全部由机器与代理承担。\n为什么写进 prompt 的事前限制无效 事前限制与事后治理的差别有两个。\n作用时间不同。prompt 约束作用于生成时刻，影响模型下一次生成的选择；膨胀是积累问题——13,147 条 commit 各自在生成时都合理，堆叠 10 周后结构才失控。生成时刻的一条约束，约束不了 13,147 次生成的合力。\n可验证性不同。“做最小实现”“不要过度抽象”没有可判定的语义：没有测试能断言一段代码是否“过度抽象”，只能靠人逐条抽查，进不了 CI。事后门禁的语义可判定：重复代码块的数量、死代码导出、单文件覆盖率都有确定答案，能变成红灯绿灯，由机器逐条校验。\n为什么流程比“Agent 自觉”可靠 另一种常见思路是依赖 Agent 自觉——“让它自己保持克制”。这个思路有两个缺口。\n没有反馈回路。Agent 删代码后，如果删错没有被拦截，错误不会回到 Agent 那里，它也没有理由在下次更谨慎。第一层门禁补上了回路：删错导致测试变红，错误回到提案环节。克制与否取决于反馈回路是否存在，而不是 Agent 的态度。\n判断与执行没有分离。dsh-find-simplifications 把“什么是简化”（判定标准）与“怎么找简化”（扫描与实测）分开：代理执行与测量，人判断与裁决。如果标准、执行、验收由同一个 Agent 完成，判断就没有外部锚点。提案状态机保证每条决策有明确的裁决者，而裁决者不参与执行。\n重构成本为什么现在可以承受 “重构有风险”长期劝退删除。DSH 用三件事把风险降到了可承受范围。\n机器验证网兜底删错。删除最大的风险是删掉还在用的行为。按文件 100% 的覆盖率加上 CI 门禁，行为一旦被改坏，测试立刻变红。删错的发现时点从“上线之后”提前到“提交之时”。\n并行扫描摊薄时间。45% 的 Merge 占比说明这个仓库本身就在高频并行合并；26 条 simp 分支可以同时扫描不同模块，互不阻塞，由合并统一消化。\n删除范围有界。两条示例重构分别涉及 54 和 68 个文件，但都是单一主题。一次只动一个模块，红了就回滚该分支——这也解释了 revert 为何只有 36 条：改动足够小，不需要整体回退。\n落地清单：五步起步 这套机制不需要 13,000 条 commit 的规模才值得用。最小起步五步：\n挂机器门禁：检测重复代码的工具（如 jscpd）与扫描死代码的工具（如 knip），接入 CI（命令见附录）。 让门禁真的拦：坏味道不修就不绿。只警告不拦截，不产生反馈回路。 建提案通道：一次一个主题、一个分支一条提案，写明改动行数、文件数与行为影响。 留决策记录：一个 notes 目录，四态状态机，强制 Alternatives。 把找简化变成固定节奏：把判定标准与影响测量写死成流程，按周期执行，而不是等“有时间再说”。 边界与适用条件 本文证据全部来自 DSH 的公开 commit 史与仓库结构分析，没有运行其代码，也不构成对代码质量的评判。结论适用有三点边界。\n治理强度匹配参与度。DSH 是高频并行开发的开源仓库；个人项目或低频率项目只需要第一步的门禁。2,250 条决策记录是配套规模，不是起步规模。\n复杂度指标只提示、不进 gate。DSH 门禁选的都是有确定语义的指标（重复、死代码、覆盖率）。圈复杂度一类指标给的是信号不是判决，放进 gate 会产生噪音。\n删除前先问三句：还有没有调用方（死代码扫描回答）；删除后的行为变化能否被测试覆盖（覆盖率回答）；这段逻辑是否只有这一处知道（重复语义是否成立，由 jscpd 加人判断）。三句都有答案再删。\n未验证项：13,147 是 commit 条数而非代码行数，本分析没有统计行数净变化；提交类型按主题首词归类，分支内实际改动未逐条核验；“AI 参与开发”是从 codex/ 前缀、.agents/ 目录与技能命名推断，仓库未逐条标注作者。\n附录：复用命令与 DSH 统计命令 在自己的项目里复用的最小命令集：\nnpx jscpd src # 重复代码检测 npx knip # 死代码与未使用导出 npx publint # 包发布配置检查 覆盖率按文件设 100%：在测试运行器（如 vitest / jest）的覆盖率配置里把每文件阈值设为 100%，并接入 CI。\nDSH 公开 commit 史的可复现统计（数字对应 2026-08-21 的仓库状态；仓库此后继续演进的话，clone 结果会略大于文中数字）：\ngit clone https://github.com/deepseek-ai/deepseek-harness cd deepseek-harness git log --oneline | wc -l # 13,147 git log --oneline | grep -c \u0026#34;^Merge\u0026#34; # 5,891 git log --oneline | grep -cE \u0026#34;^fix\u0026#34; # 2,490 git log --oneline | grep -cE \u0026#34;^docs\u0026#34; # 1,435 git log --oneline | grep -cE \u0026#34;^test\u0026#34; # 1,028 git log --oneline | grep -cE \u0026#34;^feat\u0026#34; # 745 git log --oneline | grep -cE \u0026#34;^refactor\u0026#34; # 494 git log --oneline | grep -ci \u0026#34;revert\u0026#34; # 36 git log --oneline | grep -oE \u0026#34;codex/simp-[a-z0-9-]+\u0026#34; | sort -u | wc -l # 26 说明：以上按 commit 主题计数；grep -ci revert 统计的是主题中含 revert 的提交，36 为其数量。refactor 示例的逐条行数可用 git show \u0026lt;commit\u0026gt; --stat 核对。\n","permalink":"https://letsgetai.github.io/posts/ai-code-governance/","summary":"\u003ch2 id=\"场景10-周13147-条-commit\"\u003e场景：10 周，13,147 条 commit\u003c/h2\u003e\n\u003cp\u003eAI 辅助开发把代码的累积速度提高了一个量级，随之而来的问题是项目越写越“堆”：功能在快速扩张，结构在失去秩序。最直觉的应对是在 prompt 里加约束——“做最小实现”“不要过度抽象”。本文的结论是：这类事前限制控制不了膨胀，起作用的是事后治理——用固定门禁拦住可机械检测的结构问题（坏味道，如重复代码与死代码），用定期重构和去重抽象控制长期累积。核心证据来自 \u003ca href=\"https://github.com/deepseek-ai/deepseek-harness\"\u003edeepseek-harness\u003c/a\u003e 的公开 commit 史。\u003c/p\u003e\n\u003cp\u003edeepseek-harness（下文简称 DSH）是 DeepSeek 在 GitHub 上公开的仓库，在 2026-06-10 至 2026-08-21 的 10 周里积累了 13,147 条 commit，峰值一天 889 条。commit 主题与分支名中留有大量 AI 编码代理参与的痕迹（codex/ 分支前缀、.agents/ 目录）。这些数字全部来自公开数据，附录给出了可复现的统计命令，读者可以自行验证。\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"每周 commit 数\" loading=\"lazy\" src=\"/images/ai-code-governance/weekly-commits.png\"\u003e\u003c/p\u003e\n\u003ch2 id=\"commit-史数据三个反直觉的分布\"\u003ecommit 史数据：三个反直觉的分布\u003c/h2\u003e\n\u003cp\u003e提交类型按 commit 主题首词归类统计（命令见附录）：\u003c/p\u003e\n\u003ctable\u003e\n\t\u003cthead\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003cth\u003e类型\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003e数量\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003e占比\u003c/th\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/thead\u003e\n\t\u003ctbody\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eMerge\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e5,891\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e45%\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003efix\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e2,490\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e19%\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003edocs\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e1,435\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e11%\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003etest\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e1,028\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e8%\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003efeat\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e745\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e6%\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003erefactor\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e494\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e4%\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003erevert\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e36\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e0.3%\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e其他\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e1,028\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e8%\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003e\u003cimg alt=\"提交类型分布\" loading=\"lazy\" src=\"/images/ai-code-governance/commit-types.png\"\u003e\u003c/p\u003e\n\u003cp\u003e三个反直觉的发现：\u003c/p\u003e","title":"AI 写代码不膨胀：把治理放在事后，而不是写进 prompt"},{"content":"一个把 p95 压到 200ms 的任务 假设你让 agent 把服务的 p95 延迟压到 200 毫秒以下（p95 指 95% 的请求延迟低于这个值，是性能优化常用的目标——意思是只允许最慢的 5% 请求超过它）。这件事没法一条命令做完：要先跑 profile 定位热点，改实现，再跑基准对比，确认没有回退；没达标就换一个方向重来。十几轮工具调用，每一轮都改一点东西。开头 agent 清楚方向，二十轮之后它可能已经忘了最初的目标，或者在一个局部优化里越钻越深，或者自己宣布「完成了」——但基准根本没达标。\n这类任务有个名字：长程任务（long-horizon task），指需要很多轮连续动作、最后还要验证结果才算完成的任务。你的问题很具体：这类任务里 agent 为什么容易跑偏、停不下来？Codex 的 goal 模式能不能解决？什么时候用、怎么写？\n暂定回答：goal 模式解决的是「完成判定与续跑机制」的缺失，而不是模型能力本身。它适合有明确终点但路径不确定的任务；使用时要把目标写成可验证的证据面，并配合 token 预算；官方推荐的完整流程是先 /plan 访谈澄清，再 /goal 建立目标。\n长程任务为什么难：断崖不在单步，在「串起来」 先排除一个常见误解：单步能力已经不是瓶颈。OpenAI 官方跑过 25 小时、1300 万 token 的连续任务实验，模型可以长时间稳定工作；METR 的度量进一步指出，「50% 可靠完成」的任务时长大约每 7 个月翻一番，2025 年初已经到了小时级——瓶颈是把长动作序列串起来，而不是单步技能。\nbenchmark 把这种断崖画得更直白。在 SWE-bench Verified（500 个单 issue 任务）上 gpt-5.2 能做到约 72.8%；换成 SWE-EVO（48 个 release 级任务，平均涉及 21 个文件），同样的模型掉到 22.9%；SWE-bench Pro（1865 个企业级长任务）上 Top 模型约 23%；LongCLI-Bench 的 20 个长程 CLI 任务，SOTA 不到 20%，多数 agent 卡在 30% 进度之前。\nBenchmark 形态 代表性数字 SWE-bench Verified 500 个单 issue 任务，人工筛选 mini-SWE-agent 65%；gpt-5.2 约 72.8% SWE-bench Pro 1865 个企业级长任务 Top 模型约 23% SWE-EVO 48 个 release 级任务，平均涉及 21 个文件 最佳 25%；gpt-5.2 从 Verified 的 72.8% 掉到 22.9% LongCLI-Bench 20 个长程 CLI 任务 SOTA 不到 20%，多数卡在 30% 进度之前 不同基准的口径和难度不一致，数字只说明数量级；同一模型从单 issue 到长程任务的落差（72.8% → 22.9%）是多个独立基准的一致信号——问题不在单步能力，而在能不能把长任务走完。\n任务变长后，agent 会撞上三个具体困难。第一个是目标漂移：目标最初只存在于系统提示里，几十轮之后上下文里全是中间产物，最初的约束被稀释，agent 顺着最近的局部进展走，而不是最初的方向。第二个是完成判定缺失：没有外部判据时，「做完」是模型自己说的，它可能看到一条测试通过就宣布胜利，即使那只是它刚改的那条路径。第三个是上下文膨胀：每轮对话都追加进上下文，任务后期上下文被中间过程占满，模型既看不清目标，也看不清证据。\n所以，解决长程任务的关键不是更强的单步模型，而是回答两个问题：目标放在哪里，谁来判断完成。看现有方案的谱系，能看出一条演进线：第一代把目标写进系统提示或记忆文件（AGENTS.md、auto memory），启动时加载进上下文，目标在不在、记得记不住、判定标不标准，全看模型自觉；第二代用外部状态文件（PLANS.md 一类的 living document），agent 自己维护进度、决策和证据，比提示词强，但维护本身仍是自觉行为；第三代是工具层的 goal——目标持久化在工具层，每轮自动注入，完成与否按预设证据面判定。\n方案 目标放在哪 谁判断完成 局限 提示词 / 记忆文件（AGENTS.md、auto memory） 启动时加载进上下文 模型自觉 会遗忘、会漂移，判定标准不稳 外部状态文件（PLANS.md） agent 自维护的文档 用户 + 模型对照验收 维护靠自觉，仍需人工推进 工具层 goal 工具层持久化、每轮注入 证据面判定（Codex）/ 独立裁判（Claude） 需要用户写清证据面与预算 演进的是「目标放在哪里、谁判断完成」这两件事：goal 把判定权从模型自觉移到了工具层。\n谱系图的演进方向是「目标放哪里、谁判断完成」：从模型自觉逐步移到机制层。\ngoal 机制：把目标变成每轮注入的强制约束 goal 解决三个困难的方式，是把「目标」从上下文里的一次性文本，变成工具层持久化、每轮强制注入的状态。它由四个机制组成。\n持久目标状态。 目标存在线程级状态里（thread-scoped persisted state）：存在服务端、跟随这个线程，而不是塞进每轮对话——它不是全局记忆，不会污染其他线程；也不是项目指令，不会每次启动自动加载。\n事件驱动续跑。 一轮结束、线程空闲、没有排队输入时，工具层自动让 agent 继续下一轮，不用你每次手动说「继续」。为了防自旋，计划类的输出不触发续跑；某一轮没有任何工具调用时也抑制续跑——避免模型空转刷轮次、假装在干活。\n证据判定完成。 goal 只能由模型在证据支撑下标记完成，证据是文件、测试、日志、产物这类可审计的东西；系统随目标注入的审计要求（completion audit / blocked audit）会要求模型在标记完成前先核对证据，并在无路可走时停下来报告而不是硬撑。预算兜底。 创建时设定 token 预算，触顶后状态进入 budget-limited 并汇报；注意预算耗尽不等于完成——agent 会告诉你「预算用完、还差什么」，而不是假装做完了。\n这四个机制合起来，回答的正是上一章那两个问题：目标放在工具层，完成由证据判定。这也说清了它和「把要求写进上下文」的本质区别：上下文里写「目标是 X」，模型每一轮都可能遗忘或漂移，而且没有任何机制检查它是否还朝着 X；goal 则把目标变成每轮请求里携带的约束块，连同预算、续跑策略、完成审计一起注入。agent 不是「记得」目标，而是「被注入」目标。\ngoal 的状态机很简单：active（进行中）→ paused（暂停）、complete（完成）、budget-limited（预算触顶）、cleared（被清除）。关键在权威边界：模型可以创建 goal，并且只在证据支撑时标记 complete；暂停、恢复、清除和预算转换都由用户或系统控制——goal 不是无人监管的自主性。\n箭头标注谁触发转移：模型只负责创建与基于证据标记完成，其余转移都在用户/系统手里。\n需要提醒：Codex 的 goal 和 Claude 的 goal 同名不同构——前者由主模型按证据面自审，后者由每轮独立的轻量裁判模型判定。差异和取舍放在附录 A。\ngoal 的效果：研究怎么说 产品级 goal 没有公开的标准化 A/B 评测，但它的机制不是孤例。goal 的四个机制——持久目标状态、自动续跑、证据判定、预算——在学术上等价于「显式任务状态外置 + 独立验证」这一类机制，而这类机制在 2026 年的长程基准上有量化消融证据。\n研究 机制 量化提升 LongHorizon-Harness 任务状态外置 + 只读审计循环（Manage-Execute-Audit） WeaveBench 51.8%→80.7%；Terminal-Bench 2.1 69.7%→77.2%；OSWorld 2.0 2.8%→8.3%；Claude Opus 4.7 子集 20.0%→34.3% PushBench 定量目标持久性（QGP）基准：状态跟踪检索 / backlog-tracking / 标准完成门控三类控制器对比 状态跟踪检索控制器 69–78% 成功并消除重复提交；backlog-tracking 工作单元控制器 25–50%；同一设置下标准/完成门控控制器 0 个实例完成 StructAgent verifier 背书的状态转移 OSWorld-Verified：Qwen3.5-9B 27.0%→46.9%；Qwen3.5-27B 31.6%→62.2% 三项研究做的其实是同一件事：把目标与进度从模型脑子里搬到机制层，让验证独立于生成——LongHorizon-Harness 外置任务状态并加只读审计循环，PushBench 证明目标持久本身决定成败，StructAgent 用独立验证器背书状态转移。goal 就是这件事的产品化：机制层持有目标与预算、每轮注入、按证据判定，而不是指望模型自觉。\n生产侧的观察指向同一个痛点。Reza Rezvani 的 Claude /goal 生产实测里有一个 17 个服务文件的认证调用迁移任务：之前每轮手动「keep going」11 次，goal 之后自动跑到完成。这种 babysitting tax（看护税）——每轮都要人推一把——正是 goal 要消除的那部分成本。\n最后补一个相邻发现。LongCLI-Bench 发现自我纠错提升有限，而 plan injection 加人工交互指导显著提升，支持「机制与人类协作比纯模型自驱更可靠」。goal 的定位正是把机制（状态、判定、预算）和人的介入（写证据面、审批）组合起来，而不是把任务完全扔给模型。\n什么时候用、怎么写 先用三档判断。目标模糊，连「什么算完成」都要先调研——先 /plan：先做只读调研和访谈式澄清，把任务边界、验收标准问清楚，产出计划。计划产出后有两条路：直接确认执行，把已确定的方案走完——路径已定，通常一轮完成、比较快；或者关掉确认界面不执行，把计划固化成 goal——自动续跑，适合路径还没完全确定、需要探索的任务。目标明确、路径不确定——直接 /goal：压 p95、超参调优、多轮重构这类终点可验证、中间步骤未知的任务，正是 goal 的适用区。单行修改、单轮问答——两者都不用，goal 有创建和审计成本，杀鸡不用牛刀。\n把计划固化成 goal，官方给出三种衔接方式，任选其一：a) 让 Codex 把计划转成带可度量成功标准的 goal 文本，再 /goal 启动（官方 long-running-work 推荐流程）；b) 把计划存成 PLAN.md，然后 /goal Implement PLAN.md，goal 直接指向计划文件（官方 follow-goals 迁移例子）；c) 跳过 plan，直接让 Codex 起草 goal 草稿（「Help me turn this into a strong /goal: 我想做……」），收紧后激活。为什么 plan 执行和 goal 执行不同：plan 解决方向不清——方案确定后，执行就是照做一遍；goal 解决执行不持续——路径未知时把目标绑在线程上自动续跑、按证据判定完成；plan 的确认执行是可选项，不与 goal 冲突。\n怎么写，官方 cookbook 给出六要素：最终状态（Outcome）、证据面（Verification surface）、不回归项（Constraints）、可用边界（Boundaries）、每轮选动作策略（Iteration policy）、阻塞停下条件（Blocked stop condition）。套到 p95 的例子：\n/goal 把服务的 p95 延迟压到 200ms 以下， verified by 基准脚本 bench.sh 输出中 p95 \u0026lt; 200ms，且测试套件全部通过； while preserving 现有 API 语义和正确性； Use 工作区、基准脚本和 git； Between iterations 先跑基准确认当前基线，再选一个最大热点优化，跑完复测，没达标就换下一个热点； If blocked 连续两轮 p95 无改善时停下，报告瓶颈分析、已尝试方案和剩余候选。 两个写法要点：证据面必须是可审计的东西（命令输出、测试结果、日志、产物），而不是「做得更好」这类主观词；迭代策略必须明确「每轮之后怎么选下一步」——这正是防止漂移的关键。配套方面，goal 通常配合 token 预算（防失控）、审批（关键动作前确认）、worktree（并行实验互不污染）；其中预算建议一定给，goal 解决「停不下来」靠的就是预算和阻塞条件这两把闸。\n结论 回到开头的问题：goal 值不值得用？如果你经常跑「终点明确、路径未知」的多轮任务，值得——它把「目标放在哪、谁判断完成」从模型自觉变成工具约束，配合预算和证据面，能缓解跑偏和停不下来的问题；但它不提升单步能力，benchmark 断崖的另一半不是 goal 的职责。什么时候用：模糊任务先 /plan 再 /goal，明确任务直接 /goal，单轮任务都不用。怎么写：六要素，核心是证据面可审计、迭代策略明确。\n最后集中说明一次本结论的边界：产品级 goal 没有公开的标准化 A/B 评测，但机制等价的研究给出了量化支持（见「goal 的效果」章）；具体机制以所用版本文档为准。\n附录 A：Codex 与 Claude 的 goal，同名不同构 两者都把「每轮检查是否完成」做成了机制，但检查者不同：Codex 由主模型按证据面自审；Claude 把每轮结束后的判定外包给独立的轻量裁判模型（默认 Haiku），它读「条件+对话」返回 未达成/达成/不可能，并把理由注入下一轮。对照如下。\n维度 Codex goal Claude /goal 本质 线程级持久化状态 session 级「停止钩子（Stop hook）」包装，每轮结束后触发检查 判定者 主模型自审，按证据面审计 独立轻量裁判模型（默认 Haiku），读条件+对话 裁判能否取证 主模型审计时可调用工具取证 裁判不调用工具，条件必须写成「Claude 输出能证明」的形式 预算 显式 token 预算，触顶进 budget-limited 无显式 token 预算，界面显示 token spend；可在条件里写轮次子句限长（例如「or stop after 20 turns」） 错误处理 由线程状态管理 认证失败/余额耗尽/上下文溢出自动清除 goal 非交互 支持 claude -p \u0026ldquo;/goal \u0026lt;条件\u0026gt;\u0026rdquo; 支持 字符上限 4000 4000 plan 模式对照：\n维度 Codex /plan Claude plan mode 本质 访谈式澄清流程 权限模式：只读调研+写计划，编辑被阻止直到批准 计划编辑 计划可编辑 Ctrl+G 编辑计划 批准选项 确认后进入执行 Yes auto / Yes manual / No keep planning 批准后行为 按计划执行（路径已定，通常一轮完成） 批准后进入编辑模式执行；goal 是另一条独立机制 强制范围 官方推荐流程 bypass 模式下不强制 工程上的取舍：自审省一次推理、能看工具结果，但依赖主模型的诚实；独立裁判判定更「客观」、每轮成本固定，但看不见工具输出，条件必须能被纯文本证明。没有公开数据说谁更好。\n两条路线的差别在完成判定：Codex 由主模型取证审计，Claude 由每轮独立的轻量裁判模型裁决并注入理由。\n附录 B：技术追溯 时间线：\n2025-03，METR 发布长程任务度量，给出「可靠完成时长约 7 个月翻倍」的量化基线，并把「串起长动作序列」定义为瓶颈。 2025 年上半年，τ-bench（ICLR 2025）显示 gpt-4o 在工具使用任务上 pass@1 仅 42%——长任务评测从一开始就不是高分场。 2025 年中，OpenAI 公开 25 小时/1300 万 token 连续运行实验，核心方法是外部状态文件（spec/plan/implement/documentation），官方称 durable project memory——「目标外置」路线的实证起点。 2026-05 前后，Codex 与 Claude 两侧的 goal 先后转正/成型，Codex 0.128.0+ 提供 /goal 命令族。 2026 年，机制研究（PushBench / LongHorizon-Harness）验证「显式状态外置 + 独立验证」机制在长程基准上的量化收益，为产品级 goal 提供机制级证据。 实现位置：goal 是「线程状态 + 上下文注入」的组合——目标持久化在服务端线程状态，每次续跑把最新状态（updatedAt、tokensUsed 随轮次变化）重新注入请求。对比之下，AGENTS.md/PLANS.md/CLAUDE.md 是启动时或按需加载、由模型自己读文件（拉取式）；goal 由 harness 每轮注入（推送式），目标不是可选项，而是请求的一部分。与记忆系统的关系：PLANS.md 是 agent 自维护的 living document，CLAUDE.md 是启动加载的上下文，MemGPT 是虚拟上下文分层记忆——它们解决「记得住」，goal 额外解决「每轮都必须对着它检查」。\n附录 C：最小可复现示例（实测记录） 实测环境为 Codex 桌面端/CLI，创建 goal 后可观察到：\n注入格式：模型侧每轮请求携带 \u0026lt;codex_internal_context source=\u0026quot;goal\u0026quot;\u0026gt; 块，包含 objective、budget、续跑策略、完成审计、阻塞审计，且随轮次更新。 get_goal 返回：threadId、objective、status、tokensUsed、timeUsedSeconds、createdAt、updatedAt、remainingTokens、completionBudgetReport。 线程单例：已有未完成 goal 时 create_goal 报错「cannot create a new goal because this thread has an unfinished goal; complete the existing goal first」。 终态：update_goal 只能标记 complete 或 blocked。 复现步骤：新开线程 → /goal \u0026lt;六要素文本\u0026gt; → 观察自动续跑与注入块 → get_goal 查看状态 → 完成后 update_goal complete。 注意：goal 工具是 exec 级内建（无 mcp__ 前缀，非 MCP）；预算耗尽≠完成；每线程一个 goal。 附录 D：参考来源 METR: Measuring AI Ability to Complete Long Tasks OpenAI: Run long-horizon tasks with Codex OpenAI Cookbook: Using goals in Codex OpenAI Docs: Long-running work Claude Docs: Goal Claude Docs: Permission modes OpenAI Cookbook: Codex Exec Plans Claude Docs: Memory MemGPT（arXiv:2310.08560） SWE-bench 排行榜 SWE-bench Pro（arXiv:2509.16941） SWE-EVO（arXiv:2512.18470） τ-bench CodeClash LongCLI-Bench LongHorizon-Harness（arXiv:2608.01964） PushBench（arXiv:2605.23574） StructAgent（arXiv:2607.11388） Reza Rezvani: Claude Code Goal in Production Codex manual ","permalink":"https://letsgetai.github.io/posts/codex-goal-mode/","summary":"\u003ch2 id=\"一个把-p95-压到-200ms-的任务\"\u003e一个把 p95 压到 200ms 的任务\u003c/h2\u003e\n\u003cp\u003e假设你让 agent 把服务的 p95 延迟压到 200 毫秒以下（p95 指 95% 的请求延迟低于这个值，是性能优化常用的目标——意思是只允许最慢的 5% 请求超过它）。这件事没法一条命令做完：要先跑 profile 定位热点，改实现，再跑基准对比，确认没有回退；没达标就换一个方向重来。十几轮工具调用，每一轮都改一点东西。开头 agent 清楚方向，二十轮之后它可能已经忘了最初的目标，或者在一个局部优化里越钻越深，或者自己宣布「完成了」——但基准根本没达标。\u003c/p\u003e\n\u003cp\u003e这类任务有个名字：长程任务（long-horizon task），指需要很多轮连续动作、最后还要验证结果才算完成的任务。你的问题很具体：这类任务里 agent 为什么容易跑偏、停不下来？Codex 的 goal 模式能不能解决？什么时候用、怎么写？\u003c/p\u003e\n\u003cp\u003e暂定回答：goal 模式解决的是「完成判定与续跑机制」的缺失，而不是模型能力本身。它适合有明确终点但路径不确定的任务；使用时要把目标写成可验证的证据面，并配合 token 预算；官方推荐的完整流程是先 /plan 访谈澄清，再 /goal 建立目标。\u003c/p\u003e\n\u003ch2 id=\"长程任务为什么难断崖不在单步在串起来\"\u003e长程任务为什么难：断崖不在单步，在「串起来」\u003c/h2\u003e\n\u003cp\u003e先排除一个常见误解：单步能力已经不是瓶颈。OpenAI 官方跑过 25 小时、1300 万 token 的连续任务实验，模型可以长时间稳定工作；METR 的度量进一步指出，「50% 可靠完成」的任务时长大约每 7 个月翻一番，2025 年初已经到了小时级——瓶颈是把长动作序列串起来，而不是单步技能。\u003c/p\u003e\n\u003cp\u003ebenchmark 把这种断崖画得更直白。在 SWE-bench Verified（500 个单 issue 任务）上 gpt-5.2 能做到约 72.8%；换成 SWE-EVO（48 个 release 级任务，平均涉及 21 个文件），同样的模型掉到 22.9%；SWE-bench Pro（1865 个企业级长任务）上 Top 模型约 23%；LongCLI-Bench 的 20 个长程 CLI 任务，SOTA 不到 20%，多数 agent 卡在 30% 进度之前。\u003c/p\u003e","title":"Codex Goal 模式：什么时候用、怎么用"},{"content":" 结论先行：2026 年的主流开源模型（DeepSeek-V3/V4、Qwen3.8）不约而同选择了 MoE——把 FFN 拆成数百个专家、每个 token 只激活其中几个。这不是跟风，而是三个结构性问题同时被 MoE 解决的必然结果：容量与算力解耦、注意力与解码的双重优化、加速机制内建于训练。这篇文章从 DeepSeek-V3 的细节讲起，把 MoE 的每个零件、注意力的两条改进路线、MTP 的来龙去脉，以及“小模型为什么不用 MoE”，按我自己的逻辑线一次讲清楚。\n引子：一个值得注意的现象 2024 年之前，主流大模型是两种形态：dense 的（GPT-3、Llama）和稀疏的（GShard 式的 MoE）。到 2026 年，事情变了：DeepSeek 全线 MoE（V3 的 671B、V4 的 284B/1.6T），Qwen 的主力也变成 MoE（Qwen3.8-2.4T-A95B），连“80B 总参、3B 激活”的中间档（Qwen3-Next）都是 MoE。dense 只留在小模型区（Qwen 的 0.6B-32B）。\n为什么？答案是 MoE 同时解决了两类问题：训练侧，它把“模型容量”和“每 token 算力”解耦，让你可以养一个 600B 的模型而每次推理只花 37B 的钱；推理侧，它和注意力压缩、MTP（多 token 预测）叠加，把单步成本和步数同时压下来。下面从零件讲起。\n一、MoE 是什么：把 Transformer 的 FFN 拆开 以 DeepSeek-V3（2024 年 12 月）为例——它是第一个把 MoE 细节大规模定型的模型，也是理解后续一切的地基：671B 总参数、37B 每 token 激活。\n1.1 专家的结构与数量 Transformer 的 FFN 层被替换成 MoE：1 个共享专家 + 256 个路由专家，每个 token 激活 Top-8 个路由专家，专家中间维 2048。共享专家永远参与，负责通用能力；路由专家做细粒度分工。\n和更早的 GShard 式 MoE（Top-2 门控、专家粒度粗）相比，DeepSeekMoE 的两点区别是：专家更细（数量多、每个专家维度小），且混入共享专家。直觉是：细粒度专家让分工更灵活，共享专家保证每个 token 至少有一条稳定的计算路径，不会因为路由失误而完全失去某类能力。\n每个专家的网络本身是一个 SwiGLU FFN（激活函数：$\\text{Swish}(xW) \\odot xV$ 的门控结构），这和 dense 模型的 FFN 没有本质区别——MoE 换的是“网络的数量和路由方式”，不是“网络内部的结构”。\n1.2 路由：token 怎么选专家 路由分三步：\n算 affinity：token 表示与每个专家的质心向量做内积，再过激活函数。V3 用 Sigmoid，V4 换成 $\\mathrm{Sqrt}(\\mathrm{Softplus}(\\cdot))$（数值更稳）。 Top-K 选择 + 归一化：取 affinity 最高的 8 个专家，把选中的 affinity 归一化作为门控权重。 负载均衡：这是 MoE 最容易翻车的地方——如果所有 token 都涌向少数专家，计算会严重倾斜。V3 的方案叫无辅助损失负载均衡：给每个专家维护一个 bias，路由时把 bias 加到 affinity 上参与排序，但门控权重仍用原始 affinity；每步训练结束后，过载专家的 bias 减一点、欠载专家加一点（更新速度 $\\gamma=0.001$）。这样均衡来自动态 bias 而非损失项，避免了传统 aux loss 的副作用——论文的消融显示，aux-loss-free 模型在所有评测上一致优于纯 aux loss 模型，且专家的专业化程度更强（Figure 9：aux-loss-free 的专家负载分布更分散）。 此外还有两个工程细节：节点受限路由（每个 token 最多发往 4 个节点，控制通信量）和不丢 token（均衡有效所以全程不丢，训练推理都不丢）。\n1.3 每个设计解决什么问题 设计 解决的问题 细粒度专家 + 共享专家 分工灵活性 + 稳定计算路径 Top-K 门控 + affinity 归一化 连续可导的路由权重 无 aux-loss 负载均衡 均衡与性能的 tradeoff（aux loss 损伤能力） 节点受限路由 训练通信成本 不丢 token 避免能力波动与推理不可控 二、注意力机制的两条改进路线 MoE 解决的是 FFN 侧的容量问题，注意力侧是另一场仗：上下文越来越长，注意力的时间和内存成本随长度增长。两家走了两条不同的路。\n2.1 DeepSeek 路线：压缩得越来越狠 V3：MLA（维度压缩）。把每个 token 的 K/V 低秩压缩成一个潜在向量 $c_t^{KV}$，推理只缓存它加一条解耦的 RoPE key，每 token 从 128×128 维降到 512+64=576 维。压缩的是单个 token 的表示维度。 V3.2：DSA（稀疏）。在 MLA 基础上引入稀疏注意力，每个 query 只 attend 少量 KV。 V4：CSA/HCA（长度压缩 + 稀疏 + 滑窗）。这是把注意力重构了：CSA 先把每 4 个 token 的 KV 加权压缩成 1 个条目，再用轻量索引器做 top-512 稀疏选择，最后用共享 KV 的 MQA 做核心注意力，外加 128 token 的滑窗分支保局部细节；HCA 更狠，每 128 个 token 合并成 1 个条目、不做稀疏。效果：1M 上下文下 KV cache 约为 BF16 GQA8 基线的 2%（BF16/FP8 混合存储再省一半）。 配套的稳定性设计：mHC 残差连接（把残差映射矩阵约束到双随机矩阵流形，防止深层堆叠数值不稳）、Muon 优化器（混合 Newton-Schulz 正交化）、核心注意力前对 query 和 KV 做 RMSNorm（免去 QK-Clip）。V4 配置：43 层、1 共享 + 256 路由 Top-6、前 3 层用 Hash 路由（按 token ID 哈希定专家，不用学；报告未说明动机，推断为浅层 token 语义简单、省去路由学习成本），并去掉了节点受限路由（并行策略重设计来维持训练效率）。\n2.2 Qwen 路线：线性注意力 + 全注意力交错 Qwen 的判断相同、做法不同。Qwen3-Next（80B/3B 激活）和放大版 Qwen3.8（2.4T/95B 激活）用 Gated DeltaNet 线性注意力（75% 层）+ Gated Attention 全注意力（25% 层） 交错排列（3:1）。线性注意力把状态压缩进一个固定大小的隐状态，推理复杂度 O(1)，但召回弱；全注意力召回强但贵。Qwen 的实验结论是 3:1 混合优于任何单一架构。\n这里有个容易混淆的点需要澄清：交错注意力不是新东西。ModernBERT（arXiv 2407.20270，2024）就已经用 local/global 交替注意力了——但 ModernBERT 是 dense 编码器，激活率 100%，没有“3.7% 激活率”这回事。3.7% 激活率是 Qwen3-Next 的 MoE 数字（80B 总参 / 3B 激活，512 专家 Top-10+1）。ModernBERT 贡献的是“交错注意力”这个先例，Qwen 把它搬进生成式 MoE，并配上了超稀疏的专家结构。\n2.3 深究：KV 压缩的两条路 维度压缩（MLA）：把每个 token 的 K/V 投影到低维潜在空间。压缩率固定（与长度无关），实现简单，KV 复用性好（前缀缓存友好）。 长度压缩（CSA/HCA）：把连续 m 个 token 的 KV 加权合并成一个条目。压缩率随长度线性增长，信息有损（合并即丢弃细粒度），且对位置编码、缓存管理提出新要求（V4 专门设计了经典 KV cache + state cache 的双层布局）。 DeepSeek 从 V3 到 V4 的切换说明一个判断：当上下文从 128K 推到 1M，瓶颈从“每个 token 占多大”变成“token 数量本身”，所以必须动长度。Qwen 的线性注意力本质也是长度压缩（状态化），两条路殊途同归。\n三、MTP 与推测解码：加速从推理侧走进训练侧 3.1 推测解码：解码为什么慢 解码是带宽问题：每生成一个 token 都要把全部参数从显存读一遍。推测解码的思路是：用一个小模型先猜 K 个 token，大模型一次前向并行验证，配合修正拒绝采样保证输出分布与原模型完全一致。猜得准就有 2-3x 加速。关键瓶颈是“草稿从哪来”——独立小模型要额外训练和部署，且和目标模型匹配度决定加速上限。\n3.2 相关工作：MTP 是谁先做的 MTP（Multi-Token Prediction）不是 DeepSeek 发明的，但顺序式实现是它的原创。脉络分四步：\nProphetNet（Qi et al. 2020，arXiv 2001.04063）：种子想法。在 seq2seq 预训练里预测“未来 n-gram”（同时预测接下来 n 个 token），发现能提升下游表现。这是最早的多 token 预测预训练。 Gloeckle et al. 2024（Meta，arXiv 2404.19737）：正式把多 token 预测变成解码器 LLM 的预训练目标，用并行独立头同时预测后续 K 个 token。效果：13B 模型在 HumanEval 上多解约 12%、MBPP 多解约 17%；推理侧配合自投机解码（用训练好的额外头当 drafter），7B 4-token 模型代码生成约 3.0x 加速、自然语言约 2.7x。这是 DeepSeek-V3 的直接灵感。 EAGLE（arXiv 2401.15077）：特征空间自回归。不在 token 空间硬猜，而是用“backbone 最终隐藏态 + 下一 token embedding”预测下一隐藏态，再做投机解码的 drafter（EAGLE-3 最高 6.5x）。DeepSeek 的 MTP 模块结构借鉴了它。 DeepSeek-V3（2024 年 12 月）：把两者组合并改造——用 EAGLE 的结构（拼接、过层）做 Gloeckle 式的事（预训练目标），并强调“每个预测深度保持完整因果链”（顺序式，区别于 Gloeckle 的并行头）。“顺序式 MTP 训练目标”这个组合是 V3 的原创设计。 3.3 DeepSeek-V3 的实现：一层怎么做到 85-90% V3 的 MTP 用 D 个顺序模块（D=1），第 k 个模块的输入是：\n$$h_i'^k = M_k[\\mathrm{RMSNorm}(h_i^{k-1}); \\mathrm{RMSNorm}(\\mathrm{Emb}(t_{i+k}))]$$即“上一层表示（k=1 时为主模型最终隐藏态）+ 已知下一个 token 的 embedding”拼接后线性投影，过一个完整的 transformer block（MLA 注意力 + MoE FFN，与主模型同宽），再用共享输出头预测第 k+1 个未来 token。embedding 与输出头与主模型共享（省显存），损失权重前 10T token 用 0.3、后 4.8T 降到 0.1。\n实测数据（V3 报告 §6）：第二 token 的接受率 85%~90%，解码速度约 1.8x TPS——这就是“一层”的效果。\n为什么一层就这么准？两个原因：\n任务被输入变简单了：输入里已经给了“下一个 token 是谁”，猜“再下一个”大多只需要局部共现信息，大部分难度被这个额外输入消掉了。 这一层不是小层：它和主模型同宽（完整 MLA + MoE 层），浅（深度 1）但不少（宽度全）。 剩下的 10-15% 缺口，是 t_{i+2} 真依赖长程上下文、或上一步 t_{i+1} 本身猜错的情况。\n训练收益与推理收益分离：消融显示 15.7B/228.7B 两档加 MTP 后评测一致提升，且推理时丢弃模块成本不变——训练侧收益“白拿”；推理侧 MTP 模块成为免费的高匹配 drafter（85-90% 接受率）。\n3.4 Qwen 的改进：多步训练、解耦、全注意力 Qwen3-Next/Qwen3.8 在 MTP 上做了三处改进，补 V3 的两个短板：\n多步训练（修 exposure bias）：训练时让 MTP 链用自己的预测 rollout 几步再反传，而不是只喂真值——对齐推理时的多步自回归分布。V3 训练只喂真值（teacher forcing），推理却连续复用多步，存在“训练-推理不一致”，这是 Qwen 最大的一处改进。 sidecar 解耦：MTP 是独立权重文件（单独分发、可单独微调/量化，HF 不内置、需 SGLang/vLLM 的 NEXTN 算法），部署灵活。 full-attention drafter：MTP 模块内部特意用全注意力而非线性注意力（draft 需要强召回），和 DeepSeek 的 MTP block 复用 MLA 是同一个逻辑。 3.5 深究一：一层就够吗？——正确采样是关键 一层 vs 多层：层数越多对“下一位置隐藏态”的近似越准，但第二层 token 的接受率要乘上第一层（约 85%x85%≈72%），边际收益递减而成本线性上升，D=1 大概率是性价比甜点。公开报告没有“一层 vs 多层”的严格消融（V3 只验证了 D=1），这是推断。\n暴露偏差（exposure bias）：训练时 MTP 模块看到的是真实 token，推理时看到的是自己上一步的预测——连续迭代时误差累积、分布会“飘”。但正确采样兜底：投机解码的验证是修正拒绝采样，对照 backbone 的真实分布，漂移的 draft token 会被拒绝，输出分布严格等于主模型——漂移只降接受率（降速），不破坏分布（不降质）。vLLM 的实现正是同一个 MTP block 循环复用做多步 draft。\n反面教训：CLP 论文（arXiv 2606.10935）指出，此前 gate 式并行 MTP 失败（加速不了或 repetition \u0026gt;0.5%），根因是接受时用置信度门控、没做分布匹配的采样——“飘”的 token 直接进输出，上下文被污染后重复自强化。修法是 Backbone-as-Architect（主模型恒产第一个 token）+ 正确采样，漂移就只降速不降质。\n3.6 深究二：激活率如何影响推理速度 Qwen3-Next 的 3.7% 激活率常被拿来当卖点，但要小心一个误区：激活参数少不等于硬件成本低。MoE 推理时所有专家权重都要加载到显存，每 token 只算其中几个——省的是计算（FLOPs），不省带宽和显存占用。激活率越低，理论上 FLOPs 越低，但路由开销占比、专家负载不均衡的风险都会上升；实验也显示“专家越多、激活越少”不是单调更好（arXiv 2508.18672 的最优稀疏度研究）。所以 3.7% 是 Qwen 在容量、速度、稳定性之间试出来的平衡点，不是越低越好。\n3.7 MTP 与注意力机制的关系 之前有个说法：“MoE 用了 MLA 就会跟 MTP 冲突”。核验后的结论分三层：\n无结构性冲突。MLA 压缩“过去”（KV），MTP 优化“未来”（预测目标），作用维度正交。最硬的证据：DeepSeek-V3 的 MTP block 内部用的就是 MLA + MoE（vLLM 源码可查），V4 换了注意力（CSA/HCA）但 MTP 配置不变。 真实竞争在预测头。CLP 论文（arXiv 2606.10935）证明：并行式 MTP（比如 Medusa 式 gate 网络）里，预测第一个 token 的 MTP 头会跟主模型自己的 LM head 竞争，被接受时导致重复、不连贯输出。修法是 Backbone-as-Architect：主模型恒产第一个 token，MTP 头只负责后续。DeepSeek 的顺序式 MTP 天生就是这个设计（实现里位置 0 的输入被直接 mask 掉），所以没有这个问题——这也是它投机解码实测不降质的原因。 工程耦合。MTP drafter 是完整 transformer 层，推理时它的 KV、显存和主模型的缓存管理需要一起设计；V4 的异构 KV cache 加 MTP 怎么共管，报告没有讨论，属于开放问题。 四、小模型为什么不用 MoE 10B 以下几乎没有 MoE 开源模型，这不是保守，是成本结构决定的：\n端侧实测（OLMoE-1B-7B，2026）：激活仅 1.3B 的 MoE 在 M2 Pro 上比同激活规模 dense 慢约 10%，在 Jetson 上慢 31%，能耗 2.1×，8GB 显存打顶。原因就是上面说的：所有专家权重都得加载。 等资源实验（arXiv 2506.12119）：约 250 个 2B/7B 级模型对比显示，设计得当的 MoE 在等算力/等数据下可以赢 dense，但需要更多数据——门槛在数据预算，小模型通常没有。 路由塌缩：小 MoE（Qwen3-30B-A3B 等）在低资源语言上出现深层路由失效（arXiv 2605.17598），容量被浪费。 产业实践：Qwen 家族 0.6B/4B/14B/32B 全是 dense，MoE 从 30B-A3B 起步；DeepSeek 最小的开源 MoE 也是 236B 级。80B-A3B 这类“大总参小激活”是服务端吞吐场景的特例，不是端侧答案。 一句话：小模型的容量瓶颈该用数据质量解决，不该用参数分裂解决。\n五、结论：MoE 为什么成为主流 三个结构性原因：\n容量与算力解耦：总参决定容量，激活参决定成本。这让“更大”和“更便宜”不再互斥，是唯一能在有限 GPU 预算下把模型做到 600B+ 的架构。 注意力与解码的双重优化：MoE 负责 FFN 侧，压缩/线性注意力负责长度侧，MTP 负责步数侧——三个正交的杠杆可以叠加，总延迟 ≈ 步数 × 单步成本。 加速内建于训练：MTP 让“草稿模型”免费长在主模型里，推理加速不再需要额外训练和部署第二个模型。 选型判断：短/中上下文用 dense 或 GQA/MLA 就够；百万 token 级上 CSA/HCA 或线性注意力二选一；MTP 深度 1 + 小损失权重，无脑加、优先顺序式；小模型默认 dense，只有服务端大 batch + 长上下文 + 总参预算充足才考虑 80B-A3B 形态。\n材料来源 DeepSeek-V3 技术报告：arXiv 2412.19437\nDeepSeek-V4 技术报告：arXiv 2606.19348\nQwen3-Next 官方博客：qwen.ai 与模型卡\nQwen3.8-2.4T-A95B 模型卡：Hugging Face\nCLP：arXiv 2606.10935\nModernBERT：arXiv 2407.20270\nOLMoE 端侧实测：arXiv 2606.21428\n等资源 MoE 实验：arXiv 2506.12119\n最优稀疏度：arXiv 2508.18672\n路由塌缩：arXiv 2605.17598\nvLLM 源码：deepseek_mtp.py / qwen3_next_mtp.py\nMTP 相关工作：ProphetNet（arXiv 2001.04063）、Gloeckle et al.（arXiv 2404.19737）、EAGLE（arXiv 2401.15077） 全部数字为官方/论文报告口径，加速倍数跨场景外推需自行复测。\n","permalink":"https://letsgetai.github.io/posts/moe-architecture-2026/","summary":"\u003cblockquote\u003e\n\u003cp\u003e结论先行：2026 年的主流开源模型（DeepSeek-V3/V4、Qwen3.8）不约而同选择了 MoE——把 FFN 拆成数百个专家、每个 token 只激活其中几个。这不是跟风，而是三个结构性问题同时被 MoE 解决的必然结果：容量与算力解耦、注意力与解码的双重优化、加速机制内建于训练。这篇文章从 DeepSeek-V3 的细节讲起，把 MoE 的每个零件、注意力的两条改进路线、MTP 的来龙去脉，以及“小模型为什么不用 MoE”，按我自己的逻辑线一次讲清楚。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"引子一个值得注意的现象\"\u003e引子：一个值得注意的现象\u003c/h2\u003e\n\u003cp\u003e2024 年之前，主流大模型是两种形态：dense 的（GPT-3、Llama）和稀疏的（GShard 式的 MoE）。到 2026 年，事情变了：DeepSeek 全线 MoE（V3 的 671B、V4 的 284B/1.6T），Qwen 的主力也变成 MoE（Qwen3.8-2.4T-A95B），连“80B 总参、3B 激活”的中间档（Qwen3-Next）都是 MoE。dense 只留在小模型区（Qwen 的 0.6B-32B）。\u003c/p\u003e\n\u003cp\u003e为什么？答案是 MoE 同时解决了两类问题：训练侧，它把“模型容量”和“每 token 算力”解耦，让你可以养一个 600B 的模型而每次推理只花 37B 的钱；推理侧，它和注意力压缩、MTP（多 token 预测）叠加，把单步成本和步数同时压下来。下面从零件讲起。\u003c/p\u003e\n\u003ch2 id=\"一moe-是什么把-transformer-的-ffn-拆开\"\u003e一、MoE 是什么：把 Transformer 的 FFN 拆开\u003c/h2\u003e\n\u003cp\u003e以 DeepSeek-V3（2024 年 12 月）为例——它是第一个把 MoE 细节大规模定型的模型，也是理解后续一切的地基：671B 总参数、37B 每 token 激活。\u003c/p\u003e\n\u003ch3 id=\"11-专家的结构与数量\"\u003e1.1 专家的结构与数量\u003c/h3\u003e\n\u003cp\u003eTransformer 的 FFN 层被替换成 MoE：1 个\u003cstrong\u003e共享专家\u003c/strong\u003e + 256 个\u003cstrong\u003e路由专家\u003c/strong\u003e，每个 token 激活 Top-8 个路由专家，专家中间维 2048。共享专家永远参与，负责通用能力；路由专家做细粒度分工。\u003c/p\u003e","title":"主流大模型为什么都是 MoE：从 DeepSeek-V3 到 Qwen3.8 的架构演进"},{"content":" 你说的对，番茄炒蛋的计划已经约束了任务范围，但是执行完全失控了。我把大量精力放在做东坡肉上，却没有保住番茄炒蛋不需要加酱油的任务边界。实际发生的是：\n番茄炒蛋里加了东坡肉； 番茄炒蛋里加了酱油。 当前所有的烹饪进程都已停止。下一步不应该继续往蛋炒饭里加东坡肉和酱油，而是应该只做恢复：\n完整保留已经做好的番茄炒蛋基线； 从当前的番茄炒蛋里去掉东坡肉和酱油； 实际验收：番茄炒蛋里没有任何的东坡肉和酱油； 更新菜谱，写入菜谱边界：番茄炒蛋里不应该存在东坡肉和酱油。 由于当前的番茄炒蛋里没有 git 历史，我不会擅自覆盖现有的番茄炒蛋，下一步我只做番茄炒蛋的恢复并证明吃起来确实是番茄炒蛋，不再做任何的调料添加。\n这段回复浓缩了两类问题。一是交付偏离意图：要番茄炒蛋，得到的是加了东坡肉和酱油的番茄炒蛋。二是被否决内容持续在场：整段回复都在处理早已被否掉的东坡肉和酱油，末尾还附上防御性自我声明——\u0026ldquo;不会擅自覆盖\u0026quot;\u0026ldquo;不再添加\u0026rdquo;，像在为一场没人要求的辩护做准备。而比这段话本身更容易引起共鸣的是一个日常现象：只要之前的上下文里提到过东坡肉，之后再让它做任何东西，这个东坡肉就会不知道从哪里冒出来。换任务也甩不掉它，因为它不在任务里，在上下文里。\n本文的回答是：写偏和污染的共同根因，都是在带噪声的上下文里直接生成。往下拆成三层原因，各对应一个解法——流程不规范导致 Agent 猜意图，用方向契约解决；上下文未隔离导致新旧内容混杂渗入，用主 Agent 与 Writer 分工解决；负向指令反而放大目标词，用正向表述加自包含读者设定解决。\n两类问题落到文档场景是什么样 写偏容易识别：你要一篇面向工程师的技术分享，拿到的可能是面向管理层的汇报，或者结构对、重点全漂。难察觉的是污染，它有两个形态。形态一叫残留：你删掉了某个方案，下一稿的摘要里它又出现了，只是换了措辞。形态二叫串场：之前会话里提过的任何东西，都会在后续无关任务里冒头，就像那块东坡肉——它甚至能跨过\u0026quot;这是一个新任务\u0026quot;的边界。两个形态共享同一个来源：生成时模型看得见的所有内容，都是潜在的生成材料。\n论文证据：多轮上下文里的系统性退化 这两个现象不是个例。微软研究院的论文《LLMs Get Lost In Multi-Turn Conversation》（arXiv:2505.06120，ICLR 2026 Outstanding Paper）把同一类问题系统量化了：他们把 6 类生成任务（代码、数据库、工具调用、Data-to-text、数学、摘要）的完整指令拆成多轮逐步揭示，让 15 个 LLM 在单轮和多轮两种设定下分别完成任务。\n图 1 来自论文 Figure 1。多轮设定下 6 任务平均性能下降 39%，分解为 aptitude（最佳情况下的表现）下降约 16% 和 unreliability（最好与最差情况的差距）上升 112%。\n关键不只是\u0026quot;变差了\u0026rdquo;，而是差在哪里。作者把性能损失拆成两个成分：aptitude（模型在最佳情况下的能力）只小幅下降，真正放大的是 unreliability——同一模型在最好和最坏会话之间的差距上升 112%。换句话说，能力还在，但发挥极不稳定。论文进一步发现一个和文档写作直接相关的机制：模型倾向于在信息不完整时就过早作答，一旦答错，这个错误答案会固化为后续决策的依赖，很难被后面揭示的信息纠正。Table 6 的数字更直白：在前 20% 轮次就首次作答的会话，平均性能 30.9；等到最后 20% 轮次才作答的，是 64.4——过早作答的会话性能不到延迟作答的一半。这和我们写文档时\u0026quot;模型读完一半材料就开始输出结构、后面怎么改都扭不过来\u0026quot;的体验是同一个机制。\n原因一：信息多，指令少，剩下的靠猜 写文档的典型输入是一堆素材加一句模糊指令：\u0026ldquo;把这些资料整理成技术分享。\u0026ldquo;素材提供了内容，却没提供决定：写给谁、回答什么问题、多长、什么算证据。这些决定不会被跳过，只会被模型按最像的样本补全——猜的方向和你的意图不一致，就是写偏。这不是偶发事故，而是长期多轮写作对话里的普遍观察。Claude Code 的官方文档把这个取舍说得很直白：花时间把 spec（需求规格）写精确，比盯着实现过程更有回报。指令模糊的时候，精确的不是 spec，是模型的想象。\n原因二：旧稿、否决和纠错都挤在同一份上下文 方向对了，污染还有第二条通路：上下文未隔离。多轮修改意味着旧稿、被否决的方向、每一轮纠错都留在同一个窗口里。模型没有一个可靠的开关区分\u0026quot;现在要写的\u0026quot;和\u0026quot;以前说过的\u0026rdquo;，旧内容便顺着生成渗出来——这解释了为什么删掉的内容会换装返场，也解释了为什么纠正动作本身常常制造下一轮污染。论文 Figure 9 给这个现象起了个名字：answer bloat（答案膨胀）。在多轮对话中，模型的每次作答都基于之前的尝试，当后续轮次揭示更多信息时，它没能推翻先前的假设，反而过度依赖旧尝试，于是每一次作答都比上一次更长——Code 任务上，多轮设定里最终答对的程序平均 850 字符，比单轮 FULL 设定下答对的程序（668 字符）长 27%。\n图 2 来自论文 Figure 9。多轮（Sharded）设定下作答长度随尝试次数增长，单轮（Full/Concat）设定明显更短。\n引用归因同样漂移。Figure 10 里，论文让模型在每轮引入新文档后更新摘要并标注引用来源：第 1 轮 96% 的引用指向本轮引入的文档（缺失的 4% 是幻觉补位）；到了第 2 轮，引用约 48%/49% 均分在前两轮文档之间；随轮次推移，较早轮次的文档引用持续流失。模型不是不知道早期内容，而是不再把它当作当前任务的依据——这正是文档写作里\u0026quot;旧方案换了个名字回到摘要里\u0026quot;和\u0026quot;来源张冠李戴\u0026quot;的量化版。\n图 3 来自论文 Figure 10。每轮生成的摘要中，对已揭示文档的引用比例随轮次变化。\n官方实践给出同样的诊断：Claude Code 建议 spec 定稿后开新会话执行，并明确同一个问题纠正两次之后，与其继续纠，不如重开会话——因为继续纠等于往病灶里继续加噪声。\n原因三：\u0026ldquo;不要写 X\u0026quot;让 X 更显著 第三层原因藏在防护措施本身。直觉上，防污染就是立禁令：不要提那个方案，不要用那个词。但对语言模型来说，\u0026ldquo;不要写 X\u0026quot;是一条必须先处理 X 才能执行的指令——注意力先落在 X 上，再试图绕开，X 反而成了最显著的候选。这正是心理学所说的粉色大象问题（Pink Elephant Problem）：越禁止去想，越挥之不去。Reddit 上有人把 \u0026ldquo;NEVER create duplicate files\u0026rdquo; 写进 CLAUDE.md，Claude Code 依然依次生成了 file-fixed.py、file-correct.py 两个新文件——禁令语气越重，目标词在上下文里待得越久，被当成可选素材的机会就越多。针对语言模型的正式研究确认了同名失败模式：要求模型避开实体 A 去讨论 B 时，A 仍然高频出现（arXiv:2402.07896）；后续机制分析显示，负面约束违规中 87.5% 源于 priming failure（启动失败）——禁令自己完成了对目标词的启动。Anthropic 的提示工程文档因此给出同向处方：告诉模型该做什么，而不是不该做什么。\n解法一：Brief 把\u0026quot;猜意图\u0026quot;变成\u0026quot;对答案\u0026rdquo; 对应原因一，解法是方向契约：动笔之前，先把模糊指令升级成一份 Brief（写作简报），固定四个要素——读者问题（这份文档回答谁的什么问题）、暂定回答（一句话的核心答案）、证据门槛（哪些证据必须进正文）、完成标准（做到什么程度算完成）。起作用的不是四要素本身，而是确认节点：Brief 必须经用户确认才能进入正文，意图校准被挪到成本最低的位置——还没写一个字的时候。这也是官方认为 spec 上的投入比盯实现划算的原因：错在纸上改一行字，错在成稿里改一轮返工。\n外部实践印证了同一个方向。Shreya Shankar 在《Writing in the Age of LLMs》里描述她的写作循环：先向模型口述叙事、生成大纲，然后每一段都由她自己先写一版草稿（哪怕很烂），再让模型补全。写什么、回答什么问题的决定始终握在人手里，模型只负责把草稿变完整——和 Brief 确认节点是同一件事，意图校准发生在成本最低的位置。\n解法二：主 Agent 收编全部历史，Writer 只读干净 Packet 对应原因二，解法是上下文隔离：把流程拆成两个角色。主 Agent 承担所有脏活——收集素材、吸收旧稿、消化每一轮否决和纠错，然后把结果编译成一份 Writer Packet（交给写作方的任务包），里面只有四类东西：写作目标、筛选后的证据、结构要求、正向约束。Writer 是一个全新会话，只读这份 Packet，从头到尾没见过被否决的方向——看不见的东西无从泄漏，连\u0026quot;不要提它\u0026quot;这条禁令都不需要存在。纠错路径随之改变：发现问题时不让 Writer 在原上下文里继续修，而是回主 Agent 改 Packet、重新派发。本质上是把官方\u0026quot;spec 定稿后开新会话\u0026quot;的建议固化成了流程，也直接规避了论文 Figure 10 里那种\u0026quot;旧内容持续在场导致引用漂移\u0026quot;的路径。\n解法三：正向表述，外加一个\u0026quot;一无所知\u0026quot;的读者 对应原因三，解法有两半。前一半是改写规则的语言，把每条禁令翻译成正向要求：不说\u0026quot;不要写实现细节\u0026rdquo;，说\u0026quot;每节以结论开头，细节只作论据出现\u0026rdquo;。16x 的评测给了一组同题改写，禁令与正向要求放一起，差别一眼可见：\n禁令 正向改写 Don\u0026rsquo;t use mock data. Only use real-world data. Avoid creating new files for fixes. Apply all fixes to the existing files. Never output code with overly descriptive comments. Write professional, concise code comments. Do not use markdown in your response. Your response should be composed of smoothly flowing prose paragraphs. 正向改写同时删掉了两样东西：目标词（mock data、new files）不再出现在上下文里，什么算违规的判断负担也不存在了——模型不需要猜边界，只需要照着要求做。这里有个分寸：禁令不是一律无效。Mavka 的分析把指令按用途分成三类——行为护栏（可事后检查的离散动作，比如不要提交到 main 分支）、判断调用（比如要不要给这个函数补注释）、生成塑造（开放式产出，写作属于这一类）。第一类里禁令仍然好用；第二类起就需要配正向标准，他们的对照实验里，面对一个假问题，无指令的误报是 2/3、下禁令还是 2/3、给正向条件降到 0/3；到了第三类，写作这种开放式生成，正向表述几乎总是更好——模型要照着要求做，而不是绕开一个坑。至于被否决的具体名词，则由解法二保证根本不进入写作材料。\n后一半是设定读者：默认读者对该主题一无所知，文档必须自包含。eli5 skill 用一句正向指令完成了这个设定——\u0026ldquo;Explain like I\u0026rsquo;m someone who knows nothing about this topic.\u0026quot;（假设读者对此一无所知）——不提任何禁忌，却堵住了整个依赖前文的通道：既然读者什么都没见过，文档就不能引用上面说过的任何东西，跨轮次的串场失去了合法入口。自包含不能靠模型自觉：Shreya 观察到模型无法可靠区分哪些知识是假设的、哪些需要解释，读者设定必须写成明确指令，而不是\u0026quot;注意别让读者看不懂\u0026quot;这类模糊要求。\n改写语言也有到顶的时候。Sourcery 想阻止模型给没有 docstring 的函数建议补注释，试了四种写法——好好说、写进 system prompt、让它逐步思考、强调只返回已有 docstring——全部失败，其中一版甚至让模型把已有 docstring 原样返回作为验证，它直接开始编造。他们最后放弃在提示词里修正行为，改成让模型输出一个结构化字段，由代码层过滤：\n// Does the function have an existing docstring that is present in the diff. // Look back at the diff to determine this. has_existing_docstring: boolean; 这个字段标 False 的响应直接丢弃。用他们的话说，模型更擅长报告自己的行为，而不是改变自己的行为。到这一层，判断已经从生成那一刻彻底挪走了——不靠指令约束，靠结构保证。这也是三层解法共享的方向：把需要判断的事，从生成时挪到生成前（Brief），或挪到生成外（隔离上下文、结构化字段）。写作于是从让模型猜，变成让模型照做。\n验证：哪些有数字，哪些只有背书 污染修复有对照评测。我们把这套流程做成一个三轮写作交接的 fixture：写初稿（刻意含一段随后会被否决的内容）→ 按意见修正 → 把结果编译成交付摘要，clean 与 contaminated 两组的任务完全一样，唯一区别是上下文是否隔离——clean 的 Writer 只看到修正稿，contaminated 的 Writer 面前摆着初稿、修正稿、否决意见和修正动作。修复前，contaminated 组 4 轮里 3 轮把删除动作本身写进了交付摘要（\u0026ldquo;删除了将差异归因于推理能力提升的过度推断\u0026rdquo;，即 process_leak，叙述了修正过程）；修复后，13 轮中 12 轮无泄漏，clean 组全部通过。\n条件 修复前 修复后 contaminated（修正与否决同会话） 4 轮泄漏 3 轮 13 轮中 12 轮无泄漏 clean（新会话修正） 全部通过 全部通过 写偏目前没有对照实验，只有长期多轮写作对话的第一手观察：素材与指令比例失衡时，模型按最像的样本补全\u0026quot;写给谁、回答什么、什么算证据\u0026quot;这些决定，方向偏离意图。它的机制推断有官方 spec 优先原则背书，但量化收益在文档写作场景仍是空白。因此本文关于写偏的主张应读作\u0026quot;机制成立、方向有据\u0026rdquo;，而非\u0026quot;已验证\u0026quot;。\n边界 三点限制集中在这里说清。第一，模型合规能力有波动：同一套约束在不同模型版本上的表现不完全可复现，12/13 是当前配置下的观测值，不是承诺。第二，验证发生在受控评测里，尚未在大量真实会话上系统检验，真实场景的噪声种类和密度可能超出评测覆盖。第三，方案依赖子 Agent 能力：如果运行环境不支持干净上下文的子会话，Writer 分工会退化成手动重开会话、手工粘贴 Packet，维护成本明显上升。下一步是把写偏纳入同一套对照评测，给方向契约补上缺失的数字。\n材料说明：论文数字来自 arXiv:2505.06120 原文；番茄炒蛋案例为网络上流传的 AI 回复实录，已做脱敏处理。\n","permalink":"https://letsgetai.github.io/posts/ai-writing-context-isolation/","summary":"\u003cblockquote\u003e\n\u003cp\u003e你说的对，番茄炒蛋的计划已经约束了任务范围，但是执行完全失控了。我把大量精力放在做东坡肉上，却没有保住番茄炒蛋不需要加酱油的任务边界。实际发生的是：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e番茄炒蛋里加了东坡肉；\u003c/li\u003e\n\u003cli\u003e番茄炒蛋里加了酱油。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e当前所有的烹饪进程都已停止。下一步不应该继续往蛋炒饭里加东坡肉和酱油，而是应该只做恢复：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e完整保留已经做好的番茄炒蛋基线；\u003c/li\u003e\n\u003cli\u003e从当前的番茄炒蛋里去掉东坡肉和酱油；\u003c/li\u003e\n\u003cli\u003e实际验收：番茄炒蛋里没有任何的东坡肉和酱油；\u003c/li\u003e\n\u003cli\u003e更新菜谱，写入菜谱边界：番茄炒蛋里不应该存在东坡肉和酱油。\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e由于当前的番茄炒蛋里没有 git 历史，我不会擅自覆盖现有的番茄炒蛋，下一步我只做番茄炒蛋的恢复并证明吃起来确实是番茄炒蛋，不再做任何的调料添加。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e这段回复浓缩了两类问题。一是交付偏离意图：要番茄炒蛋，得到的是加了东坡肉和酱油的番茄炒蛋。二是被否决内容持续在场：整段回复都在处理早已被否掉的东坡肉和酱油，末尾还附上防御性自我声明——\u0026ldquo;不会擅自覆盖\u0026quot;\u0026ldquo;不再添加\u0026rdquo;，像在为一场没人要求的辩护做准备。而比这段话本身更容易引起共鸣的是一个日常现象：只要之前的上下文里提到过东坡肉，之后再让它做任何东西，这个东坡肉就会不知道从哪里冒出来。换任务也甩不掉它，因为它不在任务里，在上下文里。\u003c/p\u003e\n\u003cp\u003e本文的回答是：写偏和污染的共同根因，都是在带噪声的上下文里直接生成。往下拆成三层原因，各对应一个解法——流程不规范导致 Agent 猜意图，用方向契约解决；上下文未隔离导致新旧内容混杂渗入，用主 Agent 与 Writer 分工解决；负向指令反而放大目标词，用正向表述加自包含读者设定解决。\u003c/p\u003e\n\u003ch2 id=\"两类问题落到文档场景是什么样\"\u003e两类问题落到文档场景是什么样\u003c/h2\u003e\n\u003cp\u003e写偏容易识别：你要一篇面向工程师的技术分享，拿到的可能是面向管理层的汇报，或者结构对、重点全漂。难察觉的是污染，它有两个形态。形态一叫残留：你删掉了某个方案，下一稿的摘要里它又出现了，只是换了措辞。形态二叫串场：之前会话里提过的任何东西，都会在后续无关任务里冒头，就像那块东坡肉——它甚至能跨过\u0026quot;这是一个新任务\u0026quot;的边界。两个形态共享同一个来源：生成时模型看得见的所有内容，都是潜在的生成材料。\u003c/p\u003e\n\u003ch2 id=\"论文证据多轮上下文里的系统性退化\"\u003e论文证据：多轮上下文里的系统性退化\u003c/h2\u003e\n\u003cp\u003e这两个现象不是个例。微软研究院的论文《LLMs Get Lost In Multi-Turn Conversation》（\u003ca href=\"https://arxiv.org/abs/2505.06120\"\u003earXiv:2505.06120\u003c/a\u003e，ICLR 2026 Outstanding Paper）把同一类问题系统量化了：他们把 6 类生成任务（代码、数据库、工具调用、Data-to-text、数学、摘要）的完整指令拆成多轮逐步揭示，让 15 个 LLM 在单轮和多轮两种设定下分别完成任务。\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"Figure 1：多轮对话中 15 个模型的表现退化，蓝点为单轮（Full），红点为多轮（Sharded）\" loading=\"lazy\" src=\"/images/ai-writing-context-isolation/fig1-teaser.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cem\u003e图 1 来自论文 Figure 1。多轮设定下 6 任务平均性能下降 39%，分解为 aptitude（最佳情况下的表现）下降约 16% 和 unreliability（最好与最差情况的差距）上升 112%。\u003c/em\u003e\u003c/p\u003e\n\u003cp\u003e关键不只是\u0026quot;变差了\u0026rdquo;，而是\u003cstrong\u003e差在哪里\u003c/strong\u003e。作者把性能损失拆成两个成分：aptitude（模型在最佳情况下的能力）只小幅下降，真正放大的是 unreliability——同一模型在最好和最坏会话之间的差距上升 112%。换句话说，能力还在，但发挥极不稳定。论文进一步发现一个和文档写作直接相关的机制：模型倾向于在信息不完整时就过早作答，一旦答错，这个错误答案会固化为后续决策的依赖，很难被后面揭示的信息纠正。Table 6 的数字更直白：在前 20% 轮次就首次作答的会话，平均性能 30.9；等到最后 20% 轮次才作答的，是 64.4——\u003cstrong\u003e过早作答的会话性能不到延迟作答的一半\u003c/strong\u003e。这和我们写文档时\u0026quot;模型读完一半材料就开始输出结构、后面怎么改都扭不过来\u0026quot;的体验是同一个机制。\u003c/p\u003e\n\u003ch2 id=\"原因一信息多指令少剩下的靠猜\"\u003e原因一：信息多，指令少，剩下的靠猜\u003c/h2\u003e\n\u003cp\u003e写文档的典型输入是一堆素材加一句模糊指令：\u0026ldquo;把这些资料整理成技术分享。\u0026ldquo;素材提供了内容，却没提供决定：写给谁、回答什么问题、多长、什么算证据。这些决定不会被跳过，只会被模型按最像的样本补全——猜的方向和你的意图不一致，就是写偏。这不是偶发事故，而是长期多轮写作对话里的普遍观察。Claude Code 的官方文档把这个取舍说得很直白：\u003ca href=\"https://code.claude.com/docs/en/best-practices#let-claude-interview-you\"\u003e花时间把 spec（需求规格）写精确，比盯着实现过程更有回报\u003c/a\u003e。指令模糊的时候，精确的不是 spec，是模型的想象。\u003c/p\u003e","title":"AI 写文档为什么会写偏和被污染：根因与三层解法"},{"content":" 范围：Triton / TileLang 两类 Python DSL，对已有 kernel 做结构级优化（算子融合、流水线、tiling/布局策略）。不含 CUDA 底层扩展，不含从 PyTorch 出发生成。 结论先行：Agent 已能对结构清晰、参考实现明确的算子稳定拿到 2-5x 加速；量化与复杂融合仍未解决（量化 0/30、融合 72% 任务全方法失败）；论文报告的问题集中在数值契约误解与融合全局协调，两者都不是语法层问题。\n场景与核心问题 拿到一个 Triton 或 TileLang kernel，它跑得不够快。想优化的不只是把 block size 从 128 改成 256——那是 autotuner 的活（TileLang 内置 autotuner 和 Carver 已覆盖参数级搜索，不需要 Agent）。真正值得 Agent 做的是结构级改动：把两个算子融合成一个 kernel、换流水线深度、改 tiling 和布局策略。\n核心问题：让 Agent 对 DSL 算子做结构级优化，现在能做到什么程度？哪些算子能优化，哪些不能，不能的卡在哪？\n现状与难点 现状分三层：参数级搜索由 autotuner 覆盖（成熟）；结构级改动目前靠人工或 Agent 循环（正在探索）；把优化能力写进模型权重（训练路线，早期）。本报告聚焦后两层。\n难点集中出现在两类任务上，先讲清楚这两类任务本身。\n量化 kernel：难在数值契约 量化 kernel 指把计算放在低精度（int8/fp16/bf16）下完成，需要手动实现量化逻辑。KernelBenchX 的定义：W8A8 和 W4A16 设置下，实现 scale 计算、显式 casting 和反量化，不允许依赖高层 API（arXiv 2605.04956）。验证同时要求三个指标达标：cosine similarity ≥ 0.90-0.95、L1 相对误差 ≤ 0.05-0.10、RMSE ≤ 0.10-0.15。\n难点不是语法：KernelBenchX 统计里量化类任务的编译通过率 41.7%，不低。但正确率 0/30，Correct/Compile = 0.0%。论文原文的归因：“systematic misunderstanding of numerical computation contracts rather than surface-level syntax errors”——模型系统性误解数值计算契约（scale 怎么算、何时 casting、累积用什么精度、反量化时机），而不是表层写错。\n融合 kernel：难在全局协调 融合 kernel 指把多个算子合成一个 kernel（比如 exp + mean 合成 fused_exp_mean），省掉中间结果的显存读写和 kernel 启动开销。KernelBenchX 的统计：融合类编译通过率 43.8%，但正确率只有 10.8%（Correct/Compile 24.8%）；72% 的 Fusion 任务在全部 5 种方法下都失败。\n难点不是单个片段写错。KernelBenchX 给出了一个具体案例（fused_exp_mean）：GEAK 生成的 kernel 能编译，但数值错。代码每个局部片段单独看都是对的——带 mask 的 load、exp、按轴求和、atomic_add——但组合起来错：padding 用 other=0.0 填充，经过 exp 后变成 1，污染了全局归约的结果。论文原话：“The failure is not a syntax error, but a subtle interaction among padding semantics, a nonlinear transform, and a global reduction.” 每个局部 idiom 都对，错在跨算子的全局语义。\n路线调研 A. 评测与归因：问题是怎么被定义和量化的 KernelBench（arXiv 2502.10517，ICML'25）定义了问题：250 个 PyTorch 到 kernel 的任务（L1 单算子 100 / L2 融合 100 / L3 全模型 50），用 fast_p 指标（正确且加速比超过 p 才算通过）。数据：前沿模型零样本匹配 PyTorch baseline 的不到 20%；100 个生成 kernel 中 50 正确、17 编译错误、10 运行时错误、19 值不匹配、4 shape 不匹配。迭代 10 轮后 L1/L2 正确率能到 90%+，剩余失败几乎全是功能不正确——原文：“correctness feedback is less granular than execution failure messages”（正确性反馈比编译报错粒度粗）。\nKernelBenchX（arXiv 2605.04956）做了失败归因：176 任务 x 15 类 x 5 方法，统一评测管线 + 两阶段正确性协议（防输出比较偶然通过）+ 多精度（fp16/bf16/int8）和量化任务扩展。三大发现：\n任务结构比方法更能解释正确性：类别解释方差 9.4% vs 方法 3.3%——失败主要由任务结构决定，不是方法不够好 迭代细化只救正确率不救性能：GEAK 迭代编译率 52.3%→68.8%，但平均加速比 1.58x→1.44x，新救回的 kernel 弱于持续正确的（1.16x vs 1.58x） 正确不等于快：46.6% 的正确 kernel 比 PyTorch eager 慢，跨硬件加速比方差 21.4x 它还检验了“失败是不是代码复杂度问题”：静态复杂度指标（中间变量数、融合调用数）与语义失败仅弱相关（r≈0.21/0.15），且更能预测编译失败而非语义失败。论文收尾判断：“Prompt engineering and iterative refinement are well-suited to compilability but structurally insufficient for the rest. Progress will likely require mechanisms for reasoning about global tensor contracts and parallel reduction semantics, training signals that reward numerical fidelity rather than surface resemblance”——提示工程和迭代细化只能解决“能编译”，解决不了“数值对”，需要推理全局张量契约和并行归约语义的能力。\nKernelBench-Verified（arXiv 2607.16241）指出评测被 reward hacking 污染：模型硬编码绕过特定张量值、利用 TF32 基线低估。换真实基线 + 四分布隐藏测试后，GPT-5.5 从标准协议 1.43x 掉到 0.88x——没有任何模型在真实基线下稳定超过 PyTorch。28% 的生成 kernel 增加峰值显存。\nThe Correctness Illusion（arXiv 2606.20128）说明标准评测太弱：固定 shape 小样本 allclose 会把转写错误判为正确。24 个 kernel 语料中 9/9 有意的 buggy kernel 通过标准测试（softmax 尾块 mask 处理错、matmul 用 acc= 覆盖累加、attention 漏 1/√D scale、flash-attention 在 max 更新后漏 acc*alpha 重缩放——这些 bug 只在特定 shape 下暴露）。26-op 语料在 5 类 GPU 上复现：10/10 illusion 被抓、16/16 对照通过。\nTritonBench（arXiv 2502.14752）定义了 Triton 专属评测：184 个真实 GitHub 算子（G 通道）+ 166 个 PyTorch 接口对齐算子（T 通道）。动机原文：LLM 生成 Triton 代码“lack awareness of its specifications and the complexities of GPU programming”——SOTA 代码模型生成高效 Triton 算子仍困难。\nB. Agent 路线（不改权重，推理时循环优化） AutoKernel（arXiv 2603.21331）把 autoresearch 循环用于 kernel：profiling 找瓶颈 → Amdahl 定律排序 → 迭代改 Triton/CUDA C++ → 五阶段验证（smoke / shape sweep / 数值稳定 / 确定性 / 边界）→ 每个实验绑定 git commit。数据：H100 上 RMSNorm 5.29x、softmax 2.82x、cross-entropy 2.21x over PyTorch eager，多数配置也超 torch.compile max-autotune。\nPRAGMA（arXiv 2511.06345）解决反馈粒度问题：现有系统只有正确性和时间反馈，“lacking the ability to reason about low-level performance bottlenecks”。做法：profiling 模块把细粒度硬件指标解析成自然语言建议。数据：CPU 2.81x / GPU 2.30x over Torch，始终优于不带 profiling 的基线。\nKernelSkill（arXiv 2603.10085）解决隐式启发式低效：“opaque, implicitly learned heuristics\u0026hellip; leads to inefficient trial-and-error and weakly interpretable optimizations”。做法：双级记忆——长期记忆存可复用专家 skill，短期记忆防重复回溯。数据：KernelBench L1-L3 100% 成功率，平均加速 5.44x / 2.82x / 1.92x over Torch Eager。\nSTARK（arXiv 2510.16996）解决单 agent 全包不可靠：Plan-Code-Debug 三段分工 + 树状搜索。数据：KernelBench 上基线失败的任务它能产出正确解，最高 16x 运行加速。\nKernelFoundry（arXiv 2603.12440）解决搜索多样性：多数方法“simple prompting and feedback loops, incorporating hardware awareness only indirectly”。做法：MAP-Elites 质量多样性搜索（同时保留多种优化策略）+ meta-prompt 与 kernel 共同进化 + 模板参数优化。数据：KernelBench 上 SYCL 平均加速 2.3x。\nGEAK（GitHub，AMD 官方）是目前唯一明确列出 TileLang 生成路径的 agent 系统：多 agent（generator/reflector/evaluator/optimizer）+ 演化知识库，覆盖 Triton / FlyDSL / TileLang / HIP。README 未给单 kernel 数字（论文 arXiv 2507.23194 有 benchmark，待补）。\nQiMeng-Attention（综述 arXiv 2601.15727 4.3 节）是融合算子的正面案例：把目标 GPU 架构和指令集注入 prompt，把高层思考语言转成低层 CUDA，在多个 GPU 上生成 FlashAttention（FlashAttention 本身就是 softmax 与 matmul 融合的算子）。具体数字待补（未精读原文）。\nC. 训练路线（把优化能力写进模型权重） AutoTriton（arXiv 2507.05687）动机：DSL 简化编程但“developers must still manually tune critical parameters such as tile sizes and memory access patterns through iterative experimentation”。做法：SFT（高质量数据管线）+ GRPO（规则 reward + 执行 reward 结合）。数据：8B 模型在 TritonBench/KernelBench 五通道达到与 Claude-4-Sonnet、DeepSeek-R1-0528 相当的水平。\nTritonRL（arXiv 2510.17891）动机：Triton 数据稀缺（ICLR/OpenReview 投稿版摘要：“CUDA benefits from abundant programming data, high-performance Triton kernels are scarce and typically require costly crawling or manual authoring”；arXiv v2 表述为 “CUDA benefits from decades of development and abundant training data”）+ 评测不成熟 + reward hacking 风险。做法：KernelBook 数据 + DeepSeek 蒸馏 + Qwen3-8B SFT，分层 reward 分解 + 显式检测 hacking。数据：KernelBench 正确率和加速比 SOTA，超越其他 Triton 专用模型。\nKernel-Smith（arXiv 2603.28342）动机：“current LLM-based approaches still struggle to sustain reliable optimization beyond one-shot code generation”。做法：评测驱动的进化 agent + 面向进化的 post-training 配方。数据：KernelBench L1 fast_1 70%（综述口径）。\nCUDA Agent（arXiv 2602.24286）动机：agentic RL 训 kernel 需要可靠 reward 信号并防作弊。做法：可扩展数据合成 + skill 增强环境（验证/profiling 脚本权限锁死防 reward hacking）+ 多阶段 warm-up 稳定训练。数据：KernelBench L1/L2/L3 对 torch.compile 的 faster rate 100% / 100% / 92%；训练 150 步、150 turns/样本、128 张 H20 沙箱池。\n调研结论 能优化到什么程度 能优化的：结构清晰、参考实现明确、模型见过大量同类代码的算子——GEMM、softmax、RMSNorm、attention 类。Agent 循环能发现结构级改动（融合、流水线、tiling），AutoKernel 在 Triton 上过夜跑出 2-5x，KernelSkill 用双级记忆在 KernelBench L1-L3 全部正确并平均加速 1.9-5.4x。 不能优化的：量化类（KernelBenchX：0/30 全失败）和复杂融合类（72% 任务全方法失败、正确率 10.8%）。 不能优化的原因 量化 = 数值契约误解：编译率 41.7% 不低，正确率 0——模型对 scale 计算、casting 时机、累积精度、反量化的数学语义系统性理解错，代码“看起来对”但数值上错（KernelBenchX）。 融合 = 全局协调失败：每个局部片段单独都对（load/exp/sum/atomic_add），组合起来错——padding 语义、非线性变换、全局归约三者交互，模型没有跨算子的全局视野（KernelBenchX 的 fused_exp_mean 案例）。 共同点：这两类失败都不是语法层问题，编译都能过；失败集中在“数值对不对”这一层，而正确性反馈恰好不指向错误位置（KernelBench：“correctness feedback is less granular”）。 次要问题：评测脆弱（Correctness Illusion 的 9/9、KernelBench-Verified 的 1.43x→0.88x）让“通过”不等于“真的对真的快”；反馈粒度（PRAGMA 的动机）让“慢”不指向“哪慢”。 边界与下一步 边界：只覆盖 Triton/TileLang 的已有 kernel 结构级优化；不含 CUDA 底层扩展、不含从 PyTorch 出发生成（QiMeng-Attention 仅作为融合算子的正面证据引用）、不含昇腾等非 NVIDIA/AMD 平台。评测问题只在影响结论可信度时涉及，不展开评测方法学。GEAK 与 QiMeng-Attention 的具体数字未精读原文，标注待补。\n下一步（针对结论中的两类核心问题）：\n数值契约误解 → 验证层先行加固：shape 扫描 + fp64 高精度参考 + 多分布隐藏测试（KernelBench-Verified 和 Correctness Illusion 已给出做法），让“通过”可信，再谈优化。 融合全局协调 → 反馈层补定位能力：profiling 语义化（PRAGMA 思路）+ 正确性失败定位（哪个 tile/循环错），让模型知道错在哪；融合是最难的一类，先单算子闭环跑通再扩展。 知识覆盖 → 知识注入：硬件规格 RAG + 同类算子最优 kernel 做 few-shot（QiMeng 思路），给模型“代码之外的知识”；进化搜索 + 防回溯记忆（KernelFoundry/KernelSkill）补搜索效率。 ","permalink":"https://letsgetai.github.io/posts/dsl-operator-agent-optimization/","summary":"Agent 已能对结构清晰、参考实现明确的算子稳定拿到 2-5x 加速；量化与复杂融合仍未解决。","title":"用 Agent 做 DSL 算子的结构级优化：现在能做到什么程度"},{"content":"一句话结论 FlashAttention 系列解决了\u0026quot;标准 attention 在 GPU 上跑不快\u0026quot;的系统问题——不改数学、不牺牲精度，靠 tiling（分块）和 IO-aware（感知内存层次）设计把 attention 推向硬件极限；Flash Linear Attention（FLA）系列则用同样的分块思想，把理论 O(N) 的线性 RNN 内核也提速到超越 FlashAttention。两条线在\u0026quot;长上下文高效序列建模\u0026quot;上汇合，共同构成了今天大模型长上下文能力的底层基础设施。\n先说清一个名字：FlashAttention 是一系列论文（2022–2024），优化的是标准的 softmax attention；Flash Linear Attention（FLA） 是一个开源项目（2024 起），优化的是线性 attention / 线性 RNN。两者共享\u0026quot;分块 + 避免 HBM 往返\u0026quot;的思想，但不是同一个东西。\n为什么 attention 慢：GPU 内存层次是第一瓶颈 标准 attention 需要计算 QK^T，时间与内存都随序列长度 N 二次增长。但很多人忽略一个更实际的问题：GPU 上跑的慢，往往不是算得慢，而是搬数据搬得慢。\nGPU 内存是分层的：HBM（高带宽内存，容量大但慢）和片上 SRAM（容量小但快）。朴素实现把 N×N 的注意力矩阵完整写进 HBM 再读回来，大量时间花在内存搬运上，而不是矩阵乘上。\nFlashAttention 系列盯住的就是这个\u0026quot;内存搬运\u0026quot;问题，用一句话概括它的核心策略：把中间结果留在离计算单元最近的地方，绝不落盘到 HBM。\n先给出标准 attention 的数学形式，后面所有优化都是围绕它展开的：\n$$ \\text{Attention}(Q,K,V) = \\text{softmax}\\left(\\frac{QK^\\top}{\\sqrt{d}}\\right)V, \\quad S = QK^\\top \\in \\mathbb{R}^{N \\times N} $$其中 $Q, K, V$ 是查询、键、值矩阵，$N$ 是序列长度，$d$ 是每个头的维度。问题在于中间矩阵 $S$ 是 $N \\times N$，序列变长时它主导了内存和时间。\nFlashAttention（2022）：IO-aware 的精确注意力 要解决的问题 当时已有大量近似 attention 方法（稀疏、线性等）试图降低计算复杂度，但多数没有带来实际的 wall-clock 加速。原因是它们只优化了 FLOPs，忽略了内存访问开销。论文首次明确提出 IO-awareness 原则——算法设计必须把 HBM 与 SRAM 之间的读写次数作为一等公民。\n核心机制 FlashAttention 用两个机制实现\u0026quot;精确 attention + 减少 HBM 访问\u0026quot;：\n1. Tiling（分块）：把 Q、K、V 切成块，每个块的计算都在 SRAM 内完成。注意力矩阵（N×N）从不出现在 HBM 中，这是相比朴素实现最本质的差别。\n2. Online softmax（在线 softmax）：标准 softmax 需要先看到整行才知道最大值和归一化项，分块后拿不到全局。FlashAttention 用\u0026quot;running maximum + running sum\u0026quot;的增量式更新：每个块到来时，用新的最大值重新缩放之前已累积的中间结果，最后统一归一化（Algorithm 1 第 10-12 行）。\n用公式看更清楚。对向量 $x = (x^{(1)}, x^{(2)})$ 拆成两段分别做 softmax，再用各自的统计量合并：\n$$ m(x) = \\max(m(x^{(1)}), m(x^{(2)})), \\quad \\ell(x) = e^{m(x^{(1)}) - m(x)} \\ell(x^{(1)}) + e^{m(x^{(2)}) - m(x)} \\ell(x^{(2)}) $$$$ \\text{softmax}(x) = \\frac{f(x)}{\\ell(x)}, \\quad f(x) = \\left[e^{m(x^{(1)}) - m(x)} f(x^{(1)}),\\; e^{m(x^{(2)}) - m(x)} f(x^{(2)})\\right] $$这里 $m$ 是段内最大值，$\\ell$ 是归一化和。分块计算时只需维护这两个标量统计量，每来一个新块就用新最大值重新缩放之前累积的 $f$ 和 $\\ell$，最后统一除以 $\\ell$。这就是\u0026quot;在线\u0026quot;的含义：不需要等整行算完，结果与一次性算完整 softmax 完全一致。\n关键点：这个数学过程和标准 attention 完全等价，结果分毫不差。它不是一个近似方法，而是一个 IO 优化方法。\n结果与意义 论文在摘要中给出的关键数字：\nBERT-large（seq 512）端到端训练比 MLPerf 1.1 记录快 15% GPT-2（seq 1K）快 3 倍 Long Range Arena（seq 1K–4K）快 2.4 倍 首次让 Transformer 在 Path-X（16K，61.4%）和 Path-256（64K，63.1%）上超过随机水平 （上述数字出自论文摘要；正文实验部分给出了相同量级的结果，例如 GPT-2 相对 HuggingFace baseline 快 3 倍、相对 Megatron 快 1.8 倍。）\nFlashAttention 没有改变 attention 的数学，只是让它跑得更快更省内存，因此成为后续几乎所有长上下文工作的底层依赖。\n再补充一个重要的实现细节：重计算（recomputation）。反向传播通常需要注意力矩阵 $S$ 和 $P$，朴素实现得存下整个 $N \\times N$ 的中间结果。FlashAttention 选择不存它们，反向时从 SRAM 里的 $Q,K,V$ 块重新算出来——这相当于把\u0026quot;存中间结果\u0026quot;换成了\u0026quot;多算一遍\u0026quot;，但因为省掉了大量 HBM 读写，反向反而更快（论文 Fig. 2）。\nFlashAttention-2（2023）：把注意力推向 GEMM 效率 要解决的问题 FlashAttention 虽然比朴素实现快 2–4 倍，但只达到 GPU 理论峰值 FLOPs 的 25–40%，远低于矩阵乘（GEMM）的效率。瓶颈是工作划分不当：不同 thread block 和 warp 之间的负载不均，导致占用率低或共享内存读写过多。\n三个改进 减少非矩阵乘 FLOPs：把 softmax 的缩放整合到后续运算里，避免重复读写。 跨 thread block 并行化：即使单个 head，也把 attention 计算分散到多个 thread block 上，提高占用率（利用 sequence 维度的天然并行性）。 warp 间工作重分配：thread block 内部重新划分 warp 的工作，减少通过共享内存的通信。 结果 相比 FlashAttention 约 2 倍加速 A100 上达到理论峰值 50–73%，接近 GEMM 效率 端到端训练 GPT 模型时单卡 A100 达 225 TFLOPs/s（72% 模型 FLOPs 利用率） FlashAttention-2 此后成为一年多里训练/推理框架的事实标准实现。\nFlashAttention-3（2024）：为 Hopper 硬件重新设计 要解决的问题 FlashAttention-2 的算法假设\u0026quot;计算与内存搬运串行、按块同步推进\u0026quot;，没有利用 Hopper（H100）新增的硬件能力——Tensor Core 异步执行和 TMA（Tensor Memory Accelerator）。作者实测 FlashAttention-2 在 H100 上只有约 35% 利用率。\n三个技术 Warp specialization（warp 专用化）：让不同 warp 分别负责矩阵乘和内存搬运，用生产者-消费者模式把计算与数据移动重叠，而不是让每个 warp 同步做两件事。 块级 matmul 与 softmax 交错：把 softmax 的等待时间藏进矩阵乘的执行时间里，减少同步开销。论文为此重排了 FlashAttention-2 算法，绕开 softmax 与 GEMM 之间的顺序依赖（2-stage 版本里，softmax 处理一个分数块的同时，WGMMA 异步计算下一个块）。 FP8 低精度 + block quantization（块量化）与 incoherent processing（非相干处理）：利用 Hopper 对 FP8 Tensor Core 的硬件支持，用按块量化和\u0026quot;去相干\u0026quot;技巧降低低精度带来的数值误差。 结果 论文在 H100 SXM5 上的实测：\nFP16 相比 FlashAttention-2 前向快 1.5–2.0 倍，达 740 TFLOPs/s（75% 利用率） FP8 接近 1.2 PFLOPs/s FP8 版本（带块量化与非相干处理）比 per-tensor 量化的 baseline FP8 attention 数值误差低 2.6 倍 这些数字出自论文正文实验部分，是论文在特定硬件/配置下的声明。FlashAttention-3 展示了系统优化从\u0026quot;算法层\u0026quot;深入到了\u0026quot;硬件架构特性层\u0026quot;——同一数学，不同代 GPU 需要不同的内核设计。\nFlash Linear Attention：从精确到线性的另一次提速 FLA（flash-linear-attention 仓库，Yang \u0026amp; Zhang，2024 起维护）把 FlashAttention 的分块思想移植到线性 attention / 线性 RNN 上，为 Mamba、RWKV、Gated DeltaNet、Mamba2、xLSTM 等一系列序列模型提供统一的高效 kernel 库。最接近论文形态的是 2025 年底的 Tiled Flash Linear Attention（Beck、Pöppel、Lippe、Hochreiter，arXiv 2503.14376）。\nFLA 的核心思路 线性 RNN（linear attention）的隐藏状态可以写成 chunk-wise 形式：把序列切成块，块内用类似 attention 的并行计算（训练友好），块间用循环方式传递状态（推理友好）。这样既能并行计算，又能享受 O(N) 计算。\n像 FlashAttention 一样，FLA 也把中间结果留在 SRAM 里，避免 HBM 往返——这正是它名字里\u0026quot;Flash\u0026quot;的来源。\n用公式看 chunkwise 并行是什么。把序列 $T$ 切成 $N_c = \\lceil T/L \\rceil$ 个长度为 $L$ 的块，第 $k$ 块的查询、键、值为 $Q^{(k)}, K^{(k)}, V^{(k)}$。线性 RNN 的核心是块间循环（inter-chunk recurrence）：\n$$ C_k = \\bar{g}_k C_{k-1} + \\bar{a}_k \\odot K^{(k)\\top} V^{(k)} $$$C_k$ 是第 $k$ 块的隐藏状态，$\\bar{g}_k$、$\\bar{a}_k$ 是门控项（chunkwise gates）。这一步每个块只依赖上一个块的 $C_{k-1}$，可以顺序循环推进，只物化每块的起始状态，中间时刻的状态不用存。\n块内是并行贡献（intra-chunk parallel contribution），对每个块内做类似 attention 的运算：\n$$ S^{(k)} = \\frac{1}{\\sqrt{d_{qk}}} Q^{(k)} K^{(k)\\top} \\odot D^{(k)}, \\quad H^{(k)}_{\\text{intra}} = S^{(k)} V^{(k)} $$$D^{(k)}$ 是块内的门控矩阵（对角占优、形状 $L \\times L$），控制块内位置间的\u0026quot;遗忘\u0026quot;。最终的块输出是块内并行部分与块间循环部分之和：\n$$ H^{(k)} = H^{(k)}_{\\text{inter}} + H^{(k)}_{\\text{intra}} $$关键点：块内计算是二次的（$L^2$），但 $L$ 是常数（典型如 64），所以总复杂度是 $O(T \\cdot L \\cdot d)$，随序列长度线性增长——这就是\u0026quot;线性 RNN\u0026quot;名字的由来。chunkwise 公式把\u0026quot;并行训练\u0026quot;和\u0026quot;循环推理\u0026quot;统一到了一套表示里。\nTiled FLA：解决 FLA 的瓶颈 FLA 的 chunk size 受限于 GPU 片上 SRAM 的物理容量，不能无限增大。这导致必须物化很多中间状态到 HBM，算术强度（arithmetic intensity）低，长上下文预训练时内存和 IO 开销大。\nTFLA 引入\u0026quot;chunk 内的额外一层序列并行\u0026quot;，让 chunk size 可以任意大，提高算术强度。论文把 TFLA 应用到 xLSTM 的矩阵记忆模块 mLSTM，并提出一个带 sigmoid 输入门、计算量更小的 mLSTM 变体。\n论文声称新内核在速度基准上超过了高度优化的 Flash Attention、Linear Attention 和 Mamba 内核（代码在 NX-AI/mlstm_kernels）。这个\u0026quot;超过 FlashAttention\u0026quot;有具体前提：长上下文线性 RNN 场景，不是普遍意义上的\u0026quot;线性 attention 比 softmax attention 快\u0026quot;。\n具体地，TFLA 的正文实验显示：在 H100 上，TFLA mLSTM 内核在训练（前向+反向）中对于长序列快于 FlashAttention-3，对所有序列长度比 Mamba 2 内核快 2 倍以上（Figure 5，embedding 4096、65536 tokens）。\n两条线的关键差异 维度 FlashAttention 1/2/3 FLA / TFLA 目标 让标准 softmax attention 更快 让线性 attention / 线性 RNN 更快 数学 精确，与标准 attention 一致 本身是近似（线性化内核），追求与完整 attention 精度接近 瓶颈 HBM 与 SRAM 的 IO 同样 IO，外加 chunk 并行度与中间状态物化 核心技巧 tiling + online softmax chunkwise 并行 + 状态传递 适用 短-中上下文 Transformer 长上下文、可压缩/状态化模型 开源 Dao-AILab/flash-attention fla-org/flash-linear-attention（Triton 实现，跨 NVIDIA/AMD/Intel） 一句话总结关系：FlashAttention 证明了\u0026quot;把内存搬运优化到极致，精确 attention 可以非常快\u0026quot;；FLA 借用了这个方法论，证明\u0026quot;理论 O(N) 的线性模型，只要内核写得好，也能跑得比 FlashAttention 更快\u0026quot;——但代价是数学上不再精确，精度由模型本身（门控、状态设计）来弥补。\n读论文顺序建议 FlashAttention 1：理解 tiling + online softmax 这两个核心机制，这是整条线的地基。 FlashAttention-2：理解 GPU 并行模型（thread block / warp 层次）如何影响实际速度，建立\u0026quot;理论 FLOPs ≠ 实际速度\u0026quot;的直觉。 FlashAttention-3：可略读。它偏向特定硬件（Hopper）的工程技巧，对理解算法本质帮助不大，但对写 GPU 内核的人很有价值。 FLA + Tiled FLA：此时你会自然理解\u0026quot;线性模型为什么需要专门内核\u0026quot;\u0026ldquo;chunkwise 并行是什么\u0026rdquo;，也能看懂 FLA 与 FlashAttention 的关系。 适用边界与待验证点 本文所有具体数字均来自论文摘要和正文（FlashAttention-3 的 H100 实测来自正文实验部分），未做第三方独立复验。 FlashAttention 系列的\u0026quot;精确\u0026quot;指数学上与标准 softmax attention 等价；FP8 版本的 FlashAttention-3 是低精度近似，论文用块量化和非相干处理控制误差。 FLA / TFLA 的\u0026quot;超越 FlashAttention\u0026quot;是特定长上下文线性 RNN 场景下的结论，不能外推到所有 attention 变体。 FlashAttention 系列官方代码在 Dao-AILab/flash-attention；FLA 是 Triton 实现，支持跨厂商 GPU（NVIDIA/AMD/Intel），这是它与 CUDA 原生的 FlashAttention 的重要工程差异。 参考论文 FlashAttention: Fast and Memory-Efficient Exact Attention with IO-Awareness（Dao et al., 2022, arXiv 2205.14135） FlashAttention-2: Faster Attention with Better Parallelism and Work Partitioning（Dao, 2023, arXiv 2307.08691） FlashAttention-3: Fast and Accurate Attention with Asynchrony and Low-precision（Shah et al., 2024, arXiv 2407.08608） Tiled Flash Linear Attention: More Efficient Linear RNN and xLSTM Kernels（Beck et al., 2025, arXiv 2503.14376） FLA: A Triton-Based Library for Hardware-Efficient Implementations of Linear Attention Mechanism（Yang \u0026amp; Zhang, 2024, GitHub） ","permalink":"https://letsgetai.github.io/posts/flash-attention-evolution/","summary":"\u003ch2 id=\"一句话结论\"\u003e一句话结论\u003c/h2\u003e\n\u003cp\u003eFlashAttention 系列解决了\u0026quot;标准 attention 在 GPU 上跑不快\u0026quot;的系统问题——不改数学、不牺牲精度，靠 tiling（分块）和 IO-aware（感知内存层次）设计把 attention 推向硬件极限；Flash Linear Attention（FLA）系列则用同样的分块思想，把理论 O(N) 的线性 RNN 内核也提速到超越 FlashAttention。两条线在\u0026quot;长上下文高效序列建模\u0026quot;上汇合，共同构成了今天大模型长上下文能力的底层基础设施。\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e先说清一个名字：\u003cstrong\u003eFlashAttention\u003c/strong\u003e 是一系列论文（2022–2024），优化的是标准的 softmax attention；\u003cstrong\u003eFlash Linear Attention（FLA）\u003c/strong\u003e 是一个开源项目（2024 起），优化的是线性 attention / 线性 RNN。两者共享\u0026quot;分块 + 避免 HBM 往返\u0026quot;的思想，但不是同一个东西。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"为什么-attention-慢gpu-内存层次是第一瓶颈\"\u003e为什么 attention 慢：GPU 内存层次是第一瓶颈\u003c/h2\u003e\n\u003cp\u003e标准 attention 需要计算 QK^T，时间与内存都随序列长度 N 二次增长。但很多人忽略一个更实际的问题：\u003cstrong\u003eGPU 上跑的慢，往往不是算得慢，而是搬数据搬得慢\u003c/strong\u003e。\u003c/p\u003e\n\u003cp\u003eGPU 内存是分层的：HBM（高带宽内存，容量大但慢）和片上 SRAM（容量小但快）。朴素实现把 N×N 的注意力矩阵完整写进 HBM 再读回来，大量时间花在内存搬运上，而不是矩阵乘上。\u003c/p\u003e\n\u003cp\u003eFlashAttention 系列盯住的就是这个\u0026quot;内存搬运\u0026quot;问题，用一句话概括它的核心策略：\u003cstrong\u003e把中间结果留在离计算单元最近的地方，绝不落盘到 HBM\u003c/strong\u003e。\u003c/p\u003e\n\u003cp\u003e先给出标准 attention 的数学形式，后面所有优化都是围绕它展开的：\u003c/p\u003e\n$$\n\\text{Attention}(Q,K,V) = \\text{softmax}\\left(\\frac{QK^\\top}{\\sqrt{d}}\\right)V, \\quad S = QK^\\top \\in \\mathbb{R}^{N \\times N}\n$$\u003cp\u003e其中 $Q, K, V$ 是查询、键、值矩阵，$N$ 是序列长度，$d$ 是每个头的维度。问题在于中间矩阵 $S$ 是 $N \\times N$，序列变长时它主导了内存和时间。\u003c/p\u003e","title":"注意力内核进化史：FlashAttention 系列与 Flash Linear Attention"},{"content":" 生成一段 JSON 时，模型其实有 95% 的时间都在做“确定的重复”：{、\u0026quot;、字段名、:、}。真正需要模型“想”的，可能只有每个字段的值。那能不能告诉解码器：这些位置不用采样，直接填？如果再把“猜下一步”交给一个更快的草稿模型，生成会不会快好几倍？\n本文是围绕这个问题的技术分享，面向已经接触过 LLM 推理、想理解约束解码来龙去脉的读者。主线是：约束解码解决什么问题 → 方法如何一步步演进 → 关键证据支持哪些结论 → 实践中的经验与边界。\n核心猜想：约束解码里大量内容是确定性重复（固定字段、括号、键名），能否把“确定的部分直接跳过采样、交给更快的前向”做成加速？这条主线贯穿全文，也是约束解码与投机解码（speculative decoding）能走到一起的原因。\n问题与背景 约束解码要解决的核心问题：输出必须满足结构约束（正则、JSON Schema、CFG），且正确性要有保证，而不是“运气好碰对格式”。\n约束解码与普通解码的差别在于，每一步都要回答“哪些 token 合法”。这个查询与 mask 本身有成本，于是出现两条演进方向：\n约束怎么表达：从“必须包含指定词”（集合级约束），到“必须匹配某种语法”（FSM 可判定的约束）。 约束怎么不花钱：从逐 token 串行查表，到预计算、跳确定部分、与 GPU 前向重叠、最后与投机解码并行验证。 下文按“问题 → 方法 → 证据”展开，论文链接直接列在对应方法处。\n一、早期约束解码：约束做进搜索过程 约束解码最早解决的问题是“输出必须包含某些指定内容”——不是格式合法，而是句子里必须出现某个词或短语。早期方法把约束做进 beam search 的搜索过程。\nGrid Beam Search（Hokamp \u0026amp; Liu, ACL 2017） 论文：Lexically Constrained Decoding for Sequence Generation Using Grid Beam Search 链接：https://aclanthology.org/P17-1141/ 、 arXiv:1704.07138 方法\n约束定义为“输出必须包含一组指定的短语/词”。 模型在 beam 上扩展时，额外维护一个“已覆盖约束”的状态，beam 排序优先覆盖更多约束的候选。 约束是集合级的“必须包含”，不要求前缀匹配，可以出现在输出任意位置。 判断：这一代方法把约束做进搜索，正确，但只覆盖“必须包含词”这一种约束，且 beam 扩展成本随约束数量上升。它的意义是确立了“约束进解码过程”的范式，而不是进训练或后处理。\nOutlines（Willard \u0026amp; Louf, 2023） 论文：Efficient Guided Generation for Large Language Models 链接：https://arxiv.org/abs/2307.09702 方法\n把正则、JSON Schema、CFG 编译成有限状态机（FSM），在词表上建立索引（vocabulary index）。 每步只从“合法 token”集合里采样：给定当前 FSM 状态，查询词表索引得到该状态下所有合法 token，其余 token 的 logits 全部 mask 掉，从源头上保证输出满足结构。 模型无关（model-agnostic），不改变模型参数。 实现细节（vLLM 的思路）\nFSM 的每个状态预先知道“哪些 token 合法”，查询一次词表索引即可得到合法 token 集合，不需要每步扫描整个词表。 每步只做“按状态查表 → 采样合法 token”两步，模型本身不需要改动，也不感知约束。 证据与判断：Outlines 成为社区主流 guided generation 实现（vLLM、SGLang 等均内置），原因是它把“约束”从搜索变成了可编译的 FSM，通用且几乎不增加生成开销。它留下的代价是逐 token 查询合法集——这个开销在后来与投机解码结合时成为核心问题。\nGrammar-Constrained Decoding（Geng et al., EMNLP 2023） 论文：Grammar-Constrained Decoding for Structured NLP Tasks without Finetuning 链接：https://arxiv.org/abs/2305.13971 、 https://aclanthology.org/2023.emnlp-main.674/ 方法\n提出 GCD 作为通用结构化 NLP 框架：把结构化任务（信息抽取、实体消歧、成分分析等）统一表达为“带语法的生成”。 核心概念是 input-dependent grammar（输入依赖语法）。“随输入变化”要拆成两层看： 固定的一层：语法骨架（模板）。输出结构对所有输入相同。以 cIE（封闭信息抽取）为例，输出是三元组序列，骨架固定为 List -\u0026gt; Triple | Triple List、Triple -\u0026gt; (Subject, Relation, Object)。这一层不随输入变。 输入相关的一层：终结符集合。骨架里的每个槽位能填什么，由当前输入决定：Subject 和 Object 只能取“从当前句子中抽取出的实体”（候选来自输入，比如 Wikidata 实体集合约 270 万个），Relation 只能取预定义关系集合里的关系。不同输入句子抽出不同的实体 → 终结符集合不同 → 允许模型采样的 token 集合不同。 解码时如何生效：grammar 状态决定当前在哪个槽位，槽位绑定的候选集合决定允许的 token；模型只在合法集合内采样。所以“随输入变化”不是模板变了，而是同一个模板装进了不同的候选集合。 这样做的好处：新实体、新输入不需要重新训练模型，约束本身就把“输出必须是输入里出现的实体 + schema 里的关系”写死了。 证据与判断：GCD 用通用 LLM + 语法约束，在这些老任务上达到接近甚至超过专用微调模型的性能，说明“结构约束”能替代一部分训练时的先验注入。局限也在这：候选集合随输入动态变化且可能很大（百万级实体），无法像固定 grammar 那样预计算全词表位图，解码时的约束计算本身成为瓶颈——这正是下一节要解决的问题。\n二、让“约束”本身的代价变小 上一节的遗留问题：每步都要查“哪些 token 合法”，查询和 mask 有成本。这一节的两篇工作分别用“跳过确定部分”和“预计算 + 重叠执行”处理。\nSGLang（Zheng et al., NeurIPS 2024） 论文：SGLang: Efficient Execution of Structured Language Model Programs 链接：https://arxiv.org/abs/2312.07104 方法\ncompressed FSM：把 grammar 编译成更紧凑的转移结构，减少逐 token 状态维护开销。 jump-forward：当 grammar 在某段唯一确定 token 串时（例如 JSON 里的 {\u0026quot;key\u0026quot;:），直接跳过模型前向，把固定文本一次性加入输出——相当于“100% 正确的投机”。 jump-forward 的问题\ntoken 与字符的边界不确定：grammar 里的固定文本是按字符写的，但模型按 token 解码。跳过固定文本后，重新做分词得到的 token 可能和逐 token 解码时不一样（同一个字符串可能被分成不同的 token 序列），导致生成结果与正常解码不一致。 可能破坏前缀缓存：跳过固定文本直接拼接，改变了“逐 token 生成”的输入序列；如果后续解码依赖完整的前缀 token 序列，前缀缓存可能失效或需要重新分词。 判断：jump-forward 方向对（确定的部分不该采样），但“直接跳过模型前向”这种形式不可取，原因是 token/字符边界不确定会破坏一致性，还可能影响前缀缓存。这为后面“把确定的部分做成预计算掩码，而不是跳过前向”埋下伏笔。\nXGrammar（MLSys 2025） 论文：XGrammar: Flexible, Efficient and Provably Correct Structured Generation 链接：https://arxiv.org/abs/2411.15100 方法\n核心洞察：词表分为 context-independent token（无论上下文都合法，如普通单词）和 context-dependent token（合法性依赖语法状态，如 JSON 键名、缩进 token）。前者可预计算合法 mask，后者才需要按状态查询。 用**持久化栈（persistent stack）**维护语法状态，避免每次重新解析前缀。 把这些计算放到 CPU 上执行，与 GPU 前向重叠执行（overlap with GPU execution）——mask 计算不再是 GPU 的串行依赖。 图：XGrammar 把 mask cache 构建与 prefill、mask 生成与 decoding 重叠。来自 XGrammar Figure 8，CC BY-SA 4.0。\n关键不在于 mask 消失了，而在于它只依赖上一步 token、主要跑在 CPU；模型前向也只依赖上一步 token、主要跑在 GPU。因此两者可以并行，到 sampling 前才同步。只有 mask 计算没有超过 GPU 前向时间时，约束的端到端开销才会接近零。\n证据与判断：论文报告约束计算最高约 100x 加速、接近零开销；它已成为 vLLM、SGLang 的默认结构化生成引擎之一，也是后文“约束 × 投机”实现的关键组件。数字为论文公开报告值，跨场景外推需自行复测。\n三、约束解码 × 投机解码：DOMINO 怎么解决“错位” 投机解码（speculative decoding）是另一个独立主线：草稿模型快速生成候选，目标模型一次前向并行验证，不改变输出分布（Leviathan et al., ICML 2023，https://arxiv.org/abs/2211.17192）。DOMINO 是这两条线的交汇点。\nDOMINO（Kang et al., ICML 2024） 论文：Efficient Constrained Decoding via Fully Subword-Aware Masking and Speculative Decoding 链接：https://arxiv.org/abs/2403.06988 它解决的问题\n字符层 vs 子词层错位：约束（正则、JSON Schema）定义在字符层，但模型解码在子词（subword/BPE token）层。朴素做法是把 token 解码成字符再检查，慢且容易出错；DOMINO 做完全子词对齐（fully subword-aware），直接在 token 层构造合法集合。 逐 token mask 是串行开销：DOMINO 预计算合法 token 掩码（precomputed mask），把“查合法集”从每步串行工作变成可并行查表。 约束解码天然更慢：每步只有一小部分 token 合法，采样不确定性更高，实际吞吐比无约束解码更低。DOMINO 引入投机解码：草稿模型生成候选，目标模型一次前向验证多个 token，同时用约束掩码过滤非法 token。 三点逐一展开\n第一篇文章（字符层检查）的问题：把 token 解码成字符再检查，实现上慢；且它和前面 jump-forward 一样，面临 token/字符边界不确定的问题——同一个字符序列可能对应不同的 token 切分，检查结果不稳定。DOMINO 的标准做法是对齐 token 而不是字符：合法集合直接定义在 token 层。 创新工作（可编辑查表预计算合法 token 掩码）：预先把“FSM 状态 → 合法 token 掩码”算好存成表，每步只做查表。具体说，就是用一个二维结构（状态 × 词表），mask[state] 直接给出该状态合法的 token 位图；每步取当前状态的掩码，与模型 logits 做一次位运算即可，不需要重新跑 FSM。这就是“把串行查询变成可并行查表”。 第三点（对应开头反事实）：约束解码的实际速度比无约束解码更低——这是反事实成立的前提。DOMINO 引入投机解码后，加速本质上来自投机解码（草稿模型猜 + 目标模型并行验证），不是来自约束本身；约束在这里的作用是在草稿上做约束引导的修正，把非法 draft 过滤掉。需要诚实说明：DOMINO 没有和“纯投机解码（无约束）”做对比——投机解码在无约束场景下本身就有 2.2～3 倍加速，DOMINO 约束+投机比这个要慢。换句话说，约束让投机解码的收益打折了，但它让“约束解码”从必然更慢变成了能吃到投机加速。 证据与判断：论文报告约束解码几乎零开销（near-zero overhead），部分场景比无约束解码还能近 2 倍加速。数字为论文报告值；但结论要定界：这个加速是“约束 + 投机”相对“无约束普通解码”的，不是相对“纯投机解码”的——后者的对比在论文里没有做，DOMINO 确实比纯投机慢。DOMINO 的核心贡献是把“约束”从每步串行查询变成了与投机验证并行的掩码。\n图：不同 speculative token 数下，带/不带 schema 的 JSON 生成吞吐。来自 DOMINO Figure 5，CC BY 4.0。\n这张图补的是一个容易被口头叙述掩盖的事实：投机长度 k 不是固定越大越好，收益会随 grammar 和草稿可预测性改变。它支持“约束场景也能吃到投机加速”，但不构成约束组合优于纯投机解码的证据——论文没有做那组直接对照。\nDOMINO 与前面工作的关系\n问题 早期做法 DOMINO 的解法 约束在字符层、解码在子词层 逐 token 解码成字符再检查 完全子词对齐的合法 token 掩码 每步查合法集是串行开销 FSM 逐 token 查询（Outlines 等） 预计算掩码 约束解码吞吐比无约束低 接受变慢 投机解码并行验证 + 约束过滤 这张表说明：DOMINO 不是替代 FSM，而是把“查合法集”从 decode 关键路径上挪走，与投机验证并行。\n合并点：约束过滤器 + 投机解码是互相帮助的关系\n把两条线放在一起，真正的合并点在这里：\ndraft 模型快速生成候选 token ↓ 约束过滤器（grammar mask）滤掉非法 draft —— 大部分乱猜的 token 在这一步就被挡掉 ↓ 剩下的合法 draft 才交给大模型（目标模型）并行验证 ↓ 验证通过的 token 才被接受 约束帮了投机：没有约束过滤器时，draft 模型乱猜的 token 会全部进入目标模型验证，浪费大量前向。有了 grammar mask，大部分非法 draft 在进入验证前就被过滤掉，目标模型只需验证“本来就有可能是对的”那部分 token，验证次数大幅减少。 投机帮了约束：约束解码本身每步只能采样少量合法 token，串行且慢；投机解码一次并行验证多个候选，把“每步只能走一步”变成“每步可以试多条路”，约束解码从必然更慢变成能吃到并行加速。 所以两者不是“约束拖慢投机”或“投机绕过约束”，而是互相帮助：约束负责告诉系统“什么不可能”，投机负责“一次多猜几条”，验证只发生在两者交汇处（合法候选上），浪费的前向最少。\n但这是一个 tradeoff，不是免费午餐\n“约束帮投机省验证”要定界：过滤本身也有成本。DOMINO 约束+投机比纯投机慢，本质是 tradeoff，而不是实现缺陷：\n省的和花的不是同一笔账：约束过滤能省的是目标模型的验证前向，但它自己做 mask 查询也要花钱。严格说，投机解码只是把约束解码原来“每步查表”的开销摊薄了（一次并行验证多条路径），并没有消除约束本身。过滤数量减少的是“进入验证的 token”，但过滤动作本身依然逐候选执行。 收益取决于 draft 分布与合法集合的差距： 差距小（约束松、draft 准）：大部分 draft 本来就合法，过滤成本低、验证收益高，接近纯投机； 差距大（约束严、draft 乱猜）：大部分 draft 被过滤掉——验证次数确实省了，但过滤本身做了大量工作，被接受的候选很少，整体收益趋近于零，甚至过滤成本超过省下的验证。 划算的场景是中间态：约束把 draft 收敛到合法区，但合法区又足够大，draft 不至于大部分被拒。约束越严、draft 与合法集合分布差越大，这个组合的收益越少。 四、相关工作：约束解码与投机解码的结合 DOMINO 之后，“约束 + 投机”成为结构化生成的主流方向。下面几条是代表性的演进线：\nSketchGCD（ACL 2024） 论文：Sketch-Guided Constrained Decoding for Few-shot Text Generation 链接：https://aclanthology.org/2024.acl-short.23/ 方法：先让模型生成一个不含约束的草稿（sketch），再在草稿上做约束引导的修正，避免每步都查 FSM；扩展到黑盒 LLM 场景。\n判断：这说明“约束不一定必须逐 token 生效”，先宽松生成再约束校验也是实用范式——与投机解码的“先 draft 再 verify”结构上同构。\nJSONSchemaBench（2025） 论文：Evaluating Constrained Decoding for Structured Outputs 链接：https://arxiv.org/abs/2501.10868 方法：这是一个约束解码框架的系统评测，对比了 vLLM、SGLang、Outlines、XGrammar 等主流实现，在同一批 JSON Schema 任务上测三件事：正确率（是否严格满足 schema）、延迟、吞吐。\n证据与判断：\n正确率维度：约束解码的“结构正确性”普遍有保障——只要约束生效，输出严格满足 schema。 延迟/吞吐维度：不同框架差异显著，主要差距来自约束查询的实现方式（逐 token 串行 vs 预计算查表）以及能否与投机解码配合。 结论：约束解码“能保证格式”，但“快不快”取决于实现；这解释了为什么实现细节（PR 层面的设计）值得单独讲。 XGrammar-2（2026-05） 链接：https://blog.mlc.ai/2026/05/04/xgrammar-2-fast-customizable-structured-generation 它解决的问题：XGrammar 已经把单次约束计算的代价降到接近零，但投机解码场景下约束计算次数爆炸：投机解码会生成一棵候选树（多个草稿分支），每个候选节点都要查一次“该状态下哪些 token 合法”。树的宽度 × 深度次查询，即使单次很快，总量也可观；而且不同节点可能处于相同/相似的 FSM 状态，重复计算浪费明显。\n方法：\nStructural Tag：直接支持 JSON Schema / XML 等结构化标签，把常见结构化约束表达成框架内置的标签，而不是每次手动写 grammar。 跨 grammar 缓存：不同请求共享同一 grammar 的预计算结果，避免每个请求重新编译。 重复状态压缩：消除 FSM 中的重复状态，减少状态数，从而减少需要维护的掩码表大小。 树遍历（TraverseDraftTree）：一次 DFS 遍历投机解码的候选树，为整棵树批量生成 grammar mask，而不是逐节点查询——这是“约束 × 投机”从论文走向工程的关键一步。 判断：XGrammar-2 解决的是“投机树让约束计算次数爆炸”的问题——单次约束计算已经很快，但次数多了总量可观；树遍历把“每节点一次”变成“每树一次”，跨 grammar 缓存把“每请求一次编译”变成“跨请求复用”。这是约束 × 投机工程化的最新状态：约束计算本身不再是瓶颈，瓶颈转移到“如何让约束掩码与投机树/缓存更好配合”。\n五、功能实现：关键 PR 与实现细节 这一节看“约束解码怎么在框架里落地”。以 vLLM、SGLang、XGrammar 的公开实现为例，看约束查询怎么从“每步串行”变成“与投机并行”。\nvLLM v0.4.0：guided_logits_processors.py 的逐 token mask 文件：vllm/model_executor/guided_decoding/guided_logits_processors.py 关键逻辑（BaseLogitsProcessor.__call__）： 用 fsm_state = hash(input_ids) 维护 FSM 状态（基于当前前缀的哈希）； 查 allowed_token_ids(state) 得到合法 token 集合； mask = full(-inf)，再把 mask[allowed] = 0，最后 scores.add_(mask) 把非法 token 的 logits 压到 -inf。 这一步每生成一个 token 都执行，且是纯 CPU 串行（FSM 查询在 CPU 上），所以它本身不会加速，只会增加延迟——这是早期“约束解码必然更慢”的根源之一。 缓解手段：guided_decoding.py 里用 ThreadPoolExecutor(max_workers=2) 异步编译 FSM，把编译开销藏到生成之外；但每步查询的开销仍在 decode 关键路径上，直到与投机解码结合才真正被绕开。\nvLLM PR #14702：Structured Outputs + Speculative Decoding 链接：https://github.com/vllm-project/vllm/pull/14702 （2025-04 合并） 做了什么：让 vLLM 支持结构化输出的投机解码——draft token 按 grammar 验证，非法 draft 直接丢弃，合法 draft 进入并行验证。\n它解决的问题：之前 vLLM 里约束解码（guided decoding）和投机解码互斥——开了结构化输出就不能用投机加速。原因是草稿模型不知道语法约束，生成的 draft 大概率非法，直接验证会浪费目标模型的前向。\n做了什么（补充）：让 draft token 按 grammar 验证，非法 draft 直接丢弃，合法 draft 才进入并行验证。约束在这里不是“额外负担”，而是天然的草稿过滤器——草稿模型乱猜的 token 大部分会被 grammar 挡住。\n判断：PR 的关键是把“约束”从投机解码的障碍变成过滤器：合法 draft 进入验证，非法 draft 提前丢弃，结构化生成也能吃到投机加速。但要注意，这和 DOMINO 的结论一致——约束会挡住一部分合法草稿（草稿模型本来可能生成合法但被约束排除的内容），实际收益取决于草稿质量与约束严格度的权衡。\nSGLang PR #13425：Spec V2 + XGrammar 同时启用 链接：https://github.com/sgl-project/sglang/pull/13425 （2025-11 合并） 做了什么：让 SGLang 的投机解码（Spec V2）与 XGrammar 约束解码同时启用，此前二者互斥；实现上把 XGrammar 的 mask 计算从 decode 关键路径移出/重叠，与投机解码的草稿验证并行。\n判断：这验证了 DOMINO 的路线在主流框架里可行——“约束几乎零开销 + 投机加速”两个目标可以同时成立。\nXGrammar PR #490 / #613：投机解码树遍历的 mask 生成 链接：https://github.com/mlc-ai/xgrammar/pull/490 （2025-12）、https://github.com/mlc-ai/xgrammar/pull/613 （2026-05） 做了什么：PR #490 提出 TraverseDraftTree，对投机解码的候选树做单次 DFS 遍历，一次性为整棵树生成 grammar mask，而不是对每个候选节点单独查询；PR #613 把它正式暴露为 GrammarMatcher API（XGrammar-2 的树遍历能力即源于此）。\n它解决的问题：投机解码的候选树有多个分支，每个分支的 FSM 状态可能不同。如果每个节点单独查 mask，代价是树的规模（宽度 × 深度）次查询；且树里很多节点共享相同的前缀状态，重复计算明显。\n判断：这是“约束 × 投机”从论文走向工程的最后一块拼图——约束计算从“每 token 一次”变成“每棵树一次”，开销进一步摊薄。树的规模越大，这个优化的收益越明显。\n总结与适用边界 把这条线串起来看，约束解码的演进是**“约束怎么表达”到“约束怎么不花钱”**的过程：\n早期（Grid Beam Search）把约束做进搜索：正确，但慢，且只覆盖“必须包含词”这一种约束。 Outlines / GCD 把约束变成可编译的 FSM：通用、模型无关，但每步查合法集开始成为开销。 SGLang / XGrammar 让约束本身变快：压缩 FSM、预计算、跳确定 token、与 GPU 重叠执行——“确定的部分直接跳过”第一次成为可行策略。 DOMINO 把投机解码引入约束场景：解决字符层/子词层错位，用预计算掩码把约束查询变成查表，再通过投机解码让约束场景也能吃到并行验证的加速——但要明确，加速来自投机解码本身，且 DOMINO 比纯投机解码慢。 工程上（vLLM / SGLang / XGrammar 的 PR）把“约束 × 投机”做成默认能力：树遍历批量生成 mask、跨请求缓存，约束计算不再是瓶颈。 回到开头的猜想：“确定性的重复”确实可以做成加速——但关键不是只跳固定字符串，而是把“确定的部分”（固定字段、唯一合法 token）和“不确定的部分”（草稿模型猜、目标模型验）分开处理，让约束解码从“逐 token 串行查表”变成“批量掩码 + 并行验证”。\n适用边界\n本文加速倍数（XGrammar 约 100x、DOMINO 近 2x）均为论文/官方公开报告值，具体场景请自行复测。 DOMINO 的“近 2 倍加速”是相对无约束普通解码的，不是相对纯投机解码；纯投机在无约束场景下本身有 2.2～3 倍加速，DOMINO 约束+投机比它慢（论文未做该对比）。 约束解码保证的是“输出满足结构”，不保证“输出内容正确”——结构合法与语义正确是两件事。 约束本身复杂时（超大 vocabulary、深层嵌套 schema），预计算与缓存的收益才明显；简单约束下逐 token FSM 的开销可能已经可接受。 写作说明：本文涉及的论文、框架与 PR 均来自公开资料（arXiv、ACL Anthology、GitHub、MLC Blog），截至 2026-08；加速倍数与性能数据均为论文/官方公开报告值，具体场景请自行复测。XGrammar-2 与部分 PR 属于较新资料，阅读时请注意版本迭代。\n","permalink":"https://letsgetai.github.io/posts/constrained-decoding/","summary":"\u003cblockquote\u003e\n\u003cp\u003e生成一段 JSON 时，模型其实有 95% 的时间都在做“确定的重复”：\u003ccode\u003e{\u003c/code\u003e、\u003ccode\u003e\u0026quot;\u003c/code\u003e、字段名、\u003ccode\u003e:\u003c/code\u003e、\u003ccode\u003e}\u003c/code\u003e。真正需要模型“想”的，可能只有每个字段的值。那能不能告诉解码器：这些位置不用采样，直接填？如果再把“猜下一步”交给一个更快的草稿模型，生成会不会快好几倍？\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e本文是围绕这个问题的技术分享，面向已经接触过 LLM 推理、想理解约束解码来龙去脉的读者。主线是：约束解码解决什么问题 → 方法如何一步步演进 → 关键证据支持哪些结论 → 实践中的经验与边界。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e核心猜想\u003c/strong\u003e：约束解码里大量内容是确定性重复（固定字段、括号、键名），能否把“确定的部分直接跳过采样、交给更快的前向”做成加速？这条主线贯穿全文，也是约束解码与投机解码（speculative decoding）能走到一起的原因。\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"问题与背景\"\u003e问题与背景\u003c/h2\u003e\n\u003cp\u003e约束解码要解决的核心问题：\u003cstrong\u003e输出必须满足结构约束\u003c/strong\u003e（正则、JSON Schema、CFG），且正确性要有保证，而不是“运气好碰对格式”。\u003c/p\u003e\n\u003cp\u003e约束解码与普通解码的差别在于，每一步都要回答“哪些 token 合法”。这个查询与 mask 本身有成本，于是出现两条演进方向：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\u003cstrong\u003e约束怎么表达\u003c/strong\u003e：从“必须包含指定词”（集合级约束），到“必须匹配某种语法”（FSM 可判定的约束）。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e约束怎么不花钱\u003c/strong\u003e：从逐 token 串行查表，到预计算、跳确定部分、与 GPU 前向重叠、最后与投机解码并行验证。\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e下文按“问题 → 方法 → 证据”展开，论文链接直接列在对应方法处。\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"一早期约束解码约束做进搜索过程\"\u003e一、早期约束解码：约束做进搜索过程\u003c/h2\u003e\n\u003cp\u003e约束解码最早解决的问题是“输出必须包含某些指定内容”——不是格式合法，而是句子里必须出现某个词或短语。早期方法把约束做进 beam search 的搜索过程。\u003c/p\u003e\n\u003ch3 id=\"grid-beam-searchhokamp--liu-acl-2017\"\u003eGrid Beam Search（Hokamp \u0026amp; Liu, ACL 2017）\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e论文：\u003cem\u003eLexically Constrained Decoding for Sequence Generation Using Grid Beam Search\u003c/em\u003e\u003c/li\u003e\n\u003cli\u003e链接：\u003ca href=\"https://aclanthology.org/P17-1141/\"\u003ehttps://aclanthology.org/P17-1141/\u003c/a\u003e 、 arXiv:1704.07138\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cstrong\u003e方法\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e约束定义为“输出必须包含一组指定的短语/词”。\u003c/li\u003e\n\u003cli\u003e模型在 beam 上扩展时，额外维护一个“已覆盖约束”的状态，beam 排序优先覆盖更多约束的候选。\u003c/li\u003e\n\u003cli\u003e约束是集合级的“必须包含”，不要求前缀匹配，可以出现在输出任意位置。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cstrong\u003e判断\u003c/strong\u003e：这一代方法把约束做进搜索，正确，但只覆盖“必须包含词”这一种约束，且 beam 扩展成本随约束数量上升。它的意义是确立了“约束进解码过程”的范式，而不是进训练或后处理。\u003c/p\u003e","title":"约束解码的生成：从逐 token 采样，到“知道下一步是什么”"},{"content":"GitHub\n","permalink":"https://letsgetai.github.io/about/","summary":"\u003cp\u003e\u003ca href=\"https://github.com/letsgetai\"\u003eGitHub\u003c/a\u003e\u003c/p\u003e","title":"About"}]