目录
一、简介
二、概念
三、组件具体介绍
3.1 Models
3.1.1 LLMs
3.1.2 Chat Models
3.1.3 Text Embedding Modesl
3.1.4 总结
3.2 Prompts
3.2.1 LLM Prompt Template
3.2.1.1 自定义PromptTemplate
3.2.1.2 partial PromptTemplate
3.2.1.3 序列化PromptTemplate
3.2.2 Chat Prompt Template
3.2.3 Example Selector
3.2.4 Output Parser
3.2.5 总结
3.3 Indexes
3.3.1 Document Loaders
3.3.2 Text Splitters
3.3.3 VectorStores
3.3.4 Retrievers
3.4 Memory
3.4.1 Memory在Chain中的使用
3.4.2 LangChain内置Memory实现
3.4.3 总结
3.5 Chains
3.5.1 Chain的通用方法
3.5.2 常见Chain
3.5.3 常用工具Chain
3.6 Agents
3.6.1 Tools
3.6.2 Agents
3.6.2.1 自定义BaseSingleActionAgent
3.6.2.2 自定义LLMAgent(更简单的方法:直接使用ZeroShotAgent,自定义LLMChain)
3.6.2.3 使用ZeroShotAgent自定义MRKLAgent
3.6.2.4 自定义Agent with Tool Retrieval
3.6.3 Toolkits
3.6.4 AgentExecutor
五、评估框架
六、关于业务实现框架的想法(待完善)
一、简介
LangChain是一个用来基于语言模型开发应用的框架。我们相信,最强大、最与众不同的应用程序不仅会通过API调用语言模型,而且还会:
-
数据感知:将语言模型连接到其他数据源
-
代理性:允许语言模型与其环境交互
LangChain为开发者提供了很多模块化的组件,但从组件的效果来看,它们的最终目的只有一个:协助开发者构造更合理、信息更为丰富的Prompt,来从LLM获得更为准确和详细的回答。LangChain自身并没有太高的技术壁垒,但它可以为我们省下许多的开发时间。
二、概念
LangChain提供了以下组件:
-
Schema:整个代码库中使用的数据类型和schema,分为以下几种类型:
-
Text:和语言模型交互使用Text,许多语言模型都是接收Text并输出Text的。因此LangChain中的许多接口都是以Text为中心的
-
ChatMessage:一些模型期望以聊天的方式为用户提供对底层API的访问,用户类型可以分为System、Human、AI。(ChatMessage和ChatModel组合使用)
-
SystemChatMessage:代表系统对AI系统的指示
-
HumanChatMessage:人类和AI的交互
-
AIChatMessage:从AI系统来的交互信息
-
ChatMessage:任意一个角色的信息
-
-
Example:代表对函数(LLM)的输入和预期的输出,可以用于训练和评估模型。
-
Document:一段非结构化的数据,由page_content(数据内容)和metadata(描述数据属性的辅助信息)组成
-
-
Models:LangChain中使用的不同类型的Models,常用的类型有
-
LLMs:大规模语言模型Large Language Models。它接收text作为输入,输出text。
-
Chat Models:它通常由语言模型支持,但是api更结构化。具体来说,接收Chat Messages,输出一个Chat Message
-
Text Embedding Models:文本嵌入模型,输入text,输出一个浮点数列表
-
-
Prompts:prompts指的是model的输入。这种输入很少是硬编码的,而是通常由多个组件构造而成。PromptTemplate负责构造这个input。LangChain提供了几个类和函数来简化prompt的构造和使用。Prompt相关的概念有以下这些:
-
PromptValue:代表model的输入。
-
Prompt Templates:负责构造PromptValue的类。PromptValue不是硬编码的,而是基于用户输入、其他非静态信息(通常来源于多个源)和一个固定的模板字符串的结合动态构建的。
-
Example Selectors:通常情况下,在prompts中包含example是很有用的,example可以是硬编码的,但是如果可以动态选择example会更强大。ExampleSelectors可以接受用户输入并返回一组可用examples。
-
Output Parsers:语言模型(或是ChatModel)输出text,但是我们想要更结构化的数据,所以需要一个output parser。Output Parser负责:1. 指示模型如何结构化输出;2. 将输出解析为所需的格式(必要时重试)。
-
-
Indexes:Indexes是对文档进行结构化以便LLMs能够最好地和它们交互的方法。这个模块包含处理文档的工具函数、不同类型的indexes、以及在Chain中使用这些indexes的例子。
-
Memory:Memory是在对话过程中存储和检索数据的概念。Memory有两个主要的类型:short term和long term。Short term通常指的是如何在单个对话的上下文中传递数据(通常是以前的ChatMessage和它们的摘要),Long term处理的是如何在对话之间获取和更新信息。
-
Chains:Chains会以一组以特定方式组合的模块化组件(或是其他chains)来完成一个常见的用户用例。最通用的使用chain的方法是LLMChain,它结合了PromptTemplate、Model和Guardrails,来获得用户输入、格式化、传递给Model获取一个response、然后验证和修正(如果需要)这个输出。Chain的类型分为以下几种:
-
LLMChain:LLMChain是最常见的chain,它有一个PromptTemplate、Model(LLM或是ChatModel)和一个可选的output parser组成。它接收多个输入变量,使用PromptTemplate来将它们格式化为一个prompt。然后传递给model,最后,使用OutputParser来解析LLM的输出到一个最终的格式。
-
Index-related Chain:这一类的chain用于和indexes交互。这些chains的目的是将你自身的数据(在indexes中存储)和LLM结合起来。最好的例子是给予你自己的文档回答问题。这里很大一部分的内容是理解如何传递多个文档给语言模型。有以下几种不同的方式或是chains可以实现这个目的,注意,没有最佳方法,可以通过上下文决定,这4种类型从简至繁排序如下:
-
Stuffing:最简单的方法,将所有相关的数据塞到promt中作为上下文传递个语言模型。LangChain中的实现类是StuffDocumentsChain
-
MapReduce:在每个数据块(或是汇总任务,这可能是数据块的摘要;对于问答任务,它可能是一个完全基于该数据块的回答)上运行初始prompt。然后运行另一个prompt来结合所有的初始output。对应实现类为MapReduceDocumentsChain。
-
Refine:在第一个数据块上运行初始prompt,生成一些输出。对于后面的文档,输出会和接下来的文档一起传递,要求LLM基于新的文档细化输出。
-
Map-Rerank:在每个数据块上运行一个初始的prompt,评分、排序、选择评分最高的结果返回
-
-
PromptSelector:LangChain中的chain的目标之一是让用户能够尽快基于特定的用例开始工作。其中很大一部分就是需要有好的prompt。PromptSelector负责根据传入的模型动态选择默认的prompt。PromptSelector最常用的例子是为LLM和Chat Model设置不同的默认prompt。
-
-
Agents:一些应用可能不仅仅依赖于对LLM或是其他工具的预先确定的chain,还要就用户的输入依赖一个未知的chain。在这些chain类型中,存在一个Agent,它可以访问一套工具。根据用户的输入,Agent决定调用这些工具中的哪一个。
-
Tools:语言模型和其他资源的交互。是围绕着一个函数的抽象,以便于语言模型交互。具体来说,tool的接口接收单个text输入,输出单个text。(因次,Chain和Agent都可以再次注册成为一个tool)
-
Toolkits:一组结合起来使用以完成特定task的工具集合。
-
Agent Executor:使用tool运行agent的逻辑。一个Agent Executor是一个Agent和一组tools,agent executor负责调用agent,获取action和action input,使用对应的input调用action引用的tool,获取tool的output,然后将所有的信息传回Agent以获取下一个action。简单地理解:Agent类执行单个步骤,AgentExecutor类执行循环。
-
接下来我们可以简单看一下这些组件的交互图。需要注意的是:
-
下图不代表具体实现类在LangChain中的引用关系,eg. OutputParser是PromptTemplate中的成员,但它在交互过程中是针对Model的输出进行结构化
-
下图只是一个典型的使用场景,但LangChain中对这些组件进行了模块化设计,它们可以有更丰富的使用场景。eg. Index除了用于丰富Prompt之外还可以单独使用;Agent也可以不用Chain驱动;Memory也可以作用于Agent。
三、组件具体介绍
LangChain的组件模块化除了为各个组件定义好了标准接口之外,也基于用户的使用场景做了很多的默认实现,通过利用这些默认实现的组件和工具,我们可以降低开发成本,或是学习参考实现其流程。接下来我们对LangChain的各个组件以及默认实现进行详细介绍。
3.1 Models
LangChain目前支持3种类型的模型:
-
LLMs:接收text,返回text
-
Chat Models:底层由语言模型支持,但API更结构化。接收Chat Messages,返回Chat Messages
-
Text Embedding Models:接收text,返回floats的列表(向量)
LangChain为这3种类型的Models分别定义了标准的交互接口,在每种类型下支持了不同的Model提供方。
3.1.1 LLMs
LLM的提供方有很多:OpenAI、Cohere、Hugging、Face等。这一类模型的核心类是LLM,OpenAI是LLM的一个实现类。
LLM的基础方法有:
-
初始化:LLM有一系列的配置参数,可以在初始化时指定,具体可以参考它的序列化后的格式:model_name、temperature等等。
-
_call:直接调用生成text(str)
-
generate:接收一组input,返回一个复杂的response,包含对多个input的响应、LLM提供者的特定信息,例如token统计值等等
-
get_num_tokens:获取一个text对应的token数量。这个接口是很有用的,因为model通常有长度限制
LLM的通用方法有:
-
agenerate:异步调用LLM(利用asyncio库),这对于批量调用多个LLM是很有用的,目前LangChain支持了针对OpenAI、PromptLayerOpenAI、ChatOpenAI和Anthropic的异步API,其他的模型支持在规划中。
-
自定义LLM wrapper:可以引入自己的model,或是定义一个模拟的model。唯一要求实现的方法就是_call,其次也可以实现_identifying_params来提示用户参数信息。
-
FakeLLM:LangChain提供的一个模拟类,可以自定义response来模拟一个LLM。在agent测试时很有用。
-
缓存LLM calls:可以缓存单个LLM的call,有多个实现类型:
-
InMemoryCache:定义langchain.llm_cache = InMemoryCache(),langchain会自动缓存LLM的调用,key为输入的text
-
SQLiteCache:
-
RedisCache
-
GPTCache:用embedding技术缓存语义相等或是相近的调用。省钱、省流。需要初始化指定一个文件的cache_path。
-
SQLAlchemyCache
-
自定义SQLAlchemyCache
-
可选缓存配置(LLM):可以在开启了缓存后针对一个LLM不使用cache,初始化LLM时设置cache = False即可
-
Chain中的可选配置:通过LLM的cache配置实现,例如一个map_reduce chain,我们可以创建2个LLM,在chain中指定reduce_llm为no_cache_llm,那么它就会在map步骤执行cache,在reduce步骤不执行cache
-
-
序列化/反序列化:将LLM的config信息保存为可读写文件
-
load_llm:从json/yaml中创建llm
-
save:将llm的config信息保存为json或是yaml
-
-
流式响应:对使用流程无影响,只是让传输的过程流式化。在初始化是指定callback_manager即可,默认实现为StreamingStdOutCallbackHandler,也可以自定义
-
跟踪token使用:跟踪特定调用的token使用,只针对OpenAI实现。基于一个langchain提供的callback实现:get_openai_callback
3.1.2 Chat Models
ChatModel是基于LLM的一个变体,但它的输入输出是message,可以让prompt更加格式化。简单来说,通过区分角色,让最终的prompt能够明确标记role,使得prompt更贴近于两人chat的模式,也更方便ChatModel区分聊天记录中的角色。
message的类型分为:
-
HumanMessage:代表human所讲的message
-
AIMessage:AI讲的message
-
SystemMessage:system的message
-
ChatMessage:任意一个speaker的message
针对这些Message分类,LangChain也分别提供了对应的PromptTemplate类:SystemMessagePromptTemplate、HumanMessagePromptTemplate、AIMessagePromptTemplate、ChatPromptTemplate。
ChatOpenAI model同样也支持streaming的交互方式,和LLM一样,设置callback=StreamingStdOutCallbackHandler即可。
chat = ChatOpenAI(streaming=True, callback_manager=CallbackManager([StreamingStdOutCallbackHandler()]), verbose=True, temperature=0)
ChatModel中可以使用few shot example的方式,即为model提供一些example来进行答案生成。关于如何在few shot prompting做好最好没有明确的共识,因此,这部分还没有任何固化的抽象,而是直接使用已有的概念。当前的few shot example的实现方式有以下2种:
-
交替的Human/AI messages:即,给出一个ai的回复例子:system + example_human + example_ai + human
-
System messages:OpenAI也提供了一个可选的name参数,他们还建议将该参数与System message相结合来实现few shot prompting。
-
example_human = SystemMessagePromptTemplate.from_template("Hi", additional_kwargs={"name": "example_user"})
-
example_ai = SystemMessagePromptTemplate.from_template("Argh me mateys", additional_kwargs={"name": "example_assistant"})
-
3.1.3 Text Embedding Modesl
Embedding类是设计来和embeddings交互的类,同样有很多提供方:OpenAI、Cohere等等,这个类提供了与提供方交互的标准接口。Embedding会为一段text创建一个vector来代表它。这样我们就可以在向量空间做语义搜索或是文本匹配等任务。
LangChain中的Embedding类开放了2个方法:embed_documentts、embed_query,这两个方法一个可以作用于多个文档,一个只能作用于单个文档,另一个区分这两份发的愿意是embedding提供者对于documents(要被搜索的)和查询(搜索查询本身)有不同的embedding方法。
LangChain中Embedding对OpenAI适配的一个简单例子如下:
from langchain.embeddings import OpenAIEmbeddings
embeddings = OpenAIEmbeddings()
text = "This is a test document."
query_result = embeddings.embed_query(text)
doc_result = embeddings.embed_documents([text])
3.1.4 总结
参考https://foresightnews.pro/article/detail/28959
一旦在 LLM 领域花了足够多的时间,在兴奋之余你会意识到当前模型本身的两点局限:
1. 它只有“脑子”没有“手臂”,无法在外部世界行动,不论是搜索网页、调用 API 还是查找数据库,这些能力都无法被 OpenAI 的 API 提供;
2. 甚至它的“脑子”也不完美,OpenAI 的训练数据截止至 2021 年,并且没有任何企业和个人的私有数据,这让模型只能根据自己的“记忆”回答问题,并且经常给出与事实相悖的答案。一个解决方法是在 Prompt 中将知识告诉模型,但是这往往受限于 token 数量,在 GPT-4 之前一般是 4000 个字的限制。
要打破这些局限,我们可以给LLM更多的信息,或是根据LLM的一些输出来执行对工具的调用,将LLM和外部世界、用户数据、历史数据等信息结合起来。LangChain为我们提供了模块化的实现框架,让我们能够以更低的成本来实现目标。
3.2 Prompts
Prompts是编程模型的一种新方式,它指的是对model的input,这种input很少是硬编码的,通常是由多个组件构造的。参考第二章组件交互图,所有的组件几乎都是在为如何构建更好的Prompt而服务,Prompt就是我们和LLM交互的关键,正确的 Prompt 可以激发出 LLM 的能力,为了打破LLM的局限,我们需要在prompt中传递更多的上下文信息,这也就是传说中的Prompt Engineering。
将 Context 注入 LLM 实际上在 Prompt Engineering 的上游,把知识告诉 LLM,Prompt 只是中间桥梁。前 Stitch Fix 的 ML 总监 John McDonnell 画的这幅图很好地展示出了二者的关系:
本章分为四小节:
-
LLM Prompt Templates:如何使用PromptTemplates来prompt LLM
-
Chat Prompt Templates:如何使用PromptTemplates来prompt Chat Models
-
Example Selectors:在prompts中添加example是非常有用的,这些example通常是硬编码的,但是如果能动态地选择可以发挥更大的作用。
-
Ouput Parsers:负责将model输出的text解析为结构化的信息。它负责:1. 指示model的输出应该如何格式化 2. 将输出解析为所需的格式(必要时重试)
3.2.1 LLM Prompt Template
语言模型接收text,这个text就被成为是一个prompt。Prompt通常不是硬编码的,而是一个template、一些examples和用户输入的结合。LangChain提供了几个类和函数来让prompt的构造和工作更加简单。
PromptTemplate是一种可复制的生产prompt的方式,它包含一个text string(template)、一组参数(来源于终端用户),并且可以产生一个prompt。
PromptTemplate可能会包含:
-
对语言模型的指令
-
一组少量的shot examples来帮助语言模型产生更好的response
-
一个对语言模型的question
LangChain中提供的PromptTemplate的相关类如下图。
在使用LLM时,它接收的输入是text,我们可以更倾向于使用StringPromptTemplate及其子类,它的format_prompt方法将会返回一个封装了text的StringPromptValue。如果不需要在prompt中封装examples,可以直接使用PromptTemplate;
如果需要为LLM提供example时,可以使用FewShotPromptTemplate(它和FewShotPromptWithTemplates功能差不多,看起来是后者被废弃了),FewShotPromptTemplate中包含一个example_prompt、一组可选的examples和可选的example_selector、prefix和suffix,在format时选出example、格式化、结合prefix和suffix构建为最终的输出。
在使用FewShotPromptTemplate时,如果你有很多examples,那么可以使用ExampleSelector来选择一组对于语言模型来说能够提供最多信息的例子。这可以帮助获得更好的response。LangChain当前提供了以下内置的ExampleSelector:
-
LengthBasedExampleSelector:基于长度限制选择长度合适的例子
-
MaxMarginalRelevanceExampleSelector:基于余弦相似度选择最相似的例子
-
NGramOverlapExampleSelector:基于ngram重叠分数选择合适的例子
-
SemanticSimilarityExampleSelector:用embedding进行余弦相似度计算,选择最相似的例子
注意,FewShotPromptTemplate只是LangChain提供的一个工具类,本质上也是为了让Prompt的拼接成本更低。如果不使用FewShotPromptTemplate和ExampleSelector,我们直接基于python也可以做到在PromptValue中拼接上example信息。
3.2.1.1 自定义PromptTemplate
虽然LangChain提供了以上的默认prompt templates,可用于为各种任务生成prompts。但是在某些情况下,默认的prompt templates可能不能满足你的要求。例如,你可能希望为你的语言模型生成带有特定指令的prompt template。在这种情况下,可以创建一个自定义的prompt template。
参考上一小节中的类图,prompt template分为两类:string prompt template和chat prompt template,后者能够支持更结构化的chat prompt。
以自定义string prompt template为例,基于它进行自定义有2个要求:
-
input_variables属性,它公开了prompt template预期的输入输入变量
-
format方法,它接收对应于input_variables的keyword arguments,返回格式化的prompt
一个接收function name,返回该function对应的source code的自定义prompt template的样例如下所示:
from langchain.prompts import StringPromptTemplate
from pydantic import BaseModel, validatorclass FunctionExplainerPromptTemplate(StringPromptTemplate, BaseModel):""" A custom prompt template that takes in the function name as input, and formats the prompt template to provide the source code of the function. """@validator("input_variables")def validate_input_variables(cls, v):""" Validate that the input variables are correct. """if len(v) != 1 or "function_name" not in v:raise ValueError("function_name must be the only input_variable.")return vdef format(self, **kwargs) -> str:# Get the source code of the functionsource_code = get_source_code(kwargs["function_name"])# Generate the prompt to be sent to the language modelprompt = f"""Given the function name and source code, generate an English language explanation of the function.Function Name: {kwargs["function_name"].__name__}Source Code:{source_code}Explanation:"""return promptdef _prompt_type(self):return "function-explainer"
3.2.1.2 partial PromptTemplate
前面所说的prompt template是一个具有.format方法的类,它接收一个key-value的map,并返回一个string(一个prompt)来传递给语言模型。像其他方法一样,对prompt template进行“partial”是有意义的 -- 例如,传入所需值的子集,来创建一个新的prompt template,这个新的prompt template只需要剩余的值子集。
LangChain通过两种方式支持partial prompt template:
-
以string value的形式:常见的场景是,多个参数获取的时间点不同,eg,有2个参数,分别在不同的chain中获得。PromptTemplate.partial(key=value)
-
用返回string values的functions:场景是,有一些变量想通过通用的方式获取,例如,时间/日期变量。 PromptTemplate.partial(date=_get_datetime),其中_get_datetime是一个无参数function。也可以在PromptTemplate初始化的时候将partial_variable进行初始化(key=function)
3.2.1.3 序列化PromptTemplate
通常更可取的做法是将提示符存储为文件而不是python代码。这可以很容易地共享、存储和版本提示。
LangChain在设计PromptTemplate的序列化功能时遵循以下原则:
-
可读性:同时支持JSON和YAML格式,以增加其可读性。
-
组合性:可以选择将PromptTemplate的所有内容都序列化到一个文件中,也可以将不同的组件(templates、examples)序列化到不同的文件中,以引用的方式使用,便于复用。注意,这些组件可以选择其他格式,例如,templates可以选择保存在一个txt文件中,在JSON中以file path的方式引用。
LangChain对于PromptTemplate的序列化和反序列化方法较为简单
-
加载:from langchain.prompts import load_prompt
-
保存:BasePromptTemplate::save(self, file_path: Union[Path, str]) -> None
3.2.2 Chat Prompt Template
Chat Model可以接受一组chat message作为输入,这个message的列表被称为一个prompt。通常chat prompt也不是一组硬编码的messages,而是template、一些examples和用户输入的组合。
参考3.1.2 小节中的类图,Chat Model所用的prompt template类型为ChatPromptTemplate,ChatPromptTemplate由一组BaseMessagePromptTemplate组成,而BaseMessagePromptTemplate的实现类则是与ChatMessage对齐的。这里相关类的UML如下:
其中ChatMessagePromptTemplate、HumanMessagePromptTemplate、AIMessagePromptTemplate、SystemMessagePromptTemplate的format方法会分别产生对应的message,ChatPromptTemplate则由一组BaseMessagePromptTemplate或是BaseMessage组成,它的format方法将会返回一个str。
3.2.3 Example Selector
如果你有很多的examples,你可能需要选择哪些应该被用在prompt中。ExampleSelector就是负责做这件事的类,它的核心方法为select_examples,接收输入变量,返回一组examples。
当我们想要自定义一个ExampleSelector时,需要实现2个方法:
-
add_example:接收一个example,添加到ExampleSelector中。
-
select_examples:接收一组输入变量(即用户输入)返回一组examples,用于few shot prompt。
当前LangChain提供了一些内置的ExampleSelector,用于不同的场景(在3.2.1小节中已经提过):
-
LengthBasedExampleSelector:基于长度限制选择长度合适的例子
-
MaxMarginalRelevanceExampleSelector:基于余弦相似度选择最相似的例子
-
NGramOverlapExampleSelector:基于ngram重叠分数选择合适的例子
-
SemanticSimilarityExampleSelector:用embedding进行余弦相似度计算,选择最相似的例子
3.2.4 Output Parser
语言模型输出text,但是我们想要更格式化的数据,所以有了OuputParser,它用来帮助对语言模型的response进行结构化。
OutputParser的两个主要方法和一个可选方法:
-
get_format_instructions() -> str:返回一个包含语言模型的输入应该如何格式化的字符串
-
parse(str) -> Any:接受一个string(语言模型的response),将它解析为一些结构化的数据
-
(option) parse_with_prompt(str) -> Any:接受一个string(response)和一个prompt(产生response的prompt),并解析为一些结构化数据。使用场景为OutputParser希望以某种方式重试或是修复输出,因此需要prompt信息。
从以上方法可以看出,OutputParser的功能会在2个地方发挥作用:
-
构建prompt:OutputParser所生成的格式化命令,会构建prompt,传递给LLM/Chat Model
-
解析结果:当LLM按照指令返回了最终结果后,OutputParser可以尝试将返回值解析为结构化的数据
我们来看看这2个地方对OutputParser的使用方法:
-
构建prompt:从parser获取format_instructions,传递给PromptTemplate,利用它的partial方法传入一个预定义好的参数key中:format_instructions。注意,这种格式是LangChain预设好的,它在后续拼接prompt时会去专门识别format_instructions参数,相当于这是它的一致内置参数。我们来看一个LangChain内置的format_instructions的例子:
-
conversation_chat agent中的一个format_instructions
FORMAT_INSTRUCTIONS = """RESPONSE FORMAT INSTRUCTIONS
----------------------------
When responding to me please, please output a response in one of two formats:
**Option 1:**
Use this if you want the human to use a tool.
Markdown code snippet formatted in the following schema:
```json
{{{{
"action": string \\ The action to take. Must be one of {tool_names}
"action_input": string \\ The input to the action
}}}}
```
**Option #2:**
Use this if you want to respond directly to the human. Markdown code snippet formatted in the following schema:
```json
{{{{
"action": "Final Answer",
"action_input": string \\ You should put what you want to return to use here
}}}}
```"""
-
-
解析结果:直接使用OutputParser::parser方法,或是可以将OutputParser传入PromptTemplate类中作为它的一个成员,便于后续解析。当我们想在chain中进行output parser时,一般都会采用后者的方法,便于程序进行管理。
当前LangChain内置的OutputParser UML图如下:
大部分OutputParser通过名称即可望文知意,这里我们重点看一些比较特殊的实现:
-
StructuredOutputParser:它和PydanticOutputParser一样,都可以允许用户指定任意的JSON schema,要求LLM按照schema输出。但StructuredOutputParser可以被取代。PydanticOutputParser初始化时会接受一个python object类,在获取format_instructions会获取这个object的schema,dump成json并按照下面的模板格式化输出;在parser时会去尝试从str中匹配到json,并使用json.load构建一个JSON对象,再使用python object的parse_obj方法来生成结构化的对象
-
PydanticOutputParser的format_instructions template
PYDANTIC_FORMAT_INSTRUCTIONS = """The output should be formatted as a JSON instance that conforms to the JSON schema below.
As an example, for the schema {{"properties": {{"foo": {{"title": "Foo", "description": "a list of strings", "type": "array", "items": {{"type": "string"}}}}}}, "required": ["foo"]}}}}
the object {{"foo": ["bar", "baz"]}} is a well-formatted instance of the schema. The object {{"properties": {{"foo": ["bar", "baz"]}}}} is not well-formatted.
Here is the output schema:
```
{schema}
```"""
-
-
OutputFixingParser:它可以封装其他的output parser尝试解决错误,即它会获取到其他output parser的错误输出,和format指令,要求model去解决它
-
RetryOutputParser:有些场景下错误的输出没办法修复,例如返回结果只填充了部分,此时OutputFixingParser也解决不了问题,可以采用RetryOutputParser,它会结合prompt再次请求model,尝试获取正确的结果,因此retry_parser里需要传入llm作为成员
3.2.5 总结
LangChain所提供的Prompt相关类都是为了让开发者能够更加低成本、准确地拼接出高质量的Prompts。在实际开发中,我们也需要重视Prompt的设计,让Prompt的设定规范化。这里需要提前确定一些Prompt的设计原则,一般原则如下:
-
清晰Clarity:清晰而简洁的提示将有助于确保ChatGPT理解手头的主题或任务,并能够生成适当的响应。避免使用过于复杂或模棱两可的语言,在提示中尽量做到具体。
-
重点Focus:明确定义的提示应该有明确的目的和重点,有助于引导对话并保持在正轨上。避免使用过于宽泛或开放式的提示,这可能会导致脱节或不集中的对话。
-
相关性Relevance:确保你的提示与用户和对话相关。避免引入不相关的话题,否则会分散谈话的主要焦点。
另外可以在定义Prompts时注意的一些技巧,例如:
-
避免术语和歧义:如果一定要使用术语,可以提前定义,为model提供清晰的定义或解释
-
角色提示:通过为AI分配角色的方式引导AI给出更准确的输出
-
恰当使用思维链:思维链(Chainn of Thought)是一种使用少量示例来指导人工智能模型完成任务的技巧。思维链的目的是让模型能够理解任务的逻辑和步骤,而不仅仅是复制示例。例如,如果您想让模型回答一个问题,您可以在输入中提供一些相关的信息和推理过程,而不只是一个简单的问题。
-
禁止胡编乱造的指令:避免ChatGPT编造错误的答案,Prompt中可以默认添加诚实回答禁止胡编乱造的指令:如果你不知道,就请回答不知道。
除此之外,LangChain所提供的一些内置的实现中,也有较多的PromptTemplate定义,我们可以充分参考,例如:
【PydanticOutputParser的format_instructions template:让LLM按照指定格式进行输出的指令】
PYDANTIC_FORMAT_INSTRUCTIONS = """The output should be formatted as a JSON instance that conforms to the JSON schema below.
As an example, for the schema {{"properties": {{"foo": {{"title": "Foo", "description": "a list of strings", "type": "array", "items": {{"type": "string"}}}}}}, "required": ["foo"]}}}}
the object {{"foo": ["bar", "baz"]}} is a well-formatted instance of the schema. The object {{"properties": {{"foo": ["bar", "baz"]}}}} is not well-formatted.
Here is the output schema:
```
{schema}
```"""【ConversationChain的默认promptTemplate】
_DEFAULT_TEMPLATE = """The following is a friendly conversation between a human and an AI. The AI is talkative and provides lots of specific details from its context. If the AI does not know the answer to a question, it truthfully says it does not know.
Current conversation:
{history}
Human: {input}
AI:"""
3.3 Indexes
Index是指结构化document的方法,以便LLM能够和它们最好地交互。这个模块包含了处理document的实用函数、不同类型的index,以及在chains中使用index的例子。
在chain中使用index的最常见方式是在retrieval步骤,这个步骤指的是获取用户的查询并返回最相关的文档。需要注意:
-
index除了retrieval之外还可以做其他事
-
retrieval处理使用index之外还可以用其他逻辑来查找相关文档
这里的index和retrieval主要指非结构化的document,目前主要围绕着vector databases实现。LangChain主要侧重于构造indexes,目标是将它们用作Retriever。LangChain中提供了一个BaseRetriever类,它有一个方法,可以根据自己的需求实现:
-
get_relevant_documents:接收一个str的query,返回一个Document的List
当前LangChain默认使用Chroma作为vector store来index和search embeddings。基于document问答的步骤如下,其中每个步骤都有多个子步骤和潜在的配置信息:
-
创建index:VectorstoreIndexCreator基于TextLoader创建index
-
基于index创建Retriever
-
创建一个问答chain
-
问问题
我们来看下Index、Retriever、Chain之间的关系,参考下面UML图和代码细节可以了解到这些信息:
-
VectorstoreIndexCreator可以创建VectorStore
-
BaseRetriever是VectorStore中的检索器,但不具备更新数据的能力,可以从VectorStore中获取retriever
-
retriever不是chain的必要组件,只有在少部分特定场景的chain的实现类(RetrievalQA、VectorDBQA...)中才会使用vectorstroe和retriever
-
使用到retriever或是vectorstore的chain,它们利用retriever/vectorstore的最终目的都是为了丰富prompt内容,为prompt填充更丰富的、相关的上下文。以RetrievalQA为例:
-
RetrievalQA的父类BaseRetrievalQA,在初始化时创建了一个默认的document_prompt = "Context:\n{page_content}",且定义了一个固定的combine_documents_chain = StuffDocumentsChain成员。同时,基于llm进行了一次prompt selector,为chat model选择的默认PromptTemplate为:
-
"""Use the following pieces of context to answer the users question.
If you don't know the answer, just say that you don't know, don't try to make up an answer.
----------------
{context}""" + """{question}"""
-
-
BaseRetrievalQA在执行时,会先根据用户输入调用_get_docs获得一批docs,将docs传入combine_documents_chain中获得answer。combine_documents_chain在执行时会根据类型分别处理docs,例如,对于stuff类型,那么就是直接将doc原样拼接起来作为一个prompt
-
现在,我们对index有了一个更深入的了解了,index、retriever的抽象是LangChain为了简化携带context与LLM交互而抽象出来的概念(但不要忘记index还可以在其他场景使用),基于这些组件LangChain提供了类似RetrievalQA这样的chain,自动将:基于输入检索文档 -> 填充Prompt -> 与LLM交互并解析结果 这个流程进行了封装。
我们可以在场景匹配的时候直接使用LangChain所提供的这些现成工具,但也不要被LangChain所提供的工具限制了。我们可以低成本地切换prompt中上下文的获取方式,不一定需要使用LangChain的index和retriever,只要能保证最终Prompt能够获取到有效的上下文信息即可,如果需要自定义,可以参考LangChain设置的默认prompt template。
3.3.1 Document Loaders
DocumentLoader是为了让用户将LLM和自己的文本数据结合起来,它实现了这个过程的第一步,即load。LangChian为了让load数据变得更简单,支持了多种数据格式/数据源的loader。它的实现充分利用了python的Unstructured包,这个包是一个转换类型文件到text的很好的方法:text,powerpoint,图像,htlm,pdf等等。
LangChain中的loader基类为BaseLoader,它定义了2个方法:
-
load:将数据加载为List[Document],是一个抽象方法
-
load_and_split:接收一个可选的text_splitter,先load数据后后,再进行split,同样返回List[Document],具有默认实现
我们来看下2个典型的Loader实现:
-
DataFrameLoader:
-
初始化时接收一个pandas的DataFrame和一个page_content_column,说明当前只能处理DataFrame中的单列数据。
-
load方法实现中,从DataFrame的每一行数据取出page_content_column列,每行数据都转换为一个Document
-
-
TextLoader:
-
初始化时接收一个file_path与encoding方式
-
load方法实现中,将整个文件读取出来作为一个text,转换为一整个Document
-
3.3.2 Text Splitters
在处理长文本时,将它们分成多个块是很有必要的,即我们可以在DocumentLoader类中使用它的load_and_split方法获得多个Document块,能够提升检索的效果。理想情况下,我们希望将语义相关的文本放在一起。
Text Splitter的工作流程如下:
-
将text分为语义上有意义的小块(通常是句子)
-
将小块合并起来生成更大的快,直到单个chunk的size达到要求
-
达到size要求后,基于这些块开始创建一些有上下文重叠的新文本块(保持块之间的上下文)
这也意味着,当我们定制自己的text splitter时,需要考虑:text如何split,以及,chunk size如何衡量。
默认推荐的text splitter是RecursiveCharacterTextSplitter,它接收一组characters,基于characters进行拆分,分隔符是:是["\n\n", "\n", "", ""]。用户可控的配置如下(即TextSplitter的初始化参数):
-
length_function:chunk长度计算方法,默认情况下是按照characters进行计数,但其实使用token进行计数是更合理的。默认是len function。
-
chunk_size:最大的chunk size,由length方法计算。默认为4000
-
chunk_overlap:chunks之间重叠的最大值。默认为200
LangChain的内置TextSplitter有:
-
CharacterTextSplitter:基于character进行拆分和衡量length,分隔符为“\n\n”
-
LatexTextSplitter:基于Latex语法进行分割,考虑headings、headlines、enumerations等,它是RecursiveCharacterSplitter基于Latex-specific separators的一个子类。length function默认为characters,也可以替换
-
MarkdownTextSplitter:基于Markdown语法分割
-
NLTKTextSplitter:基于自然语言NLTK(Natural Language Toolkit)分割
-
PythonCodeTextSplitter:基于python类和方法定义分割,是一个基于Python语法作为分隔符的RecursiveCharacterSplitter实现
-
RecursiveCharacterTextSplitter:默认分割符["\n\n", "\n", " ", ""]。这样做的效果是尽量将段落-句子-单词放在一起,因为段落通常是语义相关度最高的文本片段。一般文本建议使用此类。
-
SpacyTextSplitter:一个NLTK的可选替代方案
-
TokenTextSplitter:基于tiktoken进行分割
LangChain的内置length function有:
-
HuggingFaceLengthFunction:由于LLM通常是对token size有限制而不是characters size,CharacterTextSplitter可以通过from_huggingface_tokenizer来切换chunk length计算方式为token size。该方法为父类TextSplitter中定义的_huggingface_tokenizer_length,TextSplitter的实现类可以调用from_huggingface_tokenizer进行初始化。
-
tiktoken(OpenAI) Length Function:OpenAI提供的token计算器。该方法为父类TextSplitter中定义的_tiktoken_encoder,TextSplitter的实现类可以调用from_tiktoken_encoder进行初始化
3.3.3 VectorStores
VectorStore的一个重要工作是通过embeddings创建vector并将它们存储起来。它是构建indexes过程中最重要的组件。
VectorStore工作的经典流程:
-
从file中读文件
-
创建TextSplitter
-
将文件split成chunks
-
初始化Embedding服务(OpenAIEmbedding)
-
使用chunks和embedding服务创建一个vectorstore(LangChain默认的Chroma.from_texts(texts, embeddings), 也可以from_documents,不知道有啥区别)
-
创建一个string的query
-
用vectorstore.similarity_search搜索相似的docs
如3.3章节中的UML图所示,VectorStore的抽象类定义了以下方法:
-
add_texts:抽象方法,接收一个texts和一个可选的metadatas。具体的VectorStore可以通过embedding将texts加入到存储中。
-
add_documents:默认实现方法,将document转换为texts和metadatas,调用add_texts方法
-
similarity_search:抽象方法,接收一个str的query、一个限定返回值数量的k,返回和query最相似的k个Document
-
similarity_search_by_vector:默认实现方法,直接接收经过embedding的一个vector,返回和其最相似的k个Document。默认实现逻辑为直接抛NotImplementedError
-
max_marginal_relevance_search:默认实现方法,接收一个str的query、一个限定返回值数量的k,使用最大边际相关性(maximal marginal relevance)返回k个Document。
-
Maximal Marginal Relevance (a.k.a MMR) 算法目的是减少排序结果的冗余,同时保证结果的相关性。最早应用于文本摘要提取和信息检索等领域。在推荐场景下体现在,给用户推荐相关商品的同时,保证推荐结果的多样性,即排序结果存在着相关性与多样性的权衡。最大边际相关性(MMR)标准致力于减少冗余,同时在重新排序检索文档和选择适当的段落进行文本摘要时保持查询相关性。
-
-
max_marginal_relevance_search_by_vector:和前一方法相似,区别在于接收的query从str变成了vector
-
from_documents:类方法,基于一组documents和embedding初始化一个VectorStore并返回。默认实现,调用了from_texts方法
-
from_texts:类方法,抽象方法,接收一组texts、metadatas和embedding,初始化一个VectorStore并返回。在实现时,它大概会做以下事情:
-
对text进行embedding
-
为这一组texts创建一个index,index可以认为是一个逻辑独立的集合。如果是像redis这样的外部服务,那么会为这一批texts创建相同的key前缀;如果是像chroma这样的内存组件,那么会为它创建一个Chroma实例
-
将第一步的embedding加入到第二步创建的index中
-
-
as_retriever:默认实现方法,默认创建一个引用自身的VectorStoreRetriever,并返回。Retriever也有不同的实现,VectorStoreRetriever是一个和vector store绑定的默认实现。
LangChain支持了多种vector store,我们也可以自定义vector store。常用的vectorstore如下:
-
Chroma:可以持久化到文件,可以本地创建,在数据量不大时可以使用,是LangChain的默认vectorstore
-
ElasticSearch:可以指定集群地址进行连接
-
...
3.3.4 Retrievers
retriever接口是一个通用接口,它可以很容易地将文档与语言模型结合起来。这个接口公开了一个get_relevance _documents方法,该方法接受一个查询(一个字符串)并返回一个文档列表。
需要注意的是,VectorStore也提供了检索接口,一个retriever只能有一个search_type:similarity(默认)或是mmr,可以认为VectorStore的检索接口覆盖了retriever的接口功能,而retriever是一个固定search_type的只读的vectorstore。LangChain所提供的默认实现chain中,有一些是使用retriever进行检索如RetrievalQA,有一些是直接使用vectorstore进行检索如VectorDBQA。个人认为这两种实现是几乎等价的。
retriever的实现中,常用的是和vectorstore绑定的VectorStoreRetriever,除此之外还有:
-
ChatGPT Plugin Retriever
-
Databerry
-
ElasticSearch BM25
-
Metal
-
Pinecone Hybrid Search
-
SVM Retriever
-
TF-IDF Retriever
-
Time Weighted VectorStore Retriever
-
VectorStore Retriever(和VectorStore绑定)
-
Weaviate Hybrid Search
3.4 Memory
默认情况,chains和agent是无状态的。但在有一些场景,例如chat,记住之前的交互是非常有用的,无论是短期的还是长期的。这就由Memory实现。
LangChain提供两种形式的Memory组件。首先,LangChain提供了辅助工具,用于管理和操作以前的聊天消息。这些设计都是模块化的,无论如何使用都很有用。其次,LangChain提供了将这些工具合并到chain中的简单方法。
Memory涉及在用户和LLM的整个交互中保持状态的概念。用户和LLM的交互是在ChatMessages概念中被捕获的。因此这可以归结于,从一系列的ChatMessages中摄取、捕获、转换和提取知识。这有很多的实现方法,每个方法都有自己的memory类型。Memory可以返回多段信息,例如,可以返回最近N调messages以及前面所有messages的总结。返回信息可以是一个str,也可以一个messages的list。
支撑大多数(不是全部)内存模块的核心使用工具类是ChatMessageHistory类,这是一个超级轻量的包装器,它开放了用于保存Human messages、AI messages,以及获取它们的便捷方法。我们可以认为它是一个基于内存保存的history,它的父类还有基于其他存储引擎的实现,如下图所示。它们的方法都是一致的,只是存储引擎不相同。
现在我们可以来看,如果在chain中使用memory。我们首先看ConversationBufferMemory,它是一个ChatMessageHistory的封装器,它可以从将消息提取到一个变量中。我们可以来看下Memory相关的整体类图:
基于代码结构,我们可以总结关于memory的以下要点:
-
memory分为chat和非chat,其中chat相关的memory限制了存储的消息格式是BaseMessage,而非chat的memory没有限制存储格式
-
当在chat场景想要提取memory,通常会将所有的对话一并提取出来。因此,在一些Conversation相关的memory实现中,通常会定义一个确定的memory_key = "history",注意,这个history最后会拼接到prompt中,也需要和prompt中的参数匹配。
-
除了提取全量历史之外,也有一些场景需要执行key来提取memory。因此在提取memory时,可以指定input,相当于是去memory中做了一次匹配(可以是精确匹配,也可以自定义处理逻辑)。
-
memory开放了接口,因此对memory数据的处理可以按需自定义。例如,ConversationEntityMemory中,在保存历史数据时使用了一个LLMChain,要求LLM从输入中提取entity出来,这里的这个LLM的prompt见下方。
-
_DEFAULT_ENTITY_SUMMARIZATION_TEMPLATE
"""You are an AI assistant helping a human keep track of facts about relevant people, places, and concepts in their life. Update the summary of the provided entity in the "Entity" section based on the last line of your conversation with the human. If you are writing the summary for the first time, return a single sentence.
The update should only include facts that are relayed in the last line of conversation about the provided entity, and should only contain facts about the provided entity.
If there is no new information about the provided entity or the information is not worth noting (not an important or relevant fact to remember long-term), return the existing summary unchanged.
Full conversation history (for context):
{history}
Entity to summarize:
{entity}
Existing summary of {entity}:
{summary}
Last line of conversation:
Human: {input}
Updated summary:"""
-
3.4.1 Memory在Chain中的使用
memory的作用是记录下历史交互,并在新的prompt中提供历史信息。LangChain提供了一种memory使用的便捷方法,即在Chain中直接使用memory,由Chain来自动为我们处理历史消息的存储,和提取拼接到prompt的过程。
我们先来看一个简单的例子,在ConversationChain中使用memory,如下方代码所示,初始化memory并将它传入ConversationChain作为一个成员即可:
from langchain.llms import OpenAI
from langchain.chains import ConversationChainllm = OpenAI(temperature=0)
conversation = ConversationChain(llm=llm, verbose=True, memory=ConversationBufferMemory()
)
同时ConversationChain具有一个内置的PromptTemplate(可修改),这个PromptTemplate有2个变量:history、input,其中history来源于memory,用户是无感知的,input则是对话过程中用户的输入。
_DEFAULT_TEMPLATE = """The following is a friendly conversation between a human and an AI. The AI is talkative and provides lots of specific details from its context. If the AI does not know the answer to a question, it truthfully says it does not know.
Current conversation:
{history}
Human: {input}
AI:"""
再看ConversationChain的执行过程,我们可以发现:
-
最基础的基类Chain中,具有一个可选的memory成员,默认为空:memory: Optional[BaseMemory] = None。在某几个具体的Chain实现类中会为memory赋予默认的Memory类,用户在构建chain时也可以自行指定memory组件。
-
在Chain的执行过程中,首先会对input进行处理,目的是区分出系统参数memory和用户参数,同时会请求memory组件,获取到系统参数memory对应的值
-
在获取到值后,会将memory作为PromptTemplate的一个正常值进行format,并请求LLM。这里需要注意的是,memory的参数key(在Memory组件中定义)和PromptTemplate中的参数名(一般在Chain中自己定义或是由用户输入)必须保证一致(一般都定义为history)。
-
处理LLM的输出,请求memory组件保存这一次的会话,并将输出返回给用户
我们可以了解到memory在这里的目的:为了让在Prompt中添加历史数据的流程更自动化的工具组件。如果不使用memory组件,我们手动定义PromptTemplate并拼接上历史数据也能达到一样的效果。当然,如果能够直接使用LangChain的组件我们可以节省不少开发成本。
3.4.2 LangChain内置Memory实现
注意,Memory除了在Chain中使用,还可以在Agent中使用。同时Memory还可以共享,可以基于一个Memory构建一个ReadOnlySharedMemory,在Agent和Chain之间共享Memory(或是在多个Chains建共享)。
常用的一些Memory实现及其主要功能如下(不是全部):
-
ConversationBufferMemory:可以保存messages,然后以string或是message的方式提取,在chain中使用时,直接作为chain的一个初始化成员即可
-
ConversationBufferWindowMemory:保存一段时间内的交互,只使用最后的K次交互,可以保证buffer不会太大。
-
ConversationEntityMemory:保存特定实体的memory,使用LLM提取指定实体的知识,并随着时间的推移建立关于该实体的知识(也使用llm)。这里的entity不是一个指定的entity,而是LLM自己提取出的泛化的实体,有点类似下面的知识三元组。
-
ConversationKGMemory:Conversation Knowledge Graph Memory,使用知识图谱来重新创建memory。使用时可以从一个string中提取出entity,也可以更模块化地从一个新消息中获取知识三元组。
-
memory.get_current_entities("what's Sams favorite color?")
-
memory.get_knowledge_triplets("her favorite color is red")
-
ConversationSummaryMemory:总结历史消息,对于从对话中浓缩信息非常有用。
-
ConversationSummaryBufferMemory:它在内存中保留了一个最近交互的缓冲区,但并不是完全刷新旧的交互,而是将它们编译成一个摘要并同时使用。但与之前的实现不同,它使用token长度而不是交互数量来确定何时刷新交互。
-
ConversationTokenBufferMemory:ConversationTokenBufferMemory在内存中保存最近交互的缓冲区,并使用token长度而不是交互数量来确定何时刷新交互。
3.4.3 总结
Memory本质上是一个按照key存储和提取的服务,一般来说,我们使用Memory是为了填充PromptTemplate为历史数据预留的参数,但Memory的作用也并不局限于此,我们也可以利用Memory存储一些全局变量,从Memory提取出来的数据也并不一定填充到PromptTemplate中。了解Memory的本质功能:一个指定key存储和提取的服务,我们可以利用它做更多事情。
但如果场景匹配,我们直接使用LangChain的Memory和Chain组件也能节省不少开发成本,根据实际使用场景选择即可。
3.5 Chains
单独使用LLM对于一些简单应用来说是合适的,但是许多更复杂的场景需要将LLM连接起来 - 和其他LLM连接或是和其他服务连接。LangChain为Chains提供了一个标准的接口,以及一些易于使用的Chain的通用实现。
为什么我们需要Chain?Chains允许我们将多个组件结合起来创建一个独立的、连贯的应用。例如,我们可以创建一个chain,它接收用户输入,通过一个PromptTemplate格式化,然后将格式化后的结果传递给LLM。我们可以通过将多个chains结合在一起,或是将chains和其他组件结合在一起,来构建更复杂的chains。
需要注意的是,Chain和LLM之间并没有强绑定关系,Chain的设计理念更贴近于查询引擎中的火山模型设计理念。Chain就是Operator,每个Chain/Operator只需要关心自身的输入输出,它们可以作为迭代器来组装一个复杂的执行过程。
LangChain提供了很多Chain的实现,和前面章节提到的一样,很多的Chain实现类的目标是为特定的场景提供更简易的解决方案,将LLM和一些确定的组件/服务进行了链接。我们可以先来看一下Chain的UML:
3.5.1 Chain的通用方法
Chain的成员有:
-
memory: Optional[BaseMemory]:memory组件,默认为None,chain的默认_call方法在运行前会首先针对memory组件进行memory的参数提取
-
callback_manager: BaseCallbackManager:chain调用的callback,会处理llm的start、end、new_token、error;chain的start、end、error;tool的start、end、error;agent的action、finish等等一系列时间
-
verbose:调试参数,当开启时会输出更多中间信息
Chain的通用方法有(调试相关的忽略):
-
_chain_type:获取str的类型,每个chain的实现都有一个字符串的类型,便于Chain序列化和反序列化
-
input_keys:获取Chain所期望的输入参数的key,类型为一个List[str],可以用来进行参数合法性校验
-
output_keys:Chain所期望的输出keys,即_call方法的输出keys
-
_validate_inputs:接收一个dict类型的inputs,利用input_keys进行输入合法性校验
-
_validate_outputs:接受一个dict类型的outputs,利用output_keys进行输出合法性校验
-
_call:抽象方法,是用户需要实现的Chain的实际执行方法,接受一个dict的input,返回一个dict的output
-
_acall:_call的异步方法
-
__call__:Chain的执行入口,具有默认实现,其执行逻辑为:
-
调用prep_inputs进行输入的预处理,即当有memory组件时,获取memory组件的数据,加入到inputs中,并调用_validate_inputs执行输入参数的合法性校验
-
通知callback_manager有chain_start事件发生
-
尝试调用_call方法获得output
-
基于inputs、outputs和return_only_outputs进行prep_outputs,此时如果有memory组件,会自动保存本次会话的信息,并按照要求返回输出(只返回输出/同时返回输入输出)
-
-
acall:__call__的异步方法
-
prep_outputs:如果有memory组件,会自动保存本次会话的信息,并按照要求返回输出(只返回输出/同时返回输入输出)
-
prep_inputs:当Chain只有一个用户参数时,它可以接受一个非dict类型的输入。此函数主要是为了处理memory的系统输入信息并格式化输入
-
apply:接受一组input,依次调用__call__执行,返回一个output的list
-
run:接受位置参数和关键字参数,返回一个str,只针对inputkeys(要么是位置参数要么是关键字参数)和outputkeys只有一个的场景,实际执行时调用__call
-
arun:run的异步方法
-
dict:将Chain自身转换为一个Dict,用来进行序列化和反序列化,但只能在没有memory组件时调用
-
save:建自身序列化并存储到一个指定的文件路径或是str中
基于Chain的类信息,我们也可以知道,Chain和LLM并没有强绑定,我们可以用Chain来实现任意功能;同时,只要输入和输出能够匹配,我们可以任意地将两个Chain连接起来,此时每个Chain也只需要关注自身的处理逻辑即可(和火山模型的设计理念类似)
3.5.2 常见Chain
LangChain提供了很多种内置的Chain,这里我们重点看几个常见的Chain:LLMChain、SequentialChain、RetrievalQA,并简单列举出剩余Chain。
LLMChain:虽然Chain本身没有和LLM强绑定,但LangChain针对需要和LLM交互的场景定义了LLMChain,它会基于LLM运行一些查询。同时基于LLMChain又发展出适用于更具体场景的Chain:ConversationChain、QAGenerationChain、QAEvalChain。注意,并不是说所有需要和LLM交互的chain都要继承LLMChain。
相比于父类Chain,LLMChain具有以下特点(注意LLMChain的定义是不带memory组件的,因此没有调用prep_inputs方法):
-
新增成员prompt:BasePromptTemplate、llm:BaseLanguageModel
-
约束output_keys只能是一个叫做text的str类型
-
input_keys等价于BasePromptTemplate的input_variables
-
重写了apply方法,调用generate方法生成reponse(LLMResult),并调用create_outputs将LLMResult转换为list dict的结果
-
针对和LLM的交互,新增了方法:
-
generate:利用inputs来prep_promts,得到prompts和stop信息,调用LLM的generate_prompt,得到LLMResult
-
agenerate:generate的异步方法,调用aprep_promts和agenerate_prompt
-
prep_prompts:接收一个list的input(dict),基于prompt进行format,最终得到一组PromptValue
-
aprep_prompts:prep_prompts的异步方法
-
aapply:通用apply的异步方法,调用agenerate
-
create_outputs:将LLMResult中Generation的text提取出来,构建一个List[Dict[str, str]],其中key即为output_key,固定位text
-
predict:接收一组kwargs,将其作为input调用__call__方法,获得一个str返回值
-
apredict:predict的异步方法,调用acall
-
predict_and_parse:PromptTemplate中可能包含OutputParser,这里先调用predict方法获取str返回值,再调用output_parser对齐记性解析。返回值类型为Union[str, List[str], Dict[str, str]]。注意,如果走parser,它就是一个独立的、需要被单独调用的执行入口,且parse不能在SequentialChain中调用,因为SequentialChain只会调用__call__方法。
-
apredict_and_parse:predict_and_parse的异步方法
-
apply_and_parse:接收一组输入,调用apply方法获得结果,再调用_parser_result方法解析结果
-
_parse_result:针对一组输出结果,使用PromptTemplate中的OutputParser进行解析
-
aapply_and_parse:异步方法
-
from_string:接受一个llm和str的template,构造自身
-
注意,这里LLMResult并没有限定PromptTemplate,当我们需要自定义PromptTemplate时可以直接使用它(且不需要memory、index等组件时)。它的实现类ConversationChain、QAGenerationChain、QAEvalChain则是预定义了PromptTemplate或是LLM的,是针对特定场景提供的内置Chain。
SequentialChain:SequentialChain用于连接多个Chain。它有一个更简单的实现版本SimpleSequentialChain,只适用于input/output都是单个参数的场景。SequentialChain则更通用,它允许有多个input/ouput,那么此时对input/ output变量的命名就非常重要了。在定义SequentialChain时,我们需要为其中的LLMChain定义output_key,前一个Chain的output_key必须和后一个Chain的input_variable一致。同时,SequentialChain的input_variables是第一个Chain的input_variables,它的output_variables是多个chains的output_keys汇总。SequentialChain类的修改点为
-
新增chains成员,类型为List[Chain]
-
新增return_all属性,控制输出信息
-
新增方法validate_chains,基于一个dict的values,验证顺序的chains前后的输入输出是相匹配的,这里也会考虑到memory。(工程中没被调用过,具体用法不是很明确,values应该穿什么)
-
修改方法_call,循环调用chains(注意,调用的是__call__方法,不带parser的),将前一个chain的输出作为下一个chain的输入
RetrievalQA:RetrievalQA是一个整合了index的问答chain,它的父类是BaseRetrievalQA。BaseRetrievalQA中封装了一个BaseCombineDocumentsChain,它可以接受一组Document,将其combine成为一个str,其中combine的方式可以是stuff、refine、mapreduce、maprerank等。而RetrievalQA则是通过一个BaseRetriever来生成BaseCombineDocumentsChain所需的Document。RetrievalQA类的修改点为:
-
新增成员retriever: BaseRetriever
-
实现其父类BaseRetrievalQA中定义的_get_docs和_aget_docs,基于retriever和question获取一组Document
3.5.3 常用工具Chain
-
Transformation Chain
-
TransformChain是不和LLM交互的,它接收输入和输出变量名和一个transform_func。可以在多个chains中对数据进行一些必要的转换。
-
-
AnalyzeDocumentChain(分析文档):这个chain接收一个document,将其拆分,然后通过一个CombineDocumentsChain运行。基于CombineDocumentsChain的类型可以实现基于文档的总结和问答功能
-
总结功能实现方法:
-
读取document
-
通过load_summarize_chain创建一个类型为map_reduce的chain
-
创建AnalyzeDocumentChain,其参数为combine_docs_chain,设置为前一步创建的load_summarize_chain
-
运行AnalyzeDocumentChain,第一步读取的文档为运行时的输入。将会得到一个总结的输出。
-
问答功能实现方法:
-
读取document
-
-
通过load_qa_chain创建一个类型为map_reduce的chain
-
创建AnalyzeDocumentChain,其参数为combine_docs_chain,设置为前一步创建的load_qa_chain
-
运行AnalyzeDocumentChain,输入参数为input_documen(第一步读取的document)和一个字符的question,得到基于document的这个question的answer
-
ConversationalRetrievalChain(带着chat history基于文档的chat):建立一个有聊天历史的、基于documents的chat chain。它和RetrievalQAChain的唯一区别是它有历史。
-
使用步骤
-
-
读取document,注意这里可以读取多个docs,即生成一个list
-
split documents,为它们创建embeddings,并放在一个vectorstore中,这可以运行我们后续进行语义检索(可以默认使用Chroma)
-
初始化ConversationalRetrievalChain,使用from_llm方法,指定model和从vectorstore中获取的retriever
-
运行ConversationalRetrievalChain,传入question和chat_history两个方法。
-
其他辅助方法:
-
从ConversationalRetrievalChain中获取source docuemnts
-
在运行时设置一个search_distance阈值
-
可以创建一个map_reduce的qa_chain,作为ConversationalRetrievalChain的combine_docs_chian组合起来
-
可以创建一个map_reduce的load_qa_with_sources_chain,组合
-
ConversationalRetrievalChain的输出可以stream到stdout
-
可以指定一个get_chat_history方法,来作为ConversationalRetrievalChain中的chat_history的输入
-
GraphQAChain(基于图数据结构的问答)
-
使用步骤
-
使用GraphIndexCreator来创建一个index_creator
-
读取文件,将text作为index_creator的输入,它会基于LLM来自动构建一个graph(构建一个知识图谱?)
-
用graph构建一个GraphQAChain
-
运行用户输入
-
-
其他辅助方法:
-
-
保存graph为gml文件
-
从gml文件反序列化graph
-
从graph中获取所有的三元组
-
注意,这里应该是可以使用ConversationKGMemory的,基于知识图谱的memory
-
BaseCombineDocumentsChain带着source的问答:在一组documents上使用LangChain来运行带着source的问答,覆盖了4个不同的chain type:stuff、map_reduce、refine、map-rerank。这里除了问答功能外,还可以调整prompt去做一些总结等功能,具体可以参考LangChain官网。
-
使用步骤
-
首先,准备数据,可以在vectorstore上搜索,也可以通过其他方式获得
-
创建不同类型的chain执行问答
-
-
stuff:stuff是最简单的方法,将所有相关的数据塞到promt中作为上下文传递个语言模型。LangChain中的实现类是StuffDocumentsChain
-
通过load_qa_with_sources_chain创建一个类型为stuff的chain,然后执行查询,input_documents参数为第一步的搜索结果,question为用户输入。
-
load_qa_with_sources_chain有默认的prompts,但也可以自定义prompts
-
map_reduce:在每个数据块(或是汇总任务,这可能是数据块的摘要;对于问答任务,它可能是一个完全基于该数据块的回答)上运行初始prompt。然后运行另一个prompt来结合所有的初始output。对应实现类为MapReduceDocumentsChain。
-
步骤是类似的,只是load_qa_with_sources_chain时指定chain_type = "map_reduce"。可以开启配置让LangChain返回map-reduce过程中的中间结果。
-
此时如果要自定义prompts,就需要定义2个,因为是map-reduce:question_prompt_template、combine_prompt_template
-
可以在LLM中设置batch_size
-
refine:在第一个数据块上运行初始prompt,生成一些输出。对于后面的文档,输出会和接下来的文档一起传递,要求LLM基于新的文档细化输出。
-
类似
-
自定义prompts有refine_prompt和question_prompt
-
map-rerank:在每个数据块上运行一个初始的prompt,评分、排序、选择评分最高的结果返回
-
类似,初始化是要制定一个metadata_keys ['source']
-
自定义prompts注意有output_parser,因为要提取score、和一个prompt_template
-
直接基于LLMChain的Vector BD Text生成:在vector index上生成text。其实也可以定义一个模板化的Chain,将从index检索数据到拼接PromptTemplate过程封装起来。
-
准备数据:访问文档,split
-
基于split结构构造vectordb
-
构造LLMChain,注意有一个context参数,这部分是预留给从vector store检索的内容
-
运行LLMChain,context是从vector store检索来的结果
-
-
APIChain:使用LLM和API交互来检索相关信息 APIChain,api管理使用open_meteo_docs/ tmdb_docs/ podcast_docs
-
chain_new = APIChain.from_llm_and_api_docs(llm, open_meteo_docs.OPEN_METEO_DOCS, verbose=True)
-
chain_new.run('What is the weather like right now in Munich, Germany in degrees Farenheit?')
-
LLM会分析自然语言,根据API参数塞值(里面好像还自己去检索了一下?检索了城市的经纬度),得到结果后用自然语言组织
-
ConstitutionalChain:合规Chain
-
有时llm会产生有害的、有毒的或其他不受欢迎的输出。该链允许您对现有链的输出应用一组构成原则,以防止意外行为。
-
构建ConstitutionalPrinciple,设置critique_request(限制)、revision_request(重写要求)
-
基于ConstitutionalPrinciple构建ConstitutionalChain,封装其他的chain,执行。
-
可以在ConstitutionalPrinciple中添加多个principles,可以在principle中要求回答的风格
-
BashChain:利用LLM和bash进程来执行简单的文件系统命令
-
初始化LLMBashChain执行即可,也可以自定义prompt template,但是注意要写好一点。。
-
LLMCheckerChain:多次check以提高回答准确性
-
LLMCheckerChain,把第一次生成的结果再次提交给LLM判断对错,不断细化回答,以提高回答准确率
-
LLMMathChain:使用LLM和Python REPLs来做复杂的word math problems。prompt很复杂
-
LLMRequestsChain:使用request库来从一个URL获取HTML result然后让LLM解析结果,然后基于结果回答问题
-
LLMSummarizationCheckerChain:和LLMCheckerChain有点类似又有点区别,同样没看太懂
-
Moderation:审核chain,用于检测一些可能是仇恨、暴力的text。可以作用于用户的输入,和模型的输出。
-
OpenAIModerationChain ,直接run
-
可以使用SequentialChain来把一个OpenAIModerationChain append到其他chain后面
-
OpenAIChain:调用自然语言中的一个endpoint,获得一个自然语言的response
-
加载spec,OpenAPISpec。这是啥?
-
选择操作
-
构造chain
-
PALChain:实现编程辅助语言模型
-
math:PALChain.from_math_prompt
-
colored objects:PALChain.from_colored_object_prompt,一个专门的编程领域
-
SQLDatabaseChain:使用LLM基于db回答问题
-
db_chain = SQLDatabaseChain(llm=llm, database=db, verbose=True)
-
连接上db后,猜测是先基于问题匹配table,然后基于table生成sql
-
可以设置chain中的top_k参数,会转化为limit
-
add example rows from each table:看起来是,指定表,指定采样行数,让LLM能够理解每一列存的是什么信息。就是通过对表的采样给出example
-
自定义table info:手动提供更准确的example信息
-
-
SQLDatabaseSequentialChain:查询db的顺序chain。
-
首先基于query,决定使用那些tables
-
基于tables,调用常规的SQL database chain
3.6 Agents
有些应用不仅仅要求使用预定义好的访问LLM或是其他tool的chains,而且可能需要依赖于用户输入的维值chains。在这些类型的chain,有一个agent概念,它可以访问一组tools。根据用户的输入,agent可以决定调用这些tools中的哪一个(如果有的话)。Agent使用LLM来决定采取什么样的actions以及用什么顺序。action可以是使用tool并观察其output,或是返回给user。
为了理解Agent,需要先了解下面的概念:
-
Tool:执行特定职责的function,可以是:Google Search、Database lookup、Python REPL,以及其他chains或是Agent。tool接口只有一个方法,接收string input,返回string output。
-
LLM:支持agent的语言模型
-
Agent:使用的agent。可以是一个指向支持的agent class的string。agent可以自定义
Agent的UML图如下所示,基于这个UML我们可以得知:
-
Agent并不是和LLM强绑定的,我们也可以用一个Agent去实现一个独立的操作。但由LLM驱动的Agent(Agent类及其子类)使用得更广泛
-
Agent类只做单步选择,多轮循环由AgentExecutor完成
-
AgentExecutor是一个Chain,意味着Agent和Chain可以混用,我们可以在Agent中将Chain当做一个tool调用,也可以在一组Chain中间执行一次Agent的循环过程
本章分为以下几个小节:
-
Tools:LangChain支持的不同tools的概览
-
Agents:不同agent类型的概览
-
Toolkits:toolkits的概览,以及LangChain支持的不同toolkit的示例
-
Agent Executor:Agent Executor的概览和用例。注意,AgentExecutor是一个Chain!
3.6.1 Tools
tools是agent可以用来和外部世界交互的方法。tools可以是通用工具(如search),其他chains,甚至是其他agents。tool的定义只需要是输入为str,输出也为str即可。tool可以使用LLM,也可以不使用LLM。tool的相关信息如下:
-
Tool Name:LLM对tool的称呼
-
Tool Description:传递给LLM的tool的描述
-
Notes:不会传递给LLM的tool的notes
-
Requires LLM:tool是否需要一个LLM来初始化
-
(Optional)Extra Parameters:tool初始化需要的其他参数
当前支持的内置tools:
-
python_repl:执行python命令的shell,input必须有一个有效的python命令,如果需要output那它必须被print,有状态
-
serpapi:搜索引擎,会调用Serp api然后解析result
-
wolfram-alpha:wolfram alpha搜索引擎。当你需要回答有关数学、科学、技术、文化、社会和日常生活的问题时非常有用。输入应该是一个搜索查询。
-
requst:对internet的一个接口
-
pal-math:一个接近复杂word math问题非常优秀的语言模型,需要LLM,输入应该是一个完整的数学难题
-
pal-colored-objects:一个推测物体的位置和颜色属性非常出色的语言模型,输入应该是一个完整的应推理问题。需要LLM
-
llm-math:回答math问题时有用,需要LLM
-
open-meteo-api:需要OpenMeteo API的天气数据时非常有用,输入应该是一个API可以回答的自然语言问题,需要LLM
-
news-api:当需要一些当前news stories的top headlines信息时有用,输入同样应该是API可以回答的自然语言问题,需要LLM,需要extra parameters:news_api_key
-
tmdb-api:访问Movie database信息,输入同样需要适配API,需要LLM,需要extra parameters:tmdb_bearer_token
-
google-search:用来search google,不需要LLM,需要extra parameters:google_api_key,google_cse_id
-
searx-search:SearxNG meta search engine,需要extra parameters:searx_host
-
google-serper:low cost的google search api,需要extra parameters:serper_api_key
-
wikipedia:wikipedia的搜索引擎,在搜索人、地、公司、历史事件或是其他子项时很有用,需要extra parameters:top_k_results
-
podcast-api:使用收听笔记播客API搜索所有播客或剧集。输入应该是这个API可以回答的自然语言问题,需要LLM,需要extra parameters:listen_api_key
-
openweathermap-api:OpenWeatherMap API的封装器,搜索指定location的weather时很有用,需要extra parameters:openweathermap_api_key
自定义tool有以下两种路径:
-
全新的Tools:有两种实现方法
-
使用Tool dataclass:初始化Tool类,传入它的必要参数,func只要是一个输入输出都是str的func就行
-
继承BaseTool class:继承BaseTool,给name、description属性赋值,实现_run和_arun两个方法,其中_arun方法可以抛出异常
-
使用tool decorator装饰器:@tool 装饰器可以快速地基于一个简单的function创建一个Tool,装饰器默认使用function的name作为tool name,也可以传入一个string作为第一个参数来覆盖。另外,装饰器会使用function的docstring来作为tool的description。同样地,也可以在装饰器中指定return_direct
如果我们想要定义tool之间的优先级,可以在tool的description中添加一些语句:use this more than xxx if the question is about yyy。注意,定义优先级时的description需要从整体上来评估。
如果我们希望有一个tool的输出结果永远是直接返回的,那么可以定义它的 return_direct = True。
在agent中使用multi-input tools的难点在于,agent会使用语言模型输出的string来决定下一步。因此如果这一步需要多个input,就需要对string进行解析。因此,当前支持的方法是写一个小的wrapper function来将一个string解析为多个input。即具体实现:Tool的func还是接受string,输出string,只不过在func内部接收到string后会先去解析出自己所需要的多个input变量
由此可见:如果将一个chain定义为tool,如果chain有多个input variables,那么就需要一个封装函数,能从一个结构中解析出来多个input variables。agent同理。 最好是,每个chain有对应的输入输出类型,放在tool描述里,能让agent自动识别到输入输出是否匹配,然后类型序列化后再给下一个chain解析。
除了预定义好的Tool,LangChain还提供了一系列的通用工具,我们可以将这些工具低成本地封装为Tool进行使用(有一部分已经封装好了):
-
Apify:一个用于网络抓取和数据提取的云平台,提供了由一千多个现成的应用程序-被称为actor,组成的生态系统,用于各种网络抓取、抓取、和数据提取。eg,可以用它来提取google搜索结果、Instagram和Facebook的个人资料、亚马逊或是Shopify的产品,谷歌地图评论等。
-
Bash:执行Bash命令
-
Bing Search
-
ChatGPT Plugins:AIPluginTool
-
当前只适用于不带auth的plugins,发展中,可能还有更好的方案
-
Google Search
-
Google Serper API
-
Human as a tool:human是AGI,可以在AI agent困惑时作为一个tool提高帮助
-
首先要在tools中load一个叫human的tool
-
这个tool的输入是用户的命令行,可以自定义prompt_func和input_func
-
当agent发现找不到合适的tool调用时它会在命令行中向用户发起提问,如果用户的回答不够详细还会再次发问
-
然后基于用户的回答判断是否还要再次调用tool,最终生成回答
-
IFTTT WebHooks:不是很理解这个工具
-
OpenWeatherMap API:天气
-
Python REPL:对于复杂计算,与其让LLM直接回答,不如先让LLM生成代码,然后运行代码获得回答。Python REPL是用来执行python命令并输出回答的
-
Request:python requests model,可以从一个URL中获得数据
-
Search Tools:google-serper、serpapi、google-search、searx-search、
-
SearxNG Search API
-
SerpAPI
-
Wikipedia API
-
Wolfram Alpha:一个解决数学、科学&技术、社会&文化、日常生活的LLM
-
Zapier Natural Language Actions API
3.6.2 Agents
Agent可以使用LLM来决定触发的ations及其顺序。action可以是使用一个tool并观察其输出,或是返回response给用户。下面是LangChain中支持的agent 类型(仅指使用了LLM的Agent):
-
zero-shot-react-description:(使用最广)使用ReAct框架来,根据tool的描述来决定使用那个tool。可以提供任意数量的tools,该agent要求为每个tool提供description。
-
react-docstore:使用ReAct框架来和一个docstore交互,必须提供2个tools:一个Search tool和一个Lookup tool(名称必须一直)。Search tool应该搜索出一个文档,Lookup tool应该在最近找到的文档中查找一个术语。这个Agent相当于最初的ReAct论文,特别是Wikipedia的例子。
-
self-ask-with-search:使用一个单独的,名称为Intermediate Answer的tool。这个tool应该具备查找问题的事实答案的能力。这个agent相当于原始的self ask search 论文,其中提供了一个Google search API作为tool
-
conversational-react-description:这个agent被设计用在对话设置中。prompt的设计目的是使agent具有帮助性和会话性。它使用ReAct框架来决定使用哪个tool,并且使用memory来记住前面的对话交互。
除此之外,我们也可以自定义Agent。Agent由3个部分组成(其实只有2个,官网文档里估计写错了):
-
Tools:agent可以使用的tools
-
Agent自身
3.6.2.1 自定义BaseSingleActionAgent
现在我们来尝试自定义一个极简Agent,即只有一个tool、每次只执行单个Action的Agent的定义过程
-
创建tools(这里只有1个tool)
-
创建FakeAgent,实现BaseSingleActionAgent,(注意,它不需要LLM!说明它的执行过程不需要语言模型的能力!)实现方法:
-
input_keys
-
plan : 需要返回一个AgentAction或是AgentFinish,其中AgentAction包含了指定的tool
-
aplan:plan的异步方法
-
使用AgentExecutor.from_agent_and_tools,利用agent、tool来创建一个AgentExecutor
-
执行AgentExecutor.run
-
3.6.2.2 自定义LLMAgent(更简单的方法:直接使用ZeroShotAgent,自定义LLMChain)
LLMAgent由3个部分组成(其实有4个,官网文档里估计写错了):
-
PromptTemplate:用来指示语言模型的行为
-
LLM:驱动agent的语言模型
-
stop序列:指示LLM在找到此string后立即停止生成
-
OutputParser:决定如何将LLMOutput解析为一个AgentAction或是AgentFinish
LLMAgent在一个AgentExecutor中使用,这个AgentExecutor很大程度上可以被认为是一个循环,它会执行以下操作:
-
将用户的input和任意的前置steps传递给Agent(这里是LLMAgent)
-
如果Agent返回AgentFinish,直接返回给用户
-
如果Agent返回AgentAction,使用它来调用一个tool然后获得一个Observation
-
重复,将AgentAction和Observation传递给Agent,直到有一个AgentFinish输出
-
AgentAction是一个有action和action_input组成的response。action指的是使用哪个tool,action_input指的是这个tool的input。
-
AgentFinish是一个包含了需要返回给用户的final message,它会用来中止agent的run
LLMAgent的自定义步骤:
-
构建tools,可以有多个,agent的必须知道用了哪些tools,因为这可能会被写进prompt里
-
构建PromptTemplate,它指导agent应该做什么。一般来说,PromptTemplate应该包含:
-
tools:agent可以访问哪些tools,以及应该如何、合适调用它们
-
intermediate_steps:这些是(AgentAction, Observation)对。它们通常不会直接传递给model,但是prompt template会一特定的方式格式化它们。
-
input:用户的输入
-
一个简单的例子如下:
-
LLMAgent的PromptTemplate
# Set up the base template template = """Answer the following questions as best you can, but speaking as a pirate might speak. You have access to the following tools: {tools} Use the following format: Question: the input question you must answer Thought: you should always think about what to do Action: the action to take, should be one of [{tool_names}] Action Input: the input to the action Observation: the result of the action ... (this Thought/Action/Action Input/Observation can repeat N times) Thought: I now know the final answer Final Answer: the final answer to the original input question Begin! Remember to speak as a pirate when giving your final answer. Use lots of "Arg"s Question: {input} {agent_scratchpad}"""
-
-
构建OutputParser:OutputParser负责将LLM的output解析为AgentAction和AgentFinish。这通常很大程度上取决于prompt。即基于prompt要求的LLM输出格式,解析为格式化的AgentAction/AgentFinish,注意LLM输出时已经确定了是action还是final了。
-
启动LLM,选择你所需要的LLM
-
定义stop sequence:这很重要,因为它告诉了LLM什么时候停止生成。它也很大程度上取决于prompt和你所使用的model。通常,你希望这是你在prompt用来标识Observation开始的任意token(否则,LLM可能会为你产生Observation开始的幻觉)。也就是,通常是 stop=["\nObservation:"],
-
构建Agent类(LLMAgent)
-
先用LLM和prompt构建LLMChain
-
再用llm_chain、output_parser、top、allowed_tools来构建一个LLMSingleActionAgent
-
通过AgentExecutor运行Agent
3.6.2.3 使用ZeroShotAgent自定义MRKLAgent
定义自定义Agent的最简单方法是,使用已有的Agent类,但使用一个自定义的LLMChain。强烈建议使用ZeroShotAgent,这是目前为止最通用的方法。创建自定义LLMChain的大部分工作都是定义prompt。因为我们使用了一个已有的Agent类来解析output,所以prompt格式化的方式是非常重要的。此外,我们需要一个agent_scratchpad 输入变量来记录以前的action和observations。
用ZeroShotAgent构建自定义MRKL Agent的步骤:
-
创建tools,这里创建一个google search tool
-
利用ZeroShotAgent创建prompt,啊???啥意思,是说ZeroShotAgent有预设好的prompt,我们只需要给prompt添加一些前后缀 =》 是的,前后缀的作用可以是强调一下回答风格:speak like xxx,ZeroShotAgent预设好的prompt就是那些format(Thought、Action等等)
3.6.2.4 自定义Agent with Tool Retrieval
这个agent建立在自定义LLMAgent基础之上。该agent提出的新颖方法是,使用检索来选择合适的tools set来回答agent query,当tools太多时很有用。因为常规方法是将tools的description都放在prompt中,但是tools太多prompt就可能长度超限。因此可以在运行时动态地选择N个tools。
构建步骤
-
构建tools
-
检索tools:使用vectorstore来为每个tool的description创建embeddings,然后,接收到查询后,为查询创建embeddings,再根据相似度搜索来选择tools。这一步提供一个检索tools的工具
-
创建prompt template:非常标准的prompt template,因为并没有改变逻辑
-
创建OutputParser:将llm的output解析为AgentAction/ AgentFInish,不变
-
创建LLM、stop sequence、agent,这里创建一个LLMSingleAgent即可
-
执行agent:使用AgentExecutor
3.6.3 Toolkits
ToolKit是LangChain内置的一些用在特定场景的agents。
-
CSV Agent:和csv交互
-
create_csv_agent,底层调用了Pandas DataFrame agent,进而调用了Python agent,运行了LLM生成的python code
-
可以指定一个csv文件,并提一些问题
-
JSON Agent:和json交互,可以迭代地探索json知道找到回答
-
create_json_agent
-
OpenAPI agents:我们可以构造使用任意api的agent,这里的api符合OpenAPI/Swagger规范。
-
eg1. 层及规划agent:不太懂,略
-
Natural Language APIs(NLAToolkits):允许LangChain agents有效地计划和组合endpoints之间的调用。
-
不太懂
-
Pandas Dataframe Agent:和pandas df交互,针对问答优化
-
create_pandas_dataframe_agent
-
Python Agent:写python代码并执行,以回答问题
-
create_python_agent
-
tool=PythonREPLTool(),
-
SQL Database Agent:和sql db交互,这个agent会构建SQLDatabaseChain,针对数据库相关问题调优。
-
Vectorstore Agent:从一个或多个vectorstores中检索信息,可以with/without source
-
create_vectorstore_agent
3.6.4 AgentExecutor
Agent Executor接收一个agent和tools,使用agent来决定调用哪个tool及调用顺序。注意,一些agent初始化时需要接收tool_names,然后AgentExecutor也接受同样的tools作为参数。
注意,AgentExecutor是一个Chain,它对Agent的迭代调用是在Chain的_call方法中实现的。
下面是一些使用AgentExecutor实现的功能
-
结合agent和vectorstore:
-
场景:数据已经存储在vectorstore中了,希望能用agent的方式和它交互
-
推荐方式:创建一个RetrievalQA(chain),然后将它作为agent中的一个tool。你可以使用多个不同的vectordbs,然后用agent在它们之间进行路由。有2种实现方案:一是将vectorstore作为普通tool使用,agent可能会迭代多次访问多个vectorstore;二是设置return_direct=True,来仅仅将agent作为router使用(访问一个vectorstore就立刻返回)
-
使用方式:
-
创建vectorstore
-
使用vectorstore的retriever来创建RetrievalQA chain
-
用RetrievalQA chain注册tools,创建AgentType.ZERO_SHOT_REACT_DESCRIPTION 的AgentExecutor(注意,如果想要把agent当做router使用,这些tools的return_direct=True)
-
AgentExecutor.run:注意这里上一步返回
-
使用Agent的异步API
-
LangChain通过利用asyncio库来支持了Agent的异步请求,异步方法当前仅在部分tools中支持:SerpAPIWrapper、LLMMathMath。
-
对于实现了coroutine的tool,AgentExecutor会直接await。否则它会通过asyncio.get_event_loop().run_in_executor来调用Tool的func
-
使用时,调用AgentExecutor的arun方法即可
-
创建ChatGPT Clone
-
思路:chain通过结合一个指定的prompt和memory的概念来复制ChatGPT。就是创建一个LLMChain,用一个确定的prompt
-
-
访问intermediate steps
-
在initialize_agent时设置return_intermediate_steps=True
-
限制最大迭代次数
-
在输入prompt里说明次数
-
在initialize_agent时限定max_iterations
-
设置agent的timeout
-
initialize_agent时限定max_execution_time = 1,单位为s
-
为Agent和它的tools添加SharedMemory
-
场景:agent和tools都需要访问conversation memory
-
步骤:
-
创建ConversationBufferMemory memory
-
基于memory创建ReadOnlySharedMemory readonlymemory,给chain用 =》 为什么不让chain修改memory?
-
把memory给agent,agent调用chain生成结果,然后agent负责写memory,chain只能读memory
五、评估框架
评估LangChain的chains和agents非常难,有2个主要的原因
-
缺少数据:通常我们没有许多数据能在启动项目之前来评估你的chains/agents。这是因为LLM(chain和agent的核心)是很好的few-shot和zero-shot的学习者,这意味着我们总是能够在没有大量示例数据的情况下开始特定任务,不像传统的机器学习需要在使用模型之前收集一堆数据。
-
缺少指标:大多数的chains/agents没有很好的指标来评估性能。例如,一个按照格式生成文本的功能。评估生成的文本比评估分类预测或是数值预测要复杂的多。
解决方案(初步的):
-
缺少数据的解决方案:我们已经在Hugging Face上建立了LangChainDatasets社区空间。我们希望这是一个用于评估公共链和代理的开源数据集的集合。我们还致力于让人们尽可能容易地创建自己的数据集。作为这方面的第一步,我们添加了一个QAGenerationChain,它给出了一个文档,提供了可以用于评估该文档上的问答任务的问答对。
-
QAGenerationChain基于一个文档自动生成问答,用来评估。场景是高中老师考阅读理解。。
-
缺少指标的解决方案:
-
方案一:直接不使用指标,人工评估,可以用tracing组件,可以UI可视化
-
方案二:使用LLM自身来评估输出,LangChain也提供了一些chains、prompts来解决这个问题。QAEvalChain
评估的基本流程为:
-
加载数据
-
构造chain
-
执行prediction
-
评估性能(肉眼评估、让LLM评估(QAEvalChain)、用第三方服务评估(如果有的话))
-
文本的其他评估方式:critical库