LangChain源码逐行解密之系统
search.py源码逐行剖析
本节将通过源代码与大家分享,LangChain框架作为核心的企业级大模型开发的最后一个环节,即代理(Agent)环节。之前我们已经多次提到代理,并从源代码和案例的角度对多个代理进行了剖析,如图20-1所示。Gavin大咖微信:NLP_Matrix_Space
图20- 1 AutoGPT的运行架构图
本节我们将从代理的实例化开始,深入剖析其代码,并全面介绍代理运行的全生命周期。这个环节非常重要,可以说,如果你没有掌握本节的内容,不太认为你掌握了LangChain的精髓。为什么这么说呢?我们之前多次提到过,整个企业级开发有三个关键元素,第一个元素是语言模型;第二个元素是工具,我们可以将所有关于API的调用、数据的封装和第三方库的库,都视为工具的调用,当然,这是一个与环境进行交互的过程。第三个元素,从实战开发的角度讲是最重要的,对于工程师来说,也是最重要的部分,那就是对代理本身的理解和内部机制的把握。因为对代理的理解和内部机制的把握,决定了你能够以多么高的质量和什么样的速度,去开发大模型应用程序。从根本上讲,是我们的代理控制了整个流程。当然,我们之前多次提到过,代理是由模型驱动的。之所以说大模型开发是新一代的开发范式,是因为它解决了一个问题,这个问题是什么呢?给定一个任务,具体如何执行,这完全由模型自动决定。这是一个根本性的开发变革。以前的开发方式是,你需要自己弄清楚规则,然后通过编写代码将这些规则写入系统中。过去几十年的开发一直在努力解决一个问题,那就是你一旦依赖规则系统,它往往缺乏灵活性。尽管我们有许多架构用于解耦或分离,或者强调内聚性和弱耦合等,但其本质并没有改变。Gavin大咖微信:NLP_Matrix_Space
而语言模型给开发者带来了革命性的变化,这种变化不仅仅是它可以生成信息,让你参考。更重要的是,它能够根据你的指令或目标,在当前上下文中生成解决这个目标的具体步骤。换句话说,它解决了如何完成任务的问题,而无需你去探索大量的业务规则或业务系统。当然,并不是说你不需要探索,因为你的领域知识可以更好地增强大模型的推理能力,这个模型融合了你的领域知识。
在理想情况下,我们的模型可以不断进行自我反思。自我反思的基本前提是进行评估,无论是通过评估,还是自我反思,都是由我们的语言模型操作的,包括Actor(行动者),它具体执行一些规划等相关操作,也是基于语言模型的。如图20-2所示。
图20- 2 代理运行示意图
对于所有的工具或API的调用,它们都是在环境级别上进行的。图20-2展示了一个比较理想的企业级开发大模型应用程序的内部核心原理图。如果你能够理解并实现这张图中每一步的流程,那么你无疑是一位高级的大模型开发工程师,或者是一位领导级的工程师,这是从工程师或开发者的角度来与大家分享这个内容。无论是从图20-1,还是从图20-2,我们都可以看到,代理实际上起着主导作用,它扮演着一个决策者的角色。在之前的分享中,我们已经向大家展示了许多案例和源代码。这一节中,很有必要全面介绍LangChain框架中的代理,在框架层面上对代理进行透彻的讨论,现在是时候全面深入地探讨一下LangChain框架中的Agent了。Gavin大咖微信:NLP_Matrix_Space
如图20-3所示,是LangChain框架中的agents代码目录,包括mrkl、react等内容。
图20- 3 LangChain框架中的agents代码目录
我们之前已经对此进行了详细分析,现在,我们要从整个生命周期的角度,讨论它的核心要点。因此,我们提供了一个非常简单的基本程序示例Search.py,这个程序本身的代码非常简单,之所以使用如此简单的代码,是为了聚焦于LangChain框架内部整个生命周期的运行。
Search.py代码实现了一个简单的Web查询应用程序,用户可以输入查询并获取相应的结果,使用了OpenAI模型和LangChain框架的功能,以及Google Serper API工具来执行搜索。Gavin大咖微信:NLP_Matrix_Space
Search.py的代码实现:
- import streamlit as st
- from langchain.llms.openai import OpenAI
- from langchain.agents import load_tools, initialize_agent
- # 从会话状态设置API密钥
- openai_api_key = st.session_state.openai_api_key
- serper_api_key = st.session_state.serper_api_key
- # Streamlight应用程序
- st.subheader('Web Search')
- search_query = st.text_input("Enter Search Query")
- #如果单击“搜索”按钮
- if st.button("Search"):
- # 验证输入
- if not openai_api_key or not serper_api_key:
- st.error("Please provide the missing API keys in Settings.")
- elif not search_query.strip():
- st.error("Please provide the search query.")
- else:
- try:
- with st.spinner('Please wait...'):
- # 初始化OpenAI模块,加载Google Serper API工具,并使用代理运行搜索查询
- llm = OpenAI(temperature=0, openai_api_key=openai_api_key, verbose=True)
- tools = load_tools(["google-serper"], llm, serper_api_key=serper_api_key)
- agent = initialize_agent(tools, llm, agent="zero-shot-react-description", verbose=True)
- result = agent.run(search_query)
- st.success(result)
- except Exception as e:
- st.exception(f"An error occurred: {e}")
上述代码第24行,我们使用OpenAI作为大型语言模型,如果没有指定版本,默认将选择LangChain框架内置的OpenAI的版本,当然你自己也可以指定一些版本,语言模型是真正的驱动。在实例化时,OpenAI类是一个封装器(wrapper),用于封装OpenAI。当进行实例化时,它会建立与底层OpenAI连接的语言模型。当然,你需要提供API密钥才能获得访问权限。
openai.py的代码实现:
- class OpenAI(BaseOpenAI):
- """ OpenAI大型语言模型的包装器。
- 要使用,应该安装openai python包,并使用API键设置环境变量openai_API_KEY
- 可以传入任何有效传递给openai.create调用的参数,即使没有显式保存在此类上。
- 示例:
- .. code-block:: python
- from langchain.llms import OpenAI
- openai = OpenAI(model_name="text-davinci-003")
- """
- @property
- def _invocation_params(self) -> Dict[str, Any]:
- return {**{"model": self.model_name}, **super()._invocation_params}
第二个关键的方法是load_tools,可以指定具体的名称,我们使用Google Serper作为工具,并将llm作为参数传递给它。同时,我们还传递了serper_api_key。现在,我们可以看一下load_tools函数。
load_tools.py的代码实现:
- def load_tools(
- tool_names: List[str],
- llm: Optional[BaseLanguageModel] = None,
- callbacks: Callbacks = None,
- **kwargs: Any,
- ) -> List[BaseTool]:
- """根据工具的名称加载工具。
我们创建了一个工具对象的列表(List[BaseTool])。在正常的开发过程中,你可能会使用LangChain内置的一些工具。当然,根据你具体的业务场景和私有数据,大概率你会开发自己的工具。什么是工具呢?简单来说,工具可以被理解为封装了业务逻辑或数据接口等功能的实体。在企业中,如果你拥有IT基础设施,那么你肯定会有这样的工具存在。从语言模型的角度来看,它认为你已有的所有功能和服务,都可以被看作是工具。我们使用LangChain框架的一个重要优势是,你可以直接基于它的BaseTool接口进行开发。
tools的base.py的代码实现:Gavin大咖微信:NLP_Matrix_Space
- class BaseTool(ABC, BaseModel, metaclass=ToolMetaclass):
- """接口 LangChain工具必须实现."""
- name: str
- """工具的唯一名称,清晰地传达其目的。"""
- description: str
- """用于告诉模型如何/何时/为什么使用该工具。
- 可以在描述中提供几个示例以进行快速学习。
- """
- args_schema: Optional[Type[BaseModel]] = None
- """用于验证和解析工具输入参数的Pydantic模型类。"""
- return_direct: bool = False
- """是否直接返回工具的输出。将此设置为True意味着
- 在调用工具后,AgentExecutor将停止循环。
- """
- verbose: bool = False
- """是否记录工具的进展。"""
- callbacks: Callbacks = Field(default=None, exclude=True)
- """在工具执行期间要调用的回调函数。"""
- callback_manager: Optional[BaseCallbackManager] = Field(default=None, exclude=True)
- """已弃用。请改用回调函数。"""
- ...
- @abstractmethod
- def _run(
- self,
- *args: Any,
- **kwargs: Any,
- ) -> Any:
- """使用该工具
- 增加 run_manager: Optional[CallbackManagerForToolRun] = None
- 到实现跟踪的子实现
- """
- @abstractmethod
- async def _arun(
- self,
- *args: Any,
- **kwargs: Any,
- ) -> Any:
- """异步使用该工具
- 增加run_manager: Optional[AsyncCallbackManagerForToolRun] = None
- 到实现跟踪的子实现
- """
以上代码定义了一个BaseTool类,是LangChain工具必须实现的接口。
BaseTool类具有以下属性:
- name:工具的名称,清楚地传达了工具的目的,而且这个名称是唯一的,这是框架要求的。当你加载它时,它相当于一个ID。
- description:这是一个绝对核心的关键,描述是为了谁设计的?我们之前多次提到是为了语言模型,描述的重要性在这个注释中已经说得很明白,描述用于告诉模型如何、何时以及为何使用工具,但是,这也依赖于语言模型自身的推理能力。它还包含一些具体的步骤,例如,你可能要聚合网络上的信息及结合本地数据库的信息。可以使用链(Chain)级别的工具,也可以使用两个工具,一个是用于搜索的工具,一个是与数据库交互的工具,语言模型怎么知道何时进行搜索,何时调用数据库呢?显然是根据你的描述。实际上,这里还有一件非常重要的事情,你可以将少量示例作为描述的一部分来提供,这一点非常重要,因为你可以提供两个或三个范例,清楚地说明何时应该调用它。这也是平时优化的一个重点。
- args_schema:可选的Pydantic模型类,用于验证和解析工具的输入参数。
- return_direct:布尔值,指定是否直接返回工具的输出。将其设置为True意味着在调用工具后,AgentExecutor将停止循环。如图20-4所示,AgentExecutor是一个非常重要的对象,它描述了一个核心关系,那就是谁在运行这个工具,我们是AgentExecutor运行或调用这个工具。从理解框架或开发应用的角度来看,这是非常重要的,因为AgentExecutor相当于一个容器。
图20- 4 BabyAGI的运行架构图
其中,执行代理(Execution Agent)、上下文代理(Context Agent)、任务创建代理(Task Creation Agent)等,实际上都可以从Agent Executor的角度来理解,因为除了模型本身进行推理外,Agent Executor是真正的主控,或者我们可以称之为管理器(Manager),因为整个代理系统中,你可以将Agent视为主控,而Agent Executor在其中扮演着管理器的角色,因为它可能需要执行许多任务,而且,Agent Executor通常是一个循环,直到完成目标为止。那么,你如何知道何时完成目标呢?原因很简单,肯定是通过代理来告诉你,代理是如何知道的呢?它是通过语言模型告知代理。
- verbose:布尔值,指定是否记录工具的进度。如果你想查看内部运行过程,可以将其设置为True。
- callbacks:回调管理器或回调处理程序的列表,用于在工具执行过程中调用。回调函数非常重要,特别是当你想要精确捕获内部的运行信息,尤其是在出现异常的时候。
- callback_manager:可选的回调管理器,已弃用。请使用callbacks代替。
callbacks、callback_manager这两个函数,可以让你的程序在出现问题的时候,让你看到更多内部的细节信息,让程序更健壮。
BaseTool类具有以下方法:
- _run方法:任何一个工具都有Run方法,它是一个同步方法,在子类中进行具体实现。该方法增加了一个可选参数run_manager,该参数用于实现跟踪工具运行过程的子类实现。
- _arun 方法是一个异步方法,该方法用于异步使用该工具,并在子类中进行具体实现。类似于_run 方法,_arun 方法也增加了一个可选参数run_manager,该参数的类型是AsyncCallbackManagerForToolRun,用于实现异步工具运行过程的跟踪。