有时我们希望LLM能直接调用一个外部函数或工具,尽管LLM通常只能输出文本,但现在其实很多LLM都实现了“Tool calling”功能,它并没有什么神奇的地方,“Tool calling”的底层仍是根据约束生成结构化数据实现的,但它却大大提升LLM和现实世界交互的能力。这篇笔记我们学习LangChain中如何让LLM调用工具函数。
注意:不是所有的LLM都支持工具调用,本篇笔记以Ollama本地部署的Llama3.1:8b模型为例进行介绍,该模型具有工具调用能力。
为了让LLM调用工具函数,我们需要“描述”工具函数,告知LLM我们的函数名是什么,它有哪些参数,返回值是什么,各个参数和返回值都是什么意思。手写这些描述信息是较为复杂的,LangChain中封装了这个功能,最简单的方式是直接使用@tool
装饰器将一个Python函数封装为工具函数。下面例子就是一个工具函数,它的输入参数是城市名,返回值是代表天气的字符串。
@tool
def query_weather(city: str) -> str:
"""
Query today's weather of a city
:param city: city name
:return: weather string
"""
return 'Sunny'
工具函数需要使用@tool
装饰器,这个装饰器会将我们的Python函数封装为StructuredTool
工具函数对象,另外要注意对于工具函数,我们必须使用完整的Python的类型约束,函数必须添加DocString,否则工具的描述信息是缺失的,LLM就不知道该如何调用。
上面定义工具函数的方式比较简易,我们其实也可以直接构造StructuredTool
对象,这种方式能更明确的指定更多信息,对于参数较为复杂的工具函数我们可能需要手动构造以获取更好的效果。
def query_weather(city: str) -> str:
"""
Query today's weather of a city
:param city: city name
:return: weather string
"""
return 'Sunny'
class QueryWeatherInput(BaseModel):
city: str = Field(description='city name')
query_weather_tool = StructuredTool(
func=query_weather,
name="query_weather",
description="Query today's weather of a city",
args_schema=QueryWeatherInput,
)
StructuredTool
中,我们需要指定函数、工具名字(建议和函数名保持一致)、描述信息和参数的Pydantic约束信息,相比之前@tool
定义方式,直接构造StructuredTool
我们能更灵活的定义工具函数。
前面我们定义了工具函数,下面代码我们将工具函数绑定到LangChain框架的ChatModel
并调用LLM。
from langchain.globals import set_debug, set_verbose
from langchain_core.messages import HumanMessage
from langchain_core.tools import tool
from langchain_ollama import ChatOllama
set_debug(True)
set_verbose(True)
@tool
def query_weather(city: str) -> str:
"""
Query today's weather of a city
:param city: city name
:return: weather string
"""
return 'Sunny'
@tool
def query_temperature(city: str) -> str:
"""
Query today's temperature of a city
:param city: city name
:return: temperature string
"""
return '36.5℃'
# 将工具绑定到LLM
tools = [query_weather, query_temperature]
llm = ChatOllama(model='llama3.1:8b', temperature=1)
llm_with_tools = llm.bind_tools(tools)
# 向LLM发送我们输入的问题
messages = [HumanMessage("What is weather and temperature today in Boston?")]
ai_messages = llm_with_tools.invoke(messages)
print(ai_messages)
上面代码我们打印了返回值ai_messages
,观察返回内容我们可以看到对象中有下面这样的一些信息。
tool_calls=[{"name": "query_weather", "args": {"city": "Boston"}, "id": "5481cab2-3d01-40bc-872f-cb182fdebe32", "type": "tool_call"}, {"name": "query_temperature", "args": {"city": "Boston"}, "id": "b17712c3-28e6-4938-99cd-08b36dfc8c08", "type": "tool_call"}]
LLM想要调用的工具函数可以是多个,tool_calls
中我们可以看到LLM输出了想要调用的工具名和参数。在接下来的代码中我们就可以读取这些信息,并真正的调用工具函数。
# 解析返回内容的tool_calls信息,并真正调用工具函数,调用结果是ToolMessage,我们将其加回到messages中
for tool_call in ai_messages.tool_calls:
selected_func = {"query_weather": query_weather, "query_temperature": query_temperature}[tool_call["name"].lower()]
tool_message = selected_func.invoke(tool_call)
messages.append(tool_message)
LangChain中,除了SystemMessage
、HumanMessage
、AIMessage
其实还有一种ToolMessage
,它代表LLM调用工具函数后返回的结果。上面代码中,我们基于LLM返回的tool_calls
真正调用了工具函数,并将ToolMessage
返回值又塞回了message
数组。现在的messages
将有1条HumanMessage
和若干条ToolMessage
。
现在我们可以将整个messages
再输入LLM,便可以得到总结性的文本响应。
# 带着ToolMessage再次调用LLM
result = llm_with_tools.invoke(messages)
print(result.content)
最终,我们可能会得到类似如下的文本输出。
According to my tool call, it's sunny today in Boston, with a temperature of 36.5 degrees Celsius (97.7°F).
这样一个简单的工具调用场景便实现了。
前面介绍了自定义工具函数,实际上LangChain框架还集成了大量第三方API并将其封装为了工具,很多通用的功能我们可以直接使用这些封装好的工具包,不必自己再开发了。当然,这些工具包大多是社区维护的,存在质量参差不齐、API变化频繁的情况,具体使用时我们需要仔细参考相关文档,并具有足够的搜索和解决问题能力。我们这里以Tavily Search作为一个例子,介绍如何集成这些工具包。
Tavily Search是一个搜索引擎API,目前它支持每月1000次的免费调用,足够我们使用了,我们注册账号即可获取API Key。Tavily Search相关的集成包包含在langchain-community
中,我们需要先确保安装了这个包。
pip install langchain-community
注册Tavily Search后我们需要复制一下API Key,在我们的程序中需要将其设置为环境变量TAVILY_API_KEY
。
Tavily Search给出的工具函数和我们之前自定义的工具函数用法是一样的,这里我们直接给出完整的例子代码。
from langchain.globals import set_debug, set_verbose
from langchain_community.tools import TavilySearchResults
from langchain_core.messages import HumanMessage
from langchain_ollama import ChatOllama
set_debug(True)
set_verbose(True)
tavily_search = TavilySearchResults(
max_results=5,
search_depth="advanced",
include_answer=True,
include_raw_content=True
)
tools = [tavily_search]
llm = ChatOllama(model='llama3.1:8b', temperature=1)
llm_with_tools = llm.bind_tools(tools)
messages = [HumanMessage("What is the weather today in Boston?")]
ai_messages = llm_with_tools.invoke(messages)
for tool_call in ai_messages.tool_calls:
selected_func = {tavily_search.name: tavily_search}[tool_call["name"].lower()]
tool_message = selected_func.invoke(tool_call)
messages.append(tool_message)
result = llm_with_tools.invoke(messages)
print(result.content)
运行后输出的内容参考如下。
The current weather in Boston is overcast with a temperature of 30°F (−1.1°C). The wind speed is 18.4 km/h (11.4 mph) from the South-South-West direction, and the humidity is 66%. There is no precipitation expected.