0%

使用LangChain开发Agent

最近看了一些关于 AI Agent 开发相关的文档和开源项目,包括 AI Agent 的开发流程和步骤以及代码实现,打算分几次总结一下。

这篇博客总结了使用 LangChain 开发 AI Agent 的相关知识和代码。

开发 AI Agent 的大致步骤如下:

1
2
3
1. 定义 Tools。可以使用现有的工具,或者定义你自己的工具。
2. 创建 Agent。就是将 LLM 、Tools、Memory、Prompt 组合起来,放在 Agent 里面
3. 运行 Agent

这篇文章分为两部分,第一部分先介绍一些概念,第二部分介绍下具体的 Agent 代码。

Agent 相关概念

在进行开发前,先总结一下 LangChain 中 Agent 相关的一些概念。

AgentExecutor

AgentExecutor 是代理的运行时。这实际上是调用代理,执行它选择的操作,将操作输出传递回代理,然后重复。虽然这看起来很简单,但 AgentExecutor 会为您处理一些复杂的问题,包括:

  1. 处理代理选择不存在的工具的情况
  2. 处理工具错误的情况
  3. 处理代理生成无法解析为工具调用的输出的情况
  4. 所有级别(代理决策、工具调用)的日志记录和可观察性到标准输出和/或 LangSmith。

Tools

工具是 Agent 可以调用的功能。工具抽象由两个组件组成:

  1. 工具的输入架构。这告诉 LLM 调用该工具需要哪些参数。如果没有这个,它将不知道正确的输入是什么。这些参数应该被合理地命名和描述。
  2. 要运行的函数。这通常只是调用一个 Python 函数。

并且围绕工具有两个重要的设计考虑因素:

  1. 让 Agent 能够使用正确的工具
  2. 以对 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
2
3
4
from langchain_community.tools.tavily_search import TavilySearchResults

search = TavilySearchResults()
search.invoke("what is the weather in WuHan")

执行上面的代码后,输出结果为:

1
2
3
4
5
6
7
8
9
10
[{'url': 'https://www.weatherapi.com/',
'content': "{'location': {'name': 'Wuhan', 'region': 'Hubei', 'country': 'China', 'lat': 30.58, 'lon': 114.27, 'tz_id': 'Asia/Shanghai', 'localtime_epoch': 1713361288, 'localtime': '2024-04-17 21:41'}, 'current': {'last_updated_epoch': 1713360600, 'last_updated': '2024-04-17 21:30', 'temp_c': 18.0, 'temp_f': 64.4, 'is_day': 0, 'condition': {'text': 'Clear', 'icon': '//cdn.weatherapi.com/weather/64x64/night/113.png', 'code': 1000}, 'wind_mph': 4.3, 'wind_kph': 6.8, 'wind_degree': 30, 'wind_dir': 'NNE', 'pressure_mb': 1012.0, 'pressure_in': 29.88, 'precip_mm': 0.0, 'precip_in': 0.0, 'humidity': 77, 'cloud': 0, 'feelslike_c': 18.0, 'feelslike_f': 64.4, 'vis_km': 10.0, 'vis_miles': 6.0, 'uv': 1.0, 'gust_mph': 7.7, 'gust_kph': 12.3}}"},
{'url': 'https://world-weather.info/forecast/china/wuhan/april-2024/',
'content': 'Detailed ⚡ Wuhan Weather Forecast for April 2024 - day/night 🌡️ temperatures, precipitations - World-Weather.info. Add the current city. Search. Weather; Archive; Widgets °F. World; China; Hubei; Weather in Wuhan; Weather in Wuhan in April 2024. ... 17 +72° +68° 18 +79° +63° 19 ...'},
{'url': 'https://weatherspark.com/h/y/128408/2024/Historical-Weather-during-2024-in-Wuhan-China',
'content': 'Raw: ZHHH 082300Z 05002MPS CAVOK 13/11 Q1021 NOSIG. This report shows the past weather for Wuhan, providing a weather history for 2024. It features all historical weather data series we have available, including the Wuhan temperature history for 2024. You can drill down from year to month and even day level reports by clicking on the graphs.'},
{'url': 'https://www.qweather.com/en/historical/wuhan-101200101.html',
'content': 'Wuhan historical weather, historical air quality, Wuhan average temperature and average precipitation, typical meteorological year data, historical weather data services in China, etc. ... Hubei - China 2024-04-08 Monday 30.58N, 114.30E. Wuhan. Weather; 30 Days Forecast; Precipitation; Air Quality; Warning; Satellite+Radar; Map; Indices ...'},
{'url': 'https://en.climate-data.org/asia/china/hubei/wuhan-2629/t/april-4/',
'content': 'Weather ☀ ⛅ Wuhan ☀ ⛅ April ☀ ⛅ Information on temperature, sunshine hours, water temperature & rainfall in April for Wuhan. ... Are you planning a holiday with hopefully nice weather in Wuhan in April 2024? Here you can find all information about the weather in Wuhan in April: ... 17 °C | 63 °F : 21 °C | 71 °F: 13 °C | 55 °F ...'}]

