LlamaIndex 实战

LlamaIndex 原理介绍

llamaindex 原理介绍

LlamaIndex(GPT Index)是一个对话式文档问答解决方案,可以针对特定语料进行文档检索,通过索引文件把外部语料数据和GPT连接起来。它主要帮我们做了如下几件事:
文档拆分、向量化、向量存储检索、基于文档对话等。

现在ChatGPT的API是无状态的,意味着你需要自己去维持会话状态,保存上下文,每次请求的时候将之前的历史消息全部发过去,但是这里面有两个问题:1. 请求内容会越来越大;2. 费用很高。

今天在Twitter上看到有人分享的一个很好的解决方案,可以借助OpenAI的embedding模型和自己的数据库,现在本地搜索数据获得上下文,然后在调用ChatGPT的API的时候,加上本地数据库中的相关内容,这样就可以让ChatGPT从你自己的数据集获得了上下文,再结合ChatGPT自己庞大的数据集给出一个更相关的理想结果。

这种模式尤其适合针对一些特定著作、资料库的搜索和问答。我想之前有人做了模拟乔布斯风格的问答应该也是基于这种模式来做的。

具体解释一下它的实现原理(参考图一)。

  1. 首先准备好你要用来学习的文本资料,把它变成CSV或者Json这样易于处理的格式,并且分成小块(chunks),每块不要超过8191个Tokens,因为这是OpenAI embeddings模型的输入长度限制

  2. 然后用一个程序,分批调用OpenAI embedding的API,目前最新的模式是text-embedding-ada-002,将文本块变成文本向量。(参考图1从Script到OpenAI的步骤)

  1. 需要将转换后的结果保存到本地数据库。注意一般的关系型数据库是不支持这种向量数据的,必须用特别的数据库,比如Pinecone数据库,比如Postgres数据库(需要 pgvector 扩展)。如果你数据不大,存成csv文件,然后加载到内存,借助内存搜索也是一样的。

当然你保存的时候,需要把原始的文本块和数字向量一起存储,这样才能根据数字向量反向获得原始文本。这一步有点类似于全文索引中给数据建索引。(参考图一从Script到DB的步骤)

  1. 等需要搜索的时候,先将你的搜索关键字,调用OpenAI embedding的API把关键字变成数字向量。

(参考图一 Search App到OpenAI)

拿到这个数字向量后,再去自己的数据库进行检索,那么就可以得到一个结果集,这个结果集会根据匹配的相似度有个打分,分越高说明越匹配,这样就可以按照匹配度倒序返回一个相关结果。

(参考图一 Search App到DB的步骤)

  1. 聊天问答的实现要稍微复杂一点

当用户提问后,需要先根据提问内容去本地数据库中搜索到一个相关结果集。

(参考图一中Chat App到Search App的步骤)

然后根据拿到的结果集,将结果集加入到请求ChatGPT的prompt中。

(参考图一中Chat App到OpenAI的步骤)

比如说用户提了一个问题:“What’s the makers’s schedule?”,从数据库中检索到相关的文字段落是:“What I worked on…”和”Taste for Makers…”,那么最终的prompt看起来就像这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
[
{
role: "system",
content: "You are a helpful assistant that accurately answers queries using Paul Graham's essays. Use the text provided to form your answer, but avoid copying word-for-word from the essays. Try to use your own words when possible. Keep your answer under 5 sentences. Be accurate, helpful, concise, and clear."
},
{
role: "user",
content: `Use the following passages to provide an answer
to the query: "What's the makers's schedule?"
1. What I worked on...
2. Taste for Makers...`
}
]

这样ChatGPT在返回结果的时候,就会加上你的数据集。

项目地址:github.com/mckaywrigley/paul-graham-gpt
相关Twitter:twitter.com/mckaywrigley/status/1631328308116996097

LlamaIndex 实战

LlamaIndex (GPT Index) 官方教程
Build an AI that answers questions based on user research data. 这是一篇利用自己的知识库做 QA 的博客教程,里面有作者的 Colab Notebook ,一眼就会,开箱即炼。

LlamaIndex(原来的名字是gpt_index)库已经把上述这套逻辑封装了,可以直接使用。下面给出一个基于llama-index实现文档问答的具体demo:

前期准备

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# pip3 install openai llama-index tiktoken

import os
import openai

