Bootstrap

使用langchain及llama_index实现基于文档(长文本)的相似查询与询问

前面的总结是半年前的知识与代码,但是框架不变,所以不再修改。
但是半年更新了不少内容,包括一些类都更换了不少,有同学问到,所以就又重新写一份,贴在下面。
考虑到这篇费了不少时间,关注一下不过分吧(❁´◡`❁)

2023.5.31

1. 引言

在调用ChatGPT接口时,我们常常受到4096个字符(token)的限制。这种限制对于处理长文本或者需要对文档进行相似查询和询问的场景来说是一个挑战。然而,通过结合使用langchain和llama_index这两个强大的工具,我们可以克服这个限制,实现对长文本的高效查询和询问。

2. 简介

langchain是一个功能强大的库,它为我们提供了许多方便的工具和模型,包括OpenAI模型。它通过链式调用的方式将这些组件连接在一起,创造出一个连贯的应用程序。同时,langchain还提供了内存组件Memory,可以帮助我们管理之前的聊天消息,以及Indexes和Agents等功能。

LlamaIndex(GPT Index)是一个用于LLM应用的数据框架,集成了langchain及chatgpt相关应用,更便于我们实现结构化数据和高级检索的相关功能。

3. 带关键字的查询方案

基于文档的查询场景有一种情况是,提问的内容与全部文档中的一小块相关,而其他内容无关。比如《百草园到三味书屋》中美女蛇的故事。

若询问中带有关键字,我们推荐使用相似匹配的方式进行筛选有关内容。

  • 拆分文档:首先,我们将长文本拆分成较小的块,并使用OpenAI的Embeddings功能将每个块向量化。
  • 相似性匹配:当用户提出查询时,我们将用户的查询文本也进行向量化。
  • 相似查询:然后,我们遍历已拆分并向量化的文档块,将其与向量化后的查询文本进行相似性比较。通过计算相似度,我们可以找到最相似的文档块
  • 传递上下文:根据之前的映射关系,我们找到与最相似文档块相对应的原始文档内容。将这个内容作为上下文传递给ChatGPT模型。
  • 询问与回答:最后,ChatGPT(LLM)根据这个上下文,对用户的查询进行回答。

总体langchain的内容如下:
在这里插入图片描述

对于文本的处理流程如下:
在这里插入图片描述

看上去很复杂,但是langchain都替我们做好了。

在这里插入图片描述

我们在库源码里面一层层输出中间变量,可以验证它确实是一种相似匹配。

在这里插入图片描述
如果不想使用OpenAI的API接口,也可以使用Hugginface上的模型来做相似匹配,从而传入自身的llm模型中。

在这里插入图片描述

4. 不带关键字的总结询问

如果用户询问的是“这篇文章写了什么”,这种无关键字的询问,这时候我们不能使用相似查询了,这样会有上下文的缺失。

这里我们推荐使用tree_summarize的方式进行询问。它的工作原理如下:

在这里插入图片描述
例如可以生成如下结果:
在这里插入图片描述

当然,除此之外还有其他的响应方式,比如简单总结simple_summarize、轮询迭代的refine等等,我们修改下方的response_mode即可。

不同的响应类型ResponseMode可见下篇博客:llama_index中query_engine的response_mode详解

5. 实现代码

import re
import os
from langchain import OpenAI

os.environ["OPENAI_API_KEY"] = 'sk-xx(apikey)'

from llama_index import SimpleDirectoryReader,LLMPredictor,ServiceContext
from llama_index import GPTListIndex, SimpleDirectoryReader
from llama_index.indices.response.type import ResponseMode

from llama_index import SimpleDirectoryReader
from llama_index.readers.schema.base import Document

# 可以直接输入文本
def textToDocuments(text):
    documents = [Document(text)]
    return documents

# 输入目录后将目录下的所有文件转成Documents对象数组,同上
def fileToDocuments(filePath):
    documents = SimpleDirectoryReader(filePath).load_data()
    return documents


# 模型名称参数
model_name = "text-davinci-003"
"""
其他模型名称及对应可接收每段的最大token数
    "gpt-4": 8192,
    "gpt-4-0314": 8192,
    "gpt-4-32k": 32768,
    "gpt-4-32k-0314": 32768,
    "gpt-3.5-turbo": 4096,
    "gpt-3.5-turbo-0301": 4096,
    "text-ada-001": 2049,
    "ada": 2049,
    "text-babbage-001": 2040,
    "babbage": 2049,
    "text-curie-001": 2049,
    "curie": 2049,
    "davinci": 2049,
    "text-davinci-003": 4097,
    "text-davinci-002": 4097,
    "code-davinci-002": 8001,
    "code-davinci-001": 8001,
    "code-cushman-002": 2048,
    "code-cushman-001": 2048,
"""

llm_predictor = LLMPredictor(llm=OpenAI(temperature=0, model_name=model_name,max_tokens=1800))

service_context = ServiceContext.from_defaults(llm_predictor=llm_predictor)

query_str = "美女蛇的故事是什么?"

response_mode = "compact"
"""
    REFINE = "refine"
    COMPACT = "compact"
    SIMPLE_SUMMARIZE = "simple_summarize"
    TREE_SUMMARIZE = "tree_summarize"
    GENERATION = "generation"
    NO_TEXT = "no_text"
"""

documents = fileToDocuments("./data")
# documents2 = textToDocuments("不必说碧绿的菜畦,光滑的石井栏,高大的皂荚树,紫红的桑椹;也不必说鸣蝉在树叶里长吟,肥胖的黄蜂伏在菜花上,轻捷的叫天子(云雀)忽然从草间直窜向云霄里去了。单是周围的短短的泥墙根一带,就有无限趣味。油蛉在这里低唱,蟋蟀们在这里弹琴。翻开断砖来,有时会遇见蜈蚣;还有斑蝥,倘若用手指按住它的脊梁,便会拍的一声,从后窍喷出一阵烟雾。何首乌藤和木莲藤缠络着,木莲有莲房一般的果实,何首乌有拥肿的根。有人说,何首乌根是有象人形的,吃了便可以成仙,我于是常常拔它起来,牵连不断地拔起来,也曾因此弄坏了泥墙,却从来没有见过有一块根象人样。如果不怕刺,还可以摘到覆盆子,象小珊瑚珠攒成的小球,又酸又甜,色味都比桑椹要好得远。")

index = GPTListIndex.from_documents(documents,service_context=service_context)

query_engine = index.as_query_engine(
    response_mode=response_mode
)

response = query_engine.query(query_str)
print(response)

2024.1.24 更新

其实直接看llamaIndex的官网就可以了,现在的示例代码已经比较丰富了。

1. 基于文档的query代码(需openai api-key)

from llama_index import (
    VectorStoreIndex,
    get_response_synthesizer,
)
from llama_index.retrievers import VectorIndexRetriever
from llama_index.query_engine import RetrieverQueryEngine
from llama_index.postprocessor import SimilarityPostprocessor
from llama_index import SimpleDirectoryReader
import os

os.environ["OPENAI_API_KEY"] = 'sk-xxx'


# 输入目录后将目录下的所有文件转成Documents对象数组,同上
def fileToDocuments(filePath):
    documents = SimpleDirectoryReader(filePath).load_data(show_progress=True)
    return documents


documents = fileToDocuments("./data-test")
# build index
index = VectorStoreIndex.from_documents(documents)

# configure retriever
retriever = VectorIndexRetriever(
    index=index,
    similarity_top_k=10,
)

# configure response synthesizer
response_synthesizer = get_response_synthesizer()

# assemble query engine
query_engine = RetrieverQueryEngine(
    retriever=retriever,
    response_synthesizer=response_synthesizer,
    node_postprocessors=[SimilarityPostprocessor(similarity_cutoff=0.7)],
)

# query
response = query_engine.query("summarize the rapeseed traits mainly studied by the authors, the technologies employed,rapeseed genes mainly studied by the authors,relations between rapeseed genes,relations between rapeseed genes and rapeseed raits from the article.")
print(response)

2. 基于query搜索相似文档(huggingface embedding + faiss)

基于huggingface embedding后做相似度匹配

# pip install sentence-transformers
# pip install -U langchain-community
# pip install faiss-cpu 或者 pip install faiss-gpu
# pip install unstructured
# pip install "unstructured[pdf]"

# 1. 文档加载器 Document Loader
# from langchain.document_loaders import TextLoader,DirectoryLoader
from langchain_community.document_loaders import TextLoader, DirectoryLoader
# loader = TextLoader('data-test/xx.txt',encoding="utf-8")
loader = DirectoryLoader('data-test') # 直接读整个目录内的文件
documents = loader.load()

# 2. 分词器 Text Splitter
from langchain.text_splitter import CharacterTextSplitter
text_splitter = CharacterTextSplitter(chunk_size=1000, chunk_overlap=0)
docs = text_splitter.split_documents(documents)

# 3. 嵌入向量 Embeddings
# from langchain.embeddings import HuggingFaceEmbeddings
from langchain_community.embeddings import HuggingFaceEmbeddings
embeddings = HuggingFaceEmbeddings()
# from langchain.vectorstores import FAISS
from langchain_community.vectorstores import FAISS
db = FAISS.from_documents(docs, embeddings)

# 4. 相似性搜索
query = "summarize the rapeseed traits mainly studied by the authors, the technologies employed,rapeseed genes mainly studied by the authors,relations between rapeseed genes,relations between rapeseed genes and rapeseed raits from the article."
docs = db.similarity_search(query)
print("=========")
print(docs[0].page_content) # 输出结果页面内容


# 5. 保存和加载 Save and load
# 保存之后(后续可以单独调用load这块,就不用再嵌入一次了):
db.save_local("faiss_index")
query = "summarize the rapeseed traits mainly studied by the authors, the technologies employed,rapeseed genes mainly studied by the authors,relations between rapeseed genes,relations between rapeseed genes and rapeseed raits from the article."
new_db = FAISS.load_local("faiss_index", embeddings)
docs = new_db.similarity_search(query)
# print(docs[0].page_content)
;