教程 · 2026-09-25
用本地 Laya 替代 Jev:部署、切换与一次真实对比
在 常驻 Agent Worker 那篇里,Agent 的结论要再经过 ctx.jev.judge() 过一遍:问一句「候选结果与输入证据不符吗」,拿回一个 0 到 1 的概率。背后是 TypeSafe 托管的 Jev,需要一个 TypeSafe Key,每次判断都要往云端走一趟。
大多数时候这没问题。但下面几种情况,你会希望判断在本机完成:
- 数据不能出机器:事件里有客户信息,哪怕只是做一次是非判断,也不希望发到第三方。
- 量大:每条消息都要判断,调用次数跟着消息量线性增长。
- 离线或内网:Worker 所在的机器根本连不上外网。
- 延迟:一次判断走一次公网,和在本机算一次,差着一到两个数量级。
Laya 是一个开源的类型化判断模型,Apache-2.0 许可。它最重要的一点是:HTTP 接口和 Jev 一样,都是 POST /v1/systemone,请求和响应的形状相同。所以对 dsh-agent-kit 来说,替换它不需要新的 Service,只需要让 ctx.jev 把请求发到另一个地址。
从这个版本开始,Jev 行多了一个 provider 配置,typesafe(默认)和 laya 二选一。业务代码一行不用改。
第一步:把 Laya 跑起来
有两种方式,按机器选。
方式一:官方 laya-serve
官方包自带 HTTP 服务,基于 PyTorch,适合有 CUDA 的 Linux 服务器:
pip install "laya[serve]"
LAYA_API_KEY=<随机串> LAYA_DEVICE=cuda LAYA_PRELOAD=1 laya-serve按官方说明,它通过环境变量配置:LAYA_HOST、LAYA_PORT、LAYA_DEVICE、LAYA_PRELOAD、LAYA_MODELS,以及 LAYA_API_KEY。
方式二:Apple Silicon 上用 laya-mlx
在 Mac 上,社区移植的 laya-mlx(pip install laya-mlx)用 Apple 的 MLX 跑 Laya,比走 PyTorch 轻得多。它目前只提供命令行(predict / convert),没有 HTTP 服务,不过自己包一层只要二十来行:
# ~/.local/share/laya-mlx/serve.py
import hmac, os, threading
from pathlib import Path
from fastapi import FastAPI, HTTPException, Request
from laya_mlx import Agent
MODEL = os.environ.get("LAYA_MODEL", "aac6fef/laya-multilingual-mlx")
KEY = Path(os.environ["LAYA_API_KEY_FILE"]).read_text().strip()
agent, lock = Agent(MODEL), threading.Lock()
app = FastAPI(docs_url=None, redoc_url=None, openapi_url=None)
@app.post("/v1/systemone")
async def systemone(req: Request):
if not hmac.compare_digest(req.headers.get("authorization", ""), f"Bearer {KEY}"):
raise HTTPException(401, "unauthorized")
body = await req.json()
with lock: # MLX 一次只跑一个请求
return agent.predict(body.get("state"), body["questions"])用 uvicorn 起在本机端口上:
uvicorn serve:app --app-dir ~/.local/share/laya-mlx --host 127.0.0.1 --port 18765想让它开机自启、挂了自动拉起,写一个 launchd 配置 ~/Library/LaunchAgents/ai.laya.mlx-serve.plist,关键是这几项:
<key>ProgramArguments</key>
<array>
<string>/Users/you/.local/share/laya-mlx/venv/bin/uvicorn</string>
<string>serve:app</string>
<string>--app-dir</string><string>/Users/you/.local/share/laya-mlx</string>
<string>--host</string><string>127.0.0.1</string>
<string>--port</string><string>18765</string>
</array>
<key>EnvironmentVariables</key>
<dict>
<key>HF_HUB_OFFLINE</key><string>1</string>
<key>LAYA_API_KEY_FILE</key><string>/Users/you/.local/share/laya-mlx/api-key</string>
</dict>
<key>RunAtLoad</key><true/>
<key>KeepAlive</key><true/>然后 launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/ai.laya.mlx-serve.plist。我们杀掉进程试过,5 秒内会被重新拉起。
几个细节:
- Key 放文件,不放 plist。plist 通常谁都能读,Key 文件用
chmod 600收紧。 HF_HUB_OFFLINE=1:模型下载好以后,启动时不再联网检查更新,离线也能起来。- 只绑
127.0.0.1:Worker 和 Laya 在同一台机器上,没必要对外开端口。
第二步:把 ctx.jev 指过去
在 Profile 的 cordis.patch.yml 里,把 Jev 行改成:
- id: agent-kit-jev
disabled: false
config:
provider: laya
baseURL: http://127.0.0.1:18765
# apiKeyRef: LAYA_API_KEY # 默认就是它再把 Laya 的 Key 存成 LAYA_API_KEY:设成环境变量,或者存进 dsh 凭据文件。用 admin 包的话,设置页的 Jev 卡片里选「本地 Laya」,填上地址,下面会出现 Laya Key 的输入框;npx @mc/dsh-agent-kit-admin setup 也会一步步问。
业务代码保持原样:
const { answers } = await ctx.jev.judge({
state: { input: frame.input, candidate: draft.output },
questions: {
wrong: noul('候选结果与输入证据不符', { true: '不符', false: '相符' }),
},
signal,
traceId: frame.id,
})这一步有几处特意做的保护,值得说一下:
- 选了
laya,就不碰 TypeSafe Key。Service 不会去读环境变量、钥匙串或凭据文件里的 TypeSafe Key,更不会把它放进发往 Laya 的请求头。Laya 用自己的 Key,取不到就不带鉴权。 - 选了
typesafe,baseURL会被忽略,并记一条警告。这样就不会因为配置里残留了一个地址,把 TypeSafe Key 发到别的主机上。 - 错误信息说明是谁出的错:
Laya API returned 503、Laya API rejected the API key。Key 不对时,health 同样变成failed,并触发agent-kit/service-failed,监控不用改。 doctor换一套检查:不再要求 TypeSafe Key,而是检查 Laya Key 是否配置(没配只提示),以及baseURL能不能连上(连不上直接失败,并告诉你该启动什么)。
实测一:延迟
测试环境是一台 Apple M5,用上面的 laya-mlx 服务加多语言模型,走 ctx.jev 同样的 HTTP 路径。每次请求带三道题:一道是非题、一道三选一、一道四档评分。
| 指标 | 数值 |
|---|---|
| 延迟 p50 / p90 / p99(200 次,暖机后) | 13.9 / 14.8 / 15.5 ms |
| 服务启动后的第一次请求 | 约 66 ms |
| 服务进程常驻内存 | 约 1 GB |
| 模型文件 | 约 1.3 GB |
同一台机器上,Jev 的 p50 约 260 ms,其中大部分是网络往返。只看速度,Laya 快一个数量级以上。
实测二:拿一个真实任务对比
速度不能说明判断对不对。所以我们拿一个线上在用 Jev 的真实任务做了对比:给定一个域名,判断它属于 16 个业务场景中的哪一个,比如电商、社媒、广告、基础设施。线上的用法有两步:Agent 先给出分类,再让 Jev 回答两个问题——「只看域名,你会选哪一类」「候选类别与域名证据不符吗」。
两组数据:
- 文档示例:分类口径文档里每个类别列出的示例域名,共 303 个,比如 amazon.com、graph.facebook.com、doubleclick.net。测哪个域名,就先把它从口径里删掉(留一法),避免模型直接在题面里查到答案。
- 真实流量:从线上流量里取的 100 个域名。其中 89 个在口径下有明确的类别,用来算准确率;另外 11 个本身就有争议,不计入准确率。这组以各大平台的 CDN 子域名和基础设施域名为主,比文档示例难得多。
每组都用两种口径提问:
- 生产口径:和线上完全一样,state 里带上 16 个类别的完整判定口径,约 5000 token。
- 精简口径:state 只放域名,选项只用类别名,不到 700 token。
选类准确率(16 选 1,随机猜约 6%):
| 文档示例(303) | 真实流量(89) | |
|---|---|---|
| Jev,生产口径 | 93.4% | 86.5% |
| Jev,精简口径 | 84.5% | 59.6% |
| Laya,生产口径 | 7.6% | 0% |
| Laya,精简口径 | 9.9% | 0% |
| Laya,换成英文选项与说明后最好的一组 | 22.1% | 10.1% |
核对题「候选类别与域名证据不符吗」:每个域名各给一个正确候选和一个错误候选,以线上阈值 0.7 为界。
| AUC | 正确候选被误拒 | 错误候选被拦下 | |
|---|---|---|---|
| Jev,生产口径(文档 / 流量) | 0.998 / 0.998 | 3.0% / 1.1% | 99.0% / 100% |
| Laya,生产口径 | 0.500 | 100% | 100% |
| Laya,精简口径(文档 / 流量) | 0.53 / 0.61 | 55% / 84% | 58% / 92% |
在这个任务上,结论没有悬念:Laya 替代不了 Jev。 原因有两个,都值得单独说。
第一,state 超出窗口会被静默截断。 Laya 多语言模型的窗口是 1024 token,choice 所有选项合计只有 256 token 的预算。生产口径的 state 约 5000 token,而域名和候选答案放在 16 个类别的口径之后,被整段截掉了,服务端却不报错。结果就是 Laya 根本没看到要判断的对象:选类时几乎全部落在排第一的类别上,核对题里正确和错误候选的分数完全相同,AUC 正好 0.5。
有一个简单的信号可以发现这种情况:judge() 返回的 usage.input_tokens 如果正好等于窗口上限(这里是 1024),就说明输入被截断了。
那把窗口调大行不行?编码器支持 8192 token,我们把 max_len 改成 8192,让完整的 state 能放进去(约 4400 token),再跑一遍生产口径:
| Laya,生产口径 | 1024 窗口 | 8192 窗口 |
|---|---|---|
| 选类准确率(文档 / 流量) | 7.6% / 0% | 5.3% / 0% |
| 核对题 AUC(文档 / 流量) | 0.500 / 0.500 | 0.525 / 0.568 |
| 延迟 p50 | 50 ms | 238 ms |
输入不再被截断了,判断却没有变好:选类从「全选第一类」变成了「几乎全选市场调研」,核对题仍接近随机,延迟反而变成原来的 5 倍。模型只在 1024 长度上训练过,调大窗口不等于学会处理长输入;而且下面这个原因,窗口再大也解决不了。
第二,这个任务靠的是常识,而不是题面里的证据。 就算把 state 缩到只剩域名、选项换成英文并附上说明,Laya 最好也只有 22%。只保留 5 个类别时,真实流量组的准确率还低于随机。从一个域名看出「这是哪家公司、做什么业务」,需要模型本身知道这些公司。一个几亿参数的编码器模型做不到,这在它的设计范围之外。
Jev 也不是没有短板。真实流量组里它错了 12 个:8 个是把谷歌的下载 CDN 当成了视频流媒体,3 个是没认出两个国内社交平台的 CDN。对比两种口径还能看出,完整的判定口径让准确率在真实流量上提高了 27 个百分点。所以对 Jev 来说,把易错的域名写进口径是最直接的改进。
什么时候用哪个
Laya 擅长的是证据就在题面里的判断:一段工单文本是不是在说账单问题,一条回复是否礼貌,候选摘要和原文是否一致。这类判断 state 短、选项少,它又快又便宜,还不出机器。
需要外部常识、长上下文或很多选项的判断,比如本文的域名分类,目前应该留给 Jev。
| 场景 | 建议 |
|---|---|
| 证据在文本里、state 短、选项少 | 可以考虑 Laya,先用题集验证 |
| 需要常识(公司、品牌、域名)或 state 很长 | Jev |
| 数据不能出机器,但任务又需要常识 | Laya 不是答案,考虑本地部署更大的模型 |
| 判断结果直接触发不可撤销的动作 | 没有对比数据前,继续用 Jev |
切换本身只改配置:provider: laya 加一个 baseURL。但切换前一定要用自己的任务跑一遍对比。本文的任务在 Jev 上是 86%,在 Laya 上是 0%,接口一样并不代表判断能力一样。