写在前面

这是"计科智伴"项目实训系列的第六篇博客。在前五篇中,我们完整记录了从项目立项、技术选型、基础架构搭建,到前后端联调与核心功能攻坚的全过程。

如果说之前的努力是为了让项目"跑起来",那么本篇博客所记录的,则是让项目"跑得稳、跑得好"的关键一步。我们将聚焦于博客五之后新增的几个核心模块与系统级优化:RAG知识库的完整实现SSE流式通信的稳定性保障语音输入功能的集成数据库版本化管理实践以及题库的系统化建设。这些工作标志着项目从"功能可用"向"生产就绪"的重要跨越。


一、RAG知识库模块完整实现

1.1 需求背景

AI对话虽能回答通用问题,却难以触及特定课程教材、PPT、笔记等私有学习资料。用户期望AI在回答时能"言之有物",引用具体的课程资料,提供更精准、更有依据的解答。这正是构建检索增强生成(RAG,Retrieval-Augmented Generation)系统的初衷。

1.2 架构设计

知识库模块采用经典的三层架构,职责分明:

前端展示层(科目导航 + 资料浏览)
    ↓
后端服务层(CRUD + 搜索 + 推荐)
    ↓
AI集成层(自动分类 + 检索增强 + 资料来源推送)

1.3 数据库设计

新增 knowledge_base表,核心设计是利用PostgreSQL的 JSONB类型存储灵活标签,并对科目、知识点、类型等高频查询字段建立1.4 后端实现

Controller层提供五个核心接口:科目统计、分页查询、详情获取、全文搜索、个性化推

1.5 前端实现

知识库首页采用科目网格 + 推荐列表的布局。科目卡片以4列网格展示图标、名称和资料数量;推荐卡片通过不同颜色的类型标签(教材/PPT/习题等)和难度标签,让信息一目了然。

1.6 AI集成

我们做了两件事来让知识库"活"起来。

自动分类与摘要:新增 AiClassificationService,资料入库时自动调用大模型提取科目、章节、类型、标签和摘要,实现资料的自动化治理。

检索增强对话:改造 AiChatServiceImpl,用户开启知识库模式时,先检索相关资料注入提示词,再将资料来源以卡片形式推送给用户。


二、SSE流式通信稳定性保障

2.1 问题背景

在实际使用中,SSE通信暴露出三个稳定性问题:AI生成长文时的超时无响应、网络波动导致的连接断连、以及各类错误的提示模糊

2.2 超时控制机制

核心思路是设置60秒超时阈值,但每次收到数据块时重置计时器,避免生成长文本时被误判超时。

2.3 断连重连机制

实现自动重试,最多重试2次,每次间隔3秒,仅对超时和网络错误重试。

2.4 错误分类提示

SSE解析器区分 delta(增量内容)、done(正常结束)、error(服务端错误)、cancelled(取消)等事件类型,分别处理。


三、语音输入功能集成

3.1 需求背景

移动端用户更习惯语音输入,尤其是长问题或复杂术语的场景。语音输入可以大幅提升移动端使用体验。

3.2 录音管理

录音管理器同时支持小程序端和Web端。Web端使用Web Audio API录制标准WAV格式(Sophnet语音识别API不支持浏览器默认的webm格式),小程序端使用 uni.getRecorderManager()

3.3 后端语音识别服务

后端调用Sophnet异步语音转写API,先上传文件创建任务获取 taskId,再轮询查询结果,最多等待30秒。

public String transcribe(MultipartFile file, String fileName) {
    String taskId = createTranscriptionTask(file, fileName);
    return pollTranscriptionResult(taskId, 30);
}

private String pollTranscriptionResult(String taskId, int maxWaitSeconds) {
    int elapsed = 0;
    while (elapsed < maxWaitSeconds) {
        Thread.sleep(1000);
        elapsed++;
        SophnetResultResponse response = restTemplate.exchange(url, GET, entity, SophnetResultResponse.class).getBody();
        if ("success".equalsIgnoreCase(response.status)) {
            return response.results.get(0).transcripts.get(0).text;
        } else if ("failed".equalsIgnoreCase(response.status)) {
            throw new RuntimeException("转写失败: " + response.errorMsg);
        }
    }
    throw new RuntimeException("转写超时");
}

3.4 页面交互

语音模式下长按按钮录音,松开结束。录音时按钮变为紫色渐变背景,麦克风图标添加脉冲动画。


四、数据库版本化管理实践

4.1 问题背景