定义 Shell 工具

使用 Shell 工具时,需要先安装 langchain-experimental 包:pip install langchain-experimental

定义工具的代码为:

1
2
3
4
from langchain_community.tools import ShellTool

shell_tool = ShellTool()
print(shell_tool.run({"commands": ["echo 'Hello World!'", "date"]}))

上面代码的输出结果为:

1
2
3
4
Executing command:
["echo 'Hello World!'", 'date']
Hello World!
Wed Apr 17 02:49:54 PM UTC 2024

定义 SearchApi 工具

首先需要配置 SearchApi 的环境变量 SEARCHAPI_API_KEY,这个 key 的值可以到 https://www.searchapi.io/ 上获得。

然后定义工具的代码如下:

1
2
3
4
from langchain_community.utilities import SearchApiAPIWrapper

search = SearchApiAPIWrapper()
search.run("Who is America president?")

执行上面的代码后,输出结果为:

1
'Joe Biden'

自定义工具

自定义 Retriever 工具

我们可以自定义一个 Retriever 检索器作为工具,代码实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
from langchain_community.document_loaders import TextLoader
from langchain_community.vectorstores import FAISS
from langchain_openai import OpenAIEmbeddings
from langchain_text_splitters import RecursiveCharacterTextSplitter

# 加载数据
loader = TextLoader("./personal.txt")
docs = loader.load()
# 分割文件内容
documents = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=200).split_documents(docs)
# 保存数据到向量数据库
vector = FAISS.from_documents(documents, OpenAIEmbeddings())
retriever = vector.as_retriever()
1
2
3
4
5
6
7
8
from langchain.tools.retriever import create_retriever_tool

# 创建 Tool,三个参数的含义依次是:指定检索的retriever、工具名称、工具描述
retriever_tool = create_retriever_tool(
retriever,
"wz_search",
"Search for information about wz. For any questions about wz, you must use this tool!",
)

自定义函数工具

我们还可以自定义一个函数作为工具,具体方法如下。

自定义工具需要用到 langchain_core.tools 中的 tool 函数,tool 函数可以作为装饰器把一个函数变为工具:

1
2
3
4
5
6
7
8
from langchain_core.tools import tool


@tool
def add(x: int, y: int) -> int:
"""calc x + y.
"""
return x + y

执行 add.invoke({“x”: 1, “y”: 2}) 就可以调用工具了。

还可以查看 add 的相关信息:

1
2
3
print(add.name)
print(add.description)
print(add.args)

打印出来的内容如下:

1
2
3
add
add(x: int, y: int) -> int - calc x + y.
{'x': {'title': 'X', 'type': 'integer'}, 'y': {'title': 'Y', 'type': 'integer'}}

tool 函数还可以添加一些参数,用法可以参考这个文档:

https://api.python.langchain.com/en/latest/tools/langchain_core.tools.tool.html#langchain_core.tools.tool


有了自定义工具,我们可以通过 chain 来调用它。

首先定义一个 LLM,并绑定要执行的工具:

1
2
3
4
5
6
7
8
9
10
11
12
from langchain_openai import ChatOpenAI

# 定义一个 llm
llm = ChatOpenAI(model="gpt-3.5-turbo-0125")
# 使用 bind_tools 将工具的定义作为每次调用 llm 的一部分传递,以便 llm 可以在适当的时候调用该工具
llm_with_tools = llm.bind_tools([add])

# 这一段代码的功能是通过 llm 提取出工具和它对应的参数
msg = llm_with_tools.invoke("whats 5 add forty two")
"""msg的内容是:
AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_p0o4LFIQDMNM7PSIvPPJQ1nc', 'function': {'arguments': '{"x": 5, "y": 42}', 'name': 'add'}, 'type': 'function'}]}, response_metadata={'token_usage': {'completion_tokens': 32, 'prompt_tokens': 65, 'total_tokens': 97}, 'model_name': 'gpt-3.5-turbo-0125', 'system_fingerprint': 'fp_c2295e73ad', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-e9e5023b-042f-4a75-b403-5476a2fae556-0', tool_calls=[{'name': 'add', 'args': {'x': 5, 'y': 42}, 'id': 'call_p0o4LFIQDMNM7PSIvPPJQ1nc'}])
"""

然后定义一个 chain 来执行工具:

