1. 项目概述基于文档布局感知的智能检索增强生成最近在折腾一个文档智能处理的项目核心目标是把那些结构复杂、图文混排的PDF或扫描件变成大语言模型LLM能高效“理解”和“利用”的知识库。相信很多做企业知识管理、智能客服或者文档分析的朋友都遇到过类似的痛点传统的文档切分Chunking方法比如按固定字符数或句子分割在处理包含表格、列表、多级标题的文档时往往会破坏原有的语义结构和上下文关联。结果就是当你用向量检索Vector Search去查找信息时要么找不到关键内容要么返回的片段缺乏必要的背景信息导致后续的问答或总结效果大打折扣。这个项目我称之为“布局感知的文档处理与检索增强生成RAG管道”正是为了解决这个问题。它的核心思路非常直接在切分文档之前先理解文档的视觉和逻辑布局。我们不再把文档看作一维的文本流而是将其视为一个由标题、段落、表格、列表、页眉页脚等元素构成的二维结构。通过识别这些结构元素我们可以在切分时保留它们的归属关系比如某个表格属于哪个章节从而生成带有丰富上下文信息的“智能分块”。当进行检索时这些分块不仅能基于语义相似度匹配还能利用其结构元数据如所属章节标题进行更精准的筛选和上下文扩充最终显著提升RAG应用的回答质量。整个方案构建在AWS云服务之上主要利用了Amazon Textract的布局分析能力、Amazon SageMaker JumpStart部署的嵌入模型以及Amazon OpenSearch Service作为向量数据库。接下来我将拆解整个流程的设计思路、每一步的实操细节并分享我在搭建和调试过程中踩过的坑和总结的经验。2. 核心思路与架构设计解析为什么传统的“暴力分块”在复杂文档上会失效我们来看一个典型的业务报告页面它可能有一个主标题H1几个副标题H2/H3下面跟着几段文字然后插入一个复杂的合并单元格表格最后还有一个带项目符号的列表。如果你用固定500个字符去切很可能一刀把表格从中间劈开或者把列表的标题和它的项目分隔到两个不同的块里。对于LLM来说一个没有表头的表格行或者一个没有说明的列表项其信息价值是大打折扣的。因此本项目的设计哲学是“先理解后分割”。其整体架构可以分解为两个主要阶段文档处理与索引、检索与生成。2.1 文档处理与索引阶段这个阶段的目标是将原始的非结构化文档转化为富含语义和结构信息的向量片段并存入向量数据库。其流程如下图所示概念图文档上传与布局解析将多页文档如PDF上传至Amazon S3。然后调用Amazon Textract的StartDocumentAnalysisAPI并指定FeatureTypes为[“TABLES”, “LAYOUT”]。Textract的布局Layout功能非常强大它能识别出文档中的十大类元素标题Titles、页眉Headers、子标题Sub-headers、正文文本Text、表格Tables、图形Figures、列表Lists、页脚Footers、页码Page Numbers和键值对Key-Value pairs。它返回的不仅是被OCR识别出的文字更重要的是每个文字块在页面上的坐标Bounding Box以及它们之间的层级和阅读顺序关系。这为我们后续的智能分块提供了最基础的数据结构。文本增强与结构化直接使用Textract的原始响应进行分块仍然比较粗糙。这里我们引入了Amazon Textract Textractor这个Python工具库。它相当于一个高级封装能更方便地解析Textract的响应并将其转化为更易编程操作的对象。本项目的关键一步是利用Textractor库给提取出的文本“穿上XML外衣”。例如它会用title、section、table、list等标签来包裹对应的内容区域。这一步的“富文本”输出使得文档的语义边界变得清晰可辨是进行布局感知分块的前提。布局感知的智能分块这是整个项目的核心算法部分。分块策略不再是“一刀切”而是根据元素类型进行差异化处理按标题章节划分顶级块首先文档会按照最高层级的标题通常是H1或章标题被划分成几个大的部分Chapter。这是第一级分割。在章节内按元素类型精细化分块表格以行为单位进行分块。如果一个表格很大超过了预设的最大单词数例如500词则会被拆分成多个块。关键技巧在于每个表格块都会附带该表格的列标题表头以及文档中紧邻表格上方的那段说明文字。这样即使表格被拆分每一部分都知道自己是什么表格、有哪些列。对于复杂的合并单元格表格算法会先“解合并”将合并单元格的值复制到拆分后的每一个单元格中确保数据结构规整便于后续处理。列表以列表项为单位进行分块。同样每个列表项块都会附带整个列表的标题或说明文字。这解决了“只有第一个列表块知道自己在说什么”的经典问题。段落与章节正文部分以自然段落为最小分块单位。每个段落块都会附加上它所属的子章节标题Section Header。这样一个关于“财务风险”的段落永远会带着“财务风险”这个标签为其提供了明确的上下文。元数据构建与向量化索引为每个生成的分块创建丰富的元数据Metadata这是实现高级检索技巧的基础。元数据至少包括chunk_id: 分块唯一标识。parent_section_id: 所属的子章节ID。parent_chapter_id: 所属的顶级章节ID。element_type: 元素类型如paragraph,table_row,list_item。raw_table_csv: 如果该分块来自表格这里会存储整个表格的CSV格式数据而不仅仅是分块的那几行。 随后使用通过Amazon SageMaker JumpStart部署的文本嵌入模型例如BGE、GTE等为每个分块的文本内容生成向量Embedding。最后将{向量 文本内容 元数据}作为一个整体记录存入Amazon OpenSearch Service的向量索引中。注意元数据中存储完整表格的CSV是一个非常重要的设计。在检索时我们可能只根据语义相似度召回了一行表格数据。但为了生成更好的答案我们可以通过元数据轻松地将这个分块所属的整个表格都提取出来作为补充上下文提供给LLM。这为后续的“小到大”检索模式奠定了基础。2.2 检索增强生成阶段当索引构建完成后就进入了查询和回答阶段。这个过程同样体现了“布局感知”的优势混合检索当用户提出一个问题时系统首先将问题文本同样向量化然后在OpenSearch的向量索引中进行近似最近邻搜索找到最相似的K个分块例如top 5。然而单纯的向量搜索语义搜索有时会受到“词汇不匹配”或“语义漂移”的干扰。因此一个更健壮的方案是采用混合搜索同时结合向量相似度得分和基于元数据/关键词的文本匹配得分例如BM25。OpenSearch原生支持这种混合检索可以更精准地定位相关片段。动态上下文组装检索到的分块携带了丰富的元数据。系统可以根据查询的复杂程度动态决定提供给LLM的上下文范围。这是本项目的一大亮点基础模式只将检索到的分块文本本身作为上下文。扩展模式如果问题涉及对某个概念的深入理解系统可以额外附加上该分块所属的parent_section子章节的标题和内容。全景模式对于需要宏观背景的问题系统甚至可以附加上整个parent_chapter顶级章节的内容。 这种能力使得系统可以灵活应对从具体事实查询到开放性分析的不同问题类型避免了固定上下文窗口的局限性。提示工程与答案生成将组装好的上下文和用户问题填入预设的提示词模板中。模板会指示LLM“基于以下文档片段回答问题”并可能包含一些指令如“如果信息不足请说明”。最后将完整的提示发送给通过Amazon Bedrock或SageMaker接入的LLM生成最终答案。3. 核心工具链选型与配置实操纸上谈兵终觉浅我们来具体看看如何搭建这个环境。工具链的选择直接决定了方案的可行性、性能和成本。3.1 Amazon Textract布局解析的基石Textract是本项目的起点它的布局分析精度直接决定了后续所有流程的上限。为什么选Textract相比于开源的OCR工具如Tesseract或某些只提供文本坐标的商用APITextract的Layout功能是独一无二的。它不仅能识别文字还能理解“这是一个标题”、“这是一个表格单元格”、“这几项属于同一个列表”。这种语义级别的识别省去了我们自行开发复杂的版面分析算法既准确又省心。实操调用与成本控制API选择对于异步处理多页文档务必使用StartDocumentAnalysis异步API并通过S3 URI指定文档位置。同步APIAnalyzeDocument只适用于单页。功能开关在请求中明确指定FeatureTypes[“TABLES”, “LAYOUT”]。如果不需要表格可以只开LAYOUT能稍微降低成本。配额与权限确保你所在区域的Textract服务有足够的TPS每秒事务数配额。同时执行调用的IAM角色必须对目标S3存储桶有读取权限并对Textract服务有调用权限。使用Textractor库强烈建议使用amazon-textract-textractor库。它提供了Document、Page、Table等高级对象能让你用几行代码就遍历所有布局元素远比直接解析JSON响应高效。安装命令pip install amazon-textract-textractor。3.2 嵌入模型与向量数据库检索的核心引擎检索的效果一半取决于分块质量另一半则取决于嵌入模型和向量数据库。嵌入模型选型通过Amazon SageMaker JumpStart部署嵌入模型是最佳实践。JumpStart提供了大量预训练好的前沿模型如BGE-large-en-v1.5、GTE-large等并提供了现成的推理容器和端点配置。部署步骤在SageMaker控制台进入JumpStart。搜索你心仪的嵌入模型建议选择在MTEB等基准测试中排名靠前的模型。点击“部署”选择一个合适的实例类型如ml.g5.2xlarge对于大型模型。关键点对于嵌入模型推理延迟Latency比吞吐量更重要因为通常是同步调用。g5实例的GPU能显著加速。部署成功后你会得到一个终端节点Endpoint名称通过SageMaker SDK即可调用。调用示例import boto3 import json runtime boto3.client(‘sagemaker-runtime’) response runtime.invoke_endpoint( EndpointName‘your-embedding-endpoint-name’, ContentType‘application/x-text’, Bodychunk_text ) embedding json.loads(response[‘Body’].read())向量数据库选型Amazon OpenSearch Service是AWS生态下的自然选择。它从7.x版本开始原生支持向量索引k-NN索引并且与IAM集成良好便于权限管理。集群配置要点启用k-NN插件在创建域时确保“精细访问控制”中启用了OpenSearch原生权限这通常也意味着k-NN插件可用。索引映射设计这是重中之重。你需要定义一个同时包含向量字段和元数据字段的映射。{ “settings”: { “index.knn”: true, “number_of_shards”: 3, “number_of_replicas”: 1 }, “mappings”: { “properties”: { “chunk_vector”: { “type”: “knn_vector”, “dimension”: 1024, // 必须与你的嵌入模型维度一致 “method”: { “name”: “hnsw”, “space_type”: “cosinesimil”, “engine”: “nmslib” } }, “chunk_text”: {“type”: “text”}, “parent_section”: {“type”: “keyword”}, “parent_chapter”: {“type”: “keyword”}, “element_type”: {“type”: “keyword”} // … 其他元数据字段 } } }dimension必须与你使用的嵌入模型输出维度完全匹配否则无法插入数据。space_typecosinesimil余弦相似度是最常用的对于大多数语义搜索任务效果良好。写入与查询使用OpenSearch的Python客户端opensearch-py进行数据操作。写入时将向量、文本和元数据组成一个文档插入。查询时使用knn查询子句进行向量搜索并可结合bool查询进行元数据过滤。3.3 编排与无服务器架构建议对于生产环境建议采用无服务器架构以提高弹性和降低运维成本文档上传触发用户上传文档到S3 → 触发AWS Lambda函数。异步处理链Lambda调用Textract异步API → Textract处理完成后将结果JSON写入另一个S3路径 → 此事件触发第二个Lambda函数。处理与索引第二个Lambda函数或由Step Functions编排的一组Lambda负责加载Textract结果、调用Textractor库进行分块、调用SageMaker端点生成向量、最后写入OpenSearch索引。查询接口通过Amazon API Gateway暴露一个REST API后端连接Lambda函数处理用户查询执行混合检索并调用Bedrock/SageMaker LLM端点生成答案。4. 布局感知分块算法的深度实现与调优理论部分讲完了我们来深入代码层面看看“布局感知分块”这个核心算法具体怎么实现以及有哪些调优空间。4.1 利用Textractor解析与富文本生成首先我们需要从Textract的原始JSON中构建出结构化的文档对象。from textractcaller import call_textract from textractprettyprinter.t_pretty_print import get_lines_string from textractor import Textractor from textractor.data.constants import TextractFeatures import xml.etree.ElementTree as ET # 初始化Textractor客户端 (假设已配置好AWS凭证) extractor Textractor(profile_name‘default’) # 假设document_s3_uri是S3上的文档地址 document_s3_uri “s3://your-bucket/path/to/document.pdf” # 调用Textract指定LAYOUT和TABLES response extractor.start_document_analysis( file_sourcedocument_s3_uri, features[TextractFeatures.LAYOUT, TextractFeatures.TABLES], s3_upload_path“s3://your-bucket/textract-output/” # 可选指定输出位置 ) # 等待处理完成 document response.get() # 此时document是一个包含所有页面、布局元素的丰富对象 # 我们可以将其转换为带标签的文本 # Textractor的get_text_with_geometry等方法可以获取基础文本和位置 # 但为了得到XML风格的富文本我们需要自己遍历布局元素实际上Textractor库本身不直接输出XML。我们需要根据document.pages[0].layouts等属性来自行遍历。每个Layout对象有type如 ‘TITLE’ ‘HEADER’ ‘TABLE’和text属性。我们可以根据类型手动添加XML标签。一个更实用的方法是利用Textractor提取出结构化的块列表然后用自己的逻辑来组装和分块。4.2 实现差异化分块策略假设我们已经有了一个结构化的块列表structured_blocks每个块包含type,text,page_num,bbox以及从布局分析中推断出的层级关系例如通过bbox的包含关系和阅读顺序推断出的chapter_id,section_id。下面是分块逻辑的伪代码框架def layout_aware_chunking(structured_blocks, max_words_per_chunk500): “”” 基于布局的结构化分块。 structured_blocks: 列表每个元素是一个字典包含type, text, metadata等。 max_words_per_chunk: 每个块允许的最大单词数用于拆分大表格或长段落。 “”” final_chunks [] current_chapter None current_section None # 第一遍建立章节-子章节的映射关系 # 这里需要根据TITLE, HEADER等块的层级来构建文档树。 # 假设我们有一个函数 build_document_tree(blocks) 来完成这个任务。 doc_tree build_document_tree(structured_blocks) # 第二遍遍历所有块根据类型和所属章节进行分块 for block in structured_blocks: if block[‘type’] ‘TITLE’: current_chapter block[‘text’] # 标题本身通常作为一个独立的块或者作为后续块的元数据 chunk_metadata {‘chapter’: current_chapter, ‘section’: None, ‘element_type’: ‘title’} final_chunks.append({‘text’: block[‘text’], ‘metadata’: chunk_metadata}) elif block[‘type’] ‘HEADER’ or block[‘type’] ‘SECTION_HEADER’: current_section block[‘text’] # 章节标题也作为独立块或元数据 chunk_metadata {‘chapter’: current_chapter, ‘section’: current_section, ‘element_type’: ‘header’} final_chunks.append({‘text’: block[‘text’], ‘metadata’: chunk_metadata}) elif block[‘type’] ‘TABLE’: # 处理表格 table_csv convert_table_to_csv(block[‘table_data’]) # 假设有原始表格数据 table_rows table_csv.split(‘\n’)[1:] # 去掉标题行 header_row table_csv.split(‘\n’)[0] # 获取表格前的描述文本需要根据bbox位置从blocks里查找 preceding_text find_preceding_text(block, structured_blocks) chunk_text_accumulator “” for row in table_rows: potential_chunk f“{preceding_text}\n表头{header_row}\n本行数据{row}” # 简单的基于单词数的拆分逻辑 if count_words(chunk_text_accumulator potential_chunk) max_words_per_chunk and chunk_text_accumulator: # 保存当前累积的块 final_chunks.append({ ‘text’: chunk_text_accumulator, ‘metadata’: {‘chapter’: current_chapter, ‘section’: current_section, ‘element_type’: ‘table_chunk’, ‘full_table_csv’: table_csv} }) chunk_text_accumulator potential_chunk else: if chunk_text_accumulator: chunk_text_accumulator ‘\n’ row else: chunk_text_accumulator potential_chunk # 添加最后一个表格块 if chunk_text_accumulator: final_chunks.append({…}) elif block[‘type’] ‘LIST’: # 处理列表每个列表项作为一个块附带列表标题 list_title find_list_title(block, structured_blocks) for item in block[‘list_items’]: chunk_text f“{list_title}\n - {item}” final_chunks.append({ ‘text’: chunk_text, ‘metadata’: {‘chapter’: current_chapter, ‘section’: current_section, ‘element_type’: ‘list_item’} }) elif block[‘type’] ‘PARAGRAPH’: # 处理段落一个段落一个块附带当前章节标题 chunk_text block[‘text’] # 如果段落太长可以按句子进一步拆分但尽量保持段落完整性 if count_words(chunk_text) max_words_per_chunk * 1.5: # 给段落更大的容忍度 sentences split_into_sentences(chunk_text) temp_accumulator “” for sent in sentences: if count_words(temp_accumulator sent) max_words_per_chunk: final_chunks.append({ ‘text’: f“{current_section}\n{temp_accumulator}”, ‘metadata’: {…} }) temp_accumulator sent else: temp_accumulator ‘ ‘ sent if temp_accumulator: final_chunks.append({…}) else: final_chunks.append({ ‘text’: f“{current_section}\n{chunk_text}”, # 将章节标题前置 ‘metadata’: {‘chapter’: current_chapter, ‘section’: current_section, ‘element_type’: ‘paragraph’} }) return final_chunks关键调优点max_words_per_chunk这个参数需要根据你使用的LLM上下文窗口和嵌入模型性能来权衡。太小会导致上下文碎片化太大会降低检索精度并增加LLM处理负担。对于类似GPT-4 128K的模型可以设大一些如1000-1500。对于小模型可能需要在300-500之间。段落拆分策略对于超长段落按句子拆分是次优选择因为可能破坏连贯性。更好的方法是尝试按语义边界如转折词、分号拆分或者干脆不拆作为一个大块处理并在检索时注意。表格和列表的标题查找find_preceding_text和find_list_title函数需要根据布局块的位置bbox的y坐标和阅读顺序来实现确保找到的是真正关联的文本而不是上一页的无关内容。4.3 元数据策略与高级检索准备分块时附带的元数据是为未来留出的扩展接口。除了基本的章节信息我还建议添加page_number来源页码便于溯源和人工核对。confidenceTextract识别该文本块的置信度可用于在检索结果中加权或过滤低置信度内容。file_source原始文档标识。在索引到OpenSearch时这些元数据字段都应被定义为合适的类型keyword,integer,text等。keyword类型适用于精确匹配过滤text类型可支持全文检索。例如你可以这样进行混合查询# 伪代码OpenSearch混合查询 query { “query”: { “bool”: { “must”: [ { “knn”: { “chunk_vector”: { “vector”: question_embedding, “k”: 10 } } } ], “filter”: [ {“term”: {“element_type”: “paragraph”}}, # 可以过滤只检索段落 {“term”: {“parent_chapter”: “财务报告”}} # 可以限定在某个章节 ] } } }5. 实战避坑指南与性能优化在实际部署和测试这个管道时我遇到了不少挑战也总结出一些让系统更稳健、更高效的经验。5.1 准确性陷阱与应对策略Textract布局识别并非100%准确尤其是对于设计花哨、排版非常规的文档标题和正文的识别可能会有误。应对在关键业务场景中可以引入一个“置信度阈值”。对于识别为标题但置信度低的块可以结合其字体大小、位置等启发式规则进行二次判断。或者准备一个小的验证集对Textract的结果进行抽样人工检查针对特定类型的文档进行微调虽然Textract本身不可训练但你的后处理逻辑可以适配。分块边界模糊有时一个逻辑段落会被Textract错误地拆分成两个PARAGRAPH块。应对在后处理时可以检查相邻的PARAGRAPH块。如果它们在页面位置上非常接近例如y坐标差值小于某个阈值并且结尾/开头没有句号等明显句子结束标志可以考虑将它们合并成一个块。表格内容提取不完整复杂表格如嵌套表头、跨页表格的提取可能会丢失信息。应对对于非常重要的表格可以考虑启用Textract的QUERIES功能。你可以预先定义一些问题如“提取第三季度的总收入”让Textract以键值对的形式返回答案作为对常规表格提取的补充。5.2 性能与成本优化异步处理与批处理处理大量文档时切勿同步调用Textract。务必使用StartDocumentAnalysis并结合S3事件和Lambda或Step Functions构建异步管道。对于大量小文档可以考虑在Lambda中批量提交注意Textract的异步API也有批量限制。向量索引优化选择合适的k-NN方法OpenSearch支持hnsw和ivf两种近似算法。hnswHierarchical Navigable Small World通常召回率更高但索引构建慢、内存占用大。ivfInverted File Index构建快、内存占用小但召回率可能略低。对于文档检索这种对召回率要求较高的场景通常首选hnsw。调整HNSW参数ef_construction和m参数影响索引构建的质量和速度。增加它们可以提高召回率但也会增加构建时间和内存。需要在质量和效率间权衡。可以从默认值开始在测试集上调整。分片策略OpenSearch索引由多个分片组成。分片数在创建索引时设定之后不能更改。一个经验法则是确保每个分片的大小在10GB到50GB之间。如果你的向量数据量预计很大需要提前规划好分片数。嵌入模型端点优化实例自动缩放为SageMaker端点配置自动缩放策略。根据每分钟的调用次数InvocationsPerInstance来动态增加或减少实例数量可以在流量低谷时节省成本。批量嵌入如果是在离线构建索引不要逐条调用端点。将多个文本分块组合成一个批次Batch进行推理可以极大提高吞吐量降低成本。SageMaker端点支持批量请求。缓存策略查询缓存对于频繁出现的、通用的用户查询可以考虑在Lambda前面加上Amazon ElastiCacheRedis/Memcached来缓存查询向量和Top K结果。这能显著降低对OpenSearch和LLM的重复调用。嵌入缓存甚至可以对常见的文档分块文本进行预计算并缓存其向量避免在索引更新时重复计算。5.3 可观测性与监控一个生产级的系统必须可监控。关键指标流水线延迟从文档上传到完成索引的总时间。拆分为Textract处理时间、嵌入时间、OpenSearch写入时间。Textract API错误率与置信度分布。OpenSearch索引延迟与搜索延迟。LLM生成延迟与令牌使用量。端到端问答准确率需要人工或通过验证集定期评估。实现方式利用Amazon CloudWatch来收集所有服务的原生指标如Lambda执行时长、SageMaker端点延迟。在业务代码中使用put_metric_dataAPI发送自定义业务指标如分块数量、平均块大小、检索召回率等。为所有Lambda函数和Step Functions状态机设置详细的日志记录并导入CloudWatch Logs以便排查问题。6. 效果评估与迭代方向搭建好管道只是第一步持续评估和迭代才能让系统真正产生价值。6.1 如何评估RAG系统的好坏不能只看最终答案“看起来”对不对需要多维度评估检索阶段评估召回率对于一个已知答案的问题系统检索到的Top K个分块中包含正确答案源文本的比例是多少这是衡量检索能力的基础。精确率检索到的Top K个分块中有多少是真正相关的可以通过人工标注来判断。对比实验将“布局感知分块”与“固定大小滑动窗口分块”在同一个测试集上进行对比看前者的召回率和精确率是否有显著提升。生成阶段评估忠实度LLM生成的答案是否严格基于提供的上下文有没有“胡编乱造”幻觉答案相关性生成的答案是否直接、完整地回答了问题可读性与连贯性答案是否通顺、符合逻辑 这部分评估通常需要人工进行或者利用更强大的LLM作为裁判进行打分。6.2 可能的迭代与扩展方向多模态RAG当前方案主要处理文本和表格。如果文档中包含大量信息丰富的图表Figures可以扩展流程。利用Textract提取出的图形位置信息调用多模态模型如CLIP为图表生成描述性文本然后将这些描述作为特殊的分块与文本分块一起索引。在检索时可以实现图文联合检索。查询理解与重写在用户查询进入检索之前可以先用一个轻量级LLM对查询进行理解和重写。例如将“去年赚了多少钱”重写为“2023年净利润是多少”并提取出可能的过滤条件如“在财务报告章节中查找”从而提升检索的精准度。递归检索与智能路由实现更复杂的检索策略。例如先进行一轮粗检索如果返回的分块元数据显示它们属于多个不同的章节且答案可能需要对不同章节进行综合则可以触发第二轮检索专门去获取这些章节的摘要或核心段落进行“递归”式的信息搜集。Agentic RAG将整个RAG系统嵌入到一个智能体框架中。让LLM不仅生成最终答案还能决定检索策略例如“我需要先查一下定义再找一个例子”甚至能对检索结果进行批判性思考决定是否需要进一步检索或要求用户澄清问题。这个基于布局感知的文档处理与RAG方案从一个被忽视的维度——文档结构——入手显著提升了复杂文档的信息提取和问答质量。它不再是简单的“文本转向量”而是“理解、重构、再利用”的完整知识工程流程。虽然初始搭建涉及多个云服务和复杂的处理逻辑但一旦跑通其带来的精度提升和对业务场景的贴合度是传统方法难以比拟的。在实际操作中耐心调试分块策略和检索参数并建立有效的评估闭环是成功的关键。