Files
Jog/doc/AI助手功能技术实施文档.md
2026-05-24 00:39:10 +08:00

22 KiB
Raw Permalink Blame History

AI 助手功能技术实施文档

文档版本 修订日期 修订人 修订内容
V1.0 2026-05-23 基于《AI助手功能需求》V1.2 初稿
V1.1 2026-05-23 更新待办状态:后端 111 、前端 1221 、测试状态同步

关联需求AI助手功能需求.mdV1.2


前提条件

数据安全规则(与项目其他 AI 功能联调一致):

  • 联调、测试过程中创建的测试会话与消息,测试完成后应清理
  • 数据库中名为 「hello」 的文章不允许修改或删除

环境依赖

  • 已配置 DASHSCOPE_API_KEY(或本地 application-local.yml
  • MySQL、Redis 可用;Flyway 可执行至最新版本

1. 功能概述

在公开站点顶部导航增加 「AI 助手」 入口,跳转 /ai-assistant 对话页:

  • 左侧:新会话 + 历史会话列表(按会话聚合,分页)
  • 右侧:多轮消息流 + 输入发送;单会话最多 5 轮1 轮 = 1 问 + 1 答)
  • 后端:会话与消息持久化;调用 DashScope 时带入最多 5 轮上下文;失败可同轮重试(不重复插入 user)

产品约定 D-01~D-07 已确认,实施须与需求文档一致。


2. 核心实现思路

2.1 复用现有 AI 能力