1
2
3
4
5
# | 用来连接不同的处理步骤,从 llm_with_tools 中获取参数值并交给 add 去执行
chain = llm_with_tools | (lambda x: x.tool_calls[0]["args"]) | add
chain.invoke("I want know four add 23")
"""执行的结果是 27
"""

上面使用 chain 调用工具的情况是我们已经知道了用户输入与其对应的工具名称,如果我们想要 LLM 自己决定用户的输入使用哪个工具,就要使用到 Agent 了。

创建 Agent

创建 Agent 前,我们需要先定义 prompt 模板,这里我们可以使用 langchainhub 中的模板:

1
2
3
4
from langchain import hub

prompt = hub.pull("hwchase17/openai-tools-agent")
prompt.pretty_print()

prompt 的格式为:

1
2
3
4
5
6
7
8
================================ System Message ================================
You are a helpful assistant
============================= Messages Placeholder =============================
{chat_history}
================================ Human Message =================================
{input}
============================= Messages Placeholder =============================
{agent_scratchpad}

或者我们自定义一个 prompt 模板,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder

prompt = ChatPromptTemplate.from_messages(
[
(
"system",
"You are very powerful assistant,
),
("user", "{input}"),
MessagesPlaceholder(variable_name="agent_scratchpad"),
MessagesPlaceholder(variable_name="chat_history"),
]
)

然后我们看看定义好的两个工具:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from langchain_core.tools import tool


@tool
def add(x: int, y: int) -> int:
"""calc x + y.
"""
return x + y

@tool
def multiply(x: int, y: int) -> int:
"""calc x * y
"""
return x * y


tools = [add, multiply, search] # search 是开头的那个 Tavily 工具

我们使用 create_tool_calling_agent 创建一个工具调用的 agent:

1
2
3
from langchain.agents import AgentExecutor, create_tool_calling_agent

agent = create_tool_calling_agent(llm, tools, prompt)

运行 Agent

创建好 agent 后,我们可以创建一个 agent executor 并通过它来执行 agent:

1
2
3
4
5
6
7
8
9
# 创建 agent executor
agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True)

# 执行 agent
agent_executor.invoke(
{
"input": "I want to know the result of 3 times 4 and then add 5"
}
)

可以看看上面 invoke 的输出结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
> Entering new AgentExecutor chain...

Invoking: `multiply` with `{'x': 3, 'y': 4}`


12
Invoking: `add` with `{'x': 12, 'y': 5}`


17The result of 3 times 4 is 12, and when you add 5 to it, you get 17.

> Finished chain.
{'input': 'I want to know the result of 3 times 4 and then add 5',
'output': 'The result of 3 times 4 is 12, and when you add 5 to it, you get 17.'}

通过执行结果可以看出 LLM 通过调用 multiply 和 add 获取结果。

创建 AgentExecutor 对象时,还可以传入其它参数如:

1
2
max_iterations:表示 agent 要执行的最大步骤数
max_execution_time:表示 agent 在执行时花费的最大时间

还有其它参数可以查看文档:

https://api.python.langchain.com/en/latest/agents/langchain.agents.agent.AgentExecutor.html#langchain.agents.agent.AgentExecutor


我们换一个问题执行一下:

1
2
3
4
5
agent_executor.invoke(
{
"input": "what is the weather in Shenzhen"
}
)

上面 invoke 的执行结果是:

1
2
3
4
5
6
7
8
9
10
> Entering new AgentExecutor chain...

Invoking: `tavily_search_results_json` with `{'query': 'weather in Shenzhen'}`


