上一篇当中介绍了将 PDF 转换为 Markdown 的过程本文将介绍 RAG 中 PDF 文档处理和存储的第二阶段Markdown 文档的分块处理与入库。本文提供的实现过程和代码既可用于对通过上一篇由 PDF 转换得到的 Markdown 的进一步处理也可用于直接对 Markdown 原件进行分块处理和入库。一、总体分块策略1.1 总体分块步骤第一步按标题进行结构化拆分函数首先使用MarkdownHeaderTextSplitter工具根据 Markdown 的标题层级即#,##,###将文档拆分成多个大的“章节块”chunks。这样做的好处是保留了文档的原始结构确保每个分块都带有其所属的标题信息作为元数据使得内容在语义上更加完整。第二步对长章节进行滑动窗口拆分对于上一步生成的“章节块”如果其内容长度超过了预设的chunk_size函数会对其进行二次拆分。采用滑动窗口的方法将长文本按句子进行切分并组合成符合chunk_size要求的小块。为了避免上下文断裂相邻的两个分块之间会保留chunk_overlap长度的重叠内容。1.2 特殊元素的识别与独立处理表格和算法块这些元素会被从正文中识别并提取出来作为独立的Document对象进行处理。它们的metadata中的type字段会被分别标记为table和algorithm并赋予唯一的id。在原来的正文位置会留下一个占位符如{{table_id}}以表明此处曾有一个表格或算法块。代码块函数会根据代码块的长度采取不同策略。短代码块直接保留在原始文本中不进行独立拆分。长代码块与表格和算法块类似会被提取为独立的Document对象type标记为code并在原文中留下占位符如{{Code 0}}。图片函数会预先扫描整个文档提取所有图片的编号、名称和描述信息并存储起来。在拆分文本时它会分析每个文本块识别出其中引用的图片并将这些图片的元数据记录在该文本块的dependencies元数据字段中。二、分块部分的代码实现2.1 markdown_splitter 函数定义、参数处理和有效性检查def markdown_splitter(document_path: str, rename: strNone, chunk_size500, chunk_overlap100, extra_metadata: Dict[str, int|float|str|None]None): Markdown 分块处理先按标题分块对于长度超过 chunk_size 的章节再进行滑动分块。 Args: document_path: Markdown 文档路径 rename: 文档别名 chunk_size: 分块的最小长度 chunk_overlap: 重叠部分的最小长度 extra_metadata: 文档元数据例如版本号、修订时间、管理人、权限等 Returns: 处理后的 Document 列表 if extra_metadata is None: extra_metadata dict() elif not extra_metadata.get(authority): extra_metadata[authority] 0 # 默认权限为 0公开生产环境不建议设置较低默认值 if not os.path.isfile(document_path): raise FileNotFoundError(f文件 {document_path} 不存在) if not document_path.endswith(.md): raise ValueError(f文件 {document_path} 不是 markdown 文件)2.2 读取 Markdown 文档对图片统一提取和处理# 接续markdown_splitter函数 # 读取 Markdown 文档 with open(document_path, r, encodingutf-8) as file: markdown_text file.read() # {图 x-y: {name: 图片名称, description: 图片描述}, ...} figures: Dict[str, Dict[str, str]] dict() # 先对文中图片统一提取和处理 splitlines markdown_text.splitlines() for i, splitline in enumerate(splitlines): if splitline.startswith(![): figure_description re.findall(r!\[([^]])], splitline)[0] j i1 while jlen(splitlines) and not splitlines[j]: j 1 if jlen(splitlines): continue # 图片位于文档末尾无法提取编号 if splitlines[j].startswith((*图, *Fig, *FIG)): figure_id, figure_name re.findall(r\*([^*])\*[\s|:]*(.*), splitlines[j])[0] elif re.match(r(?:图\s*|figure |fig.\s*)[0-9A-Z.\-], splitlines[j], re.IGNORECASE): figure_id, figure_name re.findall(r((?:图\s*|figure |fig.\s*)[0-9A-Z.\-])[\s|:]*(.*), splitlines[j], re.IGNORECASE)[0] else: continue # 无编号图片无需提取和处理 if not figure_id: continue # 无编号图片无需提取和处理 if figure_id[-1] .: figure_id figure_id[:-1] figures[figure_id] {name: figure_name, description: figure_description}2.3 第一轮分块按标题分块MarkdownHeaderTextSplitter# 接续markdown_splitter函数 # 第一步按标题分块 headers_to_split_on [(#, Header 1), (##, Header 2), (###, Header 3), (####, article_id)] splitter MarkdownHeaderTextSplitter(headers_to_split_onheaders_to_split_on) chunks0 splitter.split_text(markdown_text)2.4 第二轮分块较长章节滑动分块总体和每个大块分块前的准备# 接续markdown_splitter函数 # 第二步较长章节滑动分块 chunks1: List[Document] [] # 处理后的 Document 列表 table_list: Dict[str, Document] dict() # 表格列表 algorithm_list: Dict[str, Document] dict() # 算法块列表 code_list: List[Document] [] # 代码块列表 for chunk in tqdm(chunks0): page_content page_content1 # 滑动分块重叠部分 # 相关图片、表格、算法、代码标记存入 metadata 的 dependencies字段 related_figures: Dict[str, Dict[str, str]] dict() related_tables: List[str] [] related_algorithms: List[str] [] related_codes: List[int] [] # 大块章节元数据 metadata chunk.metadata # 文档级元数据 for k, v in extra_metadata.items(): metadata[k] v if metadata.get(Header 1, ).lower() in (目录, 目 录, table of contents): continue metadata[document_name] document_name逐个段落区分类型处理表格检测前面是否有表名将表格单独划分为Document并存入表格列表在正文相应位置插入占位符算法块检测编号将算法块单独划分为Document并存入算法块列表在正文在相应位置插入占位符代码块短代码直接插入正文长代码则单独划分为Document存入代码列表普通文本提取文本中的图片、表格、算法编号列入Metadata中的依赖项字段然后使用spacy将段落划分为若干句子随后将文本块逐句加入到 page_content 缓冲区当中当 page_content 的长度大于等于 chunk_size - chunk_overlap 时进入块间重叠部分 page_content1文本块存入重叠部分当重叠部分page_content1 的长度大于等于 chunk_overlap 时将两个缓冲区内容合并并添加元数据和依赖项信息作为文档的一个 Document 分块对象。# 接续 markdown_splitter - for chunk in tqdm(chunks0): paragraphs chunk.page_content.split(\n) i 0 while ilen(paragraphs): paragraph paragraphs[i].strip() #logger.info(paragraph) if not paragraph or (paragraph[0] * and paragraph[:2] ! **): i 1 continue # 跳过图名、表名段落 # 表格 elif paragraph[0] |: # 检测表名 table_name paragraphs[i-1] if i0 else i1 while ilen(paragraphs) and paragraphs[i][0] |: paragraph \n paragraphs[i] i 1 i - 1 if table_name[:2] in (*表, *Tab, *TAB): table_id re.findall(r\*([^*])\*, table_name)[0] if table_id[-1] .: table_id table_id[:-1] table_list[table_id] Document(table_name \n paragraph, metadatametadata) else: match re.match(r(?:表\s*|table |tab.\s*)[0-9A-Z.\-], table_name, re.IGNORECASE) if match: table_id match.group() if table_id[-1] .: table_id table_id[:-1] table_list[table_id] Document(table_name \n paragraph, metadatametadata) else: # 无名表格 table_id fUnnamed table {len(table_list)} table_list[table_id] Document(paragraph, metadatametadata) # 插入表格占位符 sentences [f{{{{{table_id}}}}}] # 算法块 elif paragraph.startswith(*****Algorithm Block*****): while not paragraph.strip().endswith(*****End Algorithm*****): i1 paragraph \n paragraphs[i] paragraph paragraph[25:-23].strip() # 检测算法编号 algorithm_id re.match(r算法\s?[0-9A-Z.\-]|algorithm [0-9A-Z.\-], paragraph, re.IGNORECASE).group() if algorithm_id[-1] .: algorithm_id algorithm_id[:-1] algorithm_list[algorithm_id] Document(paragraph, metadatametadata) # 插入算法占位符 sentences [f{{{{{algorithm_id}}}}}] # 代码块 elif paragraph.startswith(): while len(paragraph.strip())6 or not paragraph.strip().endswith(): i1 paragraph \n paragraphs[i] # 短代码直接插入正文长代码则存入代码列表 if len(paragraph) 100: related_codes.append(len(code_list)) sentences [f{{{{Code {len(code_list)}}}}}] code_list.append(Document(paragraph, metadatametadata)) else: sentences [paragraph] # 普通文本可能包含前述未涵盖的其他特殊类型 else: # 处理图片、表格、算法标号 for figure_id in re.findall(rFigure [0-9A-Z.\-]|图\s*[0-9A-Z.\-], paragraph): if figure_id[-1] .: figure_id figure_id[:-1] if figures.get(figure_id): related_figures[figure_id] figures[figure_id] for table_id in re.findall(rTable [0-9A-Z.\-]|表\s*[0-9A-Z.\-], paragraph): if table_id[-1] .: table_id table_id[:-1] related_tables.append(table_id) for algorithm_id in re.findall(rAlgorithm [0-9A-Z.\-]|算法\s*[0-9A-Z.\-], paragraph): if algorithm_id[-1] .: algorithm_id algorithm_id[:-1] related_algorithms.append(algorithm_id) doc1 nlp(paragraph) sentences [sent.text for sent in doc1.sents] # 分句 # 逐句加入文本块 for sentence in sentences: if sentence[-1] in .?!:\”)]: # 给英文标点后面加空格 sentence if len(page_content) chunk_size - chunk_overlap: page_content sentence else: if len(page_content1) chunk_overlap: page_content1 sentence else: metadata[type] text # 添加依赖信息 dependencies: Dict[str, Any] dict() dependencies[related_figures] related_figures dependencies[related_tables] related_tables dependencies[related_algorithms] related_algorithms dependencies[related_codes] related_codes metadata[dependencies] json.dumps(dependencies, ensure_asciiFalse) chunks1.append(Document(page_content page_content1, metadatametadata)) page_content page_content1 sentence page_content1 related_figures dict() related_tables [] related_algorithms [] related_codes [] if len(page_content) chunk_size - chunk_overlap: page_content \n else: if len(page_content1) chunk_overlap: page_content1 \n i 1全部大块处理完毕后处理缓冲区中残余内容# 接续 markdown_splitter - for chunk in tqdm(chunks0): # 如果大块已经读取完毕page_content仍然有剩余未保存的内容则保存 if page_content: metadata[type] text # 添加依赖信息 dependencies: Dict[str, Any] dict() dependencies[related_figures] related_figures dependencies[related_tables] related_tables dependencies[related_algorithms] related_algorithms dependencies[related_codes] related_codes metadata[dependencies] json.dumps(dependencies, ensure_asciiFalse) chunks1.append(Document(page_content page_content1, metadatametadata))2.5 表格、算法、代码块统一处理保存为 Document 对象# 接续markdown_splitter函数 # 表格、算法、代码块统一处理 logger.info(正在处理表格...) for table_id, table_document in table_list.items(): table_document.metadata[type] table table_document.metadata[id] table_id chunks1.append(table_document) logger.info(正在处理算法块...) for algorithm_id, algorithm_document in algorithm_list.items(): algorithm_document.metadata[type] algorithm algorithm_document.metadata[id] algorithm_id chunks1.append(algorithm_document) logger.info(正在处理代码块...) for i, code_document in enumerate(code_list): code_document.metadata[type] code code_document.metadata[id] fCode {i} chunks1.append(code_document) logger.info(f文档切片完毕共 {len(chunks1)} 个大块) return chunks1三、分块部分的本地测试if __name__ __main__: parser argparse.ArgumentParser(descriptionPDF文档分块处理工具) # 必填参数 parser.add_argument(document_path, typestr, helpPDF文档路径) # 可选参数 parser.add_argument(--rename, typestr, defaultNone, help文档别名) parser.add_argument(--chunk-size, typeint, default500, help分块的最小长度 (默认: 500)) parser.add_argument(--chunk-overlap, typeint, default100, help重叠部分的最小长度 (默认: 100)) parser.add_argument(--title-format, typestr, nargs, defaultNone, help标题格式列表例如: 第一章 一、 1. ) parser.add_argument(--extra-metadata, typestr, defaultNone, help额外元数据JSON格式字符串例如: \{version: 1.0, authority: 1}\) parser.add_argument(--output, typestr, defaulttemp/output.md, help输出文件路径 (默认: temp/output.md)) args parser.parse_args() # 解析 extra_metadata JSON 字符串为字典 extra_metadata None if args.extra_metadata: try: extra_metadata json.loads(args.extra_metadata) except json.JSONDecodeError as e: raise ValueError(fextra-metadata 必须是有效的JSON格式: {e}) # 调用 pdf_splitter documents pdf_splitter( document_pathargs.document_path, renameargs.rename, chunk_sizeargs.chunk_size, chunk_overlapargs.chunk_overlap, title_formatargs.title_format, extra_metadataextra_metadata ) # 输出结果 import os os.makedirs(os.path.dirname(args.output), exist_okTrue) with open(args.output, w, encodingutf-8) as output: for i, document in enumerate(documents): output.write(f\n\n# Document {i 1}:) output.write(\n\n**Page Content:**\n{}.format( document.page_content.replace(\n, \n\n).replace(|\n\n|, |\n|) )) output.write(f\n\n**Metadata:**\n{document.metadata}) print(f处理完成共生成 {len(documents)} 个文档块输出至: {args.output})使用方法示例# 基本用法 python data_splitter.py path/to/document.pdf # 完整参数 python data_splitter.py path/to/document.pdf \ --rename 我的文档 \ --chunk-size 800 \ --chunk-overlap 150 \ --title-format 第一章 一、 1. \ --extra-metadata {version: 2.0, authority: 1} \ --output result/output.md四、文档入库4.1 入库策略1. 预处理基于内容指纹的去重在向数据库写入任何数据之前函数会首先对传入的文档块列表进行一次内部去重。去重依据它并非简单地比较整个文档对象而是提取每个文档块的document_name文档名和page_content文本内容组合成一个唯一的“内容指纹”content_fingerprint。处理逻辑函数会遍历所有文档块如果一个“内容指纹”是首次出现则保留该文档块如果该指纹已存在则视为重复内容并跳过。目的确保进入后续存储流程的文档块列表本身没有重复项避免了因上游处理不当导致的数据冗余。2. 索引生成基于内容的唯一ID为了实现精准的数据同步函数为每个文档块生成了一个稳定且唯一的ID。ID生成方式使用SHA256哈希算法。哈希内容将document_name和page_content拼接后进行哈希计算。策略优势这种策略保证了只要文档名和内容不变生成的ID就永远不变。这是实现后续“增量更新”和“精准删除”的基石。即使文档块在列表中的顺序发生变化其ID依然稳定。3. 同步智能的增、删、改策略核心部分检测目标 Chroma 数据库是否存在并据此采取不同的同步策略。如果目标目录中没有检测到现有的数据库函数会直接调用Chroma.from_documents方法将所有传入的文档块一次性全部存入创建一个全新的向量数据库。当检测到目标目录中已有数据库时函数会执行一套完整的同步逻辑而不是盲目地覆盖或追加。精准删除识别目标首先确定本次操作涉及哪些文档通过incoming_doc_names集合。执行逻辑遍历数据库中所有已存在的文档块如果某个文档块满足以下两个条件就会被标记为“待删除”它属于本次正在更新的文档即其document_name在incoming_doc_names中。它的内容ID基于旧内容生成不在本次新生成的ID列表中。目的这能精准地清理掉原文档中已被删除或修改的旧段落保证数据库内容与最新文档保持一致。精准新增识别目标在执行删除操作后检查数据库中当前剩余的所有ID。执行逻辑将本次新生成的ID列表与数据库中现有的ID列表进行比对。只有那些在数据库中完全不存在的新ID才会被当作“待新增”的内容。目的这确保了只有真正新增的段落或被修改过的段落其内容变化导致ID也变化才会被向量化并写入数据库避免了对未变动内容的重复计算和存储极大地提升了更新效率。4.2 代码实现my_chroma.pyimport hashlib import os from typing import List, Set, Dict from langchain_chroma import Chroma from langchain_core.documents import Document from langchain_core.embeddings import Embeddings from summary import doc_summary, markdown_summary def textblock_chroma(documents: List[Document], embedding_model: Embeddings, save_dir: str | Nonechroma) - Chroma | None: 将分块后的文档存入 Chroma 数据库支持按 document_name 增量更新/覆盖 Args: documents: 分块后的文档 embedding_model: 文本嵌入模型例如 OpenAIEmbeddings、DashScopeEmbeddings、OllamaEmbeddings、HuggingFaceEmbeddings等 save_dir: 存储目录 Returns: 向量数据 if not documents: print(传入的文档列表为空跳过处理。) return None # --- 修复基于 ID 生成规则进行去重 (精准去重) --- # 核心思想去重的判断标准必须和 generate_ids 的逻辑完全一致 seen_content_keys set() unique_docs [] for doc in documents: # 1. 提取用于生成 ID 的核心要素 (与 generate_ids 函数内部逻辑保持一致) content doc.page_content doc_name doc.metadata.get(document_name, unknown) # 2. 创建一个“内容指纹”仅包含影响 ID 生成的因素 content_fingerprint (doc_name, content) # 3. 如果这个“指纹”没有出现过则保留该文档 if content_fingerprint not in seen_content_keys: seen_content_keys.add(content_fingerprint) unique_docs.append(doc) else: # 可选打印被忽略的重复项信息方便调试 print(f发现重复内容块 (文档: {doc_name}, 内容: {content[:15]}...), 已跳过。) print(f原始文档数量: {len(documents)}去重后文档数量: {len(unique_docs)}) documents unique_docs # --- 去重结束 --- if save_dir documents_summary: print(当前使用的向量数据库名称 documents_summary 为文档摘要专用已自动切换为默认名称 chroma。) save_dir chroma if save_dir and not os.path.isdir(save_dir): os.mkdir(save_dir) def generate_ids(documents): 为文档列表生成基于内容的唯一哈希ID ids [] for doc in documents: # 使用文档内容生成哈希确保相同内容ID相同 content doc.page_content.encode(utf-8) # 建议将 document_name 也纳入哈希计算防止不同文档出现完全相同的段落导致ID冲突 doc_name doc.metadata.get(document_name, unknown).encode(utf-8) unique_content doc_name b||| content doc_id hashlib.sha256(unique_content).hexdigest() ids.append(doc_id) return ids print(正在构建内容索引...) # 2. 为传入的 documents 生成 ID new_ids generate_ids(documents) new_contents [doc.page_content for doc in documents] new_metadatas [doc.metadata for doc in documents] # 提取本次涉及的所有 document_name去重 incoming_doc_names: Set[str] set( doc.metadata.get(document_name) for doc in documents if doc.metadata.get(document_name) ) if save_dir and os.path.isfile(os.path.join(save_dir, chroma.sqlite3)): # --- 情况 A: 数据库已存在 --- print(f检测到现有数据库正在按 document_name 同步数据...) vectorstore Chroma(persist_directorysave_dir, embedding_functionembedding_model, collection_namedocument_fulltext) # 2. 获取数据库中现有的所有 ID 和 metadata existing_items vectorstore.get(include[metadatas]) existing_ids_all existing_items[ids] existing_metadatas existing_items[metadatas] # 3. 【精准删除逻辑】找出需要淘汰的旧段落 # 规则属于本次更新的文档且其 ID 不在本次的新数据列表中 ids_to_delete [] for i, meta in enumerate(existing_metadatas): doc_name meta.get(document_name) if meta else None # 只有当该向量属于本次更新的文档且它的指纹(new_ids)没有出现在新版本中时才视为被删除的旧章节 if doc_name in incoming_doc_names and existing_ids_all[i] not in new_ids: ids_to_delete.append(existing_ids_all[i]) if ids_to_delete: print(f发现 {len(ids_to_delete)} 个被淘汰的旧段落如被删减的章节正在清理...) vectorstore.delete(idsids_to_delete) # 4. 【精准新增逻辑】只向量化并写入真正的新增/修改段落 # 获取数据库里目前已有的所有 ID包含刚才没被删的同名段落 current_db_ids set(vectorstore.get(include[])[ids]) # 只有本次生成的新 ID 中那些数据库里完全不存在的才需要执行 add ids_to_add [id for id in new_ids if id not in current_db_ids] if ids_to_add: # 根据 ids_to_add 过滤出对应的文本和元数据 id_to_content_map dict(zip(new_ids, new_contents)) id_to_meta_map dict(zip(new_ids, new_metadatas)) add_contents [id_to_content_map[i] for i in ids_to_add] add_metadatas [id_to_meta_map[i] for i in ids_to_add] print(f正在向量化并添加 {len(ids_to_add)} 个新段落如新增的章节或改动的内容...) vectorstore.add_texts( textsadd_contents, metadatasadd_metadatas, idsids_to_add ) else: print(所有段落均为最新版本无需新增或修改。) else: # --- 情况 B: 数据库不存在直接创建 --- print(未检测到数据库正在创建新数据库...) vectorstore Chroma.from_documents( documentsdocuments, embeddingembedding_model, persist_directorysave_dir, # 注意这里直接传目录即可Chroma会自动处理文件名 collection_namedocument_fulltext ) if save_dir else Chroma.from_documents( documentsdocuments, embeddingembedding_model, collection_namedocument_fulltext ) return vectorstore五、可选功能提取摘要和目录存入文档级向量库当向量库中文档较多时如果直接针对所有文档进行检索大量与用户查询无关的文档将明显降低检索的效率。本节考虑了先筛选文档缩小检索范围再在筛选出的文档中检索分块的方式在进行文档分块处理之前先提取文档的摘要和附录存入文档级向量数据库。在markdown_splitter函数的“nlp spacy.load(zh_core_web_sm)”前面添加# 先提取摘要和目录存入文档级向量库 # 向量模型可根据需要选用OpenAIEmbeddings、DashScopeEmbeddings、OllamaEmbeddings、HuggingFaceEmbeddings等 document_chroma(document_path, embedding_model OllamaEmbeddings(modelqwen3-embedding:0.6b, base_urlhttp://localhost:11434), renamerename, extra_metadataextra_metadata)my_chroma.py中对document_chroma函数的实现这里还考虑了docx等其他格式文档的处理不在本文讨论范围内def document_chroma(document_path: str, rename: str None, extra_metadata: Dict[str, int | float | str | None] None, save_dirdocuments_summary): 摘要数据存储 if not os.path.isfile(document_path): print(f文件 {document_path} 不存在) return if document_path.endswith(pdf): print(f文件 {document_path} 是 PDF 文件为了准确提取摘要内容请先转换为 Markdown 格式。) return if document_path.endswith((docx, doc)): summary_document doc_summary(document_path, renamerename, extra_metadataextra_metadata) # 这一段是本文主要涉及的 markdown 文档 elif document_path.endswith(md): summary_document markdown_summary(document_path, renamerename, extra_metadataextra_metadata) else: print(f暂不支持的文件格式 {document_path}) return vector_store Chroma( collection_namedocument_summaries, # 集合名称可根据需求自定义 embedding_functionembedding_model, persist_directorysave_dir ) # 2. 将新提取的摘要文档添加到现有的向量数据库中 # add_documents 方法会在原有数据的基础上进行追加不会删除原有数据 vector_store.add_documents([summary_document]) print(f成功将文档 {document_path} 的摘要和目录存入向量数据库。)其中 markdown_summary 是为 Markdown 文档创建摘要目录数据的函数在 summary.py 中实现# summary.py import os from typing import Dict from langchain_core.documents import Document from langchain_text_splitters import MarkdownHeaderTextSplitter def markdown_summary(document_path: str, rename: strNone, extra_metadata: Dict[str, int|float|str|None]None): 为 Markdown 文档创建摘要数据 metadata extra_metadata or dict() document_name rename or os.path.basename(document_path) metadata[document_name] document_name with open(document_path, r, encodingutf-8) as file: markdown_text file.read() headers_to_split_on [(#, Header 1), (##, Header 2), (###, Header 3)] splitter MarkdownHeaderTextSplitter(headers_to_split_onheaders_to_split_on) chunks splitter.split_text(markdown_text) extracted_toc [] # 存储目录结构 title_buffer [, , ] # 用于检测是否有新的标题 summary # 摘要内容 alterative_summary # 备选摘要内容前部一定长度内容 for chunk in chunks: # 目录的生成 h1 chunk.metadata.get(Header 1, ) if h1 and h1! title_buffer[0]: title_buffer[0] h1 extracted_toc.append(- h1) h2 chunk.metadata.get(Header 2, ) if h2 and h2! title_buffer[1]: title_buffer[1] h2 extracted_toc.append( - h2) h3 chunk.metadata.get(Header 3, ) if h3 and h3! title_buffer[2]: title_buffer[2] h3 extracted_toc.append( - h3) # 摘要的生成优先提取“前言”或“摘要”部分 if h1.lower() in (前言, 前 言, forward, 摘要, 摘 要, abstract)\ or h2.lower() in (前言, 前 言, forward, 摘要, 摘 要, abstract)\ or h3.lower() in (前言, 前 言, forward, 摘要, 摘 要, abstract): summary chunk.page_content \n # 未遇到过“前言”或“摘要”部分则先提取正文 elif not summary: alterative_summary chunk.page_content \n # 文档不含“前言”或“摘要”部分则从正文中提取前 5000 个字符 if not summary: summary alterative_summary[:5000] page_content # 摘要\n{}\n# 目录\n{}.format(summary, \n.join(extracted_toc)) return Document(page_contentpage_content, metadatametadata)本文源代码获取方式git clone https://gitee.com/dsy0221/my-rag.git