|
在之前介绍包含知识库检索Agent的推文发布之后,不少同学追问:为啥文档处理这么重要的内容不介绍呢? (PS:文档处理,尤其是文档分段,是整个智能问答与知识检索流程中至关重要的基础环节。) 为了响应同学们的号召,从本期起,我们将结合 dify 中的实际处理逻辑,用几期内容把这块讲明白、说透彻。 第一期,我们聚焦在最核心的步骤上:文档分段(Text Chunking)。它看似只是“把长文本切开”,却直接决定了后续检索的精度与回答的质量。
手搓实现文档分段方法:我们在这部分将手搓实现几类文本分段的代码,现在有那么多的成熟框架(比如Langchain)不用,为什么偏要自己从头造轮子呢? 因为成熟框架虽好,但高度封装的特性的确会带来一些局限。而亲自动手实现,能为我们带来三大核心优势:灵活、稳健、出错了排查快。 我们实现了五种文本分段的方法,下面我们将分类详细介绍每种文本分段的实现方法。 基础版文本分段方法按字符数量分段字符级的分段是最简单的一种方法,但是缺点十分明显,直接按照chunk字符数量上限分段破坏了文件内容的逻辑连贯和语义,增加了rag的检索难度。所以这种方法并不常用。 defsegment_by_char_count(text:str,max_chars:int=500,overlap:int=0)->list[str]:chunks=[]start=0whilestart<len(text):end=start+max_charschunks.append(text[start:end])start=end-overlapreturnchunks --- 下面是我们对字符级分段的测试结果 --- 待分段文本: 在过去的十年中,人工智能经历了爆发式的增长。从最初的图像识别、语音识别,到如今的大语言模型(LargeLanguageModels),AI的能力已经远远超出了人们的预期。它不仅能够生成文本、理解语义,还能进行逻辑推理、编写代码,甚至参与艺术创作。然而,AI的发展也带来了新的挑战。数据安全、隐私保护、模型可解释性,这些议题正在成为研究者和监管机构关注的重点。Forexample,whenamodelgeneratesamedicaldiagnosis,howcanweensurethatitsreasoningprocessistransparentandtrustworthy?Thisquestionisnotonlytechnicalbutalsodeeplyethical.未来的人工智能将不再仅仅依赖模型的规模,而是更关注“智能的组织结构”。换句话说,AI将像人类一样学会“协作”和“记忆”。一个智能体(agent)可以调用另一个智能体的能力,共同完成复杂任务。这正是当前AgenticAI研究的方向。 ----Chunk0----在过去的十年中,人工智能经历了爆发式的增长。从最初的图像识别、语音识别,到如今的大语言模型(LargeLanguageModels),AI的能力已经远远超出了人们的预期。它不仅能够生成文本、理解语义,还能进行逻辑推理、编写代码,甚至参与艺术创作。然而,AI的发展也带来了新的挑战。数据安全、隐私保护、模型可解释性,这些议题正在成为研究者和监管机构关注的重点。Forexample,whenamodelgeneratesamedicaldiagnosis,howcanweensurethatitsreasoningproce----Chunk1----ssistransparentandtrustworthy?Thisquestionisnotonlytechnicalbutalsodeeplyethical.未来的人工智能将不再仅仅依赖模型的规模,而是更关注“智能的组织结构”。换句话说,AI将像人类一样学会“协作”和“记忆”。一个智能体(agent)可以调用另一个智能体的能力,共同完成复杂任务。这正是当前AgenticAI研究的方向。 按句子分段按照句子分段是字符级分段的进阶版,这种方法考虑到了句子完整对于检索的重要性,所以首先将文本内容按照标点符号分割,然后在满足chunk大小的基础上将句子添加到chunk内,同时考虑到了单个句子长度超过chunk限制的情况,采用滑动窗口的方法进行字符级的切分。 importrefromtypingimportListdefsplit_sentences_multilang(text:str) ->List[str]: # 中文标点 + 英文句号/问号/感叹号 + 换行段落 sentence_endings =r"([。!?!?]|(?<!\w)\.(?=\s|$)|\n{2,})" parts = re.split(sentence_endings, text) sentences, buffer = [],"" forpinparts: ifnotp: continue buffer += p # 如果是句子终结或段落 ifre.match(sentence_endings, p): sentences.append(buffer.strip()) buffer ="" ifbuffer.strip(): sentences.append(buffer.strip()) returnsentencesdefsegment_by_sentence_multilang(text:str, max_chars:int=500, overlap:int=0) ->List[str]: sentences = split_sentences_multilang(text) chunks = [] buffer =""
forsentinsentences: sent = sent.strip() ifnotsent: continue # 单句过长,回退到字符滑窗切分 iflen(sent) > max_chars: ifbuffer: chunks.append(buffer) buffer ="" start =0 whilestart <len(sent): end = start + max_chars chunks.append(sent[start:end]) start =max(end - overlap, start +1) # 保证前进 continue iflen(buffer) +len(sent) <= max_chars: buffer += sent else: chunks.append(buffer) buffer = sent ifbuffer: chunks.append(buffer)
# overlap 处理 ifoverlap >0: overlapped = [] fori, cinenumerate(chunks): ifi ==0: overlapped.append(c) else: ov_text = chunks[i-1][-overlap:]ifoverlap <len(chunks[i-1])elsechunks[i-1] overlapped.append(ov_text + c) chunks = overlapped returnchunks
--- 下面是我们对字符级分段的测试结果 --- 待分段文本: 在过去的十年中,人工智能经历了爆发式的增长。从最初的图像识别、语音识别,到如今的大语言模型(LargeLanguageModels),AI的能力已经远远超出了人们的预期。它不仅能够生成文本、理解语义,还能进行逻辑推理、编写代码,甚至参与艺术创作。然而,AI的发展也带来了新的挑战。数据安全、隐私保护、模型可解释性,这些议题正在成为研究者和监管机构关注的重点。Forexample,whenamodelgeneratesamedicaldiagnosis,howcanweensurethatitsreasoningprocessistransparentandtrustworthy?Thisquestionisnotonlytechnicalbutalsodeeplyethical.未来的人工智能将不再仅仅依赖模型的规模,而是更关注“智能的组织结构”。换句话说,AI将像人类一样学会“协作”和“记忆”。一个智能体(agent)可以调用另一个智能体的能力,共同完成复杂任务。这正是当前AgenticAI研究的方向。 分段结果如下,可以看到句子级的分段避免了句子或者英文单词被截断的风险: ----Chunk0----在过去的十年中,人工智能经历了爆发式的增长。从最初的图像识别、语音识别,到如今的大语言模型(LargeLanguageModels),AI的能力已经远远超出了人们的预期。它不仅能够生成文本、理解语义,还能进行逻辑推理、编写代码,甚至参与艺术创作。然而,AI的发展也带来了新的挑战。数据安全、隐私保护、模型可解释性,这些议题正在成为研究者和监管机构关注的重点。----Chunk1----Forexample,whenamodelgeneratesamedicaldiagnosis,howcanweensurethatitsreasoningprocessistransparentandtrustworthy?Thisquestionisnotonlytechnicalbutalsodeeplyethical.未来的人工智能将不再仅仅依赖模型的规模,而是更关注“智能的组织结构”。换句话说,AI将像人类一样学会“协作”和“记忆”。一个智能体(agent)可以调用另一个智能体的能力,共同完成复杂任务。----Chunk2----这正是当前AgenticAI研究的方向。 按语义分段句子级别的分段就可以实现高召回率和精准度吗?当然不够,句子级别的分段没有考虑句子之间的关系,一股脑儿的都装进一个chunk内部,最终的目标只有不截断句子和尽可能接近chunk字符上限。这样就增加了召回chunk的噪声,无关的内容增多,浪费token不说,还给了LLM“胡说八道”足够的内容。 因此,我们在句子级别的分段上计算句子之间的语义相似度,实现基于语义相似度的分段。根据我们测试来看语义分段可以满足大部分的文件内容了。 为了保证数据安全,我们使用本地部署的embedding模型,代码实现如下: fromsklearn.metrics.pairwiseimportcosine_similarityXINFERENCE_MODEL="bge-large-zh-v1.5"XINFERENCE_API_URL="http://localhost:9997/v1/embeddings"defget_xinference_embeddings(texts,model=XINFERENCE_MODEL,api_url=XINFERENCE_API_URL):"""调用Xinference获取文本向量"""payload={"model":model,"input":texts}headers={"Content-Type":"application/json"}response=requests.post(api_url,headers=headers,json=payload)response.raise_for_status()data=response.json()#确保兼容返回格式embeddings=[item["embedding"]foritemindata.get("data",[])]returnnp.array(embeddings)defsegment_by_semantic(text:str,threshold:float=0.7,max_chars:int=500,overlap:int=0):"""基于语义相似度分段,同时控制chunk最大长度"""sentences=segment_by_sentence_multilang(text)#使用多语言句子分割embeddings=get_xinference_embeddings(sentences)chunks,buffer=[],[sentences[0]]buffer_len=len(sentences[0])foriinrange(1,len(sentences)):sim=cosine_similarity([embeddings[i-1]],[embeddings[i]])[0][0]#当前句子长度sent_len=len(sentences[i])#超长句子单独处理ifsent_len>max_chars:#先输出当前bufferifbuffer:chunks.append("".join(buffer))buffer=[]buffer_len=0#字符滑窗切分超长句start=0s=sentences[i]whilestart<len(s):end=start+max_charschunks.append(s[start:end])start=max(end-overlap,start+1)continue#判断是否与前一句语义相似且长度不超max_charsifsim>thresholdandbuffer_len+sent_len<=max_chars:buffer.append(sentences[i])buffer_len+=sent_lenelse:#输出当前bufferifbuffer:chunks.append("".join(buffer))buffer=[sentences[i]]buffer_len=sent_lenifbuffer:chunks.append("".join(buffer))#overlap处理(可选)ifoverlap>0 verlapped=[]fori,cinenumerate(chunks):ifi==0 verlapped.append(c)else:prev=overlapped[-1]ov_text=prev[-overlap:]ifoverlap<len(prev)elseprevoverlapped.append(ov_text+c)chunks=overlappedreturn[cforcinchunksifc]--- 下面是语义分割的结果 --- 待分段文本: 在过去的十年中,人工智能经历了爆发式的增长。从最初的图像识别、语音识别,到如今的大语言模型(LargeLanguageModels),AI的能力已经远远超出了人们的预期。它不仅能够生成文本、理解语义,还能进行逻辑推理、编写代码,甚至参与艺术创作。然而,AI的发展也带来了新的挑战。数据安全、隐私保护、模型可解释性,这些议题正在成为研究者和监管机构关注的重点。Forexample,whenamodelgeneratesamedicaldiagnosis,howcanweensurethatitsreasoningprocessistransparentandtrustworthy?Thisquestionisnotonlytechnicalbutalsodeeplyethical.未来的人工智能将不再仅仅依赖模型的规模,而是更关注“智能的组织结构”。换句话说,AI将像人类一样学会“协作”和“记忆”。一个智能体(agent)可以调用另一个智能体的能力,共同完成复杂任务。这正是当前AgenticAI研究的方向。 ----Chunk0----在过去的十年中,人工智能经历了爆发式的增长。从最初的图像识别、语音识别,到如今的大语言模型(LargeLanguageModels),AI的能力已经远远超出了人们的预期。它不仅能够生成文本、理解语义,还能进行逻辑推理、编写代码,甚至参与艺术创作。然而,AI的发展也带来了新的挑战。数据安全、隐私保护、模型可解释性,这些议题正在成为研究者和监管机构关注的重点。Forexample,whenamodelgeneratesamedicaldiagnosis,howcanweensurethatitsreasoningprocessistransparentandtrustworthy?Thisquestionisnotonlytechnicalbutalsodeeplyethical.未来的人工智能将不再仅仅依赖模型的规模,而是更关注“智能的组织结构”。换句话说,AI将像人类一样学会“协作”和“记忆”。一个智能体(agent)可以调用另一个智能体的能力,共同完成复杂任务。----Chunk1----这正是当前AgenticAI研究的方向。 markdown格式文档分段讨论完常见的文件,我们来看一下自带规范结构的文件,例如markdown、html、代码脚本等。一般我们就按照文本自身结构进行分段就可以了,下面我们看一下markdown格式文件的分段代码: importredefsegment_markdown( text:str, max_chars:int=1000, overlap:int=100) ->list[str]: """ Args: text (str): Markdown 文本 max_chars (int): 每个 chunk 的最大字符长度 overlap (int): 上一个 chunk 尾部在下一个 chunk 中重叠的字符数(防止上下文割裂)
Returns: list[str]: 分段后的 Markdown 文本列表 """ lines = text.splitlines() chunks, current_chunk = [], [] current_length =0 forlineinlines: # 遇到标题(H1-H6)时,强制开始一个新的块 ifre.match(r"^#{1,6}\s", line): ifcurrent_chunk: chunk_text ="\n".join(current_chunk).strip() chunks.append(chunk_text) current_chunk = [line] current_length =len(line) else: # 普通文本行 ifcurrent_length +len(line) +1> max_chars: # 达到 chunk 上限,切分 chunk_text ="\n".join(current_chunk).strip() chunks.append(chunk_text) # 处理 overlap:取上一个 chunk 的尾部部分作为新起点 ifoverlap >0andlen(chunk_text) > overlap: overlap_text = chunk_text[-overlap:] current_chunk = [overlap_text, line] current_length =len(overlap_text) +len(line) else: current_chunk = [line] current_length =len(line) else: current_chunk.append(line) current_length +=len(line) +1 # 追加最后的chunk ifcurrent_chunk: chunks.append("\n".join(current_chunk).strip()) return[cforcinchunksifc]
--- 下面是我们关于markdown格式分段的测试结果 --- 待分段文件内容: #人工智能的起源与发展人工智能(ArtificialIntelligence,AI)这一概念最早诞生于20世纪50年代。最初的研究者们试图让计算机模拟人类思考的过程,例如推理、学习与决策。然而,受限于算力与数据的匮乏,这一阶段的AI发展相对缓慢。##机器学习的崛起进入21世纪后,机器学习(MachineLearning)成为AI的核心方向。它不再依赖人工设定规则,而是通过数据驱动模型的自我学习。这标志着AI从“编程智能”迈向了“学习智能”。#现实挑战与伦理思考AI的发展带来了巨大的社会影响。隐私泄露、算法歧视、虚假信息传播等问题,正在成为亟需解决的现实挑战。与此同时,AI的应用边界也在被不断讨论——它是否应该参与教育、医疗甚至法律判断?##可解释性与安全性一个重要议题是模型的可解释性(Explainability)。人类希望理解模型为何得出某个结论,尤其是在关键领域如医学诊断或司法判决。因此,提高AI的可解释性与安全性,是未来研究的重点方向。##人机协作与未来愿景未来的AI不会取代人类,而会成为人类的协作者。人机协同(Human-AICollaboration)将推动知识创造、科学研究乃至社会治理的新模式。这种“共智”模式,或许才是人工智能的真正终极形态。 ----Chunk0----#人工智能的起源与发展人工智能(ArtificialIntelligence,AI)这一概念最早诞生于20世纪50年代。最初的研究者们试图让计算机模拟人类思考的过程,例如推理、学习与决策。然而,受限于算力与数据的匮乏,这一阶段的AI发展相对缓慢。----Chunk1----##机器学习的崛起进入21世纪后,机器学习(MachineLearning)成为AI的核心方向。它不再依赖人工设定规则,而是通过数据驱动模型的自我学习。这标志着AI从“编程智能”迈向了“学习智能”。----Chunk2----#现实挑战与伦理思考AI的发展带来了巨大的社会影响。隐私泄露、算法歧视、虚假信息传播等问题,正在成为亟需解决的现实挑战。与此同时,AI的应用边界也在被不断讨论——它是否应该参与教育、医疗甚至法律判断?----Chunk3----##可解释性与安全性一个重要议题是模型的可解释性(Explainability)。人类希望理解模型为何得出某个结论,尤其是在关键领域如医学诊断或司法判决。因此,提高AI的可解释性与安全性,是未来研究的重点方向。----Chunk4----##人机协作与未来愿景未来的AI不会取代人类,而会成为人类的协作者。人机协同(Human-AICollaboration)将推动知识创造、科学研究乃至社会治理的新模式。这种“共智”模式,或许才是人工智能的真正终极形态。
进阶版文本分段方法LLM文本分段基于LLM实现的分段让大模型先理解内容语义再决定如何分块。模型不只是看句号或者行数来分段,而是理解哪几句话在讲一个主题,哪个小标题下的内容属于同一个语义单元。 importreimportjsonbase_url ="http://localhost:8977/v1/chat/completions"api_key ="xxxxxxx"defsegment_by_llm(text:str, max_chars:int=800, overlap:int=0) ->list[str]: """ 使用大模型理解文本语义后进行分块。 """ headers = { "Content-Type":"application/json", "Authorization":f"Bearer{api_key}" } system_prompt ="""你是一个文本分块专家,请将输入文本分成语义完整、长度适中的段落。 每个段落应尽量保持上下文连贯。 请输出 JSON 格式,如: [ {"chunk_id": 1, "content": "第一段文本"}, {"chunk_id": 2, "content": "第二段文本"} ] """ user_prompt =f"以下是要分块的文本(建议每块不超过{max_chars}字):\n{text[:6000]}"# 限制输入长度避免超限 messages = []
ifsystem_prompt: messages.append({"role":"system","content": system_prompt})
messages.append({"role":"user","content": user_prompt}) payload = { "model":"Qwen3-32B", "messages": messages, "temperature":0.6 } response = requests.post(base_url, headers=headers, json=payload) result = response.json() content = result["choices"][0]["message"]["content"] # 尝试提取 JSON json_match = re.search(r"\[.*\]", content, re.S) ifnotjson_match: return[text] # fallback try: chunks_data = json.loads(json_match.group(0)) chunks = [c["content"].strip()forcinchunks_dataif"content"inc] exceptException: chunks = [text] returnchunks
--- 下面是LLM Chunking的测试结果 --- 待分段文本: #人工智能的起源与发展人工智能(ArtificialIntelligence,AI)这一概念最早诞生于20世纪50年代。最初的研究者们试图让计算机模拟人类思考的过程,例如推理、学习与决策。然而,受限于算力与数据的匮乏,这一阶段的AI发展相对缓慢。#现实挑战与伦理思考AI的发展带来了巨大的社会影响。隐私泄露、算法歧视、虚假信息传播等问题,正在成为亟需解决的现实挑战。与此同时,AI的应用边界也在被不断讨论——它是否应该参与教育、医疗甚至法律判断? ----Chunk0----#人工智能的起源与发展人工智能(ArtificialIntelligence,AI)这一概念最早诞生于20世纪50年代。最初的研究者们试图让计算机模拟人类思考的过程,例如推理、学习与决策。然而,受限于算力与数据的匮乏,这一阶段的AI发展相对缓慢。----Chunk1----#现实挑战与伦理思考AI的发展带来了巨大的社会影响。隐私泄露、算法歧视、虚假信息传播等问题,正在成为亟需解决的现实挑战。与此同时,AI的应用边界也在被不断讨论——它是否应该参与教育、医疗甚至法律判断? 虽然LLM Chunking相较于之前传统的基于规则分段的任务对于语义和逻辑的理解更透彻,但是存在一些挑战: 1. 每个文档都要调用模型,成本较高; 2. 可能会出现chunk太大或者太碎的情况; 3. 难以调试或者标准化; 以上就是我们本期关于文档分段的全部内容,我们从可精准控制的基于规则的基础方法,一路探讨到大模型时代更为智能、理解上下文的进阶分段策略,希望能为大家构建高效的检索问答系统打下坚实基础。 |