几周前我曾经写了关于llama_index的两篇文章:【一】 【二】,感兴趣的同学不少。也有不少同学也直接上手尝试了
然而,深入使用后,许多同学发现llama_index的一些使用问题,比如:
- 如何发起并行、异步请求?
- 搜索准确度的问题:文件分片后,往往搜不到正确的相关资料
- 灵活性问题:如何自己定制搜索流程?
当然,这些问题笔者我都有遇到过,所以在这篇文章中,我想和大家分享的就是:用别的东西替代llama_index。
llama_index为什么会有这些困难
在上一篇文章中,我花了很多篇幅来解释每种index做了什么,简单总结一下:
- SimpleVectorIndex 将文档拿去调用embedding API转为向量。然后输入query,将query转为向量进行向量搜索后,得到相关资料文档,用于给GPT回答问题。如果top_k值大于1,则会让GPT对几个答案进行合并归纳。
- ListIndex 搭配ComposableGraph使用时,会逐个执行子index的query操作。后让GPT对所有答案进行合并归纳
- TableIndex 提取子index的关键词。然后输入query,提取query的关键词,将关键词产生匹配的子index逐个执行query操作。后让GPT对所有答案进行合并归纳
- TreeIndex 让GPT决定哪n个子index最能回答问题。对这些子index逐个执行query操作。后让GPT对所有答案进行合并归纳。
llama_index在各种Index里做了非常多的事情。除了这些基础的Index
,后面还推出了GPTPandasIndex
, GPTKnowledgeGraphIndex
等。
这些封装好的Index
,就是llama_index的核心价值所在。
这样大包大揽的封装,使得它易于使用,但同时也减少了其灵活性。
虽然大多数新手可以很快上手使用它,比如快速做出像ChatPDF这样的应用,但是当你需要定制自己逻辑,就会遇到困难。例如,如果想要自定义请求GPT前整理参考资料的方式、或是修改GPT处理多个结果的方式,那该怎么办?
对于 llama_index 来说,唯一的改变逻辑的方法就是通过参数来控制,比如在llama_index的设计里,在 Index 的构造函数或者查询时,可以传递 post_processor、result、query_mode、query_transform 等多个参数。
但是这样做的话,你觉得多少个参数才能满足需求呢?随着时间的增长,这个参数量会无节制增长。它的理解成本和记忆成本也会不断增加。
为了得到更好的灵活性,从而实现我们的需求,我们其实需要寻找逻辑划分更细的工具来替代llama_index。比如搜索相关资料的步骤是一个函数,得到结果后,我们可以写一些自己的代码。请求GPT也是一个独立的函数,在请求前后我们也可以写一些自己的代码。
将逻辑划分得更为零散,而不是一个大包大揽的逻辑单元,这样才能像搭乐高一样从零件搭建出我们想要的东西。
所以,我们要将llama_index所做的事情做一个分析,看看它可以拆分出哪些步骤,
“拆分”llama_index
如果你有一定的llama_index使用经验,或是结合我前面画的图,你就能知道llama_index的职责,或者说所做的事情,最有价值的其实就几个:
- 生成Embedding:llama_index会去调用GPT的
text-embedding-ada-002
接口,将文本转为向量。 - 向量搜索:把外部数据源(比如一个txt、html、pdf)转换成一个向量索引,然后根据用户输入的query,用余弦相似度(cosine similarity)或其他度量方式,找出最匹配的数据项。
- 通过prompt engineering获得答案:llama_index内置了一些合适的prompt,结合向量搜索得到的参考资料等作为上下文,传给GPT-3.5,让它生成一个符合目标的答案。或是得到多个答案然后合并优化为最终答案。
其中生成embedding这个事情比较简单,只需要调OpenAI的api就可以了。当然我们也可以自己找一个文本转向量的工具,只不过OpenAI的embedding接口价格本来就很便宜,所以,embedding的事情我们就不过多讨论了。
剩下的两件事,向量搜索和prompt engineering,如果我们能找到办法实现它们,那事情就妥了。
用langchain完成”prompt engineering”
前面的文章提到过,llama_index
其实实际上是基于langchain
的,像llama_index
的refine操作等prompt engineering
,就是靠langchain
完成的。
langchain
本来就是最早的一批大模型应用开发库之一,用它来做prompt engineering
再轻松不过了。
举个例子,笔者在做文档问答机器人的时候,发现把文档转向量之前,如果做一次“SEO”(在第一篇最后有说过我的这个想法)。具体来说,是让GPT理解这段文本的内容,提出一些可能跟这段文本有关的问题,然后将这批问题以注释的形式放在文档内。这样子用户提问时,向量搜索出来的准确度可以提高非常多。
这种事情,写个prompt让GPT帮你做不要太简单。
// 使用langchain.js写的例子
// 创建LLM请求器
const model = new ChatOpenAI({
modelName: 'gpt-3.5-turbo',
openAIApiKey: KEY,
temperature: 0,
maxRetries: 3,
maxConcurrency: 5
});
// 向LLM传输prompt和输入文字
const res = await model.call([
new HumanChatMessage(`
I'll give you a markdown document. As a QA tester, you have to write some annotations in this format:
<!-- the slogan of the document - the title of the document -->
<!-- related question 1 about this document -->
<!-- related question 2 about this document -->
...
<!-- related question n about this document -->
attention:
1. use the fullname instead using "this" "that" "it" to refer to anything.
2. return the annotations to me directly, please do not add any extra content.
3. the annotations must contains both Chinese and English
`)
])
// 获得"SEO"优化结果
console.log(res.text)
向量搜索
chatgpt-retrieval-plugin的向量搜索
上篇文章的末尾,我提到过一下 chatgpt-retrieval-plugin,它的职责其实就和llama_index很像。
而retrival-plugin的向量搜索方式,是建议开发者采用第三方向量数据库,因为向量数据库服务其实已经成熟了好几年,现在有很多产品可以选择。随便选一个,都能得到比使用llama_index更强大的向量管理能力。
可以看到这里llama_index成为了retrieval_plugin的一个可选后端。可以看出来作者也在让llama_index适应变化。相比起别的向量数据库,确实llama_index有着自己的竞争优势。这个转型也是非常漂亮
chatgpt-retrieval-plugin除了实现和llama_index类似的效果外。还展示了chatgpt-plugin的一大亮点,就是开发者只需编写
.well-known
目录下的接口描述文件,chatgpt-plugin的后端就能理解plugin有什么接口。这个晚点笔者会再介绍
langchain的向量搜索
尽管llama_index
是基于langchain
开发的,但向量搜索这件事本来也被OpenAI官方吸收了,向量数据库也成熟了那么多年。langchain
有什么理由不整合呢?毕竟langchain
是拿了高额的投资的,自然不会放弃扩张自己版图的机会。
所以,langchain
也有自己的index
模块和vectorstore
对象。其设计和retrieval-plugin的思路比较像 ———— 接入第三方的向量数据库。而且这俩模块专注于完成向量搜索,你输入一个问句,它只会将相关的参考文档返回给你。并不急于做后续的GPT请求。这样你就可以在发GPT请求之前添加许多自己的逻辑了。
比如以下代码,就是你可以对向量搜索得出的结果进行一次GPT检查,让GPT确认这个答案是否有用。最后将过滤后的有用的内容再给到GPT去生成最终答案。
// 从提前创建好的HNSW向量数据库文件创建vectorStore
const vectorStore = await HNSWLib.fromDocuments(allDocPiece, embeddingModel);
// 搜出相关的文档。
const result = await vectorStore.similaritySearchWithScore(QUESTION, 15);
const docs = result.map(res=> res[0])
// 让GPT决定哪些资料有用
let positiveRes = await Promise.all(docs.map(async (docpiece) => {
const res = await model.call([
new HumanChatMessage(
`Now, I will show you a context
------
${docpiece.pageContent}
------
I'll give you a question.
Ignoring all your previous knowledge, with these context, I want you to tell me:
zeta. can answer the question
theta. cannot answer the question
just return me the option value, don't add any extra text.
`
),
new AIChatMessage('Sure, please show me your question'),
new HumanChatMessage(QUESTION)
])
if (res.text.indexOf('zeta') != -1 || (res.text.indexOf('theta') == -1 && res.text.indexOf('cannot') == -1)( {
return docpiece
}
})).filter(r=> r)
if (positiveRes.length == 0) {
return {
response: '没有在文档里找到相关资料,可能是GPT3.5尚未能理解相关词汇',
sources: []
}
}
// 将有用的资料拿去生成最终答案
const finalRes = await model.call([
new HumanChatMessage(
`
Now, I will show you a context
------
${positiveRes.slice(0, 3).map(item=> item.pageContent).join('n')}
------
I'll give you a question.
Ignoring all your previous knowledge, with these context,
Please answer the question
请用中文回答
`
),
new AIChatMessage('Sure, please show me your question'),
new HumanChatMessage(QUESTION)
])
console.log(finalRes)
像llama_index一样快速实现基于向量搜索的问答
langchain
还有一个概念叫chain,可以让你得到一个像llama_index一样大包大揽的对象,直接输入query得到答案。但这个跟llama_index就完全一样了,易于快速调用,但不易于灵活定制,所以笔者就不过多介绍了。
js.langchain.com/docs/module…
langchain
还有一个更厉害的东西:Agent,这个东西和前面说到的retrieval-plugin的接口描述文件理解能力有一些相关,笔者可能会在可能会写的下一篇文章里提到。
总结
我们为llama_index找到了更好的替代品 —— 各种向量数据库和langchain,将它们组合起来使用。能够带来更灵活的开发体验,便于实现更定制化的效果。而且langchain还支持JS,对笔者来说完全没有理由不改用。
GPT相关技术发展的很快,每项新技术流行个几周就被替代是很正常的事。但llama_index的易用性还是非常有优势,非常适合用于这个领域的入门学习,先玩懂llama_index再上手Langchain会快得多。并且llama_index也快速转型成为retrieval-plugin的一个检索后端选项。总之,它依然是一个非常好的技术。