项目初期数据库脚本管理混乱:重复备份文件、脚本无执行顺序、不具备幂等性、缺乏文档说明。

4.2 版本化迁移方案

参考Flyway命名规范,将迁移脚本版本化:

V1__init_courses_and_kp.sql
V2__alter_user_profile.sql
V3__user_sign.sql

命名规范V{版本号}__{描述}.sql,版本号递增确保执行顺序。

4.3 幂等性设计

所有迁移脚本可重复执行,不会产生副作用。

4.4 脚本结构与执行文档

最终脚本目录结构如下:

db/
├── README.md
├── V1__init_courses_and_kp.sql
├── V2__alter_user_profile.sql
├── V3__user_sign.sql
├── insert_*.sql (20个)
├── supplement_*.sql (4个)
├── add_notification_settings.sql
└── myapp_db_clean_backup_20260616.sql

README.md记录执行顺序和验证SQL:

SELECT COUNT(*) FROM course;          -- 应返回 21
SELECT COUNT(*) FROM knowledge_point; -- 应返回 102
SELECT COUNT(*) FROM question;        -- 应返回 510+

五、题库系统化建设

5.1 建设目标

项目涵盖21门计算机核心课程,目标是每门课程都有完整题库,支持知识前测和课后练习。具体要求:21门课程全部关联知识点,每个知识点至少5道题,覆盖不同难度和题型。

5.2 知识点补充

查询发现16门课程没有关联知识点,编写SQL脚本为每门课程补充5个核心知识点,知识点总数从22个增加到102个。

5.3 题目批量插入

为每个知识点生成5道题目,共400道新题目。题目格式包含选项(JSONB)、类型、难度、答案和解析。

5.4 题目数据分布

科目

知识点数

题目数

高等数学、线性代数、概率论等21门课程

每科5个

每科25道

总计

102

510+

5.5 题目质量把控

题目来源包括教材课后习题、历年期末考试题、考研真题和在线题库。审核标准为:选项明确无歧义、答案唯一正确、解析详细、难度分级合理。


六、其他关键修复

6.1 答题计时器修复

问题:答题完成后计时器未停止,导致时间重复计算。

修复:提交答案后清除计时器。

6.2 任务完成状态同步修复

问题:前端标记任务完成后,刷新页面状态恢复为未完成。根因是前端调用了 PUT /tasks/{taskId}/status接口(只更新状态字段),而非 POST /tasks/{taskId}/complete接口(同时设置 completedAt和知识掌握度)。

修复:区分完成任务和普通状态更新,完成任务时调用专用接口,并用后端返回的最新数据同步前端。

6.3 任务详情智能化优化

问题:任务详情弹窗仅展示基础信息,缺乏学习引导和操作入口。

优化:将 uni.showModal替换为自定义底部弹出式弹窗,新增"问AI伴学"功能。点击后跳转到AI对话页,自动发送包含任务类型、标题、科目、练习题数量的预填消息。

6.4 流式消息渲染优化

问题:AI流式响应过程中,消息气泡出现白框(有背景色但无内容)。

根因:渲染节流机制导致 renderedHtml更新滞后,rich-text渲染空节点时气泡背景仍存在。

修复:流式过程中只累积原始文本不渲染HTML,流式结束后统一用完整内容渲染Markdown。

6.5 历史消息渲染修复

问题:刷新页面后,历史消息中的AI回复显示空白。

根因:自定义 codespan渲染器生成 <code>标签,uni-app的 <rich-text>解析失败导致整个HTML渲染异常。

修复:移除 codespan自定义渲染器,让marked使用默认渲染方式。


七、知识库体验全面升级

7.1 全局搜索能力打通

问题:首页搜索栏仅为占位展示,科目标签、热力图知识点不可点击搜索。

修复:搜索栏改为可输入框,支持关键词搜索。科目卡片点击后传递中文名到列表页筛选。首页元素添加 @click.stop搜索联动。

后端科目筛选从精确匹配改为模糊匹配:

if (query.getSubject() != null && !query.getSubject().isEmpty()) {
    wrapper.like(KnowledgeBase::getSubject, query.getSubject());
}

7.2 列表页搜索体验优化

搜索进入列表页时,顶部显示黄色提示栏展示当前关键词,提供一键清除功能。筛选下拉框在有选中值时显示蓝色激活态。资料卡片升级为带图标、标签、来源信息的完整卡片。

7.3 骨架屏加载动效

数据加载期间展示骨架屏占位动画,使用呼吸闪烁和流光扫过两种动效。

