写点什么

LLM 大语言模型应用的分段策略

作者:Tiger Wang
  • 2023-10-26
    广东
  • 本文字数:4061 字

    阅读完需:约 13 分钟

在构建 LLM 相关应用时,我们往往会遇到这样的问题


  • 需要将输入的 token 嵌入(embedding)为向量(vector),但过多的 token 无法适配有限的向量长度

  • 而如果增加向量长度的话,又增加了推理(inferencing)成本,降低模型响应时间


因此,在短期内想做好一个基于 LLM 的应用,需要在有限的 token 数量内完成推理任务。常见的做法是将大量的信息根据一定的策略,进行分段(chunking),然后输入到大模型中。分段是一种有助于优化从矢量数据库获取的内容相关性的基本技术。

分段大小

在讨论分段策略之前,我们需要先对分段大小有一个认知。


对于搜索型 LLM 应用,往往会有一个误区,那就是以为越大的分段越好,最好充分利用全部的 token 数量。实际上,过大的分段会产生很长的向量(vector)。而往 vector store 中索引的时候,更长的向量会产生更多的噪音。本质上向量空间中的搜索是相似性搜索,噪音会让本来相似的两个向量在比较的时候看起来不那么相似。


例如

  • “吃了个晚饭”

  • “晚上吃了个饭”


在 vector store 中本来是很相似的了。但如果分段太长,出现下面两句话,就会引入一些无关主线的 token,形成噪音。


  • “忙了一天饿的精疲力尽,回家快速洗了个澡,然后吃了个晚饭,才有了些力气……”

  • “终于到周末了,和家人们晚上吃了个饭,然后在饭桌旁聊了一晚上最近发生的事……”


总的来说,分段大小并没有具体的优劣之分。我们只是需要认识到:


  • 过小的分段会忽略段落或文档中更广泛的上下文,但是会集中于具体内容,可能更适合与句子级嵌入式匹配。

  • 较大的输入文本大小可能会引入噪音或削弱单个句子或短语的重要性,使得在查询索引时更难找到精确的匹配项。


下面分段策略中,有些会利用 LLM 的能力,对大段文字进行精简总结,来帮助在两个问题中找到平衡。

分段策略

在选择分段策略前,我们需要做以下几个考量:


  • 索引内容的性质

  • 是长篇的文档资料

  • 还是短篇的聊天记录

  • 所使用到的 embedding 模型在不同分段大小上的表现

  • sentence-transformers 更适合短句

  • text-embedding-ada-002 更适合 256 或 512 token 的长句

  • 用户的查询长度和复杂度

  • 是一句话一句话问

  • 还是一口气贴很长的一段文字


有了这些考量,下面我们就不同的场景,列举一些不同的分段策略,希望可以抛砖引玉。

针对文档资料的分段策略


  1. 固定大小分段

这是最常见最直接的一种分段策略,基本上网上看到的教程都是这个策略

text = "..." # your textfrom langchain.text_splitter import CharacterTextSplittertext_splitter = CharacterTextSplitter(    separator = "\n\n",    chunk_size = 256,    chunk_overlap  = 20)docs = text_splitter.create_documents([text])
复制代码

这个分段策略维护上下文关系的思路比较简单粗暴,就是在 chunk 和 chunk 之间保留一定的重叠。

  • 优点:不需要使用任何 NLP 库或者 LLM,因此计算成本低,使用简单。

  • 缺点:比较长的段落有可能被掐头捏尾,结果断章取义。


  1. 段落分段

这个分段策略是为了解决固定大小分段中断章取义的问题。

比较粗暴的做法就是,以句号和换行为分隔符,把整个句子或者段落作为一个 chunk。。

text = "..." # your textdocs = text.split(".")
复制代码

这样做既照顾了计算成本,又避免了断章取义。但问题也是显而易见的,即 "." 有时候并不真的是用作句号,可能也是小数点或者省略符。


  1. 语义分段

比段落分段高级一点的做法,是用 NLTK 这种 Python 库来识别语义,并根据语义进行分段,避免对 "." 产生误解。

