很多团队接大模型时都撞过同一堵墙:同一条 prompt,今天判对,明天判错,而程序里写死的阈值对此毫无察觉。路由逻辑要么全信模型、要么全不信,中间那片模糊地带只能靠人肉复核,量一大就崩。Jev 提供的是一条更细的线:每次调用都返回一个 0~100 的 score,路由决策围绕这条线展开,而不是围绕「模型说行就行」。
常规做法为什么不够用?固定阈值看起来省事,实际是把一次性的拍脑袋固化成生产规则:数据分布一漂移、模型一换版本,70 分的含义就变了,而代码里的 if score > 70 还在忠实地执行过期契约。真要改,就得改代码、发版、回滚,代价远大于当初省下的那点工夫。
本文分享一套可直接落地的方案:申请 Key + 封装裁判函数 + 置信度分层路由 + TypeSafe 类型契约,从裸 HTTP 调用一路接到 Agent 循环里,附阈值标定方法、超时降级策略和上线检查清单。
一、Jev 是什么:一次调用换一个可路由的分数
先定位 Jev 在架构里的层级:它不是模型,而是一层可复用的判断服务:
- 纯 HTTP 契约:一次
POST换一份 JSON,不依赖任何 SDK,任何语言都能接。 - 可路由的输出:返回体里
success表示判断是否成功,score是 0~100 的置信分,两者必须分开消费。 - 独立于业务:打分逻辑与你的业务代码解耦,阈值调整不用改主流程。
- TypeSafe 落点:分数最终被翻译成有限的几个类型(通过 / 修正 / 复核),这是路由的真正依据。
问题:阈值散落在十几个 if-else 里,改一次要全仓搜索。
治理:分数入口只有一处,阈值收敛到一个配置对象,改一行全生效。
核心结论:Jev 卖的不是「对错」,而是一个可以写进路由的数,先拿到这个数,后面所有分层策略才有地基。
二、申请 API Key:从注册到跑通第一次调用
拿到凭据分四步走,全程在控制台完成:
- 注册登录:打开 Jev 官网入口,完成账号注册并登录控制台。
- 生成 Key:进入
user/generateapikey页面点击生成,Key 只在生成时完整显示一次。 - 立即存档:把 Key 写进环境变量或密钥管理服务,不要提交进 Git。
- 冒烟测试:用最短的 curl 验证 Key 有效,再开始写业务代码。
下面这段在 macOS/Linux 的终端里执行,用一行 curl 打一次最小请求验证凭据:
export JEV_API_KEY="你的Key"
curl -sS -X POST "https://api.jev.example/chat/api/judgment/get" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $JEV_API_KEY" \
-d '{"prompt":"请总结这段话的核心观点","judgment":"总结准确"}'
强约束凭据 → 最小请求 → 拿到 score 再谈业务。
常见坑是把 Key 直接硬编码进前端或开源仓库,一旦推送就只能作废重发,所以第一原则是 Key 只活在服务端环境变量里。
**核心结论:**冒烟能通就说明 Key、域名、请求头三件套都对了,后面所有问题都只剩业务层。
三、请求格式:三个字段决定成败
先把请求与响应的字段约定钉死,后面封装才有依据:
| 字段 | 位置 | 类型 | 说明 |
|---|---|---|---|
prompt | 请求体 | string | 待判断的原始内容,Jev 真正打分的对象 |
judgment | 请求体 | string | 你期望的判断标准,决定分数的语义 |
Authorization | 请求头 | string | Bearer <Key>,缺失直接 401 |
success | 响应体 | boolean | 判断是否成功执行,不等于判断结论为真 |
score | 响应体 | number | 0~100 置信分,路由的唯一输入 |
下面这段 Python 用 requests 打一次调用并把关键字段拆出来,适配 Python 3.9+ 环境:
import os, requests
JUDGE_URL = "https://api.jev.example/chat/api/judgment/get"
def judge(prompt: str, judgment: str, timeout: float = 5.0) -> dict:
resp = requests.post(
JUDGE_URL,
headers={"Authorization": f"Bearer {os.environ['JEV_API_KEY']}"},
json={"prompt": prompt, "judgment": judgment},
timeout=timeout,
)
resp.raise_for_status()
data = resp.json()
if not data.get("success"):
raise RuntimeError(f"judgment failed: {data}")
return {"score": int(data["score"]), "raw": data}
if __name__ == "__main__":
print(judge("天空为什么是蓝色的", "解释瑞利散射且结论正确"))
success 管调用、score 管路由,两者混用会把服务故障当成低分。
注意 judgment 写得越具体,分数越可比:写「准确」和写「包含关键数据且无编造」得到的 80 分完全不是一回事,阈值标定的前提是判断标准先固定下来。
**核心结论:**字段少不代表可以随手写,判断标准文本就是分数的标尺,标尺一换,历史阈值全部作废。
四、封装裁判函数:把 HTTP 细节收敛成一次调用
业务代码不该关心超时、状态码和 JSON 解析,所以要收口:
- 单一入口:全项目只保留一个
judge(),将来换域名、加请求头只改这一处。 - 显式超时:
timeout必须传,默认 5 秒,绝不让请求无限挂着。 - 失败即异常:
success为false时抛异常,而不是返回一个假的 0 分污染统计。 - 结果归一:返回
int(score),避免上游拿到float后比较逻辑出现精度意外。
下面这段在上一节基础上补齐重试与结构化日志,适配生产环境直接复用:
import time, logging
from tenacity import retry, stop_after_attempt, wait_exponential
log = logging.getLogger("jev")
@retry(stop=stop_after_attempt(3), wait=wait_exponential(min=0.2, max=2))
def judge(prompt: str, judgment: str, timeout: float = 5.0) -> dict:
resp = requests.post(
JUDGE_URL,
headers={"Authorization": f"Bearer {os.environ['JEV_API_KEY']}"},
json={"prompt": prompt, "judgment": judgment},
timeout=timeout,
)
resp.raise_for_status()
data = resp.json()
if not data.get("success"):
raise RuntimeError(f"judgment failed: {data}")
score = int(data["score"])
log.info("judge score=%s judgment=%s", score, judgment[:40])
return {"score": score, "raw": data}
收口 + 指数退避重试 + 结构化日志,三件事一次做齐。
日志里记 judgment 前 40 字很关键,日后做阈值复盘时,你能凭日志还原当时用的是哪把标尺。
核心结论:裁判函数是整个链路的单一事实来源,路由写得再花哨,也要从这一个出口拿分数。
五、阈值标定:用真实分布定三条线
阈值不是拍出来的,是量出来的,先看三档路由的含义:
| 分数区间 | 路由目标 | 典型动作 | 成本 |
|---|---|---|---|
| 90 ~ 100 | TypeSafe 模型 | 直接结构化输出,进入下游 | 低 |
| 70 ~ 89 | LLM 修正 | 追加一轮改写后再判断 | 中 |
| 0 ~ 69 | 人工复核 | 挂起并推给值班同学 | 高 |
下面这段 Python 拿标注集跑一遍 Jev,统计分数分布并自动求出候选阈值,适配离线标定任务:
import numpy as np
def calibrate(samples: list[dict], target_pass: float = 0.9):
scores = np.array([judge(s["prompt"], s["judgment"])["score"] for s in samples])
labels = np.array([s["label"] for s in samples]) # 1=正确, 0=错误
# 选出让「通过组里真正确」的比例 ≥ target_pass 的最低阈值
best = None
for t in range(50, 101):
passed = labels[scores >= t]
if len(passed) == 0:
continue
precision = passed.mean()
if precision >= target_pass and (best is None or scores[scores >= t].mean() > best[1]):
best = (t, scores[scores >= t].mean())
return best # (阈值, 该组平均分)
print(calibrate(SAMPLES, target_pass=0.95))
问题:阈值 70 沿用了三年,没人知道它从哪来。 治理:每次模型换版都用 200 条标注样本重跑标定,阈值带版本号入库。
先保证 90 分 以上组的准确率达标,再反推阈值,而不是先定阈值再祈祷准确率——顺序反了,标定就没有意义。
核心结论:阈值必须可复算、可追溯、可版本化,否则它只是写进代码里的迷信。
六、两层兜底:修正与人工复核怎么接
低分不等于失败,它只是告诉你这条路不能直走:
- 第一层 LLM 修正:70~89 分段追加一轮改写,把模糊结论补成可验证陈述。
- 第二层人工复核:70 分以下直接挂起,宁可慢也不让低置信内容进生产。
- 修正后重判:改写完成必须重新调一次 Jev,用新分数决定去留,不许自我通过。
- 兜底上限:修正最多一次,避免无限循环烧钱。
下面这段 JS 用原生 fetch 实现分层路由,适配 Node 18+ 与浏览器同构环境:
export async function route(prompt, judgment) {
const first = await judge(prompt, judgment);
if (first.score >= 90) return { path: 'typesafe', payload: first };
if (first.score >= 70) {
const fixed = await rewrite(prompt, first); // 追加一轮改写
const second = await judge(fixed, judgment); // 改写后必须重判
if (second.score >= 90) return { path: 'typesafe', payload: second };
return { path: 'manual', payload: second, reason: 'correction_failed' };
}
return { path: 'manual', payload: first, reason: 'low_score' };
}
低分 → 修正 → 重判 → 仍低则挂起,四步构成完整兜底。
修正失败的分支必须显式带 reason,否则一周后你分不清是分数太低还是改写模型超时,排障成本翻倍。
核心结论:兜底的价值不在多聪明,而在每条路径都有终点,绝不允许内容卡在中间状态。
七、速度与失败:超时、重试与降级
判断服务也是服务,必须按故障件对待,先看延迟预算怎么排:
| 环节 | 预算 | 超时动作 | 降级策略 |
|---|---|---|---|
| Jev 打分 | 500ms ~ 5s | 指数退避重试 3 次 | 降级为保守人工路径 |
| LLM 修正 | 3 ~ 10s | 单次重试 | 直接挂起待复核 |
| TypeSafe 输出 | 1 ~ 3s | 快速失败 | 缓存上一次成功结果 |
| 端到端 | ≤ 15s | 中断并告警 | 返回降级响应体 |
下面这段 bash 用 curl 探测 Jev 的实际延迟分布,适配上线前的压测脚本:
#!/usr/bin/env bash
for i in $(seq 1 20); do
t=$(curl -o /dev/null -sS -w '%{time_total}' -X POST \
"https://api.jev.example/chat/api/judgment/get" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $JEV_API_KEY" \
-d '{"prompt":"latency probe","judgment":"有效回答"}')
echo "run $i total=${t}s"
sleep 0.2
done
测出 p95 延迟,才配给 timeout 赋值。
降级顺序要提前写死:打分服务不可用时,路由宁可全走人工复核,也不能默认高分放行——失败方向必须朝安全侧偏。
**核心结论:**超时值来自实测分位数,拍脑袋的 30 秒超时只会把故障掩盖成慢查询。
八、TypeSafe 模型:把分数翻译成有限类型
分数是连续的,路由需要离散的类型,这一步就是 TypeSafe 的用武之地:
- 枚举封顶:只有
PASS/CORRECT/REVIEW三种取值,新增路径必须改类型定义。 - 构造函数守门:类型只能由
score构造,外部无法直接 new 出一个越界结果。 - 穷尽检查:
switch必须never兜底,漏写分支时编译期就报错。 - 序列化固定:入库字段与枚举一一对应,历史数据永远可读。
下面这段 TypeScript 定义分数到类型的唯一映射,适配任意 TS 工程:
type Verdict =
| { kind: 'PASS'; score: 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 }
| { kind: 'CORRECT'; score: 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79
| 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 }
| { kind: 'REVIEW'; score: 0 };
export function toVerdict(raw: number): Verdict {
const score = Math.max(0, Math.min(100, Math.round(raw)));
if (score >= 90) return { kind: 'PASS', score: score as Verdict extends { kind: 'PASS' } ? never : never } as Verdict;
if (score >= 70) return { kind: 'CORRECT', score } as unknown as Verdict;
return { kind: 'REVIEW', score: 0 };
}
export function assertNever(x: never): never { throw new Error(`unhandled: ${x}`); }
分数裁剪 → 区间映射 → 类型收窄,越界值在入口就被吞掉。
真正的价值在下游:拿到 verdict.kind === 'PASS' 时,你不需要再判断分数,编译器已经替你排除了其他两种可能。
核心结论:TypeSafe 的本质是把运行期的分数不确定性,在类型层一次性关掉。
九、接入 Agent 循环:把判断嵌进真实工作流
Agent 里每次工具调用后都该有一次判断,否则错误会逐轮放大:
- 判断时机:工具返回结果之后、下一步规划之前,插入一次 Jev 调用。
- 按类型分流:
PASS直接进记忆,CORRECT触发改写,REVIEW中断循环并请求人工。 - 轮次上限:单个任务最多修正 2 次,超限直接转人工,防止自嗨循环。
- 全程留痕:每轮记录
score与verdict,回放时能精确定位从哪一步开始跑偏。
下面这段 Python 把判断嵌进最小 Agent 主循环,适配自研 Agent 或现有框架:
def run_agent(task: str, max_rounds: int = 5) -> str:
memory = []
for round_no in range(max_rounds):
result = call_model(task, memory)
verdict = to_verdict(judge(result, "结论可验证且无编造")["score"])
log_round(round_no, verdict)
if verdict["kind"] == "PASS":
memory.append(result)
continue
if verdict["kind"] == "CORRECT":
result = rewrite(result) # 改写后重新入池
memory.append(result)
continue
return escalate(task, memory) # REVIEW:交人
return escalate(task, memory) # 轮次耗尽同样交人
打分 → 分型 → 分流,三步插在每轮规划之前。
escalate 一定要连同 memory 一起交出去,否则值班同学接手时得从零复现整个上下文,兜底就变成了负担。
核心结论:判断不进循环,Agent 的错误只会逐轮复利,而每轮一次打分是成本最低的刹车。
十、上线检查清单:从测试到监控
最后一公里靠清单兜住,逐项打勾再放量:
| 检查项 | 验收标准 | 常见翻车点 |
|---|---|---|
| Key 存储 | 仅服务端环境变量,仓库有扫描 | 提交进前端代码 |
| 冒烟用例 | 生产同款域名跑通 20 次 | 用测试域名验收 |
| 阈值标定 | 有标注集与版本号 | 直接抄默认 70/90 |
| 超时重试 | p95 实测后再赋值 | 沿用 30 秒默认值 |
| 降级路径 | 打分挂掉时走人工复核 | 失败默认高分放行 |
| 监控埋点 | 分数分布、路由占比、失败率 | 只监控 HTTP 状态码 |
| 留痕回放 | 每轮 score 与 verdict 可查 | 只存最终结果 |
# 每日阈值漂移巡检:分数分布偏移超过 5 分即告警
curl -sS "$PROMETHEUS/api/v1/query" \
--data-urlencode 'query=histogram_quantile(0.95, sum(rate(jev_score_bucket[1h])) by (le))' \
| jq '.data.result[0].value[1]'
盯分布而不是盯单点,漂移早在用户投诉之前就能看见。
放量节奏建议三步:先 5% 流量观察一天路由占比,再 30% 观察修正触发率是否异常,最后全量并保留一键回滚到人工复核的开关。
**核心结论:**上线不是终点,分数分布的日常巡检才是这套路由能长期活着的原因。
结语
这篇指南从申请 Key 起步,把 Jev 的裸 HTTP 契约逐步封装成可复用的裁判函数,再用标定出来的三档阈值驱动 TypeSafe 类型路由,最后嵌进 Agent 主循环并配上完整的上线清单。整条链路没有一处依赖「模型说行就行」,每一次分支都有分数依据、每一次降级都有明确终点,出了问题也能凭留痕回放还原现场。
真正值得带走的方法论是:别让程序猜模型的意图,让分数说话,让类型封口。