7.4 科目卡片自适应优化

使用 aspect-ratio: 1 / 1确保正方形比例,文字字号缩小至20rpx,支持最多2行显示,超出部分省略。网格间距从20rpx缩小至12rpx,提升空间利用率。

7.5 统一色彩体系

为科目卡片引入8种循环渐变配色,资料类型图标使用独立渐变。页面背景改为三段渐变,搜索栏添加主题色阴影。

7.6 向AI提问跳转与资料上下文传递

问题chat页面是tabBar页面,navigateTo跳转报错。

修复:改为 switchTab跳转,通过 uni.setStorageSync传递资源上下文。

goToChat() {
    const content = this.detail?.content?.replace(/<[^>]+>/g, '').substring(0, 500) || ''
    uni.setStorageSync('chatAutoResource', {
        subject: this.detail.subject,
        title: this.detail.title,
        contentPreview: content
    })
    uni.switchTab({ url: '/pages/chat/chat' })
}

Chat页面在 onShow中检测资源上下文,自动构造提问消息并发送。


八、个人主页样式优化

8.1 优化思路

在不改变任何功能的前提下,通过添加底纹装饰、优化色彩搭配、增强视觉层次来提升页面美感。核心原则是"轻装饰"——底纹透明度控制在0.04-0.15之间,不干扰内容阅读。

8.2 页面底纹装饰

页面背景使用三段渐变(浅蓝→浅灰→浅蓝),叠加8个装饰元素:大圆装饰(径向渐变半透明圆形)、小圆点、菱形(旋转45度边框)。

8.3 头部区域与统计卡片

头部用户信息区域使用紫色渐变背景,添加浮动装饰元素(圆形边框、旋转方形、小圆点)。统计卡片使用 ::before::after伪元素添加径向渐变光晕。

8.4 菜单图标渐变优化

为每个功能入口的图标添加独立渐变背景和阴影,使用与功能语义相关的颜色:蓝色(个人信息)、靛蓝(学习计划)、橙色(错题本)、翠绿(学情报告)、灰色(设置)。


九、会话生命周期管理优化

9.1 问题背景

此前用户退出AI对话页面后再次进入,会自动恢复上一次会话,导致无法快速开始新对话,不符合大多数AI产品的交互习惯。

9.2 优化方案

页面卸载时清空会话状态,页面进入时保持空态(欢迎页)。用户可通过"历史"按钮手动选择历史会话,发送第一条消息时自动创建新sessionId。

// Store层
resetSession() {
    this.messages = []
    this.conversationId = ''
    this.isStreaming = false
    this.streamingContent = ''
    this.streamingMsgId = ''
    this.historyPage = 1
    this.hasMoreHistory = true
    this.persistLastSession('')
}

// 页面生命周期
onUnload() {
    this.chatStore.resetSession()
    uni.$off('voiceRecognized')
}

十、练习系统多题型支持

10.1 问题背景

此前练习系统仅支持选择题。题库建设中导入了大量填空题和简答题,前端缺乏对应的交互组件。

10.2 题型体系

系统支持四种题型:单选题(选项列表单选)、多选题(选项列表多选)、填空题(单行输入框)、简答题(多行文本域)。

10.3 前端实现

根据题型动态切换交互组件:

<!-- 选择题 -->
<view v-if="questionType === 'single' || questionType === 'multiple'" class="options-section">
  <view v-for="option in currentQuestion.options" :key="option.label" 
        class="option-item" @click="selectAnswer(option.label)">
    {{ option.content }}
  </view>
</view>

<!-- 填空题 -->
<view v-else-if="questionType === 'fill'">
  <input v-model="fillAnswer" placeholder="请输入你的答案..." @confirm="submitFillAnswer" />
</view>

<!-- 简答题 -->
<view v-else-if="questionType === 'judge' || questionType === 'short'">
  <textarea v-model="fillAnswer" placeholder="请输入你的详细答案..." maxlength="500" />
</view>

10.4 后端答案比对优化

支持填空题的模糊匹配:精确匹配、包含匹配、多答案匹配(用 /|分隔)。

private boolean checkAnswer(String userAnswer, String correctAnswer) {
    String u = normalizeAnswer(userAnswer);
    String c = normalizeAnswer(correctAnswer);
    if (u.equalsIgnoreCase(c)) return true;
    if (c.length() > 2 && u.contains(c)) return true;
    for (String possible : c.split("[/|]")) {
        if (u.equalsIgnoreCase(possible.trim()) || u.contains(possible.trim())) return true;
    }
    return false;
}

