目录
前两篇已经完成了基础模型调用、PromptTemplate 和结构化输出。
这一篇开始进入 LangChain 里非常关键的一块:工具调用和 Agent。
本文对应代码位于 code/langchain-demo/chapter03。这一章依然保持独立目录,避免后续代码改动影响前面文章的示例。
本篇目标
在开始之前,先明确一下这篇要解决的问题。
前两篇里,模型主要是在“回答问题”。即使第二篇加了结构化输出,本质上也还是让模型按照格式生成内容。
但真实应用里,经常希望模型可以做一些自己不擅长,或者不应该靠猜的事情,比如:
- 计算两个数字相乘。
- 统计文本长度。
- 查询数据库。
- 读取文件。
- 调用某个业务接口。
这里就需要工具调用。
第三篇先不接数据库、文件系统或者网络搜索,只定义几个结果确定的小工具。这样能把注意力放在 LangChain 的工具调用机制上。
本篇主要完成两件事:
- 使用
bind_tools观察模型如何提出工具调用。 - 使用
create_agent让 Agent 自动执行工具调用流程。
代码结构
第三篇的代码结构如下:
code/langchain-demo/
chapter03/
__init__.py
config.py
llm.py
simple_tools.py
tool_calling_demo.py
agent_demo.py
其中:
config.py:读取.env。llm.py:创建ChatOpenAI模型对象。simple_tools.py:定义工具。tool_calling_demo.py:演示bind_tools和手动执行工具。agent_demo.py:演示create_agent自动执行工具调用流程。
config.py 和 llm.py 和前面章节差不多,这里就不重复展开了。重点看工具和调用流程。
定义工具
工具代码放在 chapter03/simple_tools.py:
from langchain_core.tools import tool
@tool
def count_characters(text: str) -> int:
"""统计一段文本中的字符数量。"""
return len(text)
@tool
def count_words(text: str) -> int:
"""统计英文文本中的单词数量。"""
return len(text.split())
@tool
def multiply(a: int, b: int) -> int:
"""计算两个整数相乘的结果。"""
return a * b
# 第三章只放几个结果确定的工具,方便观察模型有没有真的发起工具调用。
TOOLS = [count_characters, count_words, multiply]
这里用了 @tool 装饰器,把普通 Python 函数包装成 LangChain 可以识别的工具。
有几个点需要注意:
- 函数名会成为工具名。
- 函数参数会变成工具入参。
- docstring 会作为工具说明提供给模型。
也就是说,模型判断是否调用工具时,不只是看函数名,也会参考工具说明。
所以工具的 docstring 不要随便写,最好直接说明这个工具能做什么。
这一篇只定义了三个工具:
count_characters:统计字符数量。count_words:统计英文单词数量。multiply:计算两个整数相乘。
这些工具都非常简单,但结果确定,适合用来观察工具调用过程。
使用 bind_tools
接下来先看比较底层的工具调用方式。
文件是 chapter03/tool_calling_demo.py:
import json
import sys
from langchain_core.messages import HumanMessage, SystemMessage, ToolMessage
from chapter03.config import load_environment
from chapter03.llm import create_chat_model
from chapter03.simple_tools import TOOLS
DEFAULT_QUESTION = (
"请计算 23 乘以 19,"
"再统计文本 LangChain tool calling 的字符数,最后汇总结果。"
)
这个 demo 的默认问题里有两个任务:
- 计算
23 * 19。 - 统计
LangChain tool calling的字符数。
这两个任务正好分别对应 multiply 和 count_characters 工具。
核心逻辑如下:
tools_by_name = {item.name: item for item in TOOLS}
# bind_tools 只是把“有哪些工具可以用”告诉模型。
# 这里还不会自动执行工具,模型会先返回 tool_calls。
model = create_chat_model(temperature=0).bind_tools(TOOLS)
messages = [
SystemMessage(content="你是一个会使用工具解决问题的助手。"),
HumanMessage(content=question),
]
ai_message = model.invoke(messages)
这里需要重点看 bind_tools。
bind_tools 的作用是把工具列表绑定到模型上,让模型知道当前有哪些工具可以使用。
但是要注意:bind_tools 不等于自动执行工具。
调用模型后,模型可能返回 tool_calls,意思是模型觉得接下来应该调用哪些工具,以及工具参数是什么。
这一步可以理解成:
用户问题 -> 模型判断需要什么工具 -> 返回 tool_calls
查看 tool_calls
代码里会把 tool_calls 打印出来:
print("Tool calls:")
print(json.dumps(ai_message.tool_calls, ensure_ascii=False, indent=2))
运行后可以看到类似结果:
[
{
"name": "multiply",
"args": {
"a": 23,
"b": 19
},
"id": "call_xxx",
"type": "tool_call"
},
{
"name": "count_characters",
"args": {
"text": "LangChain tool calling"
},
"id": "call_xxx",
"type": "tool_call"
}
]
可以看到,模型没有直接计算,而是返回了两个工具调用请求:
- 调用
multiply,参数是a=23、b=19。 - 调用
count_characters,参数是text="LangChain tool calling"。
这时候工具还没有真正执行。
手动执行工具
接下来代码会手动执行这些工具:
tool_messages: list[ToolMessage] = []
# 手动执行模型请求的工具,这一步能帮助理解 Agent 背后的基本过程。
for tool_call in ai_message.tool_calls:
tool_name = tool_call["name"]
tool_args = tool_call["args"]
selected_tool = tools_by_name[tool_name]
tool_result = selected_tool.invoke(tool_args)
print()
print(f"Executed tool: {tool_name}")
print(f"Args: {tool_args}")
print(f"Result: {tool_result}")
tool_messages.append(
ToolMessage(
content=str(tool_result),
name=tool_name,
tool_call_id=tool_call.get("id") or tool_name,
)
)
这里的流程很清楚:
- 根据工具名找到真实工具对象。
- 把模型给出的参数传给工具。
- 得到工具执行结果。
- 把结果包装成
ToolMessage。
ToolMessage 的作用是把工具执行结果重新交回给模型。
最后再调用一次模型:
final_response = model.invoke([*messages, ai_message, *tool_messages])
这一次模型就可以根据工具结果生成最终回答。
完整流程就是:
用户问题
-> 模型返回 tool_calls
-> Python 执行工具
-> 工具结果交回模型
-> 模型总结最终答案
运行工具调用示例
在 code/langchain-demo 目录下运行:
uv run python -m chapter03.tool_calling_demo
正常输出类似这样:
Question:
请计算 23 乘以 19,再统计文本 LangChain tool calling 的字符数,最后汇总结果。
Tool calls:
[
{
"name": "multiply",
"args": {
"a": 23,
"b": 19
},
"id": "call_xxx",
"type": "tool_call"
},
{
"name": "count_characters",
"args": {
"text": "LangChain tool calling"
},
"id": "call_xxx",
"type": "tool_call"
}
]
Executed tool: multiply
Args: {'a': 23, 'b': 19}
Result: 437
Executed tool: count_characters
Args: {'text': 'LangChain tool calling'}
Result: 22
Final answer:
汇总结果:
- 23 × 19 = 437
- 文本 `LangChain tool calling` 的字符数 = 22
通过这个 demo,可以比较直观看到模型和工具之间的关系。
模型负责判断“该调用什么工具、参数是什么”,Python 负责真正执行工具。
Agent 是什么
理解了 bind_tools 后,再看 Agent 就比较容易了。
前面的手动流程里,我们自己写了这些步骤:
- 调用模型。
- 读取
tool_calls。 - 执行工具。
- 把工具结果传回模型。
- 再让模型生成最终回答。
Agent 做的事情,就是把这套流程自动化。
可以简单理解为:
Agent = 模型 + 工具 + 执行循环
当然,真实 Agent 能做的事情会更多,但入门时先这样理解就够了。
使用 create_agent
Agent 示例放在 chapter03/agent_demo.py:
import sys
from langchain.agents import create_agent
from chapter03.config import load_environment
from chapter03.llm import create_chat_model
from chapter03.simple_tools import TOOLS
DEFAULT_QUESTION = (
"请计算 23 乘以 19,"
"再统计文本 LangChain tool calling 的字符数,最后给出简洁答案。"
)
核心代码如下:
# Agent 会在模型和工具之间做执行循环:
# 模型决定调用哪个工具,工具返回结果后,Agent 再把结果交给模型总结。
agent = create_agent(
model=create_chat_model(temperature=0),
tools=TOOLS,
system_prompt="你是一个会优先使用工具解决计算和统计问题的助手。",
)
result = agent.invoke(
{
"messages": [
{
"role": "user",
"content": question,
}
]
}
)
这里使用的是 create_agent。
和 bind_tools 相比,create_agent 更进一步,它会自动完成工具调用循环。
也就是说,你不用自己遍历 tool_calls,也不用自己创建 ToolMessage,Agent 会帮你处理。
为了看清楚 Agent 中间做了什么,代码里把返回的消息列表都打印出来:
for message in result["messages"]:
message_type = getattr(message, "type", message.__class__.__name__)
content = getattr(message, "content", "")
tool_calls = getattr(message, "tool_calls", None)
print(f"- {message_type}: {content}")
if tool_calls:
print(f" tool_calls: {tool_calls}")
这样可以看到人类消息、模型消息、工具消息和最终回答。
运行 Agent 示例
在 code/langchain-demo 目录下运行:
uv run python -m chapter03.agent_demo
正常输出类似这样:
Question:
请计算 23 乘以 19,再统计文本 LangChain tool calling 的字符数,最后给出简洁答案。
Messages:
- human: 请计算 23 乘以 19,再统计文本 LangChain tool calling 的字符数,最后给出简洁答案。
- ai:
tool_calls: [{'name': 'multiply', 'args': {'a': 23, 'b': 19}, ...}]
- tool: 437
- tool: 22
- ai: 23 × 19 = 437;文本字符数 = 22。
Final answer:
23 × 19 = 437;文本字符数 = 22。
这个结果和手动工具调用示例做的是同一件事,只是执行流程交给 Agent 处理了。
bind_tools 和 Agent 的区别
这里简单总结一下两者区别。
bind_tools 更底层:
- 把工具能力告诉模型。
- 模型返回
tool_calls。 - 工具执行需要自己写代码。
- 更适合学习和调试工具调用流程。
create_agent 更完整:
- 把模型和工具组合起来。
- 自动执行工具调用循环。
- 自动把工具结果交回模型。
- 更适合写真正的业务流程。
所以这一篇先写 tool_calling_demo.py,再写 agent_demo.py。
只有先看过手动工具调用过程,Agent 做了什么才不会显得太神秘。
常见问题
模型不调用工具怎么办
工具调用是模型自己决定的。
如果模型没有返回 tool_calls,可以从几个方向排查:
- 问题是否真的需要工具。
- 工具 docstring 是否描述清楚。
- system prompt 是否明确要求优先使用工具。
- 当前模型服务是否支持 tool calling。
本篇示例里使用的是计算和字符统计,目的就是让模型更容易判断应该调用工具。
为什么不直接让模型计算
简单数字模型也可能直接算对,但这不是重点。
工具调用的核心价值是:把确定性的事情交给确定性的工具。
比如计算、查数据库、调接口,这些事情不应该靠模型猜,而应该由程序执行。
模型更适合做的是理解用户意图、选择工具、组织最终回答。
第三章为什么不用真实外部工具
这一篇没有接文件、数据库和网络搜索,是刻意的。
因为第三章要先讲清楚工具调用机制本身。
如果一上来就接很多外部系统,读者很容易把注意力放在文件路径、接口鉴权、网络问题上,反而忽略了 LangChain 里的工具调用流程。
等工具调用理解清楚后,后面再接 RAG 或博客文章检索,会顺很多。
小结
这一篇完成了 LangChain 工具调用和 Agent 的入门实践:
- 使用
@tool把普通 Python 函数包装成工具。 - 使用
bind_tools把工具绑定到模型。 - 观察模型返回的
tool_calls。 - 手动执行工具,并通过
ToolMessage把结果交回模型。 - 使用
create_agent自动完成工具调用流程。
到这里,模型就不只是生成文本了,它已经可以通过工具完成一些确定性的任务。
下一篇可以继续往 RAG 方向走,尝试把博客文章作为知识库,让模型基于本地文章内容回答问题。
参考
- https://docs.langchain.com/oss/python/langchain/overview
- https://docs.langchain.com/oss/python/langchain/agents
- https://python.langchain.com/api_reference/core/tools/langchain_core.tools.convert.tool.html
- https://python.langchain.com/api_reference/core/messages/langchain_core.messages.tool.ToolMessage.html
原创文章,转载请注明来源: LangChain 入门学习第三篇-工具调用和 Agent
相关文章
- LangChain 入门学习第二篇-PromptTemplate 和结构化输出
- LangChain 入门学习第一篇-环境准备和模型调用
- 使用 Cursor 进行 AI 编程的年度总结
- 真的,AI 可能就是新时代的信息差
- 充值 Cursor 之后,工作有了哪些变化?🤔
- 个人'蒸馏'大模型能做哪些有意思的事情
- DeepSeek 大模型在 Mac 上的部署和运行
- 博客图床迁移记
- Python 函数式编程
- Python 命令行参数解析工具 docopt