# AI 流式问答与出题接口 - 前端使用指南 > **适用范围**:知识库问答、会话管理、题目生成、答案批改 > **Base URL**:`http://localhost:8080/api` > **认证方式**:`Authorization: Bearer ` > **接口版本**:v2.17.0 > **更新时间**:2026-06-05 --- ## 目录 1. [快速开始](#一快速开始) 2. [AI 流式问答接口](#二ai-流式问答接口) 3. [AI 会话管理接口](#三ai-会话管理接口) 4. [题目生成接口](#四题目生成接口) 5. [题目批改接口](#五题目批改接口) 6. [答题记录接口](#六答题记录接口) 7. [前后端字段对照表](#七前后端字段对照表) 8. [常见问题 FAQ](#八常见问题-faq) 9. [附录:cURL 完整示例](#九附录curl-完整示例) --- ## 一、快速开始 ### 1.1 认证流程 所有接口(登录接口除外)均需携带 JWT Token: ```http POST /api/auth/login Content-Type: application/json { "username": "admin", "password": "123456" } ``` **响应**: ```json { "code": 200, "data": { "token": "eyJhbGciOiJIUzUxMiJ9...", "user": { "id": 1, "username": "admin", "role": 1 } } } ``` 后续所有接口请求头需添加: ``` Authorization: Bearer ``` ### 1.2 接口总览 | 模块 | 接口 | 方法 | 功能 | |------|------|------|------| | **流式问答** | `/ai/chat/stream` | `POST` | 发起 AI 流式问答 | | **中断问答** | `/ai/chat/stop?sessionId=xxx` | `POST` | 中断流式回答 | | **会话列表** | `/ai/sessions?page=1&pageSize=10` | `GET` | 获取当前用户的会话列表 | | **会话详情** | `/ai/session/{sessionId}` | `GET` | 获取指定会话的消息记录 | | **消息引用** | `/ai/message/{messageId}/references` | `GET` | 获取某条消息的引用列表 | | **消息文件来源** | `/ai/message/{messageId}/sources` | `GET` | 获取某条消息引用的文件来源(去重) | | **会话引用** | `/ai/session/{sessionId}/references` | `GET` | 获取会话的全部引用 | | **删除会话** | `/ai/session/{sessionId}` | `DELETE` | 删除单个会话及相关数据 | | **批量删除会话** | `/ai/sessions` | `DELETE` | 批量删除多个会话 | | **生成题目** | `/exam/generate` | `POST` | 根据文档生成题目(异步) | | **生成试卷** | `/exam/paper/generate` | `POST` | 动态生成试卷 | | **开始考试** | `/exam/paper/{paperId}/start` | `POST` | 获取试卷题目 | | **考试结果** | `/exam/record/{recordId}/result` | `GET` | 获取批阅结果 | | **正式考试批改** | `/exam/record/{recordId}/grade` | `POST` | 正式考试提交并批改 | | **批改答案** | `/exam/grade` | `POST` | 批改学生答案(通用) | | **查询答题记录** | `/exam/answers/query` | `POST` | 查询用户答题记录 | | **考试记录列表** | `/exam/records?type=practice` | `GET` | 获取考试/练习记录列表 | --- ## 二、AI 流式问答接口 ### 2.1 发起流式问答 **接口**:`POST /api/ai/chat/stream` **Content-Type**:`application/json` **返回类型**:`text/event-stream` (Server-Sent Events) #### 请求体 | 字段 | 类型 | 必填 | 说明 | |------|------|------|------| | `message` | string | ✅ | 用户提问内容 | | `session_id` | string | ❌ | 会话ID,不传则自动生成;传空字符串 `""` 也会自动生成 | | `id` | string | ❌ | 自定义请求ID,用于追踪 | **请求示例**: ```json { "message": "请介绍一下知识库管理系统", "session_id": "sess_1780410472658_9qg9a9hhp" } ``` #### 内部逻辑说明 - 后端自动获取用户可访问的知识库(`collections`),无需前端传递 - 后端自动从数据库加载该会话的历史消息作为上下文 - 流式返回过程中,finish 事件到达时会自动保存: - 用户提问 → `context_message`(role=user) - AI 回答 → `context_message`(role=assistant,含 answer、images 等) - 引用切片 → `context_reference`(自动从 RAG 获取 preview 字段) - 图片 → `context_image`(保留 score 字段) #### 流式事件类型 | 事件 type | 说明 | 是否入库 | |-----------|------|----------| | `start` | 对话开始事件 | ❌ | | `connected` | RAG 服务连接成功 | ❌ | | `thinking` | AI 思考中 | ❌ | | `searching` | 知识库检索中 | ❌ | | `sources` | 检索到的候选来源信息 | ❌ 仅前端展示,**不写入数据库** | | `chunk` | 流式内容片段 | ❌ 前端拼接显示 | | `result` / `finish` | 最终完整结果(最重要) | ✅ 存入数据库 | | `error` | 错误信息 | ❌ | #### finish 事件核心字段 ```json { "type": "finish", "answer": "根据【参考资料】...完整的AI回答内容,其中包含 [ref:文档名_86] 这样的引用标记。", "duration_ms": 75512, "citations": [ { "chunk_id": "文档名_86", "chunk_index": 86, "source": "文档名.docx", "collection": "dept_1_kb", "doc_type": "other", "section": "3.1 权重计算公式", "chunk_type": "text", "page": 1, "score": 0.838, "preview": "【系统自动从 RAG 接口获取的目标切片完整正文】", "content": { "stem": "切片摘要内容", "data": {} } } ], "images": [ { "image_id": "49d2910b.jpg", "path": "/api/image/49d2910b.jpg/data", "source": "来源文档.docx", "page": 1, "chunk_type": "image", "description": "图片描述", "score": 0.85, "collection": "dept_1_kb" } ] } ``` > **重要说明**: > - `answer` 中的 `[ref:chunk_id]` 标记:前端需解析为可点击链接,点击后跳转/展示对应引用信息 > - `preview` 字段:后端写入 `context_reference` 表时,会自动调用 RAG 预览接口 `GET /documents/{collection}/{source}/preview?chunk_index=N&context=0` 填充该字段;若接口报错,则填入错误信息 > - `context_message.answer` 字段保留完整的 `[ref:chunk_id]` 标记文本 #### answer 字段引用标记解析 前端需解析 `[ref:chunk_id]` 为可点击元素: ```text "市场状态 = Σ(二级指标均值 × 权重)[ref:文档名_86]。 此外,一级指标合计权重不低于70%[ref:文档名_93]。" ``` #### 前端 JavaScript 调用示例(fetch + ReadableStream) ```javascript // 发起流式问答 async function chatStream(message, sessionId, token) { const response = await fetch('/api/ai/chat/stream', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token }, body: JSON.stringify({ message, session_id: sessionId }) }); const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = ''; let fullAnswer = ''; while (true) { const { value, done } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const lines = buffer.split('\n'); buffer = lines.pop() || ''; // 保留未完成的行 for (const line of lines) { if (!line.startsWith('data:')) continue; try { const data = JSON.parse(line.slice(5).trim()); handleEvent(data); // 分发到事件处理器 } catch (e) { console.warn('解析失败:', line); } } } } // 事件处理器(前端自行实现 UI 更新) function handleEvent(data) { switch (data.type) { case 'chunk': // 实时拼接显示到对话框 fullAnswer += data.content; // renderAnswer(fullAnswer); break; case 'finish': // 对话完成,拿到完整 answer、citations、images console.log('完整回答:', data.answer); console.log('引用列表:', data.citations); console.log('图片列表:', data.images); break; case 'error': console.error('AI 错误:', data.message); break; } } ``` > **为什么不用 EventSource**:EventSource 只能 GET 请求,且无法自定义请求头(无法传 Token)。推荐使用 `fetch + ReadableStream` 方式。 ### 2.2 中断流式问答 **接口**:`POST /api/ai/chat/stop?sessionId=sess_xxx` **响应**: ```json { "success": true, "message": "已中断AI回答" } ``` --- ## 三、AI 会话管理接口 > 统一响应结构(`Map`): > ```json > { > "success": true, > "data": [...], > "total": 10, > "page": 1, > "pageSize": 10, > "pages": 1 > } > ``` ### 3.1 获取用户会话列表 **接口**:`GET /api/ai/sessions?page=1&pageSize=10` **查询参数**: | 参数 | 类型 | 必填 | 默认值 | 说明 | |------|------|------|--------|------| | `page` | int | ❌ | 1 | 页码 | | `pageSize` | int | ❌ | 10 | 每页数量 | **响应 data 元素字段**(ChatSession): | 字段 | 类型 | 说明 | |------|------|------| | `id` | long | 数据库主键 | | `sessionId` | string | 会话唯一ID | | `userId` | long | 用户ID | | `createTime` | datetime | 创建时间 | ### 3.2 获取会话详情(消息记录) **接口**:`GET /api/ai/session/{sessionId}?page=1&pageSize=20` **权限检查**:后端自动校验该会话是否属于当前用户 **响应 data 元素字段**(ChatMessage): | 字段 | 类型 | 说明 | |------|------|------| | `id` | long | 消息ID | | `conversationId` | long | 会话ID(数据库主键) | | `role` | string | `user` / `assistant` | | `content` | string | 用户提问原文(role=user)或 AI 回答内容(role=assistant 时通常与 answer 一致) | | `messageType` | string | `rag` 等 | | `isFinished` | int | 1=完成 | | `answer` | string | AI 完整回答(含 `[ref:chunk_id]` 标记),仅 role=assistant 时有值 | | `images` | string(JSON数组) | 图片列表(JSON 字符串,需前端解析) | | `durationMs` | long | 耗时(毫秒) | | `createTime` | datetime | 创建时间 | > **注意**:`ChatMessage` 实体中已**移除 `sources` 字段**。引用信息需通过 `/ai/message/{messageId}/references` 接口单独获取。 ### 3.3 获取消息引用列表 **接口**:`GET /api/ai/message/{messageId}/references?docName=xxx` **查询参数**: | 参数 | 类型 | 必填 | 说明 | |------|------|------|------| | `docName` | string | ❌ | 按文档名称过滤引用 | **响应 data 元素字段**(ChatReference): | 字段 | 类型 | 说明 | |------|------|------| | `id` | long | 引用记录ID | | `messageId` | long | 关联的消息ID | | `chunkId` | string | 切片唯一标识 `{docName}_{chunkIndex}` | | `chunkIndex` | int | 切片序号 | | `collection` | string | 向量库名称 | | `docName` | string | 文档名称 | | `docType` | string | 文档类型 | | `page` | int | 页码 | | `section` | string | 章节/段落位置 | | `chunkType` | string | `text` / `image` / `table` | | `excerpt` | string | 切片摘要内容 | | **`preview`** | string | **目标切片完整正文(系统自动从 RAG 获取,失败则填错误信息)** | | `score` | decimal | 相关性评分 0-1 | | `createTime` | datetime | 创建时间 | ### 3.4 获取消息文件来源列表(去重) **接口**:`GET /api/ai/message/{messageId}/sources` **用途**:对同一条消息的引用按文档名做去重汇总,用于展示"参考文档列表"侧栏 **响应 data 元素字段**: | 字段 | 类型 | 说明 | |------|------|------| | `docName` | string | 文档名称(去重后的唯一标识) | | `docType` | string | 文档类型 | | `referenceCount` | int | 该文档被引用的次数 | | `pages` | string | 页码范围,如 `"5"` 或 `"1-10"` | **响应示例**: ```json { "success": true, "data": [ { "docName": "薪酬制度.docx", "docType": "other", "referenceCount": 5, "pages": "1-8" }, { "docName": "考核办法.docx", "docType": "other", "referenceCount": 3, "pages": "3" } ], "total": 2 } ``` ### 3.5 获取会话全部引用 **接口**:`GET /api/ai/session/{sessionId}/references` **用途**:返回整个会话中所有 AI 回答的引用汇总,字段与单条消息引用接口一致 ### 3.6 删除单个会话 **接口**:`DELETE /api/ai/session/{sessionId}` 删除指定会话及其所有消息和引用数据。后端自动校验会话所属权。 ### 3.7 批量删除会话 **接口**:`DELETE /api/ai/sessions` **请求体**: ```json ["sess_xxx1", "sess_xxx2", "sess_xxx3"] ``` **响应**: ```json { "success": true, "message": "会话批量删除成功", "deletedCount": 3 } ``` --- ## 四、题目生成接口 > 本模块接口统一响应结构(`Result`): > ```json > { > "code": 200, > "message": "操作成功", > "data": { ... } > } > ``` ### 4.1 生成题目(异步) **接口**:`POST /api/exam/generate` **Content-Type**:`application/json` #### 请求体字段 | 字段 | 类型 | 必填 | 说明 | |------|------|------|------| | `file_id` | Long | ⭐二选一 | 文件ID,系统自动从 `knowledge_file` 表查询向量库信息(**推荐**) | | `file_path` | string | ⭐二选一 | 文件路径(如 `dept_1_kb/薪酬制度.docx`) | | `collection` | string | file_path 必填 | 向量库名称(使用 file_path 时必须提供) | | `question_types` | object | ❌ | 指定题型及数量;不传则使用 AI 智能生成模式 | | `question_scores` | object | ❌ | 自定义题型分数配置 | | `difficulty` | int | ❌ | 难度等级 1-5,默认 3 | | `request_id` | string | ❌ | 请求ID,用于幂等性 | #### question_types 可选值 | 题型 | key | 说明 | |------|-----|------| | 单选题 | `single_choice` | 四个选项单选 | | 多选题 | `multiple_choice` | 四个选项多选 | | 判断题 | `true_false` | 对/错两选 | | 填空题 | `fill_blank` | 按空格填写答案,支持同义词 | | 简答题 | `subjective` | 开放回答,由 LLM 评分 | #### 请求示例:指定题型模式 ```json { "file_id": 1, "question_types": { "single_choice": 3, "multiple_choice": 2, "true_false": 2, "fill_blank": 2, "subjective": 1 }, "difficulty": 3, "request_id": "gen_req_001" } ``` #### 请求示例:智能生成模式(推荐) 不提供 `question_types` 参数时,系统调用 AI 智能出题接口,自动根据文档内容判断题型和数量: ```json { "file_id": 1, "difficulty": 3 } ``` #### 响应示例(成功) ```json { "code": 200, "message": "收到请求", "data": { "requestId": "gen_req_001", "fileId": 1, "fileMatchStatus": "success", "matchedCollection": "dept_1_kb", "documentName": "薪酬制度.docx", "collectionFilled": true, "asyncTaskStarted": true, "nextStep": "请通过文件状态接口查询生成进度", "status": "success" } } ``` #### 响应示例(文件验证失败) ```json { "code": 400, "message": "文件不存在,请确认 file_id 是否正确", "data": { "status": "error", "errorType": "FILE_VALIDATION_ERROR", "errorDetail": "文件不存在", "suggestion": "请检查 file_id 是否正确、文件是否已删除、或文件是否已完成向量化" } } ``` **fileMatchStatus 可能值**: | 值 | 说明 | HTTP 状态码 | |----|------|------------| | `success` | 文件验证通过,启动异步任务 | 200 | | `FILE_NOT_FOUND` | 文件不存在 | 400 | | `FILE_DELETED` | 文件已被删除 | 400 | | `NOT_VECTORIZED` | 文件尚未完成向量化 | 400 | | `PARAM_MISSING` | 参数不完整 | 400 | | `INTERNAL_ERROR` | 服务器内部错误 | 500 | ### 4.2 轮询题目生成进度 **接口**:`GET /api/file/{fileId}` **关键字段**: | 字段 | 说明 | |------|------| | `processStatus` | 文件处理状态(如 `INDEXED`) | | `examStatus` | **题目生成状态**:`UNGENERATED` / `GENERATING` / `GENERATED` / `FAILED` | | `processMessage` | 生成详情,包含题目数量和 AI 分析结果 | **examStatus 流转**: ``` UNGENERATED ──► GENERATING ──► GENERATED │ └──────► FAILED ``` **轮询策略建议**:初始间隔 2-3 秒;超时 5-10 分钟;examStatus 达到终态则停止。 ### 4.3 生成试卷 **接口**:`POST /api/exam/paper/generate` **请求体**: ```json { "single_choice_count": 5, "multiple_choice_count": 3, "true_false_count": 2, "fill_blank_count": 2, "subjective_count": 1, "difficulty": 3, "include_personal": false, "file_ids": [1, 2, 3] } ``` **响应 data 字段**(`ExamPaperGenerateResponse`): | 字段 | 类型 | 说明 | |------|------|------| | `paperId` | string | 试卷ID | | `paperTitle` | string | 试卷标题 | | `totalScore` | decimal | 总分 | | `questionCount` | int | 题目总数 | | `generatedAt` | datetime | 生成时间 | | `questions` | array | 题目列表 | --- ## 五、题目批改接口 ### 5.1 批改答案(日常练习 / 模拟考 / 正式考试通用) **接口**:`POST /api/exam/grade` **请求体字段**: | 字段 | 类型 | 必填 | 说明 | |------|------|------|------| | `request_id` | string | ❌ | 请求ID | | `type` / `answerType` | string | ❌ | `practice` / `mock_exam` / `formal_exam` | | `record_id` | string | ❌ | 正式考试记录ID;提供时走正式考试提交/批改分支 | | `paper_id` | string | ❌ | 试卷ID | | `session_id` | string | ❌ | 会话ID | | `answers` | array | ✅ | 答案列表 | **answers 数组元素字段**: | 字段 | 类型 | 必填 | 说明 | |------|------|------|------| | `question_id` | string | ✅ | 题目ID | | `question_type` | string | ✅ | `single_choice` / `multiple_choice` / `true_false` / `fill_blank` / `subjective` | | `question_content` | object | ✅ | 题目内容(含 stem / options / answer 等) | | `student_answer` | any | ✅ | 学生提交的答案 | | `max_score` | number | ✅ | 满分 | **请求示例**: ```json { "request_id": "grade_req_001", "answer_type": "practice", "answers": [ { "question_id": "q_001", "question_type": "single_choice", "question_content": { "stem": "根据公司规定,员工薪资由哪几部分组成?", "data": { "options": [ { "key": "A", "content": "基本工资+奖金" }, { "key": "B", "content": "基本工资+绩效奖金+津贴补贴" } ] }, "answer": "B" }, "student_answer": "B", "max_score": 5.0 }, { "question_id": "q_002", "question_type": "multiple_choice", "question_content": { "stem": "以下哪些属于绩效奖金的评定因素?", "data": { "options": [...] }, "answer": ["A", "B", "D"] }, "student_answer": ["A", "B"], "max_score": 4.0 }, { "question_id": "q_003", "question_type": "true_false", "question_content": { "stem": "公司规定员工每月绩效奖金上限为工资的20%。", "answer": "F" }, "student_answer": "T", "max_score": 2.0 }, { "question_id": "q_004", "question_type": "fill_blank", "question_content": { "stem": "员工薪资由___、___和___三部分组成。", "data": { "blank_count": 3 }, "answer": [["基本工资"], ["绩效奖金", "绩效"], ["津贴补贴", "补贴"]] }, "student_answer": ["基本工资", "绩效奖金", "交通补贴"], "max_score": 6.0 }, { "question_id": "q_005", "question_type": "subjective", "question_content": { "stem": "请简述公司薪酬制度的核心原则。", "data": { "scoring_points": [ { "point": "公平性", "weight": 0.3 }, { "point": "激励性", "weight": 0.3 }, { "point": "竞争力", "weight": 0.2 }, { "point": "合法性", "weight": 0.2 } ] }, "answer": "公司薪酬制度遵循公平、激励、竞争、合法四大原则..." }, "student_answer": "我认为公司的薪酬制度应该公平公正...", "max_score": 10.0 } ] } ``` **响应 data 字段**(`ExamGradeResponse`): | 字段 | 类型 | 说明 | |------|------|------| | `success` | boolean | 是否成功 | | `totalScore` | decimal | 得分 | | `totalMaxScore` | decimal | 满分 | | `scoreRate` | decimal | 得分率(百分比) | | `results` | array | 每题批改结果 | **results 元素字段**: | 字段 | 类型 | 说明 | |------|------|------| | `questionId` | string | 题目ID | | `questionType` | string | 题型 | | `score` | decimal | 本题得分 | | `maxScore` | decimal | 本题满分 | | `correct` / `isCorrect` | boolean | 是否答对 | | `studentAnswer` | any | 学生答案 | | `correctAnswer` | any | 正确答案 | | `feedback` | string | 批改反馈 | **批阅规则**: | 题型 | 批阅方式 | 得分规则 | |------|----------|----------| | 单选题 | 精确匹配 | 正确得满分,错误得 0 | | 多选题 | 集合比对 | 全对满分,少选一半,错选得 0 | | 判断题 | 精确匹配 | 正确得满分,错误得 0 | | 填空题 | 多答案匹配 | 每空独立评分,支持同义词匹配 | | 简答题 | LLM 评分 | 根据得分点评分,不超过 max_score | ### 5.2 正式考试提交并批改 **接口**:`POST /api/exam/record/{recordId}/grade` **路径参数**:`recordId`(正式考试记录ID) **请求体**(`ExamSaveRequest`): ```json { "answers": [ { "question_id": "q_001", "question_type": "single_choice", "answer": "B" } ] } ``` --- ## 六、答题记录接口 ### 6.1 开始考试(获取题目) **接口**:`POST /api/exam/paper/{paperId}/start` 系统自动查找或创建考试记录,返回试卷题目内容。 ### 6.2 获取考试结果 **接口**:`GET /api/exam/record/{recordId}/result` **权限检查**:后端校验该记录是否属于当前用户 **响应 data 字段**: | 字段 | 类型 | 说明 | |------|------|------| | `recordId` | string | 记录ID | | `paperId` | string | 试卷ID | | `paperTitle` | string | 试卷标题 | | `status` | string | `submitted` 等 | | `totalScore` | decimal | 得分 | | `totalMaxScore` | decimal | 满分 | | `scoreRate` | decimal | 得分率 | | `submitTime` | datetime | 提交时间 | | `answers` | array | 每题结果 | ### 6.3 查询用户答题记录 **接口**:`POST /api/exam/answers/query` **请求体**(`UserAnswerQueryRequest`): | 字段 | 类型 | 必填 | 说明 | |------|------|------|------| | `paper_id` | string | ⭐二选一 | 按试卷ID查询 | | `session_id` | string | ⭐二选一 | 按会话ID查询 | | `answer_type` | string | ❌ | 过滤类型:practice / mock_exam / formal_exam | | `page` | int | ❌ | 页码,默认 1 | | `page_size` | int | ❌ | 每页数量,默认 20 | **请求示例**: ```json { "paper_id": "paper_abc123", "answer_type": "practice", "page": 1, "page_size": 20 } ``` ### 6.4 获取考试记录列表 **接口**:`GET /api/exam/records?type=practice` **查询参数**: | 参数 | 类型 | 必填 | 默认值 | 说明 | |------|------|------|--------|------| | `type` | string | ❌ | `practice` | `practice` / `mock_exam` / `formal_exam` | **分支逻辑**: - `type=practice` → 返回日常练习答题记录(`UserAnswerQueryResponse`) - `type=mock_exam` 或 `formal_exam` → 返回试卷考试记录列表(`List`) --- ## 七、前后端字段对照表 ### 7.1 ChatReference 引用字段映射 | 后端字段 | 前端字段(AI返回) | 类型 | 说明 | |----------|-------------------|------|------| | `chunkId` | `chunk_id` | string | 切片唯一标识 `{docName}_{chunkIndex}` | | `chunkIndex` | `chunk_index` | int | 切片序号 | | `collection` | `collection` | string | 向量库名称 | | `docName` | `source` / `doc_name` | string | 文档名称 | | `docType` | `doc_type` | string | 文档类型 | | `page` | `page` | int | 页码 | | `section` | `section` | string | 章节/段落位置 | | `chunkType` | `chunk_type` | string | `text` / `image` / `table` | | `excerpt` | `content.stem` | string | 切片摘要 | | **`preview`** | —(系统自动填充) | string | **目标切片完整正文**,由后端从 RAG 预览接口获取,失败则填错误信息 | | `score` | `score` | decimal | 相关性评分 0-1 | | `createTime` | — | datetime | 创建时间 | ### 7.2 ChatMessage 消息字段映射 | 后端字段 | 前端字段 | 类型 | 说明 | |----------|----------|------|------| | `id` | `message_id` | long | 消息唯一ID | | `conversationId` | `conversation_id` | long | 会话(数据库主键)ID | | `role` | `role` | string | `user` / `assistant` | | `content` | `content` | string | 用户提问原文 / AI 回答 | | `answer` | `answer` | string | AI 完整回答(含 `[ref:chunk_id]` 标记),role=assistant 有效 | | `messageType` | `message_type` | string | `rag` 等 | | `isFinished` | `is_finished` | int | 1=完成 | | `images` | `images` | string(JSON数组) | 图片列表(JSON 字符串,前端需 JSON.parse) | | `durationMs` | `duration_ms` | long | 耗时(毫秒) | | `createTime` | `create_time` | datetime | 创建时间 | > **已移除字段**:`sources`。引用信息需通过 `/ai/message/{messageId}/references` 接口获取。 ### 7.3 Image 图片字段映射(context_image 表) | 后端字段 | 前端字段(AI返回) | 类型 | 说明 | |----------|-------------------|------|------| | `imageId` | `image_id` | string | 图片标识 | | `path` | `path` / `url` | string | 图片访问路径,如 `/api/image/xxx/data` | | `source` | `source` | string | 来源文档 | | `page` | `page` | int | 页码 | | `section` | `section` | string | 章节位置 | | `chunkType` | `chunk_type` | string | 切片类型(image) | | `description` | `description` | string | 图片描述 | | `score` | `score` | decimal | 相关性评分(**保留**) | | `collection` | `collection` | string | 向量库名称 | > **已移除字段**:`filename`、`size_bytes`、`format`、`page_end`、`type`、`full_description` --- ## 八、常见问题 FAQ ### Q1: answer 中的 `[ref:chunk_id]` 标记如何解析? **答**:前端需要在渲染 `answer` 时,使用正则匹配 `\[ref:([^\]]+)\]`,将其替换为可点击的引用链接: ```javascript const renderAnswer = (answer) => { const regex = /\[ref:([^\]]+)\]/g; return answer.replace(regex, (match, chunkId) => { return `[引用]`; }); }; ``` ### Q2: 流式问答如何处理?为什么不用 EventSource? **答**:由于 EventSource 只能 GET 请求,且无法自定义请求头(无法传 Token),推荐使用 `fetch + ReadableStream` 方式: ```javascript const response = await fetch('/api/ai/chat/stream', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token }, body: JSON.stringify({ message, session_id: sessionId }) }); const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = ''; let fullAnswer = ''; while (true) { const { value, done } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const lines = buffer.split('\n'); buffer = lines.pop() || ''; for (const line of lines) { if (!line.startsWith('data:')) continue; const dataStr = line.slice(5).trim(); if (!dataStr) continue; try { const data = JSON.parse(dataStr); if (data.type === 'chunk') { fullAnswer += data.content; // renderAnswer(fullAnswer); // 更新 UI } else if (data.type === 'finish') { console.log('引用:', data.citations); console.log('图片:', data.images); } } catch (e) { console.warn('解析失败:', dataStr); } } } ``` ### Q3: 题目生成接口为什么是异步的?如何获取生成结果? **答**:题目生成需要调用 AI LLM 进行内容理解和题目构造,耗时较长(通常 10-60 秒)。使用异步方式可避免前端超时等待。 获取结果方式: 1. 调用 `POST /api/exam/generate` 提交任务,得到 `fileId` 2. 轮询 `GET /api/file/{fileId}` 检查 `examStatus` 字段 3. 当 `examStatus` 变为 `GENERATED` 时,通过题目列表接口查看题目 ### Q4: citations 和 sources 有什么区别? **答**: - **citations**(finish 事件中):AI 回答中真正引用到的切片。会写入 `context_reference` 表,可通过引用列表接口查询 - **sources**(流式事件中):检索阶段命中的候选来源文档。**仅用于前端展示**,**不写入数据库**,`context_message` 表也没有 `sources` 字段 ### Q5: context_reference.preview 字段是怎么来的? **答**:当 citations 写入 `context_reference` 表时,后端会对每条引用自动调用 RAG 预览接口: ``` GET /documents/{collection}/{source}/preview?chunk_index=N&context=0 ``` 获取该切片的完整正文写入 `preview` 字段。若接口调用失败,`preview` 填入错误信息(如"预览获取失败: ...")。前端可直接展示 `preview` 内容,无需再调用 RAG 接口。 ### Q6: 同一会话多次提问,如何区分不同消息的引用? **答**:`context_reference` 表通过 `messageId` 字段关联到具体消息。每个消息的引用列表独立。可通过: - `/ai/message/{messageId}/references` — 查询某条消息的引用 - `/ai/session/{sessionId}/references` — 查询整个会话的所有引用 ### Q7: context_image 表中的 score 字段有什么用? **答**:保留 score 字段用于记录 AI 返回的图片相关性评分(0-1)。前端可根据 score 排序展示图片,或过滤低分图片。 --- ## 九、附录:cURL 完整示例 > 以下示例均需替换 `` 为实际登录获取的 JWT Token ### 9.1 登录获取 Token ```bash curl -X POST "http://localhost:8080/api/auth/login" \ -H "Content-Type: application/json" \ -d '{"username":"admin","password":"123456"}' ``` ### 9.2 发起流式问答 ```bash curl -X POST "http://localhost:8080/api/ai/chat/stream" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{"message":"市场状态评价指标体系中一级指标有哪些","session_id":""}' \ --no-buffer ``` ### 9.3 中断流式问答 ```bash curl -X POST "http://localhost:8080/api/ai/chat/stop?sessionId=sess_xxx" \ -H "Authorization: Bearer " ``` ### 9.4 获取会话列表 ```bash curl -X GET "http://localhost:8080/api/ai/sessions?page=1&pageSize=10" \ -H "Authorization: Bearer " ``` ### 9.5 获取会话详情 ```bash curl -X GET "http://localhost:8080/api/ai/session/sess_1780410472658_9qg9a9hhp" \ -H "Authorization: Bearer " ``` ### 9.6 获取消息引用列表 ```bash curl -X GET "http://localhost:8080/api/ai/message/178/references" \ -H "Authorization: Bearer " ``` ### 9.7 获取消息文件来源列表(去重) ```bash curl -X GET "http://localhost:8080/api/ai/message/178/sources" \ -H "Authorization: Bearer " ``` ### 9.8 删除单个会话 ```bash curl -X DELETE "http://localhost:8080/api/ai/session/sess_1780410472658_9qg9a9hhp" \ -H "Authorization: Bearer " ``` ### 9.9 批量删除会话 ```bash curl -X DELETE "http://localhost:8080/api/ai/sessions" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '["sess_xxx1","sess_xxx2"]' ``` ### 9.10 生成题目(指定题型) ```bash curl -X POST "http://localhost:8080/api/exam/generate" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "file_id": 1, "question_types": { "single_choice": 3, "multiple_choice": 2, "true_false": 2, "fill_blank": 2, "subjective": 1 }, "difficulty": 3, "request_id": "gen_req_001" }' ``` ### 9.11 生成题目(智能模式) ```bash curl -X POST "http://localhost:8080/api/exam/generate" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{"file_id":1,"difficulty":3}' ``` ### 9.12 轮询题目生成状态 ```bash curl -X GET "http://localhost:8080/api/file/1" \ -H "Authorization: Bearer " ``` ### 9.13 生成试卷 ```bash curl -X POST "http://localhost:8080/api/exam/paper/generate" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "single_choice_count": 5, "multiple_choice_count": 3, "true_false_count": 2, "fill_blank_count": 2, "subjective_count": 1, "difficulty": 3, "include_personal": false }' ``` ### 9.14 批改答案(日常练习) ```bash curl -X POST "http://localhost:8080/api/exam/grade" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "request_id": "grade_req_001", "answer_type": "practice", "answers": [ { "question_id": "q_001", "question_type": "single_choice", "question_content": { "stem": "题目内容...", "data": { "options": [...] }, "answer": "B" }, "student_answer": "B", "max_score": 5.0 } ] }' ``` ### 9.15 开始考试(获取题目) ```bash curl -X POST "http://localhost:8080/api/exam/paper/paper_xxx/start" \ -H "Authorization: Bearer " ``` ### 9.16 获取考试结果 ```bash curl -X GET "http://localhost:8080/api/exam/record/record_xxx/result" \ -H "Authorization: Bearer " ``` ### 9.17 查询答题记录 ```bash curl -X POST "http://localhost:8080/api/exam/answers/query" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{"paper_id":"paper_xxx"}' ``` ### 9.18 获取考试记录列表 ```bash # 日常练习 curl -X GET "http://localhost:8080/api/exam/records?type=practice" \ -H "Authorization: Bearer " # 正式考试 curl -X GET "http://localhost:8080/api/exam/records?type=formal_exam" \ -H "Authorization: Bearer " ``` --- **文档版本**:v2.17.0 **最后更新**:2026-06-05 **适用代码版本**:与当前 Spring Boot 后端(`AIChatController` / `ExamController`)同步