尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

基于布局感知的智能文档处理:优化RAG系统在复杂PDF与表格中的检索精度

基于布局感知的智能文档处理:优化RAG系统在复杂PDF与表格中的检索精度 1. 项目概述基于文档布局感知的智能检索增强生成最近在折腾一个文档智能处理的项目核心目标是把那些结构复杂、包含大量表格和列表的PDF或扫描件变成大语言模型能“理解”并能精准回答问题的知识库。这听起来像是典型的RAG应用但实际做起来你会发现传统“一刀切”的文本分块方法在这里完全失灵。想象一下你把一份年度财报按固定字符数切碎结果一个完整的表格被拦腰截断表头信息和数据行分家了或者一个多级列表只有第一块保留了列表标题后面的条目成了无意义的孤岛。这种信息割裂直接导致后续的检索质量断崖式下跌模型给出的答案要么不完整要么干脆是错的。这个项目的灵感正是为了解决这个痛点。它不是一个简单的文本提取工具而是一套布局感知的文档处理流水线。它深度整合了Amazon Textract的布局分析能力能够智能识别文档中的标题、章节、段落、表格、列表、页脚等语义区块。基于此我们实现了“按结构分块而非按字数切割”的策略。简单来说就是让分块过程尊重文档的原始逻辑一个表格尽量保持在一起一个列表项及其标题要绑定一个段落要带着它所属的小节标题。这样处理后的文本块再存入向量数据库进行检索当用户提问时系统不仅能找到语义相关的片段还能根据元数据比如这个片段属于哪个章节、哪个表格灵活地组织上下文信息喂给大模型从而生成更准确、更具上下文关联的回答。这套方案特别适合处理企业内部的技术手册、法律合同、财务报告、研究论文等高度结构化的文档。如果你正在为RAG系统在处理复杂文档时“智商不足”而头疼或者厌倦了手动标注文档结构的繁琐工作那么接下来对这套方案核心思路和实操细节的拆解或许能给你带来一些新的启发。我们将从文档的智能解析与增强开始一步步深入到分块索引策略和检索增强的实战技巧。2. 核心思路与架构设计解析为什么传统的RAG在复杂文档上会“翻车”根本原因在于它把文档视为一维的、均匀的文本流。而现实世界的文档是二维的、充满结构的。一个“表格单元格里的数字”和“段落里描述这个数字的文字”在空间上是关联的这种关联是理解文档含义的关键。本项目的架构设计核心就是引入“布局”作为第三维信息构建一个理解文档空间与语义关系的处理管道。2.1 从“文本提取”到“结构重建”的范式转变传统文档处理流程通常是OCR识别文字 - 按固定长度或标点分块 - 向量化 - 检索。这个过程丢失了所有版面信息。本项目的流程则截然不同结构感知提取利用Amazon Textract的Layout分析功能它不仅能识别文字还能返回每个文字块的边界框坐标并智能地将其归类为TITLE,HEADER,SECTION_HEADER,FOOTER,PAGE_NUMBER,LIST,FIGURE,TABLE,KEY_VALUE_SET等类型。这相当于给文档做了一次“CT扫描”得到了带有器官标签的解剖图。语义增强与线性化原始Textract的输出是零散的区块集合。这里引入了关键工具——Amazon Textract Textractor库。它的作用就像一个“翻译官”将这些带标签的区块按照人类阅读的自然顺序通常是从左到右、从上到下拼接起来并在不同的语义区块之间插入XML标签。例如一个标题会被包裹在title标签内一个表格会被table标签标记。输出结果是一份保留了完整逻辑结构的“富文本”。基于结构的智能分块这是最具创新性的一环。分块策略不再是盲目的而是“看菜下饭”对于表格以行为单位进行分块。如果表格很大超过预设的单词数阈值则按行拆分。关键是每一块都会附上完整的表头信息以及文档中出现在表格前面的描述性文字作为表格上下文。这确保了任何一块表格数据都不会失去其列定义和背景说明。对于列表以列表项为单位分块。同样每个列表项分块都会带上列表的标题或引导句。避免了只有第一个列表项知道自己在说什么后面项变成“无头冤案”的问题。对于普通文本段落以段落为单位并附上其直属的章节标题。这建立了“段落-小节-大章”的层级归属关系。富元数据索引每个文本块在存入向量数据库如Amazon OpenSearch时都携带丰富的元数据。这些元数据包括parent_section_id: 该块所属的小节ID。parent_chapter_id: 该块所属的章节ID。table_csv: 如果块包含表格这里会存储该表格的CSV格式字符串。element_type: 区块类型如paragraph,table_row,list_item。上下文感知检索与生成在检索阶段系统通过向量相似度找到top-K个相关块。此时得益于之前建立的层级元数据系统可以执行灵活的上下文组装策略。例如对于需要宏观背景的问题可以不仅返回匹配的块还将其parent_chapter的文本一起作为上下文对于需要细节验证的问题可能只返回精确的块及其直接所属的parent_section。这种“按需装配上下文”的能力极大地提升了回答的精准度和可控性。整个架构的优势在于它通过利用云服务Textract的现成强大能力将最难的结构化解析问题外包团队则可以聚焦于更高价值的业务逻辑设计——即如何利用这些结构信息来优化RAG的每一个环节。这是一种典型的“借助专业工具解决专业问题”的工程思维。2.2 关键组件选型与考量为什么选择这些AWS服务这背后有清晰的成本、性能和易用性权衡Amazon Textract (Layout Analysis)这是基石。自研版面分析算法需要巨大的标注数据和机器学习工程投入。Textract提供了开箱即用的高精度服务支持多种格式和语言并且其Layout功能是专门为复杂文档设计的。相比通用OCR它输出的结构化信息是本项目得以进行的前提。选择它相当于站在了巨人的肩膀上。Amazon Textract Textractor库这是“胶水”。Textract的原始API响应是JSON格式处理起来较为复杂。Textractor库由AWS样本团队维护提供了高级的、Pythonic的接口能将JSON响应轻松转换为Pandas DataFrame或带有XML标签的文本极大简化了开发流程。使用它避免了重复造轮子也保证了与Textract服务的最佳兼容性。向量数据库 (Amazon OpenSearch)这里选择OpenSearch而非其他向量数据库如Pinecone, Chroma主要基于以下几点混合搜索能力OpenSearch支持同时进行向量相似度搜索和传统的文本关键词搜索如BM25。这对于RAG场景非常宝贵因为有些查询如精确的产品代码、人名用关键词匹配更准确、更快。丰富的过滤与聚合利用我们注入的元数据element_type,parent_chapter_id可以轻松实现过滤查询例如“只检索属于第三章的表格”。这为高级检索策略提供了可能。与AWS生态集成在AWS内部OpenSearch服务管理方便与IAM的集成简化了访问控制对于已经在AWS上运行的项目减少了运维复杂度。大语言模型 (通过Amazon Bedrock或SageMaker JumpStart)生成答案的“大脑”。Bedrock提供了多种主流模型的统一API方便切换和试验。SageMaker JumpStart则便于部署和定制开源模型。选择哪种取决于对模型性能、成本和控制力的具体要求。本项目架构与模型解耦可以灵活适配。这个选型组合体现了一个务实的原则在核心且困难的问题上文档解析使用托管服务以保障质量在需要灵活性和业务逻辑的地方分块策略、检索逻辑保持自主控制在整个技术栈上优先选择集成度好、能降低长期运维成本的方案。3. 文档处理与索引从原始文件到智能向量这是整个流水线中最具技术含量的一环。我们将把一个上传到S3的PDF文档转化为带有丰富标签和元数据的文本块并索引到OpenSearch中。这个过程充满了细节和“坑”我会结合代码片段和实操经验来详细说明。3.1 第一步启动Textract布局分析首先你需要确保你的文档比如annual-report.pdf已经在S3桶中。调用Textract的start_document_analysisAPI是异步的这对于多页文档是必须的因为处理可能需要数十秒。import boto3 from trp import Document textract_client boto3.client(textract, region_nameus-east-1) s3_bucket your-document-bucket s3_key path/to/annual-report.pdf response textract_client.start_document_analysis( DocumentLocation{S3Object: {Bucket: s3_bucket, Name: s3_key}}, FeatureTypes[TABLES, LAYOUT], # 关键同时请求表格和布局分析 NotificationChannel{...}, # 可选用于异步回调 OutputConfig{S3Bucket: s3_bucket, S3Prefix: textract-output/} ) job_id response[JobId]注意FeatureTypes中必须同时包含TABLES和LAYOUT。如果只选LAYOUT表格会被当作普通文本处理失去行列结构如果只选TABLES你会失去标题、列表等其他布局信息。这是一步关键的配置决定了后续能拿到多少“原材料”。获取结果需要轮询或通过SNS通知。拿到结果JSON后我们使用textractor库来解析。from textractor import Textractor from textractor.data.constants import TextractFeatures extractor Textractor(region_nameus-east-1) # 假设我们已经从S3下载了Textract的输出JSON文件 document extractor.analyze_document( file_source./textract-output/job_id.json, features[TextractFeatures.LAYOUT, TextractFeatures.TABLES] )此时document对象包含了所有分析后的信息。你可以通过document.pages访问每一页每个页面有.tables,.layouts等属性。3.2 第二步使用Textractor进行语义增强与线性化Textractor库的export_to_txt方法有一个强大的参数keep_text_format当与布局分析结合时它会在输出文本中插入XML标签。linearized_text_with_tags document.export_to_txt( keep_text_formatTrue, include_page_breaksFalse )得到的linearized_text_with_tags可能看起来像这样title2023年度可持续发展报告/title header执行摘要/header section_header1. 环境承诺/section_header paragraph本公司致力于在2030年前实现碳中和。我们的主要举措包括/paragraph list_item• 提升能源效率投资可再生能源。/list_item list_item• 优化物流网络减少运输排放。/list_item section_header1.1 碳排放数据/section_header paragraph以下是我们近三年的范围一和范围二碳排放总量单位吨二氧化碳当量。/paragraph table | 年份 | 范围一排放 | 范围二排放 | 总计 | |------|------------|------------|------| | 2021 | 12,500 | 8,200 | 20,700 | | 2022 | 11,800 | 7,900 | 19,700 | | 2023 | 10,500 | 6,500 | 17,000 | /table这个输出是后续所有智能处理的基础。它完美保留了文档的视觉和逻辑结构使得程序能够像人类一样理解“这是一个标题下面跟着几个段落和一个表格”。3.3 第三步实现布局感知的智能分块策略这是核心算法所在。我们需要解析上面的XML格式文本并按照之前说的策略进行分块。下面是一个简化的、概念性的分块函数用于说明如何处理混合内容import xml.etree.ElementTree as ET from typing import List, Dict import pandas as pd def layout_aware_chunking(enriched_text: str, max_words: int 200) - List[Dict]: 将带有XML标签的增强文本按布局结构分块。 返回一个字典列表每个字典代表一个块及其元数据。 chunks [] current_chunk {text: , metadata: {type: None, parent_headers: []}} # 这里需要一个XML解析器来遍历元素 # 为简化我们假设用一个状态机来模拟解析过程 lines enriched_text.split(\n) i 0 while i len(lines): line lines[i] # 检测标签开始 if line.startswith(title): # 遇到新的大标题强制结束上一个块如果有内容 if current_chunk[text]: chunks.append(current_chunk.copy()) current_chunk {text: , metadata: {type: paragraph, parent_headers: []}} title_text line.replace(title, ).replace(/title, ) # 标题通常单独成块 chunks.append({ text: title_text, metadata: {type: title, parent_headers: []} }) # 更新当前上下文标题用于后续段落 current_header_context [title_text] elif line.startswith(section_header): # 遇到小节标题同样可能结束上一个段落块 if current_chunk[text] and current_chunk[metadata][type] paragraph: chunks.append(current_chunk.copy()) header_text line.replace(section_header, ).replace(/section_header, ) current_header_context [current_header_context[0], header_text] # 假设两级标题 # 小节标题也可能单独成块或作为后续内容的元数据 current_chunk[metadata][parent_headers] current_header_context.copy() elif line.startswith(table): # 处理表格提取表格直到/table table_lines [] i 1 while i len(lines) and not lines[i].startswith(/table): table_lines.append(lines[i]) i 1 table_markdown \n.join(table_lines) # 将Markdown表格转换为CSV便于存储和后续处理 # 这里需要一个从Markdown到CSV的转换函数假设存在 table_csv convert_markdown_table_to_csv(table_markdown) # 表格分块策略按行分块 df pd.read_csv(pd.compat.StringIO(table_csv)) table_header current_chunk[text] # 表格前的描述文本 for _, row in df.iterrows(): row_text , .join([f{col}: {val} for col, val in row.items()]) chunk_text f{table_header} | 表格行数据: {row_text} chunks.append({ text: chunk_text, metadata: { type: table_row, parent_headers: current_header_context.copy(), table_csv: table_csv, # 存储完整表格作为元数据 table_header: table_header } }) # 清空当前块因为表格内容已处理完 current_chunk[text] elif line.startswith(list_item): # 处理列表项 item_text line.replace(list_item, ).replace(/list_item, ) # 列表标题通常是前一个非列表项的行 list_header current_chunk[text].strip() chunk_text f{list_header}: {item_text} chunks.append({ text: chunk_text, metadata: { type: list_item, parent_headers: current_header_context.copy(), list_header: list_header } }) current_chunk[text] # 列表标题已被消耗 else: # 普通段落文本 if line.strip() and not line.startswith(/): # 忽略空行和闭合标签 current_chunk[text] line current_chunk[metadata][type] paragraph current_chunk[metadata][parent_headers] current_header_context.copy() # 检查当前块是否超过最大单词数 if len(current_chunk[text].split()) max_words: chunks.append(current_chunk.copy()) current_chunk {text: , metadata: {type: paragraph, parent_headers: current_header_context.copy()}} i 1 # 添加最后一个块 if current_chunk[text]: chunks.append(current_chunk) return chunks实操心得在实际开发中直接解析拼接的XML字符串可能比较脆弱。更稳健的做法是直接使用Textractor库提供的document对象它已经将布局元素document.layouts和表格document.tables封装成了Python对象你可以通过遍历这些对象来构建文档树并根据它们的bounding_box坐标判断层级和顺序关系。上面的代码仅用于阐述分块的逻辑思想。处理复杂表格的“坑”与技巧 Textract有时会返回合并的单元格。在分块前必须“解合并”。例如一个跨两行的单元格“2022-2023”在分块时需要将其值复制到它覆盖的每一个单元格中。否则当你按行分块时某些行会缺失数据。Textractor库的table.to_pandas()方法通常会自动处理一些合并情况但对于极端复杂的表格可能仍需后处理。# 假设table是一个Textractor的Table对象 df table.to_pandas() # 检查并处理df中的NaN值它们可能来自合并单元格 df df.ffill(axis0) # 向下填充适用于跨行合并 df df.ffill(axis1) # 向右填充适用于跨列合并需谨慎根据实际情况3.4 第四步向量化与索引到OpenSearch分块完成后我们需要将每个块的文本转换为向量嵌入并连同其丰富的元数据一起存入OpenSearch。首先为OpenSearch索引定义一个映射这相当于数据库的表结构。index_body { settings: { index: { knn: True, # 启用k-NN插件以支持向量搜索 knn.algo_param.ef_search: 512, } }, mappings: { properties: { text: {type: text}, # 原始文本用于关键词搜索 embedding: { # 向量字段 type: knn_vector, dimension: 1536, # 假设使用text-embedding-3-small模型维度为1536 method: { name: hnsw, space_type: cosinesimil, engine: nmslib } }, metadata: { type: object, properties: { type: {type: keyword}, # 块类型paragraph, table_row, list_item等 parent_headers: {type: text}, # 父级标题可用于过滤 parent_section_id: {type: keyword}, # 可关联到更结构化的ID parent_chapter_id: {type: keyword}, table_csv: {type: text, index: False}, # 不用于搜索仅存储 list_header: {type: text}, source_document: {type: keyword} } } } } }然后遍历所有分块生成嵌入并批量索引。import boto3 from opensearchpy import OpenSearch, RequestsHttpConnection, helpers from requests_aws4auth import AWS4Auth # 1. 生成嵌入向量 (假设使用Bedrock的Titan Embeddings模型) bedrock_runtime boto3.client(bedrock-runtime, region_nameus-east-1) def get_embedding(text): response bedrock_runtime.invoke_model( modelIdamazon.titan-embed-text-v2:0, bodyjson.dumps({inputText: text}) ) response_body json.loads(response.get(body).read()) return response_body.get(embedding) # 2. 准备OpenSearch客户端 host your-opensearch-domain-endpoint awsauth AWS4Auth(your-access-key, your-secret-key, us-east-1, es) client OpenSearch( hosts[{host: host, port: 443}], http_authawsauth, use_sslTrue, verify_certsTrue, connection_classRequestsHttpConnection ) # 3. 创建索引如果不存在 if not client.indices.exists(indexsmart-doc-index): client.indices.create(indexsmart-doc-index, bodyindex_body) # 4. 批量索引数据 actions [] for idx, chunk in enumerate(chunks): embedding get_embedding(chunk[text]) action { _index: smart-doc-index, _id: fdoc_1_chunk_{idx}, _source: { text: chunk[text], embedding: embedding, metadata: chunk[metadata] } } actions.append(action) # 使用helpers.bulk进行高效批量插入 helpers.bulk(client, actions)重要提示在实际生产环境中务必注意速率限制和错误处理。Bedrock的InvokeModel API有TPS限制大量文档需要索引时要设计队列和重试机制。同时OpenSearch的批量插入helpers.bulk也要控制每次提交的文档大小避免内存溢出。至此一个结构复杂、富含表格和列表的文档就被转化成了一组带有语义标签、层级关系和原始表格数据的向量块安静地躺在OpenSearch中等待着被智能检索。4. 检索增强生成从精准检索到上下文装配索引建立后RAG的“R”部分就变得格外强大。我们不再是从一堆无差别的文本片段中搜索而是从一个结构化的知识网络中做精准定位。这里的核心在于如何利用我们精心准备的元数据。4.1 执行混合搜索当用户提出一个问题例如“2023年的范围二排放量是多少”我们首先将其转换为向量。query 2023年的范围二排放量是多少 query_embedding get_embedding(query)然后我们向OpenSearch发起一个混合搜索请求结合语义向量搜索和关键词匹配。search_body { size: 5, # 返回Top-5结果 query: { hybrid: { # OpenSearch的混合查询功能 queries: [ { knn: { # 向量相似度查询 embedding: { vector: query_embedding, k: 10 } } }, { match: { # 文本关键词查询 (BM25) text: query } } ] } } } response client.search(indexsmart-doc-index, bodysearch_body)这个查询会返回最相关的文档块。由于我们的块是细粒度的例如一行表格数据返回的结果可能直接就是包含“2023”和“范围二排放”的那一行数据块。4.2 动态上下文装配策略传统的RAG通常简单地将Top-K个块的文本拼接起来作为上下文。但在我们的体系里我们有更多选择。每个返回的块都带有metadata告诉我们它是什么类型、属于哪个章节。我们可以设计一个上下文装配器根据查询的意图和返回块的类型动态决定给大模型喂多少“上下文”。def assemble_context(search_results, strategyauto): 根据策略组装上下文。 strategy: - precise: 仅使用匹配块本身。 - section: 使用匹配块及其所在小节的文本。 - chapter: 使用匹配块及其所在章节的所有相关块。 - auto: 根据块类型和查询复杂度自动判断。 contexts [] for hit in search_results[hits][hits]: chunk hit[_source] meta chunk[metadata] if strategy precise: context chunk[text] elif strategy section: # 需要根据parent_section_id去索引中查询同属该小节的所有块 section_id meta.get(parent_section_id) section_chunks fetch_chunks_by_section(section_id) # 假设的函数 context \n\n.join([c[text] for c in section_chunks]) elif strategy chapter: chapter_id meta.get(parent_chapter_id) chapter_chunks fetch_chunks_by_chapter(chapter_id) # 假设的函数 context \n\n.join([c[text] for c in chapter_chunks]) elif strategy auto: # 简单的启发式规则 if meta[type] table_row: # 对于表格行把整个表格作为上下文可能更好 context f相关表格数据:\n{meta.get(table_csv, )}\n\n具体行信息: {chunk[text]} elif summary in query.lower() or 概述 in query: # 对于概括性问题使用章节级上下文 context assemble_context([hit], strategychapter) else: # 默认使用小节级上下文 context assemble_context([hit], strategysection) contexts.append(context) # 合并所有上下文并去重 final_context \n---\n.join(dict.fromkeys(contexts)) # 用dict.fromkeys保持顺序去重 return final_context例如对于“2023年范围二排放量”这种事实型问题strategyprecise可能就足够了因为返回的块本身很可能就包含了答案。但对于“请总结一下第三章的环境承诺措施”这种问题strategychapter就更合适因为它需要聚合该章节下的多个段落和列表。4.3 构建提示词与调用LLM生成答案有了精心组装的上下文构建提示词就水到渠成了。一个好的提示词应该明确指令、提供格式、并利用好上下文。from langchain.prompts import PromptTemplate from langchain_aws import ChatBedrock # 假设使用LangChain集成 llm ChatBedrock(model_idanthropic.claude-3-sonnet-20240229-v1:0) prompt_template PromptTemplate.from_template( 你是一个专业的文档分析助手。请严格根据以下提供的上下文信息来回答问题。如果上下文中的信息不足以回答问题请直接说“根据提供的资料我无法回答这个问题”不要编造信息。 上下文信息 {context} 问题{question} 请用清晰、准确的语言回答。如果答案涉及数据请注明出处例如来自哪个表格或章节。 ) def generate_answer(question, context): prompt prompt_template.format(contextcontext, questionquestion) response llm.invoke(prompt) return response.content # 整合流程 query 2023年的范围二排放量是多少 search_results perform_hybrid_search(query) # 封装了之前的搜索函数 context assemble_context(search_results, strategyauto) answer generate_answer(query, context) print(answer) # 期望输出: “根据2023年度可持续发展报告中的碳排放数据表格2023年的范围二排放量为6,500吨二氧化碳当量。”这种方法的优势在于答案的生成不仅基于语义匹配还基于文档的结构化理解。模型知道答案来自一个表格并且可以准确地引用它。5. 实战避坑指南与性能优化在实际部署和运行这套流水线的过程中我踩过不少坑也总结出一些优化点。这里分享出来希望能帮你节省时间。5.1 文档预处理与Textract调优图像质量是生命线Textract对扫描文档的图像质量非常敏感。在上传前建议对图像进行预处理去噪、纠偏、提高对比度。一个简单的ImageMagick命令如convert input.jpg -deskew 40% -enhance -normalize output.jpg能显著提升识别准确率尤其是对表格线的检测。选择合适的Textract API对于超过一页的文档务必使用异步的StartDocumentAnalysis。同步的AnalyzeDocument有页数和文件大小限制。异步API虽然需要轮询结果但更稳定适合生产环境。控制成本与延迟Textract按页计费。对于超长文档如数百页的手册可以考虑先提取目录然后仅对用户可能查询的章节进行深度处理这是一种“懒加载”策略。同时设置合理的异步任务超时和重试机制。5.2 分块策略的微调最大单词数阈值不是固定的max_words200是一个起点。你需要根据你的文档类型和查询特点进行调整。技术文档段落可能较长可以设到300而新闻稿段落较短150可能更合适。最好的方法是用一批真实问题做测试观察不同阈值下检索到的块是否“刚好”包含答案所需的信息既不过于碎片化也不过于冗长。处理“悬挂标题”问题在解析XML时一个常见的难题是确定一个标题如section_header1.1 目标/section_header到底管辖后面多少内容直到下一个同级或更高级别的标题出现。这需要精确的解析逻辑。Textractor的document.layouts对象提供了层级关系通过reading_order等属性利用好这些属性比单纯解析文本标签更可靠。表格分块的边界情况超宽表格如果一个表格列数非常多一行就可能超过max_words。这时需要考虑按单元格分组或者将一行拆分成多个块但必须确保每个块都包含表头信息。嵌套表格有些文档在单元格内还有表格。Textract可能将其识别为一个复杂表格。在这种情况下可能需要递归处理。一个实用的方法是如果检测到单元格内文本有明确的表格特征如管道符|可以尝试二次解析。5.3 OpenSearch索引与查询优化向量维度与索引参数嵌入模型的输出维度如1536必须与OpenSearch索引中knn_vector的dimension设置完全一致。hnsw算法的参数如ef_construction,m会影响索引构建速度和搜索精度/召回率需要根据数据量进行调优。对于千万级以下的文档块默认参数通常够用。混合搜索的权重调整在hybrid查询中默认情况下两个子查询的分数会进行归一化后结合。你可以通过boost参数调整关键词搜索和向量搜索的权重。例如如果发现对于专业术语缩写关键词搜索更准可以适当提高其权重。{ query: { hybrid: { queries: [ {knn: {...}, boost: 0.7}, {match: {...}, boost: 1.3} ] } } }利用元数据进行过滤这是提升检索精度的利器。例如如果用户明确问“在财务章节中去年的利润是多少”你可以在搜索时添加过滤器search_body { query: { bool: { must: [{hybrid: {...}}], filter: [{term: {metadata.parent_headers: 财务章节}}] # 假设标题已标准化为关键词 } } }这能确保只从相关章节检索排除大量无关信息显著提升答案质量。5.4 生成阶段的提示工程与评估指令要明确防止幻觉提示词中“严格根据上下文”和“不要编造”的指令至关重要。对于Claude、GPT-4等模型还可以在系统提示中进一步强化这一点。可以要求模型在答案后引用上下文中的片段ID或行号如果元数据中有便于人工核查。上下文长度管理装配的上下文总长度不能超过LLM的上下文窗口。需要设计截断策略。一个简单有效的方法是优先保留向量相似度得分最高的块然后按相关性得分降序添加直到总token数接近上限。建立评估体系不要凭感觉判断系统好坏。定义一组测试问题涵盖事实型、概括型、推理型等。人工或通过LLM-as-a-judge评估答案的准确性是否基于上下文、完整性是否回答了问题的所有部分和相关性是否聚焦于问题。定期运行测试集监控系统性能变化。6. 扩展思路与应用场景这套布局感知的RAG流水线是一个强大的基础框架你可以在此基础上进行多种扩展以适应更复杂的业务场景。多模态RAGTextract也能提取图片中的文字。你可以将文档中的FIGURE元素对应的图片也保存下来例如存储到S3并生成预签名URL。在检索时如果相关块关联了图片可以将图片URL也放入上下文中。像Claude-3、GPT-4-Vision这样的多模态模型就能“看到”图片并描述其内容实现真正的图文并茂问答。实现“Small2Big”或“句子窗口”检索这是高级RAG技术。我们的元数据中已经存储了块与章节的父子关系。当检索到一个细粒度块如一个表格行时我们可以很容易地获取其“父节点”整个表格或所在段落甚至“祖父节点”整个小节的内容。在生成答案时先让模型基于细粒度块生成一个初步答案再让它基于更广泛的上下文进行修正和润色可以提高答案的连贯性和背景信息。图检索增强你可以将文档结构标题-小节-段落-列表项构建成一个知识图谱。当用户提出一个复杂、需要跨章节推理的问题时传统的向量检索可能只能找到孤立的点。而图检索可以沿着关系边进行遍历找到相关联的多个节点从而组装出更全面的上下文。应用于特定垂直领域法律合同审查将合同条款、责任方、金额、日期等作为关键元素进行分块和索引。提问时可以精准定位到“违约责任”条款或“支付方式”章节。学术论文研究识别摘要、引言、方法、结果、讨论、参考文献等部分。研究者可以提问“这篇论文用了什么方法验证假设”或“在结果部分图3显示了什么”系统能直接定位到相应部分。产品手册支持将故障代码、操作步骤、安全警告分别处理。用户输入故障代码系统不仅能返回解决步骤还能关联到相关的安全注意事项。这个项目的价值在于它提供了一种方法论在处理非结构化数据时尽可能多地保留和利用其内在的结构信息。这比单纯追求更大的模型或更复杂的算法往往能带来更直接、更可解释的性能提升。从工程实现角度看它巧妙地组合了多个成熟的云服务将复杂的AI问题分解为可管理、可调试的组件最终构建出一个强大且实用的企业级智能文档问答系统。
返回列表