【从0搭建AI智能体·3】从「会聊天」到「能干活」:智能体工具调用(Function Calling)实战全解
【从0搭建AI智能体·3】从「会聊天」到「能干活」:Function Calling 工具调用实战全解
📚 本文是《从 0 搭建你的 AI 智能体》专栏第 3 篇。
上一篇:《RAG 向量记忆实战》标签:
Function CallingTool UseAI Agent大模型Python智能体LLM
📌 前言:大模型的「天生残疾」
再强大的大模型,本质上也只是个「文字接龙高手」。它有三个天生的短板:
- 不知道实时信息:你问「今天天气怎样」「现在几点」「这单快递到哪了」,它只能瞎猜或告诉你不知道;
- 不会精确计算:稍复杂的数学、金额计算,它经常一本正经地算错;
- 动不了外部世界:它没法帮你查数据库、发邮件、下单、调用任何一个真实 API。
Function Calling(函数调用,也叫 Tool Use / 工具调用) 就是给大模型装上「手和脚」的技术。它让模型在需要时主动告诉你的程序:「请帮我调用某个函数,参数是这些」,程序执行后把结果喂回去,模型再据此组织最终回答。
这是「聊天机器人」进化成「智能体(Agent)」的决定性一步。本文从原理到完整代码彻底吃透它,文末附一个可直接运行的完整智能体脚本。
💡 本文适合谁:能跑通大模型基础对话、想让 AI 真正调用外部能力干活的开发者。阅读约需 16 分钟。
目录
- Function Calling 到底怎么工作(附时序图)
- 它和「自己解析模型输出」有何本质区别
- 核心:工具的「说明书」怎么写
tool_choice四种取值,别再瞎用 auto- 环境准备
- 完整实战:让 AI 学会查天气 + 算数学
- 逐行拆解四步调用流程
- 进阶:多工具协作与自动循环(ReAct 雏形)
- 工程化:错误兜底、参数校验、并行调用
- 常见坑与安全红线
- FAQ:读者最常问的 7 个问题
- 完整可运行 Demo
- 从 Function Calling 到真正的 Agent
① Function Calling 到底怎么工作(附时序图)
很多人以为「Function Calling = 大模型自己执行了函数」。大错特错。模型永远不会、也不能执行你的代码。它只负责「决策」,执行永远由你的程序完成。
完整流程是一个「四步握手」,用时序图看最清楚:
用户 你的程序 大模型
│ │ │
│ 提问 │ │
│────────────────►│ │
│ │ 问题 + 工具说明书 │
│ │────────────────────►│
│ │ │ 思考:需要查天气
│ │ 「请调用 │
│ │ get_weather │
│ │◄────────────────────│ city=北京」
│ │ │
│ 本地真正执行 │
│ get_weather("北京") │
│ 得到「25℃ 晴」 │
│ │ 把结果回传 │
│ │────────────────────►│
│ │ │ 组织自然语言
│ │ 最终回答 │
│ │◄────────────────────│
│ 「北京晴25℃, │ │
│ 适合出门」 │ │
│◄────────────────│ │
一句话记牢:模型是「大脑」,判断该用哪个工具、传什么参数;你的代码是「双手」,负责真正执行。 二者来回交互,缺一不可。
② 它和「自己解析模型输出」有何本质区别
有人会说:我让模型输出一段 JSON,自己 json.loads 解析不也行?能行,但脆弱。对比一下:
| 维度 | 自己让模型吐 JSON 再解析 | Function Calling |
|---|---|---|
| 输出稳定性 | 模型可能多说废话、格式跑偏,解析常崩 | 平台层保证结构化,稳定 |
| 参数校验 | 全靠自己写正则/兜底 | Schema 约束类型与必填项 |
| 多工具选择 | 要自己写一堆 if-else 判断意图 | 模型自动选,还能一次选多个 |
| 是否要调用 | 难判断「这次到底要不要调」 | 模型显式给 tool_calls,没有就是纯聊天 |
结论:能用 Function Calling 就别手撸 JSON 解析,省下的是无穷无尽的兜底代码。
③ 核心:工具的「说明书」怎么写
要让模型知道有哪些工具可用,得用结构化的 JSON Schema「说明书」描述每个工具。模型全靠它判断「该不该用、怎么用」,所以描述写得好不好,直接决定调用准不准。
weather_tool = {
"type": "function",
"function": {
"name": "get_weather", # 函数名,与你的实际函数对应
"description": "查询指定城市的实时天气。当用户询问天气、气温、是否下雨时使用。",
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "城市名称,例如「北京」「上海」",
}
},
"required": ["city"], # 必填参数
},
},
}
🎯 写好说明书的三条黄金法则:
description要说清「什么时候用」(如「当用户询问天气时」),而不只是「做什么」;- 每个参数都写清含义和示例,减少模型传错值;
- 用
required明确必填项,避免模型漏传关键参数。
⚠️ 工具说明书会占 Token(每次请求都带上)。工具别贪多,十几个是舒适区,几十上百个会稀释模型注意力、也烧钱。工具太多时可先做一轮「工具路由」筛选。
④ tool_choice 四种取值,别再瞎用 auto
调用时的 tool_choice 参数控制模型「能不能/必须不必须」用工具,很多人只知道 auto:
| 取值 | 含义 | 典型场景 |
|---|---|---|
"auto" |
模型自己决定用不用、用哪个 | 通用对话助手(最常用) |
"none" |
禁止调用,只输出文字 | 只想让它总结、不想触发副作用时 |
"required" |
必须至少调用一个工具 | 你确定这一步就是要查数据,不许闲聊 |
| 指定某工具 | 强制调用指定函数 | 表单抽取、固定流程节点 |
指定某工具的写法:
tool_choice = {"type": "function", "function": {"name": "get_weather"}}
⑤ 环境准备
python -m venv agent_env
source agent_env/bin/activate # Windows: agent_env\Scripts\activate
pip install openai python-dotenv
.env(沿用系列一贯规范,密钥绝不硬编码):
AGENT_KEY=sk-your-actual-key-here
API_BASE_URL=https://api.example.com/v1
import os, json
from dotenv import load_dotenv
from openai import OpenAI
load_dotenv()
client = OpenAI(api_key=os.getenv("AGENT_KEY"), base_url=os.getenv("API_BASE_URL"))
MODEL = "standard-agent-v1" # 换成你使用的、支持 tools 的模型
⚠️ 不是所有模型都支持 Function Calling。调用前确认你用的模型开启了
tools能力。
⑥ 完整实战:让 AI 学会查天气 + 算数学
先定义真正干活的本地函数:
def get_weather(city):
"""真实场景应调气象 API,这里用假数据演示"""
fake_db = {"北京": "晴,25℃", "上海": "多云,28℃", "广州": "雷阵雨,31℃"}
return fake_db.get(city, f"暂无 {city} 的天气数据")
def calculate(expression):
"""安全计算数学表达式(用 AST,杜绝任意代码执行,见第 ⑩ 节)"""
import ast, operator
ops = {ast.Add: operator.add, ast.Sub: operator.sub,
ast.Mult: operator.mul, ast.Div: operator.truediv, ast.USub: operator.neg}
def _ev(node):
if isinstance(node, ast.Constant): return node.value
if isinstance(node, ast.BinOp): return ops[type(node.op)](_ev(node.left), _ev(node.right))
if isinstance(node, ast.UnaryOp): return ops[type(node.op)](_ev(node.operand))
raise ValueError("不支持的表达式")
try:
return str(_ev(ast.parse(expression, mode="eval").body))
except Exception as e:
return f"计算错误: {e}"
# 工具名 → 真实函数 的映射,方便按名调用
AVAILABLE_FUNCTIONS = {"get_weather": get_weather, "calculate": calculate}
再写两份工具说明书:
TOOLS = [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "查询指定城市的实时天气。用户问天气、气温、下不下雨时使用。",
"parameters": {
"type": "object",
"properties": {"city": {"type": "string", "description": "城市名,如「北京」"}},
"required": ["city"],
},
},
},
{
"type": "function",
"function": {
"name": "calculate",
"description": "计算数学表达式。涉及加减乘除等精确计算时使用。",
"parameters": {
"type": "object",
"properties": {"expression": {"type": "string", "description": "如「(3+5)*2」"}},
"required": ["expression"],
},
},
},
]
⑦ 逐行拆解四步调用流程
把「四步握手」落地成代码,注释标出每一步:
def run_agent(user_question):
messages = [{"role": "user", "content": user_question}]
# ===== 第 1 步:问题 + 工具清单发给模型 =====
response = client.chat.completions.create(
model=MODEL, messages=messages, tools=TOOLS, tool_choice="auto",
)
msg = response.choices[0].message
# ===== 第 2 步:判断模型是否要求调用工具 =====
if not msg.tool_calls:
return msg.content # 不需要工具(比如只是打招呼),直接就是答案
messages.append(msg) # 把模型「要求调用工具」这条消息加入历史
# ===== 第 3 步:本地真正执行每个被请求的工具 =====
for tool_call in msg.tool_calls:
fn_name = tool_call.function.name
fn_args = json.loads(tool_call.function.arguments) # 模型给的参数是 JSON 字符串
print(f"[模型决定调用] {fn_name}({fn_args})")
result = AVAILABLE_FUNCTIONS[fn_name](**fn_args) # 按名调用真实函数
# 执行结果作为 role=tool 的消息回传,必须带 tool_call_id
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": str(result),
})
# ===== 第 4 步:带着工具结果再问一次,让模型组织最终回答 =====
final = client.chat.completions.create(model=MODEL, messages=messages)
return final.choices[0].message.content
if __name__ == "__main__":
print(run_agent("北京今天天气怎么样?适合出门吗?"))
print(run_agent("帮我算一下 (128 + 72) * 3 等于多少"))
print(run_agent("你好呀")) # 不需要工具,模型直接回答
运行效果大致:
[模型决定调用] get_weather({'city': '北京'})
北京今天晴,25℃,很适合出门~
[模型决定调用] calculate({'expression': '(128 + 72) * 3'})
(128 + 72) * 3 = 600。
你好呀!有什么我可以帮你的吗?
同一段代码,模型自己判断该查天气、该算数、还是直接聊天——这就是 Function Calling 的威力。
⑧ 进阶:多工具协作与自动循环(ReAct 雏形)
真实任务常需连续调用多个工具。比如「北京和广州哪个更热?把更热那个的气温数字加 10」——模型可能先查两次天气、再算一次。
这就要把「四步」变成自动循环:只要模型还想调工具,就继续执行并回传,直到它给出最终文字答案。
def run_agent_loop(user_question, max_steps=5):
messages = [{"role": "user", "content": user_question}]
for step in range(max_steps): # max_steps 防止无限循环
resp = client.chat.completions.create(
model=MODEL, messages=messages, tools=TOOLS, tool_choice="auto"
)
msg = resp.choices[0].message
if not msg.tool_calls:
return msg.content # 模型给出最终答案,结束循环
messages.append(msg)
for tool_call in msg.tool_calls: # 一次可能要求调多个工具,逐个执行
fn = AVAILABLE_FUNCTIONS[tool_call.function.name]
args = json.loads(tool_call.function.arguments)
result = fn(**args)
messages.append({
"role": "tool", "tool_call_id": tool_call.id, "content": str(result),
})
return "已达到最大工具调用次数,请简化问题后重试。"
💡
max_steps是必须的安全阀:万一模型陷入「反复调工具」的怪圈,没有步数上限就会无限烧钱、卡死。这个循环其实就是 ReAct 模式(推理→行动→观察→再推理)的最简实现,LangChain 的 Agent 内核就是它。
⑨ 工程化:错误兜底、参数校验、并行调用
第 ⑦⑧ 节的代码是「happy path」,上生产还差三层防护。把执行工具那段抽成一个健壮的函数:
def execute_tool_call(tool_call):
"""健壮地执行单个工具调用,任何异常都转成结果回传,绝不让主流程崩"""
fn_name = tool_call.function.name
# ① 参数解析容错:模型偶尔给出非法 JSON
try:
fn_args = json.loads(tool_call.function.arguments)
except json.JSONDecodeError:
return {"role": "tool", "tool_call_id": tool_call.id,
"content": "参数不是合法 JSON,请修正后重试"}
# ② 工具名校验:模型可能幻觉出不存在的工具
fn = AVAILABLE_FUNCTIONS.get(fn_name)
if fn is None:
return {"role": "tool", "tool_call_id": tool_call.id,
"content": f"工具 {fn_name} 不存在"}
# ③ 执行异常兜底:把错误信息回传,让模型自行应对/换方案
try:
result = fn(**fn_args)
except Exception as e:
result = f"执行出错: {e}"
return {"role": "tool", "tool_call_id": tool_call.id, "content": str(result)}
关于并行:模型一次可能返回多个 tool_call(比如同时查两个城市的天气)。它们彼此独立,可以并发执行以提速:
from concurrent.futures import ThreadPoolExecutor
def execute_all(tool_calls):
"""并发执行多个独立工具调用,缩短总耗时"""
with ThreadPoolExecutor(max_workers=4) as pool:
return list(pool.map(execute_tool_call, tool_calls))
🎯 三层防护缺一不可:非法 JSON、不存在的工具、执行报错——线上这三种情况每天都在发生。把错误信息回传给模型而非直接抛异常,模型往往能自己纠偏重试。
⑩ 常见坑与安全红线
| 坑 / 风险 | 说明 | 正确做法 |
|---|---|---|
| 🚨 直接 eval 输入 | eval 能执行任意代码,等于把服务器交给外人 |
用 ast 解析(第 ⑥ 节)或专用库(sympy/numexpr) |
忘记回传 tool_call_id |
漏了 id,模型无法对应结果,直接报错 | 每条 tool 结果必须带对应 id |
| 参数解析崩溃 | 模型偶尔给非法 JSON | json.loads 包 try(第 ⑨ 节) |
| 工具报错直接抛出 | 一个工具挂了整个流程崩 | 捕获异常,把错误当结果回传 |
| 没有步数上限 | 多工具循环可能无限调用 | 设 max_steps |
| 高权限工具无校验 | 模型可能被诱导调「删库/转账」 | 危险操作加二次确认 / 人工审批 / 权限隔离 |
| 工具太多 | 说明书稀释注意力、烧 Token | 控制在十几个内,多了先做工具路由 |
🔴 安全红线:只要工具能触碰真实世界(改数据、花钱、发消息),就必须假设「模型可能被恶意提示词诱导误调用」。权限最小化 + 危险操作人工确认,是不可省的防线。举例:一个「发送邮件」工具,绝不能让模型全自动触发,必须弹给用户确认收件人和内容。
⑪ FAQ:读者最常问的 7 个问题
Q1:Function Calling 和 MCP 是什么关系?
Function Calling 是「模型请求调工具」的底层机制;MCP(Model Context Protocol)是把工具/数据源标准化封装、便于复用的协议。可以理解为 MCP 在 Function Calling 之上做了工程化封装。
Q2:模型会不会乱调工具?
会。所以要靠好的 description(讲清何时用)+ tool_choice 约束 + 危险操作人工确认三重把关。
Q3:一次能调几个工具?
支持模型在一轮里返回多个 tool_call(并行),也支持跨多轮连续调用(串行)。第 ⑧⑨ 节分别覆盖。
Q4:工具执行很慢会怎样?
整个对话会卡在等待工具返回。慢工具建议加超时、并发执行(第 ⑨ 节),或改成异步任务 + 轮询。
Q5:能和流式输出一起用吗?
能,但更复杂:流式下 tool_call 的参数是分片到达的,要把碎片拼完整再执行。建议先用非流式跑通逻辑,再上流式。
Q6:工具返回的数据太长(比如查出几千条)怎么办?
别整坨塞回模型(爆 Token)。在工具内部先做摘要/分页/字段裁剪,只回传关键部分。
Q7:怎么调试模型「为什么没调工具」?
打印完整的 response.choices[0].message,看是 tool_calls 为空(说明模型觉得不需要,多半是 description 没写清),还是参数传错了。
⑫ 完整可运行 Demo
整合成一个能直接跑的健壮智能体(配好 .env 即可):
import os, json
from dotenv import load_dotenv
from openai import OpenAI
load_dotenv()
client = OpenAI(api_key=os.getenv("AGENT_KEY"), base_url=os.getenv("API_BASE_URL"))
MODEL = "standard-agent-v1"
def get_weather(city):
return {"北京": "晴,25℃", "广州": "雷阵雨,31℃"}.get(city, f"暂无{city}数据")
def calculate(expr):
import ast, operator
ops = {ast.Add: operator.add, ast.Sub: operator.sub,
ast.Mult: operator.mul, ast.Div: operator.truediv,
ast.USub: operator.neg} # 支持负数,如 "-5+10"
def ev(n):
if isinstance(n, ast.Constant): return n.value
if isinstance(n, ast.BinOp): return ops[type(n.op)](ev(n.left), ev(n.right))
if isinstance(n, ast.UnaryOp): return ops[type(n.op)](ev(n.operand))
raise ValueError("不支持")
try: return str(ev(ast.parse(expr, mode="eval").body))
except Exception as e: return f"计算错误:{e}"
FUNCS = {"get_weather": get_weather, "calculate": calculate}
TOOLS = [
{"type": "function", "function": {"name": "get_weather",
"description": "查城市天气,用户问天气时用",
"parameters": {"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]}}},
{"type": "function", "function": {"name": "calculate",
"description": "算数学表达式,需精确计算时用",
"parameters": {"type": "object", "properties": {"expr": {"type": "string"}}, "required": ["expr"]}}},
]
def agent(question, max_steps=5):
messages = [{"role": "user", "content": question}]
for _ in range(max_steps):
msg = client.chat.completions.create(
model=MODEL, messages=messages, tools=TOOLS, tool_choice="auto"
).choices[0].message
if not msg.tool_calls:
return msg.content
messages.append(msg)
for tc in msg.tool_calls:
try:
args = json.loads(tc.function.arguments)
out = FUNCS[tc.function.name](**args)
except Exception as e:
out = f"出错:{e}"
messages.append({"role": "tool", "tool_call_id": tc.id, "content": str(out)})
return "超出最大步数"
if __name__ == "__main__":
print(agent("北京和广州哪个更热?把更热那个的气温数字加10是多少?"))
⑬ 从 Function Calling 到真正的 Agent
Function Calling 是「智能体」的引擎,但一个完整 Agent 还要在它之上叠几层:
- 规划(Planning):把复杂任务拆成多步(ReAct、Plan-and-Execute);
- 记忆(Memory):结合上一篇的 RAG,让 Agent 记住历史与知识;
- 工具生态:接入搜索、代码执行、数据库、第三方 API;
- 反思(Reflection):执行后自我检查、纠错重试。
你在本文写的 run_agent_loop,本质就是最简版 ReAct 循环(推理→行动→观察→再推理)。LangChain、LlamaIndex 封装的「Agent」,内核正是这套逻辑,只是加了更多工程化能力。理解了本文,你再看那些框架就会豁然开朗——它们只是把这个循环包装得更漂亮而已。
| 关键点 | 一句话记住 |
|---|---|
| 本质 | 模型只做「决策」,执行永远在你的代码里 |
| 说明书 | description 决定调用准不准,重点写「何时用」 |
| 四步流程 | 发问+工具 → 模型请求调用 → 本地执行 → 回传生成答案 |
| 多工具 | 带 max_steps 的循环支持连续调用,独立调用可并发 |
| 工程化 | 非法JSON、假工具、执行报错,三层兜底 |
| 安全 | 拒绝裸 eval,危险操作必须人工确认 |
Function Calling 让大模型从「只会说」变成「能动手」,是构建实用 AI 应用的分水岭。配合上一篇的 RAG 记忆,你已掌握智能体的两大核心引擎——记忆与行动。剩下的,就是发挥想象力去组合它们。
🔜 下一篇预告:《流式输出全链路:后端 SSE + 前端打字机效果》——让你的智能体用起来像 ChatGPT 一样丝滑。
👍 如果本文帮到你,点赞 / 收藏 / 关注,追更不迷路。评论区聊聊你想让 Agent 帮你干什么活~
更多推荐


所有评论(0)