LlamaIndex(也称为GPT Index)是一个用户友好的界面,它将您的外部数据连接到大型语言模型(Large Language Models, llm)。它提供了一系列工具来简化流程,包括可以与各种现有数据源和格式(如api、pdf、文档和SQL)集成的数据连接器。此外,LlamaIndex为结构化和非结构化数据提供索引,可以毫不费力地与大语言模型一起使用。
本文将讨论LlamaIndex提供的不同类型的索引以及如何使用它们。这可能包括列表索引、矢量存储索引、树索引和关键字表索引的分解,以及特殊索引,如图索引、Pandas索引、SQL索引和文档摘要索引。此外,我将详细介绍每个索引的情况,可能有必要讨论使用LlamaIndex的成本,并将其与其他选项进行比较。
商业ChatGPT还不够好吗?
是的,它在一般用例中可能足够了,但请记住,我们的目标是在您的文档湖(类比数据湖)上构建通用聊天机器人应用程序。想想你的公司文档可能有超过1000页,那么ChatGPT广告将不足以分析你的东西。主要原因是token的限制。
1,000 tokens大概是750单词
GPT-3, GPT-3.5, GPT-4和LlamaIndex被Flyps接受的tokens数量
如果可用的tokens不多,则无法在prompt中输入更大的数据集,这可能会限制您对模型的操作。然而,您仍然可以训练模型,尽管有一些优点和缺点需要考虑。不过别担心,LlamaIndex会帮你的!
使用LlamaIndex,您可以为各种数据集(如文档、pdf和数据库)建立索引,然后轻松地查询它们以查找所需的信息。
想象一下,只需点击几下就可以访问您需要的所有信息!您可以直接向知识库、Slack和其他通信工具以及数据库和几乎所有SaaS内容提出复杂的问题,而无需以任何特殊方式准备数据。最好的部分是什么?您将得到由GPT推理能力支持的答案,所有这些都在几秒钟内完成,甚至不必将任何内容复制和粘贴到prompts符中。
通过正确实现GPT Index,您可以使这一切成为可能!在下一节中,我们将深入研究不同类型的索引,以及为您的应用程序准备的适用代码。
在能够有效地用自然语言提出问题并获得准确的答案之前,有必要对相关数据集进行索引。如前所述,LlamaIndex能够索引广泛的数据类型,随着GPT-4即将到来,多模式索引也将很快可用。这一部分,我们将研究LlamaIndex提供的不同类型的索引,看看什么索引用于什么用例。
在深入了解索引的细节之前,您应该知道LlamaIndex的核心是将文档分解为多个Node对象。节点是LlamaIndex中的一等公民。节点表示源文档的“块”,无论是文本块、图像块还是更多。它们还包含元数据以及与其他节点和索引结构的关系信息。当您创建索引时,它抽象了节点的创建,但是,如果您的需求需要,您可以手动为文档定义节点。
让我们先设置一些底层代码。
1 2 |
pip install llama-index pip install openai |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
import os os.environ['OPENAI_API_KEY'] = ' |
列表索引是一种简单的数据结构,其中节点按顺序存储。在索引构建期间,文档文本被分块、转换为节点并存储在列表中。
来自LlamaIndex官方文件
在查询期间,如果没有指定其他查询参数,LlamaIndex只是将列表中的所有node加载到Response Synthesis模块中。
来自LlamaIndex官方文件
列表索引确实提供了许多查询列表索引的方法,从基于嵌入的查询中获取前k个邻居,或者添加一个关键字过滤器,如下所示:
来自LlamaIndex官方文件
此列表索引对于综合跨多个数据源的信息的答案非常有用
LlamaIndex为列表索引提供Embedding支持。除了每个节点存储文本之外,每个节点还可以选择存储Embedding。在查询期间,我们可以在调用LLM合成答案之前,使用Embeddings对节点进行最大相似度检索。
由于使用Embeddings的相似性查找(例如使用余弦相似性)不需要LLM调用,Embeddings作为一种更便宜的查找机制,而不是使用大语言模型来遍历节点
这意味着在索引构建过程中,LlamaIndex不会调用LLM来生成Embedding,而是在查询时生成。这种设计选择避免了在索引构建期间为所有文本块生成Embeddings的需要,这可能会导致大量数据的开销。
您很快就会发现,将多个索引组合在一起可以帮助您避免高昂的Embedding成本。但这还不是全部——它还可以提高应用程序的整体性能!另一种方法是使用自定义Embedding(而不是使用OpenAI),但我们不会在本文中研究这种方法,因为它值得另一种方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
from llama_index import GPTKeywordTableIndex, SimpleDirectoryReader from IPython.display import Markdown, display from langchain.chat_models import ChatOpenAI ## by default, LlamaIndex uses text-davinci-003 to synthesise response # and text-davinci-002 for embedding, we can change to # gpt-3.5-turbo for Chat model index = GPTListIndex.from_documents(documents) query_engine = index.as_query_engine() response = query_engine.query("What is net operating income?") display(Markdown(f"{response}")) ## Check the logs to see the different between th ## if you wish to not build the index during the index construction # then need to add retriever_mode=embedding to query engine # query with embed_model specified query_engine = new_index.as_query_engine( retriever_mode="embedding", verbose=True ) response = query_engine.query("What is net operating income?") display(Markdown(f"{response}")) |
它是最常见且易于使用的,允许对大型数据语料库回答查询
来自LlamaIndex官方文件
默认情况下,GPTVectorStoreIndex
使用内存中的 SimpleVectorStore
作为默认存储上下文的一部分初始化。
与列表索引不同,基于向量存储的索引在索引构建期间生成Embeddings
这意味着在索引构建期间将调用LLM端点以生成Embeddings数据。
Query(查询)向量存储索引包括获取top-k最相似的节点,并将它们传递到我们的响应合成模块。
来自LlamaIndex官方文件
1 2 3 4 5 6 |
from llama_index import GPTVectorStoreIndex index = GPTVectorStoreIndex.from_documents(documents) query_engine = index.as_query_engine() response = query_engine.query("What did the author do growing up?") response |
它对总结一组文件很有用
树状索引是树结构索引,其中每个节点是子节点的摘要。在索引构建期间,树以自下而上的方式构建,直到我们最终得到一组根节点。
树状索引从一组节点(成为该树中的叶节点)构建层次树。
来自LlamaIndex官方文件
查询树状索引涉及从根节点向下遍历到叶节点。默认情况下(child_branch_factor=1
),查询在给定父节点的情况下选择一个子节点。如果child_branch_factor=2
,则查询在每个级别选择两个子节点。
来自LlamaIndex官方文件
与向量索引不同,LlamaIndex不会调用LLM来生成Embedding,而是在查询时生成。Embeddings被惰性地生成,然后缓存(如果retriver_mode ="Embedding"
在query(…)
期间指定),而不是在索引构建期间。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
from llama_index import GPTTreeIndex new_index = GPTTreeIndex.from_documents(documents) response = query_engine.query("What is net operating income?") display(Markdown(f"{response}")) ## if you want to have more content from the answer, # you can add the parameters child_branch_factor # let's try using branching factor 2 query_engine = new_index.as_query_engine( child_branch_factor=2 ) response = query_engine.query("What is net operating income?") display(Markdown(f"{response}")) |
为了在查询期间构建树状索引,我们需要将retriver_mode和response_mode添加到查询引擎,并将GPTTreeIndex中的build_tree参数设置为False
1 2 3 4 5 6 |
index_light = GPTTreeIndex.from_documents(documents, build_tree=False) query_engine = index_light.as_query_engine( retriever_mode="all_leaf", response_mode='tree_summarize', ) query_engine.query("What is net operating income?") |
这对于将查询路由到不同的数据源非常有用
关键字表索引从每个Node提取关键字,并构建从每个关键字到该关键字对应的Node的映射。
来自LlamaIndex官方文件
在查询时,我们从查询中提取相关关键字,并将其与预提取的Node关键字进行匹配,获取相应的Node。提取的节点被传递到响应合成模块。
来自LlamaIndex官方文件
注意到
GPTKeywordTableIndex
-使用LLM从每个文档中提取关键字,这意味着它确实需要在构建期间调用LLM
但是,如果您使用GPTSimpleKeywordTableIndex
,它使用regex关键字提取器从每个文档中提取关键字,则在构建期间不会调用LLM
1 2 3 4 |
from llama_index import GPTKeywordTableIndex index = GPTKeywordTableIndex.from_documents(documents) query_engine = index.as_query_engine() response = query_engine.query("What is net operating income?") |
它对于构建知识图谱很有用
使用LlamaIndex,您可以通过在现有索引之上构建索引来创建复合索引。该特性使您能够有效地索引完整的文档层次结构,并为GPT提供量身定制的知识。
通过利用可组合性,您可以在多个级别定义索引,例如为单个文档定义低级索引,为文档组定义高级索引。考虑下面的例子:
您可以为每个文档中的文本创建树索引。
生成一个列表索引,涵盖所有的树索引为您的整个文档集合。
通过一个场景编写代码:我们将执行以下步骤来演示可组合性图索引的能力:
从多个文档创建树索引
从树索引生成摘要。如前所述,Tree Index对于总结文档集合很有用。
接下来,我们将在3个树索引的顶部创建一个列表索引的图。为什么?因为列表索引适合于合成跨多个数据源组合信息的答案。
最后查询图。
实现:
我会阅读苹果2022年和2023年的10k报告,并在两个季度之间提出财务问题。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
## re years = ['Q1-2023', 'Q2-2023'] UnstructuredReader = download_loader('UnstructuredReader', refresh_cache=True) loader = UnstructuredReader() doc_set = {} all_docs = [] for year in years: year_docs = loader.load_data(f'../notebooks/documents/Apple-Financial-Report-{year}.pdf', split_documents=False) for d in year_docs: d.extra_info = {"quarter": year.split("-")[0], "year": year.split("-")[1], "q":year.split("-")[0]} doc_set[year] = year_docs all_docs.extend(year_docs) |
为每个季度创建矢量指数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
## setting up vector indicies for each year #--- # initialize simple vector indices + global vector index # this will use OpenAI embedding as default with text-davinci-002 service_context = ServiceContext.from_defaults(chunk_size_limit=512) index_set = {} for year in years: storage_context = StorageContext.from_defaults() cur_index = GPTVectorStoreIndex.from_documents( documents=doc_set[year], service_context=service_context, storage_context=storage_context ) index_set[year] = cur_index # store index in the local env, so you don't need to do it over again storage_context.persist(f'./storage_index/apple-10k/{year}') |
从树索引生成摘要。如前所述,Tree Index对于总结文档集合很有用。
1 2 |
# describe summary for each index to help traversal of composed graph index_summary = [index_set[year].as_query_engine().query("Summary this document in 100 words").response for year in years] |
接下来,我们将在3个树索引之上创建一个列表索引的Graph
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
### Composing a Graph to Synthesize Answers from llama_index.indices.composability import ComposableGraph from langchain.chat_models import ChatOpenAI from llama_index import LLMPredictor # define an LLMPredictor set number of output tokens llm_predictor = LLMPredictor(llm=ChatOpenAI(temperature=0, max_tokens=512, model_name='gpt-3.5-turbo')) service_context = ServiceContext.from_defaults(llm_predictor=llm_predictor) storage_context = StorageContext.from_defaults()\ ## define a list index over the vector indicies ## allow us to synthesize information across each index graph = ComposableGraph.from_indices( GPTListIndex, [index_set[y] for y in years], index_summaries=index_summary, service_context=service_context, storage_context=storage_context ) root_id = graph.root_id #save to disk storage_context.persist(f'./storage_index/apple-10k/root') ## querying graph custom_query_engines = { index_set[year].index_id: index_set[year].as_query_engine() for year in years } query_engine = graph.as_query_engine( custom_query_engines=custom_query_engines ) response = query_engine.query("Outline the financial statement of Q2 2023") response.response |
想知道我们如何利用Langchain Agent作为聊天机器人,请关注/订阅未来的更多更新:)
它对结构化数据很有用
简单和非常直接,我将直接进入演示。
Pandas Index:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
from llama_index.indices.struct_store import GPTPandasIndex import pandas as pd df = pd.read_csv("titanic_train.csv") index = GPTPandasIndex(df=df) query_engine = index.as_query_engine( verbose=True ) response = query_engine.query( "What is the correlation between survival and age?", ) response |
SQL Index:
考虑一个很酷的应用程序,你可以将你的LLM应用程序附加到你的数据库,并在它上面提问。这个示例代码取自这里
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
# install wikipedia python package !pip install wikipedia from llama_index import SimpleDirectoryReader, WikipediaReader from sqlalchemy import create_engine, MetaData, Table, Column, String, Integer, select, column wiki_docs = WikipediaReader().load_data(pages=['Toronto', 'Berlin', 'Tokyo']) engine = create_engine("sqlite:///:memory:") metadata_obj = MetaData() # create city SQL table table_name = "city_stats" city_stats_table = Table( table_name, metadata_obj, Column("city_name", String(16), primary_key=True), Column("population", Integer), Column("country", String(16), nullable=False), ) metadata_obj.create_all(engine) from llama_index import GPTSQLStructStoreIndex, SQLDatabase, ServiceContext from langchain import OpenAI from llama_index import LLMPredictor llm_predictor = LLMPredictor(llm=LLMPredictor(llm=ChatOpenAI(temperature=0, max_tokens=512, model_name='gpt-3.5-turbo'))) service_context = ServiceContext.from_defaults(llm_predictor=llm_predictor) sql_database = SQLDatabase(engine, include_tables=["city_stats"]) sql_database.table_info # NOTE: the table_name specified here is the table that you # want to extract into from unstructured documents. index = GPTSQLStructStoreIndex.from_documents( wiki_docs, sql_database=sql_database, table_name="city_stats", service_context=service_context ) # view current table to verify the answer later stmt = select( city_stats_table.c["city_name", "population", "country"] ).select_from(city_stats_table) with engine.connect() as connection: results = connection.execute(stmt).fetchall() print(results) query_engine = index.as_query_engine( query_mode="nl" ) response = query_engine.query("Which city has the highest population?") |
在底层,有一个Langchain库插件可以使用。我们将在另一篇文章中介绍Langchain。
这是一个全新的LlamaIndex数据结构,它是为了问答而制作的。到目前为止,我们已经讨论了单个索引,当然我们可以通过使用单个索引或将多个索引组合在一起来构建LLMQA应用程序。
通常,大多数用户以以下方式开发基于llm的QA系统:
它们获取源文档并将其分成文本块。
然后将文本块存储在矢量数据库中。
在查询期间,通过使用相似度和/或关键字过滤器进行Embedding来检索文本块。
执行响应综合。
然而,这种方法存在一些影响检索性能的局限性。
现有方法的缺点:
为了增强检索结果,一些开发人员添加了关键字过滤器。然而,这种方法有其自身的挑战,例如通过手工或使用NLP关键字提取/主题标记模型为每个文档确定适当的关键字,以及从查询中推断正确的关键字。
这就是LlamaIndex引入文档摘要索引的地方,它可以为每个文档提取和索引非结构化文本摘要,从而提高了现有方法的检索性能。该索引包含比单个文本块更多的信息,并且比关键字标签具有更多的语义含义。它还允许灵活的检索,包括LLM和基于嵌入的方法。
在构建期间,该索引摄取文档并使用LLM从每个文档提取摘要。在查询期间,根据摘要检索相关文档进行查询,使用以下方法:
- **基于LLM的检索:**获取文档摘要集合,请求LLM识别相关文档+相关性评分
- **基于嵌入的检索:**利用摘要Embedding相似度检索相关文档,并对检索结果的数量施加top-k限制。
注意:文档摘要索引的检索类为任何选定的文档检索所有节点,而不是在节点级返回相关块。
看看例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
import nest_asyncio nest_asyncio.apply() from llama_index import ( SimpleDirectoryReader, LLMPredictor, ServiceContext, ResponseSynthesizer ) from llama_index.indices.document_summary import GPTDocumentSummaryIndex from langchain.chat_models import ChatOpenAI wiki_titles = ["Toronto", "Seattle", "Chicago", "Boston", "Houston"] from pathlib import Path import requests for title in wiki_titles: response = requests.get( 'https://en.wikipedia.org/w/api.php', params={ 'action': 'query', 'format': 'json', 'titles': title, 'prop': 'extracts', # 'exintro': True, 'explaintext': True, } ).json() page = next(iter(response['query']['pages'].values())) wiki_text = page['extract'] data_path = Path('data') if not data_path.exists(): Path.mkdir(data_path) with open(data_path / f"{title}.txt", 'w') as fp: fp.write(wiki_text) # Load all wiki documents city_docs = [] for wiki_title in wiki_titles: docs = SimpleDirectoryReader(input_files=[f"data/{wiki_title}.txt"]).load_data() docs[0].doc_id = wiki_title city_docs.extend(docs) # # LLM Predictor (gpt-3.5-turbo) llm_predictor_chatgpt = LLMPredictor(llm=ChatOpenAI(temperature=0, model_name="gpt-3.5-turbo")) service_context = ServiceContext.from_defaults(llm_predictor=llm_predictor_chatgpt, chunk_size_limit=1024) # default mode of building the index response_synthesizer = ResponseSynthesizer.from_args(response_mode="tree_summarize", use_async=True) doc_summary_index = GPTDocumentSummaryIndex.from_documents( city_docs, service_context=service_context, response_synthesizer=response_synthesizer ) doc_summary_index.get_document_summary("Boston") |
它通过在一组文档上以以下形式提取知识三元组(主题、谓词、对象)来构建索引。
在查询期间,它可以只使用知识图作为上下文进行查询,也可以利用来自每个实体的底层文本作为上下文进行查询。通过利用底层文本,我们可以对文档的内容进行更复杂的查询。
把一个图想象成这样,你可以看到所有的边和顶点都是相互连接的。
来自LlamaIndex官方文件
你可以看看这个页面作为参考。
在我们的PDF聊天机器人实施大语言模型期间,我提请注意我们想与您分享的重要方面,即:索引成本和索引时间(速度)。
索引费用是需要考虑的一个关键因素,正如我在本文前面所强调的那样。这在处理大量数据集时尤为重要,这也是我提倡使用LlamaIndex的原因。
你可以找到各个OpenAI模型的价格(https://openai.com/pricing)。
第二个重要问题是文档索引的时间,即为操作准备整个解决方案的时间。根据我的实验,索引时间各不相同,但这是一次性的,也取决于OpenAI服务器。
通常,40页的pdf大约需要5秒。想象一下,一个拥有超过10万页的庞大数据集,可能需要几天的时间。我们可以利用async方法来减少索引时间。我将在另一篇文章中写这一点。