[{'url': 'https://www.weatherapi.com/', 'content': "{'location': {'name': 'Shenzhen', 'region': 'Guangdong', 'country': 'China', 'lat': 22.53, 'lon': 114.13, 'tz_id': 'Asia/Hong_Kong', 'localtime_epoch': 1713441931, 'localtime': '2024-04-18 20:05'}, 'current': {'last_updated_epoch': 1713441600, 'last_updated': '2024-04-18 20:00', 'temp_c': 26.0, 'temp_f': 78.8, 'is_day': 0, 'condition': {'text': 'Partly cloudy', 'icon': '//cdn.weatherapi.com/weather/64x64/night/116.png', 'code': 1003}, 'wind_mph': 2.2, 'wind_kph': 3.6, 'wind_degree': 176, 'wind_dir': 'S', 'pressure_mb': 1007.0, 'pressure_in': 29.74, 'precip_mm': 0.0, 'precip_in': 0.0, 'humidity': 70, 'cloud': 50, 'feelslike_c': 28.2, 'feelslike_f': 82.7, 'vis_km': 10.0, 'vis_miles': 6.0, 'uv': 1.0, 'gust_mph': 12.1, 'gust_kph': 19.4}}"}, {'url': 'https://www.wunderground.com/history/daily/cn/shenzhen/IHONGKON188/date/2024-4-18', 'content': 'Current Weather for Popular Cities . San Francisco, CA 53 ° F Clear; Manhattan, NY warning 48 ° F Cloudy; Schiller Park, IL (60176) warning 50 ° F Fair; Boston, MA 48 ° F Cloudy; Houston, TX ...'}, {'url': 'https://www.weather25.com/asia/china/guangdong/shenzhen?page=month&month=April', 'content': "The average temperatures are between 71°F and 78°F. You can expect about 3 to 8 days of rain in Shenzhen during the month of April. It's a good idea to bring along your umbrella so that you don't get caught in poor weather. Our weather forecast can give you a great sense of what weather to expect in Shenzhen in April 2024."}, {'url': 'https://www.timeanddate.com/weather/china/shenzhen/ext', 'content': 'Weather Today Weather Hourly 14 Day Forecast Yesterday/Past Weather Climate (Averages) Currently: 79 °F. Clear. (Weather station: Shenzhen Airport, China). See more current weather.'}, {'url': 'https://www.metoffice.gov.uk/weather/forecast/ws10k3j56', 'content': 'UK video forecast\nNearest forecasts\nMore from the Met Office\nCauses of climate change\nFreezing rain\nSleet\n10 facts about snow\n6 facts about the winter solstice\nDriving safely in winter weather\nHelp us improve our website\n© Crown Copyright Today\nWed 10 Jan\nWed 10 Jan\nThu 11 Jan\nThu 11 Jan\nFri 12 Jan\nFri 12 Jan\nSat 13 Jan\nSat 13 This means that the symbol for 09:00 shows you what you will see from 09:00 to 10:00.\nChance of precipitation represents how likely it is that rain (or other types of precipitation, such as\nsleet, snow, hail and drizzle) will fall from the sky at a certain time.\n Jan\nEight\nday forecast for Shenzhen\nOur weather symbols tell you the weather conditions for any given hour in the day or night.\n Shenzhen (China) weather\nFind a forecast\nPlease choose your location from the nearest places to :\nForecast days\nToday\n'}]The current weather in Shenzhen, China is partly cloudy with a temperature of 26.0°C (78.8°F). The wind is blowing at 3.6 km/h from the south. The humidity is at 70% and the visibility is 10.0 km.

> Finished chain.
{'input': 'what is the weather in Shenzhen',
'output': 'The current weather in Shenzhen, China is partly cloudy with a temperature of 26.0°C (78.8°F). The wind is blowing at 3.6 km/h from the south. The humidity is at 70% and the visibility is 10.0 km.'}

通过执行结果可以看出 LLM 通过调用 tavily_search_results_json 获取结果。

给 Agent 添加记忆功能

上面运行的 agent 是没有历史记录的,也就是没有上下文信息,例如看下面的例子:

1
agent_executor.invoke({"input": "hi! my name is wyzane"})

上面是我告诉他我叫 wyzane,上面的输出是:

1
2
3
4
5
6
> Entering new AgentExecutor chain...
Hello Bob! How can I assist you today?

> Finished chain.
{'input': 'hi! my name is bob',
'output': 'Hello Bob! How can I assist you today?'}

然后我再执行:

1
agent_executor.invoke({"input": "who am i"})

上面的输出是:

1
2
3
4
5
6
> Entering new AgentExecutor chain...
I'm not sure who you are. Could you please provide me with more information so I can assist you better?

> Finished chain.
{'input': 'who am i',
'output': "I'm not sure who you are. Could you please provide me with more information so I can assist you better?"}

从上面两个问题可以看出 agent 是没有记忆功能的。


有两种方式可以给 agent 添加记忆功能:

方式一:手动把对话内容传入到 chat_history 参数中

方式二:使用 RunnableWithMessageHistory 自动跟踪信息。RunnableWithMessageHistory 允许我们将消息历史记录添加到某些类型的链中。它包装另一个 Runnable 并管理它的聊天消息历史记录。

RunnableWithMessageHistory 的详细使用可以查看以下文档:

https://python.langchain.com/docs/expression_language/how_to/message_history/


使用方式一,我们可以通过把对话内容传入到 chat_history 参数中,来给 agent 添加记忆,使用方式如下:

1
2
3
4
5
6
7
8
9
10
11
from langchain_core.messages import AIMessage, HumanMessage

agent_executor.invoke(
{
"chat_history": [
HumanMessage(content="hi! my name is wyzane"),
AIMessage(content="Hello wyzane! How can I assist you today?"),
],
"input": "what's my name?",
}
)

使用方式二,使用 RunnableWithMessageHistory,使用步骤如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from langchain_community.chat_message_histories import ChatMessageHistory
from langchain_core.runnables.history import RunnableWithMessageHistory

# 创建一个 ChatMessageHistory 对象,用于保存历史信息
message_history = ChatMessageHistory()

# 使用 RunnableWithMessageHistory 封装 agent_executor,添加自动记忆功能
# RunnableWithMessageHistory 包装另一个 Runnable 并为其管理聊天消息历史记录;它负责读取和更新聊天消息历史记录。
agent_with_chat_history = RunnableWithMessageHistory(
agent_executor, # 指定一个 Runnable 对象,这里是 agent_executor
lambda session_id: message_history, # 该参数指定返回 BaseChatMessageHistory 的函数。该函数应该采用单个位置参数 string 类型的`session_id`并返回对应的聊天消息历史记录实例。
input_messages_key="input", # 指定输入参数名
history_messages_key="chat_history", # 指定历史记录参数名
)

然后执行代码:

1
2
3
4
agent_with_chat_history.invoke(
{"input": "hi! I'm wyzane"},
config={"configurable": {"session_id": "xxxx"}},
)

输出结果是:

1
2
3
4
5
6
7
> Entering new AgentExecutor chain...
Hello Wyzane! How can I assist you today?

> Finished chain.
{'input': "hi! I'm wyzane",
'chat_history': [],
'output': 'Hello Wyzane! How can I assist you today?'}

再执行代码:

1
2
3
4
agent_with_chat_history.invoke(
{"input": "who am i"},
config={"configurable": {"session_id": "xxx"}},
)

输出结果是:

1
2
3
4
5
6
7
8
> Entering new AgentExecutor chain...
You are Wyzane! How can I help you further, Wyzane?

> Finished chain.
{'input': 'who am i',
'chat_history': [HumanMessage(content="hi! I'm wyzane"),
AIMessage(content='Hello Wyzane! How can I assist you today?')],
'output': 'You are Wyzane! How can I help you further, Wyzane?'}

可以看到是有上下文信息的。

给 Agent 添加流式输出

流式输出是 LLM 应用程序一个很重要的用户体验考虑因素,Agent 也不例外。使用 Agent 进行流式传输变得更加复杂,因为我们不仅仅想要流式传输最终的答案,而且还可能想要流式输出 Agent 所采取的中间步骤。下面介绍下如何在 Agent 中使用流式输出。

首先,我们创建 llm、tools、prompt:

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
40
41
42
43
44
import random

from langchain import hub
from langchain.tools import tool
from langchain_openai import ChatOpenAI


llm = ChatOpenAI(temperature=0, streaming=True)


import random


@tool
async def where_cat_is_hiding() -> str:
"""Where is the cat hiding right now?
"""
return random.choice(["under the bed", "on the shelf"])


@tool
async def get_items(place: str) -> str:
"""Use this tool to look up which items are in the given place.
"""
if "bed" in place:
return "shoes"
if "shelf" in place:
return "books"
else: # if the agent decides to ask about a different place
return "cat snacks"

@tool
async def items_usecase(items: str) -> str:
"""Use this tool to description items's usecase.
"""
if "shoes" in items:
return "we can wear"
if "books" in items:
return "we can read"
else:
return "do nothing"


prompt = hub.pull("hwchase17/openai-tools-agent")

然后,我们可以使用 create_openai_tools_agent 创建一个代理,并创建对应的 agent_executor:

1
2
3
4
5
6
7
8
9
from langchain.agents import AgentExecutor, create_openai_tools_agent

tools = [get_items, where_cat_is_hiding]
agent = create_openai_tools_agent(
llm.with_config({"tags": ["agent_llm"]}), tools, prompt
)
agent_executor = AgentExecutor(agent=agent, tools=tools).with_config(
{"run_name": "Agent"}
)

最后使用 AgentExecutor 的 .stream 方法来流式传输代理的中间步骤:

1
2
3
4
5
6
7
8
chunks = []

async for chunk in agent_executor.astream(
{"input": "find the items are located where the cat is hiding and tell me the items's usecase"}
):
chunks.append(chunk)
print("------")
print(chunk)

我们来看一下流式输出的打印结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
------
{'actions': [ToolAgentAction(tool='where_cat_is_hiding', tool_input={}, log='\nInvoking: `where_cat_is_hiding` with `{}`\n\n\n', message_log=[AIMessageChunk(content='', additional_kwargs={'tool_calls': [{'index': 0, 'id': 'call_wabgPwHScZwIm82Ws8LplG5o', 'function': {'arguments': '{}', 'name': 'where_cat_is_hiding'}, 'type': 'function'}]}, response_metadata={'finish_reason': 'tool_calls'}, id='run-1d3c0bec-31bf-4b1b-bcee-c72b314a6bff', tool_calls=[{'name': 'where_cat_is_hiding', 'args': {}, 'id': 'call_wabgPwHScZwIm82Ws8LplG5o'}], tool_call_chunks=[{'name': 'where_cat_is_hiding', 'args': '{}', 'id': 'call_wabgPwHScZwIm82Ws8LplG5o', 'index': 0}])], tool_call_id='call_wabgPwHScZwIm82Ws8LplG5o')], 'messages': [AIMessageChunk(content='', additional_kwargs={'tool_calls': [{'index': 0, 'id': 'call_wabgPwHScZwIm82Ws8LplG5o', 'function': {'arguments': '{}', 'name': 'where_cat_is_hiding'}, 'type': 'function'}]}, response_metadata={'finish_reason': 'tool_calls'}, id='run-1d3c0bec-31bf-4b1b-bcee-c72b314a6bff', tool_calls=[{'name': 'where_cat_is_hiding', 'args': {}, 'id': 'call_wabgPwHScZwIm82Ws8LplG5o'}], tool_call_chunks=[{'name': 'where_cat_is_hiding', 'args': '{}', 'id': 'call_wabgPwHScZwIm82Ws8LplG5o', 'index': 0}])]}
------
{'steps': [AgentStep(action=ToolAgentAction(tool='where_cat_is_hiding', tool_input={}, log='\nInvoking: `where_cat_is_hiding` with `{}`\n\n\n', message_log=[AIMessageChunk(content='', additional_kwargs={'tool_calls': [{'index': 0, 'id': 'call_wabgPwHScZwIm82Ws8LplG5o', 'function': {'arguments': '{}', 'name': 'where_cat_is_hiding'}, 'type': 'function'}]}, response_metadata={'finish_reason': 'tool_calls'}, id='run-1d3c0bec-31bf-4b1b-bcee-c72b314a6bff', tool_calls=[{'name': 'where_cat_is_hiding', 'args': {}, 'id': 'call_wabgPwHScZwIm82Ws8LplG5o'}], tool_call_chunks=[{'name': 'where_cat_is_hiding', 'args': '{}', 'id': 'call_wabgPwHScZwIm82Ws8LplG5o', 'index': 0}])], tool_call_id='call_wabgPwHScZwIm82Ws8LplG5o'), observation='on the shelf')], 'messages': [FunctionMessage(content='on the shelf', name='where_cat_is_hiding')]}
------
{'actions': [ToolAgentAction(tool='get_items', tool_input={'place': 'on the shelf'}, log="\nInvoking: `get_items` with `{'place': 'on the shelf'}`\n\n\n", message_log=[AIMessageChunk(content='', additional_kwargs={'tool_calls': [{'index': 0, 'id': 'call_Z4AHJJ2BMmNc43bHwumOuwjV', 'function': {'arguments': '{"place":"on the shelf"}', 'name': 'get_items'}, 'type': 'function'}]}, response_metadata={'finish_reason': 'tool_calls'}, id='run-d1103575-3f26-48c6-955a-0e200ad29675', tool_calls=[{'name': 'get_items', 'args': {'place': 'on the shelf'}, 'id': 'call_Z4AHJJ2BMmNc43bHwumOuwjV'}], tool_call_chunks=[{'name': 'get_items', 'args': '{"place":"on the shelf"}', 'id': 'call_Z4AHJJ2BMmNc43bHwumOuwjV', 'index': 0}])], tool_call_id='call_Z4AHJJ2BMmNc43bHwumOuwjV')], 'messages': [AIMessageChunk(content='', additional_kwargs={'tool_calls': [{'index': 0, 'id': 'call_Z4AHJJ2BMmNc43bHwumOuwjV', 'function': {'arguments': '{"place":"on the shelf"}', 'name': 'get_items'}, 'type': 'function'}]}, response_metadata={'finish_reason': 'tool_calls'}, id='run-d1103575-3f26-48c6-955a-0e200ad29675', tool_calls=[{'name': 'get_items', 'args': {'place': 'on the shelf'}, 'id': 'call_Z4AHJJ2BMmNc43bHwumOuwjV'}], tool_call_chunks=[{'name': 'get_items', 'args': '{"place":"on the shelf"}', 'id': 'call_Z4AHJJ2BMmNc43bHwumOuwjV', 'index': 0}])]}
------
{'steps': [AgentStep(action=ToolAgentAction(tool='get_items', tool_input={'place': 'on the shelf'}, log="\nInvoking: `get_items` with `{'place': 'on the shelf'}`\n\n\n", message_log=[AIMessageChunk(content='', additional_kwargs={'tool_calls': [{'index': 0, 'id': 'call_Z4AHJJ2BMmNc43bHwumOuwjV', 'function': {'arguments': '{"place":"on the shelf"}', 'name': 'get_items'}, 'type': 'function'}]}, response_metadata={'finish_reason': 'tool_calls'}, id='run-d1103575-3f26-48c6-955a-0e200ad29675', tool_calls=[{'name': 'get_items', 'args': {'place': 'on the shelf'}, 'id': 'call_Z4AHJJ2BMmNc43bHwumOuwjV'}], tool_call_chunks=[{'name': 'get_items', 'args': '{"place":"on the shelf"}', 'id': 'call_Z4AHJJ2BMmNc43bHwumOuwjV', 'index': 0}])], tool_call_id='call_Z4AHJJ2BMmNc43bHwumOuwjV'), observation='books')], 'messages': [FunctionMessage(content='books', name='get_items')]}
------
{'actions': [ToolAgentAction(tool='items_usecase', tool_input={'items': 'books'}, log="\nInvoking: `items_usecase` with `{'items': 'books'}`\n\n\n", message_log=[AIMessageChunk(content='', additional_kwargs={'tool_calls': [{'index': 0, 'id': 'call_1OaAd45pqtDf75tkLldWVInT', 'function': {'arguments': '{"items":"books"}', 'name': 'items_usecase'}, 'type': 'function'}]}, response_metadata={'finish_reason': 'tool_calls'}, id='run-9be97181-2979-4f89-924b-4637ab498cf9', tool_calls=[{'name': 'items_usecase', 'args': {'items': 'books'}, 'id': 'call_1OaAd45pqtDf75tkLldWVInT'}], tool_call_chunks=[{'name': 'items_usecase', 'args': '{"items":"books"}', 'id': 'call_1OaAd45pqtDf75tkLldWVInT', 'index': 0}])], tool_call_id='call_1OaAd45pqtDf75tkLldWVInT')], 'messages': [AIMessageChunk(content='', additional_kwargs={'tool_calls': [{'index': 0, 'id': 'call_1OaAd45pqtDf75tkLldWVInT', 'function': {'arguments': '{"items":"books"}', 'name': 'items_usecase'}, 'type': 'function'}]}, response_metadata={'finish_reason': 'tool_calls'}, id='run-9be97181-2979-4f89-924b-4637ab498cf9', tool_calls=[{'name': 'items_usecase', 'args': {'items': 'books'}, 'id': 'call_1OaAd45pqtDf75tkLldWVInT'}], tool_call_chunks=[{'name': 'items_usecase', 'args': '{"items":"books"}', 'id': 'call_1OaAd45pqtDf75tkLldWVInT', 'index': 0}])]}
------
{'steps': [AgentStep(action=ToolAgentAction(tool='items_usecase', tool_input={'items': 'books'}, log="\nInvoking: `items_usecase` with `{'items': 'books'}`\n\n\n", message_log=[AIMessageChunk(content='', additional_kwargs={'tool_calls': [{'index': 0, 'id': 'call_1OaAd45pqtDf75tkLldWVInT', 'function': {'arguments': '{"items":"books"}', 'name': 'items_usecase'}, 'type': 'function'}]}, response_metadata={'finish_reason': 'tool_calls'}, id='run-9be97181-2979-4f89-924b-4637ab498cf9', tool_calls=[{'name': 'items_usecase', 'args': {'items': 'books'}, 'id': 'call_1OaAd45pqtDf75tkLldWVInT'}], tool_call_chunks=[{'name': 'items_usecase', 'args': '{"items":"books"}', 'id': 'call_1OaAd45pqtDf75tkLldWVInT', 'index': 0}])], tool_call_id='call_1OaAd45pqtDf75tkLldWVInT'), observation='we can read')], 'messages': [FunctionMessage(content='we can read', name='items_usecase')]}
------
{'output': 'The items located where the cat is hiding are books. The use case of books is that we can read them.', 'messages': [AIMessage(content='The items located where the cat is hiding are books. The use case of books is that we can read them.')]}

从输出结果可以看出,agent 依次调用了三个 tool,并且 .stream 的输出在 actions 和 steps 之间交替,如果代理实现了其目标,则最终输出答案。其中 actions 表示调用某个 tool,steps 表示对应 tool 的输出结果。

我们可以打印 chunk[‘messages’] 更清楚的看到每一步的执行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
------
[AIMessageChunk(content='', additional_kwargs={'tool_calls': [{'index': 0, 'id': 'call_Df8O5PEdVYlDpT4x22PaU08n', 'function': {'arguments': '{}', 'name': 'where_cat_is_hiding'}, 'type': 'function'}]}, response_metadata={'finish_reason': 'tool_calls'}, id='run-0c58ba5e-cf85-4fb2-89b2-f1ce4f8f3ce4', tool_calls=[{'name': 'where_cat_is_hiding', 'args': {}, 'id': 'call_Df8O5PEdVYlDpT4x22PaU08n'}], tool_call_chunks=[{'name': 'where_cat_is_hiding', 'args': '{}', 'id': 'call_Df8O5PEdVYlDpT4x22PaU08n', 'index': 0}])]
------
[FunctionMessage(content='on the shelf', name='where_cat_is_hiding')]
------
[AIMessageChunk(content='', additional_kwargs={'tool_calls': [{'index': 0, 'id': 'call_bgSx7XGMyKO1tXhs33JYn2R9', 'function': {'arguments': '{"place":"on the shelf"}', 'name': 'get_items'}, 'type': 'function'}]}, response_metadata={'finish_reason': 'tool_calls'}, id='run-ab3b2c4e-e7e3-4b67-8463-a069929e736d', tool_calls=[{'name': 'get_items', 'args': {'place': 'on the shelf'}, 'id': 'call_bgSx7XGMyKO1tXhs33JYn2R9'}], tool_call_chunks=[{'name': 'get_items', 'args': '{"place":"on the shelf"}', 'id': 'call_bgSx7XGMyKO1tXhs33JYn2R9', 'index': 0}])]
------
[FunctionMessage(content='books', name='get_items')]
------
[AIMessageChunk(content='', additional_kwargs={'tool_calls': [{'index': 0, 'id': 'call_SK6muOHWiNLBgqRy3HycCSq3', 'function': {'arguments': '{"items":"books"}', 'name': 'items_usecase'}, 'type': 'function'}]}, response_metadata={'finish_reason': 'tool_calls'}, id='run-bee643c4-0d44-4c35-980d-034e8b5c4b90', tool_calls=[{'name': 'items_usecase', 'args': {'items': 'books'}, 'id': 'call_SK6muOHWiNLBgqRy3HycCSq3'}], tool_call_chunks=[{'name': 'items_usecase', 'args': '{"items":"books"}', 'id': 'call_SK6muOHWiNLBgqRy3HycCSq3', 'index': 0}])]
------
[FunctionMessage(content='we can read', name='items_usecase')]
------
[AIMessage(content='The items located where the cat is hiding are books. The use case of books is that we can read them.')]

