chatgpt的api距今上线一周,已经出现了几款热度很高的文档问答产品,例如chatpdf,接下来解析一下这些产品背后的技术原理。
任务描述和问题分析
对话式文档问答,字面意思就是基于文档建立的问答系统。使用这项技术,开发者无需梳理意图、词槽,无需进行问题和答案的整理,只需准备文本格式的业务文档(例如洗碗机的使用手册),就可以得到一个问答系统,回答用户的各种问题(例如“洗碗机排水管堵塞了怎么办“)。
如果使用openai api来实现对话式文档问答,最朴素的想法把这个当成一个阅读理解问题,构建如下的prompt:
- 现有一个问题:“洗碗机排水管堵塞了怎么办”,请根据下面的文章来回答,文章内容如下:"......"
这种方法在文档较长时存在两个问题:
- 第一,openai api存在最大长度的限制,例如chatgpt的最大token数为4096,此时直接对文档截断,存在上下文丢失的问题
- 第二,api的调用费用和token长度成正比,tokens数太大,则每次调用的成本都会很高
解决思路
可以参考搜索引擎中“先检索再重排”的思路,针对文档问答设计“先检索再整合“的方案,整体思路如下:
- 首先准备好文档,并整理为纯文本的格式。把每个文档切成若干个小的chunks
- 调用文本转向量的接口,将每个chunk转为一个向量,并存入向量数据库
- 当用户发来一个问题的时候,将问题同样转为向量,并检索向量数据库,得到相关性最高的一个或几个chunk
- 将问题和chunk合并重写为一个新的请求发给openai api,可能的请求格式如下:
结合下面的段落来回答问题:“ 如何使用预约功能”
* 段落1: 您可以按照以下步骤使用预约功能....
* 段落2: 在使用预约功能之前,请确保您已正确地设置了洗涤程序....
* 段落3: .......
上述“先检索再整合的逻辑”已经封装在llama-index库中: https://github.com/jerryjliu/gpt_index
基于llama-index的demo
llama-index的前身是gpt-index项目,最近才刚改名为'llama-index'。下面给出一个基于llama-index实现文档问答的具体demo,代码已上传至github:
前期准备
建立索引
建立索引的代码位于build_index.py
,具体代码如下:
import os
import logging
import sys
from llama_index import SimpleDirectoryReader, GPTSimpleVectorIndex
logging.basicConfig(stream=sys.stdout, level=logging.INFO)
logging.getLogger().addHandler(logging.StreamHandler(stream=sys.stdout))
# 读取data文件夹下的文档
documents = SimpleDirectoryReader('data').load_data()
# 按最大token数500来把原文档切分为多个小的chunk,每个chunk转为向量,并构建索引
index = GPTSimpleVectorIndex(documents, chunk_size_limit=500)
# 保存索引
index.save_to_disk('index.json')
执行上述代码后可以看到日志打印如下:
> [build_index_from_documents] Total LLM token usage: 0 tokens
> [build_index_from_documents] Total embedding token usage: 1466 tokens
说明原文档中所有token数为1466,这也是请求embedding接口的调用成本。按最大token数500,则会切分为3个chunk,可以在索引文件index.json
确认chunk的数目确实为3,同时index.json
中也记录了每个chunk对应的embedding向量。
查询索引
查询索引的代码位于query_index.py
,具体代码如下:
import os
import logging
import sys
from llama_index import GPTSimpleVectorIndex
logging.basicConfig(stream=sys.stdout, level=logging.INFO)
logging.getLogger().addHandler(logging.StreamHandler(stream=sys.stdout))
# 加载索引
new_index = GPTSimpleVectorIndex.load_from_disk('index.json')
# 查询索引
response = new_index.query("What did the author do in 9th grade?")
# 打印答案
print(response)
调用query接口的时候,llama-index会构造如下的prompt:
"Context information is below. \n"
"---------------------\n"
"{context_str}"
"\n---------------------\n"
"Given the context information and not prior knowledge, "
"answer the question: {query_str}\n"
上述代码执行后,日志打印如下:
> [query] Total LLM token usage: 576 tokens
> [query] Total embedding token usage: 10 tokens
In 9th grade, the author tried writing programs on an IBM 1401 computer in the basement of their junior high school. They used an early version of Fortran and had to type programs on punch cards. They attempted to do things that didn't rely on any input, like calculate approximations of pi, but didn't have enough math knowledge to do anything interesting. They also learned that programs could not terminate, which was a social as well as a technical error.
- 'Total embedding token usage: 10 tokens'对应的是问题"What did the author do in 9th grade?"计算embedding的成本
- 'Total LLM token usage: 576 tokens'对应的是用整个prompt调用LLM语言模型的成本,又可分成四部分
- context_str: 500 tokens
- query_str: 10 tokens
- prompt模板中的其他文本:16 tokens
- answer: 50 tokens
- 通过这种方法,原来接近1500 token的查询调用成本,被降到了500 tokens左右
定制开发
小结
本文提供了一种后chatgpt时代的对话式文档问答解决方案,可供参考。
参考