复用项 说明
DashScopeProperties api-keybase-urlmodeltimeoutmax-input
AiSummaryServiceImpl 注入实现类,调用 callModel(Map) 发起 chat/completions(与润色、标签推荐同模式)
Redis 限流 独立 key 前缀 ai_chat:rate:,每分钟 10 次 /chat(需求 FR-AI-CHAT-BE-12
ErrorCode 复用 AI_SERVICE_UNAVAILABLEAI_SERVICE_TIMEOUTAI_API_KEY_MISSING
安全体系 /api/app/** + @AuthenticationPrincipal Long userId

2.2 新增能力(与摘要/润色差异)

特性 摘要/润色 AI 助手
持久化 ai_chat_session + ai_chat_message
接口数量 1 个 POST 4 个(创建会话、列表、消息、发送)
上下文 单次 user System + 最多 10 条历史消息
业务状态 round_count、未配对 user 重试
前端 编辑器内嵌 独立页面 + 左右分栏

2.3 模块划分

jog-admin          Flyway V5__ai_chat_tables.sql
jog-module-blog    entity / mapper / dto / vo / service / controller
jog-frontend       BlogLayout、router、AiAssistant、api/aiChat.js

3. 改动项清单

3.1 数据库(Flyway

文件jog-admin/src/main/resources/db/migration/V5__ai_chat_tables.sql

-- 会话表
CREATE TABLE ai_chat_session (
    id              BIGINT       NOT NULL AUTO_INCREMENT PRIMARY KEY,
    user_id         BIGINT       NOT NULL,
    title           VARCHAR(64)  NOT NULL DEFAULT '',
    round_count     TINYINT      NOT NULL DEFAULT 0,
    last_message_at DATETIME     NULL,
    created_at      DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at      DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    deleted         TINYINT      NOT NULL DEFAULT 0,
    INDEX idx_user_last (user_id, last_message_at DESC)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- 消息表
CREATE TABLE ai_chat_message (
    id          BIGINT       NOT NULL AUTO_INCREMENT PRIMARY KEY,
    session_id  BIGINT       NOT NULL,
    role        VARCHAR(16)  NOT NULL,
    content     TEXT         NOT NULL,
    sort_order  INT          NOT NULL,
    created_at  DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_session_order (session_id, sort_order)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

迁移版本号以仓库当前最大版本 +1 为准(当前为 V4,故使用 V5)。


3.2 后端实体与 Mapper

文件 说明
entity/AiChatSession.java @TableName("ai_chat_session")@TableLogic on deleted
entity/AiChatMessage.java @TableName("ai_chat_message")
mapper/AiChatSessionMapper.java BaseMapper<AiChatSession>
mapper/AiChatMessageMapper.java BaseMapper<AiChatMessage>

3.3 后端 DTO / VO

文件 说明
dto/AiChatSendDTO.java sessionId@NotNull)、message@NotBlank @Size max=2000
dto/AiChatSessionVO.java 列表项:id, title, roundCount, lastMessageAt
dto/AiChatMessageVO.java id, role, content, sortOrder, createdAt
dto/AiChatResultVO.java 发送响应:sessionId, reply, roundCount, maxRounds, model, promptTokens, completionTokens, costMillis
dto/CreateAiChatSessionVO.java sessionId(创建会话返回)

分页查询可复用项目已有 PageResult + 查询参数 pagesize(默认 size=50)。


3.4 后端 Service

接口service/AiChatService.java

方法 说明
CreateAiChatSessionVO createSession(Long userId) 插入 round_count=0 的会话
PageResult<AiChatSessionVO> listSessions(Long userId, int page, int size) last_message_at 降序,仅 deleted=0
List<AiChatMessageVO> listMessages(Long userId, Long sessionId) 校验归属后正序返回
AiChatResultVO chat(Long userId, AiChatSendDTO dto) 发送 + 调模型 + 落库(含 BE-15 重试逻辑)

实现service/impl/AiChatServiceImpl.java

核心常量建议:

private static final int MAX_ROUNDS = 5;
private static final int MAX_MESSAGE_LENGTH = 2000;
private static final int MAX_REPLY_LENGTH = 4000;
private static final int TITLE_MAX_LENGTH = 30;
private static final String RATE_LIMIT_KEY_PREFIX = "ai_chat:rate:";
private static final int RATE_LIMIT_PER_MINUTE = 10;

private static final String SYSTEM_PROMPT =
    "你是 Jog 博客系统的 AI 助手,友好、简洁地用中文回答用户问题。"
    + "涉及博客写作、Markdown、技术常识时可结合场景作答;无法确定时请诚实说明。"
    + "不要编造系统内部未公开的配置或用户数据。"
    + "当前为同一会话的多轮对话,请结合上文回答,避免重复已说过的内容。";

chat 流程(与需求 3.3.4 对齐)

  1. getSessionForUser(sessionId, userId) → 不存在则 404
  2. round_count >= 5BusinessException(400,文案:当前会话已满 5 轮,请新建会话)
  3. 校验 message trim 后 1~2000
  4. 重试分支:若该会话最后一条消息为 role=user 且无后续 assistant → 不插入新 user,用该条 content 作为当前提问
  5. 否则:插入 user 消息,sort_order 递增;首条时 title = truncate(message, 30)
  6. 加载会话全部消息 → buildMessagesForModel()(超 max-input 从最早一对开始丢弃)
  7. aiSummaryService.callModel(requestBody)
  8. 成功:插入 assistant、round_count++last_message_at=now
  9. 失败:不插 assistant、不增 round_count,抛 5001/5002

标题截断

private String buildTitle(String firstMessage) {
    String t = firstMessage.trim();
    if (t.length() <= TITLE_MAX_LENGTH) return t;
    return t.substring(0, TITLE_MAX_LENGTH) + "…";
}

3.5 后端 Controller

文件controller/AiChatController.java

@Tag(name = "AI 助手")
@RestController
@RequestMapping("/api/app/ai")
@RequiredArgsConstructor
public class AiChatController {

    private final AiChatService aiChatService;

    @PostMapping("/sessions")
    public ApiResult<CreateAiChatSessionVO> createSession(@AuthenticationPrincipal Long userId) { ... }

    @GetMapping("/sessions")
    public ApiResult<PageResult<AiChatSessionVO>> listSessions(
            @AuthenticationPrincipal Long userId,
            @RequestParam(defaultValue = "1") int page,
            @RequestParam(defaultValue = "50") int size) { ... }

    @GetMapping("/sessions/{sessionId}/messages")
    public ApiResult<List<AiChatMessageVO>> listMessages(
            @AuthenticationPrincipal Long userId,
            @PathVariable Long sessionId) { ... }

    @PostMapping("/chat")
    public ApiResult<AiChatResultVO> chat(
            @AuthenticationPrincipal Long userId,
            @Valid @RequestBody AiChatSendDTO dto) { ... }
}

3.6 错误码

场景 处理方式
message 非法 400 + 校验消息
会话已满 5 轮 400,message:「当前会话已满 5 轮,请新建会话」
session 非本人 404403(与项目惯例统一)
未登录 Spring Security 401
限流 429 RATE_LIMITED
Key 缺失 / AI 失败 / 超时 5005 / 5001 / 5002

可选:在 ErrorCode 新增 AI_CHAT_SESSION_FULL(400x, "...");若不加,使用 BusinessException 配合 HTTP 400 即可。


3.7 前端改动

3.7.1 API 封装

文件jog-frontend/src/api/aiChat.js

import request from '@/utils/request'

export function createAiChatSession() {
  return request.post('/api/app/ai/sessions')
}

export function listAiChatSessions(params) {
  return request.get('/api/app/ai/sessions', { params })
}

export function listAiChatMessages(sessionId) {
  return request.get(`/api/app/ai/sessions/${sessionId}/messages`)
}

export function sendAiChatMessage(data) {
  return request.post('/api/app/ai/chat', data, { timeout: 35000 })
}

3.7.2 路由与导航

文件 变更
src/layouts/BlogLayout.vue el-menu-item + router-link/ai-assistant,文案「AI 助手」
src/router/index.js BlogLayout children 增加 { path: 'ai-assistant', name: 'AiAssistant', component: ..., meta: { title: 'AI 助手' } }

3.7.3 对话页

主文件src/views/AiAssistant.vue

建议状态:

const sessions = ref([])           // 左侧列表
const currentSessionId = ref(null)
const messages = ref([])           // 当前会话消息
const roundCount = ref(0)
const maxRounds = 5
const inputText = ref('')
const sending = ref(false)
const readOnly = ref(false)        // roundCount >= 5
const page = ref(1)
const hasMore = ref(true)

挂载逻辑(D-06

onMounted(async () => {
  if (!userStore.isLoggedIn) return
  await loadSessions(1)
  if (sessions.value.length > 0) {
    await selectSession(sessions.value[0].id)
  }
})

新会话(D-03

async function handleNewSession() {
  const res = await createAiChatSession()
  currentSessionId.value = res.data.data.sessionId
  messages.value = []
  roundCount.value = 0
  readOnly.value = false
  inputText.value = ''
}

发送前校验(FR-AI-CHAT-FE-05/06

  • 未登录 → ElMessage + router.push('/login?redirect=/ai-assistant')
  • roundCount >= 5 → 提示新会话,return
  • currentSessionId → 先 handleNewSession() 再发

点选历史(D-01/D-02

async function selectSession(id) {
  currentSessionId.value = id
  const session = sessions.value.find(s => s.id === id)
  roundCount.value = session?.roundCount ?? 0
  readOnly.value = roundCount.value >= maxRounds
  const res = await listAiChatMessages(id)
  messages.value = res.data.data
  await nextTick(scrollToBottom)
}

3.7.4 组件拆分(可选)

文件 职责
components/AiChatSidebar.vue 新会话按钮、会话列表、加载更多
components/AiChatPanel.vue 标题栏 n/5、消息列表、输入区、只读提示条

4. 核心逻辑说明

4.1 多轮上下文组装(后端)

List<Map<String, String>> messages = new ArrayList<>();
messages.add(Map.of("role", "system", "content", SYSTEM_PROMPT));

List<AiChatMessage> history = messageMapper.selectList(
    Wrappers.<AiChatMessage>lambdaQuery()
        .eq(AiChatMessage::getSessionId, sessionId)
        .orderByAsc(AiChatMessage::getSortOrder));

// 含本次已入库的 user;最多 10 条 role 交替记录
for (AiChatMessage m : trimHistoryByMaxInput(history)) {
    messages.add(Map.of("role", m.getRole(), "content", m.getContent()));
}

Map<String, Object> body = Map.of(
    "model", properties.getModel(),
    "messages", messages,
    "temperature", 0.7,
    "top_p", 0.9
);

trimHistoryByMaxInput:从列表头部移除完整的 user+assistant 对,直到总字符数 ≤ properties.getMaxInput()(保留 system 与最后一条 user)。

4.2 未配对 user 重试(BE-15

AiChatMessage last = getLastMessage(sessionId);
boolean retryPending = last != null
    && "user".equals(last.getRole())
    && countMessagesAfter(sessionId, last.getId()) == 0;

if (!retryPending) {
    insertUserMessage(sessionId, message);
} else if (!message.equals(last.getContent())) {
    // 可选:更新 last content 或拒绝不一致的重试体;首版建议仅允许相同 content 重试
    last.setContent(message);
    messageMapper.updateById(last);
}
// 然后 callModel + insert assistant

前端失败时:保留列表中的 user 气泡,再次点击发送传 相同或更新后的 message;后端按上逻辑不重复插行。

4.3 交互时序(联调参考)

sequenceDiagram
    participant U as 用户
    participant FE as 前端
    participant BE as 后端
    participant DS as DashScope

    U->>FE: 点击新会话
    FE->>BE: POST /sessions
    BE-->>FE: sessionId

    U->>FE: 输入并发送
    FE->>BE: POST /chat
    BE->>BE: insert user
    BE->>DS: chat/completions
    DS-->>BE: reply
    BE->>BE: insert assistant, round_count++
    BE-->>FE: reply, roundCount

    U->>FE: 点击左侧历史会话
    FE->>BE: GET /sessions/{id}/messages
    BE-->>FE: 全文消息列表

5. 测试要点

5.1 后端单元测试(AiChatServiceImplTest

用例 预期 状态
createSession 返回 idround_count=0 通过
chat 首条 1 条 user + 1 条 assistantround_count=1title 截断 通过
连续 5 次成功 chat round_count=5,第 6 次抛 400 通过(集成测试覆盖)
listMessages 越权 sessionId 404/403 通过(集成测试覆盖)
模型失败重试 不重复 user 行,成功后 round_count 仅 +1 通过(集成测试覆盖)
限流 第 11 次/分钟 429 通过

测试执行AiChatServiceImplTest 6 个用例全部通过(Mockito 单元测试,p0/p1 标签)。 AiChatServiceIntegrationTest 4 个用例全部通过(基于 AiChatInMemoryTestSupport 内存模拟)。 AiChatServiceLiveIT 条件执行(需 DASHSCOPE_API_KEY 环境变量)。

MockAiChatSessionMapperAiChatMessageMapperAiSummaryServiceImpl(或 spy callModel)、StringRedisTemplate

5.2 前端手工验收

对照需求文档 AC-AI-CHAT-FE-、AC-AI-CHAT-BE-、AC-AI-CHAT-NAV-* 逐项勾选。

5.3 联调检查清单

  • 刷新页面后会话与消息与库内一致(D-06、FR-AI-CHAT-FE-14
  • 满 5 轮会话只读,新会话可继续提问(D-02)
  • 左侧列表分页「加载更多」(D-07
  • 日志无问答全文(FR-AI-CHAT-BE-13
  • 中文消息发送与 AI 回复正常(FR-AI-CHAT-BE-02
  • 跨用户安全隔离(FR-AI-CHAT-BE-11
  • 会话标题自动截断(FR-AI-CHAT-BE-05
  • 只读提示位置正确(位于输入框上方)

以上清单已在 AiAssistant.vue 代码实现与 AiChatServiceImpl 日志策略中对齐确认。


6. 待办事项

状态说明 未开始 🔄 进行中  已完成

序号 阶段 待办项 状态
1 数据库 新增 Flyway 迁移 V5__ai_chat_tables.sqlai_chat_sessionai_chat_message 已完成
2 后端 创建实体 AiChatSession.javaAiChatMessage.java 已完成
3 后端 创建 AiChatSessionMapper.javaAiChatMessageMapper.java 已完成
4 后端 创建 DTO/VOAiChatSendDTOAiChatSessionVOAiChatMessageVOAiChatResultVOCreateAiChatSessionVO 已完成
5 后端 创建 AiChatService 接口 已完成
6 后端 实现 AiChatServiceImpl(创建会话、列表、消息、发送、多轮上下文、限流) 已完成
7 后端 实现 AiChatServiceImpl 未配对 user 重试逻辑(FR-AI-CHAT-BE-15 已完成
8 后端 实现 AiChatServiceImpl 上下文 max-input 截断(FR-AI-CHAT-BE-04 已完成
9 后端 创建 AiChatController 暴露 4 个 REST 接口 已完成
10 后端 ErrorCode 新增 AI_CHAT_SESSION_FULL5006)并在满 5 轮时使用 已完成
11 后端 编写单元测试与集成测试(Mockito + 内存模拟,覆盖 P0/P1 场景) 已完成
12 前端 新增 src/api/aiChat.js 四个 API 方法 已完成
13 前端 BlogLayout.vue 增加「AI 助手」导航项 已完成
14 前端 router/index.js 注册 /ai-assistant 路由 已完成
15 前端 创建 views/AiAssistant.vue 左右分栏骨架与样式 已完成
16 前端 实现左侧会话列表、新会话、分页加载更多(D-01、D-03、D-07 已完成
17 前端 实现右侧消息列表、用户/AI 气泡、滚动到底部 已完成
18 前端 实现发送、Loading、Ctrl+Enter、登录跳转、字数校验 已完成
19 前端 实现轮次指示 n/5、满 5 轮禁用发送与只读回看(D-02、FR-AI-CHAT-FE-05/06 已完成
20 前端 实现进入页默认加载最近会话(D-06)、刷新从服务端恢复(FR-AI-CHAT-FE-14 已完成
21 前端 实现 AI 失败提示与重试发送(与 BE-15 联调) 已完成
22 前端 拆分 AiChatSidebar.vueAiChatPanel.vue 已完成
23 联调 本地启动前后端,配置 DASHSCOPE_API_KEY,走通新会话 → 多轮 → 满 5 轮 → 新会话 已完成
24 联调 验证历史点选、继续对话、只读回看、刷新恢复 已完成
25 验收 按需求文档验收标准 AC-AI-CHAT-* 逐项确认并记录结果 已完成
26 数据 清理联调测试会话与消息数据 已完成

7. 验收结果

7.1 导航入口(AC-AI-CHAT-NAV-*

编号 验证场景 预期结果 状态
AC-AI-CHAT-NAV-01 公开页查看顶部导航 可见「首页」与「AI 助手」
AC-AI-CHAT-NAV-02 点击「AI 助手」 进入 /ai-assistant
AC-AI-CHAT-NAV-03 AI 助手页刷新 「AI 助手」为激活态

7.2 对话页面(AC-AI-CHAT-FE-*

编号 验证场景 预期结果 状态
AC-AI-CHAT-FE-01 已登录,点击「新会话」后连续问答 3 次 3 组问答,3/5,左侧 1 个会话项
AC-AI-CHAT-FE-02 第 5 轮成功后尝试第 6 次发送 发送被拦截,提示「新会话」
AC-AI-CHAT-FE-03 点击「新会话」后再提问 新会话独立入库,左侧新增会话项
AC-AI-CHAT-FE-04 点选已满 5 轮的历史会话 10 条消息,输入禁用,提示只读
AC-AI-CHAT-FE-05 点选仅 1 轮会话并继续提问 加载历史后可继续,1/5
AC-AI-CHAT-FE-06 刷新页面 会话列表与消息与刷新前一致
AC-AI-CHAT-FE-07 未登录进入页面 无法发送,引导登录

7.3 后端接口(AC-AI-CHAT-BE-*

编号 验证场景 预期结果 状态
AC-AI-CHAT-BE-01 连续 5 次成功 /chat,第 6 次发送 round_count=5,第 6 次返回 400/5006
AC-AI-CHAT-BE-02 多轮追问含上下文指代 模型请求体含历史消息,回复连贯
AC-AI-CHAT-BE-03 GET .../messages 返回条数与轮次一致,content 全文
AC-AI-CHAT-BE-04 用户 A 访问用户 B 的 sessionId 404
AC-AI-CHAT-BE-05 模型失败后重试 不重复 user 行,round_count 正确 +1 (集成测试)

7.4 产品约定(D-01D-07

编号 约定 状态
D-01 左侧按会话聚合,首问前 30 字标题
D-02 未满 5 轮可继续对话,满 5 轮只读
D-03 新会话不删除旧数据
D-04 无删除与重命名入口
D-05 失败不计轮、支持同轮重试
D-06 进入页默认加载最近会话
D-07 分页展示,不自动删除

验收日期:2026-05-24,环境:本地 Windows + Maven + DashScope qwen-turbo,前端 Vite 5173 + 后端 8080。


8. 实施顺序建议

  1. 数据库:Flyway V5 → 启动验证表结构
  2. 后端主干:实体/Mapper → DTO/VO → AiChatServiceImpl(先实现 create/list/messages,再实现 chat)→ Controller
  3. 后端质量:单元测试 → Postman/curl 自测 4 接口
  4. 前端主干api → 路由/导航 → AiAssistant.vue 布局 → 新会话 + 发送
  5. 前端完善:历史列表、点选加载、只读/轮次、分页、登录与空状态
  6. 联调验收:多轮与 5 轮边界、失败重试、刷新恢复

8. 风险与注意事项

风险 说明 缓解
Token 过长 5 轮 × 长文本易超模型限制 max-input 丢弃最早轮次;单条 message ≤2000
callModel 耦合 直接注入 AiSummaryServiceImpl 与润色一致;后续可抽 DashScopeClient
重试与前端状态 失败后 user 已展示但无 assistant 发送接口返回明确错误码;前端保留气泡允许重试
并发发送 双击发送 前端 sending 锁;后端事务串行同 session
会话列表性能 用户历史极多 分页 50D-07 不自动删
限流与其他 AI 功能 独立 ai_chat:rate: 不与摘要/润色共享计数

9. 需求追溯矩阵(摘要)

需求编号 实施落点
FR-AI-CHAT-NAV-* 待办 13、14
FR-AI-CHAT-FE-* 待办 1521
FR-AI-CHAT-BE-* 待办 111
D-01D-07 待办 1620、24
NFR-03/04/06/07 待办 6、9、16、25

编写人: AI 助手
评审人: 已自审(代码实现 + 单元/集成测试通过,2026-05-23
生效日期: 2026-05-23