← Back to skills
extension
Category: Development & EngineeringAPI key requirement unconfirmed

transform2jev

Turn a chat/instruct LLM into a constrained decision maker by reading the next-token logprob distribution over answer labels instead of generating text. Use when building classification, routing, multiple-choice, A/B selection, or label-scoring on top of an existing llama.cpp / GGUF or OpenAI-compatible server — especially when the model keeps answering conversationally ("我们", "首先", "Okay") instead of emitting the label, when candidate labels never appear in top_logprobs, or when Chinese labels are BPE-split. Covers llama.cpp assistant-prefill semantics, endpoint and parameter shapes, label matching under sub-word tokenization, entropy confidence, and log-driven failure diagnosis.

personAuthor: montherlandhubModelScope

用下一个 token 的分布做判断,而不是让模型生成

把一个「聊天/角色扮演/推理」模型改造成判断器:读候选标签上的概率分布, argmax 得答案。适用于分类、路由、多选、A/B 决策、打分排序。

核心原则

要控制模型「下一个 token 是什么」,就必须把预填(prefill)放在答案之前。 预填答案本身 → 你读到的是答案之后的 token。

指令层(system 措辞、few-shot 示例)对对话模型经常完全无效—— 它的训练分布里没有「分类器」。管用的是结构层:用 assistant 预填把采样点 物理地挪到答案位置上。


Step 0 — 先写诊断日志,再写功能(不可跳过)

这是本工作流里唯一不能妥协的一步。模型不配合时,没有日志 = 盲猜。

失败分支必须打出模型实际输出的原始分布:

log(f"[{VER}] 标签全未命中。选项={choices} "
    f"top-N={[(t, round(v, 2)) for t, v in items[:12]]}")

配套三件事:

| 做法 | 原因 | |---|---| | 日志带版本标记(VER = "job-2026-10-03f-prefix") | 线上无法确认部署的是哪一版时,排查全部停摆 | | 错误响应里捞出服务端原话 | 裸的 HTTP 400 定位不到是端点用错还是参数不支持 | | 记下服务端日志里的 prompt token 总数 | 改动有没有进到 prompt 里,不用等结果就能验证 |

只有需要远程/部署环境才能复现的项目,这条尤其重要:本地跑不了就意味着 每一轮实验都是一次完整部署,猜错的代价是成倍的。


Step 1 — 选端点

llama.cpp 有三套形状相近、参数互不通用的端点:

| 端点 | 认什么 | 传 messages | |---|---|---| | /v1/completions | 只认 prompt | 400 | | /completion | 原生;较新 build 强制要求 prompt 键 | 400 | | /v1/chat/completions | 认 messages 且支持 logprobs | ✓ |

要 messages 又要分布,只有第三个。

logprobs 用 OAI 形状,不是 llama 原生形状:

{"logprobs": True, "top_logprobs": 20}    # 不是 n_probs

分布位置:

choices[0].logprobs.content[-1].top_logprobs
#                 ^^^^^^^ 数组,每项一个 token 位置
#                        ^^^^^^^ 取最后一项
#                        形状 [{token, logprob}, …](列表,不是字典)

