1. 长文本切分信息丢失问答精度曲线
长文本的头尾部分,问答的精度较高,而在中间部分则出现了精度下降的问题。这通常是因为在文本切分过程中,重要信息的上下文可能被丢失,尤其是在长段落的中间部分。
2. 长文本切分信息丢失解决思路
-
重叠切割策略(Overlapping Chunks)
-
在切割文本时设置重叠区域(overlap),确保相邻块之间共享一定的内容
-
通过增加重叠区域大小,可以减少中间部分上下文丢失的问题
-
在LangChain中可以通过设置
overlap
参数来实现:text_splitter = RecursiveCharacterTextSplitter( chunk_size=1000, chunk_overlap=200 # 增加重叠区域)
-
-
分层切割方法(Hierarchical Splitting)
- 先将文本分割为大的语义单位(如章节),然后再进行细粒度切割
- 这样可以保留章节内的上下文关系,减少中间部分信息丢失
- LangChain中可以使用
HierarchicalTextSplitter
或组合多个切割器实现
-
语义感知切割(Semantic-aware Splitting)
-
使用语义边界(如段落、句子)而非简单的字符数来切割文本
-
这样可以避免切割点破坏语义完整性
-
可以通过调整
RecursiveCharacterTextSplitter
的分隔符优先级来实现:text_splitter = RecursiveCharacterTextSplitter( separators=["\n\n", "\n", ". ", " ", ""], chunk_size=1000)
-
-
动态调整切割大小(Adaptive Chunk Sizing)
- 对文本不同部分采用不同的切割策略
- 例如,对文本的中间部分使用更小的chunk_size和更大的overlap
- 这需要自定义切割器来实现
-
切割后的后处理(Post-processing)
- 为每个切片添加前后文的摘要或关键信息
- 使用LangChain的Document Transformers对切片进行增强
- 例如,可以将每个片段的前后位置信息作为元数据添加
-
检索增强(Retrieval Enhancement)
- 在检索阶段改进策略,不仅获取最匹配的片段,还获取其前后文
- 使用LangChain的
ContextualCompressionRetriever
进行上下文感知检索 - 或者使用多查询检索,从不同角度获取相关信息
-
句子窗口方法(Sentence Window Retrieval)
-
将文档切割为句子级别,检索时返回匹配句子及其周围的句子
-
这可以通过自定义的处理管道实现:
from langchain.text_splitter import CharacterTextSplitter# 先按句子切割sentence_splitter = CharacterTextSplitter( separator=".", chunk_size=10000, chunk_overlap=0)# 检索时获取句子及其上下文
-
3. LangChain 解决长文本切分信息丢失 综合实例
需求分析
背景案例: 某法律科技公司需要构建一个智能法律助手,用于分析和检索长篇法律文件(如判决书、法律法规等)来回答用户问题。团队发现在测试过程中存在一个明显问题:系统在处理长文本时表现出明显的"位置偏见"——能够准确回答关于文档开头和结尾部分的问题,但对中间部分的回答质量明显下降。
具体问题:
- 判决书通常长达数万字,按常规切分方法(如固定字符数)导致中间部分的上下文信息丢失
- 法律文件中的因果关系和逻辑推理常常跨越多个段落,简单切分会破坏这些联系
- 关键信息(如判决结果)可能在文档多处被引用,但切分后这种联系被切断
- 检索系统倾向于返回文档首尾的内容,中间部分的召回率较低
性能指标:
- 提高中间部分内容的问答准确率(从现有的62%提升至85%以上)
- 减少"不知道"或错误回答的比例
- 保持系统响应时间在可接受范围内(<3秒)
解决方案设计
基于上述需求,我们可以设计以下三种方案组合使用:
方案1:分层递进切分 + 重叠窗口
思路: 根据法律文档的结构特点,先按照大的语义单位(如章节)进行切分,再对每个章节进行更细粒度的切分,同时在细粒度切分时保持足够的重叠度。
优势:
- 保留文档的层次结构信息
- 通过重叠窗口保证上下文连贯性
- 适合具有明确章节结构的法律文档
方案2:语义感知切分 + 上下文增强
思路: 使用语义边界(如自然段落、完整句子)进行切分,同时为每个切片添加前后文的摘要信息作为元数据。
优势:
- 避免切断自然语义单位
- 通过元数据保留上下文关联
- 特别适合逻辑关系复杂的法律推理文本
方案3:动态调整切片大小 + 多查询检索
思路: 根据文本位置和内容复杂度动态调整切片大小和重叠度,并在检索时使用多角度查询策略。
优势:
- 对文档不同部分采用不同的处理策略
- 通过多角度查询提高中间部分的检索概率
- 适应不同类型法律文档的特点
代码构建思路
我们将综合使用以上三种方案,构建一个完整的解决方案。核心思路如下:
- 实现分层递进切分器,根据文档结构进行自适应切分
- 创建语义感知的切分策略,保留自然语义单位
- 设计动态参数调整机制,根据文本位置调整切分参数
- 实现上下文增强处理器,为每个切片添加关键上下文
- 构建多策略检索系统,提高中间部分的召回率
完整实现
下面是基于LangChain的完整实现代码:
import re
from typing import List, Dict, Any, Optional, Tuple
import numpy as np
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.schema import Document
from langchain.embeddings import OpenAIEmbeddings
from langchain.vectorstores import Chroma
from langchain.retrievers import ContextualCompressionRetriever
from langchain.retrievers.document_compressors import LLMChainExtractor
from langchain.chains import RetrievalQA
from langchain.chat_models import ChatOpenAI
from langchain.prompts import PromptTemplate
class AdaptiveDocumentProcessor:
"""自适应文档处理系统,实现分层递进切分与上下文增强"""
def __init__(
self,
base_chunk_size: int = 1000,
base_chunk_overlap: int = 200,
embeddings_model: str = "text-embedding-ada-002",
llm_model: str = "gpt-3.5-turbo",
db_directory: str = "./chroma_db"
):
self.base_chunk_size = base_chunk_size
self.base_chunk_overlap = base_chunk_overlap
self.embeddings = OpenAIEmbeddings(model=embeddings_model)
self.llm = ChatOpenAI(model_name=llm_model, temperature=0)
self.db_directory = db_directory
# 定义结构识别的正则表达式
self.section_patterns = {
"header": r"^#+\s+.+$|^.+\n[=\-]+$", # Markdown标题或下划线式标题
"paragraph_break": r"\n\n+", # 段落分隔
"list_item": r"^\s*[\*\-\+]\s+.+$", # 列表项
"code_block": r"```[\s\S]+?```", # 代码块
"table": r"\|.+\|.+\|", # 简单表格识别
}
def detect_document_structure(self, text: str) -> Dict[str, List[Tuple[int, int]]]:
"""分析文档结构,返回各类结构元素的位置信息"""
structure_map = {
}
for element_type, pattern in self.section_patterns.items():
matches = list(re.finditer(pattern, text, re.MULTILINE))
if matches:
structure_map[element_type] = [(m.start(), m.end()) for m in matches]
return structure_map
def calculate_position_weights(self, text: str, position: int) -> float:
"""根据文本位置计算权重,用于动态调整切分参数
文档开头和结尾通常包含更重要的信息,给予更高权重
"""
total_length = len(text)
relative_pos = position / total_length
# 使用高斯分布使开头和结尾获得更高权重
if relative_pos < 0.3:
# 文档开头部分
weight = 1.5 - (relative_pos / 0.3) * 0.5
elif relative_pos > 0.7:
# 文档结尾部分
weight = 1.0 + ((relative_pos - 0.7) / 0.3) * 0.5
else:
# 文档中间部分
weight = 1.0
return weight
def adaptive_chunk_parameters(self, text: str, position: int) -> Tuple[int, int]:
"""根据文本位置动态调整chunk大小和重叠参数"""
weight = self.calculate_position_weights(text, position)
# 调整chunk大小和重叠参数
adjusted_chunk_size = int(self.base_chunk_size * weight)
adjusted_overlap = int(self.base_chunk_overlap * weight)
return adjusted_chunk_size, adjusted_overlap
def hierarchical_