Dify 实现长文档自定义切片:高效处理大规模文档的智能解决方案
在本文之前我制作了合同审查和知识库智能入库助手两个智能体,后续使用以及和粉丝沟通中发现,之前设计切片功能无法支持长文档。而长文档本身又是实际业务中常见的内容,因此对于长文档的支持至关重要。
01 长文档处理中的痛点
在实际使用Dify处理长文档的过程中,我遇到了以下两个问题:
1. 处理大文档时的限制
Dify的HTTP节点的输出内容不能超过1MB,因此当文件内容超出这个量级,无法通过预处理缓存到本地,然后使用时HTTP节点读取的方式获取完整文本内容。
2. LLM 节点的输入限制
Dify中的LLM节点存在输入和输出 token 数量的限制。在长文档场景下,当文档超过模型的输入输出限制时,会出现参数丢失或结果丢失内容的情况,导致输出不完整或者丧失关键信息。这在合同审查等需要处理大量文本的场景中,极大地影响了效率和准确性。
这些问题在日常应用中时常出现,尤其在处理法律类文档、合同条款时,解决文档的高效切分、处理和标注就显得尤为重要。
02 实现长文档的切片和标注
经过调试,最终的流程大致如下,先对文档按长度进行分段,缓存后使用Dify的LOOP节点循环读取并切片,最后增量缓存得到最终切片结果。
Dify中的节点结构如下:
接下来我们对该部分节点进行拆解:
1.预处理节点
该节点为一个HTTP节点,主要功能是将文档按照长度切分为多个分段并以jsonl格式缓存。主要参数为长文档内容缓存路径,每一段的长度以及冗余的首尾部分内容长度,参数配置如下:
核心代码:
def extract_chunks_to_file(self,text: str,context_len: int = 1000, head_len: int = 100, tail_len: int = 100) -> List[Dict]: """ 切片长文本,按 JSONL 格式写入文件。返回文件路径和总切片数(行数)。 :param text: 原始长文本 :param config: 切片参数,包括 context_len、head_len、tail_len :param output_path: 输出文件路径 :return: {"file_path": ..., "line_count": ...} """ output_path = self.get_cache_path("chunk_cache.json") idx = 0 line_count = 0 with open(output_path, "w", encoding="utf-8") as f: while idx < len(text): chunk = text[idx:idx + context_len] head = text[max(0, idx - head_len):idx] tail = text[idx + context_len:idx + context_len + tail_len] item = { "text": chunk.strip(), "head": head.strip(), "tail": tail.strip() } f.write(json.dumps(item, ensure_ascii=False) + "\n") idx += context_len line_count += 1 return { "file_path": output_path, "line_count": line_count }
处理结果缓存为jsonl格式,每行为一段数据,便于后续LOOP节点使用:
2.参数赋值节点
该节点主要对几个会话变量赋值,用于后续LOOP节点中各个节点使用,以下是各个参数的解释:
pre_process_filepath:预处理文件缓存地址
segment_loop_count:需要循环的次数
filename:上传的文件名,循环中无法直接使用初始参数赋值,因此使用会话变量控制
segment_loop_index:循环的索引字段,用于控制每次循环从预处理缓存文件中读取内容。
3.循环节点
该节点是文档切片的核心处理节点,循环终止条件通过会话变量控制,如下:
4.循环内部-读取预处理结果文件节点
该节点主要通过segment_loop_index读取jsonl中的指定行,然后将读取结果交由后续的切片节点以及参数提取节点使用。读取脚本内容如下:
def read_chunk_by_line(self, file_path: str, line_num: int): if not os.path.exists(file_path): return {"error": f"File not found: {file_path}"} try: with open(file_path, "r", encoding="utf-8") as f: for i, line in enumerate(f): if i == line_num: return json.loads(line) return {} # 超出范围,返回空对象 except Exception as e: return {"error": str(e)}
5.循环内部-最新的标题信息参数提取节点
该节点主要用于提取当前段文本所属的模块信息,用于元数据标注,由于预处理时直接按照长度截取,因此可能出现整段没有章节信息的情况,因此通过该节点+会话变量实现章节信息的缓存。
提示词如下(需根据实际需求微调):
你是一个文档结构分析助手,任务是从一段文本中识别其所属的章节结构,并输出该段落最接近的「大标题」和「小标题」。请按以下规则进行结构提取:1. 文档结构可能包含: - 大标题(如“第一章 总则”、“第二部分 特殊条款”) - 小标题(如“第一节 范围”、“第二节 适用对象”)2. 请提取该段中出现的最新大标题与小标题: - 若该段出现了新的章(如“第二章”),则刷新 `major_title`; - 若同段未出现节,则 `minor_title` 置为空; - 若该段只出现节(如“第一节”),则更新 `minor_title`,保留之前的 `major_title`; - 若该段未出现任何结构标题,则返回空对象(不推测上下文)。3. 不要识别“第x条”或内容标题,仅识别章 / 节 或同类结构(如“二、”、“2.”)作为章节。4. 如果段落存在多个结构标题(如同时有“第二章” 和 “第一节”),则都提取出来。5. 如果段落中没有大小结构标题,则不修改其值。6. 如果段落中只有小标题,没有大标题,则不修改大标题,只修改小标题的值。请参考以下输入:输入文本:""""""当前最新的大标题:""""""当前最新的小标题:""""""
6.循环内部-文本切片节点
该节点为实际进行切片操作的节点,实现对每一段进行更细粒度的切分。
提示词如下(需根据实际需求微调):
你是一个擅长文档结构识别与内容提取的专家,任务是接收一段文本正文(包含上下文片段 head、text、tail),并依据文档是否具备结构化格式进行智能分段,并完成信息标注。### 参考输入需要处理的文本内容:""""""规则文件内容:""""""最新的大小标题:"""大标题:小标题:"""### 你的目标如下:1. 判断当前文本片段是否属于结构化文档(如合同、法律文件等)。 - 若包含明显结构符号(如“第X章”、“第X条”、“一、”、“1.”、“1.1”、“(1)”等),视为结构化; - 否则视为非结构化文本。2. 对结构化文档: - 按照最小结构单位进行切分(例如:“第12条”、“1.1”、“三、”等); - 多个结构块请逐条输出,每条之间用分隔符 `---` 隔开。3. 对非结构化文档: - 按照语义+长度切分为 1~3 段; - 同样以 `---` 进行分段输出。4. 片段可能被截断(如只包含“第10条”的前半句或后半句),请结合上下文判断是否保留该段,或在 `text` 中做补全,避免文本丢失或断句。5. 不要解释或输出无关文本,仅输出被切分后的段落结果。6. 对每一个片段进行元数据标注,需要标注的字段从规则文件中获取,如果有多个层级,则每个层级都需要单独的字段标注,如果规则中没有设计则自行补充标注字段。7. 不可丢失任何文本内容,每一个片段的text字段需要包含本段的所有内容。8.如果段落中包含大小标题内容如章,节等,需要根据最新的大小标题判断当前段落属于哪个标题,并且将标题标注到元数据中。9.如果输入文本为摘要或者目录,需要将该部分整体作为一个段落,对应标注即可。### 输出格式要求 - 每段之间必须用 --- 分隔### 输出示例---"chapter": "第一章 基本规定","section": "第一节 xxx","text": "第一条 为了保护民事主体的合法权益,调整民事关系,维护社会和经济秩序,适应中国特色社会主义发展要求,弘扬社会主义核心价值观,根据宪法,制定本法。"---"chapter": "第一章 基本规定","section": "第一节 xxx","text": "第二条 民法调整平等主体的自然人、法人和非法人组织之间的人身关系和财产关系。"
7.循环内部-
缓存最终切片结果节点
该节点为HTTP节点,主要实现了文本的追加写入,最终将文档分别写入到md和txt两个文档中,便于后续预览及上传知识库使用。
核心代码如下:
def append_result_to_file(self, content: str, filename: str): """ 将 Markdown 格式文本内容同时追加写入 .txt 和 .md 文件。 :param content: Markdown 格式文本(str) :param filename: 原始文件名(可带或不带后缀) :return: 写入状态信息 """ export_dir = os.path.join(self.base_dir, "exports") os.makedirs(export_dir, exist_ok=True) # 去掉后缀 name_without_ext = os.path.splitext(filename)[0] txt_path = os.path.join(export_dir, f"{name_without_ext}.txt") md_path = os.path.join(export_dir, f"{name_without_ext}.md") # 清理 Markdown 块格式 clean_content = content.strip() if clean_content.startswith("```"): clean_content = clean_content.strip("`") if clean_content.lower().startswith("json"): clean_content = clean_content[4:].strip() # 写入 .txt(纯文本格式) with open(txt_path, "a", encoding="utf-8") as f_txt: f_txt.write(clean_content + "\n\n") # 写入 .md(保留 Markdown 格式) with open(md_path, "a", encoding="utf-8") as f_md: f_md.write(content.strip() + "\n\n") return { "status": "ok", "txt_written_to": txt_path, "md_written_to": md_path, "chars_written": len(content) }
8.循环内部-赋值节点
该节点实现了真正的循环控制,在该节点中对章节信息进行缓存并且递增循环索引字段,实现每次循环读取对应的内容
9.实际处理效果
从图中我们可以看到,生成了两个缓存文件,并且将每一条分为一个段,并且为每一段实现了元数据标注。
03长文档自定义切片的意义
有的读者可能会问:做这个功能的意义是什么?
我也经常会反思这个问题,最终的答案是:通过这个流程实现智能标注,以及灵活的切片规则,实现更适合自己业务的入库效果。
最终实现检索效率及质量的提升。
04 总结
在这篇文章中,我们深入探讨了如何通过Dify实现长文档自定义切片,并详细介绍了切片过程的设计与实现。长文档切片的功能,解决了实际业务中处理大规模文档的痛点,特别是在合同审查和知识库管理等场景下具有重要意义。
希望这篇文章能够在你的文档处理过程中提供帮助。如果你有任何问题或进一步的需求,欢迎随时联系我,我们一起探讨更多的智能化文档处理方法!
点个【在看】和【转发】支持我继续优化内容!你的鼓励是我继续打磨 AI 应用的最大动力💪
🎁 关注我的公众号【AI转型之路】 ,获取更多内容,有疑问也可以在公众号咨询。
加微信sgw_clj
发送66进群