# 设置 API key
os.environ["OPENAI_API_KEY"] = "sk-xxx"
openai.api_key = os.getenv("OPENAI_API_KEY")

import logging

# 记录日志
logging.basicConfig(stream=sys.stdout, level=logging.INFO)
logging.getLogger().addHandler(logging.StreamHandler(stream=sys.stdout))

核心类构建

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
60
from langchain.chat_models import ChatOpenAI
from langchain.embeddings import OpenAIEmbeddings
from llama_index import LangchainEmbedding
from llama_index import (
GPTSimpleVectorIndex,
SimpleDirectoryReader,
LLMPredictor,
PromptHelper,
ServiceContext,
)

class LLma:
def __init__(self, gptmodel, embeddingmodel) -> None:
# set maximum input size
max_input_size = 4096
# set number of output tokens
num_outputs = 512
# set maximum chunk overlap
max_chunk_overlap = 20
# set chunk size limit
chunk_size_limit = 600

# define LLM
llm = ChatOpenAI(model_name=gptmodel, temperature=0.7, max_tokens=num_outputs, request_timeout=10, max_retries=2)
self.llm_predictor = LLMPredictor(llm=llm)
self.prompt_helper = PromptHelper(max_input_size, num_outputs, max_chunk_overlap, chunk_size_limit=chunk_size_limit)
# 定制化 embedding
embedding = OpenAIEmbeddings(
document_model_name=embeddingmodel,
query_model_name=embeddingmodel
)
self.embedding_llm = LangchainEmbedding(embedding)

self.service_context = ServiceContext.from_defaults(
llm_predictor=self.llm_predictor,
embed_model=self.embedding_llm,
prompt_helper=self.prompt_helper
)

# 建立本地索引
def create_index(self,dir_path="./data",service_context=None):
# 读取data文件夹下的文档
documents = SimpleDirectoryReader(dir_path).load_data()
# 按最大token数600来把原文档切分为多个小的chunk,每个chunk转为向量,并构建索引
index = GPTSimpleVectorIndex.from_documents(documents, service_context=self.service_context)
# 保存索引
index.save_to_disk('./index.json')
print("Save to localpath")

def query_index(self,input_text,index_path="./index.json"):
# 加载索引
index = GPTSimpleVectorIndex.load_from_disk(index_path)
response = index.query(input_text, response_mode="compact")
# display(Markdown(f"Response: <b>{response.response}</b>"))
return response.response


gptmodel = "gpt-3.5-turbo" # model: gpt4
embeddingmodel = "text-embedding-ada-002"
llma = LLma(gptmodel, embeddingmodel)

建立索引

1
2
3
# 建立索引
train_dir = "./data" # 放入多篇 TXT 文档
llma.create_index(train_dir)

执行上述代码后可以看到日志打印如下:

1
2
> [build_index_from_documents] Total LLM token usage: 0 tokens
> [build_index_from_documents] Total embedding token usage: 1466 tokens

说明原文档中所有token数为1466,这也是请求embedding接口的调用成本。按最大token数600,则会切分为3个chunk,可以在索引文件index.json确认chunk的数目确实为3,同时index.json中也记录了每个chunk对应的embedding向量。

查询索引

1
2
3
4
5
6
# 查询索引
query = '讲一下美女蛇的故事'
answer = llma.query_index(query)

print('query was:', query)
print('answer was:', answer)

调用query接口的时候,llama-index会构造如下的prompt:

1
2
3
4
5
6
"Context information is below. \n"
"---------------------\n"
"{context_str}"
"\n---------------------\n"
"Given the context information and not prior knowledge, "
"answer the question: {query_str}\n"

上述代码执行后,日志打印如下:
1
2
3
4
> [query] Total LLM token usage: 4894 tokens
> [query] Total embedding token usage: 18 tokens

美女蛇的故事是这样的:有一个读书人住在古庙里用功,晚间,在院子里纳凉的时候,突然听到有人在叫他的名字。他四面看时,却见一个美女的脸露在墙头上,向他一笑,隐去了。他很高兴,但竟给那走来夜谈的老和尚识破了机关,说他脸上有些妖气,一定遇见“美女蛇


LlamaIndex 实战
http://example.com/2023/04/14/2023-04-14-LlamaIndex 实战/
作者
Ning Shixian
发布于
2023年4月14日
许可协议