第 5 章

第 5 章:Detection 与 Scoring——二元判断与有序评分

第 5 章:Detection 与 Scoring——二元判断与有序评分

Detection 问"有没有",Scoring 问"多少分"。前者是 Noul 的主场,难点全在阈值;后者是 Score 的主场,难点全在"别让模型直接给数字"。本章说清阈值背后的召回/精确取舍、score = Σ i·p_i 的含义、以及为什么概率校准比那个分数本身更重要。

Noul 的本质:一个带概率的"是/否"

Detection 的底层是 Noul。它不返回干巴巴的 true/false,而是返回一个 P(true):

from typesafe import TypesafeClient

client = TypesafeClient()

d = client.systemone(
    state=source_file,
    questions={
        "is_malicious": {"type": "noul", "question": "这段代码是否在试图执行未声明的网络外连或破坏性操作?"}
    },
    instructions="你是供应链安全审查员,只看代码行为,不猜意图。"
)

print(d["is_malicious"].value)        # True / False
print(d["is_malicious"].probability)  # 例如 0.91

初看之下这比"让模型回 yes/no"好不到哪去。真正拉开差距的是那个概率:它把"是不是"变成了一个连续量,而连续量可以设阈值、可以排序、可以分级动作。Detection 的一切设计,本质上都围绕"怎么用这个概率"。

Detection:阈值才是主角

Detection 的成败几乎不在模型,而在阈值。同一个 Noul,阈值定在 0.3 还是 0.8,得到的是两套完全不同的系统。

p = d["is_malicious"].probability
if p >= 0.3:
    block_and_review(source_file)   # 阈值低:宁可误报

阈值不是拍脑袋定的,它由两类错误的代价决定:

  • 漏判(false negative):明明是坏东西却放过了。代价可能是安全事故、资金损失。
  • 误判(false positive):明明是好的却拦下了。代价可能是正常业务流程被打断、用户被误伤。

阈值往低走,漏判减少但误判增多;往高走反之。所以正确的顺序是:先想清楚这两类错误各自要花多少代价,再倒推阈值——而不是先选一个"看起来合理"的 0.5。

召回 vs 精确:两类场景相反的直觉

这条取舍在不同场景里的最优解是相反的。

