Overview
试想一个场景:让 LLM 理解 CloudWeGo 官网上的所有文档,回答和 CloudWeGo 相关的问题时,通过对文档库理解到的内容提供置信度高的答案,而非幻想。
为了实现这个目标,最直观的方法是:在训练阶段将 CloudWeGo 语料(官方文档)喂给语言模型(LM),这样训练出来的 LM 就自然具备了根据 CloudWeGo 官网回答问题的能力。然而,这里存在的问题也非常明显:
-
训练 LLM 模型本身是一个资源消耗巨大的工程,训练成本非常高,经费在燃烧
-
由此训练出来的语言模型所拥有的知识也仅限于训练时的内容,如果 CloudWeGo 的文档进行了更新或增加(一定会的),更新的部分语言模型一定是无法知道的
那么,是否存在一种解决方案,既经济、简洁,又能高效地让语言模型学习到的特定领域知识保持更新呢?
答案是:存在的。
RAG 简述
这里引入了一个简单的名词:RAG(Retrieval-Augmented Generation),中文名:检索增强生成。是一个针对 LLM 应用的一个改进架构,请 Claude 一句话帮我做了一个简单的总结:
RAG(Retrieval-Augmented Generation)技术:利用检索机制获取相关信息,指导并增强下游生成模块的输出质量。
重点在于这里引入的所谓检索机制,就是让 LLM 以最小成本获得对新知识的理解并输出的核心秘密。
以下是一个原生/典型的 RAG 架构示意图:
想要很好的理解这个图,只需重点关注两个链路:
-
知识入库:Proprietary Data -> Embedding Model -> Vector Database
-
知识索引:User Question -> Embedding Model -> Vector Database -> Prompt -> LLM
知识入库
既然提到了通过检索机制来增强语言生成模型,那么这些知识从何而来呢?在 RAG 的架构中,对于知识来源的约束实际上并没有特别严格的限制:假设我们首先将讨论的范围限定在文本领域(非多模态输入),可以说任何文字资料都能作为知识的原材料加入其中。但是,如果这类原始知识本身并不具备强结构性,那么入库的知识质量也会相应地降低(提升知识库质量是一个独立且完整的话题,我们在这里先不深入讨论)。
如果说只要是文本内容就能作为知识的原材料,我们不妨先尝试对这类原材料在类别上做一个大致的划分,以便以更加直观的方式理解 RAG 这套框架所具备的通用性。指导后续在相似/相同场景内的进一步泛化。
常见的知识来源:
-
现存的文档库:通常是按照某种格式(如 markdown)行文和组织的语料,质量根据行文作者不同而有所不同,不过整体质量可控
-
(可选)内场 Oncall 群聊数据:分为问题咨询/需求/保障几类,针对不同的类别需要进行定制化的清洗,提升数据信息密度和价值
-
Github** Issue 数据**:类别和特点和 Oncall 数据类似
-
代码仓库:涉及到与应用知识密切相关的代码块,它们可以提供一些源码层面最直观的信息。然而,这需要高质量的数据清洗以及 code2vec 或 code2text 的转换能力
-
搜索引擎:通常来说最好是作为上述知识来源的补充而非核心来源,这部分的知识结构未知性最高,对知识清洗、密度提升有较高要求,准确度提升难度相对较大
在我们明确了需要入库的知识之后,后续的工作相对而言就明确了:
-
预备:对原始知识进行预处理:清洗、提纯、分段(chunk)/建模(图)
-
向量化:将处理后的知识进行向量化/结构化(图数据)
-
入库:向量数据库/结构化数据库(图数据库等)
知识检索
一旦我们将我们期望的知识入库之后,就可以开始尝试看看这些知识能否帮助 LLM 理解我们在特定领域的问题,并且结合知识库中的知识进行更加精准可靠的回答了。
和知识入库的流程非常相似,只是索引的关键步骤也是向量化:将用户提出的经过优化(预处理)后的问题转化为向量,之后通过和向量数据库中的预存入的知识进行匹配,查询出关联度最高的数条知识片段(chunk)。
获取到和当前问题相关联的知识片段后,就轮到我们熟悉的提示词工程(Prompt engineering)上场了,通过构造一个能够引导 LLM 结合上下文和用户原始问题的 Prompt 就完成了最后这关键结果产出。
一个简单的例子:
As an AGI who is good at documentation engineering, analyze the content in the hertz base:{cloudwego} and the user's original question: {ctx} and try your best to use the most-related content in the knowledge base list to answer the user's original question as detail as possible but it must be well-founded in the hertz base, and you are prohibited from making it up yourself:
这其实就是 CloudWeGo 睿智侠(CloudWeGo wise man)产出最终结果所使用的其中一个 Prompt。
在这个 Prompt 中,可以重点关注以下几个部分:
-
对于 LLM 角色的限定:擅长文档工程,要求 LLM 返回的结果更加专业且易读
-
回顾用户原始问题,结合知识库中获取的知识片段进行最大关联生成:保证 LLM 尽量扣题,避免答非所问
-
对结果进行二次限定:禁止幻觉,要求 LLM 对生成内容进行强关联性回顾
对于文档类任务,答案的可靠性通常需求较高。对于知识库中不存在或未匹配到的内容,我们期望语言生成模型能够做到“知之为知之,不知为不知”,这将远胜于“信马由缰”。
关于如何写好一个 Prompt 其实有蛮多前人总结的方法论,作为延伸阅读,可以参考:github.com/mattnigh/Ch… ,作者提出了一个叫做 CRISPE Prompt 框架,从不同角度出发写一个能够最大程度激发 LLM 潜能的 Prompt,来提升 LLM 最后生成的答案质量
这里有关于 RAG 的简介就基本接近尾声了,基于 RAG 架构我们也实现了一个智能问答机器人的 DEMO,但关于 RAG 的探索才刚刚开始。
工程上的问题
按照以上架构完成 RAG 架构的 LLM 应用搭建后,对外以飞书机器人作为用户界面,故事看似很美好:业务可以通过语义化的问题询问机器人研发过程中遇到的问题,RAG 后端通过对用户的问题进行 embedding 并从向量数据库中检索预存入的 CloudWeGo 官网文档库中的文档片段,之后 LLM 结合这些获取到的片段对用户的原始问题进行生成式回复。我们似乎找到了一种架构文档库“专家”的方法。
一些 Cloudwego 睿智侠应用示例:
但随着我们继续深入,RAG 或者说是当前这套 LLM 增强架构存在的一些问题也开始显现,按照问题产生的阶段,分为知识入库以及知识检索两块进行讨论。
知识入库
由于模型上下文 token 上限所带来的限制(GPT3.5 4k,GPT4 8k/32k/128k),和 LLM 交互的上下文始终存在一个上限,如果会话的总大小超出当前模型的上限,过早的上下文信息将被丢弃。这可能会带来一些致命的问题。
随着模型本身的不断
卷发展,更大上下文限制的模型将不断问世,截止到本文撰写时间(23.11.06),来自**无问芯穹(Infinigence-AI)**号称具备 256K 上下文全球最长国产大模型发布。
更大的上下文上限的 LLM 的出现是历史进程中的一个必然,但是,在真正具备强大推理能力 & 足够上下文窗口之前,对于如何更加高效的管理上下文空间一定是一个核心需要思考的问题。
当一篇文档/网页的内容长度超过一个 chunk 大小(与模型 token 上限强关联)的上限时,对该文档/网页的分段(chunk)无法避免。一旦进行 chunk,就会引入如下问题:
存在的问题:Chunk 导致文档信息损耗/丢失
这个问题可以通过一个简单的例子来理解。当人类作者在撰写一篇文章时,通常会存在一种假设:在表述文档的所有观点时,整篇文章的内容被视为一个连贯的上下文。然而,当文章被“分块”(chunk)时,这种假设就被打破了。这些被分割的片段,会自然地丧失掉不在当前片段中的信息和上下文。如果我们将这些独立的片段进行 embedding 后存入向量数据库,然后用于后续的关联度检索,会出现如下问题:
-
原始含义的破坏:如果分块的地方恰好在某个关键信息点,那么关键词或关键语义的分割可能会导致原本可以整体匹配的语义信息被割裂。当这段信息被割裂后,两部分都可能无法通过向量检索找到,从而导致关联内容无法被提取,造成无意义的片段存储。
-
强关联片段的检索困难:如前所述,由于片段的存在,后续片段对于一个语义可能同样具有强关联性。然而,由于检索语义的标记大多集中在首部片段中,后续片段可能无法被检索出。因此,即使被检索出的片段由于后续片段的缺失也无法很好的支持用户的问题,最终生成的效果可能不尽人意。
仅仅因为引入了分块机制,就可能对后续的生成结果产生不利影响,这样的结果显然不是我们希望看到的。面对这种问题,我们是否有优化的空间呢?
可能的解
问题的核心在于分块(chunk)操作打破了原始文档的连贯性。为了避免因分块造成的上下文损失,我们是否能在分块操作之后,为每一个分块重新附加上原始文档的完整上下文信息呢?这样,即使我们将一个文档分割成了若干个独立的分块,但由于每一个分块中都保留了原始文档的完整上下文信息,因此可以解决之前因上下文损失导致的分块无法被检索的问题。
图片来源网络,仅做示意,详细参考:blog.langchain.dev/semi-struct…
这种方法在大体上可以解决由于分块后上下文丢失而引发的索引失败问题,但这也对分块的切割逻辑提出了一个隐性的要求:我们需要尽量避免在具有结构化数据的中部进行切割。例如,如果一段代码块被切分为两部分,尽管我们可以通过完整的上下文信息检索到所有的部分,这种情况仍然可能对后续的语言模型生成构成很大的挑战(我们需要在输入语言模型前将这些分块合并为一个完整的上下文,这也适用于多个分块的情况)。
对这一方案进一步延伸,我们可以将存储一拆为二:向量数据库仅仅负责存储 chunk 摘要(summary),chunk 的真实内容落到另外一个存储当中,这部分存储可以是文档数据库(半结构化数据)、图数据库(知识图谱)。
知识检索
在一个原生/典型的 RAG 架构中,知识检索所做的事情其实就是拿着用户输入做一次知识入库的逆运算。用户并不是这套系统的专家,并且用户也无从得知知识库中可能存在的答案有哪些,如果对于用户输入不加思索就开始操作的话,最终的效果可能仍然不会太理想:
存在的问题:用户问不到点子上
这不是一个新的问题,在搜索引擎时代,为了尽可能让用户的问题被系统充分理解以便于检索到更加精准的内容,人们总结了很多类似小技巧(search tricks)。就像不是人人都会去遵循这些小技巧,RAG 系统的用户也不会针对系统本身的一些特性进行问题内容本身的调优,可能对应的调优手段在用户视角来看也不一定清晰可操作。
怎么协助用户的问题更好的传达用户的原始意图,以及如何高效的提升对存量知识索引的效率(关联性、完整性),成为 RAG 架构在工程上另一个亟待解决的问题。
可能的解
既然是 LLM 应用,我们可以尝试更多 LLM 风格的解题思路。
解法1
尝试对用户输入的问题使用 LLM 进行一次加工(Query Rewriting),原 paper 引入了一套基于强化学习不断增强的Rewriter 框架,通过不断的强化反馈,尝试将 Rewriter 产出的问题最终对齐 RAG 中的内容。详细参考这里。
图片来源引用论文,仅做示意,详细参考:arxiv.org/pdf/2305.14…
这个 Rewriter 的意义在于,通过对原始用户问题的重写(Query Rewriting)实现问题和答案库(RAG)的对齐。具体实现可以参考 LangChain 提供的 API。
解法2
和解法1类似,解法2不需要额外引入一套强化学习的 Rewriter 框架,而是在处理用户输入时,将原来的直接 embeding 后检索拆解为了两步:抽象 + 推理。
在抽象阶段,通过特定的 Prompt 引导 LLM 结合用户原始问题提出一个更高维度抽象的问题,使用这个问题先做一次 embeding & searching。
在推理阶段,LLM 结合在抽象阶段得到的概念,以及对原始问题的 embeding & searching 得到的内容进行综合推理。详细参考这里。
**这个方法的核心点在于,通过增加对用户原始问题的解读,尝试结合从不同角度出发得到的内容进行综合的推理,最终实现提升推理效果的目的。**具体实现可以参考 LangChain 提供的 API。
解法3
试想一个场景,小明去专业律所咨询所咨询一件事情,由于小明不具备专业的法律知识,很有可能刚开始的问题没有直切要点,但是专业的律所咨询所非常有经验,知道通常应该如何应对小明这种前来咨询的客户,一步一步引导客户提供关键的信息,来帮助完善一个真实的问题。经过多轮的对话之后小明真正想要问的问题变得清晰起来,同时经过多轮的对话,更多的上下文被补充进来。这个时候专业的法律咨询所就已经能够针对这个完整的对话分析出小明的真实诉求,并给出直切要害的回答。那对应到 LLM 应该怎么做呢?
其实,仅仅需要将这个收集到的多轮对话内容和业务原始的输入交给 LLM,让 LLM 负责总结出一个真正用于检索的终极问题,拿到这个问题之后进入典型的 RAG 流程继续处理,一个典型 Prompt 的实现如下:
Given the following conversation and a follow up question, rephrase the follow up
question to be a standalone question.
Chat History:
{chat_history}
Follow Up Input: {question}
Standalone Question:
这个方案理想状态是通过多轮的对话来补齐 RAG 系统需要的关于用户问题的尽量完整的上下文,这其中有一个难点在于:如何保证多轮对话一定能够做到有效的上下文收集?
可行的方式是根据用户最原始的输入进行多维度切分,将不同维度得到的切片做一次 embedding & search,之后根据每一个切片返回的内容结合用户原始输入,“猜测”用户真实诉求,提供给用户进行参考。
霰弹枪式的去尝试撞击最终的答案。
小结
作为一种能够显著提升信息质量的通用架构,RAG 的潜力和应用场景无疑引发了无尽的想象。OpenAI,一家以其优秀的模型质量和创新能力而闻名的公司,也在其首届 DevDay 上宣布下场和生态玩家公开竞技,发布了类似 Agent + RAG 的开发模型(Assistant)。这种模型被视为其核心生态能力,并被积极运用于对外业务。
世界很魔幻很精彩,LLM 最终将带领我们走向何方,保持满怀的期待。
参考
blog.langchain.dev/query-trans…
blog.langchain.dev/semi-struct…
platform.openai.com/docs/api-re…