一份从零到跑通的实战记录:如何在网络受限的机器上部署 Laya(Jev 的开源对标决策模型),写好协议兼容层让页面与评估工具一行不改,并与云端 Jev 并存双后端。文中每个数字、每个坑都来自本次真实实践。
Laya 是 Convai Innovations 发布的 System 1 决策模型,定位就是 TypeSafe Jev 的开源对标:给它一段状态和一组类型化问题(choice / score / noul),它在一次前向传播内返回带概率分布与置信度的结构化答案。421M 参数,ModernBERT-large 底座 + 从零训练的决策头,Apache 2.0 许可。
它不是生成式模型——不生成任何文本,所以没有「解析模型输出」这一步,也就没有解析失败与幻觉。这一点与 Jev 的设计哲学完全一致,也是两者可以互相替换的根基。
但要记住这条设计的另一面:判别式模型没有「推理能力」可以借。它不像大语言模型能从常识推出「消行是好事、造洞是坏事」,它的全部判断力来自训练分布里见过的模式。这意味着——一旦把它放到训练分布之外的领域(比如本项目的俄罗斯方块盘面),它可能直接退化成接近随机。这是本地部署前就该知道的前提,详见第 7 节的实测诊断。
| Jev(TypeSafe) | Laya(Convai) | |
|---|---|---|
| 部署 | 云端 API,数据出域 | 本地 / 内网 / 单机,数据零出域 |
| 成本 | 按 token 计费 | 自托管后为零(只烧电) |
| 延迟 | 实测 240–400ms | 单问约 33ms(GPU);本机 MPS 全链路实测约 386ms |
| 许可 | 专有 API | Apache 2.0,权重可下载、可微调 |
| 接口 | 同构:状态 + 类型化问题 → 选项 / 概率 / 置信度 / 分数 | |
一个仓库里装了三个权重,按 subfolder 加载:
| checkpoint | 底座 | 总上下文 | head 预算 | 适用 |
|---|---|---|---|---|
english(仓库根) | ModernBERT-large 421M | 512 | 192 | 英文、问题短的场景 |
multilingual | mmBERT-base 322M | 1024 | — | 100+ 语言,速度约 2 倍 |
typed-decisions | ModernBERT-large 421M | 1024 | 256 | 类型化决策工作流(本项目选择) |
两列预算要分开看:总上下文决定整个序列能装多长,head 预算(head_max_len)只决定「题干 + 全部选项」能分到多少 token——后者才是本项目真正的瓶颈,第 3 节会详述。
本项目每个决策点要携带棋盘状态(20 行棋盘 + 词化字段)+ 数十个候选落点描述,实测单次请求 1648 token 摊到多个问题上。english 的 512 预算偏紧,typed-decisions 的 1024 才是安全区。选型时先算「一个问题最坏要装多少 token」,再挑 checkpoint。
Laya 要求 torch >= 2.0、transformers >= 4.48(ModernBERT 支持)。本次实测组合:torch 2.13.0 + transformers 4.57.6 + laya 0.3.21,全部通过。建议装进独立虚拟环境:
# 建隔离环境(不要污染系统 Python)
python3 -m venv ~/.venvs/laya
~/.venvs/laya/bin/pip install "laya>=0.3.3"
# 验证版本
~/.venvs/laya/bin/pip list | grep -iE "torch|transformers|laya"
这是本次部署最大的坑:机器上的代理对 PyPI 正常,但 huggingface.co 与 hf-mirror.com 全部不通(隧道 502 / 连接重置),laya.load() 直接抛 ProxyError。
解决办法:ModelScope(魔搭)上有同一个模型的镜像,用官方 HTTP 接口逐文件下载。关键是保持 HF 仓库的目录结构,这样 laya.load() 可以直接吃本地路径,将来切换回 HF 也不用改代码:
BASE="https://www.modelscope.cn/models/convaiinnovations/laya/resolve/master"
DEST="$HOME/.workbuddy/models/laya"
mkdir -p "$DEST/encoder" "$DEST/tokenizer" \
"$DEST/typed-decisions/encoder" "$DEST/typed-decisions/tokenizer"
# 先查清单(大小对照,确认下载完整)
curl -s "https://www.modelscope.cn/api/v1/models/convaiinnovations/laya/repo/files?Recursive=true"
for f in "model.safetensors" "encoder/config.json" "rl_agent_config.json" \
"tokenizer/tokenizer.json" "tokenizer/tokenizer_config.json" \
"typed-decisions/model.safetensors" "typed-decisions/encoder/config.json" \
"typed-decisions/rl_agent_config.json" "typed-decisions/tokenizer/tokenizer.json" \
"typed-decisions/tokenizer/tokenizer_config.json"; do
curl -sL -m 600 --retry 3 -o "$DEST/$f" "$BASE/$f"
done
每个 checkpoint 的权重约 842MB,两个共约 1.7GB,实测网速下 6 分半钟下完。下载后务必核对文件大小与清单一致——残缺的 safetensors 在加载时才会报错,浪费排查时间。
transformers 在 import 时会探测 TensorFlow,若机器上装有 TF,其 abseil 运行时可能死锁模型构建,表现为 laya.load() 长时间挂起、无报错。官方文档明确建议:USE_TF=0。
当 http_proxy 环境变量存在时,curl http://localhost:8788/... 也会被转发到代理,返回 upstream connect failed: Connection refused——看起来像「服务没起来」,其实服务好好的。测本地服务必须加 --noproxy "*";Node 的 fetch(undici)不读代理环境变量,故不受影响。
正式集成前,先花五分钟验证「模型能加载 + 三类问题都能答 + 拿到延迟量级」。本项目脚本 eval/laya_smoke.py 就干这个,输出示例:
[load] 23.8s checkpoint=english
[device] mps
[first predict] 312ms
[warm predict] median 72ms runs=['85', '72', '72']
{
"placement": { "type": "choice", "choice": "p2",
"probabilities": {"p1": 0.3018, "p2": 0.4332, "p3": 0.265},
"confidence": 0.0207, "answer_confidence": 0.4332 }
// … score / noul 略
}
本次集成的核心决策:不做业务代码改造,而是写一层协议兼容层。原系统的页面与评估工具都通过 POST /v1/systemone 协议说话,那么只要让 Laya 服务也讲这套协议,所有调用方零改动,并且天然获得双后端并存的能力(改一个 API 地址即切换)。
先逐项核对原协议的每个字段 Laya 是否吃、怎么吃。核对方法是读 Laya 的输入校验源码(agent.py 的 _normalize 一带),而不是猜:
| 字段 | Laya 的要求(源码行为) | 本项目取值 |
|---|---|---|
state | 字符串 / dict / 对话列表,dict 会被 JSON 序列化 | 嵌套 dict {game:{…}} 直接可用 |
choice criteria | dict(label→描述);list 形式时 label 既是选项文本又是答案 key | dict {落点id: 描述} 直接可用 |
score criteria | list,index 0 起 | 数组(健康度分级)直接可用 |
noul criteria | dict,只接受 true / false 两个 key,其他 key 会报错 | {true:…, false:…} 直接可用 |
instructions | 字符串;非字符串会被 json.dumps 成 JSON 文本喂给模型 | 对象 {question, priorities} 需展平 |
答案 choice | 返回 criteria 的 key 原样 | 落点 id 直接可用 |
可以看到:只有 instructions 一项需要真正转换,其余天然兼容——这正是「同族模型」的价值。
Laya 对非字符串 instructions 会 json.dumps,模型读到的是 JSON 文本——能用,但不如展平后的自然语言。把对象拆成「问题句 + 编号优先级列表」:
def flatten_instructions(ins):
"""对象型 instructions 展平为自然语言;字符串原样返回。"""
if isinstance(ins, str):
return ins
if isinstance(ins, dict):
parts = []
if ins.get("question"):
parts.append(str(ins["question"]))
prios = ins.get("priorities")
if isinstance(prios, list) and prios:
parts.append("Priorities (highest first):\n" +
"\n".join(f"{i + 1}. {p}" for i, p in enumerate(prios)))
return "\n".join(parts)
return json.dumps(ins, ensure_ascii=False) # 兜底:信息不丢
Laya 的答案里同时有两个值:confidence(经 RLCD 训练的校准置信,回答「我这个判断有多可信」)与 answer_confidence(top 选项概率)。实测差异巨大:同一决策分别是 0.0181 与 0.1719。
原系统的 UI 与历史日志展示的是 top 概率口径,所以适配层把 confidence 统一为 answer_confidence,同时把原校准值保留在新字段 calibrated_confidence 里——这样既不破坏调用方,也不丢信息(将来做选择性自动化时,校准值才是真正该用的那个)。
def adapt_answer(ans):
"""choice 置信度对齐 Jev 页面口径;校准置信保留。"""
if isinstance(ans, dict) and "answer_confidence" in ans:
ans.setdefault("calibrated_confidence", ans.get("confidence"))
ans["confidence"] = ans["answer_confidence"]
return ans
调用方传来的模型名是原系统的(如 jev-latest)。兼容层做一层映射并兜底:名字不认识但带原系统前缀时按默认 checkpoint 处理,权重目录不齐时自动回退:
MODEL_MAP = {
"laya": "typed-decisions", "laya-typed": "typed-decisions",
"laya-en": None, "english": None, # None = 仓库根目录
}
if model.startswith("jev"): # 原系统默认名打到本地服务:按默认处理
model = ""
subfolder = MODEL_MAP.get(model, DEFAULT_CKPT)
if subfolder == "typed-decisions" and not (MODELS_ROOT / "typed-decisions").is_dir():
subfolder = None # 权重不齐时回退 english
Laya 的输入由 common.py: build_sequence 组装:题干与所有选项共享同一个固定预算 head_max_len(typed-decisions 为 256,english 为 192)。本项目单次决策有 9–40 个候选落点,正处在高危区。
关键澄清(本会话实测修正的一处常见误判):超预算时它并不是直接失败,而是分三层静默降级——① 等比例压缩每个选项的 token;② 把剩余预算给题干(题干最短被压到 8 token);③ 实在塞不下才抛 ValueError: options exceed head_max_len。最危险的是前两层没有任何日志:实测题干被从 190 token 悄悄压到 22 token、6 条评分标准全部丢失,而调用方看上去一切「正常成功」。
# laya/common.py: build_sequence —— 超预算时的静默压缩
opt_budget = head_max_len - sum(len(o) for o in opt_ids)
if opt_budget < 16:
per = max(4, (head_max_len - 16) // max(1, len(opt_ids)))
opt_ids = [o[:per] for o in opt_ids] # ① 选项等比截断
head_ids = head_ids[: max(8, opt_budget)] # ② 剩下的才给题干,最短 8 token
# ③ 仍放不下才抛 ValueError——这才是需要捕获的那种
所以适配层的处置有两部分:一是防御,捕获真正的溢出错误并降级重试;二是主动诊断,不要等它抛错——因为静默截断永远不会抛。
try:
result = agent.predict(state, questions)
except ValueError as e:
if "exceed head_max_len" in str(e):
result = agent.predict(state, shrink_questions(questions)) # 截短后重试一次
print("[laya-server] degraded: criteria shortened", flush=True) # 降级要留痕
else:
raise
usage.output_tokens 恒为 0——非自回归模型没有生成过程,token 统计只剩输入侧含义。文档里要写清楚,否则会被误读为「模型没输出」。{"detail": {"message": "..."}},调用方的错误解析分支同样不用改。兼容不只是成功路径,错误路径也得对齐。一次性投入一个约 200 行的适配服务,换来的是:调用方(页面 + 评估工具)零改动、双后端随时切换、以及把「模型差异」全部收敛在一个文件里。这比在每个调用点打补丁便宜得多,也更容易在模型换代时整体替换。
项目原有架构就是「零依赖」(Node 代理用裸 node:http)。为了保持一致、也为了少一层安装麻烦,本地推理服务用 Python 标准库的 ThreadingHTTPServer 实现,不引入 Web 框架。整个服务约 200 行,同时承担三件事:
| 路由 | 职责 |
|---|---|
GET /v1/health | 后端探测端点:返回 {"provider":"laya", …},让调用方自动识别「这是本地 Laya 而不是云端 Jev」,进而免去 API Key、显示对应的状态标签 |
POST /v1/systemone | 协议兼容层:适配请求 → 推理 → 适配响应 |
GET /* | 静态托管页面(路径穿越防护与 Node 代理同规则) |
MPS(以及 CUDA)上的模型推理不是线程安全的,而 ThreadingHTTPServer 天然多线程。必须用一把全局锁把推理段串行化,否则并发请求会得到错乱结果或直接崩溃:
_infer_lock = threading.Lock() # MPS 推理非线程安全,串行化
with _infer_lock:
result = agent.predict(state, questions)
本项目每块一次调用(约 386ms),单用户场景下串行化没有实际吞吐损失;若将来要并发,应改成请求队列 + 单推理线程,而不是去掉锁。
每个 checkpoint 约 1.7GB 内存(fp32)。服务启动时不加载模型,第一次真正用到某个 checkpoint 才加载并缓存——这样「启动即用」且切换 checkpoint 时只付一次代价:
_agents = {} # subfolder(str or "root") -> Agent
def get_agent(subfolder):
key = subfolder or "root"
with _agent_lock:
if key not in _agents:
print(f"[laya-server] loading checkpoint: {key} ...", flush=True)
_agents[key] = laya.load(str(MODELS_ROOT), subfolder=subfolder)
return _agents[key]
加载耗时约 24 秒,所以「第一个请求慢」是正常现象——日志里必须把加载过程打出来,否则会被误判为卡死。
除模型加载外,首次前向还包含图编译/缓存构建开销(实测首次 312ms vs 热身 72ms)。生产用法建议服务启动后主动打一发空转请求,把首帧成本挤到用户操作之前。同理,页面探测 /v1/health 时不触发加载——探测要保持轻量。
| 环节 | 数值 | 说明 |
|---|---|---|
| 权重加载 | 约 24s | 842MB,惰性加载、进程内缓存 |
| 首次前向 | 约 312ms | 含图编译/缓存构建 |
| 热身单次推理(离线) | 约 72ms | 3 个问题(choice+score+noul)一次前向 |
| 首次 HTTP 请求 | 35.7s | 含加载(这个数字不是推理慢) |
| 热身 HTTP 全链路 | 约 386ms | 真实 9 候选落点 + 3 个投机问题 |
| 对照:Jev 云端 | 240–400ms | 同量级——本地化并未牺牲实时性 |
| 单次请求 token | 1648 | 多问题共享 state,摊到每个问题上并不大 |
laya-server.py 当守护进程跑,不要每次决策重启。Laya 对 MPS 有一条内建策略:单行小 batch 不用 fp16(反而更慢),batch 变大后自动启用混合精度。因此批量提问比逐个提问更快,且不要手动干预精度设置。
# 1. 起服务(零依赖,标准库 HTTP)
<venv-python> laya-server.py 8788
# 2. 探测后端(注意 --noproxy,否则被本机代理拦截)
curl -s --noproxy "*" http://localhost:8788/v1/health
# {"provider": "laya", "checkpoints": ["english", "typed-decisions"], ...}
# 3. 真实决策请求(用业务代码构造的 payload,不要手搓假数据)
curl -s --noproxy "*" -X POST http://localhost:8788/v1/systemone \
-H "Content-Type: application/json" --data @/tmp/real-request.json \
-w "http:%{http_code} time:%{time_total}s"
不要手写假 payload。本次验证用的是「用引擎模块真跑几块棋局,再调用它自己的 buildState / buildQuestions 导出请求」——这样一旦验证通过,就等于验证了调用方的全部真实字段组合(包括嵌套 dict、对象型 instructions、多候选 criteria)。手搓的假数据测不出这类问题。
服务通了不代表集成通了。把批量评估工具的地址指向本地后端跑一局,是成本最低的全链路冒烟(本项目评估工具新增 --url 参数即可支持):
node eval/harness.mjs --player jev --url http://localhost:8788 \
--model laya-typed --games 1 --max-pieces 30
# game 1/1 seed 0 lines 0 pieces 22 ← 链路通;棋力另论(见第 7 节)
适配层的「静默降级」是排障噩梦。凡发生降级(如 criteria 截短重试)必须在服务日志打印一行醒目标记,否则你无法分辨「模型判断差」与「输入被悄悄截断了」:
[laya-server] degraded: criteria shortened after head_max_len overflow
provider: laya),调用方据此调整 UI 与认证要求——这是「可发现的后端」比「硬编码配置」更好的地方。backend 与 model 字段,日后做 A/B 对照分析时可以直接切片。本地 Laya 接上后,第一个让人意外的现象不是延迟,而是棋力:同环境 Jev 能打到几万分,Laya 只有几百分。这不是「慢一点」,而是「几乎随机」。在对它做任何优化之前,必须先分清一件事:是模型不会,还是我们没把信息喂对?
首次冒烟:本地 Laya 22 块内 top-out、0 消行;落点选择的最优概率只有约 0.17(而云端 Jev 在同一场景通常 0.76–0.90),概率分布明显更「平」。
官方文档其实早已写明前提:基座模型在类型化决策基准上接近随机(约 0.35),需要在自己的数据分布上微调才能到 0.766。但「官方说弱」不是结论——得实测确认弱在哪里,以及是不是我们自己喂错了。
要区分这两件事,关键是构造一个信息绝对充足的场景:如果在这种场景下模型依然无法区分,那问题就与适配无关。固定同一个中局盘面(seed 42,先落 5 块,第 6 块决策),挑出启发式最高与最低的两个落点,只把这两个给它选:
启发式最优 p2 -7.64
启发式最差 p8 -16.29
分差 8.65 ≈ 11 行的价值(Dellacherie 尺度:消一行仅 +0.76)
三组变体逐步放宽信息供给,判据是归一化熵:1.0 = 输出完全均匀(模型没有任何信息),0 = 完全确定。9 选项时均匀 top = 0.111,2 选项时均匀 top = 0.5——所以 C 变体的关键判据就是 top 是否显著高于 0.5。
| 变体 | 候选 | 题干入模型 | 选项入模型 | 选择 | top | 熵 |
|---|---|---|---|---|---|---|
| A 现状(线上原样) | 9 | 190 → 22 | 82 → 26 | p1 | 0.172 | 0.982 |
| B 极简(消除截断) | 9 | 190 → 39 | 23 完整 | p2 | 0.162 | 0.967 |
| C 判别力(最优 vs 最差) | 2 | 190 完整 | 22 完整 | p8 | 0.541 | 0.995 |
| C(english checkpoint) | 2 | 190 → 145 | 22 完整 | p2 | 0.507 | 1.000 |
当模型表现差时,不要笼统归因「模型弱」。构造一个「闭着眼睛都该选对」的最小场景——只给两个候选、把对标值的差距拉到最大、确保题干与选项都完整进入模型——再观察它的输出分布。
如果它在这种场景下依然是 top≈0.5、熵≈1.0,就证明问题在模型能力,不在适配层;反之如果它选对了,问题就在我们这边(信息被截断、表示不匹配、口径不一致)。这一招比逐项排查快得多,也给出了「该不该投入微调」的硬判据。
C 变体是全部证据里最硬的一条。它给足了信息:题干 190 token 完整进入,两个选项各 22 token 完整进入,没有任何截断;候选只有两个,差距悬殊到「肉眼可辨」。
结果:typed checkpoint 选走了最差的 p8,熵 0.995 已近乎完全均匀;english checkpoint 熵直接等于 1.000,即输出分布与均匀分布不可区分——它命中 p2 纯属二选一的运气。换句话说:把它放在一个「闭着眼睛都该选对」的局面里,它依然在抛硬币。这就是「模型没有棋力」的判定。
Laya 是 ModernBERT-large(421M)+ 两层 head 的非自回归判别模型,其 typed-decisions 检查点微调自通用 typed-decisions 基准——不含俄罗斯方块,也不含本项目的 state 词化。
关键认知(与第 1 节呼应):判别式模型没有推理能力可以「借」。它不像大语言模型能从常识推出「消行是好事、造洞是坏事」;它的全部判断力来自训练分布里见过的模式。一旦把它放进训练分布之外的状态表示(JSON 数组盘面、富结构候选),它给出的就是接近均匀的分数——这正是熵 0.99+ 的含义。
反面证据:连只有 4 个选项、文本完整、语义直白的 strategy 问题("The stack is low, flat, and has no holes…"),熵也高达 0.998;二选一的 next_piece_fits 给出 0.5228,约等于抛硬币。整个模型在这套输入上没有可用的判别信号。
另一个必须知道的机制:Laya 的 choice 答案是纯 argmax(agent.py: _decode_answers),没有任何采样或后处理。所以「分布平」不只是「模型犹豫一下」——当 9 个选项的概率都落在 0.11(均匀值)附近时,它实际上是在选项编号之间按微小噪声取最大值,与随机落点没有实质区别,宏观表现就是几百分。分布有多平,决策质量就有多差,没有采样补救的余地。
A 变体正命中第 3 节说的三层静默降级(源码 common.py: build_sequence):题干从 190 token 被压到 22(6 条评分标准全部丢失,连问句本身都没读完),每个候选从 82 token 被压到 26(只剩 {"where": …, "lines_cleared": …, "holes_created —— 连 holes_created 的值都没进来,height_change / stack_height_after / surface_after / wells_after 全丢)。而 state(盘面本身)259 token 完整没被截断。
但 B 变体完整修掉了截断,熵只从 0.982 降到 0.967,几乎没有改善。所以它是真实的缺陷,却不是主因——这也直接回答了那句最容易脱口而出的判断:「那把 prompt 压小是不是就好了?」不是。
顺带记住官方的另一条硬边界:选项超过 20 个时准确率显著退化——本质还是 head 预算摊薄到每个候选只剩 3–4 个 token。官方给出的解法是由粗到细两级决策(先粗选区域,再在类内细选落点),对本项目落点动辄 9–40 个的现状正对症,也是一条独立于「微调」的下游优化路线。
为了让结论站得住,以下每一项都做了验证,确认不是原因:
| 假设 | 验证方式 | 结果 |
|---|---|---|
| 请求 / 响应协议没对齐 | 用 engine.js 构造真实请求打本地服务,逐字段核对页面消费项 | 字段全兼容 |
| state 被截断,模型看不到盘面 | 实测 state token 数与可用 room | 259 / 764,完整 |
| 选项 key 映射错,答非所问 | 核对 probabilities 的键与 criteria 的键 | 一一对应 |
| 选错了 checkpoint | typed-decisions 与 english 各跑一遍完整对照 | 两个都无判别力 |
| instructions 展平方式引入失真 | C 变体中题干完整进入模型,仍无判别力 | 不是主因 |
| 候选枚举有 bug,漏掉好落点 | 9 个候选与启发式枚举结果一致 | 枚举正常 |
| 推理没跑在 MPS 上 / 权重加载不全 | 加载日志与一次性 72ms 前向 | 正常 |
| 服务层降级逻辑掩盖了错误 | 日志中无 head_max_len 溢出降级触发 | 未触发 |
同样的协议、同样的 state、同样的 criteria,Jev 能得几万分——说明这套协议没问题,问题出在协议两端模型的能力与训练分布上。一句话:Laya 复刻的是协议,不是能力。
更准确地说:本项目的 prompt 形态是按 Jev 这类服务端大模型设计的,它的信息量(900+ token 的题干与候选)从根本上超出了 421M 本地模型 256 token 的判别预算。要本地化,就得让模型重新学这套表示,而不是指望它开箱理解。
| 路径 | 做法 | 成本 | 预期天花板 |
|---|---|---|---|
| ① 微调 | 用 harness 批量产出的 (state, 候选, 结果) 日志构造训练集,以启发式或事后最优落点作标签做 SFT。官方有 T4 四小时模板 | 数千局数据 + 一次训练 | 可接近启发式基线(193.9 行) |
| ② 适配优化 | 压缩题干与候选文案(B 变体方案),让两者都落进 256 token;或减少候选数量 | 半天 | 实测熵 0.982→0.967,收益有限 |
| ③ 重新定位 | 承认 Laya 是「本地研究对照基线」而非 Jev 的替代:免费、离线、72ms、无 key。主力仍走 Jev | 零 | 不影响玩法,保住双后端研究价值 |
另外有一条与棋力正交、但很划算的改进:温度校准。官方称在自有分布上拟合一个标量温度,可把校准误差从 0.466 降到 0.081——置信度变得可用之后,才能做「高置信自动落子 / 低置信回退」这类策略。它与「模型会不会下棋」是两件事,可以独立推进。
先做一次低成本验证:把 state 从 JSON 数组改写成更口语化的散文(或 ASCII 盘面图),只跑 C 判别力测试。若熵仍逼近 1.0,「表示形式」这条线也可排除,直接决定是否投入微调。
再决定是否微调:微调需要把启发式当训练标签,会触及 ADR 0001「决策路径禁止代码估值」 的边界。判断是——训练期用标签 ≠ 运行时把估值喂进决策路径,属 imitation learning 的常规做法,但应当在 ADR 里显式记一条,而不是悄悄做。
顺便修掉截断:无论走哪条路,让选项文本落进 head 预算都是对的,否则将来微调出的模型也会被同一个坑拖累。
开箱棋力弱不是本地部署的缺点,而是它的入场券:你获得了「可以把它调成自己领域专家」的能力。云端 API 只给你一个固定模型,本版权重给你的是微调、校准、蒸馏的自由,以及数据零出域的合规性。评估这笔投入时,应该对比「微调后的本地模型」与「云端模型」,而不是「开箱本地模型」与「云端模型」。
laya.load() 可直接吃本地路径| 症状 | 原因 | 处置 |
|---|---|---|
ProxyError / 连不上 huggingface.co | 网络受限,HF 与 hf-mirror 均不通 | 改用 ModelScope 镜像逐文件下载 |
laya.load() 长时间无输出、不报错 | transformers 探测 TensorFlow 死锁 | 设 USE_TF=0 |
curl localhost 报 upstream connect failed | 本机代理转发拦截了 localhost | 加 --noproxy "*"(Node fetch 不受影响) |
| 首个请求耗时几十秒 | 模型加载 + 图编译 | 正常现象;预热请求可消除 |
options exceed head_max_len | 候选过多 / 描述过长,超出 head 预算(typed 256 / english 192) | 适配层截短重试;根治方案是两级决策 |
| 置信度看起来异常低(0.0x) | 取到了校准置信而非 top 概率 | 按需映射 answer_confidence;校准值另存 |
| token 统计只有输入没有输出 | 非自回归模型无生成过程,output_tokens 恒 0 | 预期行为,文档中说明即可 |
| 题干 / 候选「明明给了」模型却像没用 | build_sequence 三层静默降级:超预算时无日志地截断 | 用预算诊断脚本核对实际入模型的 token 数,主动压缩文案 |
| 棋力接近随机(只得几百分) | 判别式模型缺语义先验(主因);head 预算截断(次因) | 先跑判别力实验确认是模型问题,再走微调 / 重新定位 |
| 文件 | 职责 |
|---|---|
laya-server.py | 本地推理服务:静态托管 + 后端探测 + /v1/systemone 协议兼容层 |
eval/laya_smoke.py | 部署冒烟:加载 + 三类问题 + 延迟量级 |
eval/harness.mjs | 批量对局评估,--url 指定后端(云端 / 本地) |
eval/laya_prompt_diag.py | prompt 预算诊断:题干与候选实际入模型的 token 数 |
eval/laya_abc_diag.py | 判别力对照实验:A/B/C 三变体 × 两种 checkpoint 的熵对比 |
docs/laya-why-weak.html | 棋力根因诊断报告(本文第 7 节的完整版,含逐项证据) |
index.html | 页面:启动时探测后端,自动调整 Key 要求与状态显示 |
docs/jev-development-guide.html