给 Agent 添加交互功能

将 agent 作为迭代器运行时,可以添加人机互动的功能,这个是非常有用的。

我们可以使用 agent_executor.iter() 方法来添加互动功能,具体例子如下。

首先还是上面定义的几个 tools,但是不需要 async 修饰:

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
40
41
import random

from langchain import hub
from langchain.tools import tool
from langchain_openai import ChatOpenAI


llm = ChatOpenAI(temperature=0, streaming=True)


@tool
def where_cat_is_hiding() -> str:
"""Where is the cat hiding right now?
"""
return random.choice(["under the bed", "on the shelf"])


@tool
def get_items(place: str) -> str:
"""Use this tool to look up which items are in the given place.
"""
if "bed" in place:
return "shoes"
if "shelf" in place:
return "books"
else: # if the agent decides to ask about a different place
return "cat snacks"

@tool
def items_usecase(items: str) -> str:
"""Use this tool to description items's usecase.
"""
if "shoes" in items:
return "we can wear"
if "books" in items:
return "we can read"
else:
return "do nothing"


prompt = hub.pull("hwchase17/openai-tools-agent")

然后我们执行下面的代码:

1
2
3
4
5
6
7
question = "find the items are located where the cat is hiding and tell me the items's usecase"
ret = agent_executor.iter({"input": question})
for r in ret:
# 添加交互功能
_continue = input("Should the agent continue (Y/n)?:\n") or "Y"
if _continue.lower() != "y":
break

上面的输出是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
> Entering new AgentExecutor chain...

Invoking: `where_cat_is_hiding` with `{}`


under the bedShould the agent continue (Y/n)?:
y

Invoking: `get_items` with `{'place': 'under the bed'}`


shoesShould the agent continue (Y/n)?:
y

Invoking: `items_usecase` with `{'items': 'shoes'}`


we can wearShould the agent continue (Y/n)?:
y
The items located where the cat is hiding (under the bed) are shoes. The use case of shoes is that we can wear them.

> Finished chain.
Should the agent continue (Y/n)?:
y

通过上面的输出可以看出,我们可以人为干预 agent 的执行流程。


以上就是 LangChain 中开发 Agent 的大致步骤,还有很多细节有兴趣的同学可以下去研究一下。