动手点关注
干货不迷路
本文旨在让无大模型开发背景的工程师或者技术爱好者无痛理解大语言模型应用开发的理论和主流工具,因此会先从与LLM应用开发相关的基础概念谈起,并不刻意追求极致的严谨和完备,而是从直觉和本质入手,结合笔者调研整理及消化理解,帮助大家能够更容易的理解LLM技术全貌,大家可以基于本文衍生展开,结合自己感兴趣的领域深入研究。若有不准确或者错误的地方也希望大家能够留言指正。
本文体系完整,内容丰富,由于内容比较多,分多次连载 。
第一部分 基础概念
1.机器学习场景类别
2.机器学习类型(LLM相关)
3.深度学习的兴起
4.基础模型
第二部分 应用挑战
1.问题定义与基本思路
2.基本流程与相关技术
1)Tokenization与Embbeding
2)向量数据库
3)finetune(微调)
4)模型部署与推理
5)prompt
6)编排与集成
7)其它(预训练等)
第三部分 场景案例
常用参考
2.基本流程与相关技术
点此查看前面内容:
4)编排与集成
经过前面的介绍,我们对大模型应用开发的通常涉及到的重要构成组件都有了介绍,然而这些零散的组件需要有一根”线“将它们串联起来,最终变成用户可以感受到的LLM应用。这里编排集成服务便起到了这样的“线“的作用,它作为粘合剂和应用骨架,在整个应用构成中起到了提纲挈领的作用。
在本章你将学习到:
1)相关理论和应用开发范式
2)Langchain框架介绍
3)LlamaIndex框架介绍
4)Semantic Kernel框架介绍
5) 技术展望
llama-index概述
如果说langchain是编排框架里的全能王者,那么llama-index就是RAG类LLM应用构建的单项冠军。llama-index最早定位与langchain相似,但随着这一领域框架的不断发展,llama-index选择了一条与langchain不太相同的路,langchain强于应用流程编排与集成,而它将重点放在了数据摄取、转换、索引等数据处理方面,专注于构建RAG应用,并且它也可作为数据层面的框架被集成在langchain中,增强基于langchain构建的应用的效果。虽然近来也在向Agent领域探索,但其核心的亮点还是围绕着数据展开,其agent也是专注于Data Agent领域。
下面是两者 更 多 层面的比较:
在前面的文章里我们知道,对于一个RAG应用来讲,其核心在于如何建库及检索,进而提升检索的召回率和准确率。起初,在构建原型应用时,我们并没有足够其生成的效果及系统的效率,然而,随着不断深入,我们会发现制约一个RAG应用的关键除了大模型本身外,数据层面的复杂性才是RAG应用的难点。比如,如何有效的从不同存储中读取文档,进行转换,生成相应的索引,在检索阶段采用何种策略进行检索,这些方方面面都有很多的最佳实践需要总结沉淀。而llamaindex其中一个亮点就是它拥有一种面向大规模文本数据集的索引技术,可为结构化和非结构化数据提供索引支持。它将庞大的文本数据集划分为多个小块,并通过索引表快速定位目标文档,以加快检索和处理速度。
通过官方介绍可知,目前llamaindex在以下几方面面向各类初中高级开发者提供支持覆盖不同场景复杂度(从开箱即用到低阶模块定制)的LLM RAG应用:
1)数据连接器(Data connectors)从原始数据源和格式中摄取您的现有数据。这些数据可以是 API、PDF、SQL 以及(更多)其他数据。
2)数据索引(Data indexes)将您的数据结构化为便于 LLM 使用且性能良好的中间表示形式。
3)引擎(Engines)提供对数据的自然语言访问。例如:查询引擎(QueryEngine)是功能强大的检索界面,可用于知识扩充输出。聊天引擎(ChatEngine)是与数据进行多信息 "来回 "交互的对话界面。
4)数据代理(Data agents )是由 LLM 驱动的知识工作者,通过工具(从简单的辅助功能到 API 集成等)进行增强。
5)应用程序集成(Application integrations)将 LlamaIndex 与LLM生态系统的其他部分如 LangChain、Flask、Docker、ChatGPT连接起来。
可以利用llama-index很方便地构建一个RAG应用,也能结合实际需求个性化定制增强。
代码如下:
import os.path
from llama_index import (
VectorStoreIndex,
SimpleDirectoryReader,
StorageContext,
load_index_from_storage,
)
# check if storage already exists
PERSIST_DIR = "./storage"
if not os.path.exists(PERSIST_DIR):
# load the documents and create the index
documents = SimpleDirectoryReader("data").load_data()
index = VectorStoreIndex.from_documents(documents)
# store it for later
index.storage_context.persist(persist_dir=PERSIST_DIR)
else:
# load the existing index
storage_context = StorageContext.from_defaults(persist_dir=PERSIST_DIR)
index = load_index_from_storage(storage_context)
# either way we can now query the index
query_engine = index.as_query_engine()
response = query_engine.query("What did the author do growing up?")
print(response)
在此基础上,结合实际的需要还可以进行定制,比如想要更换向量数据库。只需要定义storage_context并指定即可,它负责存储文档、embedding和索引的存储后端。
import chromadb
from llama_index.vector_stores import ChromaVectorStore
from llama_index import StorageContext
chroma_client = chromadb.PersistentClient()
chroma_collection = chroma_client.create_collection("quickstart")
vector_store = ChromaVectorStore(chroma_collection=chroma_collection)
storage_context = StorageContext.from_defaults(vector_store=vector_store)
from llama_index import VectorStoreIndex, SimpleDirectoryReader
documents = SimpleDirectoryReader("data").load_data()
index = VectorStoreIndex.from_documents(
documents, storage_context=storage_context
)
query_engine = index.as_query_engine()
response = query_engine.query("What did the author do growing up?")
print(response)
领域概念及生命周期
llamaindex结合RAG架构应用的特点,将RAG整个生命周期分成了五步。
1)加载(Loading):这是指将数据从其所在的位置(无论是文本文件、PDF、其他网站、数据库还是 API)导入您的管道。LlamaHub 提供了数百种连接器供您选择。
2)索引(Indexing):这意味着创建一种数据结构,以便查询数据。对于 LLM 而言,这几乎总是意味着要创建向量嵌入、数据意义的数字表示,以及许多其他元数据策略,以便轻松准确地查找上下文相关数据。
3)存储(Storing):一旦为数据编制了索引,您几乎总是希望存储索引以及其他元数据,以避免重新编制索引。
4)查询(Querying):对于任何给定的索引策略,您都可以通过多种方式利用 LLM 和 LlamaIndex 数据结构进行查询,包括子查询、多步骤查询和混合策略。
5)评估(Evaluating):任何管道中的一个关键步骤都是检查其相对于其他策略的有效性,或者当你进行更改时的有效性。评估可以客观地衡量查询响应的准确性、忠实性和快速性。
其中有一些关键的概念需要了解:
1)节点和文档(Node和Document):文档是任何数据源的容器--例如,PDF、API 输出或从数据库中检索数据。节点是 LlamaIndex 中数据的原子单位,代表源文档的一个 "块(chunk)"。节点具有元数据,这些元数据将节点与其所在的文档以及其他节点相关联。
2)连接器(Connector):数据连接器(通常称为Reader)可将不同数据源和数据格式的数据导入文档和节点。
3)索引(Index):一旦摄取了数据,LlamaIndex 就会帮你将数据索引为易于检索的结构。这通常涉及生成embedding,并将其存储在称为矢量存储的专用数据库中。索引还可以存储有关数据的各种元数据。
4)Embbeding: 数据的向量化表示,称为embbeding。在过滤相关性数据时,LlamaIndex 会将查询转换为embedding,而向量数据库会查找与查询的嵌入在数值上相似的数据。
5)检索器(Retriever):检索器定义了在给定查询时如何从索引中高效检索相关上下文。检索策略是检索数据相关性和检索效率的关键。
6)路由器(Router):路由决定使用哪种检索器从知识库中检索相关上下文。更具体地说,RouterRetriever 类负责选择一个或多个候选检索器来执行查询。它们使用选择器,根据每个候选检索器的元数据和查询选择最佳选项。
7)节点后处理器(node postprocessor ):节点后处理器接收一组检索到的节点,并对其应用转换、过滤或重新排序逻辑。
8)响应合成器(response synthesizer ):响应合成器利用用户查询和给定的检索文本块集从 LLM 生成响应。
9)查询引擎(query engine):查询引擎是一个端到端的管道,可让您对数据提出问题。它接收自然语言查询并返回响应,同时检索参考上下文并将其传递给 LLM。
10)对话引擎(chat engine):聊天引擎是一种端到端管道,用于与数据进行对话(多个来回对话,而不是单一的问答)。
11)Agent:Agent是由 LLM 驱动的自动决策者,通过一系列工具与世界互动。Agent可以采取任意数量的步骤来完成给定任务,动态决定最佳行动方案,而不是遵循预先确定的步骤。这使它在处理更复杂的任务时更具灵活性。
框架特色
llama-index早期得以出圈的一个很大原因是它在数据处理,特别是索引结构上的创新。对于一个RAG应用来讲,召回的数据是否准确关乎大模型最终生成的答案是否准确的关键,而简单的固定长度的向量索引并不能很好的满足实际的需求,比如将一段完整的语义拆分到多个chunk中,又或者简单的chunk块缺少上下文、元数据等等。
llama-index切入到这一领域,提出了很多的索引结构以及对应索引的存储以便适应不同的业务场景。下面是一些常见的索引结构:
- VectorStoreIndex =================
这是最基本,也是最常见的索引结构,它将文档分割成节点(chunk及元数据)。然后,它会为每个节点的文本创建embedding,存储在向量数据库中,以便向量检索使用。在检索时,查询输入转换为一个embeding,然后计算输入和数据库中embeding的相似性,最终取得Top-K个候选的embedding作为召回的有效chunk。
- Su m mary Index
原来也叫“List-Index”,摘要索引以顺序链的形式存储节点。查询时,如果没有指定其他查询参数,LlamaIndex 会将列表中的所有节点召回,以便后续大模型生成。除此之外,摘要索引还提供了多种查询摘要索引的方法,包括基于embedding的查询(将获取前 k 个相邻索引),或添加关键词过滤器:
- Tree Index
树形索引从一组节点(成为树中的叶节点)构建出一棵分层树,每个节点是子节点的摘要,在索引构建期间,树以自下而上的方式构建,直到我们最终得到一组根节点。查询树索引需要从根节点向下遍历到叶节点。默认情况下(child_branch_factor=1),查询会在给定父节点的情况下选择一个子节点。如果child_branch_factor=2,查询会在每一级选择两个子节点。
- Keyword Table Index
关键字表索引从每个节点中提取关键字,并建立每个关键字到该关键字对应节点的映射。在查询期间,我们从查询中提取相关关键词,并将这些关键词与预先提取的节点关键词进行匹配,以获取相应的节点。提取的节点将被传递给我们的响应合成模块。
from llama_index import TreeIndex
storage_context = storage_context.from_defaults()
index1 = TreeIndex.from_documents(doc1, storage_context=storage_context)
index2 = TreeIndex.from_documents(doc2, storage_context=storage_context)
index3 = TreeIndex.from_documents(doc3, storage_context=storage_context)
...
from llama_index.indices.composability import ComposableGraph
graph = ComposableGraph.from_indices(
SummaryIndex,
[index1, index2, index3],
index_summaries=[index1_summary, index2_summary, index3_summary],
storage_context=storage_context,
)
...
custom_query_engines = {
index.index_id: index.as_query_engine(child_branch_factor=2)
for index in [index1, index2, index3]
}
query_engine = graph.as_query_engine(custom_query_engines=custom_query_engines)
response = query_engine.query("Where did the author grow up?")
提升RAG应用的性能和效果,将其从一个原型应用变成生产级应用,Index只是其中一个方面,llamaindex在这方面做出了很多有意义的探索,这其中包含Embedding、chunksize、metadata filter,高级的检索策略(混合检索、递归检索、查询路由等)、可观测性、效果评估,笔者之前文章中也有过一些介绍:
改进召回(Retrieval)和引入重排(Reranking)提升RAG架构下的LLM应用效果
引入元数据(metadata)提升RAG架构下LLM应用的效果和管控精度
由于本文为概要性介绍,对生产级RAG感兴趣的可以关注后续专门的文章。
llama-index当下已经成为最热门的RAG应用构建的框架之一,除了传统的单一文本模态也在向多模态方向发展,另一方面,llama-index也在尝试向数据Agent方向发展,这些不仅对于llama-index项目本身有利,也是顺应未来趋势的一个很好的探索。作为llama-index的创始人jerryWU非常乐于分享,在他的blog里有大量的有关LLM应用开发的文章值得学习。
点此查看合集: