当前位置:   article > 正文

用通俗易懂的方式讲解:想搭建企业级的聊天机器人,为什么还需要LlamaIndex?_gptvectorstoreindex

gptvectorstoreindex

上一篇文章我们介绍了使用LlamaIndex构建PDF聊天机器人,本文将介绍一下LlamaIndex的基本概念和原理。

LlamaIndex 简介

LlamaIndex(也称为GPT Index)是一个用户友好的界面,可将外部数据连接到大型语言模型(LLM)。它提供了一系列工具来简化流程,包括可以与各种现有数据源和格式(如API、PDF、文档和SQL)集成的数据连接器。此外,LlamaIndex为结构化和非结构化数据提供索引,可以轻松地与LLM一起使用。

本文将讨论LlamaIndex提供的不同类型的索引以及如何使用它们。LlamaIndex索引包括列表索引、矢量存储索引、树索引和关键字表索引,当然也包括一些特殊索引,比如图索引、Pandas索引、SQL索引和文档摘要索引。

有了ChatGPT这么强大的LLM,搭建PDF聊天机器人不够吗?为什么还需要LlamaIndex?

其实,如果想搭建企业级的聊天机器人,那么ChatGPT的上下文是不够的,常见LLM上下文大致如下:

  • GPT-3:约2000个tokens

  • GPT-3.5:约4000个tokens

  • GPT-4:最多32.000个tokens

Note:1000个tokens大约有750个words

图片

LlamaIndex将文档分解为多个Node对象,Node表示源文档的“块”,这些源文档可以是文本块、图像或者其他内容。它们还包含元数据以及与其他节点和索引结构的关系信息。创建索引,其实就是创建这些Node,也可以手动为文档定义Node。

通俗易懂讲解大模型系列

使用LlamaIndex构建索引

下面我们看一下如何创建这些索引:

安装LlamaIndex相关库

pip install llama-index
pip install openai
  • 1
  • 2

设置OpenAI API Key

import os
os.environ['OPENAI_API_KEY'] = '<YOUR_OPENAI_API_KEY>'

import logging
import sys

## showing logs
logging.basicConfig(stream=sys.stdout, level=logging.INFO)
logging.getLogger().addHandler(logging.StreamHandler(stream=sys.stdout))


## load the PDF
from langchain.text_splitter import RecursiveCharacterTextSplitter
from llama_index import download_loader

# define loader
UnstructuredReader = download_loader('UnstructuredReader', refresh_cache=True)
loader = UnstructuredReader()

# load the data
documents = loader.load_data('../notebooks/documents/Apple-Financial-Report-Q1-2022.pdf',split_documents=False)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

列表索引(List Index)

列表索引是一个简单的数据结构,其中Node存储在一个序列中。文档文本被分块,转换为节点,并在索引构建过程中存储在列表中。如下图所示:

图片

在查询期间,如果没有指定其他查询参数,LlamaIndex只需将列表中的所有节点加载到Response Synthesis模块中。

图片

列表索引提供了多种查询列表索引方式,比如通过embedding查询最相关的top-k块,或者使用关键字过滤,如下所示:

图片

LlamaIndex为列表索引提供embedding支持。每个节点存储文本之外还可以选择存储embedding。在查询期间,我们可以使用embedding对节点进行最大相似度检索,然后调用LLM来生成答案。

由于使用embedding的相似性查找(例如,使用余弦相似性)不需要LLM调用,因此embedding是一种代价更低的查询机制,而不需要遍历LLM节点。这意味着在索引构建过程中,LlamaIndex不会调用LLM来生成embedding,而是在查询时生成embedding。这种设计避免了在索引构建期间为所有文本块生成embedding,这对于大数据来说可能代价高昂。

下面是一个具体的使用案例:

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"<b>{response}</b>"))

## 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"<b>{response}</b>"))
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

向量存储索引(Vector Store Index)

向量存储索引是最常见且使用简单的,允许在大量数据中回答查询。

图片

默认情况下,GPTVectorStoreIndex使用内存中的SimpleVectorStore。与List Index不同,Vector Store Index在构建索引过程中就生成了embedding,这意味着在构建索引以生成embedding数据期间就调用了LLM。

查询Vector Store Index前k个最相似的节点,并将它们传递到我们的Response Synthesis模块中。

图片

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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

树索引(Tree Index)

树索引是一个树结构索引,其中每个节点都是子节点的摘要。在索引构建过程中,树是以自下而上的方式构建的,直到我们最终得到一组根节点。树索引从一组节点(成为该树中的叶节点)构建一个层次树。

图片
查询树索引涉及从根节点向下遍历到叶节点。默认情况下,(child_branch_factor=1),查询在给定父节点的情况下选择一个子节点。如果child_branch_factor=2,则查询会在每个级别选择两个子节点。

图片

与Vector Index不同,Tree Index是在查询时生成embedding,当然如果在查询时指定retriever_mode=“embedding”,那么embedding会延迟生成并被缓存起来。

from llama_index import GPTTreeIndex

new_index = GPTTreeIndex.from_documents(documents)
response = query_engine.query("What is net operating income?")
display(Markdown(f"<b>{response}</b>"))

## 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"<b>{response}</b>"))
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

要在查询期间构建Tree Index,我们需要向查询引擎添加retrier_mode和response_mode,并将GPTTreeIndex中的build_Tree参数设置为False。

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?")
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

关键词表索引(Keyword Table Index)

关键字表索引从每个节点提取关键字,并构建从每个关键字到该关键字的相应节点的映射。

图片

