- 后端 API(Flask + Gunicorn) - RAG 引擎(混合检索 + 云端 Reranker + 引用溯源) - 文档解析(MinerU + 多格式支持) - Docker 生产部署配置 - 排除前端项目、敏感配置、模型文件
12 KiB
12 KiB
图片处理完整流程分析
流程概览
┌─────────────────────────────────────────────────────────────────────────────────┐
│ 图片处理完整流程 │
├─────────────────────────────────────────────────────────────────────────────────┤
│ │
│ 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行):
@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行):
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行):
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行):
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行):
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
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 解析结果
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("---")