docs: 清理过时文档 + 更新关键文档

删除(10个):
- API与后端对接规范.md(已被后端对接规范.md 替代)
- image_processing_flow.md(已合入 RAG数据流程.md)
- 状态码功能更新说明.md(已合入后端对接规范.md)
- 题目模板.md(字段名与实际 API 不一致,以对接指南为准)
- 出题批卷系统设计.md(旧版出题设计,已被对接指南+变更说明替代)
- 测试指南.md(出题 API 格式过时,模型名过时)
- 企业文档更新管理方案.md(设计方案,已实施完成)
- 版本管理实施完成报告.md(里程碑报告,已完成归档)
- 生产路径优化计划.md(行号已偏移,阶段状态过时)

更新(7个):
- 出题批题后端对接指南.md:添加 generate-smart 端点、question_content→content
- 开发与系统模块说明.md:更新 reranker 和 LLM 模型名称
- 多源信息融合指南.md:标注生产路径 vs 备用路径
- 架构与部署方案.md:更新数据归属对照表(4个 SQLite DB)
- 认证与权限配置指南.md:出题 API 更新为 /exam/generate
- 向量库边界风险分析.md:标注 P1 旧切片残留和 P2 文件更新已修复
- 风险边界问题修复注意事项.md:标注所有场景已修复
This commit is contained in:
lacerate551
2026-06-21 20:53:33 +08:00
parent b2ccec79b9
commit d589c27bce
16 changed files with 40 additions and 4422 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -1,233 +0,0 @@
# 图片处理完整流程分析
## 流程概览
```
┌─────────────────────────────────────────────────────────────────────────────────┐
│ 图片处理完整流程 │
├─────────────────────────────────────────────────────────────────────────────────┤
│ │
│ 1. 解析阶段 (MinerU) │
│ ┌─────────────┐ ┌──────────────────┐ ┌───────────────────────────┐ │
│ │ PDF/Word │────→│ MinerU 解析 │────→│ MinerUChunk 对象 │ │
│ │ 文件 │ │ parsers/mineru_ │ │ ├── content (文本/标题) │ │
│ └─────────────┘ │ parser.py │ │ ├── chunk_type │ │
│ └──────────────────┘ │ ├── table_html (表格HTML) │ │
│ │ ├── image_path (独立图片) │ │
│ │ └── images (关联图片列表) │ │
│ ↓ │
│ to_page_content() │
│ ↓ │
│ 返回 chunks 列表 │
│ │
├─────────────────────────────────────────────────────────────────────────────────┤
│ │
│ 2. 存储阶段 (Knowledge Manager) │
│ ┌──────────────────┐ ┌────────────────────┐ ┌───────────────────┐ │
│ │ chunks 列表 │────→│ add_file_to_kb() │────→│ 向量库 metadata │ │
│ │ (MinerUChunk) │ │ knowledge/manager │ │ │ │
│ └──────────────────┘ │ .py │ │ ├── chunk_type │ │
│ │ │ │ ├── source │ │
│ │ ✅ 合并跨页表格 │ │ ├── page │ │
│ │ ✅ 序列化 images │ │ ├── images_json ✅│ │
│ │ ✅ 存储 image_path │ │ └── image_path ✅ │ │
│ └────────────────────┘ └───────────────────┘ │
│ │
├─────────────────────────────────────────────────────────────────────────────────┤
│ │
│ 3. 召回阶段 (RAG 检索) │
│ ┌──────────────────┐ ┌────────────────────┐ ┌───────────────────┐ │
│ │ 用户查询 │────→│ 混合检索 │────→│ 检索结果 │ │
│ │ "表3.1 数据" │ │ search_hybrid() │ │ contexts = [{ │ │
│ └──────────────────┘ │ api/chat_routes.py │ │ "doc": "...", │ │
│ └────────────────────┘ │ "meta": {...} │ │
│ ↓ │ }] │ │
│ ↓ └───────────────────┘ │
│ ┌────────────────────┐ ↓ │
│ │ _extract_rich_media│ ┌───────────────────┐ │
│ │ api/chat_routes.py │ │ 返回给前端 │ │
│ │ │────→│ { │ │
│ │ 读取 images_json │ │ "images": [...],│ │
│ │ 读取 image_path │ │ "tables": [...],│ │
│ │ ✅ 正确处理 │ │ "answer": "..." │ │
│ └────────────────────┘ │ } │ │
│ └───────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────────┘
```
## 关键代码位置
### 1. MinerU 解析 (parsers/mineru_parser.py)
**MinerUChunk 数据结构 (第107-125行)**:
```python
@dataclass
class MinerUChunk:
content: str # 文本内容
chunk_type: str # 类型: text, table, image, equation
page_start: int = 1
page_end: int = 1
title: str = ""
table_html: Optional[str] = None # 表格 HTML如果是表格
image_path: Optional[str] = None # 图片路径(独立图片)
images: Optional[List[Dict]] = None # 关联图片列表: [{"id": "abc.jpg", "order": 1}]
```
**表格切片创建 (第453-469行)**:
```python
chunk = MinerUChunk(
content=table_caption or "表格", # ⚠️ content 只有标题
chunk_type="table",
table_html=table_body, # ✅ 完整表格在 table_html
image_path=img_path, # 表格图片路径
images=table_images # 表格中嵌入的图片
)
```
### 2. 向量库存储 (knowledge/manager.py)
**add_file_to_kb() 核心逻辑 (第228-270行)**:
```python
for i, chunk in enumerate(chunks):
# 1. 获取 chunk_type
chunk_type = getattr(chunk, 'chunk_type', None)
if not chunk_type:
page_info = getattr(chunk, 'page_info', {}) or {}
chunk_type = page_info.get('chunk_type', 'text')
if chunk_type == 'table':
# 2. 表格内容优先使用 table_html
table_md = getattr(chunk, 'table_html', None) or chunk.content
semantic_content = _build_semantic_content_for_table(...)
# 3. 构建 metadata
metadata = {
'chunk_type': chunk_type,
'source': filename,
'page': page_start,
# ...
}
# 4. ✅ 序列化图片信息
if hasattr(chunk, 'images') and chunk.images:
metadata['images_json'] = json.dumps(chunk.images, ensure_ascii=False)
if hasattr(chunk, 'image_path') and chunk.image_path:
metadata['image_path'] = chunk.image_path
```
**跨页表格合并 (第323-420行)**:
```python
def _merge_cross_page_tables(self, chunks: list) -> list:
"""
合并规则:
1. 相邻两个表格切片
2. 页码连续 (page_end + 1 == next.page_start)
3. 第二个表格标题包含"续表"
"""
# 合并 table_html
current.table_html = curr_html + '\n' + next_html
# 合并 image_path 到 images
merged_images = [
{'id': curr_img, 'page': curr_page_end},
{'id': next_img, 'page': next_page_start}
]
current.images = merged_images
```
### 3. 富媒体召回 (api/chat_routes.py)
**_extract_rich_media() 核心逻辑 (第724-843行)**:
```python
def _extract_rich_media(contexts: List[Dict]) -> Dict[str, List]:
images = []
tables = []
for ctx in contexts:
meta = ctx.get("meta", {})
# 1. 独立图片切片 (image_path) - 图片/图表类型
if meta.get("chunk_type") in ("image", "chart") and meta.get("image_path"):
img_id = os.path.basename(meta["image_path"])
images.append({"id": img_id, "url": f"/images/{img_id}", ...})
# 2. 关联图片 (images_json) - 表格/文本嵌入图片
if meta.get("images_json"):
img_list = json.loads(meta["images_json"])
for img_info in img_list:
images.append({"id": img_info["id"], ...})
# 3. 表格图片 (image_path) - 表格类型的图片形式
if meta.get("chunk_type") == "table" and meta.get("image_path"):
img_id = os.path.basename(meta["image_path"])
images.append({"id": img_id, "type": "table_image", ...})
return {"images": images, "tables": tables}
```
## 当前问题分析
### 问题1: 表格显示"0行数据"
**根因**: `_build_semantic_content_for_table()` 接收的 `table_md` 可能是空的
**验证点**:
- MinerU 解析时 `table_html` 是否有值?
- `manager.py` 第240行 `table_md = getattr(chunk, 'table_html', None)` 是否正确获取?
### 问题2: 跨页表格合并不生效
**根因**: 可能是页码不连续或标题匹配失败
**验证点**:
- 检查 `_merge_cross_page_tables()` 的日志输出
- 验证两个表格切片的 `page_end``page_start` 是否连续
### 问题3: 图片重复
**根因**: 可能是 `images_json``image_path` 同时存在导致重复
**验证点**:
- 检查向量库中是否有同时存在 `images_json``image_path` 的切片
- `_extract_rich_media()` 中的去重逻辑是否有效
## 数据存储位置
| 目录 | 用途 |
|------|------|
| `.data/images/` | 全局图片存储(哈希命名,去重) |
| `.data/cache/vlm/` | VLM 图片描述缓存 |
| `.data/docstore/` | 原始表格/图片 JSON 备份 |
| `knowledge/vector_store/chroma/` | ChromaDB 向量数据库 |
## 测试验证步骤
### 1. 检查向量库 metadata
```python
from knowledge.manager import get_kb_manager
kb = get_kb_manager()
coll = kb.get_collection('my_ky')
result = coll.get(limit=10, include=['metadatas'])
for meta in result['metadatas']:
print(f"chunk_type: {meta.get('chunk_type')}")
print(f"images_json: {meta.get('images_json')}")
print(f"image_path: {meta.get('image_path')}")
print("---")
```
### 2. 检查 MinerU 解析结果
```python
from parsers.mineru_parser import parse_with_mineru
result = parse_with_mineru("tests/public/test_report.pdf")
for chunk in result.get('chunks', []):
if chunk.chunk_type == 'table':
print(f"表格标题: {chunk.title}")
print(f"table_html 长度: {len(chunk.table_html or '')}")
print(f"image_path: {chunk.image_path}")
print(f"images: {chunk.images}")
print("---")
```

View File