text = "..." # your textfrom langchain.text_splitter import NLTKTextSplittertext_splitter = NLTKTextSplitter()docs = text_splitter.split_text(text)
复制代码

但是,不管用 NLTK 还是 spaCY,对中文的支持都不是很理想。如果需要对中文文本进行段落分段,可以考虑集成 https://github.com/isnowfy/snownlp 或者 https://github.com/dongrixinyu/JioNLP 这样的 Python 库。相信其他语言也有类似的库可以用。


对于语言的检测,可以考虑用 https://github.com/pemistahl/lingua-py


  1. 递归分段

无论是段落分段还是语义分段,都不能保证类似的字数,例如小说体中,往往人物说的一句话就是一个段落;而叙事性的段落又很长。

汪淼来到大史凌乱的办公室时,见那里已被他抽得云蒸雾绕,使得在办公室中的另一位年轻女警不停地用记录本在鼻子前扇动。大史介绍说她叫徐冰冰,计算机专家,是信息安全部门的。办公室中的第三个人令汪淼很吃惊,居然是申玉菲的丈夫魏成,头发乱蓬蓬的,他抬头看看汪淼。好像已经忘记了他们见过面。

"不好意思打扰,不过我看你也没睡吧。这里有些事儿,还没有汇报作战中心,大概需要你参谋参谋。"大史对汪淼说,然后转向魏成,"你说吧。"

"我说过,我的生命受到威胁。"魏成说,脸上却是一副木然的表情。

"从头说起吧。"

"好,从头说,不要嫌我麻烦,我最近还真想找人说说话……"魏成说着转头看看徐冰冰,"不做笔录什么的吗?"

"现在不用,以前没人和你说话?"大史不失时机地问。

"也不是。我懒得说,我是个懒散的人。"


递归分段策略会结合标点符号,从小往大去分段,直到分段结果接近平均字数

汪淼来到大史凌乱的办公室时,见那里已被他抽得云蒸雾绕,使得在办公室中的另一位年轻女警不停地用记录本在鼻子前扇动。大史介绍说她叫徐冰冰,计算机专家,是信息安全部门的。办公室中的第三个人令汪淼很吃惊,居然是申玉菲的丈夫魏成,头发乱蓬蓬的,他抬头看看汪淼。好像已经忘记了他们见过面。

"不好意思打扰,不过我看你也没睡吧。这里有些事儿,还没有汇报作战中心,大概需要你参谋参谋。"大史对汪淼说,然后转向魏成,"你说吧。"

"我说过,我的生命受到威胁。"魏成说,脸上却是一副木然的表情。

"从头说起吧。"

"好,从头说,不要嫌我麻烦,我最近还真想找人说说话……"魏成说着转头看看徐冰冰,"不做笔录什么的吗?"

"现在不用,以前没人和你说话?"大史不失时机地问。

"也不是。我懒得说,我是个懒散的人。"

text = "..." # your textfrom langchain.text_splitter import RecursiveCharacterTextSplittertext_splitter = RecursiveCharacterTextSplitter(    # Set a really small chunk size, just to show.    chunk_size = 256,    chunk_overlap  = 20)
docs = text_splitter.create_documents([text])
复制代码


  1. 格式化分段

格式化分段策略会利用文档本身的格式对内容进行分段,例如 Word 格式、Markdown 格式、LaTeX 格式

from langchain.text_splitter import MarkdownTextSplittermarkdown_text = "..."
markdown_splitter = MarkdownTextSplitter(chunk_size=100, chunk_overlap=0)docs = markdown_splitter.create_documents([markdown_text])
复制代码
针对聊天历史的分段策略

聊天历史记录是一种基于时间线的文本信息,同时也具有双角色(人-AI)、多角色(聊天群)的特征。


与 LLM 对话的过程,每次发送信息本质上是把所有的历史记录都放在同一个提示词(prompt)中,扔给 LLM,然后获得回复。下一个信息又会包含 LLM 这个回复,周而复始……


