最近看了一些关于 AI Agent 开发相关的文档和开源项目,包括 AI Agent 的开发流程和步骤以及代码实现,打算分几次总结一下。
这篇博客总结了使用 LangChain 开发 AI Agent 的相关知识和代码。
开发 AI Agent 的大致步骤如下:
1 | 1. 定义 Tools。可以使用现有的工具,或者定义你自己的工具。 |
这篇文章分为两部分,第一部分先介绍一些概念,第二部分介绍下具体的 Agent 代码。
Agent 相关概念
在进行开发前,先总结一下 LangChain 中 Agent 相关的一些概念。
AgentExecutor
AgentExecutor 是代理的运行时。这实际上是调用代理,执行它选择的操作,将操作输出传递回代理,然后重复。虽然这看起来很简单,但 AgentExecutor 会为您处理一些复杂的问题,包括:
- 处理代理选择不存在的工具的情况
- 处理工具错误的情况
- 处理代理生成无法解析为工具调用的输出的情况
- 所有级别(代理决策、工具调用)的日志记录和可观察性到标准输出和/或 LangSmith。
Tools
工具是 Agent 可以调用的功能。工具抽象由两个组件组成:
- 工具的输入架构。这告诉 LLM 调用该工具需要哪些参数。如果没有这个,它将不知道正确的输入是什么。这些参数应该被合理地命名和描述。
- 要运行的函数。这通常只是调用一个 Python 函数。
并且围绕工具有两个重要的设计考虑因素:
- 让 Agent 能够使用正确的工具
- 以对 Agent 最有帮助的方式描述工具
如果不考虑这两点,你将无法构建一个有效的 Agent 。如果您不让 Agent 访问一组正确的工具,它将永远无法实现您赋予它的目标。如果你没有很好地描述工具,Agent 将不知道如何正确使用它们。
LangChain 提供了一系列广泛的内置工具,而且还可以轻松定义您自己的工具(包括自定义描述)。有关内置工具的完整列表,请查看:
https://python.langchain.com/docs/integrations/tools/
Toolkits
对于许多常见任务,Agent 将需要一组相关工具。为此,LangChain 提供了工具包的概念——完成特定目标所需的大约 3-5 个工具组。例如,GitHub工具包有用于搜索GitHub问题的工具、用于读取文件的工具、用于评论的工具等。LangChain 提供了广泛的入门工具包。有关内置工具包的完整列表,请查看:
https://python.langchain.com/docs/integrations/toolkits/
AgentFinish
AgentFinish 是 agent 准备好返回给用户时的最终结果类。它包含一个 return_values 键值映射,其中包含最终的输出结果。通常,这包含一个输出键,其中包含一个 agent 响应的字符串。
AgentAction
AgentAction 是一个数据类,表示代理应采取的操作。它有一个 tool 属性(这是应该调用的工具的名称)和一个 tool_input 属性(该工具的输入)
Intermediate Steps
Intermediate Steps 代表先前的 agent 操作以及当前 agent 运行的相应输出。这些对于传递到未来的迭代非常重要,使 agent 知道它已经完成了哪些工作。
Agent Inputs
Agent Inputs 表示 agent 的输入数据,它是键值映射。只有一个必需的键:intermediate_steps,它对应于如上所述的中间步骤。一般来说,PromptTemplate 负责将这些对转换为最适合传递到 LLM 的格式。
Agent Outputs
Agent Outputs 表示要执行的下一个操作或要发送给用户的最终响应(AgentActions 或 AgentFinish)。
具体来说,Agent Outputs 可以是 Union[AgentAction, List[AgentAction], AgentFinish]。
Agent 开发步骤
Agent 的核心思想是使用 LLM 来选择要采取的一系列操作。在 chain 中,一系列操作被硬编码。而在 Agent 中,LLM 被用作推理引擎来确定要采取哪些操作以及按什么顺序执行,就像我们人类的大脑一样。
定义工具 Tools
使用 LangChain 中集成的工具
我们可以使用 LangChain 中集成的工具,例如:
Tavily:Tavily 的搜索 API 是专为人工智能代理 (LLM) 构建的搜索引擎,可快速提供实时、准确和真实的结果
Shell (bash):可以让 LLM 执行 Shell 脚本的工具
SearchApi:可以让 LLM 调用 SearchApi 进行搜索
还有其他很多工具可以查看这个文档:
https://python.langchain.com/docs/integrations/tools/
下面就看看如何定义上面三个工具。
定义 Tavily 工具
首先需要配置 Tavily 的环境变量 TAVILY_API_KEY,这个 key 的值可以到 https://tavily.com/ 上获得。
然后定义工具的代码如下:
1 | from langchain_community.tools.tavily_search import TavilySearchResults |
执行上面的代码后,输出结果为:
1 | [{'url': 'https://www.weatherapi.com/', |
定义 Shell 工具
使用 Shell 工具时,需要先安装 langchain-experimental 包:pip install langchain-experimental
定义工具的代码为:
1 | from langchain_community.tools import ShellTool |
上面代码的输出结果为:
1 | Executing command: |
定义 SearchApi 工具
首先需要配置 SearchApi 的环境变量 SEARCHAPI_API_KEY,这个 key 的值可以到 https://www.searchapi.io/ 上获得。
然后定义工具的代码如下:
1 | from langchain_community.utilities import SearchApiAPIWrapper |
执行上面的代码后,输出结果为:
1 | 'Joe Biden' |
自定义工具
自定义 Retriever 工具
我们可以自定义一个 Retriever 检索器作为工具,代码实现如下:
1 | from langchain_community.document_loaders import TextLoader |
1 | from langchain.tools.retriever import create_retriever_tool |
自定义函数工具
我们还可以自定义一个函数作为工具,具体方法如下。
自定义工具需要用到 langchain_core.tools 中的 tool 函数,tool 函数可以作为装饰器把一个函数变为工具:
1 | from langchain_core.tools import tool |
执行 add.invoke({“x”: 1, “y”: 2}) 就可以调用工具了。
还可以查看 add 的相关信息:
1 | print(add.name) |
打印出来的内容如下:
1 | add |
tool 函数还可以添加一些参数,用法可以参考这个文档:
有了自定义工具,我们可以通过 chain 来调用它。
首先定义一个 LLM,并绑定要执行的工具:
1 | from langchain_openai import ChatOpenAI |
然后定义一个 chain 来执行工具:
1 | # | 用来连接不同的处理步骤,从 llm_with_tools 中获取参数值并交给 add 去执行 |
上面使用 chain 调用工具的情况是我们已经知道了用户输入与其对应的工具名称,如果我们想要 LLM 自己决定用户的输入使用哪个工具,就要使用到 Agent 了。
创建 Agent
创建 Agent 前,我们需要先定义 prompt 模板,这里我们可以使用 langchainhub 中的模板:
1 | from langchain import hub |
prompt 的格式为:
1 | ================================ System Message ================================ |
或者我们自定义一个 prompt 模板,代码如下:
1 | from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder |
然后我们看看定义好的两个工具:
1 | from langchain_core.tools import tool |
我们使用 create_tool_calling_agent 创建一个工具调用的 agent:
1 | from langchain.agents import AgentExecutor, create_tool_calling_agent |
运行 Agent
创建好 agent 后,我们可以创建一个 agent executor 并通过它来执行 agent:
1 | # 创建 agent executor |
可以看看上面 invoke 的输出结果:
1 | > Entering new AgentExecutor chain... |
通过执行结果可以看出 LLM 通过调用 multiply 和 add 获取结果。
创建 AgentExecutor 对象时,还可以传入其它参数如:
1 | max_iterations:表示 agent 要执行的最大步骤数 |
还有其它参数可以查看文档:
我们换一个问题执行一下:
1 | agent_executor.invoke( |
上面 invoke 的执行结果是:
1 | > Entering new AgentExecutor chain... |
通过执行结果可以看出 LLM 通过调用 tavily_search_results_json 获取结果。
给 Agent 添加记忆功能
上面运行的 agent 是没有历史记录的,也就是没有上下文信息,例如看下面的例子:
1 | agent_executor.invoke({"input": "hi! my name is wyzane"}) |
上面是我告诉他我叫 wyzane,上面的输出是:
1 | > Entering new AgentExecutor chain... |
然后我再执行:
1 | agent_executor.invoke({"input": "who am i"}) |
上面的输出是:
1 | > Entering new AgentExecutor chain... |
从上面两个问题可以看出 agent 是没有记忆功能的。
有两种方式可以给 agent 添加记忆功能:
方式一:手动把对话内容传入到 chat_history 参数中
方式二:使用 RunnableWithMessageHistory 自动跟踪信息。RunnableWithMessageHistory 允许我们将消息历史记录添加到某些类型的链中。它包装另一个 Runnable 并管理它的聊天消息历史记录。
RunnableWithMessageHistory 的详细使用可以查看以下文档:
https://python.langchain.com/docs/expression_language/how_to/message_history/
使用方式一,我们可以通过把对话内容传入到 chat_history 参数中,来给 agent 添加记忆,使用方式如下:
1 | from langchain_core.messages import AIMessage, HumanMessage |
使用方式二,使用 RunnableWithMessageHistory,使用步骤如下:
1 | from langchain_community.chat_message_histories import ChatMessageHistory |
然后执行代码:
1 | agent_with_chat_history.invoke( |
输出结果是:
1 | > Entering new AgentExecutor chain... |
再执行代码:
1 | agent_with_chat_history.invoke( |
输出结果是:
1 | > Entering new AgentExecutor chain... |
可以看到是有上下文信息的。
给 Agent 添加流式输出
流式输出是 LLM 应用程序一个很重要的用户体验考虑因素,Agent 也不例外。使用 Agent 进行流式传输变得更加复杂,因为我们不仅仅想要流式传输最终的答案,而且还可能想要流式输出 Agent 所采取的中间步骤。下面介绍下如何在 Agent 中使用流式输出。
首先,我们创建 llm、tools、prompt:
1 | import random |
然后,我们可以使用 create_openai_tools_agent 创建一个代理,并创建对应的 agent_executor:
1 | from langchain.agents import AgentExecutor, create_openai_tools_agent |
最后使用 AgentExecutor 的 .stream 方法来流式传输代理的中间步骤:
1 | chunks = [] |
我们来看一下流式输出的打印结果:
1 | ------ |
从输出结果可以看出,agent 依次调用了三个 tool,并且 .stream 的输出在 actions 和 steps 之间交替,如果代理实现了其目标,则最终输出答案。其中 actions 表示调用某个 tool,steps 表示对应 tool 的输出结果。
我们可以打印 chunk[‘messages’] 更清楚的看到每一步的执行结果:
1 | ------ |
给 Agent 添加交互功能
将 agent 作为迭代器运行时,可以添加人机互动的功能,这个是非常有用的。
我们可以使用 agent_executor.iter() 方法来添加互动功能,具体例子如下。
首先还是上面定义的几个 tools,但是不需要 async 修饰:
1 | import random |
然后我们执行下面的代码:
1 | question = "find the items are located where the cat is hiding and tell me the items's usecase" |
上面的输出是:
1 | > Entering new AgentExecutor chain... |
通过上面的输出可以看出,我们可以人为干预 agent 的执行流程。
以上就是 LangChain 中开发 Agent 的大致步骤,还有很多细节有兴趣的同学可以下去研究一下。