@@ -1,785 +0,0 @@
# 企业文档更新管理方案
> 本文档合并了企业文档管理的多种方案比较分析包括增量更新、完整版本管理和软删除等以及目前项目中所采纳的“方案C智能全量更新”的具体实现细则。
## 第一部分:文档更新管理方案横评
# 企业文档管理方案分析与建议
> 针对企业文档部分更新、文件废止等场景的最佳实践
---
## 📋 企业文档管理的实际需求
### 典型场景
1. **文档部分更新**
- 制度文件修订报销制度第3条修改
- 附件更新(如:报销单模板更新)
- 内容勘误(如:错别字修正)
2. **文档废止**
- 旧制度失效2023年报销制度被2024年版本替代
- 临时文件过期(如:疫情期间的临时政策)
- 部门撤销(如:某部门解散,相关文档废止)
3. **版本管理**
- 多版本共存(如:新旧制度过渡期)
- 历史追溯(如:查询某个时间点的制度内容)
- 变更记录(如:审计需要查看修改历史)
---
## 🔍 当前实现分析
### 现有机制sync.py
**优点**
- ✅ 自动检测文件变更(新增、修改、删除)
- ✅ 基于文件 Hash 判断内容是否变化
- ✅ 支持多向量库(按目录自动分类)
- ✅ 实时监控文件系统变化
**处理策略**
```python
# 当前的"全量更新"策略
if change.change_type == ChangeType.MODIFIED:
# 1. 删除旧文档的所有 chunks
deleted = kb_manager.delete_document(kb_name, filename)
# 2. 重新解析并添加新文档的所有 chunks
chunks_added = kb_manager.add_file_to_kb(kb_name, filepath)
```
**问题**
- ❌ 即使只修改一个字,也要重新解析整个文档
- ❌ 删除所有旧 chunks可能影响正在使用的查询
- ❌ 没有保留历史版本
- ❌ 无法追溯变更内容
---
## 💡 解决方案对比
### 方案 A增量更新diff.py 的设计思路)
**原理**
```python
# 1. 解析新旧文档
old_chunks = parse_document(old_version)
new_chunks = parse_document(new_version)
# 2. 计算差异
diff = DocumentDiffAnalyzer().compute_diff(old_chunks, new_chunks)
# 3. 增量更新
for chunk in diff.added:
kb_manager.add_chunk(chunk) # 只添加新增的
for chunk in diff.deleted:
kb_manager.delete_chunk(chunk.id) # 只删除被删的
for chunk in diff.modified:
kb_manager.update_chunk(chunk.id, chunk.new_content) # 只更新修改的
```
**优点**
- ✅ 性能优化:只处理变化的部分
- ✅ 减少重复计算:不需要重新 Embedding 未变化的内容
- ✅ 平滑过渡:不影响正在使用的 chunks
**缺点**
- ❌ 实现复杂:需要精确匹配新旧 chunks
- ❌ 匹配困难:文档结构变化时难以对应
- ❌ 边界问题chunk 边界变化导致误判
- ❌ 维护成本高525 行代码,逻辑复杂
**适用场景**
- 超大文档1000+ 页)
- 频繁小改动(每天多次更新)
- 对性能要求极高
**企业实际情况**
- ❌ 大部分企业文档 < 100 页
- ❌ 更新频率低(每月/每季度)
- ❌ 全量更新耗时可接受(几秒到几十秒)
---
### 方案 B版本管理 + 软删除lifecycle.py 的设计思路)
**原理**
```python
# 1. 保留所有版本
document_versions = [
{"version": "v1", "status": "superseded", "upload_time": "2023-01-01"},
{"version": "v2", "status": "superseded", "upload_time": "2023-06-01"},
{"version": "v3", "status": "active", "upload_time": "2024-01-01"}
]
# 2. 查询时只返回 active 版本
chunks = kb_manager.query(kb_name, query, filter={"status": "active"})
# 3. 废止文档(软删除)
lifecycle_manager.deprecate_document(kb_name, doc_id, reason="制度已更新")
# 实际操作:将 status 改为 "deprecated",不删除数据
```
**优点**
- ✅ 历史追溯:可以查询任意时间点的内容
- ✅ 安全回滚:废止操作可逆
- ✅ 审计友好:完整的变更记录
- ✅ 过渡期支持:新旧版本可以共存
**缺点**
- ❌ 存储成本:保留所有历史版本
- ❌ 查询复杂:需要过滤 status
- ❌ 数据膨胀:向量库体积增大
**适用场景**
- 合规要求高(金融、医疗)
- 需要审计追溯
- 文档变更频繁但需要保留历史
---
### 方案 C智能全量更新推荐
**原理**
```python
# 1. 检测变更
if file_hash_changed:
# 2. 标记旧版本(软删除)
kb_manager.mark_document_as_deprecated(kb_name, doc_id, version="v1")
# 3. 添加新版本
kb_manager.add_file_to_kb(
kb_name,
filepath,
extra_metadata={
"status": "active",
"version": "v2",
"previous_version": "v1",
"change_reason": "制度修订"
}
)
# 4. 异步清理旧版本(可选)
schedule_cleanup(kb_name, doc_id, version="v1", delay="7 days")
```
**优点**
- ✅ 实现简单:基于现有 sync.py
- ✅ 性能可接受:全量更新耗时短
- ✅ 可靠性高:不依赖复杂的 diff 算法
- ✅ 灵活性好:可选保留历史版本
**缺点**
- ⚠️ 短暂的双份数据(新旧版本共存期间)
**适用场景**
- ✅ 大部分企业场景
- ✅ 文档更新频率适中
- ✅ 对性能要求不极端
---
## 📊 方案对比总结
| 方案 | 实现复杂度 | 性能 | 存储成本 | 历史追溯 | 适用场景 |
|------|-----------|------|---------|---------|---------|
| **A. 增量更新** | ⭐⭐⭐⭐⭐ 高 | ⭐⭐⭐⭐⭐ 优 | ⭐⭐⭐⭐⭐ 低 | ❌ 无 | 超大文档、频繁更新 |
| **B. 完整版本管理** | ⭐⭐⭐⭐ 中高 | ⭐⭐⭐ 中 | ⭐⭐ 高 | ✅ 完整 | 金融、医疗等合规场景 |
| **C. 智能全量更新** | ⭐⭐ 低 | ⭐⭐⭐⭐ 良 | ⭐⭐⭐⭐ 中 | ✅ 可选 | **大部分企业场景** ⭐ |
---
## 🎯 最终建议
### 推荐方案:方案 C智能全量更新 + 轻量级版本管理)
**理由**
1.**实现简单**:基于现有 sync.py增量开发
2.**性能足够**:全量更新耗时可接受(秒级)
3.**功能完整**:支持版本管理、软删除、历史追溯
4.**维护成本低**:逻辑清晰,不易出错
5.**适用性广**:覆盖 90% 的企业场景
**不推荐**
- ❌ 方案 A增量更新实现复杂收益不明显
- ⚠️ 方案 B完整版本管理存储成本高大部分企业用不到
### 具体操作
1. **删除 diff.py**525 行)
2. **简化 lifecycle.py**(保留 ~200 行核心功能)
3. **增强 sync.py**(添加版本管理逻辑)
4. **补充 API**(废止、恢复、历史查询)
**预期效果**
- 减少 ~800 行冗余代码
- 保留实际需要的功能
- 满足企业文档管理需求
---
**文档版本**: v1.0
**创建时间**: 2026-04-20
**维护者**: RAG 服务开发组
---
## 第二部分选定方案方案C详细落地落实说明
# 方案 C文档、废止状态与向量库关系详解
> 详细说明智能全量更新方案的数据流和状态管理
---
## 📊 核心概念
### 三个层次
1. **物理层**`documents/` 目录(文件系统)
2. **逻辑层**:文档状态管理(数据库)
3. **检索层**向量库ChromaDB/Milvus
---
## 🔄 完整数据流图
```
┌─────────────────────────────────────────────────────────────┐
│ 1. 物理层documents/ │
│ │
│ documents/ │
│ ├── public/ │
│ │ ├── 报销制度_v1.pdf ← 旧版本(物理存在) │
│ │ ├── 报销制度_v2.pdf ← 新版本(物理存在) │
│ │ └── 临时防疫政策.pdf ← 已废止(物理存在) │
│ └── finance/ │
│ └── 差旅管理办法.pdf │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 2. 逻辑层document_versions 表 │
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ kb_name │ document_id │ version │ status │ │
│ ├─────────┼────────────────────┼─────────┼─────────────┤ │
│ │ public │ 报销制度_v1.pdf │ v1 │ superseded │ │
│ │ public │ 报销制度_v2.pdf │ v2 │ active │ │
│ │ public │ 临时防疫政策.pdf │ v1 │ deprecated │ │
│ │ finance │ 差旅管理办法.pdf │ v1 │ active │ │
│ └──────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 3. 检索层向量库ChromaDB
│ │
│ Collection: public_kb │
│ ┌────────────────────────────────────────────────────┐ │
│ │ chunk_id │ content │ metadata │ │
│ ├──────────┼──────────────┼───────────────────────────┤ │
│ │ c1 │ 报销流程... │ {doc: 报销制度_v1.pdf, │ │
│ │ │ │ status: superseded, │ │
│ │ │ │ version: v1} │ │
│ ├──────────┼──────────────┼───────────────────────────┤ │
│ │ c2 │ 报销流程... │ {doc: 报销制度_v2.pdf, │ │
│ │ │ │ status: active, │ │
│ │ │ │ version: v2} │ │
│ ├──────────┼──────────────┼───────────────────────────┤ │
│ │ c3 │ 防疫要求... │ {doc: 临时防疫政策.pdf, │ │
│ │ │ │ status: deprecated, │ │
│ │ │ │ version: v1} │ │
│ └────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
```
---
## 📝 详细场景说明
### 场景 1文档更新报销制度 v1 → v2
#### 步骤 1用户上传新版本
```bash
# 用户操作
documents/public/报销制度_v2.pdf # 上传新文件
```
#### 步骤 2系统检测变更
```python
# sync.py 自动检测
change = DocumentChange(
document_id="public/报销制度_v2.pdf",
change_type=ChangeType.ADDED,
new_hash="abc123..."
)
```
#### 步骤 3处理更新
```python
# 方案 C 的处理逻辑
def process_document_update(kb_name, old_doc_id, new_doc_path):
# 1. 标记旧版本为 superseded不删除
kb_manager.update_metadata(
kb_name="public",
filter={
"document_id": "报销制度_v1.pdf",
"status": "active"
},
update={
"status": "superseded",
"superseded_by": "报销制度_v2.pdf",
"superseded_time": "2024-01-15 10:00:00"
}
)
# 2. 添加新版本
chunks_added = kb_manager.add_file_to_kb(
kb_name="public",
filepath="documents/public/报销制度_v2.pdf",
extra_metadata={
"status": "active",
"version": "v2",
"previous_version": "v1",
"document_id": "报销制度_v2.pdf",
"upload_time": "2024-01-15 10:00:00"
}
)
# 3. 记录版本历史
db.insert_version_record({
"kb_name": "public",
"document_id": "报销制度_v2.pdf",
"version": "v2",
"status": "active",
"previous_version": "v1",
"change_reason": "制度修订"
})
# 4. 可选7天后清理旧版本
schedule_cleanup(
kb_name="public",
document_id="报销制度_v1.pdf",
delay_days=7
)
```
#### 步骤 4查询时的效果
```python
# 用户查询:"报销流程是什么?"
results = kb_manager.query_kb(
kb_name="public",
query="报销流程是什么",
top_k=5,
where_filter={"status": "active"} # 只查询 active 状态
)
# 返回结果:
# ✅ 报销制度_v2.pdf 的内容(新版本)
# ❌ 报销制度_v1.pdf 的内容(被过滤掉)
```
#### 数据状态对比
**物理层documents/**
```
documents/public/
├── 报销制度_v1.pdf ← 仍然存在(用户可能需要查看旧版)
└── 报销制度_v2.pdf ← 新版本
```
**逻辑层document_versions 表)**
```sql
-- 旧版本记录
INSERT INTO document_versions VALUES (
'public', '报销制度_v1.pdf', 'v1', 'superseded',
'2023-01-01', '被 v2 替代'
);
-- 新版本记录
INSERT INTO document_versions VALUES (
'public', '报销制度_v2.pdf', 'v2', 'active',
'2024-01-15', NULL
);
```
**检索层(向量库)**
```python
# 旧版本 chunksstatus=superseded查询时被过滤
{
"chunk_id": "c1",
"content": "报销流程:先填写申请单...",
"metadata": {
"document_id": "报销制度_v1.pdf",
"status": "superseded", # ← 关键字段
"version": "v1"
}
}
# 新版本 chunksstatus=active查询时返回
{
"chunk_id": "c2",
"content": "报销流程:使用新系统提交...",
"metadata": {
"document_id": "报销制度_v2.pdf",
"status": "active", # ← 关键字段
"version": "v2"
}
}
```
---
### 场景 2文档废止临时防疫政策失效
#### 步骤 1管理员废止文档
```python
# API 调用
POST /api/kb/public/documents/临时防疫政策.pdf/deprecate
{
"reason": "疫情结束,政策失效"
}
```
#### 步骤 2系统处理废止
```python
def deprecate_document(kb_name, doc_id, reason):
# 1. 更新向量库 metadata
kb_manager.update_metadata(
kb_name="public",
filter={
"document_id": "临时防疫政策.pdf",
"status": "active"
},
update={
"status": "deprecated",
"deprecated_reason": "疫情结束,政策失效",
"deprecated_time": "2024-01-20 15:00:00"
}
)
# 2. 更新版本表
db.update_version_status(
kb_name="public",
document_id="临时防疫政策.pdf",
status="deprecated",
reason="疫情结束,政策失效"
)
# 3. 记录废止日志
db.insert_change_log({
"kb_name": "public",
"document_id": "临时防疫政策.pdf",
"change_type": "deprecate",
"reason": "疫情结束,政策失效",
"operator": "admin"
})
```
#### 步骤 3查询时的效果
```python
# 用户查询:"防疫政策是什么?"
results = kb_manager.query_kb(
kb_name="public",
query="防疫政策是什么",
top_k=5,
where_filter={"status": "active"} # 只查询 active 状态
)
# 返回结果:
# ❌ 临时防疫政策.pdf 的内容(被过滤掉)
# ✅ 其他 active 状态的文档
```
#### 数据状态
**物理层documents/**
```
documents/public/
└── 临时防疫政策.pdf ← 仍然存在(可能需要归档)
```
**逻辑层document_versions 表)**
```sql
UPDATE document_versions
SET status = 'deprecated',
status_reason = '疫情结束,政策失效',
deprecated_time = '2024-01-20 15:00:00'
WHERE kb_name = 'public'
AND document_id = '临时防疫政策.pdf';
```
**检索层(向量库)**
```python
# 废止后的 chunksstatus=deprecated查询时被过滤
{
"chunk_id": "c3",
"content": "疫情期间需要佩戴口罩...",
"metadata": {
"document_id": "临时防疫政策.pdf",
"status": "deprecated", # ← 关键字段
"deprecated_reason": "疫情结束,政策失效"
}
}
```
---
### 场景 3恢复已废止的文档
#### 步骤 1管理员恢复文档
```python
# API 调用
POST /api/kb/public/documents/临时防疫政策.pdf/restore
{
"reason": "疫情反复,政策恢复"
}
```
#### 步骤 2系统处理恢复
```python
def restore_document(kb_name, doc_id, reason):
# 1. 更新向量库 metadata
kb_manager.update_metadata(
kb_name="public",
filter={
"document_id": "临时防疫政策.pdf",
"status": "deprecated"
},
update={
"status": "active",
"restored_reason": "疫情反复,政策恢复",
"restored_time": "2024-02-01 09:00:00"
}
)
# 2. 更新版本表
db.update_version_status(
kb_name="public",
document_id="临时防疫政策.pdf",
status="active",
reason="疫情反复,政策恢复"
)
```
#### 步骤 3查询时的效果
```python
# 用户查询:"防疫政策是什么?"
results = kb_manager.query_kb(
kb_name="public",
query="防疫政策是什么",
top_k=5,
where_filter={"status": "active"}
)
# 返回结果:
# ✅ 临时防疫政策.pdf 的内容(已恢复)
```
---
## 🔍 查询行为详解
### 默认查询(只返回 active 文档)
```python
def query_kb(self, kb_name: str, query: str, top_k: int = 5):
"""默认查询:只返回生效的文档"""
results = self.collection.query(
query_texts=[query],
n_results=top_k,
where={
"status": "active" # ← 自动过滤
}
)
return results
```
**效果**
- ✅ 返回报销制度_v2.pdfactive
- ❌ 过滤报销制度_v1.pdfsuperseded
- ❌ 过滤:临时防疫政策.pdfdeprecated
### 历史查询(包含所有版本)
```python
def query_kb_with_history(self, kb_name: str, query: str, top_k: int = 5):
"""历史查询:包含所有版本"""
results = self.collection.query(
query_texts=[query],
n_results=top_k,
where={
"status": {"$in": ["active", "superseded", "deprecated"]}
}
)
return results
```
**效果**
- ✅ 返回报销制度_v2.pdfactive
- ✅ 返回报销制度_v1.pdfsuperseded
- ✅ 返回:临时防疫政策.pdfdeprecated
### 特定版本查询
```python
def query_specific_version(self, kb_name: str, query: str, version: str):
"""查询特定版本"""
results = self.collection.query(
query_texts=[query],
n_results=5,
where={
"version": version # 指定版本
}
)
return results
```
---
## 📊 数据清理策略
### 自动清理(可选)
```python
def schedule_cleanup(kb_name: str, doc_id: str, delay_days: int = 7):
"""
定期清理 superseded 版本
策略:
1. 保留最近 7 天的 superseded 版本(防止误操作)
2. 7 天后自动删除向量库中的 chunks
3. 保留版本记录document_versions 表)
"""
# 7 天后执行
schedule_task(
task=lambda: kb_manager.delete_chunks(
kb_name=kb_name,
filter={
"document_id": doc_id,
"status": "superseded"
}
),
delay=timedelta(days=delay_days)
)
```
### 手动清理
```python
# API 端点
POST /api/kb/public/cleanup
{
"strategy": "superseded", # 清理 superseded 版本
"older_than_days": 30 # 超过 30 天的
}
```
---
## 🎯 关键优势
### 1. 物理层与逻辑层分离
**物理层documents/**
- 文件可以保留(用户可能需要下载旧版)
- 文件可以删除(不影响向量库)
- 灵活管理
**逻辑层(向量库)**
- 通过 metadata 控制可见性
- 不需要物理删除
- 支持快速恢复
### 2. 查询时自动过滤
```python
# 用户无感知,系统自动过滤废止文档
results = query_kb(kb_name, query) # 只返回 active 文档
```
### 3. 历史可追溯
```python
# 管理员可以查询历史版本
history = get_document_history(kb_name, doc_id)
# 返回v1 (superseded), v2 (active)
```
### 4. 操作可逆
```python
# 废止操作可以恢复
deprecate_document(kb_name, doc_id) # 废止
restore_document(kb_name, doc_id) # 恢复
```
---
## 📈 存储成本分析
### 短期7天内
```
向量库大小 = active 文档 + superseded 文档7天内
存储成本 = 1.2x ~ 1.5x(相比只保留 active
```
### 长期7天后自动清理
```
向量库大小 = active 文档
存储成本 = 1.0x(与只保留 active 相同)
```
### 版本记录(永久保留)
```
document_versions 表大小 = 每个版本 ~1KB
100 个文档 × 平均 3 个版本 = 300KB可忽略
```
---
## ✅ 总结
### 方案 C 的核心特点
1. **物理层**documents/ 目录可以保留或删除文件,不影响向量库
2. **逻辑层**:通过 status 字段控制文档可见性
3. **检索层**:查询时自动过滤非 active 文档
4. **历史追溯**:保留版本记录,支持审计
5. **操作可逆**:废止/恢复操作不删除数据
6. **自动清理**:定期清理旧版本,控制存储成本
### 与现有方案的区别
| 方面 | 当前方案 | 方案 C |
|------|---------|--------|
| 文档更新 | 删除旧 chunks添加新 chunks | 标记旧 chunks 为 superseded添加新 chunks |
| 文档废止 | 删除 chunks | 标记 chunks 为 deprecated |
| 历史追溯 | ❌ 无法查询旧版本 | ✅ 可以查询任意版本 |
| 操作可逆 | ❌ 删除后无法恢复 | ✅ 废止后可以恢复 |
| 存储成本 | 低 | 中(短期略高,长期相同) |
---
**文档版本**: v1.0
**创建时间**: 2026-04-20
**维护者**: RAG 服务开发组

View File

@@ -1,635 +0,0 @@
# 出题批卷系统设计
> **文档类型**: 系统设计文档
> **创建日期**: 2026-04-10
> **最后更新**: 2026-06-04
> **状态**: 已实施
---
## 一、系统概述
### 1.1 背景
出题批卷系统是 RAG 知识库系统的扩展模块,支持:
- **按文件出题**:根据指定文档自动生成题目
- **智能批卷**:支持选择题、填空题、简答题的自动批改
- **溯源追踪**:每道题可追溯到来源文件和知识片段
### 1.2 模块结构
```
exam_pkg/ # 考试系统
├── generator.py # 出题逻辑(按文件/按主题生成题目)
├── grader.py # 批卷逻辑(选择题/填空题/简答题批改)
├── manager.py # 试卷管理与协调逻辑
├── api.py # Flask Blueprint (exam_bp)
└── local_db.py # 本地题库 (SQLite)
```
**认证模块**: `auth/gateway.py` - 网关认证
---
## 二、出题系统设计
### 2.1 按文件出题接口
**接口路径**`POST /exam/generate-by-file`
**请求参数**
```json
{
"file_path": "public/产品手册.pdf",
"collection": "public_kb",
"choice_count": 5,
"blank_count": 2,
"short_answer_count": 2,
"difficulty": 3,
"choice_score": 2,
"blank_score": 3
}
```
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|------|------|------|--------|------|
| `file_path` | string | ✅ | - | 文件路径 |
| `collection` | string | ✅ | - | 向量库名称 |
| `choice_count` | int | ❌ | 3 | 选择题数量 |
| `blank_count` | int | ❌ | 2 | 填空题数量 |
| `short_answer_count` | int | ❌ | 2 | 简答题数量 |
| `difficulty` | int | ❌ | 3 | 难度等级 (1-5) |
| `choice_score` | int | ❌ | 2 | 每道选择题分值 |
| `blank_score` | int | ❌ | 3 | 每道填空题分值 |
**返回结果**
```json
{
"exam_id": "uuid-xxxx-xxxx",
"source_file": {
"path": "public/产品手册.pdf",
"collection": "public_kb"
},
"choice_questions": [
{
"id": "q_choice_001",
"content": "根据保密制度,公司最高机密的处理原则是什么?",
"options": ["A. 可向客户透露", "B. 严禁外传", "C. 部门内共享", "D. 仅领导知晓"],
"answer": "B",
"analysis": "根据保密制度第1条规定...",
"knowledge_points": ["保密制度", "信息安全"],
"difficulty": 2,
"score": 2,
"source_file": "public/产品手册.pdf",
"source_snippet": "该题依据的知识片段..."
}
],
"blank_questions": [...],
"short_answer_questions": [...],
"total_count": 9,
"total_score": 22,
"generated_at": "2026-04-10T14:00:00"
}
```
### 2.2 试卷状态流程
```
生成试卷 → draft (草稿)
提交审核 → pending_review (待审核)
管理员审核 → approved (通过) / rejected (驳回)
学生答题 → 批阅 → 生成报告
```
**状态说明**
| 状态 | 说明 | 可见范围 |
|------|------|----------|
| `draft` | 草稿,刚生成尚未提交审核 | 创建者可见 |
| `pending_review` | 待审核,已提交等待管理员审核 | 管理员可见 |
| `approved` | 已通过,可用于学生答题 | 所有用户可见 |
| `rejected` | 已驳回,不可使用 | 创建者可见 |
---
## 三、题目格式规范
### 3.1 选择题
```json
{
"id": "q_choice_001",
"content": "根据保密制度,公司最高机密的处理原则是什么?",
"options": [
"A. 可向客户透露",
"B. 严禁外传",
"C. 部门内共享",
"D. 仅领导知晓"
],
"answer": "B",
"analysis": "根据保密制度第1条规定公司最高机密严禁外传仅限特定人员知晓。",
"knowledge_points": ["保密制度", "信息安全"],
"difficulty": 2,
"score": 2,
"source_file": "public/产品手册.pdf",
"source_snippet": "原文相关片段..."
}
```
**字段说明**
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `id` | string | ✅ | 题目唯一标识 |
| `content` | string | ✅ | 题干内容 |
| `options` | array | ✅ | 选项列表,格式为 `["A. 选项内容", ...]` |
| `answer` | string | ✅ | 正确答案,单个字母(如 "A", "B" |
| `analysis` | string | ✅ | 答案解析 |
| `knowledge_points` | array | ❌ | 知识点标签 |
| `difficulty` | int | ❌ | 难度等级 1-5默认 3 |
| `score` | int | ✅ | 题目分值 |
| `source_file` | string | ❌ | 来源文件路径 |
| `source_snippet` | string | ❌ | 来源文本片段 |
### 3.2 填空题
```json
{
"id": "q_blank_001",
"content": "公司财务报表应在每季度结束后______天内提交。",
"answer": "15",
"analysis": "根据财务管理制度第5条规定季度报表需在季后15天内提交。",
"knowledge_points": ["财务管理"],
"difficulty": 3,
"score": 3
}
```
**字段说明**
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `id` | string | ✅ | 题目唯一标识 |
| `content` | string | ✅ | 题干内容,空缺处用 `______` 表示 |
| `answer` | string | ✅ | 正确答案 |
| `analysis` | string | ✅ | 答案解析 |
| `knowledge_points` | array | ❌ | 知识点标签 |
| `difficulty` | int | ❌ | 难度等级 1-5 |
| `score` | int | ✅ | 题目分值 |
### 3.3 简答题
```json
{
"id": "q_short_001",
"content": "简述公司数据安全的三道防线。",
"reference_answer": {
"points": [
{"point": "技术防线(防火墙、加密、访问控制等)", "score": 3},
{"point": "制度防线(安全规定、审批流程、应急预案)", "score": 3},
{"point": "人员防线(安全培训、意识教育、考核机制)", "score": 4}
],
"total_score": 10
},
"analysis": "评分要点说明...",
"knowledge_points": ["数据安全"],
"difficulty": 4,
"score": 10
}
```
**字段说明**
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `id` | string | ✅ | 题目唯一标识 |
| `content` | string | ✅ | 题干内容 |
| `reference_answer` | object | ✅ | 参考答案,包含评分要点 |
| `reference_answer.points` | array | ✅ | 得分点列表 |
| `reference_answer.points[].point` | string | ✅ | 得分点描述 |
| `reference_answer.points[].score` | int | ✅ | 该得分点分值 |
| `analysis` | string | ❌ | 整体解析 |
| `knowledge_points` | array | ❌ | 知识点标签 |
| `difficulty` | int | ❌ | 难度等级 1-5 |
| `score` | int | ✅ | 题目总分值 |
---
## 四、批卷系统设计
### 4.1 批卷输入格式
**接口路径**`POST /exam/grade-from-mysql`
**当前格式(完整字段)**
```json
{
"exam_id": "uuid-xxxx-xxxx",
"student_id": "STU_2023001",
"student_name": "张三",
"answers": [
{
"question_id": "q_choice_001",
"question_type": "choice",
"question_content": "根据保密制度,公司最高机密的处理原则是什么?",
"options": ["A. 可向客户透露", "B. 严禁外传", "C. 部门内共享", "D. 仅领导知晓"],
"correct_answer": "B",
"max_score": 2,
"student_answer": "B"
},
{
"question_id": "q_blank_001",
"question_type": "blank",
"question_content": "公司财务报表应在每季度结束后______天内提交。",
"correct_answer": "15",
"max_score": 3,
"student_answer": "10"
},
{
"question_id": "q_short_001",
"question_type": "short_answer",
"question_content": "简述公司数据安全的三道防线。",
"correct_answer": "{\"points\":[{\"point\":\"技术防线\",\"score\":3},{\"point\":\"制度防线\",\"score\":3},{\"point\":\"人员防线\",\"score\":4}]}",
"max_score": 10,
"student_answer": "第一道是技术防护,包括防火墙和加密;第二道是制度管理;第三道是员工培训。"
}
]
}
```
**优化后格式(最小字段)**
```json
{
"exam_id": "uuid",
"student_id": "STU_001",
"student_name": "张三",
"answers": [
{
"question_id": "q_choice_001",
"question_type": "choice",
"student_answer": "B"
},
{
"question_id": "q_blank_001",
"question_type": "blank",
"student_answer": "15"
},
{
"question_id": "q_short_001",
"question_type": "short_answer",
"student_answer": "第一道是技术防护..."
}
]
}
```
### 4.2 批卷输出格式
```json
{
"report_id": "report-uuid-xxxx",
"exam_id": "uuid-xxxx-xxxx",
"student_id": "STU_2023001",
"student_name": "张三",
"total_score": 12,
"max_score": 15,
"score_rate": 80.0,
"graded_at": "2026-04-12T14:30:00",
"results": [
{
"question_id": "q_choice_001",
"question_type": "choice",
"correct": true,
"score": 2,
"max_score": 2,
"student_answer": "B",
"correct_answer": "B",
"feedback": "回答正确!"
},
{
"question_id": "q_blank_001",
"question_type": "blank",
"correct": false,
"score": 0,
"max_score": 3,
"student_answer": "10",
"correct_answer": "15",
"feedback": "正确答案是15天请复习财务管理制度。"
},
{
"question_id": "q_short_001",
"question_type": "short_answer",
"score": 8,
"max_score": 10,
"student_answer": "第一道是技术防护...",
"score_details": [
{"point": "技术防线", "earned": 3, "max": 3},
{"point": "制度防线", "earned": 2, "max": 3},
{"point": "人员防线", "earned": 3, "max": 4}
],
"feedback": "整体回答较好,制度防线描述不够具体。",
"highlights": ["技术防线表述准确"],
"shortcomings": ["制度防线未具体说明"],
"suggestions": ["建议补充具体的制度名称"]
}
],
"summary": {
"strengths": ["选择题掌握较好", "简答题要点覆盖全面"],
"weaknesses": ["填空题记忆不准确"],
"recommendations": ["重点复习财务管理制度第3章"]
}
}
```
### 4.3 批改流程
```
┌─────────────────────────────────────────────────────────────────────┐
│ 批量批改流程 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 1. 前端传入 answers (最小字段) │
│ └─ 只有 question_id + question_type + student_answer │
│ │
│ 2. 后端查询题目详情 │
│ └─ 从数据库/缓存获取 correct_answer, max_score, content │
│ │
│ 3. 按题型分组 │
│ ├─ choice 组 → 批量调用 Dify 代码执行节点 │
│ └─ blank/short_answer 组 → 批量调用 Dify LLM 节点 │
│ │
│ 4. 合并结果返回 │
│ └─ 统一格式返回所有批改结果 │
│ │
└─────────────────────────────────────────────────────────────────────┘
```
---
## 五、数据库设计
### 5.1 题目表 (questions)
```sql
CREATE TABLE questions (
id VARCHAR(64) PRIMARY KEY, -- 题目IDUUID
question_type ENUM('choice', 'blank', 'short_answer') NOT NULL,
content TEXT NOT NULL, -- 题干内容
options JSON, -- 选择题选项JSON数组
correct_answer TEXT NOT NULL, -- 正确答案
analysis TEXT, -- 解析
knowledge_points JSON, -- 知识点JSON数组
difficulty TINYINT DEFAULT 3, -- 难度(1-5)
score INT NOT NULL, -- 分值
-- 溯源字段(核心)
source_file VARCHAR(255) NOT NULL, -- 来源文件路径
source_collection VARCHAR(64) NOT NULL, -- 来源向量库
source_snippet TEXT, -- 来源知识片段
source_hash VARCHAR(64), -- 文件哈希(用于检测文件变更)
-- 审核状态
status ENUM('pending', 'approved', 'rejected') DEFAULT 'pending',
reviewed_by VARCHAR(64),
reviewed_at DATETIME,
-- 元数据
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
created_by VARCHAR(64),
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_source_file (source_file),
INDEX idx_source_collection (source_collection),
INDEX idx_question_type (question_type),
INDEX idx_status (status)
);
```
### 5.2 试卷表 (exams)
```sql
CREATE TABLE exams (
id VARCHAR(64) PRIMARY KEY, -- 试卷ID
name VARCHAR(255) NOT NULL, -- 试卷名称
description TEXT, -- 描述
total_score INT NOT NULL, -- 总分
total_count INT NOT NULL, -- 题目总数
duration INT DEFAULT 60, -- 考试时长(分钟)
-- 状态
status ENUM('draft', 'pending', 'published', 'archived') DEFAULT 'draft',
published_at DATETIME,
-- 元数据
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
created_by VARCHAR(64),
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_status (status)
);
```
### 5.3 试卷题目关联表 (exam_questions)
```sql
CREATE TABLE exam_questions (
exam_id VARCHAR(64) NOT NULL,
question_id VARCHAR(64) NOT NULL,
question_order INT NOT NULL, -- 题目顺序
PRIMARY KEY (exam_id, question_id),
FOREIGN KEY (exam_id) REFERENCES exams(id) ON DELETE CASCADE,
FOREIGN KEY (question_id) REFERENCES questions(id) ON DELETE CASCADE,
INDEX idx_exam_id (exam_id),
INDEX idx_question_id (question_id)
);
```
### 5.4 学生答卷表 (student_answers)
```sql
CREATE TABLE student_answers (
id VARCHAR(64) PRIMARY KEY,
exam_id VARCHAR(64) NOT NULL,
student_id VARCHAR(64) NOT NULL,
student_name VARCHAR(100),
question_id VARCHAR(64) NOT NULL,
question_type ENUM('choice', 'blank', 'short_answer') NOT NULL,
student_answer TEXT NOT NULL, -- 学生答案
-- 批阅结果
score INT DEFAULT 0,
max_score INT NOT NULL,
feedback TEXT,
score_details JSON, -- 评分详情JSON
-- 元数据
submitted_at DATETIME DEFAULT CURRENT_TIMESTAMP,
graded_at DATETIME,
FOREIGN KEY (exam_id) REFERENCES exams(id) ON DELETE CASCADE,
FOREIGN KEY (question_id) REFERENCES questions(id) ON DELETE CASCADE,
INDEX idx_exam_student (exam_id, student_id),
INDEX idx_student_id (student_id)
);
```
### 5.5 批阅报告表 (grade_reports)
```sql
CREATE TABLE grade_reports (
id VARCHAR(64) PRIMARY KEY,
exam_id VARCHAR(64) NOT NULL,
student_id VARCHAR(64) NOT NULL,
student_name VARCHAR(100),
total_score INT NOT NULL,
max_score INT NOT NULL,
score_rate DECIMAL(5,2),
-- 整卷分析(可选)
analysis JSON, -- AI生成的整卷分析
graded_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (exam_id) REFERENCES exams(id) ON DELETE CASCADE,
INDEX idx_exam_id (exam_id),
INDEX idx_student_id (student_id)
);
```
---
## 六、文件修改联动
### 6.1 触发条件
当文件被修改或删除时,通过 `source_file` 字段查找受影响的题目。
### 6.2 联动逻辑
```sql
-- 查找受影响的题目
SELECT id, source_file, source_hash
FROM questions
WHERE source_file = 'public/产品手册.pdf';
-- 如果文件哈希变更,标记题目需要重新审核
UPDATE questions
SET status = 'pending',
source_hash = 'new_hash_value'
WHERE source_file = 'public/产品手册.pdf';
```
### 6.3 联动接口
**接口路径**`POST /exam/check-file-changes`
**请求参数**
```json
{
"file_path": "public/产品手册.pdf",
"new_hash": "新的文件哈希"
}
```
**返回结果**
```json
{
"success": true,
"file_path": "public/产品手册.pdf",
"affected_questions": ["q_uuid_001", "q_uuid_002", ...],
"count": 15,
"recommendation": "建议重新生成该文件的题目"
}
```
---
## 七、API 接口汇总
### 7.1 出题接口
| 接口 | 方法 | 说明 |
|------|------|------|
| `/exam/generate` | POST | 按主题生成试卷 |
| `/exam/generate-by-file` | POST | 按文件生成题目 |
| `/exam/list` | GET | 获取试卷列表 |
| `/exam/<exam_id>` | GET | 获取试卷详情 |
| `/exam/<exam_id>` | PUT | 更新试卷 |
| `/exam/<exam_id>` | DELETE | 删除试卷 |
| `/exam/<exam_id>/submit` | POST | 提交审核 |
| `/exam/<exam_id>/review` | POST | 审核试卷(仅管理员) |
| `/exam/by-file` | GET | 查询文件关联的题目 |
### 7.2 批卷接口
| 接口 | 方法 | 说明 |
|------|------|------|
| `/exam/grade-from-mysql` | POST | 基于传入题目批卷 |
| `/exam/<exam_id>/grade` | POST | 批阅试卷 |
| `/exam/report/<report_id>` | GET | 获取批阅报告 |
| `/exam/report/list` | GET | 批阅报告列表 |
### 7.3 题库接口
| 接口 | 方法 | 说明 |
|------|------|------|
| `/exam/questions/search` | GET | 搜索题目 |
### 7.4 联动接口
| 接口 | 方法 | 说明 |
|------|------|------|
| `/exam/check-file-changes` | POST | 检查文件变更影响的题目 |
---
## 八、错误处理
### 8.1 错误响应格式
```json
{
"error": "错误类型",
"message": "详细错误信息",
"details": {}
}
```
### 8.2 常见错误码
| HTTP状态码 | 错误类型 | 说明 |
|-----------|---------|------|
| 400 | bad_request | 请求参数格式错误 |
| 401 | unauthorized | 未认证 |
| 403 | forbidden | 权限不足 |
| 404 | not_found | 资源不存在 |
| 500 | internal_error | 服务器内部错误 |
---
## 九、注意事项
1. **题目ID生成**使用UUID确保全局唯一
2. **文件哈希**用于检测文件变更建议使用MD5或SHA256
3. **批量批卷性能**:简答题批卷耗时,建议使用异步处理或并发
4. **错误处理**:批卷失败时返回默认结果,不影响整体流程
5. **认证方式**:出题系统使用 JWT Bearer Token 认证
---
## 十、变更记录
| 日期 | 版本 | 变更内容 |
|------|------|---------|
| 2026-06-04 | 2.1 | 更新模块结构:移除已删除的 analysis.py、question_hook.py新增 generator.py、grader.py |
| 2026-04-13 | 2.0 | 合并出题批卷功能改造计划、批卷工作流优化计划、批卷接口规范 |
| 2026-04-12 | 1.2 | 新增最小字段输入格式,优化批量批改流程 |
| 2026-04-10 | 1.0 | 初始版本:按文件出题功能设计 |

View File

@@ -4,9 +4,12 @@
| 接口 | 方法 | 功能 | 超时建议 | | 接口 | 方法 | 功能 | 超时建议 |
|------|------|------|----------| |------|------|------|----------|
| `/exam/generate` | POST | 生成题目 | 120秒 | | `/exam/generate` | POST | 生成题目(手动指定题型数量) | 120秒 |
| `/exam/generate-smart` | POST | 生成题目AI 自动分析文档结构出题) | 120秒 |
| `/exam/grade` | POST | 批阅答案 | 60秒 | | `/exam/grade` | POST | 批阅答案 | 60秒 |
> **2026-06-05 变更**`/exam/grade` 请求字段 `question_content` 已重命名为 `content`(破坏性变更)。详见 [出题批阅接口变更说明2026-06-05](出题批阅接口变更说明2026-06-05.md)。
--- ---
## 二、出题接口 ## 二、出题接口
@@ -175,7 +178,7 @@ Content-Type: application/json
|------|------|------|------|------| |------|------|------|------|------|
| `question_id` | string | ✅ | 题目ID | 后端数据库 | | `question_id` | string | ✅ | 题目ID | 后端数据库 |
| `question_type` | string | ✅ | 题型 | 后端数据库 | | `question_type` | string | ✅ | 题型 | 后端数据库 |
| `question_content` | object | ✅ | 题目内容(含正确答案) | 后端数据库 | | `content` | object | ✅ | 题目内容(含正确答案) | 后端数据库 |
| `student_answer` | any | ✅ | 学生答案 | 学生提交 | | `student_answer` | any | ✅ | 学生答案 | 学生提交 |
| `max_score` | number | ✅ | 满分 | 后端数据库 | | `max_score` | number | ✅ | 满分 | 后端数据库 |
@@ -188,7 +191,7 @@ Content-Type: application/json
{ {
"question_id": "q-001", "question_id": "q-001",
"question_type": "single_choice", "question_type": "single_choice",
"question_content": { "content": {
"stem": "根据公司规定,员工薪资由哪几部分组成?", "stem": "根据公司规定,员工薪资由哪几部分组成?",
"data": { "data": {
"options": [ "options": [
@@ -206,7 +209,7 @@ Content-Type: application/json
{ {
"question_id": "q-002", "question_id": "q-002",
"question_type": "multiple_choice", "question_type": "multiple_choice",
"question_content": { "content": {
"stem": "以下哪些属于绩效奖金的评定因素?", "stem": "以下哪些属于绩效奖金的评定因素?",
"data": { "data": {
"options": [ "options": [
@@ -224,7 +227,7 @@ Content-Type: application/json
{ {
"question_id": "q-003", "question_id": "q-003",
"question_type": "true_false", "question_type": "true_false",
"question_content": { "content": {
"stem": "公司规定员工每月绩效奖金上限为工资的20%。", "stem": "公司规定员工每月绩效奖金上限为工资的20%。",
"answer": "F" "answer": "F"
}, },
@@ -234,7 +237,7 @@ Content-Type: application/json
{ {
"question_id": "q-004", "question_id": "q-004",
"question_type": "fill_blank", "question_type": "fill_blank",
"question_content": { "content": {
"stem": "员工薪资由___、___和___三部分组成。", "stem": "员工薪资由___、___和___三部分组成。",
"data": {"blank_count": 3}, "data": {"blank_count": 3},
"answer": [["基本工资"], ["绩效奖金", "绩效"], ["津贴补贴", "补贴"]] "answer": [["基本工资"], ["绩效奖金", "绩效"], ["津贴补贴", "补贴"]]
@@ -245,7 +248,7 @@ Content-Type: application/json
{ {
"question_id": "q-005", "question_id": "q-005",
"question_type": "subjective", "question_type": "subjective",
"question_content": { "content": {
"stem": "请简述公司薪酬制度的核心原则。", "stem": "请简述公司薪酬制度的核心原则。",
"data": { "data": {
"scoring_points": [ "scoring_points": [
@@ -397,7 +400,7 @@ Content-Type: application/json
后端从数据库查询: 后端从数据库查询:
- question_id - question_id
- question_type - question_type
- question_content (含正确答案) - content (含正确答案,原 question_content 已弃用)
- score (满分) - score (满分)
@@ -434,7 +437,7 @@ Content-Type: application/json
|------|------|------| |------|------|------|
| question_id | 后端数据库 | 题目唯一标识 | | question_id | 后端数据库 | 题目唯一标识 |
| question_type | 后端数据库 | 题型 | | question_type | 后端数据库 | 题型 |
| question_content | 后端数据库 | 题目内容(含正确答案) | | content | 后端数据库 | 题目内容(含正确答案,原 question_content 已弃用 |
| student_answer | 学生提交 | 学生作答 | | student_answer | 学生提交 | 学生作答 |
| **max_score** | **后端数据库** | 满分(决定得分上限) | | **max_score** | **后端数据库** | 满分(决定得分上限) |

View File

@@ -42,6 +42,8 @@ doc_path = docstore_dir / f"{doc_id}.json" # doc_id = chunk_id = "filename_N"
### P1同名文件重复上传 — 旧切片残留 + 搜索结果重复 ### P1同名文件重复上传 — 旧切片残留 + 搜索结果重复
> **✅ 已修复**2026-06-04上传接口现在自动替换同名文件旧切片自动标记为 `superseded`。详见 [风险边界问题修复注意事项.md](风险边界问题修复注意事项.md)。
**位置**`api/document_routes.py` 第 246-250 行 **位置**`api/document_routes.py` 第 246-250 行
```python ```python
@@ -119,6 +121,8 @@ for item in all_items:
### P2文件无原地更新机制 ### P2文件无原地更新机制
> **✅ 已修复**2026-06-04上传接口新增自动替换机制`replaced=true`),同名文件自动替换旧版本。
**场景**:用户上传 `制度.pdf` v1 后发现内容有误,修改后想替换。当前系统没有 "更新文件" 接口,只能删除后重新上传。如果用户不知道要先删除,就会触发 P1 的重复问题。 **场景**:用户上传 `制度.pdf` v1 后发现内容有误,修改后想替换。当前系统没有 "更新文件" 接口,只能删除后重新上传。如果用户不知道要先删除,就会触发 P1 的重复问题。
**修复方向**upload 接口增加 "如果同名文件已存在则替换" 选项(先 delete_document 再 add_file_to_kb或提供独立的 "更新文档" API。 **修复方向**upload 接口增加 "如果同名文件已存在则替换" 选项(先 delete_document 再 add_file_to_kb或提供独立的 "更新文档" API。
@@ -151,14 +155,14 @@ chunk_index = int(str(chunk_id_raw).rsplit('_', 1)[-1])
### 风险总结 ### 风险总结
| 等级 | 风险 | 核心原因 | 触发条件 | | 等级 | 风险 | 核心原因 | 触发条件 | 状态 |
|------|------|----------|----------| |------|------|----------|----------|------|
| **P0** | RRF 融合吞结果 | 去重 key 缺少 collection | 多库有同名文件 | | **P0** | RRF 融合吞结果 | 去重 key 缺少 collection | 多库有同名文件 | 未修复 |
| **P0** | DocStore 覆盖 | 存储路径缺少 collection | 多库有同名含表格/图片文件 | | **P0** | DocStore 覆盖 | 存储路径缺少 collection | 多库有同名含表格/图片文件 | 未修复 |
| **P1** | 旧切片残留 | 重复上传只改名不替换 | 同名文件二次上传 | | **P1** | 旧切片残留 | 重复上传只改名不替换 | 同名文件二次上传 | ✅ 已修复 |
| **P1** | _collection 回退错误 | 硬编码 collections[0] | 单库路径 + 多 collection | | **P1** | _collection 回退错误 | 硬编码 collections[0] | 单库路径 + 多 collection | 未修复 |
| **P1** | search_multiple 去重 | 去重 key 缺少 collection | 直接调用低层 API | | **P1** | search_multiple 去重 | 去重 key 缺少 collection | 直接调用低层 API | 未修复 |
| **P2** | citation 字段名不一致 | `collection` vs `_collection` | 非标准查询路径 | | **P2** | citation 字段名不一致 | `collection` vs `_collection` | 非标准查询路径 | 未修复 |
| **P2** | 无文件更新机制 | 设计缺失 | 用户需要替换文档 | | **P2** | 无文件更新机制 | 设计缺失 | 用户需要替换文档 | ✅ 已修复 |
| **P2** | 元数据不同步 | JSON 文件可能损坏 | 手动操作或异常退出 | | **P2** | 元数据不同步 | JSON 文件可能损坏 | 手动操作或异常退出 | 未修复 |
| **P3** | 文件名含下划线 | 无问题rsplit 兼容) | — | | **P3** | 文件名含下划线 | 无问题rsplit 兼容) | — | 无需修复 |

View File

@@ -1,5 +1,7 @@
# 多源信息融合设计指南 # 多源信息融合设计指南
> **⚠️ 路径说明**:本文档描述的 `AgenticRAG.process()` 多源融合路径是**备用路径**(需启用网络搜索)。生产环境的 `/rag` 问答接口使用 `chat_routes.py` 的轻量编排路径(详见 [RAG数据流程.md](RAG数据流程.md)),不经过 `AgenticRAG.process()`。两条路径的区别见 [Agentic_RAG完整指南.md](Agentic_RAG完整指南.md)。
## 一、问题背景 ## 一、问题背景
当 Agentic RAG 同时使用知识库和网络搜索时,会遇到以下情况: 当 Agentic RAG 同时使用知识库和网络搜索时,会遇到以下情况:

View File

@@ -73,8 +73,8 @@
| 文档解析 | MinerU 3.0+ | PDF/DOCX/PPTX/图片统一解析 | | 文档解析 | MinerU 3.0+ | PDF/DOCX/PPTX/图片统一解析 |
| 向量检索 | ChromaDB + BGE-base-zh | 本地向量数据库 + 嵌入模型 | | 向量检索 | ChromaDB + BGE-base-zh | 本地向量数据库 + 嵌入模型 |
| 关键词检索 | BM25 + jieba | 中文分词 + 倒排索引 | | 关键词检索 | BM25 + jieba | 中文分词 + 倒排索引 |
| 重排序 | BGE-reranker-base | CrossEncoder 精排 | | 重排序 | qwen3-rerank云端/ BGE-reranker-base(本地,图片二次评分) | CrossEncoder 精排 |
| 大模型 | Qwen (通义千问) | 问答生成、查询改写、意图分析 | | 大模型 | deepseek-v4-flash / Qwen (通义千问) | 问答生成、查询改写、意图分析 |
| 数据库 | SQLite | 会话管理、知识管理 | | 数据库 | SQLite | 会话管理、知识管理 |
--- ---

View File

@@ -75,13 +75,14 @@
| 数据类型 | 存储位置 | 管理方 | 说明 | | 数据类型 | 存储位置 | 管理方 | 说明 |
|----------|----------|--------|------| |----------|----------|--------|------|
| 向量数据 | ChromaDB | RAG 组 | 文档 embedding | | 向量数据 | ChromaDB | RAG 组 | 文档 embedding(每个知识库独立实例) |
| 文档哈希 | SQLite | RAG 组 | 同步状态检测 | | 文档哈希 | SQLite (knowledge.db) | RAG 组 | 同步状态检测 |
| 原始文档 | 文件系统 | RAG 组 | documents/ 目录 | | 原始文档 | 文件系统 | RAG 组 | documents/ 目录 |
| 反馈记录 | SQLite (feedback.db) | RAG 组 | 用户反馈、黑名单 |
| 会话数据 | SQLite (session.db) | RAG 组 | 会话管理 |
| 出题数据 | SQLite (exam.db) | RAG 组 | 题目/批阅 |
| 用户账户 | MySQL/PG | 后端组 | 账号密码信息 | | 用户账户 | MySQL/PG | 后端组 | 账号密码信息 |
| 会话历史 | MySQL/PG | 后端组 | 对话记录 |
| 审计日志 | MySQL/PG | 后端组 | 操作日志 | | 审计日志 | MySQL/PG | 后端组 | 操作日志 |
| 反馈记录 | MySQL/PG | 后端组 | 用户反馈 |
| 题库数据 | MySQL/PG | 后端组 | 题目/试卷 | | 题库数据 | MySQL/PG | 后端组 | 题目/试卷 |
--- ---

View File

@@ -1,485 +0,0 @@
# 测试指南
> **文档类型**: 测试指南
> **创建日期**: 2026-04-05
> **最后更新**: 2026-06-04
> **文档总数**: 21个测试文档
---
## 一、测试文档清单
### 1.1 文档目录结构
```
documents/
├── public/ # 公开文档 - 所有人可见(包括未登录用户)
│ ├── 公司简介.txt
│ ├── 产品手册.pdf
│ ├── 产品手册.txt
│ ├── 员工手册.txt
│ ├── 组织架构.xlsx
│ ├── 组织架构说明.txt
│ └── 常见问题.txt
├── internal/ # 内部文档 - 登录用户可见
│ ├── 差旅管理办法.txt
│ ├── 请假制度.docx
│ ├── 信息安全管理制度.pdf
│ ├── 项目管理制度.xlsx
│ └── 会议纪要_2024Q1.txt
├── confidential/ # 机密文档 - 管理层及以上可见
│ ├── 财务报表_2024.pdf
│ ├── 薪酬制度.docx
│ ├── 合同台账.xlsx
│ ├── 战略规划.txt
│ └── 人员名册.txt
└── secret/ # 绝密文档 - 仅管理员可见
├── 董事会决议.pdf
├── 并购方案.docx
├── 股权结构.xlsx
└── 核心技术机密.txt
```
### 1.2 文档格式分布
| 格式 | 数量 | 测试目的 |
|------|------|---------|
| TXT | 10个 | 测试纯文本解析、编码识别 |
| PDF | 4个 | 测试PDF解析、表格提取、中文字体 |
| DOCX | 3个 | 测试Word解析、标题样式、表格处理 |
| XLSX | 4个 | 测试Excel解析、多工作表、单元格数据 |
### 1.3 权限级别
| 目录 | 权限级别 | 可见角色 |
|------|---------|---------|
| public | public | 所有人(含未登录用户) |
| internal | internal | user, manager, admin |
| confidential | confidential | manager, admin |
| secret | secret | admin only |
---
## 二、测试环境准备
### 2.1 环境检查
```bash
# 检查 Python 版本
python --version # 需要 Python 3.8+
# 检查依赖安装
pip list | grep -E "chromadb|sentence-transformers|flask|jieba"
# 检查模型文件
ls models/bge-base-zh-v1.5/
```
### 2.2 配置检查
确保 `config.py` 配置正确:
```python
# API配置
DASHSCOPE_API_KEY = "your-api-key"
DASHSCOPE_BASE_URL = "https://dashscope.aliyuncs.com/compatible-mode/v1"
DASHSCOPE_MODEL = "qwen3.6-flash" # 主 LLM文本生成 / RAG 对话)
INTENT_MODEL = "qwen-turbo" # 意图分析模型(轻量、确定性高)
```
> **注意**: Graph RAGNeo4j功能已废弃相关配置NEO4J_URI、USE_GRAPH_RAG 等)已移除。
---
## 三、测试执行流程
### 3.1 第一阶段:索引构建测试
#### 测试 1.1:向量索引构建
**测试步骤**
```bash
# 清除旧索引
rm -rf chroma_db/
# 重建向量索引
python scripts/rebuild_multi_kb.py
```
**预期结果**
- 控制台显示文档加载进度
- 显示各格式文档解析数量
- 显示向量构建进度
- 生成 `chroma_db/` 目录
**验证方法**
```python
import chromadb
client = chromadb.PersistentClient(path="./chroma_db")
collection = client.get_collection("knowledge_base")
print(f"向量数量: {collection.count()}")
```
#### 测试 1.2BM25 索引构建
**测试步骤**
```bash
# BM25索引会随向量索引一起构建
# 检查索引文件
ls -la bm25_index.pkl
```
**预期结果**
- 生成 `bm25_index.pkl` 文件
- 文件大小约 1-5 MB
---
### 3.2 第二阶段:权限控制测试
#### 测试 2.1:未登录用户权限
**测试步骤**
```bash
# 不带 Token 访问
curl -X POST http://localhost:5001/rag \
-H "Content-Type: application/json" \
-d '{"message": "公司的产品有哪些?"}'
```
**预期结果**
- 只返回 public 目录下的内容
- 不返回 internal、confidential、secret 内容
#### 测试 2.2user 角色权限
**测试步骤**
```bash
# 使用 mock token 登录
curl -X POST http://localhost:5001/rag \
-H "Content-Type: application/json" \
-H "Authorization: Bearer mock-token-testuser" \
-d '{"message": "差旅费标准是多少?"}'
```
**预期结果**
- 返回 public + internal 目录内容
- 不返回 confidential、secret 内容
#### 测试 2.3manager 角色权限
**测试步骤**
```bash
curl -X POST http://localhost:5001/rag \
-H "Authorization: Bearer mock-token-manager" \
-H "Content-Type: application/json" \
-d '{"message": "2024年财务报表显示净利润是多少"}'
```
**预期结果**
- 返回 public + internal + confidential 内容
- 正确回答财务相关问题
- 不返回 secret 目录内容
#### 测试 2.4admin 角色权限
**测试步骤**
```bash
curl -X POST http://localhost:5001/rag \
-H "Authorization: Bearer mock-token-admin" \
-H "Content-Type: application/json" \
-d '{"message": "董事会决议的并购方案是什么?"}'
```
**预期结果**
- 返回所有目录内容
- 正确回答涉及绝密信息的问题
---
### 3.3 第三阶段:检索质量测试
#### 测试 3.1:向量语义检索
| 测试问题 | 预期命中文档 | 预期答案关键点 |
|---------|------------|--------------|
| 公司有哪些产品? | 公司简介.txt、产品手册.pdf | 智能数据分析平台、AI知识图谱平台、RAG系统 |
| 请假需要提前几天申请? | 请假制度.docx | 1天以内直属上级、3天内部门负责人、7天以上总经理 |
| 年假有几天? | 请假制度.docx | 1年5天、5年7天、10年10天、20年15天 |
**测试命令**
```bash
curl -X POST http://localhost:5001/search \
-H "Content-Type: application/json" \
-H "Authorization: Bearer mock-token-admin" \
-d '{"query": "公司有哪些产品?", "top_k": 5}'
```
#### 测试 3.2BM25 关键词检索
| 测试关键词 | 预期命中文档 |
|-----------|------------|
| 差旅补助 500元 | 差旅管理办法.txt |
| 年假 15天 | 请假制度.docx |
| 薪酬 P5 35万 | 薪酬制度.docx |
#### 测试 3.3:混合检索 + Rerank
**测试步骤**
```bash
curl -X POST http://localhost:5001/search \
-H "Content-Type: application/json" \
-H "Authorization: Bearer mock-token-admin" \
-d '{"query": "出差住宿标准是多少?", "top_k": 10}'
```
**预期结果**
- 返回结果包含 rerank_score
- 结果排序比纯向量检索更准确
---
### 3.4 第四阶段Agentic RAG 测试
#### 测试 4.1:简单问题直接回答
**测试问题**
```bash
curl -X POST http://localhost:5001/rag \
-H "Authorization: Bearer mock-token-admin" \
-H "Content-Type: application/json" \
-d '{"message": "公司的请假制度是什么?"}'
```
**预期结果**
- Agent 决策为 "answer"
- 直接返回检索结果
- 无需多轮检索
#### 测试 4.2:查询改写
**测试问题**
```bash
curl -X POST http://localhost:5001/rag \
-H "Authorization: Bearer mock-token-admin" \
-H "Content-Type: application/json" \
-d '{"message": "我想了解关于报销的事情"}'
```
**预期结果**
- Agent 决策为 "rewrite"
- 查询被改写为更具体的表述
#### 测试 4.3:问题分解
**测试问题**
```bash
curl -X POST http://localhost:5001/rag \
-H "Authorization: Bearer mock-token-admin" \
-H "Content-Type: application/json" \
-d '{"message": "请假和报销的流程分别是什么?"}'
```
**预期结果**
- Agent 决策为 "decompose"
- 问题被分解为多个子问题
- 分别检索后合并回答
#### 测试 4.4:多源融合
**测试问题**
```bash
curl -X POST http://localhost:5001/rag \
-H "Authorization: Bearer mock-token-admin" \
-H "Content-Type: application/json" \
-d '{"message": "技术部的组织架构和职责分工是怎样的?"}'
```
**预期结果**
- 同时触发向量检索和 BM25 关键词检索
- 返回结果经过 Rerank 重排序并标注来源
---
### 3.5 第五阶段:出题系统测试
#### 测试 5.1:试卷生成
**测试步骤**
```bash
curl -X POST http://localhost:5001/exam/generate \
-H "Authorization: Bearer mock-token-admin" \
-H "Content-Type: application/json" \
-d '{"topic": "公司制度基础", "choice_count": 5, "name": "公司制度测试"}'
```
**预期结果**
- 返回 exam_id
- 试卷状态为 "draft"
- 包含选择题、填空题、简答题
#### 测试 5.2:试卷审核
**测试步骤**
```bash
curl -X POST http://localhost:5001/exam/<exam_id>/review \
-H "Authorization: Bearer mock-token-admin" \
-H "Content-Type: application/json" \
-d '{"action": "approve"}'
```
**预期结果**
- 返回 success: true
- 试卷状态变为 "approved"
#### 测试 5.3:试卷批阅
**测试步骤**
```bash
curl -X POST http://localhost:5001/exam/<exam_id>/grade \
-H "Authorization: Bearer mock-token-admin" \
-H "Content-Type: application/json" \
-d '{"student_name": "测试学生", "answers": {"choice_1": "A", "choice_2": "B"}}'
```
**预期结果**
- 返回批阅报告
- 包含每题得分和总分
---
### 3.6 第六阶段API 接口测试
#### 测试 6.1:认证接口
```bash
# 获取用户信息
curl http://localhost:5001/auth/me \
-H "Authorization: Bearer mock-token-admin"
```
#### 测试 6.2:会话管理
```bash
# 创建会话并对话
curl -X POST http://localhost:5001/chat \
-H "Authorization: Bearer mock-token-admin" \
-H "Content-Type: application/json" \
-d '{"message": "你好", "session_id": "test-001"}'
# 获取会话列表
curl http://localhost:5001/sessions \
-H "Authorization: Bearer mock-token-admin"
# 获取会话历史
curl http://localhost:5001/history/test-001 \
-H "Authorization: Bearer mock-token-admin"
```
#### 测试 6.3:健康检查
```bash
curl http://localhost:5001/health
```
**预期结果**
```json
{
"status": "ok",
"knowledge_base": "多向量库模式 (按集合提供服务)",
"bm25_index": "动态按需加载",
"mode": "Agentic RAG"
}
```
---
## 四、测试报告模板
### 4.1 测试执行摘要
| 项目 | 内容 |
|------|------|
| 测试日期 | YYYY-MM-DD |
| 测试人员 | |
| 测试环境 | |
| 文档数量 | 21个 |
| 发现问题数量 | |
### 4.2 测试结果统计
| 测试类型 | 用例数 | 通过 | 失败 | 阻塞 |
|----------|--------|------|------|------|
| 索引构建测试 | 2 | | | |
| 权限控制测试 | 4 | | | |
| 检索质量测试 | 3 | | | |
| Agentic RAG测试 | 4 | | | |
| 出题系统测试 | 3 | | | |
| API接口测试 | 3 | | | |
| **总计** | **19** | | | |
### 4.3 问题列表
| 编号 | 测试用例 | 问题描述 | 严重程度 | 状态 |
|------|----------|----------|----------|------|
| BUG-001 | | | 高/中/低 | 待修复 |
---
## 五、快速测试命令
```bash
# 一键索引重建
python scripts/rebuild_multi_kb.py
# 启动服务
python main.py
# 快速测试
curl http://localhost:5001/health
```
---
## 六、测试文档生成
测试文档通过脚本 `generate_test_docs.py` 自动生成,支持以下格式:
- **TXT**: 直接文本写入
- **DOCX**: 使用 `python-docx` 库生成
- **XLSX**: 使用 `openpyxl` 库生成
- **PDF**: 使用 `reportlab` 库生成
运行命令:
```bash
python generate_test_docs.py
```
依赖安装:
```bash
pip install python-docx openpyxl reportlab
```
---
## 七、文本量统计
| 目录 | 文件数 | 总字符数 | 总词数(估计) |
|------|--------|---------|-------------|
| public | 7 | ~35,000 | ~15,000 |
| internal | 5 | ~25,000 | ~10,000 |
| confidential | 5 | ~20,000 | ~8,000 |
| secret | 4 | ~15,000 | ~6,000 |
| **合计** | **21** | **~95,000** | **~39,000** |
---
## 八、变更记录
| 日期 | 版本 | 变更内容 |
|------|------|---------|
| 2026-06-04 | 3.0 | 移除 Graph RAGNeo4j相关内容更新模型配置和章节编号 |
| 2026-04-13 | 2.0 | 合并测试文档清单和测试执行流程 |
| 2026-04-05 | 1.0 | 初始版本 |

View File

@@ -1,415 +0,0 @@
# 企业文档版本管理方案实施完成报告
## ✅ 实施完成
所有计划的功能已成功实现,代码已提交。
---
## 📊 实施总结
### 已完成的 Phase
| Phase | 任务 | 状态 | 说明 |
|-------|------|------|------|
| Phase 1 | 清理冗余代码 | ✅ 完成 | 删除 diff.py简化 lifecycle.py |
| Phase 2 | 增强 sync.py | ✅ 完成 | 添加版本管理逻辑 |
| Phase 3 | 增强 manager.py | ✅ 完成 | 添加状态标记和查询过滤 |
| Phase 4 | 添加 API 端点 | ✅ 完成 | 废止/恢复/版本历史 API |
| Phase 5 | 验证数据库 | ✅ 完成 | 添加性能优化索引 |
| Phase 6 | 创建清理机制 | ✅ 完成 | 自动清理旧版本 |
---
## 📝 代码变更清单
### 删除的文件2个
1. **knowledge/diff.py** (525行)
- 原因:完全未使用
- 影响:无
2. **knowledge/lifecycle.py** (618行)
- 原因:大部分功能未使用
- 替代knowledge/document_versions.py
### 新增的文件2个
1. **knowledge/document_versions.py** (300行)
- 功能:文档版本查询(简化版)
- 保留get_document_history, get_active_version, create_version_record, log_version_change
2. **knowledge/cleanup.py** (250行)
- 功能:自动清理 superseded/deprecated 版本
- 可选:定时任务调度
### 修改的文件4个
1. **knowledge/sync.py**
- 修改process_change 方法
- 新增_get_current_version, _generate_version_id, _record_version_change
- 功能:文档更新时标记旧版本为 superseded生成新版本号
2. **knowledge/manager.py**
- 新增mark_document_as_superseded 方法
- 修改search_single 方法(添加 include_deprecated 参数)
- 功能:状态标记和查询过滤
3. **api/kb_routes.py**
- 新增3个 API 端点
- POST /collections/<kb_name>/documents/<filename>/deprecate
- POST /collections/<kb_name>/documents/<filename>/restore
- GET /collections/<kb_name>/documents/<filename>/versions
4. **data/db.py**
- 新增2个性能优化索引
- idx_document_versions_status
- idx_version_change_logs_document
---
## 🎯 功能实现
### 1. 文档更新(版本管理)
**流程**
```
文档修改 → 检测变更 → 标记旧版本为 superseded → 添加新版本 → 记录变更日志
```
**代码位置**
- `knowledge/sync.py` - process_change 方法
**效果**
- 旧版本status = "superseded",查询时被过滤
- 新版本status = "active",查询时返回
- 版本号自动递增v1 → v2 → v3
### 2. 文档废止(软删除)
**API**
```bash
POST /api/kb/collections/public_kb/documents/报销制度.pdf/deprecate
{
"reason": "制度已废止"
}
```
**代码位置**
- `knowledge/manager.py` - deprecate_document 方法
- `api/kb_routes.py` - deprecate_document 端点
**效果**
- 文档状态status = "deprecated"
- 查询行为:不返回该文档
- 可恢复:调用 restore API
### 3. 文档恢复
**API**
```bash
POST /api/kb/collections/public_kb/documents/报销制度.pdf/restore
```
**代码位置**
- `knowledge/manager.py` - restore_document 方法
- `api/kb_routes.py` - restore_document 端点
**效果**
- 文档状态status = "active"
- 查询行为:恢复返回
### 4. 版本历史查询
**API**
```bash
GET /api/kb/collections/public_kb/documents/报销制度.pdf/versions?limit=10
```
**代码位置**
- `knowledge/document_versions.py` - get_document_history 方法
- `api/kb_routes.py` - get_document_versions 端点
**返回**
```json
{
"success": true,
"versions": [
{
"version": "v2",
"status": "active",
"created_at": "2024-01-15T10:00:00",
"chunk_count": 20
},
{
"version": "v1",
"status": "superseded",
"created_at": "2023-01-01T10:00:00",
"chunk_count": 15
}
]
}
```
### 5. 查询过滤
**代码位置**
- `knowledge/manager.py` - search_single 方法
**默认行为**
```python
# 只返回 active 状态的文档
result = search_single(kb_name, query_vector, query_text, include_deprecated=False)
```
**包含废止文档**
```python
# 返回所有状态的文档(包括 deprecated 和 superseded
result = search_single(kb_name, query_vector, query_text, include_deprecated=True)
```
### 6. 自动清理
**代码位置**
- `knowledge/cleanup.py`
**使用方式**
```python
from knowledge.cleanup import cleanup_superseded_versions
# 清理超过 7 天的 superseded 版本
cleaned = cleanup_superseded_versions(days_to_keep=7)
```
**定时任务**(可选):
```python
from knowledge.cleanup import start_cleanup_scheduler
# 每天凌晨 3 点自动清理
start_cleanup_scheduler(
superseded_days=7,
deprecated_days=30,
schedule_time="03:00"
)
```
---
## 📈 代码统计
### 代码量变化
| 指标 | 变化 |
|------|------|
| 删除代码 | -1143 行diff.py + lifecycle.py |
| 新增代码 | +550 行document_versions.py + cleanup.py + 修改) |
| **净减少** | **-593 行** |
### 功能完整性
| 功能 | 状态 |
|------|------|
| 文档版本管理 | ✅ 已实现 |
| 软删除和恢复 | ✅ 已实现 |
| 历史追溯 | ✅ 已实现 |
| 查询自动过滤 | ✅ 已实现 |
| 自动清理 | ✅ 已实现(可选) |
---
## 🧪 测试建议
### 1. 单元测试
创建 `tests/test_version_management.py`
```python
def test_document_update_creates_version():
"""测试文档更新时创建新版本"""
# 1. 上传文档 v1
# 2. 修改文档上传 v2
# 3. 验证 v1 状态为 superseded
# 4. 验证 v2 状态为 active
# 5. 查询只返回 v2
def test_deprecate_and_restore():
"""测试废止和恢复"""
# 1. 废止文档
# 2. 验证查询不返回该文档
# 3. 恢复文档
# 4. 验证查询返回该文档
def test_version_history():
"""测试版本历史查询"""
# 1. 创建多个版本
# 2. 查询版本历史
# 3. 验证返回所有版本记录
```
### 2. 集成测试
```bash
# 1. 上传文档
curl -X POST http://localhost:5001/api/kb/public/upload \
-F "file=@报销制度_v1.pdf"
# 2. 查询文档(应返回 v1
curl http://localhost:5001/api/rag \
-d '{"query": "报销流程", "kb_name": "public"}'
# 3. 上传新版本
curl -X POST http://localhost:5001/api/kb/public/upload \
-F "file=@报销制度_v2.pdf"
# 4. 查询文档(应只返回 v2
curl http://localhost:5001/api/rag \
-d '{"query": "报销流程", "kb_name": "public"}'
# 5. 查询版本历史
curl http://localhost:5001/api/kb/collections/public_kb/documents/报销制度.pdf/versions
# 6. 废止文档
curl -X POST http://localhost:5001/api/kb/collections/public_kb/documents/报销制度.pdf/deprecate \
-d '{"reason": "制度已废止"}'
# 7. 查询文档(应不返回)
curl http://localhost:5001/api/rag \
-d '{"query": "报销流程", "kb_name": "public"}'
# 8. 恢复文档
curl -X POST http://localhost:5001/api/kb/collections/public_kb/documents/报销制度.pdf/restore
# 9. 查询文档(应返回)
curl http://localhost:5001/api/rag \
-d '{"query": "报销流程", "kb_name": "public"}'
```
### 3. 性能测试
```python
import time
# 测试查询性能(带状态过滤)
start = time.time()
result = kb_manager.search_single(kb_name, query_vector, query_text)
print(f"无过滤: {time.time() - start:.3f}s")
start = time.time()
result = kb_manager.search_single(kb_name, query_vector, query_text, include_deprecated=False)
print(f"带过滤: {time.time() - start:.3f}s")
# 预期:性能差异 < 10%
```
---
## ⚠️ 注意事项
### 1. 数据库迁移
如果数据库已存在,需要运行索引创建:
```python
from data.db import get_connection
with get_connection("knowledge") as conn:
conn.execute('''
CREATE INDEX IF NOT EXISTS idx_document_versions_status
ON document_versions(document_id, collection, status)
''')
conn.execute('''
CREATE INDEX IF NOT EXISTS idx_version_change_logs_document
ON version_change_logs(document_id, collection, created_at DESC)
''')
conn.commit()
```
### 2. 现有文档处理
现有文档的 metadata 中可能没有 `status` 字段,需要批量更新:
```python
from knowledge.manager import get_kb_manager
kb_manager = get_kb_manager()
kb_names = kb_manager.list_collections()
for kb_name in kb_names:
collection = kb_manager.get_collection(kb_name)
result = collection.get()
# 更新所有没有 status 字段的 chunks
updated_metadatas = []
ids_to_update = []
for i, meta in enumerate(result['metadatas']):
if 'status' not in meta:
meta['status'] = 'active'
meta['version'] = 'v1'
updated_metadatas.append(meta)
ids_to_update.append(result['ids'][i])
if ids_to_update:
collection.update(ids=ids_to_update, metadatas=updated_metadatas)
print(f"更新 {kb_name}: {len(ids_to_update)} chunks")
```
### 3. 清理策略
建议的清理策略:
- **superseded 版本**:保留 7 天(防止误操作)
- **deprecated 版本**:保留 30 天(审计需求)
- **定时执行**:每天凌晨 3 点(低峰期)
---
## 📚 相关文档
- [企业文档更新管理方案](docs/企业文档更新管理方案.md)
---
## 🎉 总结
### 实施成果
1.**代码质量提升**
- 删除 ~1000 行冗余代码
- 降低维护成本 60%
- 提高代码可读性
2.**功能完整性**
- 支持文档版本管理
- 支持软删除和恢复
- 支持历史追溯
- 查询自动过滤废止文档
3.**性能影响**
- 查询性能:无明显影响(< 5%
- 存储成本短期略增1.2x),长期持平(自动清理)
- 更新性能:略有提升(标记 vs 删除+重建)
### 下一步
1. **测试验证**
- 运行单元测试
- 执行集成测试
- 验证性能影响
2. **部署准备**
- 更新现有文档的 metadata
- 创建数据库索引
- 配置清理任务(可选)
3. **文档更新**
- 更新 API 文档
- 更新用户手册
- 更新部署指南
---
**实施完成时间**: 2026-04-20
**实施者**: Claude Code
**代码审查**: 已完成
**测试状态**: 已实施
**部署状态**: 已部署

View File

@@ -1,219 +0,0 @@
# 状态码功能更新说明
> **更新日期**: 2026-04-30
> **改动范围**: 长时间运行端口的响应格式增强
---
## 一、更新概述
为方便后端判断 RAG 服务处理状态,以下端口增加了统一的 `status_code` 字段:
| 端口 | 改动文件 |
|------|----------|
| `/documents/upload` | `api/document_routes.py` |
| `/documents/batch-upload` | `api/document_routes.py` |
| `/sync` | `api/sync_routes.py` |
| `/exam/generate` | `exam_pkg/api.py` |
| `/exam/grade` | `exam_pkg/api.py` |
---
## 二、状态码速查表
### 处理中 (10xx)
| 状态码 | 常量名 | 说明 |
|--------|--------|------|
| 1000 | PROCESSING | 通用处理中 |
| 1010 | SYNC_RUNNING | 同步进行中 |
| 1020 | EXAM_GENERATING | 出题生成中 |
| 1021 | EXAM_GRADING | 批阅进行中 |
### 成功 (20xx)
| 状态码 | 常量名 | 说明 |
|--------|--------|------|
| 2000 | SUCCESS | 通用成功 |
| 2002 | UPLOAD_SUCCESS | 文件上传成功 |
| 2003 | BATCH_UPLOAD_SUCCESS | 批量上传成功 |
| 2010 | SYNC_SUCCESS | 同步完成 |
| 2020 | EXAM_SUCCESS | 出题成功 |
| 2021 | GRADE_SUCCESS | 批阅完成 |
### 客户端错误 (40xx)
| 状态码 | 常量名 | 说明 |
|--------|--------|------|
| 4000 | BAD_REQUEST | 请求参数错误 |
| 4004 | NO_FILE | 没有上传文件 |
| 4005 | NO_FILE_SELECTED | 没有选择文件 |
| 4006 | NO_COLLECTION | 未指定向量库 |
| 4007 | UNSUPPORTED_FORMAT | 不支持的文件格式 |
| 4008 | FILE_TOO_LARGE | 文件过大 |
### 服务端错误 (50xx)
| 状态码 | 常量名 | 说明 |
|--------|--------|------|
| 5000 | INTERNAL_ERROR | 内部错误 |
| 5010 | SYNC_ERROR | 同步失败 |
| 5020 | EXAM_ERROR | 出题失败 |
| 5021 | GRADE_ERROR | 批阅失败 |
---
## 三、各端口改动对照
### 3.1 文档上传 `/documents/upload`
| 场景 | 旧响应 | 新响应 |
|------|--------|--------|
| 成功 | `{"success": true, ...}` | `{"status": "success", "status_code": 2002, ...}` |
| 无文件 | `{"error": "没有上传文件"}` | `{"status": "failed", "status_code": 4004, ...}` |
| 格式错误 | `{"error": "不支持的文件类型"}` | `{"status": "failed", "status_code": 4007, ...}` |
| 文件过大 | `{"error": "文件大小超过限制"}` | `{"status": "failed", "status_code": 4008, ...}` |
### 3.2 批量上传 `/documents/batch-upload`
| 场景 | 旧响应 | 新响应 |
|------|--------|--------|
| 成功 | `{"success": true, ...}` | `{"status": "success", "status_code": 2003, ...}` |
### 3.3 同步服务 `/sync`
| 场景 | 旧响应 | 新响应 |
|------|--------|--------|
| 成功 | `{"success": true, ...}` | `{"status": "success", "status_code": 2010, ...}` |
| 服务不可用 | `{"error": "同步服务未启用"}` | `{"status": "failed", "status_code": 5000, ...}` |
| 同步失败 | `{"error": "xxx"}` | `{"status": "failed", "status_code": 5010, ...}` |
### 3.4 出题 `/exam/generate`
| 场景 | 旧响应 | 新响应 |
|------|--------|--------|
| 成功 | `{"success": true, ...}` | `{"status": "success", "status_code": 2020, ...}` |
| 参数缺失 | `{"error": "缺少参数"}` | `{"status": "failed", "status_code": 4000, ...}` |
| 出题失败 | `{"error": "xxx"}` | `{"status": "failed", "status_code": 5020, ...}` |
### 3.5 批阅 `/exam/grade`
| 场景 | 旧响应 | 新响应 |
|------|--------|--------|
| 成功 | `{"success": true, ...}` | `{"status": "success", "status_code": 2021, ...}` |
| 批阅失败 | `{"error": "xxx"}` | `{"status": "failed", "status_code": 5021, ...}` |
---
## 四、响应格式示例
### 成功响应
```json
{
"status": "success",
"status_code": 2002,
"message": "文件上传成功",
"success": true,
"data": {
"file": {
"filename": "document.pdf",
"collection": "public_kb",
"path": "public_kb/document.pdf",
"size": 102400
}
}
}
```
### 错误响应
```json
{
"status": "failed",
"status_code": 4007,
"message": "不支持的文件格式: .exe支持: pdf, docx, doc, xlsx, txt",
"success": false,
"error": {
"error": "UNSUPPORTED_FORMAT",
"error_code": 4007,
"details": {}
}
}
```
---
## 五、后端对接代码示例
### Python 示例
```python
def handle_rag_response(response):
"""处理 RAG 服务响应"""
data = response.json()
status_code = data.get("status_code", 0)
# 判断处理状态
if 2000 <= status_code < 3000:
# 成功
return data.get("data", data)
elif 4000 <= status_code < 5000:
# 客户端错误
raise ClientError(data.get("message"), status_code)
elif 5000 <= status_code < 6000:
# 服务端错误
raise ServerError(data.get("message"), status_code)
# 兼容旧格式
if data.get("success"):
return data
raise Exception(data.get("error", "未知错误"))
```
### JavaScript 示例
```javascript
function handleRagResponse(data) {
const statusCode = data.status_code || 0;
if (statusCode >= 2000 && statusCode < 3000) {
// 成功
return data.data || data;
} else if (statusCode >= 4000 && statusCode < 5000) {
// 客户端错误
throw new ClientError(data.message, statusCode);
} else if (statusCode >= 5000 && statusCode < 6000) {
// 服务端错误
throw new ServerError(data.message, statusCode);
}
// 兼容旧格式
if (data.success) return data;
throw new Error(data.error || "未知错误");
}
```
---
## 六、向后兼容说明
所有响应保留了 `success` 字段:
- 成功时 `success: true`
- 失败时 `success: false`
旧代码仍可通过 `success` 字段判断,无需立即修改。
---
## 七、文件变更清单
| 文件 | 变更类型 |
|------|----------|
| `core/status_codes.py` | 新增 |
| `api/response_utils.py` | 新增 |
| `api/document_routes.py` | 修改 |
| `api/sync_routes.py` | 修改 |
| `exam_pkg/api.py` | 修改 |

View File

@@ -1,367 +0,0 @@
## 生产 RAG 路径优化计划
> 原则:小步快跑,每阶段独立可测,打完 commit 验证质量后再推进下一阶段。
> 如果某阶段导致回答质量下降,直接 `git revert` 回退到上一个 commit。
---
### 实施进度总览(更新于 2026-06
| 阶段 | 内容 | 状态 | 说明 |
|------|------|------|------|
| Phase 0 | 基线建立 + 评估基础设施 | **部分实施** | `scripts/eval_e2e.py` 已创建,但 `data/eval_dataset.json``baseline.json` 尚未生成 |
| Phase 1 | Rerank 分数传递与上下文过滤 | **已实施** | `RERANK_CONTEXT_MIN_SCORE` 已配置,`_order_text_contexts_for_prompt` 已接受 `min_score` 参数 |
| Phase 2 | Token 预算控制 | **已实施** | `CONTEXT_MAX_CHARS=8000` / `CONTEXT_SOFT_LIMIT=6000` 已配置,`_build_context_with_budget()` 已实现 |
| Phase 3 | 上下文扩展精细化 | **已实施** | `EXPANSION_SCORE_THRESHOLD=0.3` / `MAX_EXPANDED_NEIGHBORS=4` 已配置,`_expanded_from_score` 元数据已记录 |
| Phase 4 | 置信度兜底 | **已实施** | `CONFIDENCE_WARN_THRESHOLD=0.15` / `CONFIDENCE_CAUTION_THRESHOLD=0.30` 已配置,`confidence_score` 已通过 SSE 输出 |
| Phase 5 | 上下文排序优化 | **部分实施** | `_build_context_with_budget()` 实现了通用排序逻辑,但列举类查询仍走独立的 `_is_enum_query` 分支 |
| Phase 6 | 引用标注改进 | **已实施** | 已迁移到 `core/agentic_citation.py`,支持动态阈值(短段落 0.55 / 长段落 0.45)和每段最多 2 引用 |
> **注**:下文各 Phase 中引用的具体函数名、行号为文档撰写时的快照,经多次迭代后已偏移。实际位置请以当前代码为准。
---
### Phase 0基线建立 + 评估基础设施 `部分实施`
**目标**:在改动任何代码之前,先有量化基线和自动化评估手段。
**现状**`scripts/eval_e2e.py` 已创建但尚未运行基线评估。`data/eval_dataset.json``data/eval_results/baseline.json` 尚未生成。
**工作内容**
1. 创建 `data/eval_dataset.json`:从现有知识库中挑选 15-20 个测试问题,覆盖以下类型:
- 事实查询("XX标准是多少"
- 列举查询("有哪些禁止情形"
- 对比查询("A和B的区别"
- 流程查询("如何申请XX"
- 跨文档查询(答案涉及多个文件)
每个问题记录:`query``query_type``expected_keywords`(答案应包含的关键信息点)、`relevant_sources`(应命中的文件名)
2. 创建 `scripts/eval_e2e.py`:端到端评估脚本
- 启动本地服务(或连接已运行的服务)
- 对每个测试问题调用 `POST /rag`
- 收集回答,用 LLM 评分(相关性 1-5、完整性 1-5、准确性 1-5
- 同时记录检索层指标Rerank 分数分布、上下文切片数、上下文总字数)
- 输出 JSON 报告 + 终端汇总
3. 运行一次基线评估,保存为 `data/eval_results/baseline.json`
4. 打 Git tag`git tag v1.0-rag-baseline`
**产出文件**
- `data/eval_dataset.json`
- `scripts/eval_e2e.py`
- `data/eval_results/baseline.json`
**验收标准**:评估脚本能正常运行,基线报告包含每个问题的评分。
---
### Phase 1Rerank 分数传递与上下文过滤 `已实施`
**目标**:让 Rerank 分数在上下文构建中发挥作用,过滤低分切片。
**改动范围**`api/chat_routes.py` + `config.py`
**实施说明**
- `config.py` 已新增 `RERANK_CONTEXT_MIN_SCORE = 0.05`
- `_order_text_contexts_for_prompt()` 已接受 `min_score` 参数(当前位于 `api/chat_routes.py` 第 188 行)
- 调用处已传入 `min_score=RERANK_CONTEXT_MIN_SCORE`(当前位于第 1750-1751 行)
- SSE `context_built` 事件已包含 `min_score_filter``score_stats``confidence_top3` 等调试字段
**原始设计**
1.`contexts.append` 时已有 score 字段,确认它包含 Rerank 分数(当前 `scores` 来自 `search_result.get('scores')`,是 RRF 融合分数还是 Rerank 分数需要确认)
2.`_order_text_contexts_for_prompt` 函数中增加分数过滤参数:
```python
def _order_text_contexts_for_prompt(contexts, query, max_chunks,
min_score=0.0):
# 过滤低于分数阈值的切片
if min_score > 0:
text_contexts = [c for c in text_contexts
if c.get('score', 0) >= min_score]
```
3. 在 `generate()` 中调用时传入阈值:
```python
text_contexts = _order_text_contexts_for_prompt(
contexts, message, MAX_CONTEXT_CHUNKS,
min_score=RERANK_CONTEXT_MIN_SCORE # 新增配置项,初始值 0.05
)
```
4. 在 `config.py` 中新增配置:
```python
RERANK_CONTEXT_MIN_SCORE = 0.05 # 上下文最低 Rerank 分数阈值
```
5. 在 SSE `context_built` 事件中增加分数信息DEV 模式),方便调试。
**风险**:低。`min_score` 初始设 0.05(几乎不过滤),逐步调高观察效果。
**验证方法**
- 运行 `eval_e2e.py`,对比基线
- 重点关注:是否有问题因为过滤了切片而回答变差
- 观察 `context_built` 事件中 `chunk_count` 的变化
**Git commit**`feat(rag): pass rerank scores through to context building with min-score filter`
---
### Phase 2Token 预算控制 `已实施`
**目标**:用字数/token 预算替代纯计数截断,避免上下文过长稀释 LLM 注意力。
**改动范围**`api/chat_routes.py` + `config.py`。
**实施说明**
- `config.py` 已新增 `CONTEXT_MAX_CHARS = 8000`、`CONTEXT_SOFT_LIMIT = 6000`、`DIRECT_CONTEXT_MAX_CHARS = 2000`
- `api/chat_routes.py` 已新增 `_build_context_with_budget()` 函数(当前第 312 行),按 Rerank 分数降序逐个加入直到预算满
- 列举类 / 对比类查询走独立分支,保持原始顺序直接拼接(当前第 1754-1758 行)
**原始设计**
1. 在 `config.py` 新增:
```python
CONTEXT_MAX_CHARS = 8000 # 上下文最大字符数(约 4000 token
CONTEXT_SOFT_LIMIT = 6000 # 软限制,超过后只保留高分切片
```
2. 在 `_order_text_contexts_for_prompt` 返回后,构建 `context_text` 时:
```python
# 按 Rerank 分数降序逐个加入,直到达到 token 预算
sorted_contexts = sorted(text_contexts,
key=lambda c: c.get('score', 0),
reverse=True)
context_parts = []
total_chars = 0
for ctx in sorted_contexts:
doc = ctx.get('doc', '')
if total_chars + len(doc) > CONTEXT_MAX_CHARS:
break
context_parts.append(doc)
total_chars += len(doc)
context_text = "\n\n".join(context_parts)
```
3. 注意:排序后需要保持同一文档切片的连续性。改为先按 (source, section, chunk_index) 分组,再按组内最高分降序排列各组,逐组加入直到预算满。
**风险**:中。如果预算设太小,可能丢掉关键信息。初始 `CONTEXT_MAX_CHARS=8000` 比较保守(当前 20 个切片平均约 10000-15000 字)。
**验证方法**
- 对比基线的评分变化
- 关注列举类查询("有哪些禁止情形")是否因为预算截断而漏条
- 观察 `context_built.context_length` 分布
**Git commit**`feat(rag): add character-based token budget for context building`
---
### Phase 3上下文扩展精细化 `已实施`
**目标**Rerank 后的上下文扩展只对高置信度切片执行,避免低分切片引入噪声邻居。
**改动范围**`core/engine.py` 的 `_expand_contiguous_chunks`(当前第 1089 行)及其调用处。
**实施说明**
- `config.py` / `engine.py` 已新增 `EXPANSION_SCORE_THRESHOLD = 0.3`、`MAX_EXPANDED_NEIGHBORS = 4`
- 扩展时传入 `min_score=EXPANSION_SCORE_THRESHOLD` 过滤低分切片
- 邻居切片 metadata 已记录 `_expanded_from_score`(种子分数)
- `MAX_EXPANDED_NEIGHBORS` 限制每个种子的邻居数量
- 上下文扩展同时在 `chat_routes.py`(第 281 行)和 `engine.py` 中实现,参数由 `CONTEXT_EXPANSION_ENABLED/BEFORE/AFTER/MAX_CHUNKS` 控制
**原始设计**(行号为撰写时快照,已偏移):
1. ~~MMR 前的扩展(第 573 行)保持不变~~ —— 已重构,实际位置见 `core/engine.py`
2. ~~Rerank 后的扩展(第 615 行)增加条件~~ —— 已实施,`EXPANSION_SCORE_THRESHOLD` 控制
```python
# 只对 Rerank 分数 > EXPANSION_SCORE_THRESHOLD 的切片扩展邻居
EXPANSION_SCORE_THRESHOLD = 0.3
```
3. 给扩展进来的邻居切片在 metadata 中记录扩展来源:
```python
n_meta['_expanded_from_score'] = seed_score # 种子切片的 Rerank 分数
```
4. 在 `_order_text_contexts_for_prompt` 中限制扩展邻居的数量:
```python
MAX_EXPANDED_NEIGHBORS = 4 # 最多 4 个扩展邻居进入上下文
```
**风险**:中。如果阈值设太高,一些中等分数的切片不会扩展邻居,可能丢失上下文。初始 0.3 比较宽松。
**验证方法**
- 对比 `retrieval_debug` 事件中第二次 `context_expansion` 的 before/after 数量
- 检查是否有回答因为缺少邻居切片而变得不连贯
- 评估分数不应低于 Phase 2 的结果
**Git commit**`feat(rag): restrict post-rerank context expansion to high-confidence chunks`
---
### Phase 4置信度兜底 `已实施`
**目标**:当检索质量整体偏低时,在 prompt 中告知 LLM 谨慎回答,减少幻觉。
**改动范围**`api/chat_routes.py`prompt 层面 + SSE 输出)。
**实施说明**
- `config.py` 已新增 `CONFIDENCE_WARN_THRESHOLD = 0.15`、`CONFIDENCE_CAUTION_THRESHOLD = 0.30`
- `generate()` 内已计算 `_confidence_score`top-3 平均 Rerank 分数,当前第 1760-1762 行)
- 根据分数在 prompt 中注入不同的置信度提示(当前第 1821-1827 行)
- SSE `finish` 事件已附带 `confidence_score` 字段(当前第 1926 行)
**原始设计**
1. 在上下文构建完成后(`context_text` 已生成),检查 top-3 切片的平均 Rerank 分数:
```python
top_scores = [ctx.get('score', 0) for ctx in text_contexts[:3]]
avg_top3_score = sum(top_scores) / len(top_scores) if top_scores else 0
```
2. 根据平均分数在 prompt 中注入不同指令:
```python
if avg_top3_score < CONFIDENCE_WARN_THRESHOLD: # 比如 0.15
confidence_note = (
"【重要提示】参考资料与问题的相关性较低。"
"请仅基于参考资料中明确包含的信息回答,"
"如果资料不足以回答问题,请直接说明'知识库中未找到直接相关的信息'。"
)
elif avg_top3_score < CONFIDENCE_CAUTION_THRESHOLD: # 比如 0.3
confidence_note = (
"参考资料的相关性一般,请优先引用资料中的原文,避免推测。"
)
else:
confidence_note = ""
```
3. 在 `config.py` 新增:
```python
CONFIDENCE_WARN_THRESHOLD = 0.15
CONFIDENCE_CAUTION_THRESHOLD = 0.30
```
4. 在 SSE `finish` 事件中附带 `confidence_score` 字段,前端可用于提示用户。
**风险**:低。纯 prompt 层面的改动,不影响检索链路。最坏情况是 LLM 过于保守拒绝回答——可以通过调低阈值解决。
**验证方法**
- 构造几个"知识库中确实没有答案"的问题,检查 LLM 是否正确拒绝
- 正常问题的评分不应下降
- 观察 `finish` 事件中的 `confidence_score` 分布
**Git commit**`feat(rag): add confidence-based prompt guidance for low-quality retrieval`
---
### Phase 5上下文排序优化 `部分实施`
**目标**:对所有查询类型(不仅是列举类)都做同章节聚合排序,保证同一文件同一章节的切片连续排列。
**改动范围**`api/chat_routes.py` 的 `_order_text_contexts_for_prompt`(当前第 188 行)。
**当前状态**
- `_build_context_with_budget()`(第 312 行)已实现了通用的分数排序 + 预算控制逻辑
- 但列举类查询(`_is_enum_query`)和对比类查询仍走独立分支(第 1754 行),直接拼接不做预算截断
- 尚未将"同章节聚合排序"推广为所有查询类型的默认行为
**原始设计**
1. 将当前只对 `_is_enum_query` 执行的排序逻辑推广为所有查询类型的默认行为
2. 排序策略:
- 第一优先级:按 Rerank 分数降序(高分切片先进入上下文)
- 第二优先级:同一 source + section 的切片按 chunk_index 连续排列
- 具体做法:先按 (source, section) 分组,组内按 chunk_index 排序,组间按组内最高分降序
3. 列举类查询保持当前行为不变(作为特例)
**风险**:低。排序变化不影响切片内容,只影响 LLM 看到的顺序。
**验证方法**
- 对比 `context_built.chunks_used` 的排列顺序
- 评估分数不应低于 Phase 4
**Git commit**`feat(rag): apply section-aware context ordering for all query types`
---
### Phase 6引用标注改进 `已实施`
**目标**:提升引用匹配精度,支持多引用。
**改动范围**:已迁移至 `core/agentic_citation.py`(独立模块),原 `api/chat_routes.py` 中也保留了 `_attach_citations`(第 393 行)作为备用。
**实施说明**
- 动态阈值已实现:短段落(< 50 字)使用 overlap 阈值 0.55,长段落 0.45
- 每段最多匹配 2 个 chunk
- 引用标记格式 `[ref:chunk_id]`,前端 `extractCitations` 已支持多个引用
- `agentic_citation.py` 中 `_attach_citations` 方法(第 220 行)为 agentic 模式提供独立的引用标注
**原始设计**
1. 动态阈值:短段落(< 50 字)使用更高的 overlap 阈值0.55),长段落保持 0.45
2. 允许一个段落匹配最多 2 个 chunk如果两个 chunk 的 overlap 分数都超过阈值且差距小于 0.1
3. 引用标记改为 `[ref:chunk_id_1][ref:chunk_id_2]`,前端 `extractCitations` 已支持多个 `[ref:xxx]`
**风险**:中。多引用可能导致引用列表变长、前端展示变化。
**验证方法**
- 检查引用数量是否合理增加(不应翻倍)
- 引用溯源弹窗的跳转是否仍然正确
- 评估分数不应下降
**Git commit**`feat(rag): improve citation matching with dynamic thresholds and multi-citation support`
---
### 阶段依赖关系
```
Phase 0 (基线+评估) [部分实施]
Phase 1 (Rerank分数传递) [已实施] ← 风险最低,收益最直接
Phase 2 (Token预算) [已实施] ← 依赖 Phase 1 的分数传递
Phase 3 (扩展精细化) [已实施] ← 依赖 Phase 1 的分数信息
Phase 4 (置信度兜底) [已实施] ← 依赖 Phase 1 的分数信息
Phase 5 (上下文排序) [部分实施] ← 独立,可与 Phase 4 互换顺序
Phase 6 (引用标注) [已实施] ← 独立,已迁移至独立模块
```
### 回退策略
每个 Phase 完成后执行:
```bash
# 运行评估
python scripts/eval_e2e.py --output data/eval_results/phase_N.json
# 对比上一阶段
# 如果评分下降 > 5%,回退:
git revert HEAD
# 如果评分持平或提升,打 tag
git tag v1.0-phase-N
```
### 预估时间
| 阶段 | 代码改动量 | 预估时间 |
|------|----------|---------|
| Phase 0 | 新建 2 个文件 | 2-3 小时 |
| Phase 1 | 改 2 个文件,约 30 行 | 1 小时 |
| Phase 2 | 改 2 个文件,约 40 行 | 1-2 小时 |
| Phase 3 | 改 1 个文件,约 20 行 | 1 小时 |
| Phase 4 | 改 2 个文件,约 25 行 | 30 分钟 |
| Phase 5 | 改 1 个文件,约 30 行 | 1 小时 |
| Phase 6 | 改 1 个文件,约 40 行 | 1-2 小时 |

View File

@@ -335,7 +335,7 @@ curl -X POST http://localhost:5001/search \
```bash ```bash
# 测试出题接口(携带网关注入的 Header # 测试出题接口(携带网关注入的 Header
curl -X POST http://localhost:5001/exam/generate-by-file \ curl -X POST http://localhost:5001/exam/generate \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-H "X-User-ID: test_user" \ -H "X-User-ID: test_user" \
-H "X-User-Role: admin" \ -H "X-User-Role: admin" \
@@ -343,7 +343,7 @@ curl -X POST http://localhost:5001/exam/generate-by-file \
-d '{ -d '{
"file_path": "public/公司简介.txt", "file_path": "public/公司简介.txt",
"collection": "public_kb", "collection": "public_kb",
"choice_count": 2 "question_types": {"single_choice": 2, "true_false": 1}
}' }'
``` ```

View File

@@ -1,80 +0,0 @@
1. 整体结构 (Envelope Pattern)所有题型统一采用以下外壳,通过 question_type 进行区分。JSON{
"metadata": {
"question_id": "string (UUID)",
"question_type": "single_choice | multiple_choice | true_false | fill_blank | subjective",
"difficulty": 3,
"tags": ["知识点A", "知识点B"],
"score": 10.0,
"version": "1.0"
},
"source_trace": {
"document_id": "string - 原始文档ID",
"document_name": "string - 文档名称",
"source_context": "string - AI出题参考的原文切片/上下文",
"page_numbers": [14, 15]
},
"content": {
"stem": "string - 题干内容支持Markdown格式",
"data": {},
"answer": null,
"explanation": "string - 详尽的答案解析"
},
"review_info": {
"status": "pending",
"reviewer_comment": null,
"created_at": "2026-04-17T18:30:00Z"
}
}
2. 各题型 content.data 与 content.answer 定义(1) 单选题 (single_choice)data: 包含选项数组。answer: 对应正确选项的 key。JSON"content": {
"stem": "根据文档,公司成立的年份是?",
"data": {
"options": [
{"key": "A", "content": "1998年"},
{"key": "B", "content": "2005年"},
{"key": "C", "content": "2010年"}
]
},
"answer": "B",
"explanation": "在文档第三页明确提到公司于2005年获得营业执照。"
}
(2) 多选题 (multiple_choice)data: 选项数组。answer: 正确选项 key 的数组。JSON"content": {
"stem": "以下属于公司核心价值观的有?",
"data": {
"options": [
{"key": "A", "content": "创新"},
{"key": "B", "content": "诚信"},
{"key": "C", "content": "加班"}
]
},
"answer": ["A", "B"],
"explanation": "手册第一章第一节列出了‘创新’与‘诚信’为唯二核心价值观。"
}
(3) 判断题 (true_false)data: 可留空或自定义文字(如:正确/错误,对/错。answer: boolean (true 为正确false 为错误)。JSON"content": {
"stem": "员工可以在办公区吸烟。",
"data": null,
"answer": false,
"explanation": "《行政管理规范》第五条严禁在办公区吸烟。"
}
(4) 填空题 (fill_blank)data: 槽位定义。answer: 数组按顺序存放每个空的标准答案列表支持同义词。JSON"content": {
"stem": "RAG的全称是___其核心在于利用___增强生成效果。",
"data": { "blank_count": 2 },
"answer": [
["检索增强生成", "Retrieval Augmented Generation"],
["外部知识库", "自有文档", "Context"]
],
"explanation": "RAG即检索增强生成通过引入外部知识库信息提升回答准确度。"
}
(5) 主观题 (subjective)data: 包含评分维度和关键词。answer: string (参考范文)。JSON"content": {
"stem": "请简述RAG架构中切片策略对检索质量的影响。",
"data": {
"scoring_points": [
{"point": "提到了语义完整性", "weight": 0.4},
{"point": "提到了切片过大导致噪声", "weight": 0.3},
{"point": "提到了切片过小丢失上下文", "weight": 0.3}
],
"keywords": ["语义窗口", "Top-K", "重叠度"]
},
"answer": "参考范文合理的切片策略能保持语义完整通过设置Overlap防止信息断层...",
"explanation": "此题考查对RAG性能优化的深度理解。"
}
3. 后端约束说明 (Tips for Backend)数据持久化:建议将 content 整体以 JSONB (PostgreSQL) 或类似的格式存储,方便扩展未来可能增加的题型(如连线题、排序题)。分值校验:后端应校验 metadata.score 是否为正数;对于多选题,可增加逻辑判断 answer 数组长度必须 $\ge 2$。RAG 闭环source_trace 字段是 RAG 项目的核心,建议后端在审核界面提供一个“点击查看原文”的按钮,直接展示 source_context 的内容。幂等性:建议由前端或 AI 服务生成 question_id (UUID),防止网络波动导致的重复入库。

View File

@@ -1,5 +1,7 @@
## 风险边界问题修复注意事项 ## 风险边界问题修复注意事项
> **状态**本文档中列出的所有场景均已修复并部署2026-06-04。保留本文档供参考避免回退。
### 一、修复了哪些会出问题的情况 ### 一、修复了哪些会出问题的情况
以下场景之前会报错或数据异常,现在已修复,不会再出问题: 以下场景之前会报错或数据异常,现在已修复,不会再出问题: