让我们在LangChain中构建简单代理示例,以帮助我们理解代理的基本概念和构建块。通过保持简单,我们可以更好地掌握这些代理背后的基本思想,使我们能够在未来构建更复杂的代理。
什么是代理
LangChain官方文档有非常好的章节来介绍其代理的高级概念。但本文强调简短易懂,绝对值得在开始之前浏览一下。
如果你查找人工智能代理的定义,你会发现“一个实体能够感知其环境,对其环境采取行动,并就如何达到给定的目标做出明智的决定,以及学习的能力。”
我觉得这很符合LangChain的定义,使这一切在软件中成为可能的是大型语言模型(LLM)的推理能力。LangChain代理的大脑是LLM,LLM用于推断执行用户请求的最佳方式。
为了执行任务,操作事物和检索信息,代理在LangChain中拥有所谓的工具。正是通过这些工具,它才能够与环境进行互动。
这些工具基本上就是代理可以访问的方法/类,它们可以通过API与Stock Market指数交互、更新办公Calendar事件或对数据库运行查询。我们可以根据需要构建工具,这取决于我们试图与代理一起执行的任务的性质。
LangChain中的工具集合称为Toolkit。在实现方面,这实际上只是代理可用的工具集合。因此,在LangChain中,代理的高级概述看起来是这样的
因此,在基本层面上,代理需要:
- 一个LLm充当它的大脑,并赋予它推理能力
- 工具,以便它可以与周围的环境进行交互,并实现其目标
构建代理
为了使这些概念更加具体,让我们构建一个简单的代理。我们将创建数学代理,它可以执行一些简单的数学运算。
环境设置
首先让我们设置环境和脚本:
mkdir simple-math-agent && cd simple-math-agent
touch math-agent.py
python3 -m venv .venv
. .venv/bin/activate
pip install langchain langchain_openai
工具
最简单的开始将是首先为我们的数学代理定义工具。
让我们给它“加”、“乘”和“平方”工具,这样它就可以对我们传递给它的问题执行这些操作。通过保持我们的工具简单,我们可以专注于核心概念,并自己构建工具,而不是依赖于现有的更复杂的工具,如wiki检索,它作为维基百科API的包装器,需要我们从LangChain库中导入它。
同样,我们在这里并没有尝试做任何花哨的事情,只是保持简单,并将代理的主要构建块放在一起,以便我们能够理解它们是如何工作的,并使我们的第一个代理启动并运行。
让我们从“加”工具开始。在LangChain中创建工具的自下而上的方法是扩展BaseTool类,设置类上的名称和描述字段,并实现_run方法。就像这样:
from langchain_core.tools import BaseTool
class AddTool(BaseTool):
name = "add"
description = "Adds two numbers together"
args_schema: Type[BaseModel] = AddInput
return_direct: bool = True
def _run(
self, a: int, b: int, run_manager: Optional[CallbackManagerForToolRun] = None
) -> str:
return a + b
注意,我们需要实现_run方法来显示我们的工具如何处理传递给它的参数。还要注意它是如何为args_schema需要一个pydantic模型的。我们在这里定义一下
AddInput
a: int = Field(description="first number")
b: int = Field(description="second number")
现在,LangChain确实为我们提供了一种更简单的方法来定义工具,然后每次都需要扩展BaseTool类。我们可以在@tool装饰器的帮助下做到这一点。使用@tool装饰器在LangChain中定义“加”工具,代码如下所示:
from langchain.tools import tool
@tool
def add(a: int, b: int) -> int:
"""Adds two numbers together""" # this docstring gets used as the description
return a + b # the actions our tool performs
简单多了,对吧?在幕后,装饰器神奇地使用提供的方法来扩展BaseTool类,就像我们前面所做的那样。有些事情需要注意:
- 方法名也成为工具名
- 方法参数定义工具的输入参数
- 文档字符串转换为工具描述
在工具上访问这些属性:
print(add.name) # add
print(add.description) # Adds two numbers together.
print(add.args) # {'a': {'title': 'A', 'type': 'integer'}, 'b': {'title': 'B', 'type': 'integer'}}
请注意,工具的描述非常重要,因为这是LLM用来决定该工具是否适合该工作的工具。错误的描述可能会导致非工具在应该使用的时候被使用,或者在错误的时间被使用。
添加工具完成后,让我们继续定义乘法和平方工具。
@tool
def multiply(a: int, b: int) -> int:
"""Multiply two numbers."""
return a * b
@tool
def square(a) -> int:
"""Calculates the square of a number."""
a = int(a)
return a * a
就是这样,很简单。
因此,我们已经定义了我们自己的三个定制工具。更常见的用例可能是使用LangChain中已经提供的和现有的一些工具,您可以在这里看到。然而,在源代码级别,它们都将使用上面描述的类似方法来构建和定义。
这就是我们所关心的工具。现在是时候将我们的工具组合成一个工具包了。
工具包
工具箱听起来很花哨,但实际上非常简单。它们实际上只是一个工具列表。我们可以将工具箱定义为如下所示的一系列工具
toolkit = [add, multiply, square]
就是这样。真的很简单,没有什么好混淆的。
通常,工具包是一组工具,这些工具组合在一起很有用,对试图执行某些任务的代理很有帮助。例如,SQLToolkit可能包含用于生成SQL查询、验证SQL查询和执行SQL查询的工具。
LangChain文档上的integration Toolkit页面有社区开发的工具箱的大列表,这些工具包可能对你有用。
LLM
如上所述,LLM是代理的大脑。它根据传递给它的问题决定调用哪个工具,根据工具描述采取的最佳下一步是什么。它还决定何时得到最终答案,并准备将答案返回给用户。
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(model="gpt-3.5-turbo-1106", temperature=0)
提示词
最后,我们需要一个提示词传递给我们的代理,这样它就有一个关于它是什么类型的代理以及它应该解决什么类型的任务的一般概念。
我们的代理需要一个ChatPromptTemplate才能工作(稍后会详细介绍)。这就是一个基本的ChatPromptTemplate的样子。我们关心的主要部分是系统提示符,其余的只是我们需要传入的默认设置。
在我们的提示中,我们包含了一个示例答案,向代理展示了我们希望它如何只返回答案,而不是随答案一起返回任何描述性文本。
prompt = ChatPromptTemplate.from_messages(
[
("system", """
You are a mathematical assistant. Use your tools to answer questions.
If you do not have a tool to answer the question, say so.
Return only the answers. e.g
Human: What is 1 + 1?
AI: 2
"""),
MessagesPlaceholder("chat_history", optional=True),
("human", "{input}"),
MessagesPlaceholder("agent_scratchpad"),
]
)
就是这样。我们已经设置了我们的Tools和Toolkit,我们的代理将需要它们作为其设置的一部分,因此它知道它可以处理的操作和功能的类型。我们还设置了LLM和系统提示符。
现在到了有趣的部分,建立我们的代理!
代理
LangChain可以创建许多不同类型的代理,具有不同的推理能力和能力。我们将使用目前可用的最强大的代理,OpenAI Tools代理。根据OpenAI工具代理的文档,它也使用更新的OpenAI模型,
更新的OpenAI模型已经进行了微调,可以检测何时应该调用一个或多个函数,并使用应该传递给函数的输入进行响应。在API调用中,你可以描述函数,并让模型智能地选择输出包含参数的JSON对象来调用这些函数。OpenAI工具API的目标是比使用通用文本完成或聊天API更可靠地返回有效和有用的函数调用。这里仅为示例,LangChain可以接入主流大模型,包括DeepSeek R1.
换句话说,这个代理擅长为调用函数生成正确的结构,并且能够理解我们的任务是否还需要多个函数(工具)。这个代理还具有调用具有多个输入参数的函数(工具)的能力,就像我们的一样。有些代理只能处理具有单个输入参数的函数。
如果你熟悉OpenAI的函数调用功能,我们可以使用OpenAI LLM生成正确的参数来调用函数,我们在这里使用的OpenAI Tools代理正在利用一些功能:
agent = create_openai_tools_agent(llm, toolkit, prompt)
最后,为了在LangChain中运行代理,我们不能直接对它们调用“run”类型的方法。它们需要通过AgentExecutor运行。
我在最后才提到代理执行者,因为我不认为它是理解代理如何工作的关键概念,把它和其他东西放在一起只会让整个事情看起来比它需要的更复杂,也会分散对其他更基本概念的理解。
因此,现在我们正在介绍它,AgentExecutor充当LangChain中代理的运行时,并允许代理保持运行,直到它准备好向用户返回其最终响应。在伪代码中,AgentExecutor的操作如下(直接引用自LangChain文档):
next_action = agent.get_action(...)
while next_action != AgentFinish:
observation = run(next_action)
next_action = agent.get_action(..., next_action, observation)
return next_action
它们基本上是一个while循环,不断调用代理上的下一个操作方法,直到代理返回其最终响应。因此,让我们在代理执行器中设置代理。我们将代理传递给它,还必须将工具包传递给它。我们将verbose设置为True,这样我们就可以了解代理在处理我们的请求时正在做什么:
agent_executor = AgentExecutor(agent=agent, tools=toolkit, verbose=True)
就是这样。现在我们已经准备好向代理传递命令了:
result = agent_executor.invoke({"input": "what is 1 + 1"})
让我们运行脚本,看看代理的输出:
python3 math-agent.py
因为我们已经在AgentExecutor上设置了verbose=True,所以我们可以看到代理所执行的操作行。它确定了我们应该调用“加”工具,调用带有所需参数的“加”工具,并返回我们的结果。
下面是完整源代码:
import os
from langchain.agents import AgentExecutor, create_openai_tools_agent
from langchain_openai import ChatOpenAI
from langchain.tools import BaseTool, StructuredTool, tool
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
os.environ["OPENAI_API_KEY"] = "sk-"
# setup the tools
@tool
def add(a: int, b: int) -> int:
"""Add two numbers."""
return a + b
@tool
def multiply(a: int, b: int) -> int:
"""Multiply two numbers."""
return a * b
@tool
def square(a) -> int:
"""Calculates the square of a number."""
a = int(a)
return a * a
prompt = ChatPromptTemplate.from_messages(
[
("system", """You are a mathematical assistant.
Use your tools to answer questions. If you do not have a tool to
answer the question, say so.
Return only the answers. e.g
Human: What is 1 + 1?
AI: 2
"""),
MessagesPlaceholder("chat_history", optional=True),
("human", "{input}"),
MessagesPlaceholder("agent_scratchpad"),
]
)
# Choose the LLM that will drive the agent
llm = ChatOpenAI(model="gpt-3.5-turbo-1106", temperature=0)
# setup the toolkit
toolkit = [add, multiply, square]
# Construct the OpenAI Tools agent
agent = create_openai_tools_agent(llm, toolkit, prompt)
# Create an agent executor by passing in the agent and tools
agent_executor = AgentExecutor(agent=agent, tools=toolkit, verbose=True)
result = agent_executor.invoke({"input": "what is 1 + 1?"})
print(result['output'])
测试代码
让我们向代理提出几个问题,看看它的表现如何。
5的平方是多少?
我们再次得到了正确的结果,并看到它确实使用了我们的平方工具。
5的6次方是多少?
这需要一个有趣的推理过程。首先使用平方工具;然后,利用这个结果,尝试使用乘法工具来得到最终的答案。无可否认,最终的答案3125是错误的,需要再乘以5才能得到正确的答案。但是看到代理如何尝试使用不同的工具和多个步骤来尝试获得最终答案是很有趣的。
1 - 3等于多少?
我们没有减号工具。但它足够聪明,可以使用我们的添加工具,但将第二个值设置为-3。有趣的是,有时他们是如此的聪明和有创造力。
64的平方根是多少
作为最后的测试,如果我们要求它执行一个不属于我们工具集的数学运算会怎么样?由于我们没有用于平方根的工具,因此它不会尝试调用工具,而是直接使用LLM计算值。
我们的系统提示词确实告诉它回答“不知道”,如果它没有正确的工具来完成这项工作,它有时在测试期间确实会这样做。改进的初始系统提示符可能有助于解决这个问题,至少在某种程度上是这样。
观察
基于对代理的使用,我注意到以下几点:
- 当直接问它有工具可以回答的问题时,它会非常一致地使用正确的工具来完成任务,并返回正确的答案。所以,从这个意义上说,它非常可靠。
- 如果问题有点复杂,例如我们的“5的6次方”问题,它并不总是返回正确的结果。
- 它有时可以只使用LLM的纯粹力量来回答我们的问题,而不调用我们的工具。
- 建议你对照示例,测试不同的大模型,尤其国内主流大模型,如智普、DeepSeek等。
总结
希望本文介绍内容能帮助你开始在LangChain中构建代理。记住,代理本质上只是一个大脑(LLM)和一堆工具,它们可以用来帮助我们完成一些特定任务。