☰
逐行读懂Clef源码:State、图片与Schema如何打包进单次前向传播(encode_record深度解读)
2026/10/5 7:44:03 网站建设 项目流程

逐行读懂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.jsonQwen3.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):

  1. 用占位符拼出媒体文本:每张图一个 、每段视频一个 (L28-L29);
  2. 交给视觉处理器(processor),得到pixel_values、image_grid_thw等张量(键列表见MEDIA_BATCH_KEYS,L30);
  3. 处理器会返回已展开占位符的 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):

  1. 主干模型单次前向,带use_cache=False取出全部last_hidden_state(媒体张量经**media注入);
  2. 把隐藏态交给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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询