文章目录
-
- [1. Kev 学习的任务是什么](#1. Kev 学习的任务是什么)
- [2. 从一条业务记录构造训练样本](#2. 从一条业务记录构造训练样本)
-
- [2.1 一条记录可以同时监督多个问题](#2.1 一条记录可以同时监督多个问题)
- [2.2 仓库里的数据从哪里来](#2.2 仓库里的数据从哪里来)
- [2.3 生成规则数据时,先算答案,再写成文字](#2.3 生成规则数据时,先算答案,再写成文字)
- [2.4 划分与冻结是训练前的一步](#2.4 划分与冻结是训练前的一步)
- [3. 训练与推理共用的解析过程](#3. 训练与推理共用的解析过程)
-
- [3.1 JSON 先变成模型能读的文本](#3.1 JSON 先变成模型能读的文本)
- [3.2 三种 API 类型统一成候选项选择](#3.2 三种 API 类型统一成候选项选择)
- [4. 一条记录如何编码成 token](#4. 一条记录如何编码成 token)
-
- [4.1 用边界标记划分状态、问题和候选项](#4.1 用边界标记划分状态、问题和候选项)
- [4.2 编码结果包含位置和读出索引](#4.2 编码结果包含位置和读出索引)
- [4.3 上下文长度有不同含义](#4.3 上下文长度有不同含义)
- [5. 模型由哪些部分组成](#5. 模型由哪些部分组成)
-
- [5.1 文本主干负责产生上下文表示](#5.1 文本主干负责产生上下文表示)
- [5.2 LoRA 让部分投影矩阵适应决策任务](#5.2 LoRA 让部分投影矩阵适应决策任务)
- [5.3 指针读出头是两层线性投影加点积](#5.3 指针读出头是两层线性投影加点积)
- [6. 多个问题如何共享状态,又保持隔离](#6. 多个问题如何共享状态,又保持隔离)
-
- [6.1 纯注意力主干可以使用一条打包序列](#6.1 纯注意力主干可以使用一条打包序列)
- [6.2 混合主干需要独立的问题行](#6.2 混合主干需要独立的问题行)
- [6.3 问题隔离不等于选项顺序无关](#6.3 问题隔离不等于选项顺序无关)
- [7. 一条样本进入训练循环后发生什么](#7. 一条样本进入训练循环后发生什么)
-
- [7.1 每个 epoch 可以生成不同的训练变体](#7.1 每个 epoch 可以生成不同的训练变体)
- [7.2 用 logits 手算一次交叉熵](#7.2 用 logits 手算一次交叉熵)
- [7.3 软目标表达不确定性](#7.3 软目标表达不确定性)
- [7.4 代码提供的可选损失](#7.4 代码提供的可选损失)
- [7.5 误差如何更新参数](#7.5 误差如何更新参数)
- [8. 训练结束后,保存什么、如何校准](#8. 训练结束后,保存什么、如何校准)
- [9. 同一条记录上线后如何推理](#9. 同一条记录上线后如何推理)
-
- [9.1 先复用输入转换,再调度模型](#9.1 先复用输入转换,再调度模型)
- [9.2 第一次请求计算状态,分支从它继续](#9.2 第一次请求计算状态,分支从它继续)
- [9.3 一次决策不等于只调用一次 forward](#9.3 一次决策不等于只调用一次 forward)
- [10. 概率如何变回业务答案](#10. 概率如何变回业务答案)
- [11. 按源码重新走一遍](#11. 按源码重新走一遍)
- 源码与复现说明
Kev 用语言模型读取状态、问题和候选项,再为每个候选项计算概率。本文沿一条退款记录,解释它如何进入数据集、变成 token、产生损失,最后作为 API 请求返回决策。
源码基线:
654f5203cd99e72616bb7d49b8a3659a701c5e99,写作日期:2026-10-17。本文描述这个代码快照的机制,不将某个历史 checkpoint 的配方当成所有版本的统一配置。退款样本为本文自建;文中 logits、概率及其派生指标是教学演算,不是模型实测结果。
1. Kev 学习的任务是什么
Kev 的输入由共享状态 state 和一组问题 questions 组成。状态可以是一封邮件、一份政策、一段代码或一个结构化业务对象。每个问题提供自己的指令和候选项。模型输出每个问题的候选项概率。
例如,同一份退款申请可以同时问:应该批准、拒绝还是转人工;是否符合退款资格;应该采用哪个处理优先级。这些问题共享申请材料,但各自作答。
这个接口对应一个条件分布:
text
p(候选项 i | 状态 S,问题指令 I,该问题的全部候选项 O)
候选项文本是输入的一部分,因此每次请求可以使用不同的选项名称、描述和数量。输出的概率只在当前问题给出的候选项内归一化。没有显式提供"未知"或"以上皆非"时,接口不会自动增加一个拒答类别。

图 1 中,训练阶段的标签用于计算误差;上线阶段由相同的文本转换路径构造输入,再使用训练好的参数计算概率。
2. 从一条业务记录构造训练样本
2.1 一条记录可以同时监督多个问题
下面是一条可供 --data 读取的训练记录。为了阅读方便,这里展开了 JSON;存为 JSONL 时,每条记录需要占一行。
json
{
"state": {
"policy": "Approve a refund if elapsed_days <= 30 and opened is false. Reject otherwise. If either fact is missing, use manual review. Priority is normal when amount <= 200, and high otherwise.",
"case": {"elapsed_days": 12, "opened": false, "amount": 128}
},
"questions": {
"decision": {
"type": "choice",
"instructions": "Apply the refund policy to this case.",
"criteria": {
"approve": "Approve the refund",
"reject": "Reject the refund",
"manual": "Send for manual review"
},
"label": "approve",
"src": "tutorial_policy"
},
"eligible": {
"type": "noul",
"instructions": "Does this case qualify for a refund?",
"label": true,
"src": "tutorial_policy"
},
"priority": {
"type": "score",
"instructions": "Choose the processing priority according to the policy.",
"criteria": ["low", "normal", "high"],
"label": 1,
"src": "tutorial_policy"
}
}
}
政策规定,未拆封且购买后不超过 30 天可以退款。样本满足这两个条件,所以 decision 的标签是 approve,eligible 的标签是 true。金额 128 不超过 200,所以 priority 的标签是从 0 开始编号的等级 1,也就是 normal。
label 是监督信号,src 用来记录问题来源。自定义文件经 data.load_records() 读取后,还会补齐记录级 _meta,包括 id、group_id、source、text_sha256 等字段。已有元数据会保留,可用于溯源、分组评估和复现实验。
_meta 不会被拼进模型输入。label、软标签 target 和 src 也不会作为问题文本提供给模型。它们在数据处理和损失计算中使用。源码:data.py
2.2 仓库里的数据从哪里来
训练入口支持三种主要来源:冻结套件的 train 分区、用户自己的 JSONL,以及从公开数据集即时构造的小规模数据。自定义数据还可以通过 --suite 与 --replay N 混入旧套件训练样本,用于继续微调。
| 数据类型 | 如何构造 | 标签从哪里来 |
|---|---|---|
| 公开分类、阅读理解、自然语言推断数据 | 把原文放进 state,把任务写成指令,把类别变成候选项 |
原始数据集标签 |
| 生成的政策、组合规则数据 | 先生成可执行规则和事实,再渲染成政策文本与案例 | Python 规则求值器 |
| 后续真实文档和领域数据 | 将文档、业务问题和候选项组织为同一请求格式 | 随数据集而异,可能是原标签、人工审核、启发式或模型裁决 |
| 用户自定义数据 | 每行一份带标签的请求 | 用户指定的业务标签 |
例如,Banking77 的客户消息变成银行意图 Choice,BoolQ 的文章和问题变成 Noul,情感评级数据变成 Score。转换器也会变换外层结构,让同一类文本以字符串、嵌套对象或数组出现。
一个可核对的基础配方是 decision-v7。其 manifest 记录了 10,000 条公开数据、896 条生成政策数据和 1,680 条组合规则数据,共 12,576 条训练记录、15,576 个问题。这是基础阶段的数据组成,后续领域微调和研究训练可能使用其他套件。来源与数量应以对应 checkpoint 的训练配置和模型卡为准。数据清单:decision-v7
2.3 生成规则数据时,先算答案,再写成文字
composition.py 中,规则是一棵由原子判断和逻辑运算组成的树。原子判断包括数值比较、区间、字段相等、日期间隔和布尔标记;逻辑运算包括 and、or、not、unless、if。
退款例子的核心可表示为:
text
and(le(elapsed_days, 30), not(opened))
求值器先在结构化事实上计算结果,再把规则和事实渲染成自然语言。缺失字段会返回 None,表示依据当前事实无法确定。生成器选择能够确定答案的案例,并保留规则、事实和决定性字段等证书信息,供校验函数重新计算。
生成器还围绕同一规则构造四条记录:一对只改变关键事实,使标签翻转;另一对只改变无关事实,使标签保持不变。这样可以检查模型是否在使用政策要求的证据。

图 2 是教学化的退款示例。实际组合规则生成器会检查:每对恰好改变一个事实,相关修改确实翻转答案,无关修改确实保持答案。相同规则可以用不同文风渲染;部分规则结构和表达形式专门留给评估。源码:composition.py
2.4 划分与冻结是训练前的一步
冻结套件包含 train、calibration、development、test 分区及 manifest.json。manifest 记录分区哈希、数量、数据集版本、来源策略和上下文限制。load_split() 会校验文件的 SHA-256 和记录数;缺少的大文件可以从 manifest 或仓库规则指定的镜像获取。读取锁定测试需要显式授权参数。
组合规则数据按 group_id 划分,避免同组的相似记录落入训练和校准两边。训练策略还会拒绝仅供评估的来源和被保留的规则结构。语义哈希用于部分精确重叠检查,其能力范围不等同于完整的预训练污染排查。
超长数据也必须在这一阶段处理。自定义数据默认丢弃超出训练上下文的记录,可用 --context_overflow error 改为报错。正式训练编码使用严格检查,避免在训练中悄悄截断状态。源码:suite.py
3. 训练与推理共用的解析过程
3.1 JSON 先变成模型能读的文本
训练路径调用 data.materialize(),内部先通过 api_request() 取出 API 允许的字段,再由 Pydantic 的 SystemOneRequest 验证,最后调用与服务端相同的 api.to_record()。
text
带标签的请求
→ api_request:提取 state、type、instructions、criteria
→ SystemOneRequest:验证问题类型与候选项结构
→ to_record:渲染文本,生成候选项与输出映射
→ materialize:在训练记录上附回数字标签、来源与软目标
api.render() 将对象和数组展开为文本,并保留字段名。它不会直接把整个对象作为 JSON 字符串送入主干。例子的 state 经过实际函数转换后如下:
text
policy: Approve a refund if elapsed_days <= 30 and opened is false. Reject otherwise. If either fact is missing, use manual review. Priority is normal when amount <= 200, and high otherwise.
case:
elapsed_days: 12
opened: False
amount: 128
这里的 False 来自 Python 对布尔值的文本转换。字段名称、嵌套层级和候选项描述都会成为模型所读文本的一部分。
3.2 三种 API 类型统一成候选项选择
| API 类型 | 模型实际接收的选项 | 训练标签 | 推理结果如何解释 |
|---|---|---|---|
choice |
选项名;有描述时为 name: description |
名称映射为当前选项序号 | 最大概率的名称及完整分布 |
noul |
固定顺序的 no、yes,可附 false、true 对应描述 |
false → 0,true → 1 |
返回 p(yes) |
score |
按列表顺序排列的等级描述 | 从 0 开始的等级序号 | 返回等级期望及完整分布 |
退款样本的内部记录可缩写为:
json
{
"state": "policy: ...\ncase:\n elapsed_days: 12\n opened: False\n amount: 128",
"questions": [
{
"instr": "Apply the refund policy to this case.",
"options": ["approve: Approve the refund", "reject: Reject the refund", "manual: Send for manual review"],
"label": 0,
"qtype": "choice",
"keys": ["approve", "reject", "manual"]
},
{"instr": "Does this case qualify for a refund?", "options": ["no", "yes"], "label": 1, "qtype": "noul"},
{"instr": "Choose the processing priority according to the policy.", "options": ["low", "normal", "high"], "label": 1, "qtype": "score"}
]
}
这段展示省略了 src、qid 等字段,并缩写了状态。真正的状态文本包含完整政策。训练时数字标签附在记录旁边;编码器只把状态、指令、候选项和边界标记变成 token。
Choice 的 label 依赖当前选项顺序。例如 approve 移到第 3 位,它的数字标签就必须从 0 改为 2。先保留选项名称,再在 materialize() 中查当前索引,可以让训练增强正确更新标签。源码:api.py
4. 一条记录如何编码成 token
4.1 用边界标记划分状态、问题和候选项
model.encode() 构造如下逻辑序列:
text
<state> 共享状态
<q> 问题一指令 <opt> 批准 </opt> <opt> 拒绝 </opt> <opt> 转人工 </opt> <decide>
<q> 问题二指令 <opt> no </opt> <opt> yes </opt> <decide>
<q> 问题三指令 <opt> low </opt> <opt> normal </opt> <opt> high </opt> <decide>
上面使用易读别名,实际实现复用 Qwen 已有的特殊 token,默认不扩展词表:
| 本文别名 | 实际特殊 token |
|---|---|
<state> |
`< |
<q> |
`< |
<opt> |
`< |
</opt> |
`< |
<decide> |
`< |
用户文本通过 user_tokens() 单独分词。形如 <\|name\|> 的文本会被重写,防止用户内容伪造这些控制边界。边界 token 则由编码器显式插入。
4.2 编码结果包含位置和读出索引
| 字段 | 含义 |
|---|---|
ids |
整条逻辑记录的 token ID |
seg |
状态为 0,各问题依次为 1、2、3 |
pos |
主干使用的位置编号 |
decide_idx |
每个问题 <decide> 的位置 |
opt_idx |
每个问题各个 </opt> 的位置 |
opt |
用于可选选项隔离的跨度标记 |
设状态长为 S,其中包含 <state>。每个问题的位置编号都从 S 开始,而不是接在前一个问题的位置后面。因此单独问某个问题和将它与其他问题一起提交时,它看到的状态、上下文和位置编号相同。

图 3 的矩阵是按状态和问题分块的示意。行表示当前 token 所在段,列表示可读取的段;对角块内部仍然使用因果下三角关系,不能读取未来 token。源码:model.encode 与 branch_mask_batch
4.3 上下文长度有不同含义
在这个快照中,历史基础训练默认值为 MAX_STATE=384、MAX_BRANCH=1024、MAX_PACKED=2048。其中 max_branch 限制的是"状态 + 一个问题分支"的总长度,max_packed 限制逻辑打包记录的总长度。
训练可以提高 --max_state,也可以显式设置 --max_question_tokens 与 --max_context。显式模式会检查状态预算与问题预算之和是否放得进主干窗口。
服务端默认状态上限为 65,536 tokens,单个"状态 + 问题"行上限为 73,728 tokens。checkpoint 若保存了显式上下文配置,加载时会优先使用它。代码允许的长度、某次训练实际使用的长度、某个 checkpoint 经验证的有效长度,是三个不同的量,不能相互替代。
5. 模型由哪些部分组成
Kev 的模型主体在 DecisionModel。它保留预训练模型的文本主干,用主干输出的最后一层隐藏向量计算候选项分数。

5.1 文本主干负责产生上下文表示
主干包括 token embedding、decoder 层堆栈和末端归一化。输入 token 经过这些层后,每个位置得到一个 d 维隐藏向量。Kev 从 last_hidden_state 取值,不需要为了读出而保留所有中间层输出。
纯注意力主干沿用普通因果注意力 decoder。当前家族所用的 Qwen3.5/3.8 混合主干还包含 Gated DeltaNet 层。这些层通过短卷积状态和循环状态传递历史信息,与完整注意力层共同构成主干。具体层数、隐藏宽度和层类型排列由所选基础模型的配置决定,Kev 没有给所有尺寸写死一套层数。
模型使用文本主干的隐藏状态,不执行词表语言模型头,也没有"预测下一个 token、再把它接回输入"的生成循环。候选项内容在前向计算前就已给定。
5.2 LoRA 让部分投影矩阵适应决策任务
常规 LoRA 模式冻结原始主干权重,为选中的线性投影加入可训练的低秩增量:
text
W_effective = W_base + (alpha / r) · B · A
A 的形状为 [r, d_in],B 的形状为 [d_out, r]
代码默认 r=16、alpha=2r、LoRA dropout 为 0.05。lora_targets=all 覆盖注意力的 Q/K/V/O 投影、MLP 的 gate/up/down 投影;混合主干还加入 DeltaNet 的输入与输出投影。也可以选择较小的目标集合。
LoRA 和指针读出头一起训练。可选的 --special_embeddings 还允许训练这几个边界 token 的 embedding;默认关闭。使用 --init_from 时,兼容性检查通过后会加载已有适配器及读出头,继续学习新数据。
框架也支持 --full_ft 1。此时训练整个文本主干和读出头,主干使用 bf16 权重,优化器维护 fp32 主权重与状态。多 GPU 使用 FSDP2 分片;单 GPU 可以把主权重和优化器状态放在主机内存。全量训练与 LoRA 训练共用数据格式和决策读出方式。
5.3 指针读出头是两层线性投影加点积
对某个问题,令 h_decide 表示 <decide> 位置的隐藏向量,h_i 表示候选项 i 的 </opt> 位置隐藏向量。PointerHead 的计算是:
text
u = Wq · h_decide + bq 形状 [dp]
v_i = Wk · h_i + bk 形状 [dp]
z_i = dot(v_i, u) / sqrt(dp) 标量分数
p_i = exp(z_i / T) / Σ_j exp(z_j / T) 当前问题内的概率
默认 dp=256。两组投影都带偏置,参数量为 2 × dp × (d+1)。对 K 个选项,头部只需产生 K 个 logits,不需要产生整个词表的 logits,也不需要为每个业务类别保存一组独立的固定分类权重。
</opt> 位于候选项文本之后,因此它的表示可以包含选项内容、问题指令和状态。<decide> 位于整个选项列表之后,因此可以读取该问题的全部选项。点积衡量这两类上下文表示之间的匹配程度。

这些向量都来自同一次决策计算。PointerHead 中的 q、k 是读出头自己的投影,与主干注意力层内部的 Q/K/V 投影是不同参数。
6. 多个问题如何共享状态,又保持隔离
6.1 纯注意力主干可以使用一条打包序列
对纯注意力模型,短记录可以使用图 3 的 block-causal mask。允许 token i 读取 token j 的条件是:
text
j <= i,并且满足以下任意一个条件:
j 属于共享状态;
j 与 i 属于同一个问题分支。
状态内部保持因果关系。问题可以读取状态和自身前面的 token,不能读取其他问题。这样状态在打包前向中只计算一份。过长的打包记录会切换到行形式,避免显式二维掩码随总长度平方增长。
6.2 混合主干需要独立的问题行
Gated DeltaNet 层的循环状态不会按任意四维注意力 mask 隔离。直接把不同问题串起来,后面的问题仍可能继承前面问题留下的循环状态。
因此混合主干固定使用独立行:
text
行 1 = 共享状态 + decision 问题
行 2 = 共享状态 + eligible 问题
行 3 = 共享状态 + priority 问题
各行具有独立的分支计算状态,可以作为 batch 运行。只要分支从同一份状态前缀开始,前向语义就与对应的单问题请求一致。
普通行式训练会为每个问题重算状态。可选的 --shared_prefix 1 在每个 decoder 层先算状态,再让分支读取该层状态留下的缓存,全量训练默认启用这个路径。注意力层传递 K/V,DeltaNet 层传递卷积窗口和循环状态。
训练中的共享前缀必须保留梯度。shared_prefix.py 用函数式状态连接前缀与分支,让各问题的梯度回传并累加到状态计算上;开启梯度检查点时,一层的状态计算和分支计算一起重算。源码:shared_prefix.py
6.3 问题隔离不等于选项顺序无关
默认编码中,同一问题内的选项按顺序排列。后面的选项边界可以读取前面的选项,因此交换选项顺序可能改变概率。
训练时打乱选项、可选的排列 KL,都是减少顺序敏感性的办法。另一个可选模式 option_isolation 会让每个选项只读取状态、指令和自身,并为不同选项复用位置编号,再由 <decide> 汇总。这个模式需要打包 mask,在本快照的混合主干上会被拒绝。
7. 一条样本进入训练循环后发生什么
7.1 每个 epoch 可以生成不同的训练变体
record_variants() 根据全局 seed、epoch 和记录 ID 创建该记录自己的随机流。Choice 选项会重排,还可能发生以下一种增强:
| 增强 | 对本例的修改 | 标签如何变化 |
|---|---|---|
| 选项重排 | 调换 approve、reject、manual 的顺序 | 名称保持 approve,数字索引随位置更新 |
| 正确项替换为"以上皆非" | 删除 approve,加入 none 选项 | 正确答案变成 none |
| 加入错误的 none 选项 | 保留 approve,再加入 none | 仍然选择 approve |
| 加入无关干扰项 | 增加天气等与案例无关的答案 | 仍然选择 approve |
这些增强针对 Choice;Noul 和 Score 保持原来的选项顺序。代码会维护选项数量等条件,不能把默认概率理解为每条记录都按该比例成功增强。
--p_none_pair 还可以让原样本额外产生一对兄弟记录。第一条保留正确答案并加入 none,第二条仅移除正确答案,让同一个 none 成为正确项。两条使用同样的状态与指令,促使模型检查"正确选项是否真的存在",而不是记住 none 的某种措辞。
软目标问题只重排 Choice 选项,不进行会改变目标含义的选项插入或替换。长状态还可以用 --none_pair_max_state 限制配对,因为额外两条记录会再次占用状态计算资源。源码:augment、none_pair、record_variants
7.2 用 logits 手算一次交叉熵
假设本例的三个问题得到如下 logits。以下数值用于演算,未运行预训练权重:
| 问题 | logits | softmax 概率 | 正确项 |
|---|---|---|---|
| decision | [2, 0, -1] |
[0.843795, 0.114195, 0.042010] |
approve,索引 0 |
| eligible | [-1, 2] |
[0.047426, 0.952574] |
yes,索引 1 |
| priority | [-0.5, 1.5, 0] |
[0.099624, 0.736125, 0.164252] |
normal,索引 1 |
训练时读出头使用 T=1。硬标签的主损失是候选项交叉熵:
text
L_question = -log p_y
L_decision = -log(0.8437947) = 0.169846
L_eligible = -log(0.9525741) = 0.048587
L_priority = -log(0.7361247) = 0.306356
L_record = (L_decision + L_eligible + L_priority) / 3
= 0.174930
这里的 log 为自然对数。batch_loss() 先对一条训练变体内部的问题取平均,再累加各变体的贡献。因此,三个问题的一条记录不会仅因为问题多,就自然获得三倍于单问题记录的主损失权重。
累积梯度时还会按本次 optimizer step 的原始记录数和微批次的变体数量归一化。如果 row_budget 把一条记录按问题拆开,代码用 share 恢复它在原记录中的占比。不能简单把实现概括成"对所有 token 求平均",也不能在存在变体时一律写成"对所有问题直接求平均"。源码:question_loss 与 batch_loss
7.3 软目标表达不确定性
某些记录无法由给定信息唯一确定答案,可以提供按选项键组织的 target 分布。materialize() 将它按当前选项顺序排列、补零并归一化。
text
L_soft = -Σ_i target_i · log(p_i)
如果两个答案的监督目标是 [0.5, 0.5],最优输出也是均匀分布。该损失的最小值是目标分布的熵,即 log(2),不是 0。不能用硬标签交叉熵趋近于 0 的直觉判断软目标训练是否收敛。
本快照的 question_loss() 遇到软目标会直接返回软交叉熵,因此函数后面的标签平滑、Brier、focal、序数项和 Choice 标签权重不会继续作用在这道题上。外层配置的其他正则项仍需看各自适用条件。
7.4 代码提供的可选损失
这些开关默认都为 0。某个损失存在于代码中,不代表所选 checkpoint 使用了它。
| 开关 | 计算或作用 |
|---|---|
--ord_w |
对 Score 加上预测累计分布与真实累计分布的均方误差,也就是归一化 ranked probability score |
--perm_kl |
用两次选项排列的输出,按选项身份对齐后计算对称 KL,约束顺序敏感性 |
--anchor_w |
对选项集合完全匹配的问题,加 `KL(冻结基础模型的分布 |
--label_smoothing |
将硬标签 CE 改为平滑目标的 CE |
--brier_w |
在硬标签 CE 上加概率与 one-hot 目标的平方差之和 |
--focal_gamma |
将硬标签 CE 乘以 (1-p_y)^gamma |
--choice_label_weights |
按正确的 Choice 标签名称给硬标签损失加权 |
序数项的具体形式为:
text
RPS = mean over k=0..K-2 of (Σ_{i<=k} p_i - 1[y<=k])²
L_score = CE + ord_w · RPS
Score 默认仍以等级分类训练,即使 API 最后返回等级期望,也不意味着默认损失是均方回归误差。标签平滑、Brier 和 focal 在代码中最多同时启用一个;序数项、排列约束和 anchor 则有各自的组合条件。
7.5 误差如何更新参数
对硬标签 CE,logit 的梯度为:
text
∂L / ∂z_i = p_i - 1[i=y]
decision 示例:[-0.156205, 0.114195, 0.042010]
这使优化更新倾向于提高正确答案的分数、降低错误答案的分数。梯度从 logits 经过指针头,再经过 <decide> 与候选项边界的隐藏向量进入主干计算。状态 token 即使没有自己的独立标签,也会通过问题对状态的依赖收到梯度。

在 LoRA 模式下,梯度经过主干运算,但原始主干参数不更新;更新的是 LoRA 参数和读出头,以及显式开启的特殊 token embedding。全量模式则更新主干本身。
微批次的梯度累积到一步后,LoRA 路径同步多卡梯度,以全局范数 1.0 裁剪,再由 AdamW 更新,随后推进学习率调度并清空梯度。全量模式使用 MasterAdamW 完成主权重更新与裁剪。基础配方通常使用 batch × accum × world_size 条原始记录构成一步,最后一组、长度调度和 token 上限模式会影响具体微批次划分。
8. 训练结束后,保存什么、如何校准
LoRA checkpoint 保存适配器配置及权重、head.pt 和 tokenizer 文件。head.pt 包含读出头参数、基础模型和 revision、LoRA rank、读出维度、精度、温度及训练来源信息。全量 checkpoint 保存文本主干的配置与 safetensors 分片,再配同样的读出头和元数据。Checkpoint 统一处理这两种加载布局。
模型选择和校准也属于这条生命周期。实验流程可以先在校准分区拟合一个温度,再在开发集观察准确率、NLL、Brier、ECE 和问题隔离等指标。锁定测试用于候选确定后的确认,而不是反复搜索超参数。
温度校准只调整最终 logits 的尺度:
text
p_i(T) = softmax(z / T)_i
T* = argmin over T>0 of mean[-log p_y(T)]

对 [2, 0, -1],T=1 得到 [0.8438, 0.1142, 0.0420];假设使用 T=2,概率变成 [0.6285, 0.2312, 0.1402]。批准仍然是最大项,但分布更平缓。这个 T=2 只是演示,并非退款任务拟合出的温度。
训练不使用已拟合的温度。PointerHead 仅在 eval 模式应用它,加载器会从 checkpoint 恢复该值。
校准数据的来源同样影响结论。训练套件内部留出的条目适合试验筛选,却仍与训练来源同分布。这个快照的正式 checkpoint 校准脚本默认要求训练未使用的数据集,并检查来源冲突;历史版本存在分布内温度。解释某个发布模型的概率时,应读取其温度的来源记录,而不能仅看到一个 temperature 数字就认为它能覆盖所有业务分布。源码:校准脚本
9. 同一条记录上线后如何推理
9.1 先复用输入转换,再调度模型
业务方发送 POST /v1/systemone,请求保留 state 和 typed questions,不需要训练标签。服务端调用 to_record()、model.encode(),检查上下文,然后把编码结果交给模型线程。
同一线程从队列中收集请求,规划缓存并调用 probs_batch()。支持的 CUDA graph 路径会将兼容形状的状态和问题行合批;其他路径可以逐请求执行。因此"队列里有多个请求"不意味着所有后端都会把它们塞进同一次主干调用。
服务端最终调用 to_answers() 把概率还原成 API 类型,再由 Python 序列化为 JSON。接口里的 output_tokens 是返回答案文本的分词统计,不表示模型生成过这些 token。源码:serve.py
9.2 第一次请求计算状态,分支从它继续
对混合主干,缓存未命中时先对状态做一次 prefill。这里的 prefill 是对已知输入 token 的前向计算。
每层留下两类可能的缓存:完整注意力层的 K/V,或 DeltaNet 层的卷积窗口与循环状态。三个问题分别从同一个状态前缀开始,继续计算自己的指令、候选项和 <decide>。

分支使用独立的缓存副本或对应批次槽位,避免一个问题的后续计算写入另一个问题的起点。需要缓存的是状态结束时的前缀,不是"状态 + 第一个问题"之后的状态。
跨请求的前缀缓存以状态 token ID 和选项隔离设置为键,在当前已加载模型内复用。它受 LRU 条目数和总状态 token 预算约束。相同状态配新问题可以命中缓存;语义近似但 token 不同的状态不会据此命中。
从处理的 token 数看,Q 个问题各自完整跑一遍需要:
text
Q × S + Σ B_q
共享前缀后,新状态只需处理 S + Σ B_q 个输入 token;跨请求命中则只新增分支 token 的前向。这里是 token 处理量,不是完整计算复杂度。注意力层的分支仍要读取前缀 K/V,缓存也占显存,延迟不会简单按上述数量比例缩短。
9.3 一次决策不等于只调用一次 forward
Kev 没有自回归生成循环,但一次请求仍可能包含状态 prefill、一个或多个分支批次、读出与格式化。问题数量、输入长度和执行后端都会改变实际的前向调用次数。
本快照的普通 torch 评估路径在短行上使用直接行式前向,可能为每个问题重算状态;服务路径则使用前缀缓存。长行评估还有高效注意力与共享前缀的专门路径。隔离语义保持一致,浮点结果的容差要以对应 dtype、算子和测试条件为准,不能假设不同精度与融合内核逐位相同。
服务可通过合并 LoRA、bf16、融合算子、CUDA graphs 和长度分组减少延迟。这些改变执行方式或数值精度,不改变"从边界隐藏状态读出候选项分布"的接口结构。源码:model.py · 加载器:checkpoint.py
10. 概率如何变回业务答案
继续使用前面的教学 logits,并令 T=1。api.to_answers() 实际计算出的答案部分如下,省略了模型名、usage 和延迟:
json
{
"decision": {
"type": "choice",
"choice": "approve",
"confidence": 0.7657,
"probabilities": {"approve": 0.8438, "reject": 0.1142, "manual": 0.0420}
},
"eligible": {"type": "noul", "noul": 0.9526},
"priority": {
"type": "score",
"score": 1.0646,
"legend": {"0": "low", "1": "normal", "2": "high"},
"probabilities": {"0": 0.0996, "1": 0.7361, "2": 0.1643},
"confidence": 0.6042
}
}
Choice 返回最大概率选项。这里的 confidence 是相对均匀分布修正后的值,不是原始最大概率:
text
choice_confidence = (p_max - 1/K) / (1 - 1/K)
本例 K=3,所以 confidence ≈ 0.7657
只有一个选项时,函数返回 1。Noul 返回第二个选项 yes 的概率 0.9526,调用方可以按业务需要再设阈值。
Score 返回按等级索引计算的期望:
text
score = Σ_i i · p_i
= 0×0.09962365 + 1×0.73612472 + 2×0.16425163
= 1.06462798
因此最可能的等级是 normal,API 的连续 score 是 1.0646。索引与业务单位需要应用自行映射,比如把 0、1、2 映射为自己的优先级编码。
Score 的 confidence 按概率质量到众数等级的平均距离计算,并用均匀分布的尺度归一化。它反映分布是否集中,与 Choice 使用的公式不同:
text
mode = 第一个最大概率等级
D = (1/K) · Σ_i |i - (K-1)/2|
score_confidence = max(0, 1 - Σ_i p_i·|i-mode| / D)
单等级同样返回 1。以上结果用未舍入的概率计算,再在序列化时保留 4 位小数。
11. 按源码重新走一遍
| 样本当前的位置 | 主要函数 | 产物 |
|---|---|---|
| 带业务标签的 JSONL | data.load_records 或 suite.load_split |
带来源与 ID 的请求 |
| 本 epoch 的训练输入 | record_variants、augment、none_pair |
重排、干扰或最小对变体 |
| 统一模型文本 | api_request、to_record、materialize |
state 文本、选项文本、数字标签 |
| token 与分支结构 | model.encode |
ids、seg、pos、读出位置 |
| 主干与头部前向 | forward_batch、PointerHead |
每个问题的候选项 logits |
| 监督与优化 | question_loss、batch_loss、训练循环 |
梯度、更新后的模型参数 |
| 可加载模型 | finish_checkpoint、Checkpoint |
主干或适配器、head、tokenizer、元数据 |
| 温度与评估 | fit_temperature、benchmark 与校准脚本 |
温度、逐题结果与评估报告 |
| 在线决策 | Server、probs_batch、PrefixCache |
候选项概率 |
| 业务响应 | api.to_answers |
Choice、Noul、Score 的 JSON |
对这条退款记录而言,业务规则和事实先决定监督标签;文本转换决定模型实际读到什么;主干把每个边界位置变成上下文向量;读出头计算选项分布;交叉熵再决定应如何修改参数。上线后,相同的输入表示经过已训练参数,并通过缓存减少重复状态计算,最终还原为业务接口中的答案。
源码与复现说明
本文以本地提交 654f5203cd99e72616bb7d49b8a3659a701c5e99 为核对基线。下列链接指向公开仓库的文件入口,main 后续可能变化;复现本文应按提交与文件哈希确认版本。
| 阅读目标 | 源码 |
|---|---|
| API 校验、文本渲染和答案还原 | kev/api.py |
| 公开数据转换、增强和内部记录 | kev/data.py |
| 规则树、事实求值、最小修改校验 | kev/composition.py |
| 分组划分与冻结套件 | kev/study_v3.py、kev/suite.py |
| 编码、掩码、主干和指针头 | kev/model.py |
| 训练变体、损失与梯度累积 | kev/train.py |
| 可微共享前缀与全量优化 | kev/shared_prefix.py、kev/full_ft.py |
| 权重布局、精度与温度加载 | kev/checkpoint.py |
| 请求队列与前缀缓存 | kev/serve.py |
| 温度拟合的数据来源要求 | scripts/calibrate_checkpoint.py |
本文的字段转换、标签映射、选项重排、none 配对及答案还原,使用同一源码快照的 api.py 和 data.py 函数核对;概率和损失用 Python 数学函数复算。验证没有下载或运行模型权重,因此本文没有把演算结果当成模型能力证据。