LangChain 索引错了
TextSplitter 的 start_index 指向后面的重复文字,检查空格重组与 text.find。保留原文分隔符,验证整组顺序覆盖后分配位置,旧索引单独回查。
LangChain 切分出的文档内容看着正确,metadata['start_index'] 却指向原文后面另一段相同文字,后续 chunk 甚至得到 -1。用 CharacterTextSplitter 开启 add_start_index 时,可以先检查切分后有没有把原来的重复空格合成一个。
正式 langchain-text-splitters 1.1.3 的位置计算仍通过 text.find 反查 chunk。原文是 foo bar baz qux foo bar baz,前面两个空格被重组为一个后,foo bar 会匹配到后面那一处,错误位置又成为下一次查找的起点。
find 找到的相同文本,未必是来源
这个问题即使 chunk_overlap=0 也能出现。CharacterTextSplitter 默认去掉分隔符再按配置重新拼接,连续两个空格可能被压成一个;生成文本不再是来源位置上的逐字子串。
create_documents 计算查找起点时,使用上一个 index、上一块长度与配置的 overlap,再执行 text.find(chunk, ...)。首次误选后面的重复文本,会让接下来的搜索也向后偏移。
因此,index 非负、或者用它切出的文字与 chunk 相等,都不足以证明来源正确:后面那段重复文字也能满足这个检查。应保留未经归一化的原文,比较真实来源范围与切分过程。
引用需要原文范围,显示文本可以另处理
如果目标是精确高亮,可以先使用保留分隔符、不去掉边缘空白的切分路径,再按完整原文顺序分配范围。下面限定为字面空格分隔、按字符计量、零重叠的本地文本示例:
from langchain_text_splitters import CharacterTextSplitter
splitter = CharacterTextSplitter(
separator=" ",
chunk_size=7,
chunk_overlap=0,
keep_separator=True,
strip_whitespace=False,
)
docs = splitter.create_documents([text])
cursor = 0
starts = []
for doc in docs:
chunk = doc.page_content
if text[cursor:cursor + len(chunk)] != chunk:
raise ValueError("chunks do not match source order")
starts.append(cursor)
cursor += len(chunk)
if cursor != len(text):
raise ValueError("chunks do not cover the complete source")
for doc, start in zip(docs, starts):
doc.metadata["start_index"] = start
text 是应用保留的原始字符串,7 是示例大小。这段没有开启自动反查位置,而是在确认所有 chunk 按顺序完整覆盖原文后赋值;它只构造本地文档,不写入索引或请求模型。
保留空格会改变切分结果,不能期待仍得到原来那组已经归一化的 chunk。展示或送给模型的文本如需清理,可另建副本,同时保留用于引用的原文范围,避免再次用清理后的字符串反推位置。
示例不覆盖重叠、token 长度计量、复杂正则分隔或先前已转换过的文本。这些路径应在实际拆分和合并时传递来源区间,不能直接把这个零重叠 cursor 算法套过去。
已入库的错误位置也要回查
引用字段已经写入索引时,改构造器不会自动重算旧元数据。按原文版本与文档 ID 定位受影响条目,在受控流程里重新生成并核对范围,不清空共享索引或把 -1 改成零来遮住失败。
验收要保留前后重复文本、连续空格和紧随其后的 chunk,检查每个来源范围确实对应这次切分的原位置。还要覆盖原文开头、末尾空白,以及应用实际用来显示引用的文本版本;只看第一块不再为 -1,没有检查后续查找是否继续偏移。


