- 将全部路由文件(12个)的 jsonify 响应迁移至 success_response/error_response 统一格式 - 修复 sync_routes.py error_response 参数错误(P0) - 新增异步任务系统:task_registry + task_routes - 新增状态码:TASK_NOT_FOUND(4014)、TASK_CONFLICT(4015)、REINDEX_ERROR(5040) - 修正 task_routes/exam_pkg 中语义不匹配的状态码 - 更新 curl 测试手册、后端对接规范文档 - 添加缓存性能报告和 Redis 迁移计划
2982 lines
83 KiB
Markdown
2982 lines
83 KiB
Markdown
# RAG 服务 API 接口规范
|
||
|
||
---
|
||
|
||
## 📋 变更记录(2026-06-05 更新)
|
||
|
||
> **本次更新内容**:新增 AI 智能出题端口、更新生产环境测试结果、**长操作改为异步任务**
|
||
>
|
||
> **2026-06-05 更新**:出题批卷接口格式优化与输入校验增强
|
||
>
|
||
> **2026-06-05 异步任务变更**:同步、上传向量化、出题、批阅等长耗时操作改为异步任务模式,立即返回 `task_id`,通过 `GET /tasks/<task_id>` 轮询结果
|
||
|
||
### 异步任务变更(⚠️ 重要,2026-06-05)
|
||
|
||
| 端点 | 变更说明 |
|
||
|------|----------|
|
||
| `POST /sync` | 改为异步任务,返回 `{"task_id": "xxx"}` 而非同步结果 |
|
||
| `POST /documents/sync` | 改为异步任务,返回 `{"task_id": "xxx"}` |
|
||
| `POST /collections/<kb>/reindex` | 改为异步任务,返回 `{"task_id": "xxx"}` |
|
||
| `POST /documents/upload` | 新增 `task_id` 字段(向量化后台执行) |
|
||
| `POST /documents/batch-upload` | 新增 `task_id` 字段(批量向量化后台执行) |
|
||
| `POST /exam/generate` | 改为异步任务,返回 `{"task_id": "xxx"}` |
|
||
| `POST /exam/generate-smart` | 改为异步任务,返回 `{"task_id": "xxx"}` |
|
||
| `POST /exam/grade` | 改为异步任务,返回 `{"task_id": "xxx"}` |
|
||
|
||
**新增任务查询接口**:
|
||
|
||
| 端点 | 方法 | 功能 | 说明 |
|
||
|------|------|------|------|
|
||
| `/tasks` | GET | 任务列表 | 支持按 status/type 过滤 |
|
||
| `/tasks/<task_id>` | GET | 任务状态(JSON) | 后端组推荐轮询接口,建议 1-2 秒间隔 |
|
||
| `/tasks/<task_id>/progress` | GET | 任务进度(SSE) | dev-ui 前端推荐使用 |
|
||
| `/tasks/stats` | GET | 任务统计 | 按状态和类型分组统计 |
|
||
|
||
### 新增端口
|
||
|
||
| 端点 | 方法 | 功能 | 说明 |
|
||
|-----|------|------|------|
|
||
| `/exam/generate-smart` | POST | AI 智能出题 | 自动分析文档决定题型和数量 |
|
||
| `/images/<image_id>` | GET | 获取图片 | 返回图片文件 |
|
||
| `/images/list` | GET | 图片列表 | 返回所有图片列表 |
|
||
| `/images/stats` | GET | 图片统计 | 返回图片数量和大小统计 |
|
||
| `/feedback/stats` | GET | 反馈统计 | 返回反馈统计数据 |
|
||
| `/reports/weekly` | GET | 周报告 | 返回周度反馈报告 |
|
||
| `/reports/monthly` | GET | 月报告 | 返回月度反馈报告 |
|
||
| `/feedback/bad-cases` | GET | 差评案例 | 返回差评反馈列表 |
|
||
| `/faq/suggestions` | GET | FAQ 建议 | 返回 FAQ 建议列表 |
|
||
| `/kb/route` | POST | 路由测试 | 测试知识库路由逻辑(调试用) |
|
||
| `/collections/<kb_name>/update-image-descriptions` | POST | 更新图片描述 | 重新生成图片切片描述 |
|
||
|
||
### 删除的端口(已废弃)
|
||
|
||
| 端点 | 原因 |
|
||
|------|------|
|
||
| `/rag/stream` | 已合并到 `/rag`(SSE 流式返回) |
|
||
| `/graph/*` | Graph RAG 功能未启用,已删除 |
|
||
| `/outline/*` | 纲要生成功能未启用,已删除 |
|
||
| `/questions/*` | 题库维护功能未启用,已删除 |
|
||
|
||
### 修复的 Bug
|
||
|
||
| 文件 | 问题 | 修复 |
|
||
|------|------|------|
|
||
| `api/kb_routes.py` | `/documents/sync` 端口报错 `NameError: name 'current_app' is not defined` | 添加 `current_app` 导入 |
|
||
|
||
### 生产环境测试结果
|
||
|
||
所有核心端口在 `DEV_MODE=false` 生产模式下测试通过:
|
||
|
||
| 端点 | 状态码 | 说明 |
|
||
|-----|--------|------|
|
||
| `/health` | 200 | ✅ 正常 |
|
||
| `/rag` | 200 | ✅ 正常(SSE 流式) |
|
||
| `/search` | 200 | ✅ 正常 |
|
||
| `/collections` | 200 | ✅ 正常 |
|
||
| `/documents/list` | 200 | ✅ 正常 |
|
||
| `/sync` | 200 | ✅ 正常 |
|
||
| `/sync/status` | 200 | ✅ 正常 |
|
||
| `/feedback` | 200 | ✅ 正常 |
|
||
| `/faq` | 200 | ✅ 正常 |
|
||
| `/images/list` | 200 | ✅ 正常 |
|
||
| `/documents/sync` | 200 | ✅ 正常(已修复) |
|
||
|
||
### 出题批卷接口变更(2026-06-05)
|
||
|
||
| 变更项 | 旧值 | 新值 | 影响 |
|
||
|--------|------|------|------|
|
||
| 批卷请求字段名 | `question_content` | `content` | ⚠️ **破坏性变更**,后端需同步修改 |
|
||
| 出题总题数上限 | 无限制 | **20 道** | 超过返回 400 |
|
||
| 批卷 question_type | 无校验 | 必须属于 5 种合法题型 | 无效值返回 400 |
|
||
| 批卷结果格式 | 各题型格式不统一 | 统一 `grading_status` + `details` | 后端需适配新格式 |
|
||
| **跨调用去重** | 无 | **`exclude_stems` 参数** | 追加出题时传入已有题干避免重复 |
|
||
| 出题结果 | 无短缺提示 | 新增 `warnings` 字段 | 可选消费 |
|
||
|
||
---
|
||
|
||
## 一、服务概述
|
||
|
||
RAG服务负责:
|
||
- **向量检索**:ChromaDB向量数据库 + BM25关键词检索
|
||
- **文档解析**:PDF/Word/Excel解析与分块
|
||
- **知识库问答**:Agentic RAG问答引擎
|
||
- **出题批阅**:本地LLM实现(可选)
|
||
- **反馈系统**:用户反馈与FAQ管理
|
||
|
||
后端服务负责:
|
||
- 用户认证与权限控制
|
||
- 会话管理与对话历史
|
||
- 业务数据存储
|
||
|
||
---
|
||
|
||
## 二、API接口清单
|
||
|
||
> **接口分类说明**:
|
||
> - **生产接口**:后端组对接时需调用的接口,文档中详细说明请求/响应格式
|
||
> - **开发自用接口**(标记为 `🛠️ DEV`):仅供 RAG 服务内部开发调试使用(如 dev-ui 前端),**后端组无需调用**
|
||
>
|
||
> **引用溯源职责边界**:RAG 服务的 `/rag` 接口确保返回的 `citations` 数据包含足够的位置信息(`chunk_index`、`section`、`page`、`bbox` 等),使后端组和 dev-ui 能够各自实现文件跳转功能。具体的跳转实现细节由各团队自行负责。
|
||
|
||
### 2.1 核心接口(必需对接)
|
||
|
||
| 端点 | 方法 | 功能 | 说明 |
|
||
|-----|------|------|------|
|
||
| `/health` | GET | 健康检查 | 服务状态监控 |
|
||
| `/rag` | POST | RAG问答(SSE) | 流式返回,推荐使用 |
|
||
| `/search` | POST | 混合检索 | 知识库检索 |
|
||
|
||
### 2.2 知识库管理(可选)
|
||
|
||
| 端点 | 方法 | 功能 |
|
||
|-----|------|------|
|
||
| `/collections` | GET | 向量库列表 |
|
||
| `/collections` | POST | 创建向量库 |
|
||
| `/collections/<name>` | DELETE | 删除向量库 |
|
||
| `/documents/upload` | POST | 上传文档 |
|
||
| `/documents/list` | GET | 文档列表 |
|
||
| `/documents/<path>` | DELETE | 删除文档 |
|
||
|
||
### 2.3 反馈系统(可选)
|
||
|
||
| 端点 | 方法 | 功能 |
|
||
|-----|------|------|
|
||
| `/feedback` | POST | 提交反馈 |
|
||
| `/feedback/list` | GET | 反馈列表 |
|
||
| `/faq` | GET/POST | FAQ管理 |
|
||
|
||
### 2.4 出题系统(可选)
|
||
|
||
| 端点 | 方法 | 功能 | 说明 |
|
||
|-----|------|------|------|
|
||
| `/exam/generate` | POST | 生成题目 | 异步任务,返回 task_id |
|
||
| `/exam/generate-smart` | POST | AI 智能出题 | 异步任务,返回 task_id |
|
||
| `/exam/grade` | POST | 批阅答案 | 异步任务,返回 task_id |
|
||
|
||
### 2.5 异步任务查询
|
||
|
||
> 所有异步操作(同步、重建索引、上传向量化、出题、批阅)返回的 `task_id` 均可通过以下接口查询进度。
|
||
|
||
| 端点 | 方法 | 功能 | 说明 |
|
||
|-----|------|------|------|
|
||
| `/tasks` | GET | 任务列表 | 支持按 status/type 过滤 |
|
||
| `/tasks/<task_id>` | GET | 任务状态(JSON) | **后端组推荐轮询接口**,建议 1-2 秒间隔 |
|
||
| `/tasks/<task_id>/progress` | GET | 任务进度(SSE) | dev-ui 前端推荐使用 |
|
||
| `/tasks/stats` | GET | 任务统计 | 按状态和类型分组统计 |
|
||
|
||
---
|
||
|
||
## 三、调用方式
|
||
|
||
### 3.1 RAG问答(核心接口)
|
||
|
||
**请求**:
|
||
```json
|
||
POST /rag
|
||
Content-Type: application/json
|
||
|
||
{
|
||
"message": "用户问题",
|
||
"collections": ["kb1", "kb2"],
|
||
"chat_history": [
|
||
{"role": "user", "content": "之前的问题"},
|
||
{"role": "assistant", "content": "之前的回答"}
|
||
]
|
||
}
|
||
```
|
||
|
||
**参数说明**:
|
||
| 参数 | 必需 | 说明 |
|
||
|-----|------|------|
|
||
| `message` | ✅ | 用户问题 |
|
||
| `collections` | ⚠️ | 用户可访问的知识库列表(权限控制),不传时默认 `["public_kb"]` |
|
||
| `chat_history` | ⚠️ | 对话历史(**生产环境必需**,首次对话传 `[]`) |
|
||
| `history` | ❌ | 对话历史(旧参数名,与 `chat_history` 等效) |
|
||
| `session_id` | ❌ | 会话标识(可选,用于日志追踪) |
|
||
|
||
> **注意**:生产环境(`APP_ENV=prod`)必须传递 `chat_history` 参数,即使是首次对话也要传空数组 `[]`。开发环境可省略。
|
||
|
||
**响应(SSE流式)**:
|
||
```
|
||
data: {"type": "start", "message": "正在检索知识库..."}
|
||
|
||
data: {"type": "sources", "sources": [...]}
|
||
|
||
data: {"type": "chunk", "content": "回答"}
|
||
|
||
data: {"type": "chunk", "content": "内容"}
|
||
|
||
...
|
||
|
||
data: {"type": "finish", "answer": "完整答案", "session_id": "xxx", "sources": [...], "images": [...], "duration_ms": 1500}
|
||
```
|
||
|
||
**SSE 事件类型**:
|
||
|
||
| 事件类型 | 说明 |
|
||
|---------|------|
|
||
| `start` | 开始处理,可显示加载状态 |
|
||
| `sources` | 检索到的来源,可提前展示溯源 |
|
||
| `chunk` | 每个 token,用于打字机效果 |
|
||
| `finish` | **完整响应对象**,包含完整答案用于存储 |
|
||
| `error` | 错误事件,显示错误提示 |
|
||
|
||
### 3.2 权限控制
|
||
|
||
**通过 `collections` 参数控制**:
|
||
- 后端根据用户权限生成可访问的知识库列表
|
||
- RAG服务只检索用户有权访问的知识库
|
||
- 无需传递用户Header
|
||
|
||
---
|
||
|
||
## 四、环境配置
|
||
|
||
### 4.1 必需配置
|
||
|
||
```env
|
||
DASHSCOPE_API_KEY=your-api-key
|
||
```
|
||
|
||
### 4.2 环境模式配置
|
||
|
||
```env
|
||
# 生产环境:关闭开发模式(认证由后端控制,通过 collections 传参)
|
||
DEV_MODE=false
|
||
|
||
# 应用环境标识(控制会话存储、审计日志等功能开关)
|
||
APP_ENV=prod
|
||
```
|
||
|
||
> **说明**:`DEV_MODE` 控制认证行为(`true`=开发模式支持 mock token,`false`=生产模式直接放行)。`APP_ENV` 控制功能模块开关(`dev`=启用会话存储和审计日志,`prod`=无状态模式)。两者独立配置。
|
||
|
||
### 4.3 可选配置
|
||
|
||
```env
|
||
ENABLE_WEB_SEARCH=false
|
||
ENABLE_GRAPH_RAG=false
|
||
ENABLE_DIFY_WORKFLOW=false
|
||
```
|
||
|
||
---
|
||
|
||
## 五、部署要求
|
||
|
||
| 项目 | 要求 |
|
||
|-----|------|
|
||
| 端口 | 5001 |
|
||
| Python | 3.10+ |
|
||
| 内存 | 建议8GB+ |
|
||
| 部署方式 | Docker / Gunicorn |
|
||
|
||
---
|
||
|
||
## 六、数据流
|
||
|
||
```
|
||
前端 → Java后端(8080) → RAG服务(5001)
|
||
│
|
||
├── 生成 collections 列表(权限控制)
|
||
├── 传入 chat_history(对话历史)
|
||
└── 调用 RAG API
|
||
```
|
||
|
||
---
|
||
|
||
**最后更新**: 2026-06-05
|
||
|
||
> 本文档供后端开发人员参考,用于对接 RAG 知识库服务。
|
||
|
||
---
|
||
|
||
## 一、职责边界
|
||
|
||
### 1.1 后端负责
|
||
|
||
| 职责 | 说明 |
|
||
|------|------|
|
||
| **用户认证** | JWT/Session 验证,确保用户身份合法 |
|
||
| **权限判断** | 判断用户可访问哪些知识库,生成 `collections` 列表 |
|
||
| **会话管理** | 创建/删除会话,存储会话元数据 |
|
||
| **消息存储** | 存储用户问题和 AI 回答的完整原文 |
|
||
| **知识库权限表** | 维护用户与知识库的权限关系 |
|
||
|
||
### 1.2 RAG 服务负责
|
||
|
||
| 职责 | 说明 |
|
||
|------|------|
|
||
| **知识库问答** | 在指定知识库中检索,生成回答 |
|
||
| **向量检索** | 向量相似度检索 + BM25 关键词检索 |
|
||
| **返回溯源** | 返回答案来源(chunks),供前端展示 |
|
||
| **文档处理** | 文档上传、切片、向量化 |
|
||
|
||
### 1.3 数据流
|
||
|
||
```
|
||
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
|
||
│ 前端 │───▶│ 后端 │───▶│ RAG │───▶│ 后端 │
|
||
└─────────┘ └─────────┘ └─────────┘ └─────────┘
|
||
│ │
|
||
▼ ▼
|
||
┌──────────┐ ┌──────────┐
|
||
│ 权限判断 │ │ 存储消息 │
|
||
│ 生成collections │ 更新会话 │
|
||
└──────────┘ └──────────┘
|
||
```
|
||
|
||
---
|
||
|
||
## 二、认证方式
|
||
|
||
### 2.1 模式说明
|
||
|
||
RAG 服务支持两种模式,通过环境变量 `DEV_MODE` 控制:
|
||
|
||
| 模式 | DEV_MODE | Header | 适用场景 |
|
||
|------|----------|--------|----------|
|
||
| **开发模式** | `true` | 可选(支持模拟用户) | 前端测试、开发调试 |
|
||
| **生产模式** | `false` | 不需要 | 后端直接调用 |
|
||
|
||
### 2.2 生产模式调用(推荐)
|
||
|
||
生产环境下,后端直接调用,**不需要传 Header**:
|
||
|
||
```http
|
||
POST http://rag-service:5001/rag
|
||
Content-Type: application/json
|
||
|
||
{
|
||
"message": "出差补助标准是什么?",
|
||
"collections": ["public_kb", "dept_finance"],
|
||
"chat_history": [
|
||
{"role": "user", "content": "之前的问题"},
|
||
{"role": "assistant", "content": "之前的回答"}
|
||
]
|
||
}
|
||
```
|
||
|
||
**说明**:
|
||
- 权限由后端控制,通过 `collections` 参数指定可访问的知识库
|
||
- 会话由后端管理,通过 `chat_history` 参数传入对话历史
|
||
- RAG 服务完全无状态,只负责问答检索
|
||
|
||
### 2.3 开发模式调用
|
||
|
||
开发环境下(`DEV_MODE=true`),支持模拟用户测试:
|
||
|
||
**方式 1:使用模拟 Token**
|
||
```http
|
||
POST http://rag-service:5001/rag
|
||
Authorization: Bearer mock-token-admin
|
||
Content-Type: application/json
|
||
|
||
{
|
||
"message": "问题",
|
||
"collections": ["public_kb"],
|
||
"chat_history": []
|
||
}
|
||
```
|
||
|
||
**方式 2:不传 Header(自动使用开发用户)**
|
||
```http
|
||
POST http://rag-service:5001/rag
|
||
Content-Type: application/json
|
||
|
||
{
|
||
"message": "问题",
|
||
"collections": ["public_kb"],
|
||
"chat_history": []
|
||
}
|
||
```
|
||
|
||
**模拟用户列表**:
|
||
| Token | user_id | role | department |
|
||
|-------|---------|------|------------|
|
||
| `mock-token-admin` | admin001 | admin | 管理部 |
|
||
| `mock-token-manager` | manager001 | manager | 财务部 |
|
||
| `mock-token-user` | user001 | user | 技术部 |
|
||
|
||
### 2.4 环境配置
|
||
|
||
```bash
|
||
# 开发环境(默认)
|
||
DEV_MODE=true
|
||
|
||
# 生产环境
|
||
DEV_MODE=false
|
||
```
|
||
|
||
---
|
||
|
||
## 三、问答接口
|
||
|
||
### 3.1 普通聊天
|
||
|
||
```
|
||
POST /chat
|
||
```
|
||
|
||
**请求体:**
|
||
```json
|
||
{
|
||
"message": "用户消息",
|
||
"history": [
|
||
{"role": "user", "content": "历史问题"},
|
||
{"role": "assistant", "content": "历史回答"}
|
||
]
|
||
}
|
||
```
|
||
|
||
> **注意**:`/chat` 接口中 `history` 为可选参数,也可使用 `chat_history` 作为参数名(两者等效)。该接口不走知识库检索,直接由 LLM 回答。
|
||
|
||
**响应:**
|
||
```json
|
||
{
|
||
"answer": "AI 回复内容",
|
||
"mode": "chat",
|
||
"sources": [],
|
||
"web_searched": false
|
||
}
|
||
```
|
||
|
||
### 3.2 知识库问答(核心接口 - SSE 流式返回)
|
||
|
||
```
|
||
POST /rag
|
||
```
|
||
|
||
> **重要变更**:`/rag` 接口已升级为 **SSE 流式返回**,不再返回阻塞 JSON。
|
||
> 原独立的 `/rag/stream` 端点已合并至此端点。
|
||
|
||
**请求体:**
|
||
```json
|
||
{
|
||
"message": "用户问题",
|
||
"collections": ["public_kb", "dept_finance"],
|
||
"session_id": "可选,用于后端日志追踪",
|
||
"chat_history": [
|
||
{"role": "user", "content": "历史问题"},
|
||
{"role": "assistant", "content": "历史回答"}
|
||
]
|
||
}
|
||
```
|
||
|
||
**参数说明:**
|
||
|
||
| 参数 | 类型 | 必需 | 说明 |
|
||
|------|------|------|------|
|
||
| `message` | string | ✅ | 用户问题 |
|
||
| `collections` | string[] | ⚠️ | 用户有权限的知识库列表,由后端判断后传入(不传时默认 `["public_kb"]`) |
|
||
| `session_id` | string | ❌ | 会话标识(首次对话不传,后续对话传入以加载历史) |
|
||
| `chat_history` | array | ⚠️ | 对话历史(**生产环境必需**,首次对话传 `[]`) |
|
||
|
||
**响应:SSE 流式事件**
|
||
|
||
```
|
||
Content-Type: text/event-stream
|
||
|
||
data: {"type": "start", "message": "正在检索知识库..."}
|
||
|
||
data: {"type": "sources", "sources": [{"source": "doc.pdf", "page": 1, "page_range": "1", "section": "", "chunk_type": "text", "score": 0.95}]}
|
||
|
||
data: {"type": "chunk", "content": "根"}
|
||
|
||
data: {"type": "chunk", "content": "据"}
|
||
|
||
data: {"type": "chunk", "content": "公司"}
|
||
|
||
...
|
||
|
||
data: {"type": "finish", "answer": "完整答案...", "mode": "rag", "session_id": "xxx", "sources": [...], "images": [], "tables": [], "sections": [], "duration_ms": 1500}
|
||
```
|
||
|
||
**SSE 事件类型:**
|
||
|
||
| 事件类型 | 字段 | 说明 |
|
||
|---------|------|------|
|
||
| `start` | `message` | 开始处理,前端可显示加载状态 |
|
||
| `sources` | `sources` | 检索到的来源,可提前展示溯源 |
|
||
| `chunk` | `content` | 每个 token,用于打字机效果 |
|
||
| `finish` | 完整响应对象 | **必须消费**,包含完整答案用于存储 |
|
||
| `error` | `message`, `traceback` | 错误事件,前端显示错误提示 |
|
||
|
||
**finish 事件完整结构:**
|
||
```json
|
||
{
|
||
"type": "finish",
|
||
"answer": "完整答案文本(含 [ref:chunk_id] 引用标记)",
|
||
"mode": "rag",
|
||
"session_id": "会话ID(首次对话自动创建)",
|
||
"sources": [
|
||
{
|
||
"source": "文档名.pdf",
|
||
"page": 5,
|
||
"page_end": 8,
|
||
"page_range": "5-8",
|
||
"section": "1. Introduction",
|
||
"chunk_type": "text",
|
||
"doc_type": "pdf",
|
||
"section_chunk_id": 3,
|
||
"score": 0.856
|
||
}
|
||
],
|
||
"citations": [
|
||
{
|
||
"chunk_id": "文档名.pdf_text_12",
|
||
"chunk_index": 12,
|
||
"source": "文档名.pdf",
|
||
"collection": "public_kb",
|
||
"doc_type": "pdf",
|
||
"page": 5,
|
||
"page_end": 5,
|
||
"bbox": [62, 480, 946, 904],
|
||
"bbox_mode": "normalized",
|
||
"section": "1. Introduction",
|
||
"preview": "内容摘要..."
|
||
}
|
||
],
|
||
"images": [
|
||
{
|
||
"id": "img_001",
|
||
"caption": "图片描述",
|
||
"url": "/images/img_001",
|
||
"source": "文档名.pdf",
|
||
"page": 2
|
||
}
|
||
],
|
||
"tables": [],
|
||
"sections": [],
|
||
"duration_ms": 1500
|
||
}
|
||
```
|
||
|
||
> **注意**:`tables` 和 `sections` 字段当前始终为空数组,功能待实现。
|
||
|
||
**sources 字段说明(Phase 2.1 更新):**
|
||
|
||
| 字段 | 类型 | 说明 |
|
||
|------|------|------|
|
||
| `source` | string | 来源文件名 |
|
||
| `page` | int | 起始页码 |
|
||
| `page_end` | int\|null | 结束页码(跨页切片时有值) |
|
||
| `page_range` | string | 页码范围显示文本,如 `"5"` 或 `"5-8"` |
|
||
| `section` | string | 所属章节路径 |
|
||
| `chunk_type` | string | 切片类型:`text`、`table`、`image` |
|
||
| `doc_type` | string | 文档类型:`pdf`、`word`、`excel` |
|
||
| `section_chunk_id` | int | 章节内段落序号(Word文档语义定位) |
|
||
| `score` | float | 相关性分数(0-1) |
|
||
|
||
**citations 字段说明(Phase 3.0 新增 - 引用溯源):**
|
||
|
||
> `citations` 是结构化的引用列表,用于实现精确的引用溯源功能。
|
||
> `answer` 中的 `[ref:chunk_id]` 标记需要后端根据 `citations` 替换为用户可见的编号。
|
||
|
||
| 字段 | 类型 | 说明 |
|
||
|------|------|------|
|
||
| `chunk_id` | string | 切片唯一标识,格式 `{文件名}_{序号}` |
|
||
| `chunk_index` | int | 全局切片序号(从 chunk_id 提取),用于文档预览跳转 |
|
||
| `source` | string | 来源文件名 |
|
||
| `collection` | string | 所属向量库名称,用于定位文档所在知识库 |
|
||
| `doc_type` | string | 文档类型:`pdf`、`word`、`excel` |
|
||
| `section` | string | 所属章节路径(已清洗,过滤掉正文内容) |
|
||
| `preview` | string | 内容摘要(用于搜索定位) |
|
||
| `content` | string | 切片内容(截断至 300 字) |
|
||
| `chunk_type` | string | 切片类型:`text`、`table`、`image` |
|
||
| `page` | int | 起始页码(仅 PDF) |
|
||
| `page_end` | int | 结束页码(仅 PDF) |
|
||
| `bbox` | array | 边界框坐标 [x0,y0,x1,y1](仅 PDF) |
|
||
| `bbox_mode` | string | 坐标模式:`normalized`(0-1000) |
|
||
| `section_chunk_id` | int | 章节内段落序号(仅 Word) |
|
||
|
||
**按文档类型差异化定位:**
|
||
|
||
| 文档类型 | 定位字段 | 定位方式 |
|
||
|---------|---------|--------|
|
||
| **PDF** | `page` + `bbox` | 坐标定位(跳转到页码 + 高亮区域) |
|
||
| **Word** | `chunk_index` + `section` + `preview` | 切片序号定位(跳转到具体切片,复用 `/documents/<path>/preview` 接口) |
|
||
| **Excel** | `page`(工作表序号)+ `preview` | 表格定位(工作表 + 搜索) |
|
||
|
||
**answer 引用标记格式:**
|
||
|
||
```
|
||
三峡船闸2022年运行10400闸次[ref:三峡公报.pdf_text_12],过闸货运量达1.56亿吨[ref:三峡公报.pdf_text_15]。
|
||
```
|
||
|
||
**后端处理建议:**
|
||
1. 解析 `answer` 中的 `[ref:chunk_id]` 标记
|
||
2. 根据 `citations` 数组顺序生成用户可见编号(1, 2, 3...)
|
||
3. 替换标记为编号,构建最终展示文本
|
||
|
||
```javascript
|
||
// 后端处理示例
|
||
function processAnswerWithCitations(answer, citations) {
|
||
const citationMap = {};
|
||
citations.forEach((c, i) => {
|
||
citationMap[c.chunk_id] = i + 1;
|
||
});
|
||
|
||
return {
|
||
text: answer.replace(/\[ref:([^\]]+)\]/g, (match, chunkId) => {
|
||
const num = citationMap[chunkId];
|
||
return num ? `[${num}]` : '';
|
||
}),
|
||
citationMap: citationMap
|
||
};
|
||
}
|
||
```
|
||
|
||
**前端显示建议:**
|
||
- 单页:`第5页`
|
||
- 跨页:`第5-8页`
|
||
- 带章节:`第5页 / 1. Introduction`
|
||
|
||
**后端消费示例(JavaScript):**
|
||
```javascript
|
||
const response = await fetch('http://rag-service:5001/rag', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
message: '出差补助标准是什么?',
|
||
collections: ['public_kb'],
|
||
chat_history: []
|
||
})
|
||
});
|
||
|
||
const reader = response.body.getReader();
|
||
const decoder = new TextDecoder();
|
||
let fullAnswer = '';
|
||
let sources = [];
|
||
|
||
while (true) {
|
||
const { done, value } = await reader.read();
|
||
if (done) break;
|
||
|
||
const text = decoder.decode(value);
|
||
const lines = text.split('\n');
|
||
|
||
for (const line of lines) {
|
||
if (line.startsWith('data: ')) {
|
||
const event = JSON.parse(line.slice(6));
|
||
|
||
switch (event.type) {
|
||
case 'start':
|
||
console.log('开始处理:', event.message);
|
||
break;
|
||
case 'sources':
|
||
sources = event.sources;
|
||
break;
|
||
case 'chunk':
|
||
fullAnswer += event.content;
|
||
// 可实时推送给前端显示打字机效果
|
||
break;
|
||
case 'finish':
|
||
// 使用完整答案存储到数据库
|
||
fullAnswer = event.answer; // 以 finish 中的为准
|
||
sources = event.sources;
|
||
break;
|
||
case 'error':
|
||
console.error('RAG 错误:', event.message);
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 存储 fullAnswer 和 sources 到数据库
|
||
```
|
||
|
||
**后端消费示例(Python):**
|
||
```python
|
||
import requests
|
||
import json
|
||
|
||
def call_rag_stream(message, collections, history=None):
|
||
response = requests.post(
|
||
'http://rag-service:5001/rag',
|
||
json={
|
||
'message': message,
|
||
'collections': collections,
|
||
'chat_history': history or []
|
||
},
|
||
stream=True # 启用流式读取
|
||
)
|
||
|
||
full_answer = ''
|
||
sources = []
|
||
|
||
for line in response.iter_lines():
|
||
if not line:
|
||
continue
|
||
|
||
line = line.decode('utf-8')
|
||
if line.startswith('data: '):
|
||
event = json.loads(line[6:])
|
||
|
||
if event['type'] == 'start':
|
||
print(f"开始: {event['message']}")
|
||
elif event['type'] == 'sources':
|
||
sources = event['sources']
|
||
elif event['type'] == 'chunk':
|
||
full_answer += event['content']
|
||
elif event['type'] == 'finish':
|
||
full_answer = event['answer'] # 以 finish 为准
|
||
sources = event['sources']
|
||
break
|
||
elif event['type'] == 'error':
|
||
raise Exception(event['message'])
|
||
|
||
return full_answer, sources
|
||
|
||
# 使用
|
||
answer, sources = call_rag_stream('出差补助标准是什么?', ['public_kb'])
|
||
# 存储 answer 和 sources 到数据库
|
||
```
|
||
|
||
### 3.3 会话管理(多轮对话)
|
||
|
||
> **开发环境特性**:RAG 服务内置 SQLite 会话存储,支持完整的多轮对话测试。
|
||
|
||
**会话管理流程:**
|
||
|
||
```
|
||
首次对话:
|
||
POST /rag { "message": "出差补助标准", "collections": ["public_kb"] }
|
||
↓
|
||
finish 事件返回 session_id
|
||
↓
|
||
前端保存 session_id
|
||
|
||
后续对话:
|
||
POST /rag { "message": "它有什么限制", "session_id": "xxx", "collections": ["public_kb"] }
|
||
↓
|
||
RAG 服务自动加载历史 → Query Rewriting 消歧 → 生成回答
|
||
↓
|
||
finish 事件返回相同 session_id
|
||
```
|
||
|
||
**关键点:**
|
||
1. 首次对话不传 `session_id`,RAG 服务自动创建新会话
|
||
2. 后续对话传入 `session_id`,RAG 服务自动加载历史(无需前端传 `history`)
|
||
3. Query Rewriting 会利用历史上下文进行消歧和实体补全
|
||
|
||
**会话相关 API:**
|
||
|
||
| 接口 | 说明 |
|
||
|------|------|
|
||
| `GET /sessions` | 获取用户会话列表 |
|
||
| `GET /history/<session_id>` | 获取会话历史 |
|
||
| `DELETE /session/<session_id>` | 删除会话 |
|
||
|
||
**后端实现参考(数据库表结构):**
|
||
|
||
```sql
|
||
CREATE TABLE sessions (
|
||
session_id VARCHAR(64) PRIMARY KEY,
|
||
user_id VARCHAR(64) NOT NULL,
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
last_active TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
metadata JSON
|
||
);
|
||
|
||
CREATE TABLE messages (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
session_id VARCHAR(64) NOT NULL,
|
||
role VARCHAR(20) NOT NULL, -- 'user' 或 'assistant'
|
||
content TEXT NOT NULL,
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
FOREIGN KEY (session_id) REFERENCES sessions(session_id)
|
||
);
|
||
|
||
CREATE INDEX idx_messages_session ON messages(session_id);
|
||
CREATE INDEX idx_sessions_user ON sessions(user_id);
|
||
```
|
||
|
||
### 3.4 混合检索
|
||
|
||
```
|
||
POST /search
|
||
```
|
||
|
||
**请求体:**
|
||
```json
|
||
{
|
||
"query": "检索关键词",
|
||
"top_k": 5,
|
||
"collections": ["public_kb", "dept_finance"]
|
||
}
|
||
```
|
||
|
||
**响应:**
|
||
```json
|
||
{
|
||
"contexts": ["文档片段1", "文档片段2"],
|
||
"metadatas": [
|
||
{"source": "doc1.pdf", "page": 1},
|
||
{"source": "doc2.pdf", "page": 5}
|
||
],
|
||
"scores": [0.95, 0.87]
|
||
}
|
||
```
|
||
|
||
**scores 说明**:相似度分数,范围 0-1,越高越相关。
|
||
|
||
---
|
||
|
||
## 四、知识库管理接口
|
||
|
||
### 4.1 获取向量库列表
|
||
|
||
```
|
||
GET /collections
|
||
```
|
||
|
||
**响应:**
|
||
```json
|
||
{
|
||
"collections": [
|
||
{
|
||
"name": "public_kb",
|
||
"display_name": "公开知识库",
|
||
"document_count": 150,
|
||
"department": null,
|
||
"description": "全员可访问"
|
||
}
|
||
],
|
||
"total": 1
|
||
}
|
||
```
|
||
|
||
### 4.2 创建向量库
|
||
|
||
```
|
||
POST /collections
|
||
```
|
||
|
||
**请求体:**
|
||
```json
|
||
{
|
||
"name": "dept_finance",
|
||
"display_name": "财务部知识库",
|
||
"department": "财务部",
|
||
"description": "财务部专用知识库"
|
||
}
|
||
```
|
||
|
||
### 4.3 修改向量库
|
||
|
||
```
|
||
PUT /collections/<name>
|
||
```
|
||
|
||
### 4.4 删除向量库
|
||
|
||
```
|
||
DELETE /collections/<name>
|
||
```
|
||
|
||
**查询参数:**
|
||
|
||
| 参数 | 类型 | 必需 | 说明 |
|
||
|------|------|------|------|
|
||
| `delete_documents` | boolean | ❌ | 是否删除文档源文件,默认 false |
|
||
|
||
**响应:**
|
||
```json
|
||
{
|
||
"success": true,
|
||
"message": "向量库 'dept_finance' 已删除",
|
||
"deleted_documents": false
|
||
}
|
||
```
|
||
|
||
**说明:**
|
||
- 默认只删除向量数据(ChromaDB 集合、BM25 索引)
|
||
- 设置 `delete_documents=true` 会同时删除文档源文件
|
||
- 公开知识库 (`public_kb`) 不允许删除
|
||
|
||
### 4.5 获取向量库文档列表
|
||
|
||
```
|
||
GET /collections/<kb_name>/documents
|
||
```
|
||
|
||
**响应:**
|
||
```json
|
||
{
|
||
"collection": "public_kb",
|
||
"documents": [
|
||
{
|
||
"chunks": 105,
|
||
"source": "1.docx"
|
||
},
|
||
{
|
||
"chunks": 39,
|
||
"source": "三峡公报_1-15页.pdf"
|
||
}
|
||
],
|
||
"total": 9
|
||
}
|
||
```
|
||
|
||
### 4.6 获取向量库切片列表
|
||
|
||
```
|
||
GET /collections/<kb_name>/chunks
|
||
```
|
||
|
||
**查询参数:**
|
||
|
||
| 参数 | 类型 | 必需 | 说明 |
|
||
|------|------|------|------|
|
||
| `document_id` | string | ❌ | 过滤指定文档的切片 |
|
||
| `limit` | int | ❌ | 返回数量,默认 100 |
|
||
| `offset` | int | ❌ | 偏移量,默认 0 |
|
||
|
||
**响应:**
|
||
```json
|
||
{
|
||
"collection": "public_kb",
|
||
"chunks": [
|
||
{
|
||
"id": "chunk_001",
|
||
"document": "考勤制度.pdf",
|
||
"content": "切片内容...",
|
||
"metadata": {
|
||
"page": 1,
|
||
"section": "第一章",
|
||
"status": "active"
|
||
},
|
||
"score": null
|
||
}
|
||
],
|
||
"total": 150
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 四点五、文档版本管理接口
|
||
|
||
> **功能说明**:支持文档的废止(软删除)、恢复和版本历史查询。
|
||
|
||
### 4.5.1 废止文档
|
||
|
||
```
|
||
POST /collections/<kb_name>/documents/<filename>/deprecate
|
||
```
|
||
|
||
**请求体:**
|
||
```json
|
||
{
|
||
"reason": "新版本已发布,旧版本废止"
|
||
}
|
||
```
|
||
|
||
**响应:**
|
||
```json
|
||
{
|
||
"success": true,
|
||
"deprecated_chunks": 15,
|
||
"document_id": "报销制度.pdf",
|
||
"collection": "public_kb",
|
||
"deprecated_date": "2026-04-20T15:00:00"
|
||
}
|
||
```
|
||
|
||
**说明:**
|
||
- 废止是软删除,切片标记为 `status: "deprecated"`,不物理删除
|
||
- 废止后的文档不会出现在检索结果中
|
||
- 可通过恢复接口恢复
|
||
|
||
### 4.5.2 恢复已废止文档
|
||
|
||
```
|
||
POST /collections/<kb_name>/documents/<filename>/restore
|
||
```
|
||
|
||
**响应:**
|
||
```json
|
||
{
|
||
"success": true,
|
||
"restored_chunks": 15,
|
||
"document_id": "报销制度.pdf",
|
||
"collection": "public_kb"
|
||
}
|
||
```
|
||
|
||
### 4.5.3 获取文档版本历史
|
||
|
||
```
|
||
GET /collections/<kb_name>/documents/<filename>/versions
|
||
```
|
||
|
||
**查询参数:**
|
||
|
||
| 参数 | 类型 | 必需 | 说明 |
|
||
|------|------|------|------|
|
||
| `limit` | int | ❌ | 返回数量,默认 10 |
|
||
|
||
**响应:**
|
||
```json
|
||
{
|
||
"success": true,
|
||
"document_id": "报销制度.pdf",
|
||
"collection": "public_kb",
|
||
"versions": [
|
||
{
|
||
"document_id": "报销制度.pdf",
|
||
"collection": "public_kb",
|
||
"version": "v2",
|
||
"status": "active",
|
||
"effective_date": "2026-01-15T10:00:00",
|
||
"deprecated_date": null,
|
||
"deprecated_reason": null,
|
||
"change_summary": "更新报销标准",
|
||
"supersedes": "v1",
|
||
"created_at": "2026-01-15T10:00:00",
|
||
"created_by": "admin001",
|
||
"chunk_count": 20
|
||
},
|
||
{
|
||
"document_id": "报销制度.pdf",
|
||
"collection": "public_kb",
|
||
"version": "v1",
|
||
"status": "superseded",
|
||
"effective_date": "2025-01-01T10:00:00",
|
||
"deprecated_date": "2026-01-15T10:00:00",
|
||
"deprecated_reason": "被 v2 替代",
|
||
"change_summary": null,
|
||
"supersedes": null,
|
||
"created_at": "2025-01-01T10:00:00",
|
||
"created_by": "admin001",
|
||
"chunk_count": 15
|
||
}
|
||
],
|
||
"total": 2
|
||
}
|
||
```
|
||
|
||
**文档状态说明:**
|
||
|
||
| 状态 | 说明 |
|
||
|------|------|
|
||
| `draft` | 草稿,未生效 |
|
||
| `active` | 生效中,参与检索 |
|
||
| `deprecated` | 已废止,不参与检索 |
|
||
| `superseded` | 被新版本替代 |
|
||
|
||
---
|
||
|
||
## 四点六、知识库路由接口(调试)
|
||
|
||
> **功能说明**:测试知识库路由逻辑,用于调试和验证路由策略。
|
||
|
||
### 4.6.1 测试路由
|
||
|
||
```
|
||
POST /kb/route
|
||
```
|
||
|
||
**请求体:**
|
||
```json
|
||
{
|
||
"query": "财务部的报销流程是什么"
|
||
}
|
||
```
|
||
|
||
**响应:**
|
||
```json
|
||
{
|
||
"query": "财务部的报销流程是什么",
|
||
"user_role": "user",
|
||
"user_department": "tech",
|
||
"target_collections": ["dept_finance", "public_kb"],
|
||
"intent": {
|
||
"is_general": false,
|
||
"department": "finance",
|
||
"confidence": 0.8,
|
||
"keywords": ["财务", "报销"],
|
||
"reason": "匹配到部门关键词: 财务, 报销"
|
||
}
|
||
}
|
||
```
|
||
|
||
**说明:**
|
||
- 根据查询内容和用户权限,返回应查询的目标向量库列表
|
||
- `intent` 字段展示意图分析结果
|
||
- 可用于调试路由策略和关键词匹配
|
||
|
||
---
|
||
|
||
## 五、文档管理接口
|
||
|
||
### 5.1 上传单个文件
|
||
|
||
```
|
||
POST /documents/upload
|
||
Content-Type: multipart/form-data
|
||
```
|
||
|
||
**表单参数:**
|
||
- `file`: 文件(必需)
|
||
- `collection`: 目标向量库名称(必需,也可用 `kb_name`)
|
||
|
||
**响应:**
|
||
```json
|
||
{
|
||
"success": true,
|
||
"status_code": 2002,
|
||
"message": "文件上传成功,已保存,向量化任务已启动",
|
||
"data": {
|
||
"file": {
|
||
"filename": "document.pdf",
|
||
"collection": "public_kb",
|
||
"path": "public_kb/document.pdf",
|
||
"size": 1024000,
|
||
"replaced": false
|
||
},
|
||
"sync_status": "已保存,向量化任务已启动",
|
||
"task_id": "a1b2c3d4e5f6"
|
||
}
|
||
}
|
||
```
|
||
|
||
> **异步说明**:文件保存为同步操作,向量化在后台线程异步执行。响应中的 `task_id` 可用于轮询向量化进度(`GET /tasks/<task_id>`)。当同步服务不可用时,`task_id` 为 `null`,`sync_status` 为 `"已保存,等待手动同步"`。
|
||
|
||
**同名文件处理**:上传同名文件时,旧版本的切片会被自动清理后覆盖(`replaced: true`),不会生成时间戳后缀文件。这确保了向量库中不会出现同一文档的新旧切片共存的情况。
|
||
|
||
### 5.2 批量上传
|
||
|
||
```
|
||
POST /documents/batch-upload
|
||
Content-Type: multipart/form-data
|
||
```
|
||
|
||
**表单参数:**
|
||
- `files`: 文件列表(必需)
|
||
- `collection`: 目标向量库名称(必需,也可用 `kb_name`)
|
||
|
||
### 5.3 文档列表
|
||
|
||
```
|
||
GET /documents/list?collection=public_kb
|
||
```
|
||
|
||
### 5.4 获取文档状态
|
||
|
||
```
|
||
GET /documents/<path>/status
|
||
```
|
||
|
||
**响应:**
|
||
```json
|
||
{
|
||
"success": true,
|
||
"status": "active",
|
||
"chunk_count": 105,
|
||
"last_processed": null
|
||
}
|
||
```
|
||
|
||
### 5.5 更新/替换文档
|
||
|
||
```
|
||
PUT /documents/<path>
|
||
Content-Type: multipart/form-data
|
||
```
|
||
|
||
**表单参数:**
|
||
- `file`: 替换的文件(必需)
|
||
|
||
**响应:**
|
||
```json
|
||
{
|
||
"success": true,
|
||
"message": "文件已更新"
|
||
}
|
||
```
|
||
|
||
**说明:**
|
||
- 文件必须已存在,否则返回 404
|
||
- 更新后自动触发重新向量化
|
||
|
||
### 5.6 删除文档
|
||
|
||
```
|
||
DELETE /documents/<path>
|
||
```
|
||
|
||
### 5.7 查看文件切片
|
||
|
||
```
|
||
GET /documents/<path>/chunks
|
||
```
|
||
|
||
**响应:**
|
||
```json
|
||
{
|
||
"success": true,
|
||
"document_id": "public/document.pdf",
|
||
"collection": "public_kb",
|
||
"chunks": [
|
||
{
|
||
"id": "chunk_001",
|
||
"content": "切片内容...",
|
||
"metadata": {"page": 1, "section": "第一章"}
|
||
}
|
||
],
|
||
"total": 25
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 5.8 🛠️ DEV 文档预览(引用溯源跳转)
|
||
|
||
> **⚠️ 开发环境自用接口,后端组无需调用。**
|
||
> 此接口仅供 dev-ui 内部前端实现引用跳转使用。后端组可根据 `citations` 中的 `chunk_index`、`page`、`bbox` 等字段自行实现文档定位功能。
|
||
|
||
```
|
||
GET /documents/<path>/preview?chunk_index=<N>&context=<M>
|
||
```
|
||
|
||
> **用途**:前端点击引用标签后,调用此接口跳转到文档中的具体切片位置。
|
||
> 复用现有切片查询逻辑,不新增存储。
|
||
|
||
**查询参数:**
|
||
|
||
| 参数 | 类型 | 必需 | 说明 |
|
||
|------|------|------|------|
|
||
| `chunk_index` | int | 是 | 目标切片序号(来自 citations 的 `chunk_index` 字段) |
|
||
| `context` | int | 否 | 上下文切片数,默认 2(前后各取 2 个) |
|
||
|
||
**响应:**
|
||
```json
|
||
{
|
||
"success": true,
|
||
"collection": "public_kb",
|
||
"source": "1.docx",
|
||
"total_chunks": 105,
|
||
"target_index": 33,
|
||
"chunks": [
|
||
{"id": "1.docx_31", "content": "...", "metadata": {...}, "is_target": false},
|
||
{"id": "1.docx_32", "content": "...", "metadata": {...}, "is_target": false},
|
||
{"id": "1.docx_33", "content": "...", "metadata": {...}, "is_target": true},
|
||
{"id": "1.docx_34", "content": "...", "metadata": {...}, "is_target": false},
|
||
{"id": "1.docx_35", "content": "...", "metadata": {...}, "is_target": false}
|
||
]
|
||
}
|
||
```
|
||
|
||
**字段说明:**
|
||
- `target_index`:目标切片的全局序号
|
||
- `chunks`:包含目标切片及其上下文的切片列表
|
||
- `is_target: true`:标记目标切片,前端可高亮显示并滚动到该位置
|
||
- 不传 `chunk_index` 时返回前 5 个切片作为文档概览
|
||
|
||
**前端调用示例:**
|
||
```javascript
|
||
// 用户点击引用标签时跳转
|
||
async function jumpToCitation(source, chunkIndex, collection) {
|
||
const path = `${collection}/${source}`;
|
||
const resp = await fetch(`/documents/${encodeURIComponent(path)}/preview?chunk_index=${chunkIndex}`);
|
||
const data = await resp.json();
|
||
// 打开文档预览模态框,高亮 is_target=true 的切片
|
||
showPreviewModal(data);
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 六、同步服务接口
|
||
|
||
> **注意**:同步服务用于检测文档变更并自动更新向量库。订阅通知功能由后端负责。
|
||
|
||
### 6.1 触发同步
|
||
|
||
```
|
||
POST /sync
|
||
```
|
||
|
||
**请求体:** 无需传递参数(同步所有知识库)
|
||
|
||
**响应(异步任务):**
|
||
```json
|
||
{
|
||
"success": true,
|
||
"status_code": 2010,
|
||
"message": "同步任务已启动",
|
||
"data": {
|
||
"task_id": "c3d4e5f6a1b2",
|
||
"message": "同步任务已启动,通过 GET /tasks/c3d4e5f6a1b2 查询进度"
|
||
}
|
||
}
|
||
```
|
||
|
||
> **⚠️ 异步变更**:此接口已从同步改为异步。不再直接返回同步结果,而是返回 `task_id`。后端需通过 `GET /tasks/<task_id>` 轮询任务状态,直到 `status` 为 `completed` 或 `failed`。任务完成后,`result` 字段包含完整的同步结果(含 `documents_processed`、`documents_added` 等)。
|
||
>
|
||
> **冲突检测**:如果已有同步任务正在运行,返回 HTTP 409:`{"error": "TASK_RUNNING", "message": "同步任务正在执行中 (task_id: xxx),请等待完成"}`
|
||
|
||
**后端轮询示例**:
|
||
|
||
```python
|
||
import time
|
||
import requests
|
||
|
||
def trigger_sync_and_wait():
|
||
"""触发同步并等待完成"""
|
||
# 1. 触发同步任务
|
||
resp = requests.post('http://rag-service:5001/sync')
|
||
task_id = resp.json()['data']['task_id']
|
||
|
||
# 2. 轮询任务状态(每 2 秒)
|
||
while True:
|
||
time.sleep(2)
|
||
status_resp = requests.get(f'http://rag-service:5001/tasks/{task_id}')
|
||
task_data = status_resp.json()['data']
|
||
|
||
if task_data['status'] == 'completed':
|
||
print(f"同步完成: {task_data['result']}")
|
||
return task_data['result']
|
||
elif task_data['status'] == 'failed':
|
||
raise Exception(f"同步失败: {task_data['error']}")
|
||
else:
|
||
print(f"同步中: {task_data['progress']}% - {task_data['message']}")
|
||
```
|
||
```
|
||
|
||
### 6.2 同步状态
|
||
|
||
```
|
||
GET /sync/status
|
||
```
|
||
|
||
**响应:**
|
||
```json
|
||
{
|
||
"enabled": true,
|
||
"monitoring": true,
|
||
"last_sync": "2026-04-19T18:30:00",
|
||
"documents_tracked": 150
|
||
}
|
||
```
|
||
|
||
### 6.3 同步历史
|
||
|
||
```
|
||
GET /sync/history?limit=20
|
||
```
|
||
|
||
**参数:**
|
||
|
||
| 参数 | 类型 | 必需 | 说明 |
|
||
|------|------|------|------|
|
||
| `limit` | int | ❌ | 返回记录数,默认 20 |
|
||
|
||
**响应:**
|
||
```json
|
||
{
|
||
"history": [
|
||
{
|
||
"sync_time": "2026-04-19T18:30:00",
|
||
"collection": "public_kb",
|
||
"added": 3,
|
||
"updated": 2,
|
||
"deleted": 1,
|
||
"status": "success"
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
### 6.4 变更日志
|
||
|
||
```
|
||
GET /sync/changes?limit=50&collection=public_kb
|
||
```
|
||
|
||
**参数:**
|
||
|
||
| 参数 | 类型 | 必需 | 说明 |
|
||
|------|------|------|------|
|
||
| `limit` | int | ❌ | 返回记录数,默认 50 |
|
||
| `collection` | string | ❌ | 过滤指定向量库 |
|
||
|
||
**响应:**
|
||
```json
|
||
{
|
||
"changes": [
|
||
{
|
||
"change_time": "2026-04-19T18:25:00",
|
||
"document": "规章制度/考勤制度.docx",
|
||
"change_type": "modified",
|
||
"collection": "public_kb"
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
### 6.5 启动/停止文件监控
|
||
|
||
```
|
||
POST /sync/start
|
||
POST /sync/stop
|
||
```
|
||
|
||
**响应:**
|
||
```json
|
||
{
|
||
"status": "success",
|
||
"status_code": 2010,
|
||
"message": "文件监控已启动"
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 七、出题接口
|
||
|
||
### 7.1 生成题目
|
||
|
||
```
|
||
POST /exam/generate
|
||
```
|
||
|
||
**请求体:**
|
||
```json
|
||
{
|
||
"file_path": "public/考勤制度.docx",
|
||
"collection": "dept_a_kb",
|
||
"question_types": {
|
||
"single_choice": 3,
|
||
"multiple_choice": 2,
|
||
"true_false": 2,
|
||
"fill_blank": 2,
|
||
"subjective": 1
|
||
},
|
||
"difficulty": 3,
|
||
"request_id": "可选,幂等性支持"
|
||
}
|
||
```
|
||
|
||
**参数说明:**
|
||
|
||
| 参数 | 类型 | 必需 | 说明 |
|
||
|------|------|------|------|
|
||
| `file_path` | string | ✅ | 文件路径(相对于 documents 目录) |
|
||
| `collection` | string 或 string[] | ✅ | 向量库名称,支持数组(按优先级排序) |
|
||
| `question_types` | object | ✅ | 题型及数量,键为题型名,值为数量 |
|
||
| `difficulty` | int | ❌ | 难度等级 1-5,默认 3 |
|
||
| `request_id` | string | ❌ | 请求 ID,相同 ID 返回缓存结果(幂等性) |
|
||
| `exclude_stems` | string[] | ❌ | 已有题目题干列表,用于跨调用去重(最多 100 条,传空或不传则不去重) |
|
||
|
||
**collection 参数说明:**
|
||
|
||
后端应根据用户权限传入可访问的向量库列表:
|
||
- 单个向量库:`"dept_a_kb"`
|
||
- 多个向量库:`["dept_a_kb", "public_kb"]`(按优先级排序,优先在第一个库检索)
|
||
|
||
#### 入参校验规则(2026-06-05 新增)
|
||
|
||
| 校验项 | 规则 | 失败返回 |
|
||
|--------|------|----------|
|
||
| question_types 题型 | 必须属于: single_choice, multiple_choice, true_false, fill_blank, subjective | HTTP 400 INVALID_PARAMS |
|
||
| question_types 数量 | 每种题型数量必须为非负整数 | HTTP 400 INVALID_PARAMS |
|
||
| difficulty | 必须为 1-5 的整数 | HTTP 400 INVALID_PARAMS |
|
||
| **总题数上限** | **所有题型数量之和不能超过 20** | HTTP 400 INVALID_PARAMS |
|
||
|
||
**响应(异步任务):**
|
||
|
||
```json
|
||
{
|
||
"success": true,
|
||
"status_code": 2020,
|
||
"message": "出题任务已启动",
|
||
"data": {
|
||
"task_id": "d4e5f6a1b2c3",
|
||
"message": "出题任务已启动 (10题),通过 GET /tasks/d4e5f6a1b2c3 查询结果"
|
||
}
|
||
}
|
||
```
|
||
|
||
> **⚠️ 异步变更**:此接口已从同步改为异步。响应仅返回 `task_id`,后端需通过 `GET /tasks/<task_id>` 轮询任务状态。任务完成后,`result` 字段包含完整出题结果(格式见下方说明)。
|
||
|
||
**轮询结果(GET /tasks/\<task_id\> 完成后的 result 字段)**:
|
||
|
||
```json
|
||
{
|
||
"success": true,
|
||
"request_id": "xxx",
|
||
"total": 10,
|
||
"source_chunks_used": 15,
|
||
"requested_types": {"single_choice": 3, "true_false": 2, "fill_blank": 2},
|
||
"actual_types": {"single_choice": 3, "true_false": 2, "fill_blank": 2},
|
||
"warnings": [],
|
||
"questions": [
|
||
{
|
||
"question_type": "single_choice",
|
||
"difficulty": 3,
|
||
"content": {
|
||
"stem": "题干内容",
|
||
"data": {
|
||
"options": [
|
||
{"key": "A", "content": "选项A"},
|
||
{"key": "B", "content": "选项B"},
|
||
{"key": "C", "content": "选项C"},
|
||
{"key": "D", "content": "选项D"}
|
||
]
|
||
},
|
||
"answer": "B",
|
||
"explanation": "答案解析"
|
||
},
|
||
"source_trace": {
|
||
"document_name": "考勤制度.docx",
|
||
"chunk_ids": ["chunk_001"],
|
||
"page_numbers": [5],
|
||
"sources": [
|
||
{
|
||
"chunk_id": "chunk_001",
|
||
"page": 5,
|
||
"section": "请假制度",
|
||
"snippet": "原文片段..."
|
||
}
|
||
]
|
||
}
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
> **`warnings` 字段说明**:当某题型实际生成数量少于请求数量时,`warnings` 数组会返回短缺提示(如 `["single_choice: 请求 5 道,实际生成 3 道"]`)。后端可据此判断是否需要重新出题。正常情况该数组为空。
|
||
|
||
**题型与 answer 格式对照:**
|
||
|
||
| 题型 | question_type | answer 格式 | data 字段 |
|
||
|------|---------------|-------------|-----------|
|
||
| 单选题 | single_choice | `"B"` | `options[]` |
|
||
| 多选题 | multiple_choice | `["A", "C"]` | `options[]` |
|
||
| 判断题 | true_false | `"T"` 或 `"F"` | 无 |
|
||
| 填空题 | fill_blank | `[["答案1"], ["答案2", "同义词"]]` | `blank_count` |
|
||
| 简答题 | subjective | `"参考范文..."` | `scoring_points[]` |
|
||
|
||
**后端职责:**
|
||
|
||
| 操作 | 说明 |
|
||
|------|------|
|
||
| 权限校验 | 判断用户是否有出题权限(通常为管理员) |
|
||
| 生成 question_id | 入库时生成 UUID |
|
||
| 设置 score | 根据题型或配置设定满分 |
|
||
| 添加 tags | 根据业务需求添加标签 |
|
||
| 审核入库 | 人工或自动审核后存入题库 |
|
||
|
||
**错误响应格式:**
|
||
|
||
```json
|
||
{
|
||
"success": false,
|
||
"error": "错误描述",
|
||
"error_code": "ERROR_CODE"
|
||
}
|
||
```
|
||
|
||
**常见错误码:**
|
||
|
||
| 错误码 | HTTP 状态码 | 说明 |
|
||
|--------|-------------|------|
|
||
| `FILE_NOT_FOUND` | 404 | 指定文件不存在 |
|
||
| `COLLECTION_NOT_FOUND` | 404 | 指定向量库不存在 |
|
||
| `NO_CONTENT` | 400 | 文件内容为空,无法出题 |
|
||
| `INVALID_PARAMS` | 400 | 入参校验失败(题型不合法、数量超限、difficulty 范围错误等) |
|
||
| `LLM_ERROR` | 500 | LLM 调用失败 |
|
||
| `PARSE_ERROR` | 500 | 解析 LLM 响应失败 |
|
||
|
||
**幂等性说明:**
|
||
|
||
- 传入 `request_id` 时,相同 ID 会返回缓存结果
|
||
- 缓存有效期:24 小时
|
||
- 建议后端生成 UUID 作为 `request_id`,便于追踪和去重
|
||
|
||
### 7.2 批改答案
|
||
|
||
> **⚠️ 重要变更(2026-06-05)**:批卷请求中的 `question_content` 字段已更名为 `content`,与出题接口返回的题目 `content` 字段保持一致。后端可直接将出题结果的 `content` 透传到批卷接口,无需额外转换。
|
||
|
||
```
|
||
POST /exam/grade
|
||
```
|
||
|
||
#### 请求体
|
||
|
||
```json
|
||
{
|
||
"request_id": "可选,用于幂等性追踪",
|
||
"answers": [
|
||
{
|
||
"question_id": "uuid-001",
|
||
"question_type": "single_choice",
|
||
"content": {
|
||
"stem": "题干内容",
|
||
"data": {"options": [{"key": "A", "content": "选项A"}, {"key": "B", "content": "选项B"}]},
|
||
"answer": "B"
|
||
},
|
||
"student_answer": "A",
|
||
"max_score": 2.0
|
||
},
|
||
{
|
||
"question_id": "uuid-002",
|
||
"question_type": "multiple_choice",
|
||
"content": {
|
||
"stem": "多选题题干",
|
||
"data": {"options": [...]},
|
||
"answer": ["A", "C"]
|
||
},
|
||
"student_answer": ["A", "B"],
|
||
"max_score": 4.0
|
||
},
|
||
{
|
||
"question_id": "uuid-003",
|
||
"question_type": "true_false",
|
||
"content": {
|
||
"stem": "判断题题干",
|
||
"answer": "T"
|
||
},
|
||
"student_answer": "F",
|
||
"max_score": 2.0
|
||
},
|
||
{
|
||
"question_id": "uuid-004",
|
||
"question_type": "fill_blank",
|
||
"content": {
|
||
"stem": "填空题有___个空",
|
||
"answer": [["答案1", "同义词1"], ["答案2"]]
|
||
},
|
||
"student_answer": ["学生答案1", "学生答案2"],
|
||
"max_score": 4.0
|
||
},
|
||
{
|
||
"question_id": "uuid-005",
|
||
"question_type": "subjective",
|
||
"content": {
|
||
"stem": "简答题题干",
|
||
"data": {
|
||
"scoring_points": [
|
||
{"point": "要点1", "weight": 0.4},
|
||
{"point": "要点2", "weight": 0.6}
|
||
]
|
||
},
|
||
"answer": "参考答案..."
|
||
},
|
||
"student_answer": "学生作答内容...",
|
||
"max_score": 10.0
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
#### 请求参数说明
|
||
|
||
| 字段 | 类型 | 必填 | 说明 |
|
||
|------|------|------|------|
|
||
| `request_id` | string | 否 | 请求 ID,用于追踪和幂等性 |
|
||
| `answers` | array | 是 | 答案列表 |
|
||
|
||
**answers 数组中每个对象的字段:**
|
||
|
||
| 字段 | 类型 | 必填 | 说明 |
|
||
|------|------|------|------|
|
||
| `question_id` | string | **是** | 题目 ID(后端生成,用于匹配结果) |
|
||
| `question_type` | string | 是 | 题型,**必须属于**:single_choice/multiple_choice/true_false/fill_blank/subjective,无效值返回 HTTP 400 |
|
||
| `content` | object | 是 | 题目内容(从出题结果中获取) |
|
||
| `student_answer` | any | 是 | 学生答案(格式见下表) |
|
||
| `max_score` | number | 是 | 该题满分 |
|
||
|
||
**各题型 student_answer 格式:**
|
||
|
||
| 题型 | 格式 | 示例 |
|
||
|------|------|------|
|
||
| single_choice | string | `"B"` |
|
||
| multiple_choice | array | `["A", "C"]` |
|
||
| true_false | string | `"T"` 或 `"F"` |
|
||
| fill_blank | array | `["答案1", "答案2"]` |
|
||
| subjective | string | `"学生作答的长文本..."` |
|
||
|
||
#### 响应
|
||
|
||
**响应(异步任务):**
|
||
```json
|
||
{
|
||
"success": true,
|
||
"status_code": 2021,
|
||
"message": "批阅任务已启动",
|
||
"data": {
|
||
"task_id": "f6a1b2c3d4e5",
|
||
"message": "批阅任务已启动 (5题),通过 GET /tasks/f6a1b2c3d4e5 查询结果"
|
||
}
|
||
}
|
||
```
|
||
|
||
> **⚠️ 异步变更**:此接口已从同步改为异步。响应仅返回 `task_id`,后端需通过 `GET /tasks/<task_id>` 轮询任务状态。任务完成后,`result` 字段包含完整批阅结果(格式见下方说明)。
|
||
|
||
**轮询结果(GET /tasks/\<task_id\> 完成后的 result 字段)**:
|
||
|
||
```json
|
||
{
|
||
"success": true,
|
||
"request_id": "可选,原样返回",
|
||
"total_score": 12.5,
|
||
"total_max_score": 22.0,
|
||
"score_rate": 56.8,
|
||
"results": [
|
||
{
|
||
"question_id": "uuid-001",
|
||
"score": 0,
|
||
"max_score": 2.0,
|
||
"grading_status": "success",
|
||
"details": {
|
||
"correct": false,
|
||
"student_answer": "A",
|
||
"correct_answer": "B",
|
||
"feedback": "正确答案: B"
|
||
}
|
||
},
|
||
{
|
||
"question_id": "uuid-004",
|
||
"score": 2.0,
|
||
"max_score": 4.0,
|
||
"grading_status": "success",
|
||
"details": {
|
||
"total_blanks": 2,
|
||
"correct_blanks": 1,
|
||
"blank_scores": [2.0, 0],
|
||
"feedback": "2 个空中答对 1 个"
|
||
}
|
||
},
|
||
{
|
||
"question_id": "uuid-005",
|
||
"score": 7.5,
|
||
"max_score": 10.0,
|
||
"grading_status": "success",
|
||
"details": {
|
||
"scoring_breakdown": [
|
||
{"point": "要点1", "weight": 0.4, "achieved": 0.35, "comment": "部分掌握"},
|
||
{"point": "要点2", "weight": 0.6, "achieved": 0.55, "comment": "基本掌握"}
|
||
],
|
||
"highlights": ["思路清晰"],
|
||
"shortcomings": ["细节不够完整"],
|
||
"overall_feedback": "整体回答良好",
|
||
"warnings": ["缺少评分标准(scoring_points),评分结果仅供参考"]
|
||
}
|
||
},
|
||
{
|
||
"question_id": "uuid-006",
|
||
"score": 0,
|
||
"max_score": 10.0,
|
||
"grading_status": "failed",
|
||
"details": {
|
||
"error": "评分结果解析失败"
|
||
}
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
#### grading_status 状态说明(2026-06-05 新增)
|
||
|
||
| 状态 | 说明 |
|
||
|------|------|
|
||
| `success` | 评分成功,score 和 details 有效 |
|
||
| `failed` | 评分失败(LLM 解析失败或超时),score 为 0,details 包含 error 描述 |
|
||
|
||
#### 批卷逻辑说明
|
||
|
||
| 题型 | 批卷方式 | 说明 |
|
||
|------|----------|------|
|
||
| single_choice | 本地判断 | 学生答案 == 正确答案 |
|
||
| multiple_choice | 本地判断 | set(学生答案) == set(正确答案),顺序无关 |
|
||
| true_false | 本地判断 | 学生答案 == 正确答案 |
|
||
| fill_blank | 本地模糊匹配 | 按空给分,支持同义词匹配(忽略大小写和空格) |
|
||
| subjective | LLM 评分 | 并发调用 LLM,带重试和限流机制 |
|
||
|
||
#### 注意事项
|
||
|
||
1. **question_id 必填**:用于匹配批卷结果,后端需在调用时传入
|
||
2. **顺序保证**:results 数组顺序与 answers 数组顺序一致
|
||
3. **主观题超时**:主观题批阅有 15 秒超时,失败时返回 score=0
|
||
4. **填空题同义词**:answer 字段支持同义词数组,如 `[["北京", "Beijing"]]`
|
||
|
||
#### 后端对接流程
|
||
|
||
**重要:RAG 服务是无状态的,不存储题库数据。所有题目信息需由后端传入。**
|
||
|
||
```
|
||
完整批卷流程:
|
||
|
||
┌─────────────┐ ┌─────────────┐
|
||
│ 后端 │ │ RAG 服务 │
|
||
├─────────────┤ ├─────────────┤
|
||
│ 1. 接收学生答案 │ │
|
||
│ 2. 查询数据库获取题目 │ │
|
||
│ 3. 组装请求 ────────────────────▶ │ 4. 批卷处理 │
|
||
│ │ - 客观题:本地比对
|
||
│ │ - 主观题:LLM评分
|
||
│ 6. 更新学生成绩 ◀──────────────── │ 5. 返回结果 │
|
||
│ (根据 question_id 匹配) │ │
|
||
└─────────────┘ └─────────────┘
|
||
```
|
||
|
||
**Step 1: 接收学生答案**
|
||
|
||
```python
|
||
# 学生提交的答案
|
||
student_answers = {
|
||
"exam_id": "exam-uuid",
|
||
"student_id": "student-uuid",
|
||
"answers": [
|
||
{"question_id": "q-001", "answer": "B"},
|
||
{"question_id": "q-002", "answer": ["A", "C"]},
|
||
{"question_id": "q-003", "answer": "三峡水库主要用于防洪..."}
|
||
]
|
||
}
|
||
```
|
||
|
||
**Step 2: 从数据库查询题目信息**
|
||
|
||
```python
|
||
def get_questions_for_grading(exam_id, answer_list):
|
||
"""根据 question_id 批量查询题目信息"""
|
||
question_ids = [a['question_id'] for a in answer_list]
|
||
|
||
questions = db.query("""
|
||
SELECT question_id, question_type, content, score
|
||
FROM questions
|
||
WHERE question_id IN (?)
|
||
""", question_ids)
|
||
|
||
# 转为字典方便查找
|
||
return {q.question_id: q for q in questions}
|
||
```
|
||
|
||
**Step 3: 组装批卷请求**
|
||
|
||
```python
|
||
def build_grade_request(answer_list, questions_map):
|
||
"""组装 RAG 批卷接口所需的请求格式"""
|
||
grade_answers = []
|
||
|
||
for ans in answer_list:
|
||
qid = ans['question_id']
|
||
question = questions_map.get(qid)
|
||
|
||
if not question:
|
||
continue
|
||
|
||
grade_answers.append({
|
||
"question_id": qid,
|
||
"question_type": question.question_type,
|
||
"content": question.content, # 含正确答案,与出题接口 content 结构一致
|
||
"student_answer": ans['answer'],
|
||
"max_score": question.score
|
||
})
|
||
|
||
return {"answers": grade_answers}
|
||
```
|
||
|
||
**Step 4: 调用 RAG 批卷接口(异步任务)**
|
||
|
||
```python
|
||
import time
|
||
|
||
def call_rag_grade(grade_request):
|
||
"""调用 RAG 批卷接口并轮询等待结果"""
|
||
# 1. 提交批阅任务
|
||
response = requests.post(
|
||
'http://rag-service:5001/exam/grade',
|
||
json=grade_request
|
||
)
|
||
task_id = response.json()['data']['task_id']
|
||
|
||
# 2. 轮询任务状态(每 2 秒)
|
||
while True:
|
||
time.sleep(2)
|
||
status_resp = requests.get(f'http://rag-service:5001/tasks/{task_id}')
|
||
task_data = status_resp.json()['data']
|
||
|
||
if task_data['status'] == 'completed':
|
||
return task_data['result']
|
||
elif task_data['status'] == 'failed':
|
||
raise Exception(f"批阅失败: {task_data['error']}")
|
||
```
|
||
|
||
**Step 5: 更新学生成绩**
|
||
|
||
```python
|
||
def update_student_scores(student_id, exam_id, grade_result):
|
||
"""根据批卷结果更新学生成绩"""
|
||
for result in grade_result['results']:
|
||
qid = result['question_id']
|
||
|
||
db.execute("""
|
||
INSERT INTO student_answers (
|
||
student_id, exam_id, question_id,
|
||
score, max_score, details
|
||
) VALUES (?, ?, ?, ?, ?, ?)
|
||
""",
|
||
student_id, exam_id, qid,
|
||
result['score'], result['max_score'],
|
||
json.dumps(result.get('details', {}))
|
||
)
|
||
|
||
# 更新总分
|
||
db.execute("""
|
||
UPDATE student_exams
|
||
SET total_score = ?, score_rate = ?, graded_at = NOW()
|
||
WHERE student_id = ? AND exam_id = ?
|
||
""",
|
||
grade_result['total_score'],
|
||
grade_result['score_rate'],
|
||
student_id, exam_id
|
||
)
|
||
```
|
||
|
||
**完整调用示例**
|
||
|
||
```python
|
||
def grade_student_exam(student_answers):
|
||
"""批阅学生试卷完整流程"""
|
||
|
||
# 1. 查询题目信息
|
||
questions_map = get_questions_for_grading(
|
||
student_answers['exam_id'],
|
||
student_answers['answers']
|
||
)
|
||
|
||
# 2. 组装请求
|
||
grade_request = build_grade_request(
|
||
student_answers['answers'],
|
||
questions_map
|
||
)
|
||
|
||
# 3. 调用 RAG 批卷
|
||
grade_result = call_rag_grade(grade_request)
|
||
|
||
# 4. 更新成绩
|
||
update_student_scores(
|
||
student_answers['student_id'],
|
||
student_answers['exam_id'],
|
||
grade_result
|
||
)
|
||
|
||
return grade_result
|
||
```
|
||
|
||
#### 数据来源说明
|
||
|
||
| 字段 | 来源 | 说明 |
|
||
|------|------|------|
|
||
| `question_id` | 后端数据库 | 用于匹配返回结果,更新成绩 |
|
||
| `question_type` | 后端数据库 | 题型,决定批卷方式 |
|
||
| `content.answer` | 后端数据库 | 正确答案(客观题直接比对,主观题作为参考) |
|
||
| `content.data` | 后端数据库 | 题目附加数据(选项、评分标准等) |
|
||
| `student_answer` | 学生提交 | 学生作答内容 |
|
||
| `max_score` | 后端数据库 | 该题满分 |
|
||
|
||
---
|
||
|
||
## 七点五、出题接口 - 后端对接指南
|
||
|
||
### 7.5.1 后端需要做的事情
|
||
|
||
**Step 1: 权限校验**
|
||
|
||
```python
|
||
def check_exam_permission(user_id):
|
||
"""检查用户是否有出题权限"""
|
||
user = get_user(user_id)
|
||
# 通常只有管理员和部门管理员有出题权限
|
||
return user.role in ['admin', 'manager']
|
||
```
|
||
|
||
**Step 2: 获取用户可访问的向量库**
|
||
|
||
```python
|
||
def get_user_collections(user_id):
|
||
"""获取用户有权限的向量库列表(按优先级排序)"""
|
||
permissions = db.query("""
|
||
SELECT kb_name, permission
|
||
FROM kb_permissions
|
||
WHERE user_id = ?
|
||
ORDER BY
|
||
CASE permission
|
||
WHEN 'admin' THEN 1
|
||
WHEN 'write' THEN 2
|
||
WHEN 'read' THEN 3
|
||
END
|
||
""", user_id)
|
||
|
||
return [p.kb_name for p in permissions]
|
||
```
|
||
|
||
**Step 3: 调用 RAG 出题接口**
|
||
|
||
```python
|
||
def generate_exam(user_id, file_path, question_types, difficulty=3):
|
||
# 1. 权限校验
|
||
if not check_exam_permission(user_id):
|
||
raise PermissionError("无出题权限")
|
||
|
||
# 2. 获取向量库列表
|
||
collections = get_user_collections(user_id)
|
||
|
||
# 3. 调用 RAG 服务
|
||
response = requests.post(
|
||
'http://rag-service:5001/exam/generate',
|
||
json={
|
||
'file_path': file_path,
|
||
'collection': collections, # 传入数组
|
||
'question_types': question_types,
|
||
'difficulty': difficulty
|
||
}
|
||
)
|
||
|
||
result = response.json()
|
||
if not result.get('success'):
|
||
raise Exception(result.get('error'))
|
||
|
||
return result['questions']
|
||
```
|
||
|
||
**Step 4: 入库存储**
|
||
|
||
```python
|
||
def save_questions_to_db(questions, exam_id, creator_id):
|
||
"""将题目存入数据库"""
|
||
for q in questions:
|
||
# 生成 question_id
|
||
question_id = str(uuid.uuid4())
|
||
|
||
# 根据题型设置满分
|
||
score = get_default_score(q['question_type'])
|
||
|
||
# 存入数据库
|
||
db.execute("""
|
||
INSERT INTO questions (
|
||
question_id, exam_id, question_type, difficulty,
|
||
content, source_trace, score, tags, creator_id, status
|
||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'pending')
|
||
""",
|
||
question_id, exam_id, q['question_type'], q['difficulty'],
|
||
json.dumps(q['content']), json.dumps(q['source_trace']),
|
||
score, json.dumps([]), creator_id
|
||
)
|
||
```
|
||
|
||
**Step 5: 题型默认分值参考**
|
||
|
||
```python
|
||
def get_default_score(question_type):
|
||
"""根据题型获取默认满分"""
|
||
score_map = {
|
||
'single_choice': 2.0,
|
||
'multiple_choice': 4.0,
|
||
'true_false': 2.0,
|
||
'fill_blank': 3.0,
|
||
'subjective': 10.0
|
||
}
|
||
return score_map.get(question_type, 2.0)
|
||
```
|
||
|
||
### 7.5.2 数据库设计建议
|
||
|
||
**题目表 (questions)**
|
||
```sql
|
||
CREATE TABLE questions (
|
||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||
question_id VARCHAR(64) UNIQUE NOT NULL, -- 后端生成的 UUID
|
||
exam_id VARCHAR(64), -- 所属试卷
|
||
question_type VARCHAR(32) NOT NULL, -- 题型
|
||
difficulty INT DEFAULT 3, -- 难度
|
||
content JSON NOT NULL, -- 题目内容
|
||
source_trace JSON, -- 溯源信息
|
||
score DECIMAL(4,1) DEFAULT 2.0, -- 满分(后端设置)
|
||
tags JSON, -- 标签(后端设置)
|
||
creator_id VARCHAR(64), -- 创建人
|
||
status ENUM('pending', 'approved', 'rejected') DEFAULT 'pending',
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
|
||
INDEX idx_exam_id (exam_id),
|
||
INDEX idx_question_type (question_type),
|
||
INDEX idx_status (status)
|
||
);
|
||
```
|
||
|
||
**试卷表 (exams)**
|
||
```sql
|
||
CREATE TABLE exams (
|
||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||
exam_id VARCHAR(64) UNIQUE NOT NULL,
|
||
title VARCHAR(255),
|
||
total_score DECIMAL(6,1),
|
||
question_count INT,
|
||
creator_id VARCHAR(64),
|
||
status ENUM('draft', 'published', 'archived') DEFAULT 'draft',
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||
);
|
||
```
|
||
|
||
---
|
||
|
||
## 八、后端数据库设计建议
|
||
|
||
### 8.1 会话表 (sessions)
|
||
|
||
```sql
|
||
CREATE TABLE sessions (
|
||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||
session_id VARCHAR(64) UNIQUE NOT NULL,
|
||
user_id VARCHAR(64) NOT NULL,
|
||
title VARCHAR(255), -- 会话标题(可从首条消息生成)
|
||
last_message TEXT, -- 最后一条消息摘要
|
||
message_count INT DEFAULT 0, -- 消息数量
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||
|
||
INDEX idx_user_id (user_id),
|
||
INDEX idx_updated_at (updated_at)
|
||
);
|
||
```
|
||
|
||
### 8.2 消息表 (messages)
|
||
|
||
```sql
|
||
CREATE TABLE messages (
|
||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||
session_id VARCHAR(64) NOT NULL,
|
||
role ENUM('user', 'assistant') NOT NULL,
|
||
content TEXT NOT NULL, -- 完整消息内容
|
||
sources JSON, -- AI 回答的来源(仅 assistant)
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
|
||
INDEX idx_session_id (session_id),
|
||
FOREIGN KEY (session_id) REFERENCES sessions(session_id) ON DELETE CASCADE
|
||
);
|
||
```
|
||
|
||
### 8.3 知识库权限表 (kb_permissions)
|
||
|
||
```sql
|
||
CREATE TABLE kb_permissions (
|
||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||
user_id VARCHAR(64) NOT NULL,
|
||
kb_name VARCHAR(64) NOT NULL, -- 知识库名称,如 'public_kb', 'dept_finance'
|
||
permission ENUM('read', 'write', 'admin') DEFAULT 'read',
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
|
||
UNIQUE KEY uk_user_kb (user_id, kb_name),
|
||
INDEX idx_user_id (user_id)
|
||
);
|
||
```
|
||
|
||
**权限判断逻辑:**
|
||
```python
|
||
def get_user_collections(user_id):
|
||
# 查询用户有权限的知识库
|
||
permissions = db.query(
|
||
"SELECT kb_name FROM kb_permissions WHERE user_id = ?",
|
||
user_id
|
||
)
|
||
return [p.kb_name for p in permissions]
|
||
```
|
||
|
||
### 8.4 文档版本表 (document_versions) - 新增
|
||
|
||
> **功能**:记录文档的版本信息,支持版本追溯和废止管理。
|
||
|
||
```sql
|
||
CREATE TABLE document_versions (
|
||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||
|
||
-- 文档标识
|
||
document_id VARCHAR(512) NOT NULL, -- 文档ID(文件名或路径)
|
||
collection VARCHAR(64) NOT NULL, -- 所属向量库
|
||
|
||
-- 版本信息
|
||
version VARCHAR(32) NOT NULL, -- 版本号,如 'v1', 'v2'
|
||
status ENUM('draft', 'active', 'deprecated', 'superseded') DEFAULT 'active',
|
||
|
||
-- 时间信息
|
||
effective_date TIMESTAMP, -- 生效日期
|
||
deprecated_date TIMESTAMP, -- 废止日期
|
||
|
||
-- 变更信息
|
||
deprecated_reason TEXT, -- 废止原因
|
||
change_summary TEXT, -- 变更摘要
|
||
supersedes VARCHAR(32), -- 替代的旧版本号
|
||
|
||
-- 元数据
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
created_by VARCHAR(64), -- 创建者用户ID
|
||
chunk_count INT DEFAULT 0, -- 切片数量
|
||
|
||
INDEX idx_collection_doc (collection, document_id),
|
||
INDEX idx_status (status),
|
||
UNIQUE KEY uk_doc_version (collection, document_id, version)
|
||
);
|
||
```
|
||
|
||
**字段说明:**
|
||
|
||
| 字段 | 类型 | 说明 |
|
||
|------|------|------|
|
||
| `document_id` | VARCHAR(512) | 文档标识,通常为文件名 |
|
||
| `collection` | VARCHAR(64) | 所属向量库名称 |
|
||
| `version` | VARCHAR(32) | 版本号 |
|
||
| `status` | ENUM | 文档状态:draft/active/deprecated/superseded |
|
||
| `effective_date` | TIMESTAMP | 生效日期 |
|
||
| `deprecated_date` | TIMESTAMP | 废止日期(废止时设置) |
|
||
| `deprecated_reason` | TEXT | 废止原因 |
|
||
| `change_summary` | TEXT | 版本变更摘要 |
|
||
| `supersedes` | VARCHAR(32) | 被替代的旧版本号 |
|
||
| `chunk_count` | INT | 该版本的切片数量 |
|
||
|
||
### 8.5 版本变更日志表 (version_change_logs) - 新增
|
||
|
||
> **功能**:记录文档版本的变更历史,便于审计追踪。
|
||
|
||
```sql
|
||
CREATE TABLE version_change_logs (
|
||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||
|
||
-- 文档标识
|
||
document_id VARCHAR(512) NOT NULL,
|
||
collection VARCHAR(64) NOT NULL,
|
||
|
||
-- 变更信息
|
||
old_version VARCHAR(32), -- 旧版本号
|
||
new_version VARCHAR(32), -- 新版本号
|
||
old_status VARCHAR(32), -- 旧状态
|
||
new_status VARCHAR(32), -- 新状态
|
||
change_type VARCHAR(32) NOT NULL, -- 变更类型:update/deprecate/restore
|
||
|
||
-- 变更原因
|
||
reason TEXT,
|
||
changed_by VARCHAR(64), -- 操作者用户ID
|
||
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
|
||
INDEX idx_collection_doc (collection, document_id),
|
||
INDEX idx_change_type (change_type),
|
||
INDEX idx_created_at (created_at)
|
||
);
|
||
```
|
||
|
||
**变更类型说明:**
|
||
|
||
| change_type | 说明 |
|
||
|-------------|------|
|
||
| `update` | 更新文档(上传新版本) |
|
||
| `deprecate` | 废止文档 |
|
||
| `restore` | 恢复已废止文档 |
|
||
|
||
### 8.6 向量库切片元数据扩展
|
||
|
||
> **说明**:在 ChromaDB 的 metadata 中增加 `status` 字段,支持软删除。
|
||
|
||
**切片 metadata 字段:**
|
||
|
||
```json
|
||
{
|
||
"source": "报销制度.pdf",
|
||
"page": 5,
|
||
"section": "第一章",
|
||
"chunk_type": "text",
|
||
"status": "active", // 新增:active / deprecated
|
||
"version": "v2", // 新增:文档版本号
|
||
"deprecated_date": null // 新增:废止时间(如有)
|
||
}
|
||
```
|
||
|
||
**检索时过滤:**
|
||
- 默认只返回 `status: "active"` 的切片
|
||
- 废止的切片不参与检索,但保留在向量库中
|
||
- 可通过恢复接口将状态改回 `active`
|
||
|
||
---
|
||
|
||
## 九、完整调用流程示例
|
||
|
||
### 9.1 用户发起问答
|
||
|
||
```
|
||
1. 前端发送请求到后端
|
||
POST /api/chat
|
||
{
|
||
"session_id": "xxx",
|
||
"message": "出差补助标准是什么?"
|
||
}
|
||
|
||
2. 后端处理
|
||
a. 验证用户身份(JWT)
|
||
b. 查询用户知识库权限,生成 collections 列表
|
||
c. 查询会话历史(最近 5-10 条)
|
||
d. 调用 RAG 服务
|
||
|
||
3. 后端调用 RAG
|
||
POST http://rag-service:5001/rag
|
||
{
|
||
"message": "出差补助标准是什么?",
|
||
"collections": ["public_kb", "dept_finance"],
|
||
"chat_history": [
|
||
{"role": "user", "content": "之前的问题"},
|
||
{"role": "assistant", "content": "之前的回答"}
|
||
]
|
||
}
|
||
|
||
4. RAG 返回结果
|
||
{
|
||
"answer": "根据公司规定...",
|
||
"sources": [...],
|
||
"duration_ms": 1500
|
||
}
|
||
|
||
5. 后端存储
|
||
a. 保存用户问题到 messages 表
|
||
b. 保存 AI 回答到 messages 表
|
||
c. 更新 sessions 表的 last_message、updated_at
|
||
|
||
6. 后端返回前端
|
||
{
|
||
"answer": "根据公司规定...",
|
||
"sources": [...]
|
||
}
|
||
```
|
||
|
||
### 9.2 用户上传文档
|
||
|
||
```
|
||
1. 前端发送文件到后端
|
||
POST /api/documents/upload
|
||
Content-Type: multipart/form-data
|
||
file: document.pdf
|
||
collection: dept_finance
|
||
|
||
2. 后端验证权限
|
||
- 检查用户是否有 dept_finance 的 write 权限
|
||
|
||
3. 后端调用 RAG
|
||
POST http://rag-service:5001/documents/upload
|
||
Content-Type: multipart/form-data
|
||
file: document.pdf
|
||
collection: dept_finance
|
||
|
||
4. RAG 返回结果
|
||
{
|
||
"success": true,
|
||
"file": {"filename": "document.pdf", "path": "finance/document.pdf"}
|
||
}
|
||
|
||
5. 后端返回前端
|
||
```
|
||
|
||
---
|
||
|
||
## 十、错误响应格式
|
||
|
||
错误响应存在两种格式,后端需同时兼容:
|
||
|
||
**格式一:统一错误格式**(大部分接口使用)
|
||
|
||
```json
|
||
{
|
||
"success": false,
|
||
"status": "failed",
|
||
"error_code": "MISSING_PARAMS",
|
||
"status_code": 4000,
|
||
"message": "缺少 file_path 或 collection 参数"
|
||
}
|
||
```
|
||
|
||
**格式二:简单错误格式**(部分接口的异常处理使用)
|
||
|
||
```json
|
||
{
|
||
"error": "错误描述信息"
|
||
}
|
||
```
|
||
|
||
**常见 HTTP 状态码:**
|
||
- `400` - 请求参数错误
|
||
- `401` - 未认证
|
||
- `403` - 权限不足
|
||
- `404` - 资源不存在
|
||
- `500` - 服务器内部错误
|
||
- `503` - 服务不可用
|
||
|
||
---
|
||
|
||
## 十一、向量库命名规范
|
||
|
||
ChromaDB 集合名称限制:
|
||
- 只能包含 `[a-zA-Z0-9._-]`
|
||
- 长度 3-63 字符
|
||
- 必须以字母或数字开头和结尾
|
||
|
||
**推荐命名:**
|
||
- `public_kb` - 公开知识库
|
||
- `dept_finance` - 财务部知识库
|
||
- `dept_hr` - 人事部知识库
|
||
- `dept_tech` - 技术部知识库
|
||
|
||
---
|
||
|
||
## 十二、部署注意事项
|
||
|
||
1. **环境变量**:
|
||
- `DEV_MODE=false` - 生产环境必须关闭开发模式
|
||
- `DOCUMENTS_PATH` - 文档存储路径
|
||
|
||
2. **网络配置**:
|
||
- RAG 服务端口:5001(默认)
|
||
- 后端网关需要配置反向代理
|
||
|
||
3. **文件存储**:
|
||
- 文档存储在 `documents/` 目录
|
||
- 向量库存储在 `vector_store/chroma/` 目录
|
||
|
||
4. **资源要求**:
|
||
- 内存:建议 4GB+(向量检索占用)
|
||
- 磁盘:根据文档数量评估
|
||
|
||
5. **数据库要求(新增)**:
|
||
- 需要 SQLite 或其他数据库支持文档版本管理
|
||
- 数据库路径:`data/knowledge.db`(默认)
|
||
- 首次启动会自动创建 `document_versions` 和 `version_change_logs` 表
|
||
|
||
---
|
||
|
||
## 十三、API 端点汇总
|
||
|
||
### 13.1 核心接口
|
||
|
||
| 端点 | 方法 | 说明 |
|
||
|------|------|------|
|
||
| `/chat` | POST | 普通聊天 |
|
||
| `/rag` | POST | 知识库问答(SSE 流式) |
|
||
| `/search` | POST | 混合检索 |
|
||
|
||
### 13.2 向量库管理
|
||
|
||
| 端点 | 方法 | 说明 |
|
||
|------|------|------|
|
||
| `/collections` | GET | 获取向量库列表 |
|
||
| `/collections` | POST | 创建向量库 |
|
||
| `/collections/<name>` | PUT | 修改向量库 |
|
||
| `/collections/<name>` | DELETE | 删除向量库 |
|
||
| `/collections/<name>/documents` | GET | 获取向量库文档列表 |
|
||
| `/collections/<name>/chunks` | GET | 获取向量库切片列表 |
|
||
| `/collections/<name>/update-image-descriptions` | POST | 更新图片描述 |
|
||
|
||
### 13.3 文档版本管理(新增)
|
||
|
||
| 端点 | 方法 | 说明 |
|
||
|------|------|------|
|
||
| `/collections/<kb_name>/documents/<filename>/deprecate` | POST | 废止文档 |
|
||
| `/collections/<kb_name>/documents/<filename>/restore` | POST | 恢复文档 |
|
||
| `/collections/<kb_name>/documents/<filename>/versions` | GET | 获取版本历史 |
|
||
|
||
### 13.4 文档管理
|
||
|
||
| 端点 | 方法 | 说明 |
|
||
|------|------|------|
|
||
| `/documents/upload` | POST | 上传文件 |
|
||
| `/documents/batch-upload` | POST | 批量上传 |
|
||
| `/documents/list` | GET | 文档列表 |
|
||
| `/documents/<path>/status` | GET | 获取文档处理状态 |
|
||
| `/documents/<path>` | PUT | 更新/替换文档 |
|
||
| `/documents/<path>` | DELETE | 删除文档 |
|
||
|
||
### 13.5 切片管理
|
||
|
||
| 端点 | 方法 | 说明 |
|
||
|------|------|------|
|
||
| `/documents/<path>/chunks` | GET | 查看文件切片 |
|
||
| `/documents/<path>/preview` 🛠️ | GET | 文档预览(dev-ui 自用,按 `chunk_index` 跳转) |
|
||
| `/chunks` | POST | 新增切片 |
|
||
| `/chunks/<id>` | PUT | 修改切片 |
|
||
| `/chunks/<id>` | DELETE | 删除切片 |
|
||
|
||
### 13.6 同步服务
|
||
|
||
| 端点 | 方法 | 说明 |
|
||
|------|------|------|
|
||
| `/sync` | POST | 触发同步(**异步任务,返回 task_id**) |
|
||
| `/sync/status` | GET | 同步状态 |
|
||
| `/sync/history` | GET | 同步历史 |
|
||
| `/sync/changes` | GET | 变更日志 |
|
||
| `/sync/start` | POST | 启动文件监控 |
|
||
| `/sync/stop` | POST | 停止文件监控 |
|
||
| `/documents/sync` | POST | 触发文档同步(**异步任务,返回 task_id**) |
|
||
| `/collections/<kb_name>/reindex` | POST | 重建索引(**异步任务,返回 task_id**) |
|
||
|
||
### 13.7 出题系统
|
||
|
||
| 端点 | 方法 | 说明 |
|
||
|------|------|------|
|
||
| `/exam/generate` | POST | 生成题目(**异步任务,返回 task_id**) |
|
||
| `/exam/generate-smart` | POST | AI 智能出题(**异步任务,返回 task_id**) |
|
||
| `/exam/grade` | POST | 批改答案(**异步任务,返回 task_id**) |
|
||
| `/exam/health` | GET | 出题服务健康检查 |
|
||
|
||
### 13.8 反馈与 FAQ 管理
|
||
|
||
| 端点 | 方法 | 说明 |
|
||
|------|------|------|
|
||
| `/feedback` | POST | 提交反馈 |
|
||
| `/feedback/list` | GET | 反馈列表 |
|
||
| `/feedback/stats` | GET | 反馈统计 |
|
||
| `/feedback/bad-cases` | GET | 差评案例 |
|
||
| `/feedback/blacklist` | GET | 切片黑名单 |
|
||
| `/faq` | GET | FAQ 列表 |
|
||
| `/faq` | POST | 创建 FAQ |
|
||
| `/faq/<id>/approve` | POST | 审批 FAQ |
|
||
| `/faq/<id>` | PUT | 修改 FAQ |
|
||
| `/faq/<id>` | DELETE | 删除 FAQ |
|
||
| `/faq/suggestions` | GET | FAQ 建议列表 |
|
||
| `/faq/suggestions/<id>/approve` | POST | 批准 FAQ 建议 |
|
||
| `/faq/suggestions/<id>/reject` | POST | 拒绝 FAQ 建议 |
|
||
|
||
### 13.9 图片服务
|
||
|
||
| 端点 | 方法 | 说明 |
|
||
|------|------|------|
|
||
| `/images/list` | GET | 图片列表 |
|
||
| `/images/<id>` | GET | 获取图片 |
|
||
| `/images/<id>/info` | GET | 图片元数据 |
|
||
| `/images/stats` | GET | 图片统计 |
|
||
|
||
### 13.10 报告服务
|
||
|
||
| 端点 | 方法 | 说明 |
|
||
|------|------|------|
|
||
| `/reports/weekly` | GET | 周报告 |
|
||
| `/reports/monthly` | GET | 月报告 |
|
||
|
||
### 13.11 调试与管理接口
|
||
|
||
| 端点 | 方法 | 说明 |
|
||
|------|------|------|
|
||
| `/kb/route` | POST | 测试知识库路由 |
|
||
| `/health` | GET | 健康检查 |
|
||
| `/stats` | GET | 系统统计(admin) |
|
||
| `/auth/login` | POST | 模拟登录(开发模式) |
|
||
| `/auth/me` | GET | 当前用户信息 |
|
||
|
||
### 13.12 🛠️ 开发环境自用接口
|
||
|
||
> 以下接口仅供 RAG 服务内部开发调试使用(dev-ui 前端、脚本测试等),**后端组无需对接**。
|
||
|
||
| 端点 | 方法 | 说明 |
|
||
|------|------|------|
|
||
| `/documents/<path>/preview` | GET | 文档预览,按 `chunk_index` 跳转到具体切片(dev-ui 引用溯源用) |
|
||
|
||
### 13.13 异步任务查询
|
||
|
||
> 所有异步操作(同步、重建索引、上传向量化、出题、批阅)返回的 `task_id` 均可通过以下接口查询进度。
|
||
|
||
| 端点 | 方法 | 说明 |
|
||
|------|------|------|
|
||
| `/tasks` | GET | 任务列表(支持 status/type 过滤) |
|
||
| `/tasks/<task_id>` | GET | 任务状态(JSON 轮询,**后端组推荐使用**) |
|
||
| `/tasks/<task_id>/progress` | GET | 任务进度(SSE 流式,dev-ui 前端使用) |
|
||
| `/tasks/stats` | GET | 任务统计 |
|
||
|
||
**任务状态字段说明**:
|
||
|
||
| 字段 | 类型 | 说明 |
|
||
|------|------|------|
|
||
| `task_id` | string | 任务唯一标识 |
|
||
| `type` | string | 类型:sync / reindex / upload / batch_upload / exam_generate / exam_grade |
|
||
| `status` | string | 状态:pending / running / completed / failed |
|
||
| `progress` | float | 进度百分比(0-100) |
|
||
| `current` | int | 当前处理项数 |
|
||
| `total` | int | 总项数 |
|
||
| `stage` | string | 当前阶段 |
|
||
| `message` | string | 当前步骤描述 |
|
||
| `result` | any | 完成后的结果数据(仅 status=completed 时存在) |
|
||
| `error` | string | 失败错误信息(仅 status=failed 时存在) |
|
||
| `duration_ms` | int | 执行耗时毫秒(仅已完成时存在) |
|
||
|
||
**后端对接轮询模式**:
|
||
|
||
```python
|
||
import time
|
||
import requests
|
||
|
||
def async_task_poll(task_id, base_url='http://rag-service:5001', interval=2, timeout=300):
|
||
"""
|
||
通用异步任务轮询函数
|
||
|
||
Args:
|
||
task_id: 任务 ID
|
||
base_url: RAG 服务地址
|
||
interval: 轮询间隔(秒)
|
||
timeout: 超时时间(秒)
|
||
|
||
Returns:
|
||
任务结果(result 字段)
|
||
|
||
Raises:
|
||
TimeoutError: 超时
|
||
Exception: 任务失败
|
||
"""
|
||
elapsed = 0
|
||
while elapsed < timeout:
|
||
time.sleep(interval)
|
||
elapsed += interval
|
||
|
||
resp = requests.get(f'{base_url}/tasks/{task_id}')
|
||
if resp.status_code == 404:
|
||
raise Exception(f"任务不存在: {task_id}")
|
||
|
||
task = resp.json()['data']
|
||
|
||
if task['status'] == 'completed':
|
||
return task.get('result')
|
||
elif task['status'] == 'failed':
|
||
raise Exception(f"任务失败: {task.get('error', '未知错误')}")
|
||
# 可选:记录进度日志
|
||
# logger.info(f"任务 {task_id}: {task['progress']}% - {task['message']}")
|
||
|
||
raise TimeoutError(f"任务超时: {task_id} (已等待 {timeout}s)")
|
||
```
|
||
|
||
---
|
||
|
||
## 十四、文件管理服务(后端负责)
|
||
|
||
### 13.1 功能概述
|
||
|
||
用户询问"我能访问哪些文件"、"我的权限能查看什么文档"等**元问题**时,需要后端提供完整的文件列表服务。
|
||
|
||
**职责划分:**
|
||
|
||
| 层面 | 负责 | 说明 |
|
||
|------|------|------|
|
||
| **文件元数据管理** | 后端 | 维护文件索引表,记录文件路径、大小、上传时间、权限等 |
|
||
| **文件目录展示** | 后端 | 根据用户权限返回可访问的文件列表 |
|
||
| **文件内容检索** | RAG | 向量检索、关键词检索、内容问答 |
|
||
|
||
**为什么不由 RAG 负责?**
|
||
|
||
```
|
||
RAG 向量库存储的是"文档切片"(chunks),不是"文件列表"
|
||
↓
|
||
向量库 metadata 只记录 source(文件名),不记录:
|
||
- 文件层级结构(目录/子目录)
|
||
- 上传时间、文件大小
|
||
- 用户权限关系
|
||
↓
|
||
用户问"有哪些文件"时,RAG 只能遍历 chunks 提取 source
|
||
↓
|
||
这种方式无法展示完整的文件目录结构
|
||
```
|
||
|
||
### 13.2 后端数据库设计
|
||
|
||
**文件索引表 (file_index):**
|
||
|
||
```sql
|
||
CREATE TABLE file_index (
|
||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||
|
||
-- 文件信息
|
||
file_path VARCHAR(512) NOT NULL, -- 相对路径,如 "public/规章制度/考勤制度.docx"
|
||
file_name VARCHAR(255) NOT NULL, -- 文件名,如 "考勤制度.docx"
|
||
file_type VARCHAR(50), -- 文件类型:pdf, docx, xlsx 等
|
||
file_size BIGINT, -- 文件大小(字节)
|
||
|
||
-- 所属知识库
|
||
kb_name VARCHAR(64) NOT NULL, -- 知识库名称,如 "public_kb", "dept_finance"
|
||
|
||
-- 层级结构
|
||
parent_path VARCHAR(512), -- 父目录路径,如 "public/规章制度"
|
||
level INT DEFAULT 1, -- 层级深度
|
||
|
||
-- 状态
|
||
status ENUM('active', 'deleted', 'processing') DEFAULT 'active',
|
||
|
||
-- 向量化状态
|
||
vectorized BOOLEAN DEFAULT FALSE, -- 是否已向量化
|
||
chunk_count INT DEFAULT 0, -- 切片数量
|
||
|
||
-- 时间戳
|
||
uploaded_by VARCHAR(64), -- 上传者用户ID
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||
|
||
INDEX idx_kb_name (kb_name),
|
||
INDEX idx_parent_path (parent_path),
|
||
INDEX idx_file_path (file_path),
|
||
UNIQUE KEY uk_file_path (file_path, kb_name)
|
||
);
|
||
```
|
||
|
||
**文件权限表 (file_permissions):**
|
||
|
||
```sql
|
||
-- 方案 A:继承知识库权限(推荐)
|
||
-- 用户对知识库有权限 = 对知识库内所有文件有权限
|
||
-- 无需单独的文件权限表
|
||
|
||
-- 方案 B:细粒度文件权限(可选)
|
||
CREATE TABLE file_permissions (
|
||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||
file_id BIGINT NOT NULL,
|
||
user_id VARCHAR(64) NOT NULL,
|
||
permission ENUM('read', 'write', 'admin') DEFAULT 'read',
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
|
||
FOREIGN KEY (file_id) REFERENCES file_index(id) ON DELETE CASCADE,
|
||
UNIQUE KEY uk_user_file (user_id, file_id)
|
||
);
|
||
```
|
||
|
||
### 13.3 后端 API 设计
|
||
|
||
**获取文件列表:**
|
||
|
||
```http
|
||
GET /api/files?kb_name=public_kb&parent_path=public
|
||
```
|
||
|
||
**响应:**
|
||
```json
|
||
{
|
||
"success": true,
|
||
"current_path": "public",
|
||
"files": [
|
||
{
|
||
"name": "规章制度",
|
||
"type": "folder",
|
||
"path": "public/规章制度"
|
||
},
|
||
{
|
||
"name": "考勤制度.docx",
|
||
"type": "file",
|
||
"path": "public/考勤制度.docx",
|
||
"size": 102400,
|
||
"uploaded_at": "2026-04-19T10:00:00",
|
||
"chunk_count": 15
|
||
}
|
||
],
|
||
"breadcrumb": [
|
||
{"name": "根目录", "path": ""},
|
||
{"name": "public", "path": "public"}
|
||
]
|
||
}
|
||
```
|
||
|
||
**获取用户可访问的文件列表:**
|
||
|
||
```http
|
||
GET /api/files/accessible
|
||
Authorization: Bearer <jwt_token>
|
||
```
|
||
|
||
**响应:**
|
||
```json
|
||
{
|
||
"success": true,
|
||
"knowledge_bases": [
|
||
{
|
||
"kb_name": "public_kb",
|
||
"display_name": "公开知识库",
|
||
"file_count": 25,
|
||
"total_size": 52428800,
|
||
"files": [
|
||
{
|
||
"path": "public/考勤制度.docx",
|
||
"name": "考勤制度.docx",
|
||
"size": 102400,
|
||
"uploaded_at": "2026-04-19T10:00:00"
|
||
}
|
||
]
|
||
}
|
||
],
|
||
"total_files": 50,
|
||
"total_size": 104857600
|
||
}
|
||
```
|
||
|
||
### 13.4 与 RAG 服务配合
|
||
|
||
**文件上传流程:**
|
||
|
||
```
|
||
┌─────────┐ ┌─────────┐ ┌─────────┐
|
||
│ 前端 │───▶│ 后端 │───▶│ RAG │
|
||
└─────────┘ └─────────┘ └─────────┘
|
||
│ │
|
||
▼ ▼
|
||
┌──────────────┐ ┌──────────────┐
|
||
│ 1. 验证权限 │ │ 4. 解析文档 │
|
||
│ 2. 存储文件 │ │ 5. 切片向量化 │
|
||
│ 3. 写 file_index │ 6. 写入 ChromaDB │
|
||
└──────────────┘ └──────────────┘
|
||
│
|
||
▼
|
||
┌──────────────┐
|
||
│ 7. 返回 chunk_count │
|
||
└──────────────┘
|
||
│
|
||
▼
|
||
┌──────────────┐
|
||
│ 8. 后端更新 │
|
||
│ file_index. │
|
||
│ vectorized=true │
|
||
└──────────────┘
|
||
```
|
||
|
||
**后端调用 RAG 上传文件后:**
|
||
|
||
```python
|
||
# 1. 调用 RAG 上传接口
|
||
response = requests.post(
|
||
'http://rag-service:5001/documents/upload',
|
||
files={'file': file},
|
||
data={'collection': kb_name}
|
||
)
|
||
|
||
# 2. 写入文件索引表
|
||
db.execute("""
|
||
INSERT INTO file_index (file_path, file_name, file_type, file_size, kb_name, parent_path, uploaded_by)
|
||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||
""", (file_path, file_name, file_type, file_size, kb_name, parent_path, user_id))
|
||
|
||
# 3. RAG 向量化完成后,更新状态
|
||
# 可以通过回调或轮询实现
|
||
db.execute("""
|
||
UPDATE file_index
|
||
SET vectorized = TRUE, chunk_count = ?
|
||
WHERE file_path = ?
|
||
""", (chunk_count, file_path))
|
||
```
|
||
|
||
### 13.5 RAG 服务配合要求
|
||
|
||
**文档上传接口响应增强:**
|
||
|
||
当 RAG 服务处理完文件后,应返回切片数量:
|
||
|
||
```json
|
||
{
|
||
"success": true,
|
||
"message": "文件上传成功,已添加到向量库",
|
||
"file": {
|
||
"filename": "考勤制度.docx",
|
||
"collection": "public_kb",
|
||
"path": "public/考勤制度.docx",
|
||
"size": 102400
|
||
},
|
||
"vectorization": {
|
||
"status": "completed",
|
||
"chunk_count": 15
|
||
}
|
||
}
|
||
```
|
||
|
||
**文档删除接口:**
|
||
|
||
删除文件时,RAG 服务需要同时:
|
||
1. 删除物理文件
|
||
2. 删除 ChromaDB 中的所有相关切片
|
||
|
||
```http
|
||
DELETE /documents/<path>
|
||
```
|
||
|
||
**响应:**
|
||
```json
|
||
{
|
||
"success": true,
|
||
"message": "文件已删除",
|
||
"deleted_chunks": 15
|
||
}
|
||
```
|
||
|
||
### 13.6 元问题处理流程
|
||
|
||
当用户询问"我能访问哪些文件"时:
|
||
|
||
```
|
||
1. RAG 服务识别为"元问题"
|
||
↓
|
||
2. RAG 服务检查是否有后端文件管理服务
|
||
↓
|
||
有 → 调用后端 API 获取文件列表
|
||
无 → 从 ChromaDB metadata 中提取 source 列表(降级方案)
|
||
↓
|
||
3. 返回文件列表给用户
|
||
```
|
||
|
||
**后端提供的文件列表 API:**
|
||
|
||
```http
|
||
GET /api/files/list-for-rag?user_id=xxx&kb_names=public_kb,dept_finance
|
||
Authorization: Bearer <service_token>
|
||
```
|
||
|
||
**响应:**
|
||
```json
|
||
{
|
||
"success": true,
|
||
"files": [
|
||
{
|
||
"name": "考勤制度.docx",
|
||
"path": "public/考勤制度.docx",
|
||
"kb_name": "public_kb",
|
||
"size": 102400,
|
||
"uploaded_at": "2026-04-19T10:00:00"
|
||
}
|
||
],
|
||
"grouped_by_kb": {
|
||
"public_kb": {"count": 25, "files": [...]},
|
||
"dept_finance": {"count": 10, "files": [...]}
|
||
}
|
||
}
|
||
```
|
||
|
||
### 13.7 RAG 服务配置
|
||
|
||
在 `config.py` 中添加后端文件服务配置:
|
||
|
||
```python
|
||
# 后端文件管理服务(可选)
|
||
BACKEND_FILE_SERVICE_URL = os.getenv('BACKEND_FILE_SERVICE_URL', '')
|
||
BACKEND_SERVICE_TOKEN = os.getenv('BACKEND_SERVICE_TOKEN', '')
|
||
```
|
||
|
||
如果配置了后端文件服务,RAG 在回答元问题时会调用后端 API 获取完整文件列表。
|
||
|
||
---
|
||
|
||
## 附录:元问题识别关键词
|
||
|
||
RAG 服务会自动识别以下关键词,判断为"元问题"并返回文件列表:
|
||
|
||
**权限相关:**
|
||
- "我的权限"、"用户权限"、"查看权限"、"访问权限"
|
||
- "权限能"、"权限可以"、"有什么权限"、"有哪些权限"
|
||
|
||
**文件列表相关:**
|
||
- "有哪些文件"、"什么文件"、"哪些文件"、"文件列表"
|
||
- "能查看"、"可以查看"、"有权限查看"
|
||
- "能访问"、"可以访问"、"有权限访问"
|
||
- "我能看"、"我可以看"、"我能查"、"我可以查"
|
||
- "能看到什么"、"能查到什么"、"可以看什么"、"可以查什么"
|
||
|
||
**知识库相关:**
|
||
- "知识库有哪些"、"库里有"、"文档有哪些"
|
||
- "有什么文档"、"有什么文件"、"包含什么"
|
||
|
||
后端可根据业务需求,要求 RAG 服务扩展此关键词列表。
|
||
|