
1. 为什么大家都在提Badcase和Eval先搞清楚问题本质聊Spring AI 2.0之前得先承认一个现实大模型应用和传统软件开发完全是两种玩法。传统代码里逻辑是确定的输入输出可预期但大模型应用不一样同样的Prompt模型今天给你A答案明天给你B答案甚至同一个请求在不同并发条件下结果都可能有细微差异。做过一段时间LLM应用的人都会经历一个阶段——模型看起来能跑但一上真实场景就露馅。我最早做的一个RAG问答系统Demo阶段效果惊艳领导看了很满意结果一上生产用户问的问题稍微绕一点回答就开始离谱。当时我第一反应是换个更好的模型然后换了更强的基座模型发现效果有提升但依然不稳定。后来才意识到问题不在模型本身而在评估方式我根本没有一套量化标准来判断好和不好全凭肉眼感觉。这就是Badcase和Eval存在的意义。Badcase字面意思就是坏案例指的是模型回答错误、不符合预期、或者虽然看似合理但实际有误导性的输入输出对。别小看这个词它其实是整个大模型应用优化链条的起点。EvalEvaluation则是一套系统化的评估体系用来衡量模型在特定任务上的表现。很多人把Eval理解成写几个测试用例跑一下这个理解太浅了。真正可用的Eval体系需要覆盖数据采集、标注标准、评估维度、自动化执行、回归对比这五个环节缺一不可。为什么这两件事在Spring AI 2.0的语境下特别重要因为Spring AI本身作为Java生态里的大模型应用框架它的定位是帮你把模型能力接入业务系统但接入只是第一步接完之后你总要回答一个问题这个能力上线后效果到底行不行没有Eval你连行不行都说不清。没有Badcase沉淀你连哪里不行都不知道。有人说我们团队人少先不做Eval等上线后看用户反馈再说。这个想法我也经历过但实际走下来会发现问题很严重用户反馈是滞后的、稀疏的、且带情绪的。一个用户说这回答完全不对你根本不知道他是哪句话不对、哪里不对、是事实错误还是逻辑错误。而Eval体系下沉淀的结构化Badcase能让你在用户发现之前就主动定位问题。用传统开发类比的话Eval之于大模型应用大致相当于单元测试之于服务端代码。区别在于单元测试的断言是确定的返回值是否等于预期值而Eval的评估是模糊的、多维度的这也是它更难落地、更需要花心思设计的原因。Spring AI 2.0把Eval相关的组件做了深度集成等于把这个模糊过程框架化、流程化给了Java开发者一条相对清晰的路。2. Spring AI 2.0在Eval和Badcase上提供了什么能力Spring AI 2.0相比1.x版本最大的变化之一就是把可观测性和评估提升到了框架级能力的位置。2.0版本的核心思路我总结为四个字链路可查。它把一次完整的大模型调用从Prompt构建、模型请求、响应解析、工具调用、上下文注入全流程都纳入可追踪范围同时提供了专门的Eval模块来对接评估流程。先说评估的数据来源。Spring AI 2.0提供了一套统一的观测数据结构核心是AiOperationObservation和相关的追踪组件能把每一次模型调用的输入输出、Token用量、耗时、模型名称、温度参数全部记录下来。这些数据有两个去向一个是用于实时监控和告警另一个就是沉淀为Eval的数据集。我自己在实际项目里的做法是先在Filter层做了一次调用数据的截获把生产环境的真实用户请求脱敏后和模型响应完整存下来然后定期抽样打标沉淀成Badcase库。Spring AI 2.0提供了对应的API不用自己再从零搭数据管道这一点省了不少事。再来看评估的执行方式。Spring AI 2.0里内置了多种评估器的接口实现常见的包括AnswerCorrectnessEvaluator判断模型回答是否正确基于参考答案对比RelevanceEvaluator评估回答和问题的相关性FaithfulnessEvaluator评估回答是否忠实于提供的上下文这个对RAG特别关键ContextRelevanceEvaluator评估检索到的上下文本身是否和问题相关CustomEvaluator允许你自己实现评估逻辑这个架构设计得很聪明。它没有把评估器写成黑盒而是暴露接口让你实现自定义逻辑这意味着你可以接入任意评估方式包括调用更强的模型来做裁判LLM-as-a-Judge也可以用规则、正则、关键词匹配来做硬性校验甚至可以接人工标注平台的结果。Spring AI 2.0还有一个值得提的点Eval和Artifact的绑定关系。在评估过程中每次评估的结果会关联到对应的Prompt模板、Model配置、以及当时的上下文内容。这意味着当评估发现Badcase时你能直接追溯到是哪一版Prompt、哪个模型参数导致的问题。这个特性我实际用下来非常有用——做Prompt迭代的时候经常会出现改了A又坏了B的情况有了这种绑定关系A/B回归对比会变得异常清晰。不过要注意一点Spring AI 2.0的Eval模块整体设计更偏向框架能力和接口约定它不会替你把评估体系完全建好。具体到你自己的业务场景需要定义什么是好答案需要准备评估数据集需要设计评估频率和回归策略。框架给的是执行通道和观测底座方法论还是要自己沉淀。3. 上手实操构建第一个Badcase追踪与Eval流水线理论说太多没用直接看一条最简的落地路径。我以Spring Boot 3.3 Spring AI 2.0 一个本地部署的DeepSeek模型为例跑通一条包含数据采集、评估执行、Badcase沉淀的完整链路。3.1 环境与依赖准备Spring AI 2.0目前要求JDK 17以上Spring Boot 3.x。Maven工程里需要引入的依赖如下dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-model-openai/artifactId version2.0.0/version /dependency dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-eval/artifactId version2.0.0/version /dependency dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-observability/artifactId version2.0.0/version /dependency如果你的模型接入走的是阿里云百炼或者DashScope可以换成spring-ai-starter-model-dashscope的依赖国内访问和部署会方便很多。注意Spring AI的版本兼容性问题2.0.0对应早期版本后续可能会有补丁版本建议使用最新的稳定Release。配置方面在application.yml里至少需要指定模型接入的基础信息spring: ai: model: openai: base-url: http://localhost:11434/v1 api-key: dummy-key chat: options: model: deepseek-r1:8b temperature: 0.7这里我用Ollama本地跑的DeepSeek做示例因为base-url指向本地api-key随便填一个占位符即可。生产环境如果要接云端模型替换成对应的Endpoint和Key就行。3.2 编写Eval数据采集与追踪代码先定义一次模型调用中我们关心的数据对象。我习惯建立一个EvalTraceRecord实体public record EvalTraceRecord( String traceId, String question, String answer, String contextDocuments, String modelName, double temperature, int promptTokens, int completionTokens, long latencyMs, LocalDateTime timestamp ) {}接着写一个过滤器把每次请求和响应的关键信息收集起来。Spring AI 2.0里可以通过Filter接口或AOP切面来实现Component public class AiCallTraceFilter implements AiFilter { Override public AiResponse filter(ChatClient client, ChatRequest request) { long start System.currentTimeMillis(); String question extractUserMessage(request); String context extractContext(request); AiResponse response client.call(request); long cost System.currentTimeMillis() - start; String answer response.getResult().getOutput().getText(); EvalTraceRecord record new EvalTraceRecord( UUID.randomUUID().toString(), question, answer, context, request.getModel(), 0.7, response.getMetadata().getPromptTokens(), response.getMetadata().getCompletionTokens(), cost, LocalDateTime.now() ); evalRecordRepository.save(record); return response; } }这里的关键思路是生产环境的每次调用都留痕但评估不在这里做。留痕是为了后续抽样本如果评估过程太慢LLM-as-a-Judge要额外调一次模型会影响线上延迟所以一定要和调用链路解耦。3.3 异步评估任务的实现评估放到异步任务里执行。Spring AI 2.0的Eval模块提供了Evaluator接口核心方法签名很简单public interface Evaluator { EvalResponse evaluate(EvalRequest request); }我自定义一个相关性评估器逻辑是先调用本地DeepSeek做判断再叠加一层规则校验Component public class RelevanceAndFaithfulnessEvaluator implements Evaluator { private final ChatClient evaluatorClient; public RelevanceAndFaithfulnessEvaluator(ChatClient.Builder builder) { this.evaluatorClient builder .defaultOptions(ChatOptions.builder() .model(deepseek-r1:8b) .temperature(0.0) .build()) .build(); } Override public EvalResponse evaluate(EvalRequest request) { String prompt 你是一个严谨的评估员。根据问题、回答和参考文档判断回答是否准确、是否忠实于参考文档。 回答必须只输出JSON{accuracy: 0或1, faithfulness: 0或1, reason: 简要说明} 问题%s 回答%s 参考文档%s .formatted(request.getQuestion(), request.getAnswer(), request.getContext()); String result evaluatorClient.prompt() .user(prompt) .call() .content(); return parseResult(result, request); } }温度设为0.0是这个评估器最关键的参数。评估模型如果温度太高同一个case每次都给出不同判定那评估结果就没有稳定性可言。异步调度我用Spring自带的Async注解配合一个TaskExecutorAsync(evalTaskExecutor) EventListener(ApplicationReadyEvent.class) public void runEvaluationLoop() { ListEvalTraceRecord pendingRecords evalRecordRepository.findPendingRecords(); for (EvalTraceRecord record : pendingRecords) { EvalRequest evalRequest new EvalRequest( record.question(), record.answer(), record.contextDocuments() ); EvalResponse evalResult relevanceEvaluator.evaluate(evalRequest); evalResultRepository.save(evalResult); if (evalResult.isBadcase()) { badcaseRepository.save(new BadcaseEntity(evalResult, record)); } } }这段代码生产化时要注意findPendingRecords()要加批次限制和游标不能一次性把所有记录都拉出来否则数据量上来后内存会爆炸。我一般每批处理500条处理完更新状态位。3.4 一个容易被忽略的坑并发评估的请求限制大模型评估器本质也是一次模型调用而且评估的请求量通常会超过线上调用量因为你要对历史样本反复回归。这就带来一个并发问题——模型服务端如果不做限流评估任务会把模型服务打挂。我自己踩过一次评估1000条历史样本开8个线程并发调用本地DeepSeek结果显存直接爆了服务OOM重启连带正在跑的生产请求也失败。后来专门加了个信号量限流核心代码如下private final Semaphore evalSemaphore new Semaphore(4); public EvalResponse safeEvaluate(EvalRequest request) { evalSemaphore.acquire(); try { return evaluator.evaluate(request); } finally { evalSemaphore.release(); } }这个教训说明Eval流水线本身也是一个需要容量规划的系统不能只把它当脚本跑。评估任务的并发度、评估模型的部署规格、评估队列的长度都要提前设计好。4. Badcase分析与Prompt调优闭环从发现问题到解决问题光有Eval还不够Eval的价值在于推动迭代。我基于Spring AI 2.0跑过一个完整的优化循环从生产环境中发现Badcase分析根因修改Prompt模板再回归验证最后沉淀为新的评估样本。这个循环走完我的RAG问答系统的准确率从62%提到了81%。4.1 发现阶段怎么找到真正值得分析的Badcase直接看评估分数高低是不够的。我总结了一个Badcase分级策略帮助你判断优先级优先级Badcase类型判断标准处理方式P0硬伤型事实性错误、涉及安全、违反指令必须立即修复暂停相关Prompt上线P1逻辑型推理链条断裂、结论与依据矛盾优先分析通常需要重写Prompt推理步骤P2遗漏型回答不完整、缺少关键信息补充Prompt约束或调整检索上下文P3风格型语气不合适、格式不符合要求低优先级可批量处理为什么分级很重要因为如果用统一标准去看所有Badcase你会被大量低质量问题淹没真正关键的事实性错误反而被漏掉。实际操作中我优先处理P0和P1用自动化规则把这两类Badcase自动筛选出来推送给值班群。4.2 根因分析三个最常见的Badcase来源经过大量Badcase复盘我观察到RAG类应用的问题主要集中在三个来源检索相关性问题。这是最大的来源占比通常超过一半。用户一个问题召回的前5篇文档里可能只有1篇和问题真正相关模型基于混杂的上下文生成的答案自然会偏离。这种问题用Faithfulness Evaluator能发现但修复往往在检索侧比如调整Embedding模型、重排序策略或者chunk切分方式。指令遵从不足。比如你在System Prompt里明确说了如果不知道答案请直接说不知道但模型依然会基于不完全信息强行生成一个看似合理的答案。这类Badcase的数量随着模型版本变化会有波动不同模型对指令的遵从度差别很大。上下文过载导致的幻觉。当塞入的上下文过长时模型反而会丢失关键信息被一些不相关但写法更吸引注意力的段落带偏。这也是为什么chunk大小不能盲目调大的原因。4.3 用Spring AI 2.0的Eval模块做回归对比找到根因后开始修改Prompt但改Prompt是有风险的——经常会出现修复了A问题但把本来正常的B问题改坏了。所以每次Prompt变更必须做一次全量回归。Spring AI 2.0的Eval模块对回归测试的支持相当到位。它允许你定义一组黄金样本集Golden Set每次改动后跑一遍对比评估指标变化public class PromptRegressionService { private final ListEvaluator evaluators; private final PromptTemplateManager templateManager; public RegressionReport runRegression(String newPromptTemplate) { ListGoldenCase goldenSet goldenCaseRepository.findAll(); MapString, EvaluateResult beforeResults evaluateWithTemplate(templateManager.getCurrentTemplate(), goldenSet); MapString, EvaluateResult afterResults evaluateWithTemplate(newPromptTemplate, goldenSet); return RegressionReport.compare(beforeResults, afterResults); } }比较报告里我重点关注两个数字整体通过率和波动率。整体通过率很好理解波动率指的是单个样本的得分变化幅度。如果一个样本从通过变成不通过但整体通过率上升了那说明这次改动有Trade-off需要人工判断哪个更重要。回归跑完把新发现的Badcase加入黄金样本集这样下次回归的覆盖面就更广。这个黄金样本集持续扩充的过程是评估体系逐渐变强的关键。4.4 Prompt版本的Badcase关联管理我在这里特别提一个Spring AI 2.0真正有价值的特性——PromptTemplate版本化与Badcase的关联。Spring AI的PromptTemplate支持模板ID和版本管理。我把每个线上的Prompt模板都注册进一个统一管理器Component public class PromptTemplateManager { private final MapString, ListPromptTemplateVersion templateStore new ConcurrentHashMap(); public void registerTemplate(String templateId, String content, String version) { templateStore.computeIfAbsent(templateId, key - new ArrayList()) .add(new PromptTemplateVersion(templateId, version, content)); } }然后在记录EvalTrace的时候把模板ID和版本号也一并记录下来。这样当某个Badcase被确认后你能立刻知道它是哪一版Prompt产生的是哪个模型的哪个温度参数上下文当时是什么。这个能力在团队协作时特别有用。我遇到过好几次这样的情况另一个同事改了Prompt性能看起来提升了但我手里有个Badcase明明是上上版才有的问题不知道是谁改出来的。有了版本关联直接一查就知道这个Badcase是你V3版Prompt引入的。5. 进阶实践让Eval体系在生产环境真正跑起来5.1 评估方式的选择模型评估器 vs 规则评估器 vs 人工评估很多人以为Eval就是用大模型评估大模型其实这是个误区。真正稳定的Eval体系是分层设计不同层级解决不同问题。我目前在项目里用的是三层结构第一层规则评估器执行成本最低用于硬性指标的自动校验比如回答中是否包含指定格式JSON、Markdown回答中是否出现敏感词或禁用词回答长度是否在合理范围是否出现重复语句、无意义废话关键字段是否为空这类评估完全不用模型参与纯代码逻辑判断速度快、稳定性高、无额外成本。它能拦截一部分最明显的低质量输出。第二层模型评估器成本适中用于语义层面判断比如相关性、忠实度、完整性、安全性。通常用更强的模型来评估线上模型的输出或者用同模型temperature0的方式。这一层是核心我们前面写的RelevanceAndFaithfulnessEvaluator就属于这层。比较强模型和弱模型的评估差异取决于你的场景。如果你的线上模型就是业界顶尖模型那评估器最好也用同档次甚至更强的模型。如果线上是7B、8B的小模型用一个14B或者32B的模型做评估器是性价比较高的选择。第三层人工评估成本最高但最准确用于抽样复检和争议裁决。规则和模型评估器都有局限一些模糊的、需要领域知识判断的Badcase只能靠人来标注。人工评估的比例可以从初始的100%逐步降到5%~10%前提是自动评估和人工评估的一致性足够高。这里有个经验值可以参考人工评估和模型评估的一致性用Cohens Kappa计算应该不低于0.8如果低于这个值说明你的评估Prompt或者评估维度定义有问题需要先修正评估Prompt本身。5.2 Badcase库的数据结构与生命周期管理Badcase不是一个简单的错误记录它应该是一个持续演进的数据资产。我建议每个Badcase实体至少包含以下字段public class BadcaseEntity { private String id; private String question; private String answer; private String expectedAnswer; // 理想答案如果有 private String contextDocuments; // 当时注入的上下文 private String modelName; private String promptTemplateId; private String promptTemplateVersion; private double temperature; private String errorType; // factual/reasoning/omission/style private String severity; // P0/P1/P2/P3 private String status; // new/analyzing/fixed/wontfix/regressed private String owner; private ListString relatedEvalIds; private LocalDateTime createdAt; private LocalDateTime updatedAt; }字段里的设计细节errorType和severity分开存是因为同一个Badcase可能既是事实错误又是逻辑混乱但修复优先级只能有一个。status状态机要非常明确尤其要区分fixed和regressed。一个Bug修复后如果后续某次变更又复现了它应该自动转为regressed而不是新建一条记录。我实现时是通过Eval回归时关联relatedEvalIds判断的。wontfix这个状态也很重要。有些Badcase是模型能力边界导致的当前模型无解或者说修复成本远大于收益需要显式标记并说明理由。否则团队会反复对同一类问题做无用功。5.3 让Eval结果推动Prompt版本迭代闭环打通的关键在于不能让Eval结果躺在数据库里吃灰。我做了两件具体的事。第一件给每个Prompt模板配置了允许的最大Badcase率。比如系统Prompt的P0级Badcase率不能超过1%P1级不能超过5%。一旦某个模板的实时评估指标超过阈值自动触发告警并把这个模板标记为异常后续请求自动切到上一个稳定版本。第二件建立评估-修复-再评估的工作流。每天上午跑一次全量回归输出一份精简报告到IM群包含今日新增Badcase数量及分布排名前5的高频问题类型是否有模板触发阈值告警待处理的P0级问题列表这份报告的价值在于让全组都对当前效果有感知。之前没有这套机制时很多人觉得反正模型输出没法保证出问题很正常。现在有了量化数据大家能明确知道上周P0从0.8%降到了0.3%这种反馈对团队凝聚力和信心提升非常明显。5.4 常见问题排雷Eval体系落地的四个坑以下是无数实战中反复被踩的坑如果你准备搭建Eval体系务必提前避开。坑一评估数据集太小。不满100条的评估集统计意义几乎为零。改动一次Prompt波动几个点根本分不清是变好还是随机波动。建议最少200条起步能到500条以上最好。坑二评估Prompt和业务Prompt混用。千万不要把评估用的Prompt和线上业务的Prompt放在同一个模板管理里。评估Prompt需要固定为temperature0、不带业务上下文、输出严格JSON而业务Prompt往往温度更高、风格更灵活。混用会导致评估结果不稳定。坑三只看平均分不看分布。两个模型A的平均分是85B的平均分也是85但A的分数分布是[90, 80, 85, 85]B的分布是[60, 100, 95, 85]B在关键场景上可能上限很高但下限很低。生产环境用哪个更稳我猜大多数人会选A。所以评估报告里应该同时展示分位值、方差和最差样本。坑四把Eval做成一次性项目。Eval体系要持续运转才有价值。它和业务一样需要维护、投入、定期审视评估标准是否符合当前需求。如果团队没有专门的人负责Eval那这套体系大概率会慢慢烂掉。哪怕每天只花半小时看一遍评估报告也比完全不管要好得多。6. 再进一步从Badcase到知识库与提示词资产的沉淀走到这一步你已经有了一个能运转的Eval流水线和Badcase库。这时候你会发现一个新的机会——把Badcase转化为团队的可复用资产。我在实际项目中做了一件事把高频Badcase按问题模式归纳成案例库每个案例包含问题描述、根因分析、修复方案、涉及模板、验证结果。比如模式A用户询问最新的XX政策但知识库数据截止到半年前模型没有表达信息可能过期反而直接回答模式B多轮对话中用户说那第二个方案呢模型丢失了指代关系回答偏离话题模式C用户用方言口语提问模型理解错误但给出自信且错误的回答这些模式成为团队内部培训的素材也成为新Prompt设计的指导原则。新手加入项目时先看一遍案例库就能少踩很多坑。从技术实现上说这个案例库完全可以构建为Spring AI的Tool供模型调用。当用户遇到问题时模型可以先检索案例库中是否已有相似Badcase及其修复方案从而给出更可靠的回答。这其实就是从评估走向自适应优化的雏形。以我个人的体会来说做Eval和Badcase分析最大的收益不只是效果提升而是让你对大模型应用的掌控感从根本上变强了。刚接触大模型应用开发的时候那种输出不可控的无力感非常难受。现在每次迭代都有数据支撑每次改动都知道影响范围每次上线都有回归保障——这套体系带来的确定感是支撑团队持续迭代的底层动力。最后分享一个小技巧如果你现在还没条件搭建完整的Eval体系先从最小单元开始——把每次模型调用的输入输出完整记录下来每周手动翻一次把明显错误的问题记下来。坚持一个月你对自己应用的真实质量会有一个远超同行的感知。之后再从这些记录中逐步提炼评估维度、构建自动化评估一切都会顺理成章。