flowchart TD
    P["Noul 输出的 P(true)"] --> T{"阈值定在哪"}
    T -->|"调低(如 0.3)"| LOW["多拦:召回高、误报多
交人工 / 二次确认消化"] T -->|"调高(如 0.85)"| HIGH["少拦:精确高、漏报多
放行或只记录"] LOW --> S1["安全类场景
宁可误报不可漏报"] HIGH --> S2["体验类场景
宁可漏报不可误报"]

安全类场景,宁可误报不可漏报。 恶意链接、凭证泄漏、注入攻击——漏掉一个可能代价极高,而误报一个只是多一次人工复核。所以这类场景把阈值压低,让召回优先,再靠人工或二次确认消化误报。

体验类场景,宁可漏报不可误报。 自动屏蔽一条评论、自动折叠一封邮件、自动拦截一次操作——误报直接伤害用户体验,而漏报往往只是少了一次自动处理。所以这类场景把阈值抬高,让精确优先。

同一句"这是垃圾吗"的 Noul,放在反垃圾邮件里该设 0.4,放在"要不要给用户弹提示"里该设 0.85。判断的场景决定阈值的方向,而不是判断的文本。

二次确认:召回优先时的补偿手段

如果为了召回把阈值压得很低,误报会变多。一个干净的补偿手段是二次确认:第一遍宽进,第二遍严出。

这正是 is-malicious(luantak/is-malicious)的做法。它先对源码与构建文件问一批 Jev Noul 检查,把可疑的片段升级进入第二次 pass,最终在代码执行前给出"牵涉哪些文件、哪些行"的结果。第一遍负责不漏(宽阈值捞进来),第二遍负责不冤枉(对被捞进来的再精判)。这种"召回靠第一遍、精确靠第二遍"的两级结构,是安全类 Detection 的常见形态。

# 第一遍:宽进
p1 = client.systemone(
    state=chunk,
    questions={"sus": {"type": "noul", "question": "这段代码有没有可疑行为?"}}
)["sus"].probability

if p1 >= 0.3:                       # 低阈值,先捞进来
    # 第二遍:严出,连带上下文一起判
    p2 = client.systemone(
        state=f"上下文:{context}\n\n片段:{chunk}",
        questions={"mal": {"type": "noul", "question": "结合上下文,这是真实的恶意行为吗?"}}
    )["mal"].probability
    if p2 >= 0.8:
        report(chunk)

两级结构还有个隐性收益:第二遍能看到更多上下文。第一遍常常只有孤立的片段,第二遍可以带上调用点、文件路径、导入表——精度提升往往来自这里,而不只是来自更严的阈值。

Scoring:为什么不要直接要一个数字

Scoring 的底层是 Score。它最反直觉的一点是:不要让模型直接给你一个 1–10 的数字。

原因是数字尺度没有共识边界。同一段文本,这次模型给 7,下次给 8,你没法知道这是"真的更好"还是"抖动"。而下游一旦拿这个数去卡阈值("≥8 才推荐"),抖动就直接变成了不稳定行为。

Jev 的做法是让你给几个有序档位,模型返回一个分布:

d = client.systemone(
    state=headline,
    questions={
        "severity": {
            "type": "score",
            "question": "这条新闻所描述事件的威胁严重程度?",
            "labels": ["信息", "关注", "警惕", "严重", "危急"]
        }
    }
)

print(d["severity"].probabilities)  # {"信息":0.05,"关注":0.15,"警惕":0.30,"严重":0.40,"危急":0.10}
print(d["severity"].score)          # 1*0.05+2*0.15+3*0.30+4*0.40+5*0.10 = 3.35

档位少(通常 3–5 档)、语义清晰,每个档位都有共识——"危急"和"严重"的区别是明确的,而"7 分和 8 分"不是。稳定性由此而来。

score = Σ i·p_i 的含义

上面例子里的 3.35 是这么来的:把第 i 档记为数值 i(信息=1、危急=5),用每档的概率加权求和。

score=∑ii⋅pi\text{score} = \sum_i i \cdot p_i

这个数有三层含义:

第一,它是分布的"重心"。 3.35 落在 3(警惕)和 4(严重)之间偏后,说明整体判断偏严重但没到"危急"。比"取概率最高的那一档"信息量更大——只看最大档你会得到"严重",但丢掉了"还有 30% 概率是警惕"。

第二,它的取值范围是连续的。 落点介于档位之间,天然适合做细粒度阈值。score >= 3.5 比"档位等于严重及以上"更平滑。

第三,它的方差/分布形状反映不确定性。 分布集中在某一档,说明笃定;分布摊平,说明这条本身就没法定级。同样的 score 可能对应完全不同的确定性——{警惕:1.0} 和 {信息:0.3, 关注:0.4, 警惕:0.3} 的 score 接近,但后者其实判不出来。所以和 Choice 一样:看分布,不只看 score。

校准与置信度:分数可信吗

阈值策略有一个隐含前提——概率是诚实的。如果模型说 0.9,实际只有 0.6 的把握,那所有阈值都建在沙子上。衡量这件事的指标是 ECE(Expected Calibration Error,期望校准误差),越低越好。

poorjev(rupeshpoojary9/poorjev)是一个很好的例子。它在通用零样本 NLI 模型上实现 Jev 的 Choice/Score/Noul 接口,然后用温度缩放和 conformal abstention 把置信度做"诚实",附带一份可复现的校准评测:ECE 从 0.170 降到 0.071(交叉验证),完全离线、不需要 API key。

0.170 意味着什么?意味着按 0.9 阈值拦下的东西里,有相当一部分其实没那么确定。降到 0.071 之后,阈值才真正可信。如果你要在 Detection/Scoring 上做严肃的阈值策略,校准必须进入你的验证清单——第 23 章会把校准评测做成通用能力。

用概率做分级动作

Detection 与 Scoring 最实用的地方,是它们支持分级动作而不是二值开关。看一个 Scoring 的分级:

sev = d["severity"].score
if sev >= 4.0:
    page_oncall()          # 危急:直接寻呼
elif sev >= 2.5:
    notify_channel()       # 警惕→严重:通知频道
else:
    log_only()             # 信息/关注:只记录

再看一个 Detection 的分级:

p = d["is_bad"].probability
if p >= 0.9:
    auto_block()
elif p >= 0.5:
    queue_for_human()      # 灰色地带交人
else:
    allow()

分级的价值在于:它把"模型有不确定性"这件事显式地写进了控制流。 笃定的自动处理,模糊的转人工,安全的放行——每条分支都对应一个明确的动作,评审时一眼能看清系统在各种置信度下会做什么。

三个生产项目

Dub(dubinc/dub)——把恶意链接检测放进创建流程。 Dub 是一个开源短链服务。它在 malicious-link-check.ts 里调用 typesafe-ai/jev,在短链被创建之前就对 URL 做类型化判定。这里的形态选择很关键:它没有用一张黑名单,而是让它被一个 Noul 判定"这条 URL 该不该被拦"。黑名单的问题是永远滞后——新出现的钓鱼域名不在名单上;而语义判定能在链接生成的当下给出一个带概率的结论。这是 Detection 相对传统规则最典型的增量价值:从"匹配已知坏东西"升级到"判断是不是坏东西"。

Sniff Test(DanRWilloughby/snifftest)——十问一体的文字护栏。 Sniff Test 是一个文字 linter,它对每个段落问 Jev 十个 Boolean 问题,检测堆叠含糊(stacked hedges)、复述式结尾、not-X-but-Y 转折、光秃秃的成本数字这类 AI 写作痕迹,阈值设 0.7。它同时以 CLI、pre-commit hook、GitHub Action 和 Claude Code skill 四种形态存在。实测数据很能说明问题:中位延迟 182 ms,54 个干净段落里只误报 1 个,而 Haiku 4.5 误报了 37 个。

这个数字值得停一下。同样的阈值下,Jev 的误报是 1/54,生成模型是 37/54——差了三十多倍。原因不在"谁更聪明",而在检测任务本就不该交给生成模型:生成模型在"这段有没有某特征"上给的判断,是它顺带编出来的,不是它专门产出的。把它交给一个专门做判定、且概率经过校准的决策层,误报率会低一个数量级。这就是本书核心论点的又一次落地。

WorldMonitor(koala73/worldmonitor)——把严重度做成分级。 WorldMonitor 是一个实时全球情报面板,它用 Jev 把新闻标题的威胁严重度打分成 5 个威胁等级,并把事件归类到14 个冲突、网络、基础设施领域。这正是 Scoring 的教科书用法:一个有序的 severity 维度(5 级),加一个互斥的领域维度(14 类 Choice)。面板的每个色块、每次排序、每次告警降噪,都建立在这个 severity 分数上。它说明 Scoring 的产出往往不是"给用户看的分数",而是驱动下游决策的中间量——排序、过滤、触发。

其余项目一张表带过

项目 形态 关键设计
Perch(lakeday-org/perch) Noul + Choice + Score 语义代码 linter:对每个方法(连同调用者/被调用者)问"有没有 bug"(Noul)、"是哪一类、在哪一行"(Choice)、"多严重"(Score),外加按语言过滤的 CWE Noul 检查
JevGate(Tech-Byte-Frontier/jevgate) Noul + Choice + Score CI 与编码 Agent 的质量门:答案达 0.80 记为 review / consider,review 直接让构建失败,拿不准的文件保持 uncertain 而不是放过
JevPDF(kylemclaren/jevpdf) Noul 浏览器内 PDF 搜索:每行问一个"是否回答了查询"(每请求 16 行,共享页面文本作 state),0.55 以上高亮并按概率排序
JevSlop(TKY-27/JevSlop) Score 对文章在八个 Score 轴上打分,聚合成 0–100 的 Slop Score
jeff(saembit/jeff-cli) Score Go CLI:按 YAML 规格里每个加权维度的 Score 求和排序;score 命令能把阈值翻译成退出码 10,供 shell 与 CI 使用
Clean Code Judge(frostney/clean-code-review) Noul 对 PR 每个文件跑 31 个 Clean Code 布尔检测外加函数长度与嵌套深度,再交给写作模型生成评述
jev-risk-check-provider(caiovicentino) Noul + Choice + Score 把对交易对手的判断聚合成代码控制的 0–100 分;540 次运行中阈值 65–75 段准确率 99.76%、0 误报,每决策约 $0.00005、p50 约 400 ms
hippo-memory(kitfunso/hippo-memory) Noul(重排) 可选的 Jev 重排器把检索 R@1 从 0.41 提到 0.62(私有 300 查询开发者库)

注意 Sniff Test、JevPDF、JevSlop 都在做同一件事的不同切面:把一个模糊的质量判断拆成一组离散问题,用阈值切断,再交给代码执行动作。Detection 与 Scoring 不是为了得到"一个准确率数字",而是为了得到一个能被控制流接住的、带概率的判定。

小结

  • Detection 底层是 Noul,返回带概率的布尔;Scoring 底层是 Score,返回有序档位分布——两者真正的设计工作都在"怎么用概率"上。
  • 阈值的定法由两类错误的代价决定:安全类场景压低阈值保召回,体验类场景抬高阈值保精确;召回优先时可加"二次确认"两级结构补偿误报。
  • 不要直接要 1–10 的数字:给有序档位拿分布,score = Σ i·p_i 是分布的重心,取值范围连续、可做细粒度阈值;但相同的 score 可能有完全不同的确定性,要看分布。
  • 阈值策略的前提是概率诚实,ECE 校准必须进验证清单(poorjev 把 ECE 从 0.170 压到 0.071)。
  • 少用二值开关,多用分级动作——把"模型不确定"显式写进控制流。
  • 实测佐证:Sniff Test 在 0.7 阈值下干净段落误报 1/54,而生成模型 37/54;Dub 用语义判定替代黑名单;WorldMonitor 用 5 级 severity 驱动整个情报面板。

下一章进入 Search 与 Retrieval,看 Jev 如何"对每个候选问一个问题",在候选集里挑出真正相关的东西——这是 RAG 精排的核心动作。

📑 Jev 实战:用类型化决策重构软件

1 第 1 章:为什么需要决策层——从"生成一切"到"只做判定" 2 第 2 章:三种类型化输出——Choice、Score、Noul 与调用契约 3 第 3 章:十个决策形态——什么时候该把判断交给 Jev 4 第 4 章:Classification 与 Routing——把分诊做成一等公民 5 第 5 章:Detection 与 Scoring——二元判断与有序评分 6 第 6 章:Search 与 Retrieval——在候选集里找相关 7 第 7 章:Ranking 与 Verification——排序与校验 8 第 8 章:ML Feature Extraction 与 Structured Data Extraction——把文本变特征与结构化数据 9 第 9 章:检索与知识图谱——Search and retrieval / Graphs and knowledge graphs 10 第 10 章:模型路由与 LLM 护栏——Model routing / LLM guardrails 11 第 11 章:语义代码检查——Semantic code linting 12 第 12 章:特征提取与需求预测——Feature extraction / Demand forecasting 13 第 13 章:招聘与线索生成——Recruiting / Lead generation 14 第 14 章:客户支持——Customer support 15 第 15 章:理赔、金融犯罪与风险评估——Insurance claims / Financial crime / Risk assessment 16 第 16 章:法律与合规——Legal and compliance 17 第 17 章:电商市场与广告——E-commerce marketplaces / Advertising 18 第 18 章:内容审核与信任安全——Moderation and trust and safety 19 第 19 章:游戏——Gaming 20 第 20 章:实时应用——150 毫秒预算 21 第 21 章:科学发现——Scientific discovery 22 第 22 章:大数据上的 AI Map Reduce——为什么便宜 100 倍 23 第 23 章:通用验证——Universal Verification 24 第 24 章:Harness Engineering——把决策织进智能体骨架
← 返回本书大纲