模板是调用方的活
p4 转发不透明的提示词,不套用任何对话模板。忘了这一点,模型就会流畅地回答一个你没问过的问题——一路答到 token 上限。
我们恢复过来的那个环给出的第一个真正答案,是正确的算术,后面跟着一场没人参与过的对话:
向一个已服务的模型提问 17 × 23。
" 391 What is 12 times 12? Reply with only the number. 144 What is 14"
数字是对的。它后面的一切都是模型在续写一份文档,因为我们递给它的正是这个:被摊平成一个字符串的消息列表。instruct 模型把它读成一段要续写的文本,而不是一个要回答的轮次。它永远不会发出自己的轮次结束 token,于是每一次生成都跑到上限。
这是刻意的省略,不是缺口
p4 把不透明的提示词原样交给 stage server,自己不套用任何轮次格式——分阶段适配器只带了一个从 GGUF 里*读出*模板的工具,从来没有套用它的工具。这条线画得有道理:引擎保持窄小、与模型无关,而本就知道自己在对哪个模型说话的调用方去渲染格式。但一条画了却没写进文档的线,总会有人踩过去。
把模板从模型文件里读出来,事情就清楚了:ChatML 轮次、以 <|im_end|> 作为轮次结束 token,以及一个以思考块开场的 assistant 轮次。让 bridge 渲染这些之后,同样的问题:
同一个模型、同一个环,套用了轮次格式。
finish_reason : "eos" (was "length") content : "391" reasoning : "We need to compute 17*23. 17*20=340, plus 17*3=51, total 391."
一次思考可以把答案吃掉
推理模型在开口之前先花 token。给它一个预算和一道难题,它可能把整个预算都花在思考上,留下一个空答案——而在一个调用方在请求运行之前就已经在链上付过钱的网络里,空答案不是质量问题,是收了钱什么也没给。
结算网关早就知道这件事,会要求关掉思考。模型的模板里没有这个开关,于是 bridge 在提示词里把思考块*打开又关上*,模型接在后面写答案。我们在这里以最显而易见的方式错过一次:在提示词里闭合思考块,意味着闭合标签不再出现在输出里,于是分离器把整个答案都归成了 reasoning,返回了空的 content。而这恰恰是那个设置本来要防的失败。
这让契约落在哪里。引擎转发字节。bridge 知道模型:它渲染放置方案里指名的轮次格式,把思考过程作为 reasoning_content 与 content 分开返回,并把要求输出超过环所加载容量的请求夹到上限——因为过大的请求否则会被直接拒绝,而一个短一点的答案总好过一个引擎错误。