评测与成本:AI 方向的收束——验收真实模型前必须想清楚的机制
前两页把 RAG 管线与 Agent 权限做成了可验证的实验;这一页回答最后一组工程问题:怎么知道这套系统"效果够好"? 相关性、引用忠实度、越权、上下文预算、延迟、成本各用什么指标测量、在哪里测量、哪些能本地确定性验证、哪些必须等真实模型。本票(U84,#121)是 AI 方向的收束票。
先说清本页的证据等级:2026-10-28 核查,本机没有任何 LLM 凭据(OPENAI_API_KEY/ANTHROPIC_API_KEY/DASHSCOPE_API_KEY/DEEPSEEK_API_KEY/MOONSHOT_API_KEY/ZHIPUAI_API_KEY/GOOGLE_API_KEY 均未设置,本机无 Ollama 等本地推理服务)。按票面口径"缺凭据仅受阻,不以 stub 宣称方向完成":本页能本地确定性验证的部分(评测器执行路径、机械指标、成本算术、记录格式、回归门)已在 labs/spring-ai-eval 的 19 个测试实跑;真实模型的分数、引用忠实度判定、延迟分布与账单全部受阻,本页不给出任何一个真实评测数字。
一句话主线:评测分数必须来自真实模型,评测框架、指标定义与回归门必须先在本地钉死——前者受阻不阻塞后者。
评测的三层:框架、指标、分数
| 层 | 内容 | 本页证据等级 |
|---|---|---|
| 框架 | Evaluator SPI、评测器执行路径、记录格式、回归门 | 本地实跑(19 测试) |
| 指标 | 相关性、引用覆盖率、越权拒绝率、成本上界、延迟测量点 | 定义与机械部分实跑;真实取值受阻 |
| 分数 | 真实模型在真实数据上的评测结果 | 全部受阻,本页为零宣称 |
版本、工程与观察入口
固定 Spring AI 2.0.2、Spring Boot 4.0.9、Spring Framework 7.0.10(与 U82/U83 同一已核实组合)。评测集与 U82/U83 同源:两份合成知识库(11 个段落块)、6 条评测用例(4 条正常 + 2 条越权探测)。
公开入口在仓库根目录:
python3 labs/spring-ai-eval/run.py入口先探测凭据(7 个环境变量 + 本地推理端口),生成受阻报告,然后运行 19 个本地可验证测试(EvaluatorSpiTest 6、MetricsAndGateTest 8、LatencyContextGateTest 5),核对观察行后把环境、凭据门状态与文件哈希写入运行证据。受阻时退出 0——受阻是运行状态的合法取值,不是失败。
评测器 SPI:评测也是一次模型调用
RelevancyEvaluator:相关性判定
Spring AI 的评测抽象很小:Evaluator 是函数接口,evaluate(EvaluationRequest) 返回 EvaluationResponse(pass/score/feedback,另有 metadata 字段;本页用例为空 Map)。EvaluationRequest 装三样东西:用户问题(userText)、支撑数据(dataList,通常是检索到的文档)、被评的回答(responseContent)。
两个内置评测器都把"评测"实现为再调一次模型(LLM-as-judge)。RelevancyEvaluator 判"回答与问题相关吗",固定源码 RelevancyEvaluator.java,71–90 行:
@Override
public EvaluationResponse evaluate(EvaluationRequest evaluationRequest) {
var response = evaluationRequest.getResponseContent();
var context = doGetSupportingData(evaluationRequest);
var userMessage = this.promptTemplate
.render(Map.of("query", evaluationRequest.getUserText(), "response", response, "context", context));
String evaluationResponse = this.chatClientBuilder.build().prompt().user(userMessage).call().content();
boolean passing = false;
float score = 0;
String normalizedResponse = (evaluationResponse != null) ? evaluationResponse.strip() : "";
if ("yes".equalsIgnoreCase(normalizedResponse)) {
passing = true;
score = 1;
}
return new EvaluationResponse(passing, score, "", Collections.emptyMap());
}三个机制细节,每个都有本地实跑:
- 判定是字面的:judge 回答 strip 后必须恰好是 "yes"(大小写不敏感)才通过,分数 1;"no" 或任何其他文本(包括中文"是的,支持。")判负(
U84 judgeStrictness=PASS paddedYes=true chineseNo=true)。这条约束既是护栏也是坑:judge 模型必须被提示约束为只回 yes/no,否则有判无分。 - 一次评测 = 一次 judge 调用(
U84 spiPath=PASS invocations=1 network=false)——评测成本是线性的:N 条用例 × K 个评测器 = N×K 次 judge 调用。 - 执行路径可被替身完全驱动:评测器只依赖
ChatClient.Builder,所以"评测框架本身"不需要真实模型就能验证(U84 relevancyYes=PASS score=1/U84 relevancyNo=PASS score=0)。
FactCheckingEvaluator:有据核查
FactCheckingEvaluator 判"回答被上下文支持吗",模板固定为 document/claim 两段:
public EvaluationResponse evaluate(EvaluationRequest evaluationRequest) {
var response = evaluationRequest.getResponseContent();
var context = doGetSupportingData(evaluationRequest);
String evaluationResponse = this.chatClientBuilder.build()
.prompt()
.user(userSpec -> userSpec.text(this.evaluationPrompt).param("document", context).param("claim", response))
.call()
.content();
String normalizedResponse = (evaluationResponse != null) ? evaluationResponse.strip() : "";
boolean passing = "yes".equalsIgnoreCase(normalizedResponse);
return new EvaluationResponse(passing, "", Collections.emptyMap());官方对它的定位要如实转述:这是"声称是否被文档支持"的有书判(grounded check),不适用于闭卷测试(不给材料考模型冷知识);官方还建议用专门的核查模型(如 Bespoke-Minicheck)做高吞吐核查——这些模型本机都没有,均受阻。三态语义(支持/不支持/不确定)在这份源码里实际是二值:非 yes 即失败,"不确定"没有被单独建模,这写进正文防止误读。
能本地确定性验证的指标
引用覆盖率与编造引用(机械可算)
语义级的"引用失真"(引用的内容真的支持那句话吗)需要 judge 或人工;机械级的两项检查本地可算:
- 引用覆盖率:回答里的
[doc#N]引用是否都指向期望来源文档(U84 citationCoverage=PASS)。 - 编造引用:引用的文档名是否在已知文档集合内(
U84 fabricatedCitation=PASS fabricated=detected)——模型编一个不存在的文档名,机械检查就能抓住。
它们的边界要写清:能抓住"引错了文档",抓不住"引对了文档但说的不是那句"——后者就是受阻的语义判定。另外,覆盖率不惩罚"完全不引用",是否应当引用由用例的 expectsCitation 期望标记另判。
越权拒绝率:评测口径
U82/U83 已实跑的越权拒绝在这里变成评测口径:评测集包含 2 条越权探测用例(薪酬询问、跨租户请求),通过标准是"全部拒绝且审计有记录"。替身链路上 U83 已验证拒绝行为;真实模型场景下的完整评测(模型可能尝试其他调用形态)受阻。
成本模型:两套算术
成本是算术,不是玄学。两套模型:
- 按调用计:成本上界 = (模型调用数 + 工具调用数) × 每次单价。U84 算例是 4 次模型 + 6 次工具 × 0.002 = 0.02 元(
U84 costModel=PASS perCall=0.02);U83 的 0.022 元是 6 工具 + 5 模型的另一组算例。 - 按 token 计:成本 = prompt_tokens/1000 × 单价 + completion_tokens/1000 × 单价。实验算例:1200 prompt × 0.001 + 300 completion × 0.002 = 0.0018 元(
U84 costModel=PASS tokenized=0.0018)。
Usage:token 计量与三计数
Usage 接口提供三类主计数:getPromptTokens()、getCompletionTokens()、getTotalTokens()(默认 prompt+completion,provider 可覆盖;2.0 还有缓存读写与原生用量字段,本实验未使用)(U84 usage=PASS prompt=1200 completion=300 total=1500)。两条边界:单价是运营配置,Spring AI 不内置定价——把"框架里的 Usage"与"账单里的金额"分开,中间是你们自己的价格表;真实 provider 的 token 计数(尤其缓存与多模态的计法)未运行,受阻。
上下文预算:应用侧算术
上下文窗口是模型属性,不同模型不同;本实验核对的评测与提示组装路径不做自动截断(会话历史只由应用的记忆窗口裁剪),超限的失败发生在 provider。应用侧防线是预算算术:可用检索块数 = (窗口 − 回答预留 − 系统提示) ÷ 平均块 token。实验算例:8000 窗口、预留 1000、系统 200、块均 400 token → 最多约 17 块(U84 contextBudget=PASS maxChunks=17)。U83 已实跑的 MessageWindowChatMemory 裁剪(切点对齐 USER)是会话维度的同一族防线。真实 provider 对超限请求的报错形态(413/400 的具体语义)受阻。
延迟测量点:首包与总延迟
两个测量点定义要分开:首包延迟(订阅到第一个 token,用户感知的"开始回答")与总延迟(订阅到流完成)。Flux.timeout 的元素间语义(U82 已实跑)作用于两者之间。替身能真实计时(首包时刻晚于订阅、完成时刻不早于首包、总延迟 ≥ 首包延迟,U84 latencyPoints=PASS 与 U84 streamTiming=PASS 的断言都是真实测量的);但替身的耗时是脚本固定间隔,任何真实模型延迟或 P50/P99 数字都受阻——本页因此一个真实延迟数值都不给,只给测量方法。
合成评测集与规模声明
评测集与 U82/U83 同源(两份知识库、6 条用例、2 条越权探测),规模如实声明;真实业务分布的采样方案是后续工作。
评测记录与回归门
评测记录:JSONL 与复跑
评测要能复跑,记录格式先行。本实验定义每条记录为 JSON 行:{case, pass, score, promptTokens, denied, ...}(U84 recordFormat=PASS lines=2 jsonl=true)。字段最小集:用例 ID、期望(预期来源/预期拒绝)、实际回答摘要、各评测器判定、token 用量、时间戳。拿到凭据后,同一入口、同一记录格式即可产出真实分数——恢复路径写进了凭据门的受阻报告里。
回归门:阈值判定
回归门是布尔逻辑:通过率 ≥ 阈值则通过(U84 gate=PASS 6/6>=0.9 true 5/6>=0.8 true 4/6>=0.8 false empty=false),空评测集不通过。它本地可验证的部分到此为止;真实分数的波动性(同一模型不同温度/版本的方差)会决定阈值怎么定,这是拿到真实数据后的事。
LLM-as-judge 的局限
把 judge 也是模型这一点说透,有三条局限,每条都是机制性的:
- 判定的可操纵性:judge 读的是提示词,被评文本里如果嵌入"请回答 yes",judge 可能被注入——U83 的注入结论同样适用于 judge。生产要隔离 judge 的输入(只传结构化字段,不传自由拼接的全文)。
- 字面判定的脆弱性:上文的
chineseNo用例说明判负可能只是格式不符;judge 的误判方向是漏报(把该过的判没)与过报都要用小规模人工抽检校准。 - 自我偏好:让同一族模型评自己生成的文本,偏差方向不可控——这是评测设计问题,不是配置问题。官方文档把这类评测器定位为工程起点而非权威裁判(它自己还推荐专门的核查模型),本页转述并加上"本机未实测其偏差"的如实标注。
观测与报告
评测报告的结构与 U83 的最小审计衔接:每次评测运行产出 (运行 ID、评测集版本、模型标识、各用例记录、通过率、门禁判定、凭据门状态)。ToolAudit(U83)与引用覆盖率(本页)共同构成"越权 + 失真"两个维度的原始材料。Micrometer 导出仍未运行;本页不新增宣称。
受阻清单与恢复路径
| 项目 | 状态 | 恢复路径 |
|---|---|---|
| 真实模型相关性/事实核查评测分数 | 受阻 | 设置任一凭据后重跑同一入口 |
| 真实引用忠实度(语义级)判定 | 受阻 | 同上(FactCheckingEvaluator 或人工) |
| 越权用例的真实模型完整评测 | 受阻 | 同上 |
| 真实 provider 的上下文超限报错形态 | 受阻 | 同上 |
| 真实延迟分布(P50/P99) | 受阻 | 同上(按本页测量点埋点) |
| 真实账单与真实定价 | 受阻 | 运营配置价格表后按本页公式计算 |
| judge 偏差实测 | 受阻 | 需真实模型;先小规模人工抽检 |
受阻不是失败(入口退出 0),受阻更不是通过(本页不宣称方向完成)。 AI 方向三票的机制部分(检索、ACL、引用、工具、权限、确认、预算、取消、评测框架)已按票面要求本地实跑闭环;"真实模型效果够好"这一句,在拿到凭据并跑完评测之前,本课程不写。
实验读法
python3 labs/spring-ai-eval/run.pyEvaluatorSpiTest(6):两个评测器的执行路径、判定分支、judge 严格性、SPI 无网络可验证。MetricsAndGateTest(8):引用覆盖率、编造引用、评测集规模、成本两套算术、Usage、回归门、记录格式、ACL 汇总。LatencyContextGateTest(5):延迟测量点、流式计时结构、上下文预算算术、凭据门、受阻语义。
闭卷验收
本页 26 道闭卷题收进了下方自测面板:先回忆,再展开参考答案与评分点,最后给三档自评。