让大模型做判断题,它总想给你写小作文。Cloudflare 开源的 Clef 干脆不生成文字:一次前向传播,直接返回每个选项的概率。本文用 Clef-Flash Q4_K_M 量化版在本地跑了一遍,还写了个 .NET 控制台来测它。
1. 引言
做 Agent 或者工作流开发时,有一类需求出现频率极高:让模型看一眼当前状态,然后做个判断:这条工单该分给哪个组?这张发票是不是逾期了?严重程度算几级?
这些事用普通大模型做,总有点别扭。你想要的是一个答案,它给你的是一段话。于是你要写提示词约束格式、让它输出 JSON、再写代码解析,祈祷它别在 JSON 外面包一层"好的,我来分析一下"。更要命的是,你无法从一段文本里可靠地读出"它有多确定"。
最近这个痛点催生了一个新物种:决策模型(decision model)。Typesafe AI 的 Jev 提出了 System One 模型的概念。不做长链推理,只做快速、结构化的直觉判断,直接输出代码能消费的类型化结果。这个赛道最近相当热闹:9 月底 StartLux(原点星辉)开源了 StartLux-Decision,一口气放出 0.8B 到 27B 五档版本;上海 AI 实验室也发布了 Intern-Decision。紧接着 Cloudflare 带着Clef入场,Apache 2.0 协议开源,目前在 Jev Decision Index 基准上排名第一。
更关键的是配套生态的跟进速度:llama.cpp 官方在三天内把 SystemOne 接口(/v1/systemone)做进了 llama-server。这套原本要调云端 API 的决策能力,现在一条命令就能在本地跑,还兼容 Jev 的客户端协议。
这篇文章主要做三件事:讲清楚 Clef 到底是什么、和传统 LLM 路线差在哪,然后用 llama.cpp 在本地跑起来,最后用一个 .NET 控制台程序实际测一测。
2. Clef 是什么:一个不会聊天的模型
Clef 是 Cloudflare 首个自研并开源的决策模型系列,一共两个:
| 模型 | 基座 | 定位 |
|---|---|---|
| Clef | Qwen3.8-27B + LoRA | 高精度版 |
| Clef-flash | Qwen3.5-9B + LoRA | 低延迟版,中位延迟 38.8ms |
名字取得挺讲究:clef(谱号)是五线谱开头定义整段音符音高的符号,决策模型则是"定义上下文的域以及随后的行动";CF 又恰好是 Cloudflare 的缩写。
它的工作方式和聊天模型完全不同。你给它两样东西:一个状态(state),可以是文本、JSON,甚至图片和视频;和一组类型化问题(typed questions)。它一次前向传播,返回每个问题每个选项的概率。没有自由文本生成,也没有输出解析这回事。
问题只有三种类型,这就是从 Jev 沿袭下来的三种"原语":
| 类型 | 问的是什么 | 返回 |
|---|---|---|
choice | 从命名选项里选一个 | 各选项概率 + 置信度 |
score | 按有序等级打分(如"不紧急/本周/今天") | 各等级概率 + 期望分数 + 置信度 |
noul | 是非判断(true/false) | 为真的概率(0~1) |
看一个官方示例就明白了。给它一段客服场景描述,同时问三个问题:
response=systemone(model,processor,{"model":"clef","state":"Our checkout started returning errors and orders are blocked.","questions":{"department":{"type":"choice","criteria":{"billing":"Payments or invoices","technical":"Bugs or outages"},},"urgency":{"type":"score","criteria":["Can wait","This week","Today"]},"outage":{"type":"noul","instructions":"Is a service down?"},},})返回的是按问题 ID 组织的结构化答案:department 大概率是 technical,urgency 偏向 Today,outage 为真的概率接近 1。代码拿到结果可以直接分支、排序、路由。这正是它和"让 LLM 输出 JSON"的本质区别:概率是模型算出来的,不是从文本里解析出来的。
Jev 的中文 cookbook(datawhalechina.github.io/jev-cookbook)里有一个设计哲学我很认同:问题要原子化,组合逻辑放在代码里。不要问"给这个创业项目打分",而是分别问市场规模、技术可行性、差异化,然后用你自己的公式组合。优先级变了,改代码里的一个系数就行,不用重写提示词。而且每个问题相互独立评估,增加问题不会造成上下文腐化,也几乎不增加延迟。
还有一个生态层面的好消息:这套/v1/systemone接口已经不是云端 API 的专属。llama.cpp 官方在 10 月 2 日合并了 PR #29818,llama-server 原生提供 SystemOne 端点,首批支持 laya、openjev、kev 等五个决策模型,次日又通过 PR #29831 加入了 Clef。也就是说,任何按 Jev 协议写的客户端,现在都可以零改动地指向一台本地 llama-server,这对数据不出内网的场景意义重大。
3. 它为什么快:把"决策"从自回归生成里拆了出来
普通 LLM 做一次分类,走的是完整的老流程:逐 token 生成文本,你再从文本里抠答案。这是自回归的,慢,而且结果本质上是"一段很像答案的话"。
Clef 的做法是另一条路。Cloudflare 冻结了 Qwen 基座,只训练两部分:一个 rank-256 的 LoRA 适配器,和一个联合 schema 打分头(joint schema head)。推理时只做 prefill,不打字。打分头直接读取骨干网络的最终隐藏状态,通过两阶段注意力机制,先把与每个选项相关的证据从状态里路由出来,再对所有问题的所有选项做联合跨字段打分。每个合法选项输出一个 logit,按问题做 softmax 就是概率。
一句话总结:它把"生成一段结构化文本再解析"换成了"直接从内部表示导出结构化答案"。决策步骤是非自回归的,没有中间文本,所以快得离谱。
后训练也有讲究:对合法 schema 输出用 label-smoothed cross-entropy,配合 Brier loss 优化概率校准,再加上 Cloudflare 自研的 RLCD(面向校准决策的强化学习),对相邻的有序选项给部分分数、奖励整条记录完全正确的输出,同时施加 reference penalty 防止分布漂移。最终效果就是输出被约束成纯概率,而且概率本身是校准过的,代码可以放心拿置信度做门控。
Cloudflare 自己已经在用了。威胁情报团队用 Clef 给域名分类:结合浏览器渲染抓取网页后,Clef 用2.2 秒完成整个流程并输出多个类别的概率(比如 95% 时尚网站、85% 电商、不到 1% 钓鱼);而最快的通用模型 gpt-oss-120b 走同样流程要4.7 秒,还只返回两个分类结果。延迟和结果丰富度都是约两倍的差距。
4. 跑分:Decision Index 第一名是什么水平
Clef 的评测跑的是 Decision Index 0.2.1 套件,对手是 Jev、DiffusionGemma Jev、Kev 9B 和 Laya。完整表格有四十多项,挑一些有代表性的(分数为百分比,加粗为该基准最佳):
| 基准 | Clef | Clef-flash | Jev | Kev 9B |
|---|---|---|---|---|
| BFCL 函数调用(准确率) | 98.5 | 98.8 | 95.8 | 94.5 |
| API-Bank(准确率) | 91.9 | 93.1 | 88.2 | 56.3 |
| BANKING77 意图分类(macro-F1) | 94.2 | 90.9 | 79.7 | 84.8 |
| CLINC150+OOS(macro-F1) | 97.4 | 66.8 | 89.3 | 79.0 |
| 家电模拟器(准确率) | 83.0 | 97.7 | 52.3 | 25.0 |
| ContractNLI 合同推理(macro-F1) | 81.4 | 84.3 | 71.7 | 57.8 |
| ForecastBench(Brier,越低越好) | 13.9 | 10.6 | 17.4 | 17.6 |
| 中位延迟(ms,越低越好) | 209.3 | 38.8 | 524.1 | 51.4 |
| p95 延迟(ms,越低越好) | 238.6 | 122.4 | 536.0 | 187.9 |
几个值得注意的点:
- Clef 系列在绝大多数决策类基准上领先,Jev 只在少数知识推理类项目(GPQA、BBH、MMLU-Pro)上占优,那些本来就更像"考试",不像"决策"。
- Clef-flash 在家电模拟器上拿到 97.7%,是全场最高,比自家大哥还高了 15 个百分点,说明小模型在设备控制这类边界清晰的任务上完全够用。
- 延迟差距是数量级的:Clef-flash 中位 38.8ms,只有 Jev 的 7%。把决策模型放进 Agent 的热路径,这个差距直接决定架构可不可行。
Typesafe 官方的端到端工作流评测(发票处理、客服、安全事件、Agent 链路观测)里,四项中 Clef 系列赢下三项,剩下一项与 Jev 基本持平。这个成绩是在"API 与 Jev 完全兼容"的前提下拿到的,意味着现有 Jev 集成可以近乎零成本切换。
和 Jev 相比,Clef 还多两张牌:带视觉编码器,可以直接输入图片和视频做分类(Jev 目前仅支持文本);上下文窗口 64k,是 Jev 32k 的两倍。
4.1 开源决策模型,现在有哪些选择
决策模型开源圈半个月内一下子热闹起来,把三家放在一起看更清楚:
| Clef / Clef-flash | StartLux-Decision | Intern-Decision | |
|---|---|---|---|
| 发布方 | Cloudflare | StartLux(原点星辉) | 上海 AI 实验室 |
| 规格 | 27B、9B 两档 | 0.8B / 2B / 4B / 9B / 27B 五档 | 4B 等 |
| 权重协议 | Apache 2.0(可商用) | CC BY-NC 4.0(仅非商业) | 见其仓库 |
| GGUF | 官方 ggml-org 转换(含打分头)+ bartowski 社区量化(仅骨干) | 官方直接提供 Q8_0 / Q4_K_M / BF16 | 见其仓库 |
| 视觉输入 | 支持(图片/视频) | 未提及 | 未提及 |
| 接口 | 兼容 Jev / SystemOne | 兼容 /v1/systemone | — |
有两点值得展开。一是协议:StartLux-Decision 的权重是 CC BY-NC 4.0,研究和非商业用途没问题,商用要单独授权;Clef 直接 Apache 2.0,对企业落地来说省心得多。二是跑分口径:StartLux 官方公众号公布的 Decision Index 成绩是 63.88(自测,对照 9 月 28 日榜单快照),高于 Jev 1.13 的 57.91;Clef 的"第一"则来自 Cloudflare 用同一套件的内部评测。两家口径、快照时间不同,严格说不能直接比大小。但结论方向一致:开源决策模型已经整体越过了 Jev 这条线,而且分数还在快速刷新。对开发者来说,与其纠结谁是榜一,不如按协议、规格和部署成本挑一个上手试。
5. 本地跑起来:llama.cpp 三天内原生支持了 Clef
模型在 Hugging Face 上完全开源,而第 2 节提到的 llama.cpp 原生支持让本地部署变得极其简单,joint schema 打分头直接进了 llama.cpp 的计算图,官方预量化 GGUF 同步放出:
llama-server-hfggml-org/Clef-Flash-GGUF# 9B,提供 BF16 / Q8_0 / Q4_K_M(6.49GB)llama-server-hfggml-org/Clef-GGUF# 27B-hf方式会从 Hugging Face 拉模型。国内网络不方便的话,官方在 ModelScope 上也放了同一份文件,下载后用-m指定本地路径即可:
# 从 ModelScope 下载 Clef-Flash-Q4_K_M.gguf 后llama-server-m./Clef-Flash-Q4_K_M.gguf--port8000-b4096-ub4096从官方 GGUF 的元数据里还能读到两个有意思的信息:一是clef.context_length = 262144,原生 256K 上下文;二是元数据里同时存在ssm.*和full_attention_interval = 4这些键,Qwen3.5 基座是线性注意力与全注意力交替的混合架构,这也解释了为什么 PR 里说这个模型"明显更复杂",需要新增llama_batch_extAPI 来标记问题段和选项段。决策头本身的配置也在元数据里:2 个路由块、4 个决策块、16 个头。
用最新版 llama.cpp 加载 Clef 后,有几个和普通模型不同的行为:
- server 只提供
/v1/systemone一个端点,聊天接口不再可用,它变成一台专职决策服务器。 - 整个 prompt(state + 所有问题和选项)在一次 batch 内算完,必须放得下
--ubatch-size。长 state 记得调大,比如-ub 4096。 - 视觉输入暂未支持(要等 PR #29622 合并),且单 batch 单序列。
5.1 一个容易踩的坑:bartowski 的 GGUF 跑不了 /v1/systemone
我手头先下载的是 bartowski 的量化版(Q4_K_M,5.84GB),这个发布的比较早,下载量也多,但用最新 llama.cpp 启动后调/v1/systemone,直接返回 501 “This model is not a decision model”。排查后确认了原因:服务器是根据 GGUF 元数据{arch}.decision.type判断决策模型的,而 bartowski 的文件是在 Clef 支持合并之前转换的。把两个文件的头部元数据拉出来对比,差异一目了然:
| bartowski 版 | ggml-org 官方版 | |
|---|---|---|
general.architecture | qwen35 | clef |
| decision 元数据 | 无 | clef.decision.type = clef |
| 张量数量 | 427(仅骨干网络) | 587(多出 160 个打分头张量) |
这不是启动参数能补救的,--override-kv之类的参数可以改元数据,但变不出文件里不存在的 160 个打分头张量。要么换 ggml-org 的官方转换版,要么用最新 convert 脚本自己从 HF 权重转。
不过 bartowski 版也不是白下载:它可以把 clef-flash 当普通生成式模型跑,正好给我提供了一个对照实验。同一个模型,"生成 JSON 再解析"和"打分头直接出概率"两种玩法到底差多少。从对话测试截图来看,生成的结果是否可靠暂且不论,这个直接 5s 才出结果的体验还是糟糕的。
6. .NET 控制台实测:两种玩法对比
测试场景统一为工单分流:一段 502 故障描述,问三个问题:该由哪个团队处理(choice)、紧急程度(score)、是否为服务中断(noul)。客户端是零第三方依赖的 .NET 文件级程序,只用HttpClient和System.Text.Json。
6.1 玩法一:生成模式模拟(bartowski GGUF + chat completions)
先用 bartowski 版走 OpenAI 兼容接口,靠提示词让模型输出 JSON:
usingSystem.Text;usingSystem.Text.Json.Nodes;varhttp=newHttpClient{BaseAddress=newUri("http://localhost:8000")};// 决策任务:状态 + 类型化问题,用提示词模拟 schemavarstate="我们的结账接口从一小时前开始对全部用户返回 502,订单全部卡住。";varprompt=$$$""" 你是一个决策模型。针对下面的状态回答问题,只输出 JSON,不要输出任何其他文字。 状态:{{{state}}}问题:1.department:该由哪个团队处理?选项 billing=支付或发票问题,technical=故障或缺陷,sales=商务咨询2.urgency:紧急程度评分?选项0=可以等,1=本周处理,2=今天处理3.outage:是否为服务中断?true/false输出格式(字符串值必须加英文双引号,只使用英文标点):{"department":"...","urgency":0,"outage":false,"confidence":{"department":0.0,"urgency":0.0,"outage":0.0}}""";varrequest=newJsonObject{["messages"]=newJsonArray(newJsonObject{["role"]="user",["content"]=prompt}),["temperature"]=0.0,["max_tokens"]=512,// llama.cpp 扩展:约束输出必须是合法 JSON(GBNF 语法层面保证,不依赖模型自觉)["response_format"]=newJsonObject{["type"]="json_object"},};// 手动序列化/解析,不走反射(文件级程序默认禁用反射序列化)usingvarresp=awaithttp.PostAsync("/v1/chat/completions",newStringContent(request.ToJsonString(),Encoding.UTF8,"application/json"));resp.EnsureSuccessStatusCode();varbody=JsonNode.Parse(awaitresp.Content.ReadAsStringAsync())!;varcontent=body["choices"]![0]!["message"]!["content"]!.GetValue<string>();// 剥离 <think> 思考段,截取第一个 { 到最后一个 } 之间的 JSONvartext=content;varthinkEnd=text.IndexOf("</think>",StringComparison.Ordinal);if(thinkEnd>=0)text=text[(thinkEnd+8)..];vardecision=JsonNode.Parse(text[text.IndexOf('{')..(text.LastIndexOf('}')+1)])!;Console.WriteLine($"团队:{decision["department"]}");Console.WriteLine($"紧急度:{decision["urgency"]}");Console.WriteLine($"服务中断:{decision["outage"]}");这段代码能跑起来也是解决了几个坑的,最主要的还是模型输出格式坑。
第一版靠提示词约束 JSON,模型返回了不带引号的值和中文逗号,根本解析不了。加上 llama.cpp 支持的response_format: {"type": "json_object"}后,输出合法性由 GBNF 语法在采样层面保证,这是本地部署相比单纯提示词工程最实用的一张牌。另外 Qwen 系的思考段(<think>...</think>)也要剥掉再解析。
格式问题解决了,但实测答案本身翻了车:
团队: technical ← 对了 紧急度: 0(可以等) ← 全站 502 一小时,还"可以等"? 服务中断: false ← 同上 置信度: 全部 0.0 ← 照抄提示词模板里的示例值格式对了,判断错了。这不能全怪模型,clef-flash 的决策能力在打分头里,而 bartowski 版只有骨干网络,相当于让一个"被拿掉决策器官"的模型用聊天的方式假装做决策。这个反面教材恰好说明了决策模型存在的意义:判断的可靠性不该建立在"模型今天愿不愿意好好说话"上。
6.2 玩法二:原生决策接口(ggml-org GGUF + /v1/systemone)
换上ggml-org/Clef-Flash-GGUF后,请求变得极其干净,没有提示词工程,没有输出解析,就是 state + questions:
usingSystem.Text;usingSystem.Text.Json.Nodes;varhttp=newHttpClient{BaseAddress=newUri("http://localhost:8000")};varrequest=newJsonObject{["state"]="我们的结账接口从一小时前开始对全部用户返回 502,订单全部卡住。",["questions"]=newJsonObject{["department"]=newJsonObject{["type"]="choice",["instructions"]="该由哪个团队处理?",["criteria"]=newJsonObject{["billing"]="支付或发票问题",["technical"]="故障或缺陷",["sales"]="商务咨询",},},["urgency"]=newJsonObject{["type"]="score",["instructions"]="紧急程度如何?",["criteria"]=newJsonArray("可以等","本周处理","今天处理"),},["outage"]=newJsonObject{["type"]="noul",["instructions"]="是否为服务中断?",},},};usingvarresp=awaithttp.PostAsync("/v1/systemone",newStringContent(request.ToJsonString(),Encoding.UTF8,"application/json"));varraw=awaitresp.Content.ReadAsStringAsync();if(!resp.IsSuccessStatusCode){Console.WriteLine($"HTTP{(int)resp.StatusCode}:{raw}");return;}varanswers=JsonNode.Parse(raw)!["answers"]!;foreach(var(id,node)inanswers.AsObject()){switch(node!["type"]!.GetValue<string>()){case"choice":Console.WriteLine($"{id}=>{node["choice"]}(confidence{node["confidence"]})");break;case"score":Console.WriteLine($"{id}=>{node["score"]}(confidence{node["confidence"]})");break;case"noul":Console.WriteLine($"{id}=> P(true) ={node["noul"]}");break;}}实测结果(RTX 4070 Ti SUPER,Q4_K_M 全量 GPU offload,328 个输入 token、三个问题一次回答):
department [choice] => technical (confidence 0.88, 概率 billing 0.07 / sales 0.01 / technical 0.92) urgency [score] => 1.59 (confidence 0.38, 概率 可以等 0.16 / 本周处理 0.10 / 今天处理 0.75) outage [noul] => P(true) = 0.90 usage: input_tokens 328, output_tokens 0和玩法一的翻车结果放在一起看,差距一目了然:同一个 state,生成模式判"可以等、非服务中断、置信度照抄 0.0",真打分头给出 technical 0.92、outage 0.90、“今天处理” 0.75,三题全部符合直觉。
这里有两个细节值得注意:
- score 返回 1.59 而不是整数:它是按概率加权的期望级别,可以落在两级之间,比硬分类的信息量更大。
output_tokens恒为 0,没有任何文本被生成,这就是"不吐一个字"的字面含义。choice 和 score 的 confidence 是打分头算出来的,可以直接拿来做置信度门控(低于阈值转人工或转大模型),而不是玩法一里模型"自报"的数字。官方也诚实地提醒:概率按模型文件里存好的温度缩放,在你的数据上不一定校准过,门控阈值要结合业务数据自己标定。
6.3 延迟实测与调参说明
在 RTX 4070 Ti SUPER 上连续请求,单次耗时稳定在64~92ms(328 个输入 token、三个问题一次回答;服务启动后的首次请求约 500ms,是 GPU 计算图编译的一次性开销)。这个数字和官方宣传 clef-flash 的 38.8ms(Cloudflare 生产环境,H200 级硬件)已经在同一量级,消费级显卡跑出两位毫秒,"决策模型很轻"这件事是实打实的。
因为启动脚本我是直接复制本地通用大模型,然后改的模型地址,所以做了参数调整的测试。有意思的是这个实验结论:怎么调都差不多。
--flash-attn开不开,耗时没有可感知的差别;-b 4096 -ub 4096加上去,也一样。把启动参数全部砍掉,输出概率逐位一致、显存占用相同(8.1GB,新版 llama.cpp 的--fit默认开启,会自动按显存调整未指定的参数)。原因不复杂:决策请求的耗时几乎全是 prompt 前向传播(prefill),纯算力瓶颈,而模型权重和计算图是固定的,-ngl、-c、-t、-ctk、-fa这些参数碰到"一次前向就出结果"的负载,全都没有发挥空间。真正影响速度的只有两件事:prompt 的 token 数(问题和选项描述越短越快,基本线性)和GPU 算力。想再快,要么缩短 prompt,要么换更小的决策模型(比如 4B 档)。
参数对速度无感,但有一个参数对能不能跑至关重要,-ub:Clef 要求整个 prompt 在一个物理 batch 里算完,而-ub默认只有 512。实测 980 个 token 的 prompt 直接返回 HTTP 500 “input is too large to process. increase the physical batch size”。所以推荐启动命令是:
llama-server-mClef-Flash-Q4_K_M.gguf--port8000-b4096-ub4096-b(逻辑 batch,默认 2048)要不小于-ub,所以两者一起给。如果你的 state 可能更长(比如丢给它一整篇日志),按需再加大。
7. 结语
Clef 让我感兴趣的不是又一个开源模型,而是它代表的分工思路:把"判断"和"表达"拆开。聊天模型负责思考和表达,决策模型负责在关键节点快速给出带概率的结构化答案,代码在两者之间做编排。Agent 工作流里大量"该走哪条路"的瞬间,其实根本不需要一段散文。
对个人开发者来说,眼下最实际的用法就是本地跑ggml-org/Clef-Flash-GGUF的 Q4_K_M:Apache 2.0,一条/v1/systemone请求就是一次带概率的决策,当分类器、路由器、意图识别器用,成本只有电费。