1. 大模型时代应用工具化以及无忠诚度现象讨论
接触大模型久了,也慢慢探到一些大模型能力表现非常自然和突出的场景,比如AI搜索(依赖大模型的理解总结能力)、AI对话(即chat,依赖大模型的生成能力)、AI工具使用(即智能体,依赖大模型的规划能力),应该说目前主流应用就是围绕这三个点进行的,没有例外。比如近期推出的夸克超级框、manus智能体工具、ima知识库都是如此。
另外,对于AI应用,当下用户其实完全没有忠诚度可言,这一特点是与之前移动应用时代app存在天壤之别。移动应用时代,更多是场景的扩展,将原来PC场景衍生到APP场景,用户忠诚的依然是平台资源本身。但到了AI应用时代,只要是哪一个产品使用更快更好更人性化,用户就会果断切换到新的产品。例如deepseek横空出世,包括腾讯、阿里云、字节在内的大厂,直接集成到自家云产品,用户也果断从之前的kimi、豆包、智谱清言,切换到各种deepseek chat工具。还有最近身边朋友看论文用夸克,但我推荐了另一款AI阅读助手后,他发现AI阅读助手表现更好,直接就放弃了夸克,改用我推荐的产品。这种现象和AI应用工具化有关,不需要依赖什么其他的资源,工具本身就是产品,随时可以切换,哪种效率提升更快就用哪一种。
所以说,要想从众多的模型、应用中凸显出来,必然需要不断追求对于场景最优的模型,用户体验越好的产品。这就绕不开产品匠心逻辑。反正当下既要速度也需要沉下心打磨出更好的产品。
说起产品打磨,回到知识库RAG场景,要想RAG能够又好又快又准确返回结果,一开始的文档解析工作就非常值得好好做。最近用了deepseek、智谱清言的工具,发现文档解析能力还有很多提升空间,只支持部分文档的解析,这或许是大模型公司更关注基础模型能力,只要有一个产品就行,但我觉得to C的产品还是有必要投入打磨。
2. 知识库生态建设之双栏PDF解析
对于文档解析,其中一个非常有意思的场景是对PDF双栏文档的解析,如果是按正常的解析工具以行为单位解析,会导致解析后的排版非常糟糕,比如夸克的解析,就不是很好。反而通义的工具表现很不错。
双栏解析的目标:
- 正确识别左右两栏
- 先处理左栏内容,再处理右栏内容
- 每栏内部按照从上到下的顺序排列
这里给一个简单的双栏解析思路:
- 利用CV工具将 pdf 每一页转换为图片
- 利用目标检测模型识别每一页中的元素并标注类型,即版面分析(例如yolo)
- 利用 OCR 来提取每一个元素中文字块信息(例如paddle系列)
- 利用表格抽取模型识别跨页表格(可以自己训)
- 完成版面分析,读取版面分析的JSON结果(包括每一模块的坐标信息)
- 对每一模块元素进行排序,排序逻辑如下:
- 计算每个元素在页面左右两侧的覆盖比例
- 根据覆盖比例确定元素属于左栏还是右栏
- 对于跨栏元素特殊处理
- 按照"先左栏后右栏,同栏内从上到下"的顺序输出
实施方案:
区分单栏和双栏:
- 计算所有文本块中心点的横坐标极差。
- 设定一个阈值(可以调整),如果极差小于阈值,则判定为单栏,否则为双栏。
单栏排序:
- 直接按中心点纵坐标(
top
)升序 排序。双栏排序:
- 计算页面中线(即所有文本块中心点的平均横坐标)。
- 文本块分类:
- 左栏:文本块的 右边界 < 中线
- 右栏:文本块的 左边界 > 中线
- 通栏:文本块的 左边界 < 中线 且 右边界 > 中线
- 排序顺序:
- 先对 左栏按
top
升序 排序。- 再对 右栏按
top
升序 排序。- 处理 通栏:
- 通栏上方 的 右栏拼接到左栏后。
- 通栏 内容放在其下方。
- 通栏下方 的 右栏拼接到左栏后。
其中:
页面宽度估计:
- 如果没有提供
page_width
,则从元素坐标中推断最大右边界作为页面宽度,可用于适配不同页面宽度的文档。中线计算:
- 采用
page_width / 2
计算页面的中线坐标,然后通过文本块覆盖比例(左/右)来判断其归属。更精准的左右栏判定:
- 计算文本块的左侧部分宽度和右侧部分宽度,再计算左右覆盖比例。
- 如果 90%以上的内容位于左侧,则归入左栏;如果 90%以上内容在右侧,则归入右栏。
- 这种方式比简单的 “右边界 < 另一文本块的左边界” 方法更加准确,能够适应不同尺寸的文本块,尤其是跨栏情况。
排序逻辑:
- 左栏文本块按照纵坐标排序,确保从上到下排列。
- 右栏文本块也按照纵坐标排序。
- 最终合并:左栏 → 右栏,保证自然的阅读顺序。
代码示例:
def sort_text_blocks(res):"""对文本块进行排序,支持单栏、双栏、通栏布局:param res: 文本块列表,每个文本块包含 'page_idx' 和 'extra_data'(包含 'position' 坐标):return: 经过排序的文本块列表"""# 按页码排序res.sort(key=lambda x: get_page_idx_value(x["page_idx"]))sorted_res = []pages = {}# 按页分组for block in res:page_idx = get_page_idx_value(block["page_idx"])pages.setdefault(page_idx, []).append(block)# 处理每一页for page_idx, blocks in pages.items():if not blocks:continuemax_page_width = extract_max_page_width(blocks)page_center_x = max_page_width / 2left_column, right_column, full_column = [], [], []for block in blocks:position = block["extra_data"]["position"]if isinstance(position, list) and len(position) > 0 and isinstance(position[0], list):x1, x2, y1, y2 = position[min(1, len(position) - 1)]else:x1, x2, y1, y2 = positionblock_width = x2 - x1left_part = max(0, min(x2, page_center_x) - x1)right_part = max(0, x2 - max(x1, page_center_x))left_ratio = left_part / block_width if block_width > 0 else 0right_ratio = right_part / block_width if block_width > 0 else 0if left_ratio >= 0.9:left_column.append(block)elif right_ratio >= 0.9:right_column.append(block)else:full_column.append(block)# 按从上到下排序key_func = lambda b: get_position_value(b["extra_data"]["position"], 2)left_column.sort(key=key_func)right_column.sort(key=key_func)full_column.sort(key=key_func)if full_column:min_full_top = get_position_value(full_column[0]["extra_data"]["position"], 2)max_full_bottom = get_position_value(full_column[-1]["extra_data"]["position"], 3)sorted_blocks = ([b for b in left_column if get_position_value(b["extra_data"]["position"], 3) < min_full_top] +[b for b in right_column if get_position_value(b["extra_data"]["position"], 3) < min_full_top] +full_column +[b for b in left_column if get_position_value(b["extra_data"]["position"], 2) > max_full_bottom] +[b for b in right_column if get_position_value(b["extra_data"]["position"], 2) > max_full_bottom])else:sorted_blocks = left_column + right_columnsorted_res.extend(sorted_blocks)return sorted_res
3. 参考材料
【1】LLM常见问题(基于 AI 的 pdf 解析)
【2】双栏学术论文转换为单栏Markdown