fix(rag): 图片描述注入安全网与 chart_contexts 宽松阈值

问题:图片/图表切片的 CrossEncoder 评分系统性偏低(0.002-0.08 vs 文本 0.3-0.9),
导致 chart_contexts 被 min_score 过滤,图片描述无法进入 LLM context。
典型案例:雷达图查询(ci=73)rerank score=0.039 < min_score=0.05,
虽然 select_images 正确选中了雷达图,但描述未出现在 LLM context 中。

修复:
1. chart_contexts 使用宽松阈值 min_score*0.5(与 table 保护逻辑一致)
   - 图片描述的 CE 分数天然偏低,不应与文本使用相同阈值
   - 实际内容相关性由 select_images 独立评分保证
2. P0 安全网:在现有图片注入代码后检查 selected_images 完整性
   - 若【相关图片信息】未生成,补注入所有选中图片描述
   - 若已有但遗漏部分图片,追加遗漏的描述
3. images_selected 调试事件增加 chunk_id/actual_score/id/desc_preview
4. P0 完整性日志:DEV 模式下检查图片描述是否完整注入 context
This commit is contained in:
lacerate551
2026-06-21 22:41:30 +08:00
parent 63a769540a
commit 4753a53487

View File

@@ -352,7 +352,11 @@ def _order_text_contexts_for_prompt(contexts: List[Dict], query: str, max_chunks
and (c.get('meta', {}).get('source', ''), c.get('meta', {}).get('section', '') or c.get('meta', {}).get('section_path', '')) in passing_sections
)
]
chart_contexts = [c for c in chart_contexts if c.get('score', 0) >= min_score]
# 图片/图表切片CrossEncoder 对图片描述评分系统性偏低0.002-0.08 vs 文本 0.3-0.9
# 使用更宽松的阈值min_score * 0.5),避免被误过滤。
# 实际内容相关性由 select_images 独立评分保证,此处仅做基础过滤。
_chart_floor = min_score * 0.5
chart_contexts = [c for c in chart_contexts if c.get('score', 0) >= _chart_floor]
# 合并:文本切片优先,图表切片补充
# 限制图表切片数量,避免过多
@@ -2752,7 +2756,7 @@ def rag():
# 调试事件:图片选择详情
if IS_DEV:
yield f"data: {json.dumps({'type': 'images_selected', 'data': {'total_scored': len([c for c in contexts if c.get('meta',{}).get('chunk_type') in ('image','chart','table')]), 'selected_count': len(selected_images), 'images': [{'source': img.get('source',''), 'page': img.get('page',0), 'score': round(img.get('_image_boost', 1.0), 2)} for img in selected_images]}}, ensure_ascii=False)}\n\n"
yield f"data: {json.dumps({'type': 'images_selected', 'data': {'total_scored': len([c for c in contexts if c.get('meta',{}).get('chunk_type') in ('image','chart','table')]), 'selected_count': len(selected_images), 'images': [{'source': img.get('source',''), 'page': img.get('page',0), 'score': round(img.get('_image_boost', 1.0), 2), 'chunk_id': img.get('chunk_id',''), 'actual_score': round(img.get('score',0), 2), 'id': img.get('id',''), 'desc_preview': (img.get('full_description','') or img.get('description',''))[:60]} for img in selected_images]}}, ensure_ascii=False)}\n\n"
# 4. 构建 promptPhase 6LLM 图片感知)
# Bug 1 修复:文本切片用于 top 5 名额竞争,图片描述不参与竞争
@@ -2854,8 +2858,53 @@ def rag():
# 添加指令让 LLM 介绍图片
context_text += "\n\n【回答要求】回答时请简要介绍每张图片的内容和用途。"
# P0 安全网:确保 select_images 选中的图片描述一定出现在 LLM context 中
# 场景chart_contexts 被 min_score 过滤、_build_context_with_budget 截断、
# 或其他过滤逻辑意外排除时selected_images 仍能提供描述
if selected_images and '【相关图片信息】' not in context_text:
# 现有注入代码未执行selected_images 为空时不会进入),补注入
image_descriptions = []
for i, img in enumerate(selected_images, 1):
full_desc = img.get('full_description', '') or img.get('description', '')
if full_desc and len(full_desc.strip()) >= 5:
img_source = img.get('source', '')
img_page = img.get('page', '')
source_info = f"(来源:{img_source}{img_page}页)" if img_source and img_page else ""
image_descriptions.append(f"【图片{i}{full_desc}{source_info}")
if image_descriptions:
context_text += "\n\n【相关图片信息】\n" + "\n\n".join(image_descriptions)
context_text += "\n\n【回答要求】回答时请简要介绍每张图片的内容和用途。"
elif selected_images:
# 已有【相关图片信息】,检查是否有遗漏(被过滤但 select_images 选中的图片)
_existing_desc_ids = set()
for img in selected_images:
img_id = img.get('id', '')
if img_id and img_id in context_text:
_existing_desc_ids.add(img_id)
_missing = [img for img in selected_images if img.get('id', '') not in _existing_desc_ids]
if _missing:
_missing_descs = []
_start_idx = len(selected_images) - len(_missing) + 1
for i, img in enumerate(_missing, _start_idx):
full_desc = img.get('full_description', '') or img.get('description', '')
if full_desc and len(full_desc.strip()) >= 5:
img_source = img.get('source', '')
img_page = img.get('page', '')
source_info = f"(来源:{img_source}{img_page}页)" if img_source and img_page else ""
_missing_descs.append(f"【图片{i}{full_desc}{source_info}")
if _missing_descs:
context_text += "\n\n" + "\n\n".join(_missing_descs)
enhanced_context = context_text
# P0 日志:图片描述注入完整性检查
if selected_images and IS_DEV:
_img_ids_in_ctx = {img.get('id', '') for img in selected_images if img.get('id', '') and img.get('id', '') in enhanced_context}
_img_ids_all = {img.get('id', '') for img in selected_images if img.get('id', '')}
_missing_ids = _img_ids_all - _img_ids_in_ctx
if _missing_ids:
logger.warning(f"[P0] 图片描述未完整注入 context: missing={_missing_ids}, total={len(selected_images)}")
# 对比类查询:添加结构化对比指令
if intent and intent.intent == "comparison" and intent.sub_queries:
comparison_instruction = (