前言 Langchain和LlamaIndex都是在 构建LLM应用当中最常用的两个库,但是他们的侧重点不一样:
为什么要采用Langchain和LlamaIndex相结合的方式,因为Langchain更偏基础层可以很方便扩展或者对接其他服务,因此整个大模型的编排交互依然由Langchain进行,但是LlamaIndex擅长什么?没错,就是文档的加载和检索,除了官方提供的一些基础加载库或者检索器,背后还有一个LlamaHub的社区库。提供了更多的解析器或者检索器,可以方便接入LlamaIndex使用。
那么现在思路很明确,整个大模型应用的链路流程由Langchain构建,包括提示词、包括对大模型进行交互、包括后续各种各样的功能,而链路中如果有需要查找知识库(或者构建知识库),由LlamaIndex进行处理。因此可能需要把LlamaIndex的检索器包括检索过程,对接到Langchain的检索器类型当中。
实现 首先我们依然是通过LlamaIndex构建索引,然后生成检索器。
第二我们需要实现一个自定义类来继承Langchain的检索器基类,通过我们的LlamaIndex来实现它的方法,并且以Langchain的文档类型返回
自定义Langchain检索器 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 from typing import Any , List from pydantic import Fieldfrom langchain_core.retrievers import BaseRetrieverfrom langchain_core.documents import Document as LangchainDocumentfrom config.config import SIMILARITY_TOP_Kfrom logger import get_loggerlogger = get_logger(__name__) class LlamaIndexRetriever (BaseRetriever ): index_retriever: Any = Field(default=None , description="LlamaIndex retriever instance" ) def __init__ (self, index_retriever: Any , **kwargs ): super ().__init__(**kwargs) self .index_retriever = index_retriever def _get_relevant_documents (self, query: str ) -> List [LangchainDocument]: logger.info(f"查询时的维度: {self.index_retriever._vector_store._faiss_index.d} " ) logger.info(f"查询向量: {self.index_retriever._vector_store._faiss_index.ntotal} " ) nodes = self .index_retriever.retrieve(query) logger.info(f"检索到的节点数量: {len (nodes)} " ) return [ LangchainDocument( page_content=node.text, metadata=node.metadata ) for node in nodes ] async def _aget_relevant_documents (self, query: str ) -> List [LangchainDocument]: return self ._get_relevant_documents(query) def get_retriever (index ): """创建检索器""" index_retriever = index.as_retriever(similarity_top_k=SIMILARITY_TOP_K) return LlamaIndexRetriever(index_retriever=index_retriever)
构建检索&问答链 简单构建 通过前面获取到了我们自定义的检索器,可以通过from_chain_type()的方式构建chain
1 2 3 4 5 6 7 你是一个智能中文助手 参考资料: {context} 请根据参考资料,准确自然的回答用户当前的问题: {query}
1 2 3 4 5 6 7 8 qa_chain = RetrievalQA.from_chain_type( llm=llm, retriever=retriever, chain_type="stuff" , chain_type_kwargs={"prompt" : prompt_template}, return_source_documents=True )
1 qa_chain.invoke({"query" :"你好" })
这样的方式就已经可以使用了效果还挺好,同时通过 return_source_documents=True , 能够返回检索过程的文档列表。
这样的方式局限在于只能单次query交互,在这样的方式情况下你的提示词模板只能有两个参数,一个是context 一个随便叫啥(question)
context是内定的,他会把检索器的资料放到context中,不需要你传。另外一个参数就是你传入的叫啥名都可以。他会拿去进行检索,然后再进行LLM问答
当使用这种方式时我们无法进行多轮对话的输入,因为只能输入一个query. 它既进行检索也进行问答.
浓缩上下文构建 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 chat_prompt = ChatPromptTemplate.from_messages([ SystemMessage(content=''' ## 你的角色 你是一智能中文助手 ## 你的任务 以亲切专业的语气完成以下职责:xxx ## 知识库 以下是查询所得到的知识库内容:\n {conntent} ''' ), HumanMessage(content="{question}" ) ]) qa_chain = ConversationalRetrievalChain.from_llm( llm=llm, retriever=retriever, condense_question_prompt=chat_prompt, return_source_documents=True )
通过ConversationalRetrievalChain的方式,虽然可以累计消息列表,也就更新chat_prompt消息列表的方式,可以累计多条消息。但是这种方式是一种浓缩的方式,所以虽然支持消息历史而不是只能单条,但是效果很差。
自定义构建 上面都是曲线救国,正常来说应该不会用,通过自定义的方式才是最简单的,也是满足大多需要的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 docs = retriever.get_relevant_documents(query) context = "\n\n" .join([doc.page_content for doc in docs]) qa_llm_chain = LLMChain( llm=llm, prompt=prompt_template ) result = qa_llm_chain.invoke({ "query" : query, "chat_history" : chat_history, "context" : context })
通过这样的方式,不用走它的检索器chain,就一个正常chain,完全自定义构建提示词,自己调用检索得到文档、自己组装消息列表 。自定义拼接到提示词模板即可。
总结 正常都是使用最后的方式,也不需要自定义检索器类。他这种配合检索器的构建链可能是快速构建rag的方式吧,但是基本上想要自定义程度或者掌控程度越高就越偏原生一点去写。