这段时间刚好在做一个智能考试系统的课程设计收尾。项目立项时定的主题是“基于SpringAI的智能考试系统”从一开始的框架选型讨论到后面真正把智能组卷、AI批改、学情分析这些模块跑通前前后后花了大约六周。这篇文章不写什么宏伟蓝图就老老实实把系统设计时踩过的坑、想清楚的和没想清楚的关键点都整理出来给后面打算用Spring AI做类似业务系统的同学一份可参考的设计笔记。写之前先把项目背景交代清楚这是一个面向高校课程考核场景的考试系统设计项目目标不是做一个颠覆传统考试流程的平台而是把现有考务管理中的高频痛点用AI能力解决掉。整个系统基于Spring Boot 3.x搭建AI能力层依赖Spring AI框架大模型底座在开发期用的是某开放平台提供的Chat模型具体到生产环境再替换成私有化部署的模型或商用模型数据库采用MySQL存储业务数据Redis负责会话与缓存向量数据库选用Milvus承载知识库检索。系统的核心用户角色是教师和学生教师负责建题库、组卷、批改、查看学情分析学生负责在线答题、查看解析、使用AI助教提问。下面按设计顺序把整件事展开讲涉及到的关键代码和配置都是项目里实际用过的简化版本重点看设计思想和取舍逻辑。1. 先从业务侧拆解考试系统的“智能”到底加在哪开始动手开发之前我拉过一个清单把传统在线考试系统从出题到考后分析的完整链路走了一遍。发现真正能让教师“少干活”、让学生“学得更准”的环节其实是比较固定的几个点。1.1 传统考试系统最让人头疼的四个环节第一个环节是组卷。大多数老系统的组卷逻辑是“随机抽题人工调整”。教师选定章节和题型之后系统按比例随机抽抽出来的试卷可能难度分布不合理可能同一知识点重复出现还可能覆盖不到关键考点。教师不得不一遍遍预览、替换、再预览非常耗时。第二个环节是题库维护。题库里的题往往是多年积累下来的没有知识点标签没有难度系数更没有考查能力维度的标注。技术再好抽题引擎也不可能从一堆没有元数据的题目里抽出高质量的试卷。所以要让组卷“智能”必须先让题目“结构化、标签化”。第三个环节是主观题批改。填空、选择、判断这类客观题计算机很好判但是简答、论述、计算题常年依赖教师人工批改。教师在一百多份卷子里来回翻手写评语、给步骤分工作量极大并且评分一致性很难保证。第四个环节是考后分析。传统系统给出的学情报告基本是分数分布、及格率这些统计数字。教师想知道“这个班对哪个知识点掌握得差”得自己拿Excel去筛数据、看错题光是把错题对应到知识点再做汇总就需要大半天。这四个环节对应的共同特征是“重复度高、规则复杂、标准模糊”。而这类问题恰恰是大语言模型的强项——它可以理解语义、生成文本、归纳模式并且支持一定程度的逻辑推理。用Spring AI来做系统级集成主要就是让这些能力以工程化的方式进入真实考务流程而不是做一个“聊天窗口”。1.2 明确智能化的边界能做和不该做的这一步非常关键。定义“智能边界”不是说能否调用大模型而是“什么环节可以让模型自主决策什么环节必须由人来兜底”。我在设计文档里明确写了几条线项目组后面所有争议都靠这几条线裁决。第一条AI负责“生成建议”不负责“执行决策”。智能组卷生成一套试卷后必须经过教师确认才能发布。AI批改给出的分数和评语是“初评建议”系统只自动保存客观题分数主观题分数默认进入“复核队列”由教师一键通过或调整。这样设计既保留了AI的效率也守住了考试公平的底线。第二条允许AI在无歧义环境中自由发挥但输出必须符合强约束格式。大模型的自由文本能力在生成解析、学习建议、问答题点评这些场景中非常有效但要接进业务系统输出必须转换成稳定的JSON或Markdown结构否则下游解析会出大问题。Spring AI给我们提供了ChatClient和结构化输出转换能力在Spring生态里做这件事非常自然不需要自己拼一堆普通HTTP调用和JsonParser代码。第三条模型错误要有显式的“已知风险”。AI批改不是100%正确所以系统内必须保留人工复核入口、修改留痕、操作日志。我们在UI上给AI初评结果加了一个橙色的“AI建议”标识教师一次点击即可替换为本人评分。从实际试运行反馈看教师对AI批改的信任度很大程度上取决于这个“可纠错”的入口是否足够顺畅。1.3 最终确定的功能全景边界定好以后功能全景就很清晰了。系统拆成七个一级模块用户与权限、题库管理、智能组卷、在线考试、AI批改、学情分析和AI助教。七个模块里与Spring AI强相关的有三个半。强相关的是智能组卷、AI批改、学情分析再加半个AI助教助教本质上是检索增强的问答和学情分析共享一套知识库底座。剩下的是传统业务模块走正常CRUD路线。这样设计的好处是AI模块不会无限蔓延整个系统的技术风险可控。同时也是对整个项目的预期管理——毕竟项目周期有限先把AI价值最高的模块做扎实。2. 技术选型为什么在多个方案里用了Spring AI技术选型阶段我们内部做过三轮比较。第一轮比较的是起来感觉方案差异并不在“能不能调用大模型”而在“调用的复杂度、与业务代码的耦合度、后续替换大模型的成本”。2.1 先比较的三条路第一条路线是直接在业务代码里用HTTP客户端调用模型API。用RestTemplate或OkHttp发请求自己拼Prompt自己解析返回的JSON。好处是简单直接、没有额外依赖。坏处是提示词管理、流式响应、函数调用、多模型切换都要自己封装而且封装质量取决于团队水平容易在接入第二个模型时推倒重来。第二条路线是引入LangChain4j。这个框架支持多种模型也提供了很多AI原生组件。但项目组大部分成员都是Spring技术栈LangChain4j虽然也能在Spring里用但在依赖管理、注解支持方面还是多少有些“外来感”部分组件的更新节奏和Spring Boot版本之间对不齐。第三条路线是Spring AI。它最核心的优势是它是一个“由Spring生态官方维护”的AI集成层模型接入、提示词模板、结构化输出、向量存储抽象、流式API这些能力都被封装成与Spring风格一致的形式。数据源、Redis、消息队列怎么在Spring里配大模型就怎么在Spring里配——这大大的降低了团队的心理负担和上手成本。2.2 Spring AI里真正用到的几个能力点先交代一下项目里用的版本Spring Boot 3.2.xSpring AI 0.8.x0.8系列API较新但设计思路和后续版本没有根本性变化。应用里实际用到的能力点如下。第一是ChatClient这是Spring AI 0.8后推荐的同步/流式调用入口。以前控制层需要注入一个ChatModel手动做系统提示词、用户消息、参数设置的拼装组合。现在只需要注入ChatClient链式调用非常清爽ChatClient chatClient ChatClient.builder(chatModel) .defaultSystem(你是一名考试命题专家请严格根据要求生成试题。) .build(); String response chatClient.prompt() .user(请生成3道关于Spring事务传播机制的选择题难度为中等) .call() .content();第二是PromptTemplate这个功能解决了项目里最头疼的“提示词模板管理”问题。智能组卷的提示词非常长里面有很多变量比如知识点、题型、难度分布、分值。如果直接在Java代码里写字符串拼接维护起来是一场灾难。PromptTemplate允许我们把提示词以资源文件形式放在classpath下用类似Mustache的模板语法填充变量并且支持结构化参数校验。这个设计思路和Spring的View层模板几乎是同一个模式团队接受度非常高。第三是结构化输出。考试系统里AI的返回内容最终都要转成业务对象例如生成一个题目对象包含题干、选项、答案、解析和知识点标签。直接用字符串返回再解析非常不可靠而用Spring AI的BeanOutputConverter可以把目标结构定义成普通Java记录类框架会替我们做模型输出格式约束与类型转换record Question( String stem, ListString options, String answer, String analysis, ListString knowledgePoints ) {} BeanOutputConverterQuestion converter new BeanOutputConverter(Question.class);第四是流式API。在线答题后的“逐字显示AI解析”体验以及AI助教对话框里的打字机效果都用的是ChatClient的stream方法。这个手感上的提升对产品评价影响很大后面会专门讲实现细节。2.3 和直接裸调大模型API相比Spring AI带来的工程化收益这里不吹捧框架纯讲实际收益。裸调API的问题在小型Demo里完全看不出来一旦系统里超过五个地方要调用模型就会出现三种脏代码提示词拼接逻辑在业务代码里反复出现、模型返回结果的解析散落在各个Service、模型切换时需要修改每个调用点的包名和参数格式。Spring AI用统一的ChatClient把这些都收口了业务代码只需要描述“我要这次对话完成什么”至于模型供应商是谁、用什么API格式、返回JSON怎么解析交给框架层去适配。从版本升级角度看也有收益。大模型的供应商API调整频率并不低以前裸调API时每次供应商更新接口签名我们都要改五六个模块的代码。使用Spring AI后多数情况下只需要升级依赖版本并调整少量配置业务层代码基本不动。还有一点容易被忽略Spring AI对Observability可观测性有原生支持。接入Micrometer的tracing能力之后一次AI调用的完整链路从业务方法到模型服务都能被追踪。联调时候非常有用可以直接定位是提示词生成慢了还是模型响应超时了。3. 系统整体架构与模块详细设计架构上我采用的分层方式比较常规前端Vue3 后端Spring Boot MySQL Redis Milvus 大模型服务侧由Spring AI适配层屏蔽差异。但在关键业务链路里增加了一层“AI编排服务”这是全系统重要的设计决策之一。3.1 架构分层与一条完整请求的流转路径所谓“AI编排服务”可以理解为所有AI能力的入口层。它不直接写业务逻辑而是负责把业务Service传来的“任务描述”翻译成一连串模型调用、知识库检索、结果清洗、降级兜底。这样做的好处是AI相关的技术细节不会污染到业务Service方便单元测试和后续替换。拿学生向AI助教提问来举例。一次完整请求的流转路径是前端发送问题到ControllerController调用AI助教Service该Service先把问题写入Redis记录会话上下文再从向量数据库检索与问题相关的内容片段最后调用AI编排服务把用户问题、检索片段、历史消息整合成上下文请求大模型最终以SSE流式返回给前端。关键点在于“整合上下文”这个动作。原始的大模型API调用并不会自动帮你做知识库检索也不会自动管理多轮会话。如果这个工作散落在业务Service里每次新功能都会重复实现一遍。放在AI编排服务里统一处理相当于为整个系统提供了一个“AI网关”后续新增一个“AI生成试卷分析报告”的功能时只需要新增一个编排方法公共逻辑直接复用。3.2 智能组卷模块的设计从“随机抽”到“策略生成”一开始我对智能组卷有一个误区以为可以直接让大模型“从零生成一张完整卷子”。实际做了两次原型测试后发现让模型从零生成一张高质量、结构稳定、且覆盖指定知识点分值的完卷成功的概率并不高。模型会在题量较多时漏掉要求也容易重复出相似题目。最后放弃了这个思路改成了“人工策略 大模型细调”的混合方案效果更稳定也更符合真实考试系统的约束。具体流程是这样的教师选择本次考试的基本要求考试范围章节、题型结构选择、填空、简答、论述、难度分布易中难 352、总分值100分。系统从题库里根据知识点和难度标签用配置好的策略抽出一批候选题目保证知识点覆盖和难度比例都在约束范围内。把候选题目、试卷框架、约束规则统统放到一条Prompt里让大模型做“优先级排序”和“冲突消解”。例如模型发现两份备选题目都在考同一个概念会结合考点的均衡度挑更合适的一题。最后生成试卷预览并输出一份组卷说明解释为什么选择这几题例如“第3题和第7题虽然考点相同但第3题考查概念理解第7题考查应用能力均予保留”。第4步里的组卷说明非常受教师欢迎。以前的抽题引擎只能回复“已按比例抽题”教师完全不知道机器为什么这么选。现在大模型给出可阅读的组卷思路教师在确认时心里有底也愿意去手动替换个别的题目。这段功能里有一段核心代码值得展示是“策略抽题”触发的Prompt构造public String buildPaperPrompt(PaperFrame frame, ListQuestion candidates) { PromptTemplate promptTemplate new PromptTemplate( 请根据以下试卷框架和候选题目生成建议试卷。 试卷框架信息 总分{{totalScore}}题型要求{{structure}} 难度分布要求{{difficultyRatio}} 候选题目编号{{candidateIds}} 候选题目详情{{candidateDetails}} 请严格按照试卷框架选择题目并输出最终试卷结构。 ); MapString, Object params Map.of( totalScore, frame.getTotalScore(), structure, frame.getStructure(), difficultyRatio, frame.getDifficultyRatio(), candidateIds, String.join(,, candidates.stream().map(Question::getId).toList()), candidateDetails, buildCandidateDetailPrompt(candidates) ); return promptTemplate.render(params); }这段代码想说明的是提示词模板与变量的天然解耦。后面的Prompt模板维护完全可以交给业务分析师去调整这段工作不应该是开发者的日常负担。所以把提示词内容放到resources/prompts/paper-assembly.st放在文件中而不是Java代码的字符串模板里。3.3 AI批改模块主观题怎么判才能服人AI批改模块是整个系统里争议最多、也最考察“工程心态”的部分。它的设计思路是“初评建议 教师复核”整个流程分为三步。第一步是规则初始化。系统从数据库取出答卷和对应题目根据题型路由到不同批改器。客观题直接走答案比对不走大模型。主观题和编程题进入AI批改流程。第二步是AI初评。把待批改的题目文本、标准答案、评分细则和学生的作答内容放入一个大Prompt中让模型输出一个结构化的评分结果。评分结果包括得分、扣分点列表、给分依据、建议评语。重点说一下评分细则它必须有明确的档位描述例如“能写出核心公式得4分能正确代入数据再得3分最终计算结果正确再得3分”。模型对档位式评分细则的遵循程度会比让它“自由判断给几分”高很多。第三步是人工复核。所有AI初评结果统一展示在教师复核页面教师可以一键接受也可以修改分数或评语。项目组同样做了一个重要决定——AI批改的得分不与最终成绩直接挂钩只有当教师点击“确认”后AI分数才会进入成绩表。这个设计在校园场景测试中极大降低了教师对系统公平性的质疑。编程题批改是项目里一个比较亮眼的亮点同时也最难。我们采用的做法是“静态检查 测试用例 大模型评语”三条线并行代码先过一遍编译和静态检查然后跑预置的单元测试用例最后把测试结果、代码片段、题目要求交给大模型请它给出针对性的改进建议和学习方向。大模型不直接决定分数它只负责提供“反馈评语”。最终成绩按照测试用例通过率来折算教师可调整。这里面有一个值得注意的经验不要试图让大模型自己“运行”代码。模型对代码输出的解析经常不稳定。让代码在真实的执行环境里跑把执行结果作为客观事实交给模型去解读比让模型凭代码文本判分稳定得多。3.4 学情分析与AI助教让数据变成可对话的知识学情分析这个模块早期计划做成“套用模板生成报告”。教师选择班级和考试后系统自动汇总出知识点掌握度、分数段分布、成绩趋势等指标并渲染成图表同时附带大模型生成的文字解读。这个目标看似不难但有一个细节需要谨慎处理——生硬拼接统计数字会让模型生成一堆“正确的废话”。解决方式是用结构化的数据分析结果来约束提示词。统计学指标用程序精确计算模型只负责对“统计结论”做符合语境的转译。举例来说如果数据显示“某知识点正确率42%低于全班平均12个百分点”我们不会让模型从原始数据里现算而是直接把结论和阈值喂给模型让它围绕这个结论组织解读语句。这么做的原因是大模型做数值计算容易出错而精确的数字和结论应由工程侧完成。AI助教模块是面向学生的问答式学习助手。它背后的知识库由两部分组成一部分是课程讲义被切割、向量化后存入Milvus的文档切片另一部分是历次考试的错题解析和知识点文档。学生询问“为什么AOP代理在Spring中有时不生效”这类问题时助教会先从向量库检索出与概念相关的讲义片段再把片段和问题一起送给大模型回答时附带参考来源。这个功能本质上是RAG应用的校园场景变体也是项目里与Spring AI结合得最顺畅的模块。向量化的实现没有想象中神秘。项目里用了一个简单的文档处理管线先把PDF或Word讲义转成纯文本按固定长度比如每500字切片并保留段落标题作为元数据然后调用嵌入模型生成向量写入Milvus集合。检索时直接用Spring AI的VectorStore抽象不需要自己处理向量计算细节ListDocument documents vectorStore.similaritySearch( SearchRequest.builder() .query(Spring AOP代理失效的原因) .topK(4) .build() );4. 实操过程中的关键点提示词、幂等与流式输出Spring AI的封装确实省了很多事但实际做下来发现模型的稳定性和系统的鲁棒性还是要靠应用层设计和大量的细节打磨。这里挑四个影响最大的关键点展开。4.1 提示词工程的设计模板先写结构再写参数最后写示例我在项目里反复迭代出的一个经验是提示词不是“让模型听懂”而是“让模型拿到所有决策信息后按固定流程输出”。我通常把每条复杂的提示词分成五个部分角色定义、任务描述、输入数据、输出约束、输出示例。举个例子AI批改的提示词结构如下你是《计算机基础》课程阅卷助教具有严格、公正的评分风格。 任务根据给定的题目、标准答案和评分细则对学生的作答进行评分。 输入 题目{{question}} 标准答案{{answer}} 评分细则{{rubric}} 学生作答{{studentAnswer}} 输出约束 1. 仅输出JSON不要输出任何解释性文字。 2. 得分必须是数字范围在0到{{maxScore}}之间。 3. 扣分点必须明确引用学生作答中的具体表述。 输出示例 {score: 8, deductions: [{point: 缺少对事务隔离级别的说明, deductedScore: 2}], comments: 总体回答完整但事务隔离级别部分表述不清晰}。这部分工作没有多高深的技术含量但非常考验耐心。项目进行到一半时我总结出一个原则提示词里的“变量”数量不要超过7个变量太多模型会顾此失彼。如果某条提示词变量数量超过10个拆分任务或减少输入信息效果通常会有明显提升。4.2 结构化输出与Token截断的博弈结构化输出虽然使用方便但它会让模型消耗更多token来生成JSON结构。在业务设计里需要考虑一个容易被忽略的问题模型输出长度受maxTokens限制如果回答内容过大结构化输出会被截断后端在反序列化时直接抛异常。针对这个问题项目组设计了三级防线。第一把maxTokens设置到足够大但同时设置合理的超时时间避免因为等待长输出而占满线程池第二在解析环节捕获转换异常并做降级处理——如果是批改模块返回“AI建议生成失败请教师人工批改”的提示而不是让请求直接报500第三大输出内容尽量改成流式输出并增量渲染前端看到的是逐步生成的内容即使内容较长也不会感知到等待很久。在设计阶段我专门做过一次压测单次批改请求的平均token消耗大约在2000到3000之间回答内容约600到900字。把这个数据乘以批改份数和调用次数才真正理解了为什么“AI应用的瓶颈往往不在模型能力而在成本”。后面成本控制部分会重点展开。4.3 幂等、重试与熔断AI接口也必须按分布式服务的要求来做大模型API的本质是远程服务它可能出现网络抖动、限流、超时以及偶发的返回格式异常。我们不能因为“它是AI”就放低对它的稳定性要求。项目里在AI调用层统一做了三件事。第一件事是增加重试机制但重试策略不是简单的“失败就重试”。模型API返回限流状态码或超时时采用指数退避重试最大次数设为2避免对供应商产生过大压力。第二件事是熔断降级如果连续多次调用失败AI编排服务直接短路返回一个固定的降级结果例如智能组卷模块会回退到Step 2的“纯随机抽题”逻辑保证考试系统的主流程不受AI服务波动影响。第三件事是幂等控制AI调用不是天然幂等的批改任务在重试时可能造成重复扣费因此所有AI调用在发出去之前先检查Redis里的任务状态处理完成后写入已完成标记重复请求直接返回旧结果。这一层代码虽然不多但很值得。上线后真实遇到过一次模型供应商服务异常那时候熔断策略把影响范围控制在AI助教这一个模块在线考试主流程完全未受影响。如果当初没有做这层防护一个上游AI服务抖动整个系统可能都瘫了。4.4 成本与并发控制实测一口价和按量包之间的取舍在真实项目里AI成本往往比预期高得多。组卷这个高频场景尤其明显教师每次点“预览试卷”都会触发一次模型调用。测试阶段因为调用量不大没有发现模拟真实使用时50个教师同时组卷预览一个上午就能消耗几百万token。最终我做了两个调整。一是大量使用“缓存 异步预生成”。同一套组卷策略可以缓存生成结果限定缓存有效期为6小时如果教师使用完全相同的条件再次预览则直接返回缓存。二是把非实时性AI任务放到消息队列里异步处理。学情分析报告允许管理员点击后异步生成生成完通过站内消息通知教师查看不占用用户的同步请求时间。成本控制的一个有效手段是限制“无效输出”。模型输出的JSON越短、字数越少成本越低。我们在提示词里加上一句话“只输出JSON不要附加任何解释。”看似简单实际却能节约大约20%的token消耗。不要小看这20%按学期峰值调用量来计算这是一笔非常可观的开支。5. 踩坑实录从联调到上线的细节清单最后这部分是碎片化的踩坑总结每一条都是从真实联调中整理出来的放在一起可以作为一张“避坑速查表”使用。5.1 流式输出与WebSocket的配合别让浏览器卡在“缓冲”第一个坑出现在AI助教的打字机效果。最初实现时用的是普通的HTTP请求配合response body流式读取。实测中发现某些浏览器对SSE的支持会有缓冲延迟前端收不到逐字效果只会在全部内容到达后一次性渲染。后来改成WebSocket通道服务端一次一条消息推送前端实时渲染体验问题才解决。Spring AI的流式API本身很好用但流式API在网关层有时会被缓冲排查时需要确认从Spring Boot到前端之间有Nginx时Nginx的proxy_buffering是否关闭。这是一个非常不起眼但能卡住项目一两天的坑。5.2 同一套接口兼容多模型配置项要先抽象开发期我们频繁切换模型。最初代码里直接硬编码了模型名称和温度参数每次切模型都要改三四处代码。后来把所有AI模型相关参数抽到配置中心通过环境变量动态指定。像这样设计抽象后联调阶段从Chat模型切到更强的推理模型只改了application.yml里的模型名和endpoint业务代码没有做任何改动。Spring AI的一大好处是模型名称是标准化配置项。只要你选定的模型厂商提供了OpenAI兼容协议或者有Spring AI官方适配切换成本就非常低。这也是当初选型时的一个重要加分项。5.3 评测反馈闭环模型答得好不好必须用数据说话项目做到后期我发现团队对AI效果的评价一直停留在“感觉很好”层面。后来花了一周时间搭建了一个简单的评测集从真实题库和真实学生作答中抽取了200道题给每道题标注了“标准答案”和“教师评分”然后在每次模型版本调整后跑一遍这200个测试用例比对模型给出的批改分数与教师评分之间的一致率。这个环比数字从此成为AI模块发版的标准门槛。评测集这件事强烈建议项目一开始就做不要等模块完成后再补。模型提示词微调的影响往往在单个例子上看不出来而在批量评测集上能看到明显波动。没有数据基线团队内部关于“AI效果好坏”的争论会永远停留在直觉层。条条框框说了不少最后再回到这个项目本身讲两句。通过这次设计我最大的体会是Spring AI确实让AI能力接入Java后端变得更加规范和顺畅搞定了模型调用方式、结构化输出和流式响应等基础设施层的问题。但它没有解决“在考试系统中AI只能做辅助决策不能做终审执行”这个业务设计问题也没有解决成本预判、评测闭环和监控告警这些工程化问题。真正的难点从来不是“怎么调大模型”而是“怎么让AI稳定地为业务产生确定性价值”。这个定位想清楚选型框架就不再是风险反而能加速整个系统的落地这也是我在下一个AI项目中仍然愿意继续使用Spring AI的原因。