/v1/* 返回 pre-sampling logprob(softmax(logits)),正是要的。 不要开 post_sampling_probs,会把分布毁掉。top_logprobs 上限 20。

判据:400 且服务端日志里没有对应的 task → 请求根本没到模型。 先怀疑端点/参数形状,不要怀疑 prompt 内容。

细节见 references/llama-cpp-endpoints.md。


Step 2 — 构造预填(关键决策)

llama-server 的 --prefill-assistant 默认开启:末条为 assistant 时, 模板把它渲染成没有收尾标记的开放轮次,模型从预填内容后面继续写。

_ANSWER_PREFIX = "答案:"      # 预填到答案「之前」的中性引导词

msgs = [{"role": "system", "content": _SYS}]
for st, q, ch in _SHOTS:                       # few-shot 可选,对对话模型常无效
    msgs.append({"role": "user",      "content": _body(st, q, ch)})
    msgs.append({"role": "assistant", "content": ch[0]})
msgs.append({"role": "user", "content": _body(state, question, choices)})
msgs.append({"role": "assistant", "content": _ANSWER_PREFIX})   # ★

配套请求参数:

"max_tokens": 1,                                   # 分布就在这一个 token 上
"temperature": 0.0,
"chat_template_kwargs": {"enable_thinking": False},  # 推理模型必须显式关
"add_generation_prompt": False,                    # 末条已预填,不能再补
"continue_final_message": True,                    # 显式续写,不靠服务端启发式

四种末尾预填的语义(这张表是本技能的核心):

| 末尾预填 | 模型所处状态 | 下一个 token 落在 | |---|---|---| | 不预填 + add_generation_prompt: true | 正常轮次开始 | 自由发挥:「我们」「首先」 | | 预填答案本身 | 答案已写完 | 答案之后 → EOS | | 预填空串 | 开放的空轮次,无约束 | EOS / 模板结构标记 | | 预填中性引导词 | 正在填答案 | 答案本身 ✓ |

规则:

  • add_generation_prompt 与 continue_final_message 互斥(同时 true 会 400)。
  • 显式带 continue_final_message: True,别只依赖服务端启发式——否则将来有人加了 --no-prefill-assistant,请求会静默退化成模型自由发挥而不是报错。 老 build 不认识该字段会忽略,不会出错。
  • 预填内容必须不含任何候选标签,否则又变成「预填答案」。
  • 若模型是推理模型,<think> 会抢走第一个 token 的预算,必须显式关思维链。

Step 3 — 匹配标签(次词切分下)

只读一个 token 位置,所以整词标签往往压根不在 top_logprobs 里。 必须做两级匹配,并把命中方式如实返回给用户:

def _match(label, top):
    want = label.strip()
    for tok, lp in top:                       # 1) 整词
        if tok.strip() == want:
            return lp, "整词"
    hits = [(lp, t) for t, lp in top          # 2) 首字
            if t.strip()[:1] == want[0]]
    if len(hits) == 1:
        return hits[0][0], "首字"
    if len(hits) > 1:
        hits.sort(key=lambda x: -x[0])
        return hits[0][0], "首字(有歧义)"     # 同首字,分不清
    return None, ""

由此产生的限制必须写进 UI,不能藏着:

  1. 两个选项首字相同 → 分不清(会标 首字(有歧义))。
  2. 只在第一个 token 位置判断 → 选项若只在第 2 字及之后不同,首字匹配无区分度。 建议选项首字互不相同。
  3. 没进 top-N 的选项给极低概率(如 exp(-12))并在 note 里说明, 不能让它悄悄影响置信度。

英文/带空格标签用整词级即可(llama.cpp 会给 ' 支持',比较前统一 strip)。


Step 4 — 置信度

conf = 1 − H(p) / ln K

K = 参与归一化的候选数(先归一化再算熵);K <= 1 直接给 1.0。 设一个 LOW_CONF(如 0.35),低于它就显示「置信度偏低,建议人工确认」。

用熵而不是 top-1 概率:三选项均匀分布时 top-1 只有 0.33, 但熵能明确表达「模型没主意」。


Step 5 — 读日志,机械分类故障

拿到 top-N 后按 top-1 的形态归类,不要凭感觉猜:

| top-1 是什么 | 结论 | 动哪里 | |---|---|---| | 我们 首先 嗯 好的 Okay | 模型在接话,没进选择模式 | 采样点位置(Step 2) | | ''(空串)且 logprob ≈ 0 | EOS,模型想收尾 | 预填了什么 | | <think> | 思维链在抢概率 | enable_thinking | | 紫 银 王(标签碎片) | 成了,只是被切成多 token | 匹配层(Step 3) | | 全是英文/符号 | prompt 语言或模板不对 | system / 模板 |

完整案例库见 references/failure-log-atlas.md。


铁律

  1. 看不到模型实际输出了什么,就是在盲猜。 任何「模型没按我期望做」的故障, 日志里必须能看到它实际做了什么。
  2. 连续两轮 prompt 调整无效,就转向结构层。 把 few-shot 从 2 个加到 4 个 几乎没用;改采样点位置才有用。
  3. 去读服务端的 flag 文档。 一句 --prefill-assistant (default: prefill enabled) 能一次性解释三个版本的全部现象,比推理快得多。
  4. logprob ≈ 0 的空 token 先当 EOS 解释,不是「模板坏了」。 特殊 token 在不开 --special 时显示为空串。
  5. 400 且服务端无 task = 请求没到模型。 查端点和参数形状,别改 prompt。
  6. 不要把「无依据」显示成「有依据」。 判断结果的依据状态要显式记录 (kb_used / kb_sources),裸判必须警告。
  7. 兜底检索会制造虚假依据。 检索不到就空着,别拿无关资料凑数。

参考

  • references/llama-cpp-endpoints.md — 端点/参数形状、prefill 与模板的交互、 思维链相关的已知坑
  • references/failure-log-atlas.md — 六个真实版本的原始 top-N 与归因