【从0搭建AI智能体·3】从「会聊天」到「能干活」:Function Calling 工具调用实战全解

📚 本文是《从 0 搭建你的 AI 智能体》专栏第 3 篇。
上一篇:《RAG 向量记忆实战》

标签:Function Calling Tool Use AI Agent 大模型 Python 智能体 LLM

📌 前言:大模型的「天生残疾」

再强大的大模型,本质上也只是个「文字接龙高手」。它有三个天生的短板:

  • 不知道实时信息:你问「今天天气怎样」「现在几点」「这单快递到哪了」,它只能瞎猜或告诉你不知道;
  • 不会精确计算:稍复杂的数学、金额计算,它经常一本正经地算错;
  • 动不了外部世界:它没法帮你查数据库、发邮件、下单、调用任何一个真实 API。

Function Calling(函数调用,也叫 Tool Use / 工具调用) 就是给大模型装上「手和脚」的技术。它让模型在需要时主动告诉你的程序:「请帮我调用某个函数,参数是这些」,程序执行后把结果喂回去,模型再据此组织最终回答。

这是「聊天机器人」进化成「智能体(Agent)」的决定性一步。本文从原理到完整代码彻底吃透它,文末附一个可直接运行的完整智能体脚本

💡 本文适合谁:能跑通大模型基础对话、想让 AI 真正调用外部能力干活的开发者。阅读约需 16 分钟。

目录

  1. Function Calling 到底怎么工作(附时序图)
  2. 它和「自己解析模型输出」有何本质区别
  3. 核心:工具的「说明书」怎么写
  4. tool_choice 四种取值,别再瞎用 auto
  5. 环境准备
  6. 完整实战:让 AI 学会查天气 + 算数学
  7. 逐行拆解四步调用流程
  8. 进阶:多工具协作与自动循环(ReAct 雏形)
  9. 工程化:错误兜底、参数校验、并行调用
  10. 常见坑与安全红线
  11. FAQ:读者最常问的 7 个问题
  12. 完整可运行 Demo
  13. 从 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"],           # 必填参数
        },
    },
}

🎯 写好说明书的三条黄金法则

  1. description说清「什么时候用」(如「当用户询问天气时」),而不只是「做什么」;
  2. 每个参数都写清含义和示例,减少模型传错值;
  3. 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 帮你干什么活~

Logo

中国智能体开发者社区,聚焦智能体与大模型开发,提供前沿资讯、实用工具链、开源项目及行业案例。通过技术沙龙、开发者大赛等活动,促进经验交流与协作,助力开发者快速构建创新智能应用。

更多推荐