34 KiB
AI 流式问答与出题接口 - 前端使用指南
适用范围:知识库问答、会话管理、题目生成、答案批改
Base URL:http://localhost:8080/api
认证方式:Authorization: Bearer <JWT Token>
接口版本:v2.17.0
更新时间:2026-06-05
目录
一、快速开始
1.1 认证流程
所有接口(登录接口除外)均需携带 JWT Token:
POST /api/auth/login
Content-Type: application/json
{
"username": "admin",
"password": "123456"
}
响应:
{
"code": 200,
"data": {
"token": "eyJhbGciOiJIUzUxMiJ9...",
"user": {
"id": 1,
"username": "admin",
"role": 1
}
}
}
后续所有接口请求头需添加:
Authorization: Bearer <token>
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,用于追踪 |
请求示例:
{
"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 事件核心字段
{
"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] 为可点击元素:
"市场状态 = Σ(二级指标均值 × 权重)[ref:文档名_86]。
此外,一级指标合计权重不低于70%[ref:文档名_93]。"
前端 JavaScript 调用示例(fetch + ReadableStream)
// 发起流式问答
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
响应:
{
"success": true,
"message": "已中断AI回答"
}
三、AI 会话管理接口
统一响应结构(
Map):{ "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" |
响应示例:
{
"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
请求体:
["sess_xxx1", "sess_xxx2", "sess_xxx3"]
响应:
{
"success": true,
"message": "会话批量删除成功",
"deletedCount": 3
}
四、题目生成接口
本模块接口统一响应结构(
Result<T>):{ "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 评分 |
请求示例:指定题型模式
{
"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 智能出题接口,自动根据文档内容判断题型和数量:
{
"file_id": 1,
"difficulty": 3
}
响应示例(成功)
{
"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"
}
}
响应示例(文件验证失败)
{
"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
请求体:
{
"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 | ✅ | 满分 |
请求示例:
{
"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):
{
"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 |
请求示例:
{
"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<ExamRecordResponse>)
七、前后端字段对照表
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:([^\]]+)\],将其替换为可点击的引用链接:
const renderAnswer = (answer) => {
const regex = /\[ref:([^\]]+)\]/g;
return answer.replace(regex, (match, chunkId) => {
return `<a class="ref-link" data-chunk-id="${chunkId}">[引用]</a>`;
});
};
Q2: 流式问答如何处理?为什么不用 EventSource?
答:由于 EventSource 只能 GET 请求,且无法自定义请求头(无法传 Token),推荐使用 fetch + ReadableStream 方式:
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 秒)。使用异步方式可避免前端超时等待。
获取结果方式:
- 调用
POST /api/exam/generate提交任务,得到fileId - 轮询
GET /api/file/{fileId}检查examStatus字段 - 当
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 完整示例
以下示例均需替换
<token>为实际登录获取的 JWT Token
9.1 登录获取 Token
curl -X POST "http://localhost:8080/api/auth/login" \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"123456"}'
9.2 发起流式问答
curl -X POST "http://localhost:8080/api/ai/chat/stream" \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"message":"市场状态评价指标体系中一级指标有哪些","session_id":""}' \
--no-buffer
9.3 中断流式问答
curl -X POST "http://localhost:8080/api/ai/chat/stop?sessionId=sess_xxx" \
-H "Authorization: Bearer <token>"
9.4 获取会话列表
curl -X GET "http://localhost:8080/api/ai/sessions?page=1&pageSize=10" \
-H "Authorization: Bearer <token>"
9.5 获取会话详情
curl -X GET "http://localhost:8080/api/ai/session/sess_1780410472658_9qg9a9hhp" \
-H "Authorization: Bearer <token>"
9.6 获取消息引用列表
curl -X GET "http://localhost:8080/api/ai/message/178/references" \
-H "Authorization: Bearer <token>"
9.7 获取消息文件来源列表(去重)
curl -X GET "http://localhost:8080/api/ai/message/178/sources" \
-H "Authorization: Bearer <token>"
9.8 删除单个会话
curl -X DELETE "http://localhost:8080/api/ai/session/sess_1780410472658_9qg9a9hhp" \
-H "Authorization: Bearer <token>"
9.9 批量删除会话
curl -X DELETE "http://localhost:8080/api/ai/sessions" \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '["sess_xxx1","sess_xxx2"]'
9.10 生成题目(指定题型)
curl -X POST "http://localhost:8080/api/exam/generate" \
-H "Authorization: Bearer <token>" \
-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 生成题目(智能模式)
curl -X POST "http://localhost:8080/api/exam/generate" \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"file_id":1,"difficulty":3}'
9.12 轮询题目生成状态
curl -X GET "http://localhost:8080/api/file/1" \
-H "Authorization: Bearer <token>"
9.13 生成试卷
curl -X POST "http://localhost:8080/api/exam/paper/generate" \
-H "Authorization: Bearer <token>" \
-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 批改答案(日常练习)
curl -X POST "http://localhost:8080/api/exam/grade" \
-H "Authorization: Bearer <token>" \
-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 开始考试(获取题目)
curl -X POST "http://localhost:8080/api/exam/paper/paper_xxx/start" \
-H "Authorization: Bearer <token>"
9.16 获取考试结果
curl -X GET "http://localhost:8080/api/exam/record/record_xxx/result" \
-H "Authorization: Bearer <token>"
9.17 查询答题记录
curl -X POST "http://localhost:8080/api/exam/answers/query" \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"paper_id":"paper_xxx"}'
9.18 获取考试记录列表
# 日常练习
curl -X GET "http://localhost:8080/api/exam/records?type=practice" \
-H "Authorization: Bearer <token>"
# 正式考试
curl -X GET "http://localhost:8080/api/exam/records?type=formal_exam" \
-H "Authorization: Bearer <token>"
文档版本:v2.17.0
最后更新:2026-06-05
适用代码版本:与当前 Spring Boot 后端(AIChatController / ExamController)同步