# RAG 服务 API 接口规范 --- ## 📋 变更记录(2026-06-05 更新) > **本次更新内容**:新增 AI 智能出题端口、更新生产环境测试结果 > > **2026-06-05 更新**:出题批卷接口格式优化与输入校验增强 ### 新增端口 | 端点 | 方法 | 功能 | 说明 | |-----|------|------|------| | `/exam/generate-smart` | POST | AI 智能出题 | 自动分析文档决定题型和数量 | | `/images/` | 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//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/` | DELETE | 删除向量库 | | `/documents/upload` | POST | 上传文档 | | `/documents/list` | GET | 文档列表 | | `/documents/` | DELETE | 删除文档 | ### 2.3 反馈系统(可选) | 端点 | 方法 | 功能 | |-----|------|------| | `/feedback` | POST | 提交反馈 | | `/feedback/list` | GET | 反馈列表 | | `/faq` | GET/POST | FAQ管理 | ### 2.4 出题系统(可选) | 端点 | 方法 | 功能 | |-----|------|------| | `/exam/generate` | POST | 生成题目 | | `/exam/grade` | POST | 批阅答案 | --- ## 三、调用方式 ### 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-04-29 > 本文档供后端开发人员参考,用于对接 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//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/` | 获取会话历史 | | `DELETE /session/` | 删除会话 | **后端实现参考(数据库表结构):** ```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/ ``` ### 4.4 删除向量库 ``` DELETE /collections/ ``` **查询参数:** | 参数 | 类型 | 必需 | 说明 | |------|------|------|------| | `delete_documents` | boolean | ❌ | 是否删除文档源文件,默认 false | **响应:** ```json { "success": true, "message": "向量库 'dept_finance' 已删除", "deleted_documents": false } ``` **说明:** - 默认只删除向量数据(ChromaDB 集合、BM25 索引) - 设置 `delete_documents=true` 会同时删除文档源文件 - 公开知识库 (`public_kb`) 不允许删除 ### 4.5 获取向量库文档列表 ``` GET /collections//documents ``` **响应:** ```json { "collection": "public_kb", "documents": [ { "chunks": 105, "source": "1.docx" }, { "chunks": 39, "source": "三峡公报_1-15页.pdf" } ], "total": 9 } ``` ### 4.6 获取向量库切片列表 ``` GET /collections//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//documents//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//documents//restore ``` **响应:** ```json { "success": true, "restored_chunks": 15, "document_id": "报销制度.pdf", "collection": "public_kb" } ``` ### 4.5.3 获取文档版本历史 ``` GET /collections//documents//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, "message": "文件上传成功,已保存并添加到向量库", "file": { "filename": "document.pdf", "collection": "public_kb", "path": "public_kb/document.pdf", "size": 1024000, "replaced": false } } ``` **同名文件处理**:上传同名文件时,旧版本的切片会被自动清理后覆盖(`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//status ``` **响应:** ```json { "success": true, "status": "active", "chunk_count": 105, "last_processed": null } ``` ### 5.5 更新/替换文档 ``` PUT /documents/ Content-Type: multipart/form-data ``` **表单参数:** - `file`: 替换的文件(必需) **响应:** ```json { "success": true, "message": "文件已更新" } ``` **说明:** - 文件必须已存在,否则返回 404 - 更新后自动触发重新向量化 ### 5.6 删除文档 ``` DELETE /documents/ ``` ### 5.7 查看文件切片 ``` GET /documents//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//preview?chunk_index=&context= ``` > **用途**:前端点击引用标签后,调用此接口跳转到文档中的具体切片位置。 > 复用现有切片查询逻辑,不新增存储。 **查询参数:** | 参数 | 类型 | 必需 | 说明 | |------|------|------|------| | `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": "success", "status_code": 2010, "message": "同步完成", "data": { "result": { "documents_added": 1, "documents_deleted": 1, "documents_modified": 0, "documents_processed": 2, "errors": [], "status": "completed" } } } ``` ### 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": 3001, "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": "success", "status_code": 2011, "message": "出题完成", "data": { "success": true, "request_id": "xxx", "total": 10, "source_chunks_used": 15, "questions": [...] } } ``` **data 内部结构**: ```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": "success", "status_code": 2021, "message": "批阅完成", "data": { "request_id": "可选,原样返回", "success": true, "total_score": 12.5, "total_max_score": 22.0, "score_rate": 56.8, "results": [...] } } ``` **data 内部结构**: ```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 def call_rag_grade(grade_request): """调用 RAG 批卷接口""" response = requests.post( 'http://rag-service:5001/exam/grade', json=grade_request ) return response.json() ``` **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/` | PUT | 修改向量库 | | `/collections/` | DELETE | 删除向量库 | | `/collections//documents` | GET | 获取向量库文档列表 | | `/collections//chunks` | GET | 获取向量库切片列表 | | `/collections//update-image-descriptions` | POST | 更新图片描述 | ### 13.3 文档版本管理(新增) | 端点 | 方法 | 说明 | |------|------|------| | `/collections//documents//deprecate` | POST | 废止文档 | | `/collections//documents//restore` | POST | 恢复文档 | | `/collections//documents//versions` | GET | 获取版本历史 | ### 13.4 文档管理 | 端点 | 方法 | 说明 | |------|------|------| | `/documents/upload` | POST | 上传文件 | | `/documents/batch-upload` | POST | 批量上传 | | `/documents/list` | GET | 文档列表 | | `/documents//status` | GET | 获取文档处理状态 | | `/documents/` | PUT | 更新/替换文档 | | `/documents/` | DELETE | 删除文档 | ### 13.5 切片管理 | 端点 | 方法 | 说明 | |------|------|------| | `/documents//chunks` | GET | 查看文件切片 | | `/documents//preview` 🛠️ | GET | 文档预览(dev-ui 自用,按 `chunk_index` 跳转) | | `/chunks` | POST | 新增切片 | | `/chunks/` | PUT | 修改切片 | | `/chunks/` | DELETE | 删除切片 | ### 13.6 同步服务 | 端点 | 方法 | 说明 | |------|------|------| | `/sync` | POST | 触发同步 | | `/sync/status` | GET | 同步状态 | | `/sync/history` | GET | 同步历史 | | `/sync/changes` | GET | 变更日志 | | `/sync/start` | POST | 启动文件监控 | | `/sync/stop` | POST | 停止文件监控 | ### 13.7 出题系统 | 端点 | 方法 | 说明 | |------|------|------| | `/exam/generate` | POST | 生成题目 | | `/exam/grade` | POST | 批改答案 | | `/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//approve` | POST | 审批 FAQ | | `/faq/` | PUT | 修改 FAQ | | `/faq/` | DELETE | 删除 FAQ | | `/faq/suggestions` | GET | FAQ 建议列表 | | `/faq/suggestions//approve` | POST | 批准 FAQ 建议 | | `/faq/suggestions//reject` | POST | 拒绝 FAQ 建议 | ### 13.9 图片服务 | 端点 | 方法 | 说明 | |------|------|------| | `/images/list` | GET | 图片列表 | | `/images/` | GET | 获取图片 | | `/images//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//preview` | GET | 文档预览,按 `chunk_index` 跳转到具体切片(dev-ui 引用溯源用) | --- ## 十四、文件管理服务(后端负责) ### 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 ``` **响应:** ```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/ ``` **响应:** ```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 ``` **响应:** ```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 服务扩展此关键词列表。