山东大学软件学院项目实训-创新实训-计科智伴(四)—— 后端第四周:智能互动 + 练习模块
前三周做完认证、画像、学习计划后这周开始做智能互动和练习模块。这个模块要对接AI、要做题目推荐和自动批改还要和画像联动更新掌握度比前几周的纯CRUD要稍复杂一些。今天把这周的工作和代码细节一起记下来。一、这周干了什么在前三周的基础上这周主要完成了智能互动与练习模块。按之前的接口设计这个模块包含四个核心功能发起智能问答、获取练习题、提交练习答案、获取专项练习。此外还顺便把聊天记录持久化做完了这样用户的历史问答可以追溯。这次设计的几个关键点复用项目原有的Question实体题干从关联的LearningResource联查获取不破坏原有表结构按知识点和难度从题库拉取题目过滤掉已经做过的题客观题直接比对答案主观题调用AI生成解析练习提交后自动更新画像中的知识点掌握度和薄弱点列表接口和表结构这边本周新增了chat_history、user_question_record两张表以及AiChatRequest/Response、ExerciseDTO、SubmitAnswerResponse等一批DTO。二、大模型选型与配置智能互动模块离不开大语言模型。我们选择阿里云通义千问作为底层模型通过Spring AI框架集成。配置文件如下spring: ai: openai: base-url: https://dashscope.aliyuncs.com/compatible-mode api-key: ${OPENAI_API_KEY} chat: options: model: qwen-max-latest embedding: options: model: text-embedding-v3 dimensions: 1024qwen-max-latest通义千问最新最强版本适合复杂问答、结构化输出、答案解析生成。text-embedding-v3用于后续语义检索RAG1024维向量能较好地支撑知识库问答。使用DashScope兼容模式的好处是OpenAI的API调用方式几乎不变只用改base-url和model代码层面无侵入。三、核心接口设计练习模块共开发了以下接口代码主要在ExerciseController里接口方法功能/ai/chatPOST智能问答支持文本pdf文件流式返回/exercise/nextPOST获取下一道/一组练习题/exercise/{exerciseId}/submitPOST提交答案并接收批改结果/exercise/targetedPOST根据薄弱点获取专项练习所有接口都加了登录校验userId从UserHolder的ThreadLocal里拿。多模态暂仅实现pdf四、获取练习题 —— 按画像推荐4.1 核心流程获取练习题入口是getNextExercises方法用户传入想练习的知识点和难度后端根据画像和未做题目前按需返回如果前端指定了knowledgePoint就按指定知识点拉取如果没指定用画像中的weakPoints薄弱知识点优先推荐难度动态调整根据画像中用户整体准确率算难度——准确率低于60%用基础难度60%-80%用中等难度高于80%用高级难度查询Question表时关联user_question_record表过滤掉用户已经做过的题目避免重复4.2 题干的特殊设计项目原有的Question实体只存了kpId知识点ID、qType、difficulty、answer、analysis题干实际存在LearningResource表里通过resourceId关联。所以DTO组装时得联查两张表private ExerciseDTO convertToExerciseDTO(Question question) { LearningResource resource learningResourceService.getById(question.getResourceId()); return ExerciseDTO.builder() .exerciseId(question.getQId()) .questionText(resource ! null ? resource.getContent() : ) .resourceUrl(resource ! null ? resource.getFileUrl() : ) .knowledgePoint(getKnowledgePointName(question.getKpId())) // ... .build(); }五、提交答案 —— 批改 画像联动5.1 批改逻辑提交答案的入口是submitAnswer方法流程如下根据exerciseId拿到题目信息联查LearningResource拿到题干原文比对用户答案和标准答案当前是字符串全等匹配后续可以扩展为AI辅助评分记录答题记录到user_question_record表如果答错了调用通义千问qwen-max-latest生成解析返回批改结果// 简化的答案比对 private boolean checkAnswer(String userAnswer, String correctAnswer) { return userAnswer.trim().equalsIgnoreCase(correctAnswer.trim()); } // 答错时用AI生成解析使用配置好的ChatClient实际调用qwen-max-latest private String generateExplanation(...) { String prompt String.format( 请为以下题目生成解析\n题目%s\n用户答案%s\n正确答案%s..., questionText, userAnswer, correctAnswer ); return chatClient.prompt().user(prompt).call().content(); }5.2 画像联动掌握度更新这是本周的一个核心联动点。练习模块不能只返回对错还得反映到学情画像上。我在UserProfileServiceImpl里补充了两个方法updateKnowledgeMastery(Long userId, String knowledgePoint, double delta)—— 增量更新某个知识点的掌握度updateWeakPoints(Long userId, String knowledgePoint)—— 将某个知识点加入薄弱点列表调用时机用户提交答案后如果答对了就小幅提升掌握度答错了就显著降低掌握度并确保该知识点在weakPoints中。// 在提交答案后调用 if (isCorrect) { userProfileService.updateKnowledgeMastery(userId, knowledgePoint, 0.05); } else { userProfileService.updateKnowledgeMastery(userId, knowledgePoint, -0.1); userProfileService.updateWeakPoints(userId, knowledgePoint); }这样画像里的知识掌握度会随着用户的练习动态变化后续的计划生成、题目推荐都会更准确。六、智能问答 —— 对接通义千问支持流式多模态这周也把智能问答的基础架子搭了。因为项目已经集成了Spring AI和之前的通义千问配置实现起来不算太复杂。现在文本问答走ChatClient调用qwen-max-latest模型返回结构化回答同时存入chat_history表方便以后做RAG检索。// 调用通义千问 qwen-max-latest String prompt 你是一个学习助手。请回答以下问题并以JSON格式输出包含字段answer简短答案explanation详细解释knowledgePoint所属知识点relatedQuestions最多3个suggestion学习建议。问题 question; String response chatClient.prompt().user(prompt).call().content(); // 解析JSON存入ChatHistory之所以选择qwen-max-latest是因为它对中文理科题目的理解能力强且支持结构化输出。后续也可以根据成本调整模型为qwen-plus或qwen-turbo。最终实现的效果支持纯文本问答支持上传pdf文件支持流式输出打字机效果自动管理会话记忆每个chatId独立上下文消息自动持久化到数据库4.1 核心代码解读几个关键点Converse 记忆CHAT_MEMORY_CONVERSATION_ID_KEY是 Spring AI 内置的会话记忆 ID只要同一个chatId多次调用模型会自动带上历史对话不需要手动拼接上下文。多模态Spring AI 的Media对象封装了文件和 MIME 类型传给spec.user()后通义千问会自动识别图片或音频内容。流式返回FluxString配合前端 EventSource 或 fetch就能实现类似 ChatGPT 的打字机效果。会话与消息存储sessionService只保存会话元信息会话 ID、类型、创建时间实际的消息内容由 Spring AI 的PersistentChatMemory自动保存。在这个版本里我没有手动保存消息因为使用了CHAT_MEMORY_CONVERSATION_ID_KEYSpring AI 的内存管理会负责存储历史。但为了后续检索和展示聊天记录我单独建了message表在需要时手动存入可以另加一个异步方法。你当前的代码中其实没有保存消息不过这不是大问题因为内存已经能维持上下文。后面如果需要做历史记录翻页再补充即可。4.2 多模态文件处理暂未完整实现上传的图片会被暂存在内存中Spring AI 自动转换成 base64 或临时 URL 发给模型。为了长期存储用户的图片/音频文件我调用了minioService.uploadFile将文件保存到 MinIO并可以关联到消息记录。// 在另一个方法中保存用户消息时上传文件到 MinIO String fileUrl minioService.uploadFile(file, CURRENT_USER_ID, chatId);这样用户上传的习题照片、手写笔记都能永久保存后续可以用于错题本或复习。4.3 会话记忆是如何工作的Spring AI 内置了ChatMemory接口我们依赖注入时没有显式配置框架默认使用了InMemoryChatMemory它把每个chatId的对话存在本地内存。生产环境可以考虑替换为RedisChatMemory支持多实例共享。只要在每次请求时传入同样的chatId模型就能记住之前的对话。例如用户什么是死锁 AI死锁是...回答 用户那如何避免 ← 不需要重复说“死锁”模型知道在问什么这个能力对连续问答非常有用。七、专项练习 —— 薄弱点突破专项练习接口getTargetedExercises的逻辑比普通练习更聚焦直接取前端传入的薄弱点列表或画像中的weakPoints按知识点拉题每个薄弱点抽1-2道组一套练习给用户集中突破。这个功能是错题本和前面积累的薄弱点数据打通的用户在首页点击“薄弱点专项训练”就能直接进入。八、踩坑与心得这周也遇到几个值得记下来的问题1. 题干到底存哪的坑项目原来Question里没有题干字段题干在LearningResource里。刚开始写DTO转换时没注意直接question.getQuestionText()拿空值查了半天才发现要联查。解决方案是写了一个convertToExerciseDTO方法每次取题都自动把LearningResource的内容带出来。2. 题库重复推荐如果用户做过的题不记录下来/exercise/next会反复推荐同一道题。解决方案是在selectExercisesSQL里加了一个条件uqr.question_id IS NULL过滤掉已做过的并且在每次提交后往user_question_record表记录。3. JSONB字段解析报错UserProfileServiceImpl里解析knowledgeMastery和weakPoints时入参类型是Object因为数据库存的JSONB字段映射到Java是Object但解析方法之前写的是接收String。改成了兼容处理如果是String就正常解析如果是Map/List就直接转换其他情况尝试转成JSON再解析。if (knowledgeMasteryObj instanceof String) { return objectMapper.readValue((String) knowledgeMasteryObj, typeRef); } else if (knowledgeMasteryObj instanceof Map) { return (MapString, Double) knowledgeMasteryObj; } else { String json objectMapper.writeValueAsString(knowledgeMasteryObj); return objectMapper.readValue(json, typeRef); }这样不管MyBatis-Plus返回什么类型都能正确解析画像里的掌握度数据。4. 大模型API限流与错误处理阿里云DashScope有并发限制如果调用频繁会返回限流错误。后续需要在调用处增加重试机制和降级规则例如限流时返回预设的默认回答。当前版本只做了简单的异常捕获这个问题会在下周优化。5. 流式响应下如何存储完整消息FluxString是边生成边发送给前端后端无法轻易拿到完整的 AI 回复。如果想存消息记录可以使用collectList()或者另起一个异步监听。目前版本没有强制存储因为 Spring AI 的内存记忆已经能满足多轮对话。后续如果需要查看历史聊天记录再考虑实现消息持久化。九、总结一周时间从两张新表、一批DTO到整套接口跑通。模块四的交互比前三个模块更复杂因为它涉及到通义千问模型调用、画像联动、题库查重等多个环节。目前文本问答和普通练习已经可以正常使用了专项练习接口也已经打通。下一步计划补充上传手写题图片的多模态问答基于qwen-vl模型及AI对编程题、主观题的智能评分。