- 将全部路由文件(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 迁移计划
555 lines
12 KiB
Markdown
555 lines
12 KiB
Markdown
# 异步任务接口变更说明
|
||
|
||
> **变更日期**:2026-06-05
|
||
>
|
||
> **变更原因**:同步、向量化、出题、批阅等操作耗时较长(数秒到数十秒),改为异步任务模式后接口立即返回 `task_id`,避免后端调用超时。
|
||
>
|
||
> **影响范围**:8 个已有端口的响应格式变更 + 4 个新增任务查询接口
|
||
|
||
---
|
||
|
||
## 一、核心变更
|
||
|
||
所有受影响的接口从 **同步阻塞返回结果** 改为 **异步任务立即返回 `task_id`**。
|
||
|
||
**调用流程变更**:
|
||
|
||
```
|
||
旧流程:
|
||
POST /sync → 等待 10-30s → 返回完整结果
|
||
|
||
新流程:
|
||
POST /sync → 立即返回 {"task_id": "xxx"}
|
||
GET /tasks/xxx → 轮询(1-2s 间隔)→ status=completed 时取 result
|
||
```
|
||
|
||
---
|
||
|
||
## 二、受影响的端口(8 个)
|
||
|
||
### 1. POST /sync — 触发同步
|
||
|
||
**旧响应**:
|
||
```json
|
||
{
|
||
"success": true,
|
||
"status_code": 2010,
|
||
"message": "同步完成",
|
||
"data": {
|
||
"result": {
|
||
"documents_processed": 20,
|
||
"documents_added": 3,
|
||
"documents_modified": 2,
|
||
"documents_deleted": 0,
|
||
"errors": []
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
**新响应**:
|
||
```json
|
||
{
|
||
"success": true,
|
||
"status_code": 2010,
|
||
"message": "同步任务已启动",
|
||
"data": {
|
||
"task_id": "c3d4e5f6a1b2",
|
||
"message": "同步任务已启动,通过 GET /tasks/c3d4e5f6a1b2 查询进度"
|
||
}
|
||
}
|
||
```
|
||
|
||
**冲突检测**:已有同步任务运行时返回 HTTP 409:
|
||
```json
|
||
{"error": "TASK_RUNNING", "message": "同步任务正在执行中 (task_id: xxx),请等待完成"}
|
||
```
|
||
|
||
---
|
||
|
||
### 2. POST /documents/sync — 触发文档同步
|
||
|
||
**旧响应**:
|
||
```json
|
||
{
|
||
"success": true,
|
||
"results": [{"collection": "public_kb", "status": "success"}],
|
||
"synced_count": 1
|
||
}
|
||
```
|
||
|
||
**新响应**:
|
||
```json
|
||
{
|
||
"success": true,
|
||
"task_id": "xxx",
|
||
"message": "同步任务已启动,通过 GET /tasks/xxx 查询进度"
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 3. POST /collections/\<kb_name\>/reindex — 重建索引
|
||
|
||
**旧响应**:
|
||
```json
|
||
{
|
||
"success": true,
|
||
"message": "重新索引完成: 处理 20 个文档",
|
||
"documents_processed": 20,
|
||
"documents_added": 3
|
||
}
|
||
```
|
||
|
||
**新响应**:
|
||
```json
|
||
{
|
||
"success": true,
|
||
"task_id": "xxx",
|
||
"message": "重建索引任务已启动: kb_name,通过 GET /tasks/xxx 查询进度"
|
||
}
|
||
```
|
||
|
||
**冲突检测**:已有重建任务运行时返回 HTTP 409。
|
||
|
||
---
|
||
|
||
### 4. POST /documents/upload — 上传单个文件
|
||
|
||
**旧响应**:
|
||
```json
|
||
{
|
||
"success": true,
|
||
"status_code": 2002,
|
||
"message": "文件上传成功,已保存并添加到向量库",
|
||
"data": {
|
||
"file": {
|
||
"filename": "test.txt",
|
||
"collection": "public_kb",
|
||
"path": "public_kb/test.txt",
|
||
"size": 1024,
|
||
"replaced": false
|
||
},
|
||
"sync_status": "已保存并添加到向量库"
|
||
}
|
||
}
|
||
```
|
||
|
||
**新响应**(新增 `task_id` 字段):
|
||
```json
|
||
{
|
||
"success": true,
|
||
"status_code": 2002,
|
||
"message": "文件上传成功,已保存,向量化任务已启动",
|
||
"data": {
|
||
"file": {
|
||
"filename": "test.txt",
|
||
"collection": "public_kb",
|
||
"path": "public_kb/test.txt",
|
||
"size": 1024,
|
||
"replaced": false
|
||
},
|
||
"sync_status": "已保存,向量化任务已启动",
|
||
"task_id": "a1b2c3d4e5f6"
|
||
}
|
||
}
|
||
```
|
||
|
||
> **注意**:文件保存仍为同步操作(毫秒级),仅向量化部分异步执行。`task_id` 为 `null` 表示同步服务不可用,需手动同步。
|
||
|
||
---
|
||
|
||
### 5. POST /documents/batch-upload — 批量上传
|
||
|
||
**旧响应**:
|
||
```json
|
||
{
|
||
"success": true,
|
||
"status_code": 2003,
|
||
"data": {
|
||
"results": [...],
|
||
"success_count": 2,
|
||
"total": 2
|
||
}
|
||
}
|
||
```
|
||
|
||
**新响应**(新增 `task_id` 字段):
|
||
```json
|
||
{
|
||
"success": true,
|
||
"status_code": 2003,
|
||
"data": {
|
||
"results": [...],
|
||
"success_count": 2,
|
||
"total": 2,
|
||
"task_id": "b2c3d4e5f6a1"
|
||
}
|
||
}
|
||
```
|
||
|
||
> **注意**:批量上传成功后自动触发后台向量化任务。无成功上传时不返回 `task_id`。
|
||
|
||
---
|
||
|
||
### 6. POST /exam/generate — 生成题目
|
||
|
||
**旧响应**:
|
||
```json
|
||
{
|
||
"success": true,
|
||
"status_code": 2020,
|
||
"message": "出题完成",
|
||
"data": {
|
||
"success": true,
|
||
"total": 10,
|
||
"questions": [...],
|
||
"warnings": []
|
||
}
|
||
}
|
||
```
|
||
|
||
**新响应**:
|
||
```json
|
||
{
|
||
"success": true,
|
||
"status_code": 2020,
|
||
"message": "出题任务已启动",
|
||
"data": {
|
||
"task_id": "d4e5f6a1b2c3",
|
||
"message": "出题任务已启动 (10题),通过 GET /tasks/d4e5f6a1b2c3 查询结果"
|
||
}
|
||
}
|
||
```
|
||
|
||
**轮询完成后的 `result` 字段**:
|
||
```json
|
||
{
|
||
"success": true,
|
||
"total": 10,
|
||
"source_chunks_used": 15,
|
||
"requested_types": {"single_choice": 5, "true_false": 3, "fill_blank": 2},
|
||
"actual_types": {"single_choice": 5, "true_false": 3, "fill_blank": 2},
|
||
"warnings": [],
|
||
"questions": [...]
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 7. POST /exam/generate-smart — AI 智能出题
|
||
|
||
**旧响应**:
|
||
```json
|
||
{
|
||
"success": true,
|
||
"status_code": 2020,
|
||
"message": "AI 智能出题成功",
|
||
"data": {
|
||
"ai_analysis": {...},
|
||
"questions": [...],
|
||
"total": 16
|
||
}
|
||
}
|
||
```
|
||
|
||
**新响应**:
|
||
```json
|
||
{
|
||
"success": true,
|
||
"status_code": 2020,
|
||
"message": "AI 智能出题任务已启动",
|
||
"data": {
|
||
"task_id": "e5f6a1b2c3d4",
|
||
"message": "AI 智能出题任务已启动,通过 GET /tasks/e5f6a1b2c3d4 查询结果"
|
||
}
|
||
}
|
||
```
|
||
|
||
**轮询完成后的 `result` 字段**:与旧 `data` 格式相同(含 `ai_analysis`、`questions`、`total`)。
|
||
|
||
---
|
||
|
||
### 8. POST /exam/grade — 批阅答案
|
||
|
||
**旧响应**:
|
||
```json
|
||
{
|
||
"success": true,
|
||
"status_code": 2021,
|
||
"message": "批阅完成",
|
||
"data": {
|
||
"success": true,
|
||
"total_score": 12.5,
|
||
"total_max_score": 22.0,
|
||
"score_rate": 56.8,
|
||
"results": [...]
|
||
}
|
||
}
|
||
```
|
||
|
||
**新响应**:
|
||
```json
|
||
{
|
||
"success": true,
|
||
"status_code": 2021,
|
||
"message": "批阅任务已启动",
|
||
"data": {
|
||
"task_id": "f6a1b2c3d4e5",
|
||
"message": "批阅任务已启动 (5题),通过 GET /tasks/f6a1b2c3d4e5 查询结果"
|
||
}
|
||
}
|
||
```
|
||
|
||
**轮询完成后的 `result` 字段**:
|
||
```json
|
||
{
|
||
"success": true,
|
||
"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"
|
||
}
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 三、新增接口(4 个)
|
||
|
||
### GET /tasks
|
||
|
||
获取任务列表。
|
||
|
||
**查询参数**:`status`(过滤状态)、`type`(过滤类型)、`limit`(返回数量,默认 50)
|
||
|
||
**响应**:
|
||
```json
|
||
{
|
||
"success": true,
|
||
"status_code": 2000,
|
||
"data": {
|
||
"tasks": [
|
||
{
|
||
"task_id": "a1b2c3d4e5f6",
|
||
"type": "sync",
|
||
"description": "文档同步",
|
||
"status": "running",
|
||
"progress": 45.0,
|
||
"current": 9,
|
||
"total": 20,
|
||
"stage": "处理文件",
|
||
"message": "已处理: 产品手册.pdf",
|
||
"created_at": "2026-06-05T10:30:00"
|
||
}
|
||
],
|
||
"total": 1
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### GET /tasks/\<task_id\>
|
||
|
||
获取单个任务状态(**后端组推荐使用的轮询接口**)。
|
||
|
||
**建议轮询间隔**:1-2 秒,当 `status` 为 `completed` 或 `failed` 时停止。
|
||
|
||
**响应**:
|
||
```json
|
||
{
|
||
"success": true,
|
||
"status_code": 2000,
|
||
"data": {
|
||
"task_id": "a1b2c3d4e5f6",
|
||
"type": "sync",
|
||
"description": "文档同步",
|
||
"status": "completed",
|
||
"progress": 100.0,
|
||
"current": 20,
|
||
"total": 20,
|
||
"stage": "完成",
|
||
"message": "同步完成",
|
||
"created_at": "2026-06-05T10:30:00",
|
||
"started_at": "2026-06-05T10:30:01",
|
||
"completed_at": "2026-06-05T10:30:15",
|
||
"duration_ms": 14000,
|
||
"result": {
|
||
"documents_processed": 20,
|
||
"documents_added": 3
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
**任务状态**:
|
||
|
||
| 状态 | 说明 |
|
||
|------|------|
|
||
| `pending` | 已创建,等待执行 |
|
||
| `running` | 正在执行 |
|
||
| `completed` | 执行完成,`result` 字段包含完整结果 |
|
||
| `failed` | 执行失败,`error` 字段包含错误信息 |
|
||
|
||
**任务类型(type)**:`sync` / `reindex` / `upload` / `batch_upload` / `exam_generate` / `exam_grade`
|
||
|
||
---
|
||
|
||
### GET /tasks/\<task_id\>/progress
|
||
|
||
SSE 流式任务进度推送(dev-ui 前端推荐使用,后端组通常不需要此接口)。
|
||
|
||
**Content-Type**:`text/event-stream`
|
||
|
||
---
|
||
|
||
### GET /tasks/stats
|
||
|
||
获取任务统计信息。
|
||
|
||
**响应**:
|
||
```json
|
||
{
|
||
"success": true,
|
||
"status_code": 2000,
|
||
"data": {
|
||
"total": 5,
|
||
"by_status": {"running": 1, "completed": 3, "failed": 1},
|
||
"by_type": {"sync": 2, "exam_generate": 2, "upload": 1}
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 四、后端对接代码参考
|
||
|
||
### 通用轮询函数
|
||
|
||
```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(从 POST 响应中获取)
|
||
base_url: RAG 服务地址
|
||
interval: 轮询间隔(秒),建议 1-2 秒
|
||
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', '未知错误')}")
|
||
|
||
raise TimeoutError(f"任务超时: {task_id} (已等待 {timeout}s)")
|
||
```
|
||
|
||
### 出题流程示例
|
||
|
||
```python
|
||
def generate_exam(file_path, collection, question_types, difficulty=3):
|
||
"""异步出题流程"""
|
||
# 1. 提交出题任务
|
||
resp = requests.post('http://rag-service:5001/exam/generate', json={
|
||
'file_path': file_path,
|
||
'collection': collection,
|
||
'question_types': question_types,
|
||
'difficulty': difficulty
|
||
})
|
||
task_id = resp.json()['data']['task_id']
|
||
|
||
# 2. 轮询等待结果
|
||
result = async_task_poll(task_id)
|
||
|
||
# 3. result 包含完整的出题结果
|
||
questions = result['questions']
|
||
return questions
|
||
```
|
||
|
||
### 批阅流程示例
|
||
|
||
```python
|
||
def grade_exam(answers):
|
||
"""异步批阅流程"""
|
||
# 1. 提交批阅任务
|
||
resp = requests.post('http://rag-service:5001/exam/grade', json={
|
||
'answers': answers
|
||
})
|
||
task_id = resp.json()['data']['task_id']
|
||
|
||
# 2. 轮询等待结果
|
||
result = async_task_poll(task_id)
|
||
|
||
# 3. result 包含完整的批阅结果
|
||
return result
|
||
```
|
||
|
||
### 同步流程示例
|
||
|
||
```python
|
||
def trigger_sync():
|
||
"""异步同步流程"""
|
||
# 1. 提交同步任务
|
||
resp = requests.post('http://rag-service:5001/sync')
|
||
|
||
if resp.status_code == 409:
|
||
print("已有同步任务在运行,跳过")
|
||
return
|
||
|
||
task_id = resp.json()['data']['task_id']
|
||
|
||
# 2. 轮询等待结果
|
||
result = async_task_poll(task_id)
|
||
print(f"同步完成: 处理 {result['documents_processed']} 个文档")
|
||
return result
|
||
```
|
||
|
||
---
|
||
|
||
## 五、注意事项
|
||
|
||
1. **超时时间**:异步任务完成后保留 1 小时,超时后自动清理。请在任务完成后及时获取结果。
|
||
|
||
2. **并发限制**:同一类型的任务(如 `sync`)同一时间只能有一个运行中,重复提交返回 HTTP 409。
|
||
|
||
3. **向后兼容**:如果 RAG 服务未升级(旧版本),POST 接口仍返回旧的同步结果格式。后端可通过检查响应中是否包含 `task_id` 字段来判断版本。
|
||
|
||
4. **轮询频率**:建议 1-2 秒间隔,过于频繁的轮询会增加服务器负担。
|
||
|
||
5. **错误处理**:任务可能因 LLM 超时、文件解析失败等原因失败,`status` 变为 `failed`,`error` 字段包含错误描述。
|