目录

一个专注音视频领域的小圈子

前两篇已经完成了基础模型调用、PromptTemplate 和结构化输出。

这一篇开始进入 LangChain 里非常关键的一块:工具调用和 Agent。

本文对应代码位于 code/langchain-demo/chapter03。这一章依然保持独立目录,避免后续代码改动影响前面文章的示例。

本篇目标

在开始之前,先明确一下这篇要解决的问题。

前两篇里,模型主要是在“回答问题”。即使第二篇加了结构化输出,本质上也还是让模型按照格式生成内容。

但真实应用里,经常希望模型可以做一些自己不擅长,或者不应该靠猜的事情,比如:

  1. 计算两个数字相乘。
  2. 统计文本长度。
  3. 查询数据库。
  4. 读取文件。
  5. 调用某个业务接口。

这里就需要工具调用。

第三篇先不接数据库、文件系统或者网络搜索,只定义几个结果确定的小工具。这样能把注意力放在 LangChain 的工具调用机制上。

本篇主要完成两件事:

  1. 使用 bind_tools 观察模型如何提出工具调用。
  2. 使用 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.pyllm.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 可以识别的工具。

有几个点需要注意:

  1. 函数名会成为工具名。
  2. 函数参数会变成工具入参。
  3. 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 的默认问题里有两个任务:

  1. 计算 23 * 19
  2. 统计 LangChain tool calling 的字符数。

这两个任务正好分别对应 multiplycount_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"
  }
]

可以看到,模型没有直接计算,而是返回了两个工具调用请求:

  1. 调用 multiply,参数是 a=23b=19
  2. 调用 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,
        )
    )

这里的流程很清楚:

  1. 根据工具名找到真实工具对象。
  2. 把模型给出的参数传给工具。
  3. 得到工具执行结果。
  4. 把结果包装成 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 就比较容易了。

前面的手动流程里,我们自己写了这些步骤:

  1. 调用模型。
  2. 读取 tool_calls
  3. 执行工具。
  4. 把工具结果传回模型。
  5. 再让模型生成最终回答。

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 更底层:

  1. 把工具能力告诉模型。
  2. 模型返回 tool_calls
  3. 工具执行需要自己写代码。
  4. 更适合学习和调试工具调用流程。

create_agent 更完整:

  1. 把模型和工具组合起来。
  2. 自动执行工具调用循环。
  3. 自动把工具结果交回模型。
  4. 更适合写真正的业务流程。

所以这一篇先写 tool_calling_demo.py,再写 agent_demo.py

只有先看过手动工具调用过程,Agent 做了什么才不会显得太神秘。

常见问题

模型不调用工具怎么办

工具调用是模型自己决定的。

如果模型没有返回 tool_calls,可以从几个方向排查:

  1. 问题是否真的需要工具。
  2. 工具 docstring 是否描述清楚。
  3. system prompt 是否明确要求优先使用工具。
  4. 当前模型服务是否支持 tool calling。

本篇示例里使用的是计算和字符统计,目的就是让模型更容易判断应该调用工具。

为什么不直接让模型计算

简单数字模型也可能直接算对,但这不是重点。

工具调用的核心价值是:把确定性的事情交给确定性的工具。

比如计算、查数据库、调接口,这些事情不应该靠模型猜,而应该由程序执行。

模型更适合做的是理解用户意图、选择工具、组织最终回答。

第三章为什么不用真实外部工具

这一篇没有接文件、数据库和网络搜索,是刻意的。

因为第三章要先讲清楚工具调用机制本身。

如果一上来就接很多外部系统,读者很容易把注意力放在文件路径、接口鉴权、网络问题上,反而忽略了 LangChain 里的工具调用流程。

等工具调用理解清楚后,后面再接 RAG 或博客文章检索,会顺很多。

小结

这一篇完成了 LangChain 工具调用和 Agent 的入门实践:

  1. 使用 @tool 把普通 Python 函数包装成工具。
  2. 使用 bind_tools 把工具绑定到模型。
  3. 观察模型返回的 tool_calls
  4. 手动执行工具,并通过 ToolMessage 把结果交回模型。
  5. 使用 create_agent 自动完成工具调用流程。

到这里,模型就不只是生成文本了,它已经可以通过工具完成一些确定性的任务。

下一篇可以继续往 RAG 方向走,尝试把博客文章作为知识库,让模型基于本地文章内容回答问题。

参考

  1. https://docs.langchain.com/oss/python/langchain/overview
  2. https://docs.langchain.com/oss/python/langchain/agents
  3. https://python.langchain.com/api_reference/core/tools/langchain_core.tools.convert.tool.html
  4. https://python.langchain.com/api_reference/core/messages/langchain_core.messages.tool.ToolMessage.html

原创文章,转载请注明来源:    LangChain 入门学习第三篇-工具调用和 Agent