逐行读懂Clef源码:State、图片与Schema如何打包进单次前向传播(encode_record深度解读)
【免费下载链接】clef项目地址: https://ai.gitcode.com/hf_mirrors/Cloudflare/clef
Clef 是 Cloudflare 开源的 27B 多模态决策模型:输入一段state(状态,JSON 或纯文本)和一组类型化问题(schema),一次前向传播就返回所有问题每个合法选项的概率——没有自由文本生成,也没有输出解析。这篇文章带你逐行读懂 Clef 的核心源码 joint_schema_model.py,重点解读encode_record:State、图片与 Schema 是如何被打包进同一条 token 序列的。
🧭 先搞懂 Clef:一次前向传播的"决策模型"
传统大语言模型回答问题是"写作文":生成一段自由文本,再靠正则或 JSON 解析把答案抠出来。Clef 换了个思路——把答案的取值范围直接写进输入,让模型对每个选项打一个 logit:
| 问题类型 | 含义 | 选项来源 |
|---|---|---|
noul | 是/否判断 | 固定的true/false |
choice | 多选项分类 | criteria字典里的命名选项 |
score | 有序打分 | criteria列表按 0、1、2… 编号 |
三种类型的映射定义在 joint_schema_model.py 的QUESTION_TYPES中。模型输出后,对每个问题做一次 softmax,就得到了结构化答案(见 README.md 的 Model 一节)。
📂 仓库地图:从哪个文件开始读
这个仓库的"戏份"高度集中,读源码前先看这张表:
| 文件 | 作用 |
|---|---|
| joint_schema_model.py | 全文核心:record 编码、批处理、联合 schema 头、推理入口 |
| joint_head.safetensors + joint_head_config.json | 小 Transformer 决策头的权重与超参(width=1024、layers=4、routing_layers=2) |
| model-00001-of-00012.safetensors 等 12 个分片 + model.safetensors.index.json | Qwen3.8-27B 多模态主干(含视觉编码器,约 273.6 亿参数) |
| config.json | 主干模型架构配置(qwen3_5,hidden_size=5120) |
| processor_config.json | 图片/视频处理器配置(Qwen2VLImageProcessor) |
| tokenizer.json、chat_template.jinja | 分词器与聊天模板 |
一句话概括架构:大主干负责"读懂",小头负责"决策"。下面进入主角encode_record。
🔍 逐行解读 encode_record:打包的完整流程
encode_record的签名很克制(joint_schema_model.py):
def encode_record( tokenizer, record, # 含 state、可选 images/videos、questions max_length=16384, # 总 token 上限 max_state_tokens=None,# state 单独上限 processor=None, # 有图片/视频时必须提供 ) -> EncodedRecord它的产物是一个EncodedRecord(L69-L74):一串input_ids、每个问题的位置坐标(span),以及媒体张量。整个函数分四步。
第一步:把 Schema 渲染成"字段清单"
函数先固定写入标题SCHEMA FIELDS:,然后遍历record["questions"],为每个问题拼出一段带编号的文本(L110-L148):
FIELD 1 ID: status TYPE: choice INSTRUCTION: What is the invoice status? ALLOWED OPTIONS: OPTION 1: {"option_id": "paid", "description": "Invoice is paid."} ... END FIELD两个细节值得注意:
- 空指令自动兜底:
instructions缺失时直接拿问题 ID 当指令(L120-L123); - noul 类型自动补全:
question_options(L46-L57)会确保true/false两个选项永远存在,可选地用criteria里的自定义描述覆盖默认语义。
最关键的一步是"边写边记坐标":每写入一段文本,就记下它在schema_ids中的起止下标——问题指令是question_span,每个选项是option_spans(L129-L137)。这些 span 就是后面"决策头"从隐藏态里抽取证据的地址。
第二步:图片与视频在哪里进场
如果 record 带了images或videos,会走_encode_media(L81-L100):
- 用占位符拼出媒体文本:每张图一个 、每段视频一个 (L28-L29);
- 交给视觉处理器(
processor),得到pixel_values、image_grid_thw等张量(键列表见MEDIA_BATCH_KEYS,L30); - 处理器会返回已展开占位符的 token 序列,直接替换掉原始的占位文本。
回到主流程,媒体 token 被接在STATE:之后(L158-L161),并记下token_offset——批处理时mm_token_type_ids要用这个偏移把媒体标记填回正确位置(collate_records)。没有媒体时这一步直接返回空,纯文本零开销。
第三步:State 进位与长度控制
state通过render(L35-L43)统一转成紧凑 JSON 字符串(sort_keys保证同样的状态永远得到同样的 token,利于缓存与复现)。然后是两段"安检"(L163-L170):
- 可选的
max_state_tokens先截断 state; - Schema 部分永远不截断——如果 prefix + schema + suffix 就已经超过
max_length(默认 16384),直接抛ValueError。设计哲学很清楚:宁可报错,也不让模型看着残缺的选项清单瞎猜。
第四步:整体拼接与坐标平移
最终的输入序列是一个四段式结构:
┌──────────────┬────────────┬─────────────┬──────────────────┬──────────────────────┐ │ prefix │ media │ state │ schema │ suffix │ │ system提示 │ tokens(可选) │ JSON 状态 │ FIELD 清单 │ "JOINT SCHEMA │ │ + "STATE:\n" │ │ │ │ DECISIONS:" │ └──────────────┴────────────┴─────────────┴──────────────────┴──────────────────────┘拼接发生在 L188:prefix + state + schema + suffix。由于 schema 被放到了 state之后,第一步记下的 span 还差一个平移量——schema_offset = len(prefix) + len(state),于是所有 span 统一加上偏移(L171-L187),得到指向最终input_ids的准确坐标。函数最后返回EncodedRecord,空输入或空问题会直接报错(L189-L190)。
⚙️ span 的用途:决策头如何完成一次前向传播
知道 span 被"记录"之后,读者会问:它在哪被"使用"?答案是ClefModel.forward(L468-L491):
- 主干模型单次前向,带
use_cache=False取出全部last_hidden_state(媒体张量经**media注入); - 把隐藏态交给
JointSchemaHead(L281-L459)做决策。
头部的流水线可以浓缩成一句话:"取均值 → 路由证据 → 联合打分":
- 对每个问题 span / 选项 span 内的隐藏态做平均池化,得到"问题向量"和"选项向量"(_mean_span、L359-L386);
- 额外取选项 token 的词表嵌入均值作为"词汇向量"(L379-L383)——即使主干没"读懂",至少选项字面本身是个锚点;
- 两层
EvidenceRoutingLayer(L242-L278)让所有选项像 query 一样从整条序列中检索证据; - 4 层 Transformer 解码器(超参见 joint_head_config.json)完成字段级联合推理,所有问题的选项同时打分,所以叫"joint(联合)"头;
- 最终 logit = 词汇先验 +
sigmoid(门控)× 联合得分(L453-L457)。
对比主干约 273.6 亿参数(model.safetensors.index.json),决策头只有 1024 宽、4 层——决策逻辑是轻量外挂,主干只负责通用理解,这也是 Clef 可以低成本后训练的原因。
🚀 快速上手:5 行代码得到答案
理解了打包流程后,实际调用非常干净(示例改编自 README.md 的 Usage 一节):
from joint_schema_model import encode_record, collate_records, load_release_model model, processor = load_release_model(path, device="cuda") record = { "state": {"invoice": {"vendor": "Acme", "total": 1250.0, "status": "overdue"}}, "questions": { "status": {"type": "choice", "instructions": "What is the invoice status?", "criteria": {"paid": "Invoice is paid.", "overdue": "Invoice is past due.", "draft": "Not sent."}}, "large": {"type": "noul", "instructions": "Is the total above 1000 USD?"}, }, } encoded = encode_record(processor.tokenizer, record, processor=processor)之后collate_records补齐批处理张量、model(batch)一次前向,对每题 logit 做 softmax 即得概率。更省事的方式是直接调用systemone(L546-L576):它接收 Jev/SystemOne 风格请求体,内部自动完成encode_record → collate → 前向 → 概率换算,并返回带answers与usage的标准响应体。
📌 要点回顾与常见疑问
三个问题类型怎么选?noul适合"是否"判断,choice适合有限类别分类,score适合有序严重度/优先级评分——三者的选项都会被完整写进 schema,模型对每个选项各给一个 logit。
Schema 太长怎么办?会直接抛错而不是静默截断(L166-L169)。正确姿势是精简问题数量或用max_state_tokens压缩 state,把预算让给 schema。
图片能参与决策吗?能。图片 token 紧跟在STATE:之后进入同一序列,决策头的证据路由层会对整条序列(含媒体区域)做注意力检索,所以"看收据判断金额是否清晰"这类多模态判断在一次前向里完成。
关键路径速查:
- 打包逻辑:encode_record
- 批处理与媒体对齐:collate_records
- 决策头结构:JointSchemaHead
- 加载与推理入口:load_release_model、systemone
读懂了encode_record的"四段式打包 + span 记账",你就掌握了 Clef 最核心的设计:不是让模型复述答案,而是让答案的候选项成为输入的一部分,再由轻量决策头对它们联合打分——这正是"单次前向传播出结构化决策"的全部秘密。
【免费下载链接】clef项目地址: https://ai.gitcode.com/hf_mirrors/Cloudflare/clef
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考