聊天历史的分段和文档资料的分段不属于一个概念,但是底层的本质逻辑是类似的。


这些分段策略并不被 LangChain 原生支持,需要自行实现。


  1. 人为主题分段


这是 ChatGPT 的做法,也是最简单的分段策略,即允许用户针对每个话题开不同的会话(Session),来方便把一类话题合并到同一个提示词。


我们可能以为这是一个功能,实际上 ChatGPT 通过这种方法来借用户的思路,人为区分话题。所以这个策略更多是一个产品上的设计,而不是一个后台的逻辑。


不过问题也是存在的,即某些用户不会非常认真地区分话题,而是把对话都放在一个话题中。


  1. 时间戳分段


针对无法人为区分话题的聊天,可以考虑简单一点的时间戳分段,和固定大小分段策略类似,只不过以时间为单位,并且通过时间戳重叠来关联上下文。


当然,聊天过程不可能平均在每个时间点上,而是每个话题会集中出现在一个固定的时间范围内。这就需要对聊天的一个连续性做一个跟踪,把连续的聊天信息划分成一个 chunk。


  1. 远近记忆分段


有时候用户确实希望对话的上下文能覆盖整个聊天历史,但 LLM 在 token 数量上又有一个限制。比较简单一点的做法是,


  • 把比较靠前的消息,通过 LLM 总结,合并成比较简短的消息

  • 保留比较新的消息,不去做改动


通过前面的提到的递归分段策略,反复这个过程,直到整个提示词符合 LLM 的 token 数量限制为止。


这个策略可以很好地照顾到整个聊天历史的上下文。问题就是,当聊天历史特别长的时候,很多前面的细节内容会丢失。而对于某些场景来说,一开始的信息往往是很重要的。


  1. 微笑曲线分段


为了更加优化远近记忆分段策略,我们可以考虑模拟人类的记忆,即


  • 更容易记得一件事最早和最近的内容

  • 更容易忽略一件事情过程中的细节


同样地,微笑曲线分段策略要求设计一个梯度,模拟人类的微笑曲线记忆


  • 把聊天记录接近中间的信息通过 LLM 做总结,合并成比较简短的信息

  • 尽量保留聊天记录开头和最新的信息


这个策略的缺点是需要 LLM 的大量介入。如果硬件能力不够高的话,整个对话体验上会有一个延迟。随着算力的不断提升,这个策略会越来越完美。


  1. 非线性分段


为了避免早期聊天历史被忽略的问题,很多人在使用 ChatGPT 都学会一个技巧:相关的问题不会以新的消息发出;而是在整个对话中找到最相关的环节,然后修改紧接着的“消息”来保证 AI 不“失忆”。


这和我们在使用微信时用“引用”这个功能一个道理,就是让对方知道你的上下文。但是问题是,它只能引用一句话,而不是整个前文。


非线性分段策略,通过产品设计,把一般的线性聊天历史,变成可以分叉引用的非线性聊天历史。类似的对话方式有 Slack, Discord 的 thread、飞书的“话题”。


就上面这例子来讲,如果是线性的话,传给 LLM 的提示词

  • 要么包含不相关的噪音(时间戳分段)

  • 要么总结的过于笼统(长远记忆分段、微笑曲线分段)

而非线性分段策略则利用了用户的能力,非常巧妙地低成本地解决了这些问题。


以上并不是所有已知的 LLM 应用的分段策略。社区中还有大量更加高效更加有创意的选择,不在这里一一列举。仅希望可以通过这些选项,举一反三。最后的效果好坏要结合具体的场景反复 A/B 测试,甚至是动态调整,来提升文档搜索和对话的质量。

发布于: 3 小时前阅读数: 6
用户头像

Tiger Wang

关注

https://github.com/tigerinus 2018-11-08 加入

早年在微软、甲骨文、阿里巴巴就职,最近一直在初创公司打杂。

评论

发布
暂无评论
LLM 大语言模型应用的分段策略_LLM_Tiger Wang_InfoQ写作社区