fix(rag): 修复表格图片替换和跨页合并三个bug
1. chat_routes._replace_table_image_placeholders:
- 处理  LLM 格式(去掉多余!和假URL)
- 支持 [N张图片] 多张占位符(html_table_to_markdown 生成)
- 修复 !! 双感叹号导致 markdown 图片语法失效
2. manager._merge_cross_page_tables:
- 表头跳过逻辑改为对比第一行而非 find_all('th')
- 修复合并后表格中间出现重复表头行的问题
This commit is contained in:
@@ -1373,12 +1373,20 @@ _FIGURE_ANSWER_KEYWORDS = frozenset([
|
|||||||
|
|
||||||
def _replace_table_image_placeholders(answer: str, images: List[Dict]) -> str:
|
def _replace_table_image_placeholders(answer: str, images: List[Dict]) -> str:
|
||||||
"""
|
"""
|
||||||
将回答中 markdown 表格内的 [图片] 占位符替换为实际图片语法。
|
将回答中 markdown 表格内的图片占位符替换为实际图片语法。
|
||||||
|
|
||||||
LLM 看到的上下文里表格单元格是 [图片](MinerU 解析产物),它也原样输出。
|
LLM 看到的上下文里表格单元格图片以 [图片] 或 [N张图片](MinerU 解析产物)表示,
|
||||||
此函数按顺序将每个 [图片] 映射到 images 列表中对应图片的 URL,
|
LLM 通常会原样输出,有时还会加上  格式。
|
||||||
|
|
||||||
|
此函数按顺序将占位符映射到 images 列表中的 URL,
|
||||||
生成  语法,前端 markdown-it 可直接渲染为 <img> 标签。
|
生成  语法,前端 markdown-it 可直接渲染为 <img> 标签。
|
||||||
|
|
||||||
|
处理的占位符格式:
|
||||||
|
- [图片] →  (单张)
|
||||||
|
-  →  (LLM 加了 ! 和假 URL)
|
||||||
|
- [3张图片] →   
|
||||||
|
-  → 同上 (LLM 加了前缀和假 URL)
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
answer: LLM 生成的回答文本
|
answer: LLM 生成的回答文本
|
||||||
images: select_images + _filter_images_by_answer 后的最终图片列表
|
images: select_images + _filter_images_by_answer 后的最终图片列表
|
||||||
@@ -1386,28 +1394,35 @@ def _replace_table_image_placeholders(answer: str, images: List[Dict]) -> str:
|
|||||||
Returns:
|
Returns:
|
||||||
替换后的回答文本
|
替换后的回答文本
|
||||||
"""
|
"""
|
||||||
if not images or '[图片]' not in answer:
|
if not images or '图片' not in answer:
|
||||||
return answer
|
return answer
|
||||||
|
|
||||||
# 按顺序替换:第 N 个 [图片] → 第 N 张图片
|
|
||||||
img_iter = iter(images)
|
img_iter = iter(images)
|
||||||
replaced_count = 0
|
replaced_count = 0
|
||||||
|
|
||||||
def _replacer(match):
|
def _replacer(match):
|
||||||
nonlocal replaced_count
|
nonlocal replaced_count
|
||||||
try:
|
n_str = match.group(1) # 数字部分,None 表示单张
|
||||||
img = next(img_iter)
|
n = int(n_str) if n_str else 1
|
||||||
url = img.get('url', '')
|
|
||||||
img_id = img.get('id', 'img')
|
|
||||||
replaced_count += 1
|
|
||||||
return f''
|
|
||||||
except StopIteration:
|
|
||||||
# 图片用完了,保留剩余占位符
|
|
||||||
return match.group(0)
|
|
||||||
|
|
||||||
result = re.sub(r'\[图片\]', _replacer, answer)
|
parts = []
|
||||||
|
for _ in range(n):
|
||||||
|
try:
|
||||||
|
img = next(img_iter)
|
||||||
|
url = img.get('url', '')
|
||||||
|
img_id = img.get('id', 'img')
|
||||||
|
parts.append(f'')
|
||||||
|
replaced_count += 1
|
||||||
|
except StopIteration:
|
||||||
|
parts.append('[图片]') # 图片用完了
|
||||||
|
return ' '.join(parts)
|
||||||
|
|
||||||
|
# !? — 可选前导 !(LLM 有时加)
|
||||||
|
# \[(\d+)?张?图片\] — [图片] 或 [3张图片](张 可选)
|
||||||
|
# (?:\(图片URL\))? — 可选的 LLM 从上下文复制的假 URL
|
||||||
|
result = re.sub(r'!?\[(\d+)?张?图片\](?:\(图片URL\))?', _replacer, answer)
|
||||||
if replaced_count > 0:
|
if replaced_count > 0:
|
||||||
logger.info(f"[图片占位符替换] 替换了 {replaced_count}/{len(images)} 个 [图片] 为 markdown 图片语法")
|
logger.info(f"[图片占位符替换] 替换了 {replaced_count}/{len(images)} 个图片为 markdown 图片语法")
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ from pathlib import Path
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
import chromadb
|
import chromadb
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
|
||||||
# 从 base.py 导入基础类和常量
|
# 从 base.py 导入基础类和常量
|
||||||
from .base import (
|
from .base import (
|
||||||
@@ -550,8 +551,39 @@ class KnowledgeBaseManager(
|
|||||||
curr_html = getattr(current, 'table_html', '') or ''
|
curr_html = getattr(current, 'table_html', '') or ''
|
||||||
next_html = getattr(next_chunk, 'table_html', '') or ''
|
next_html = getattr(next_chunk, 'table_html', '') or ''
|
||||||
if curr_html and next_html:
|
if curr_html and next_html:
|
||||||
# 合并两个表格的 HTML
|
# 正确合并两个表格的 HTML:
|
||||||
current.table_html = curr_html + '\n' + next_html
|
# 将第二个表格的 <tr> 行追加到第一个表格中
|
||||||
|
# (而非简单拼接两个 <table>,否则 html_table_to_markdown
|
||||||
|
# 的 soup.find('table') 只能找到第一个表格)
|
||||||
|
try:
|
||||||
|
soup1 = BeautifulSoup(curr_html, 'html.parser')
|
||||||
|
soup2 = BeautifulSoup(next_html, 'html.parser')
|
||||||
|
table1 = soup1.find('table')
|
||||||
|
table2 = soup2.find('table')
|
||||||
|
if table1 and table2:
|
||||||
|
# 从第二个表格提取数据行
|
||||||
|
next_rows = table2.find_all('tr')
|
||||||
|
# 跳过与第一个表格表头重复的行
|
||||||
|
# 对比第一行而非所有 th(find_all('th') 会匹配
|
||||||
|
# 整个表格的 th,无法与单行做列表比较)
|
||||||
|
first_row_t1 = table1.find('tr')
|
||||||
|
if first_row_t1 and next_rows:
|
||||||
|
row1_texts = [c.get_text(strip=True) for c in first_row_t1.find_all(['th', 'td'])]
|
||||||
|
row2_texts = [c.get_text(strip=True) for c in next_rows[0].find_all(['th', 'td'])]
|
||||||
|
if row1_texts and row2_texts and row1_texts == row2_texts:
|
||||||
|
next_rows = next_rows[1:]
|
||||||
|
logger.debug("跨页表格合并: 跳过了重复的表头行")
|
||||||
|
for row in next_rows:
|
||||||
|
table1.append(row)
|
||||||
|
current.table_html = str(soup1)
|
||||||
|
logger.debug(f"跨页表格 HTML 合并成功: 追加了 {len(next_rows)} 行")
|
||||||
|
else:
|
||||||
|
current.table_html = curr_html + '\n' + next_html
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"跨页表格 HTML 合并异常: {e},回退到简单拼接")
|
||||||
|
current.table_html = curr_html + '\n' + next_html
|
||||||
|
elif not curr_html and next_html:
|
||||||
|
current.table_html = next_html
|
||||||
|
|
||||||
# 合并 image_path 和嵌入图片到 images
|
# 合并 image_path 和嵌入图片到 images
|
||||||
curr_img = getattr(current, 'image_path', None)
|
curr_img = getattr(current, 'image_path', None)
|
||||||
|
|||||||
Reference in New Issue
Block a user