十一、App端跨端适配

11.1 问题背景

项目基于uni-app开发,最初主要在H5和小程序端运行。打包为Android App时遇到大量跨端兼容性问题,导致首页白屏、智能问答无法使用。

11.2 Markdown渲染引擎替换

App端的JS引擎不支持 \p{L}\p{N}等Unicode属性转义语法,markedv18启动时抛出 SyntaxError。尝试降级到v14、v10均无效,最终替换为 markdown-it

import MarkdownIt from 'markdown-it'
const md = new MarkdownIt({ html: true, breaks: true, linkify: true })

export function renderMarkdown(text) {
    if (!text) return ''
    try { return md.render(text) }
    catch (e) { return text.replace(/</g, '&lt;').replace(/\n/g, '<br/>') }
}

11.3 SSE流式通信桥接

App端不支持 uni.requestenableChunked模式,使用 renderjs+ XMLHttpRequest桥接。通信链路为:逻辑层 → bridgeData → AppSseBridge组件 → renderjs层 → XHR发起SSE请求 → onprogress接收数据 → dispatchEvent回传逻辑层。

关键修复:Content-Type重复设置导致后端500错误,修复为统一由headers对象控制。

// 修复前:重复设置
xhr.setRequestHeader("Content-Type", "application/json")
Object.keys(headers).forEach(key => xhr.setRequestHeader(key, headers[key]))

// 修复后:统一由headers控制
Object.keys(headers).forEach(key => {
    if (headers[key]) xhr.setRequestHeader(key, headers[key])
})

11.4 Polyfill补充

App环境缺少部分现代API,补充 String.prototype.replaceAllObject.fromEntries的polyfill。


十二、多模态图片识别问答

12.1 需求背景

用户经常遇到需要拍照提问的场景:不会做的数学题、看不懂的代码截图、复杂的图表等。纯文本输入无法满足这些需求。

12.2 架构设计

前端选择图片后通过 uni.uploadFile上传,后端进行图像预处理(缩放/压缩),调用多模态AI模型识别,返回结构化结果(识别文本、科目、知识点、答案、置信度)。

12.3 前端实现

图片选择支持相册和相机,使用压缩模式减少上传体积。发送时检测是否有图片,有则走多模态接口,无则走SSE流式接口。

async sendMessage() {
    if (this.selectedImage) {
        this.chatStore.addUserMessage(this.inputText || '图片提问', this.selectedImage)
        await this.chatStore.sendMultimodalMessage(this.selectedImage, this.inputText)
    } else {
        this.chatStore.addUserMessage(this.inputText)
        await this.chatStore.sendChatMessage(this.inputText)
    }
}

12.4 置信度提示

AI模型返回 confidence字段(0-1),低于0.7时前端自动追加提示。

if (data.confidence < 0.7) {
    const tip = `\n\n⚠️ 提示:图片识别置信度较低(${Math.round(data.confidence * 100)}%),结果可能不够准确。`
    msg.content += tip
}

总结

本篇博客记录了项目从"功能可用"向"生产就绪"跨越的关键工作:

  1. RAG知识库:实现了从资料爬取、AI自动分类到检索增强对话的完整链路,让AI回答有据可依

  2. SSE稳定性:通过超时控制、断连重连、错误分类,将流式通信的可靠性提升到生产级别

  3. 语音输入:集成移动端语音识别,提供多模态输入方式

  4. 数据库版本化:参考Flyway规范,实现可重复、可追溯的数据库迁移管理

  5. 题库建设:系统化构建102个知识点、510+道题目的完整题库,覆盖21门核心课程

  6. 任务详情智能化:将任务详情从"信息展示"升级为"学习引导入口",新增"问AI伴学"功能

  7. 流式消息渲染优化:解决流式过程中白框问题和历史消息渲染空白问题

  8. 知识库体验升级:全局搜索能力打通、骨架屏加载动效、科目卡片自适应、统一色彩体系

  9. App端跨端适配:替换markdown-it解决Unicode正则不兼容、renderjs+XHR桥接SSE流式通信

  10. 多模态图片识别:实现图片上传+AI识别问答,支持置信度提示

这些工作虽然不像核心功能那样引人注目,但它们是系统稳定性和用户体验的基石。下一步将重点关注性能优化、测试覆盖和部署自动化,让项目真正具备生产环境的能力。

Logo

一站式 AI 云服务平台

更多推荐