讲解模型如何通过工具目录调用 API 和函数的底层原理,展示 JSON 工具定义和调用流程。这是构建 AI Agent 的核心技能。
正如我们所看到的,模型在概念上其实非常简单:输入一组消息,模型反复预测下一个 token,最终形成输出消息。它无法访问互联网、你的数据库或 Slack,也无法可靠地完成算术运算,甚至无法告诉你单词 strawberry 中有几个字母 r(一个令人尴尬的例子)——还记得第一部分提到的吗?模型看到的从来不是字母,而只是 token ID。
从这个角度来看,模型就像一个没有身体的大脑。它可以推理问题、描述需要采取的步骤,甚至能背诵莎士比亚的所有作品。但没有身体,模型就无法执行任何操作。工具正是为此而生,而构建工具的过程,其实与普通的软件工程非常相似。
工具就是某种编程语言中的函数或方法。工具调用在底层的具体工作方式取决于模型,不过我们可以先从概念层面梳理一下。首先,需要提供一份模型可以使用的工具目录,通常是一个 json 列表。在 API 中,你会通过一个单独的 tools 参数传入这份目录,然后由服务提供商替你将其注入模型:
[
{
"name": "multiply",
"description": "Multiplies two numbers and returns the result.",
"parameters": {
"type": "object",
"properties": {
"a": {
"type": "number",
"description": "The first number."
},
"b": {
"type": "number",
"description": "The second number."
}
},
"required": ["a", "b"]
}
},
[..more tools..]
]
如果模型决定使用某个工具,它输出的就会是一条结构化消息,就像我们在第 2 篇文章中看到的那样:
{
"name": "multiply",
"arguments": {
"a": 24,
"b": 57
}
}
作为 AI 工程师,你的工作就是接收这类输出,并将其传递给真正执行操作的(Python)函数。然后,你再返回一条结构化消息:
Tool result (multiply):
{
"result": 1368
}
现在,工具返回的结果已经成为模型 context window 的一部分,因此可以用于后续推理。
那么,模型是怎么学会这一切的?模型接受过大量工具调用记录的训练,因此,请求工具并读取返回结果,是它已经见过许多次的一种模式。你的工具目录则是其中可变的部分,也可以说是运行时配置。接下来,我们来看一个真实示例。
支付运营需要确定性的算术计算,而模型并不是可靠的计算器。创建 app/tools.py,并创建一个空的 app/__init__.py,这样我们就可以导入自己的工具。我们将创建一个用于计算退款成本的工具。
from langchain_core.tools import tool
@tool
def calculate_refund_cost(
original_charge_eur: float,
refund_amount_eur: float,
processing_fee_pct: float,
processing_fee_fixed_eur: float,
refund_admin_fee_eur: float = 0.25,
) -> dict:
"""Calculate what refunding a payment actually costs the merchant.
Use this whenever the user gives you (a) the original charge amount
and (b) the payment method's fee structure (percentage plus fixed fee
per transaction). Do not guess the fee structure yourself if the user
hasn't provided it.
Processing fees paid on the original charge are NOT returned by the
processor when you refund, and most processors charge a small admin
fee per refund on top.
The returned total_cost_of_refund_eur represents the merchant's total
out-of-pocket cost after the refund is processed:
refunded amount + non-refundable original processing fee + refund admin fee.
Limitations:
- Assumes the original payment processing fee is fully retained by the
processor after a refund. Some processors, payment methods, or regions
may have different refund fee policies.
- Does not account for currency conversion costs, exchange-rate changes,
taxes, accounting impacts, subscription adjustments, or other
business-specific costs.
Args:
original_charge_eur: The amount of the original charge.
refund_amount_eur: The amount being refunded (full or partial).
processing_fee_pct: Percentage fee on the original charge,
e.g. 1.8 for 1.8%.
processing_fee_fixed_eur: Fixed fee on the original charge.
refund_admin_fee_eur: Processor's per-refund admin fee, default
€0.25.
Returns:
A dictionary containing:
- refund_amount_eur: The amount sent back to the customer.
- processing_fee_eur: The original payment processing
fee that the merchant still pays after issuing the refund.
- total_cost_of_refund_eur: The merchant's total out-of-pocket cost.
This includes the money returned to the customer and the processing fees.
"""
processing_fee = (
original_charge_eur * processing_fee_pct / 100
+ processing_fee_fixed_eur
)
total = refund_amount_eur + refund_admin_fee_eur + processing_fee
return {
"refund_amount_eur": round(refund_amount_eur, 2),
"processing_fee_eur": round(processing_fee, 2),
"total_cost_of_refund_eur": round(total, 2),
}
Docstring 一直都是帮助程序员决定该使用哪个函数的重要说明,现在,它们也为模型选择工具发挥着同样的作用。模型真的会阅读这些文档,因此可以说,docstring 比以往任何时候都更加重要。
编写工具 docstring 时有三个建议:
说清楚何时使用,而不只是说明它做什么。“当用户提供了 (a)……和 (b)……时使用此工具”——这就是帮助模型在不同工具之间进行选择的路由逻辑。如果 docstring 只描述计算过程,却不说明何时使用,那么工具是否会被正确调用就只能听天由命了。
说清楚何时使用,而不只是说明它做什么。“当用户提供了 (a)……和 (b)……时使用此工具”——这就是帮助模型在不同工具之间进行选择的路由逻辑。如果 docstring 只描述计算过程,却不说明何时使用,那么工具是否会被正确调用就只能听天由命了。
设定边界。“不要自行猜测费率结构”可能是整个工具定义中最关键的逻辑。虚构费率结构会严重影响计算结果。另一个例子是:即使用户不断要求给出一个更理想的答案,也要明确告诉模型不得低估成本。缺少这类边界的 Assistant 已经登上过真实世界的新闻:一家汽车经销商的 chatbot 被诱导“同意”以一美元的价格出售一辆 Chevy Tahoe;Air Canada 也因为其 chatbot 编造的退款政策,被法院判定需要承担责任。边界非常重要。
设定边界。“不要自行猜测费率结构”可能是整个工具定义中最关键的逻辑。虚构费率结构会严重影响计算结果。另一个例子是:即使用户不断要求给出一个更理想的答案,也要明确告诉模型不得低估成本。缺少这类边界的 Assistant 已经登上过真实世界的新闻:一家汽车经销商的 chatbot 被诱导“同意”以一美元的价格出售一辆 Chevy Tahoe;Air Canada 也因为其 chatbot 编造的退款政策,被法院判定需要承担责任。边界非常重要。
坦率说明局限。每段逻辑都有自己的假设。例如,我们这个简单的退款工具假设使用欧元,并且没有考虑涉及多种货币的跨境支付。明确说明这些局限很有用,因为这可以防止模型过度信任计算结果。这些限制通常也会在输出中一并传达给用户。
坦率说明局限。每段逻辑都有自己的假设。例如,我们这个简单的退款工具假设使用欧元,并且没有考虑涉及多种货币的跨境支付。明确说明这些局限很有用,因为这可以防止模型过度信任计算结果。这些限制通常也会在输出中一并传达给用户。
让我们试试新的 calculate_refund_cost 工具。创建 03_tool_manual.py,导入刚刚编写的工具,再使用 bind_tools 将它绑定到模型:
from dotenv import load_dotenv
from langchain.chat_models import init_chat_model
from app.tools import calculate_refund_cost
import pprint
load_dotenv()
model = init_chat_model("anthropic:claude-sonnet-5")
model_with_tools = model.bind_tools(
[calculate_refund_cost]
)
question = ("Customer paid €480 on a European consumer card (1.8% + €0.25). "
"Full refund - what does it cost us?")
msg = model_with_tools.invoke(question)
pprint.pp(msg)
你会看到类似下面的内容(为了方便阅读,我截短了其中较长的文本):
$ python3 03_tool_manual.py
AIMessage(
content=[...],
tool_calls=[
{
"name": "calculate_refund_cost",
"args": {
"original_charge_eur": 480,
"refund_amount_eur": 480,
"processing_fee_pct": 1.8,
"processing_fee_fixed_eur": 0.25,
},
"id": "toolu_011GofEYU4Vz1...",
"type": "tool_call",
}
],
invalid_tool_calls=[],
usage_metadata={
"input_tokens": 1259,
"output_tokens": 224,
"total_tokens": 1483,
"input_token_details": {[...]},
},
)
这个输出并不是对问题的回答,而是一个工具请求。你的应用逻辑应该检查 tool_calls 是否已设置;如果已设置,就调用相应工具,然后将结果传回模型,让模型继续执行。请注意,tool_calls 是一个列表:模型可以在一轮对话中请求多个工具,因此应该始终遍历它,而不是只取第一项。
下面的代码片段 03_tool_manual_2.py 会与模型进行两轮交互。首先,我们把问题传给模型,模型决定返回一个工具请求。然后,我们用工具执行结果进行响应,模型再给出最终答案。
from dotenv import load_dotenv
from langchain.chat_models import init_chat_model
from langchain_core.messages import ToolMessage
from app.tools import calculate_refund_cost
load_dotenv()
model = init_chat_model("anthropic:claude-sonnet-5")
model_with_tools = model.bind_tools([calculate_refund_cost])
question = (
"Customer paid €480 on a European consumer card (1.8% + €0.25). "
"Full refund - what does it cost us?"
)
msg = model_with_tools.invoke(question)
print(msg)
tool_map = {
"calculate_refund_cost": calculate_refund_cost
}
tool_messages = []
for tool_call in msg.tool_calls:
# retrieve function by name
tool = tool_map[tool_call["name"]]
# call the function
result = tool.invoke(tool_call["args"])
# store result
tool_messages.append(
ToolMessage(content=str(result), tool_call_id=tool_call["id"])
)
# give all messages as input, including the tool messages
next_msg = model_with_tools.invoke(
[{"role": "user", "content": question}, msg, *tool_messages]
)
print(next_msg.content)
专业建议:如果工具抛出异常,不要让异常直接逸出。捕获它,并将错误文本作为 ToolMessage 的内容传回模型。这样,模型就可以修正参数,或者向用户解释问题;而未处理的异常只会直接终止这一轮交互。
$ python3 03_tool_manual_2.py
Here's the breakdown for a full refund on the €480 charge
(European consumer card, 1.8% + €0.25):
| Item | Amount |
|-------------------------------------------|--------:|
| Refunded to customer | €480.00 |
| Original processing fee (non-refundable) | €8.89 |
| Refund admin fee | €0.25 |
| **Total out-of-pocket cost** | **€489.14** |
So issuing the full refund actually costs you **€489.14**:
- €480.00 returned to the customer
- €8.89 original processing fee (not refunded)
- €0.25 refund processing/admin fee
我们的 calculate_refund_cost 工具只是一个简单的示例,但希望你已经看到了工具调用的强大之处。你可以创建工具来搜索互联网、查询数据库中的数据,甚至可以让它发送电子邮件或向队列写入消息。任何能够用普通 Python 代码完成的事情,现在都可以与模型集成。工具调用正是连接这两个世界的集成点。
接下来的两篇文章将深入介绍两类高级工具:通过 RAG 检索知识,以及通过 MCP 集成 API。你可能已经听说过这些由三个字母组成的缩写,接下来我们会了解它们究竟是什么意思。
如需采取进一步措施,你可以考虑屏蔽此人和/或举报滥用行为。