在查询期间,我们从查询中提取相关关键字,并将这些关键字与预先提取的Node关键字进行匹配,以获取相应的Node。提取的节点被传递到我们的Response Synthesis模块。

图片

对于GPTKeywordTableIndex,一般情况是使用LLM从每个文档中提取关键字,这意味着它在构建时确实需要LLM调用。但是,如果使用GPTSimpleKeywordTableIndex(使用正则表达式关键字提取器从每个文档中提取关键字),则在构建时不会调用LLM。

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?")
  • 1
  • 2
  • 3
  • 4

可组合性图索引(Composability Graph Index)

LlamaIndex通过在现有Index的基础上composite indices,此功能能够高效地索引完整的文档层次结构,并为GPT提供量身定制的知识。通过利用可组合性,您可以在多个级别定义索引,例如单个文档的较低级别索引和文档组的较高级别索引。考虑以下示例:

  • 可以为每个文档中的文本创建一个树索引。

  • 生成一个列表索引,该索引覆盖整个文档集合的所有树索引。

下面通过一个实例来说明一下可组合性图索引的能力:

  1. 从多个文档创建树索引

  2. 从树索引生成摘要。如前所述,树索引对于汇总文档集合非常有用。

  3. 接下来,我们将创建一个Graph,它在3个树索引之上有一个列表索引。为什么?因为列表索引适合于合成组合多个数据源上的信息的答案。

  4. 最后查询图形。

    我们加载了苹果从Q1–2022和Q1–2023 两个季度10k的金融数据,并进行问答,代码如下:

## 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}')
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

从树索引生成摘要。如前所述,树索引对于汇总文档集合非常有用。

# 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]
  • 1
  • 2

接下来,我们将在3个树索引之上创建一个包括列表索引Graph。

### 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
  • 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

Pandas Index and SQL Index

这个相对比较好理解一些,我们直接展示一下实例代码:

Pandas Index:

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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

图片

SQL Index:

可以使用LLM去访问传统数据库了,对于传统行业是不有些激动呢?下面展示一个例子,更多例子可以参考(https://gpt-index.readthedocs.io/en/latest/examples/index_structs/struct_indices/SQLIndexDemo.html)

# 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?")
  • 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

图片

文档摘要索引(Document Summary Index)

Document Summary Index是LlamaIndex全新的数据结构,非常适合QA系统开发的。到目前为止,我们已经研究了单个索引,我们可以通过使用单个索引或将多个索引组合在一起来构建LLM QA应用程序。

一般来说,强大的LLM-QA系统通常由以下方式开发:

  1. 获取源文档并将其划分为文本块;

  2. 然后将文本块存储在矢量数据库中;

  3. 在查询时间期间,通过利用用于嵌入的相似性和/或关键字过滤器来检索文本块;

  4. 执行Response synthesis;

然而,这种方法有几个限制,会影响检索性能。

当前方法的缺点:

  1. 文本块没有完整的全局上下文,这通常会限制问答过程的有效性。

  2. 需要仔细调整top-k/相似性得分阈值,因为太小的值可能导致错过相关上下文,而太大的值可能会增加不相关上下文的成本和延迟。

  3. embedding可能并不总是为问题选择最合适的上下文,因为这个过程本质上是分别确定文本和上下文的。

为了增强检索结果,添加了关键字过滤器。然而,这种方法有其自身的一系列挑战,例如通过手动工作或使用NLP关键字提取/主题标记模型来为每个文档识别适当的关键字,以及从查询中推断正确的关键字。

图片

这就是LlamaIndex引入文档摘要索引的地方,该索引可以为每个文档提取和索引非结构化文本摘要,从而提高了现有方法之外的检索性能。这个索引比单个文本块包含更多的信息,并且比关键字标签具有更多的语义。它还允许灵活的检索,包括LLM和基于embedding的方法。

在构建期间,该索引加载文档,并使用LLM从每个文档中提取摘要。在查询期间,它根据摘要检索要查询的相关文档,方法如下:

  • 基于LLM的检索:获取文档摘要集合,并请求LLM识别相关文档+相关性得分

  • 基于embedding的检索:利用摘要嵌入相似性来检索相关文档,并对检索结果的数量施加top-k限制。

注意:Document Summary Index的检索类检索任何选定文档的所有节点,而不是在节点级别返回相关块。

下面来看一个例子:

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")
  • 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

知识图谱索引(Knowledge Graph Index)

它通过在一组文档上提取形式为(主语、谓语、宾语)的知识三元组来构建索引,了解知识图谱的读者不陌生。

在查询期间,它可以只使用知识图作为上下文进行查询,也可以利用每个实体的底层文本作为上下文。通过利用底层文本,我们可以针对文档的内容提出更复杂的查询。

把一个图想象成相互连接的边和顶点。

图片

更多例子,可以参考(https://gpt-index.readthedocs.io/en/latest/examples/index_structs/knowledge_graph/KnowledgeGraphDemo.html)

各种Index对比

图片

技术交流

技术要学会分享、交流,不建议闭门造车。一个人可以走的很快、一堆人可以走的更远。

本文完整代码、相关资料、技术交流&答疑,均可加我们的交流群获取,群友已超过2000人,添加时最好的备注方式为:来源+兴趣方向,方便找到志同道合的朋友。

方式①、微信搜索公众号:机器学习社区,后台回复:加群
方式②、添加微信号:mlc2060,备注:来自CSDN + 技术交流

在这里插入图片描述

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/繁依Fanyi0/article/detail/269142
推荐阅读
相关标签
  

闽ICP备14008679号