原文:LLM应用专辑(3) — ChatGPT遇上文档搜索:ChatPDF、ChatWeb、DocumentQA等开源项目算法思想与源码解析 - 知乎
公众号在上一篇文章中,我们介绍了chatgpt与nlp结合过程中的一些具体prompt生成方法,掌握这些可以有效地提升生产效率。
而说到生产效率,我们不得不提chatgpt与文档相结合的场景。
实际上,最近已经陆续出现了chatpdf,chatweb,chatexcel,chatpaper等文档工具,其底层先对文档进行预处理,然后利用openai生成embedding,最后再进行答案搜索,能够解决一些摘要、问答的问题。
如何理解这些项目底层的原理,包括实现过程就显得十分有趣。本文主要介绍ChatPDF、ChatWeb以及基于向量数据库与GPT3.5的通用本地知识库方案这些开源项目,供大家一起参考。
老刘说NLP老刘,刘焕勇,NLP开源爱好者与践行者。主页:https://liuhuanyong.github.io。就职于360人工智能研究院、曾就职于中国科学院软件研究所。老刘说NLP,将定期发布语言资源、工程实践、技术总结等内容,欢迎关注。
241篇原创内容
公众号一、ChatPDF、ChatWeb、Document_QA项目方案
1、chatpdf-minimal-demo项目
该项目是chatpdf 的最小实现,目标是和文章对话。
地址:https://github.com/postor/chatpdf-minimal-demo
其实现原理很简单:
1)文章切片到段落;
2)通过 OpenAI 的 embedding 接口将每个段落转换为 embedding;
3)将提问的问题转换为 embedding
4)把问题的 embedding 比较所有段落 embedding 得到近似程度并排序
5)把和提问(语义)最接近的一个或几个段落作为上下文,通过 OpenAI 的对话接口得到最终的答案
2、ChatWeb项目
ChatWeb可以爬取任意网页并提取正文,生成概要,然后根据正文内容回答你的问题。
目前是个原理展示的Demo,还没有细分逻辑,基于gpt3.5的chatAPI和embeddingAPI,配合向量数据库。其基本原理在于:
1)爬取网页;
2)提取正文;
3)对于每一段落,使用gpt3.5的embeddingAPI生成向量 ;
4)每一段落的向量和全文向量做计算,生成概要;
5)将向量和文本对应关系存入向量数据库;
6)对于用户输入,生成向量 ;
7)使用向量数据库进行最近邻搜索,返回最相似的文本列表;
8)使用gpt3.5的chatAPI,设计prompt,使其基于最相似的文本列表进行回答 ;
9)就是先把大量文本中提取相关内容,再进行回答,最终可以达到类似突破token限制的效果 ;
3、Document_QA项目
同样的,Document_QA也采用了类似的思想进行处理:
1)读取文件,并进行分割;
2)对于每段文本,使用text-embedding-ada-002生成特征向量;
3)将向量和文本对应关系存入本地pkl文件;
4)对于用户输入,生成向量;
5)使用向量数据库进行最近邻搜索,返回最相似的文本列表;
6)使用gpt3.5的chatAPI,设计prompt,使其基于最相似的文本列表进行回答;
不过,就是先把大量文本中提取相关内容,再进行回答,最终可以达到类似突破token限制的效果,后续可以考虑将openai的文本向量改成自定义的向量生成工具
项目地址:https://github.com/fierceX/Document_QA
1、请求openai获取embedding
def create_embeddings(input):
"""Create embeddings for the provided input."""
result = []
# limit about 1000 tokens per request
lens = [len(text) for text in input]
query_len = 0
start_index = 0
tokens = 0
def get_embedding(input_slice):
embedding = openai.Embedding.create(model="text-embedding-ada-002", input=input_slice)
return [(text, data.embedding) for text, data in zip(input_slice, embedding.data)], embedding.usage.total_tokens
for index, l in tqdm(enumerate(lens)):
query_len += l
if query_len > 4096:
ebd, tk = get_embedding(input[start_index:index + 1])
query_len = 0
start_index = index + 1
tokens += tk
result.extend(ebd)
if query_len > 0:
ebd, tk = get_embedding(input[start_index:])
tokens += tk
result.extend(ebd)
return result, tokens
2、基于openai completion进行补全问答
class QA():
def __init__(self,data_embe) -> None:
d = 1536
index = faiss.IndexFlatL2(d)
embe = np.array([emm[1] for emm in data_embe])
data = [emm[0] for emm in data_embe]
index.add(embe)
self.index = index
self.data = data
def __call__(self, query):
embedding = create_embedding(query)
context = self.get_texts(embedding[1], limit)
answer = self.completion(query,context)
return answer,context
def get_texts(self,embeding,limit):
_,text_index = self.index.search(np.array([embeding]),limit)
context = []
for i in list(text_index[0]):
context.extend(self.data[i:i+5])
# context = [self.data[i] for i in list(text_index[0])]
return context
def completion(self,query, context):
"""Create a completion."""
lens = [len(text) for text in context]
maximum = 3000
for index, l in enumerate(lens):
maximum -= l
if maximum < 0:
context = context[:index + 1]
print("超过最大长度,截断到前", index + 1, "个片段")
break
text = "\n".join(f"{index}. {text}" for index, text in enumerate(context))
response = openai.ChatCompletion.create(
model="gpt-3.5-turbo",
messages=[
{'role': 'system',
'content': f'你是一个有帮助的AI文章助手,从下文中提取有用的内容进行回答,不能回答不在下文提到的内容,相关性从高到底排序:\n\n{text}'},
{'role': 'user', 'content': query},
],
)
print("使用的tokens:", response.usage.total_tokens)
return response.choices[0].message.content
二、基于向量数据库与GPT3.5的通用本地知识库方案
基于向量数据库与GPT3.5的通用本地知识库方案(A universal local knowledge base solution based on vector database and GPT3.5),其主要采用了qdrant作为向量化索引库。
项目地址:https://qdrant.tech/documentation/
项目地址:https://github.com/GanymedeNil/document.ai/tree/main/code
整个流程非常简单,
1、本地文档数据其中:
普通感冒#####您会出现喉咙发痒或喉咙痛,流鼻涕,流清澈的稀鼻涕(液体),有时轻度发热。
常年过敏#####症状包括鼻塞或流鼻涕,鼻、口或喉咙发痒,眼睛流泪、发红、发痒、肿胀,打喷嚏。
2、建立索引
from qdrant_client import QdrantClient
from qdrant_client.http.models import Distance, VectorParams
from qdrant_client.http.models import PointStruct
import os
import tqdm
import openai
def to_embeddings(items):
sentence_embeddings = openai.Embedding.create(
model="text-embedding-ada-002",
input=items[1]
)
return [items[0], items[1], sentence_embeddings["data"][0]["embedding"]]
if __name__ == '__main__':
client = QdrantClient("127.0.0.1", port=6333)
collection_name = "data_collection"
openai.api_key = os.getenv("OPENAI_API_KEY")
# 创建collection
client.recreate_collection(
collection_name=collection_name,
vectors_config=VectorParams(size=1536, distance=Distance.COSINE),
)
count = 0
for root, dirs, files in os.walk("./source_data"):
for file in tqdm.tqdm(files):
file_path = os.path.join(root, file)
with open(file_path, 'r', encoding='utf-8') as f:
text = f.read()
parts = text.split('#####')
item = to_embeddings(parts)
client.upsert(
collection_name=collection_name,
wait=True,
points=[
PointStruct(id=count, vector=item[2], payload={"title": item[0], "text": item[1]}),
],
)
count += 1
3、查询服务
生成对话的示例提示语句,格式如下:
demo_q:
使用以下段落来回答问题,如果段落内容不相关就返回未查到相关信息:"成人头疼,流鼻涕是感冒还是过敏?"
1. 普通感冒:您会出现喉咙发痒或喉咙痛,流鼻涕,流清澈的稀鼻涕(液体),有时轻度发热。
2. 常年过敏:症状包括鼻塞或流鼻涕,鼻、口或喉咙发痒,眼睛流泪、发红、发痒、肿胀,打喷嚏。
demo_a:
成人出现头痛和流鼻涕的症状,可能是由于普通感冒或常年过敏引起的。如果病人出现咽喉痛和咳嗽,感冒的可能性比较大;而如果出现口、喉咙发痒、眼睛肿胀等症状,常年过敏的可能性比较大。
system:
你是一个医院问诊机器人
具体实现代码:
def prompt(question, answers):
demo_q = '使用以下段落来回答问题:"成人头疼,流鼻涕是感冒还是过敏?"\n1. 普通感冒:您会出现喉咙发痒或喉咙痛,流鼻涕,流清澈的稀鼻涕(液体),有时轻度发热。\n2. 常年过敏:症状包括鼻塞或流鼻涕,鼻、口或喉咙发痒,眼睛流泪、发红、发痒、肿胀,打喷嚏。'
demo_a = '成人出现头痛和流鼻涕的症状,可能是由于普通感冒或常年过敏引起的。如果病人出现咽喉痛和咳嗽,感冒的可能性比较大;而如果出现口、喉咙发痒、眼睛肿胀等症状,常年过敏的可能性比较大。'
system = '你是一个医院问诊机器人'
q = '使用以下段落来回答问题,如果段落内容不相关就返回未查到相关信息:"'
q += question + '"'
# 带有索引的格式
for index, answer in enumerate(answers):
q += str(index + 1) + '. ' + str(answer['title']) + ': ' + str(answer['text']) + '\n'
"""
system:代表的是你要让GPT生成内容的方向,在这个案例中我要让GPT生成的内容是医院问诊机器人的回答,所以我把system设置为医院问诊机器人
前面的user和assistant是我自己定义的,代表的是用户和医院问诊机器人的示例对话,主要规范输入和输出格式
下面的user代表的是实际的提问
"""
res = [
{'role': 'system', 'content': system},
{'role': 'user', 'content': demo_q},
{'role': 'assistant', 'content': demo_a},
{'role': 'user', 'content': q},
]
return res
4、执行查询
执行逻辑很简单,具体如下:
首先使用openai的Embedding 其API将输入的文本转换为向量,然后使用Qdrant的search API进行搜索,搜索结果中包含了向量和payload;
payload中包含了title和text,title是疾病的标题,text是摘要,最后使用openai的ChatCompletion API进行对话生成。
def query(text):
"""
"""
client = QdrantClient("127.0.0.1", port=6333)
collection_name = "data_collection"
openai.api_key = os.getenv("OPENAI_API_KEY")
sentence_embeddings = openai.Embedding.create(
model="text-embedding-ada-002",
input=text
)
"""
因为提示词的长度有限,所以我只取了搜索结果的前三个,如果想要更多的搜索结果,可以把limit设置为更大的值
"""
search_result = client.search(
collection_name=collection_name,
query_vector=sentence_embeddings["data"][0]["embedding"],
limit=3,
search_params={"exact": False, "hnsw_ef": 128}
)
answers = []
tags = []
"""
因为提示词的长度有限,每个匹配的相关摘要我在这里只取了前300个字符,如果想要更多的相关摘要,可以把这里的300改为更大的值
"""
for result in search_result:
if len(result.payload["text"]) > 300:
summary = result.payload["text"][:300]
else:
summary = result.payload["text"]
answers.append({"title": result.payload["title"], "text": summary})
completion = openai.ChatCompletion.create(
temperature=0.7,
model="gpt-3.5-turbo",
messages=prompt(text, answers),
)
return {
"answer": completion.choices[0].message.content,
"tags": tags,
}
总结
本文主要介绍了ChatPDF、ChatWeb以及基于向量数据库与GPT3.5的通用本地知识库方案这些开源项目。
从代码的角度来看,其实现路径较为简单,主要难点在于如何针对文档进行切分,尤其是针对一些扫描版文档,文档格式较为复杂的场景时,则需要使用版式分析、OCR等技术。
参考文献
1、https://qdrant.tech/documentation/
2、https://github.com/GanymedeNil/document.ai/tree/main/code
3、https://github.com/postor/chatpdf-minimal-demo
4、https://github.com/fierceX/Document_QA
5、