Function-Calling让大模型从会说到会干

    |     2026年8月16日   |   agent技术, AI大模型应用   |     0 条评论   |    19

引言

小明上周搞定了多轮对话,聊天机器人跑得有模有样。这周一上班,老板又丢来一个需求:

“咱们的 AI 客服能不能查订单状态?用户问’我的订单到哪了’,AI 直接回答,别让用户自己去系统里翻。”

小明心想这还不简单?给 AI 加条 system 消息:”你可以查询订单状态”。然后测试——

用户问:”我的订单 88899 到哪了?”

AI 回答:”您的订单 88899 已发货,预计明天送达,当前在成都转运中心。”

小明愣了。订单系统里根本没有这个订单号。AI 一本正经地编了一个答案。

他去找老张:”AI 又不老实了,我让它查订单,它直接编了个假的。”

老张说:”这叫幻觉。大模型的训练数据里没有你的订单系统,它不知道真实数据,就只能编。”

“那怎么办?让它连我的数据库?”

“大模型连不了你的数据库。但有个办法——Function Calling。让 AI 知道’有个查订单的函数可以调’,它需要查的时候就告诉你’调这个函数,参数是订单号 88899’,然后你的代码去真正查,把结果喂给它,它再基于真实数据回答用户。”

小明问:”所以 AI 自己不查,是让我去查?”

“对。AI 只是点菜,你的代码才是厨房。 今天就带你搞懂这个。”

核心认知:模型不执行函数,它只是”点菜”

老张在白板上画了个餐厅的流程:

用户:「来一份宫保鸡丁」
服务员(模型):「好的,厨房请做宫保鸡丁一份」(tool_call)
厨房(你的代码):真正炒菜,端出菜品(function result)
服务员(模型):「您的宫保鸡丁来了,请慢用」(最终回答)

“用户是顾客,大模型是服务员,你的代码是厨房。”老张说,”服务员不会炒菜,但他知道厨房有什么菜、每道菜需要什么食材。他负责把用户的需求翻译成’厨房听得懂的指令’,厨房做完菜再端给服务员,服务员最后给用户上菜。”

小明问:”很多人以为大模型能直接运行我的 Python 函数?”

“这是最常见的误解。”老张摇头,”大模型从头到尾不执行任何代码。它只做一件事——输出一个结构化的 JSON,告诉你’应该调哪个函数、参数是什么’。真正执行函数的是你的 Python 程序。”

分工是这样的:

角色 做什么
开发者(你) 定义工具列表(菜单)、实现真正的函数、编排对话循环
大模型 分析用户意图,决定调用哪个工具,输出结构化参数
用户 用自然语言提问

“明白了。”小明说,”大模型是个’翻译官’,把用户的自然语言翻译成函数调用指令。真正的活还是我干。”

“说到点子上了。”

两轮对话:Function Calling 的完整流程

老张说:”普通对话是一轮——你问,AI 答。Function Calling 至少两轮——第一轮 AI 决定用什么工具,第二轮 AI 基于工具结果生成回答。”

他在白板上画了张完整的流程图:

用户:"今天北京天气怎么样?"
    │
    ▼
┌─ 第1次 API 调用 ────────────────────────────┐
│  你发给模型:                  │
│  messages = [用户问题]             │
│  tools = [get_weather 函数定义]        │
│                         │
│  模型返回:                  │
│  tool_calls: get_weather(location="北京")   │
│  (注意:此时模型没有生成最终回答)      │
└───────────────────────────────────────────────┘
    │
    ▼
┌─ 你的代码执行函数 ──────────────────────────┐
│  调用 get_weather("北京")            │
│  返回:{"weather": "25°C 晴"}          │
└───────────────────────────────────────────────┘
    │
    ▼
┌─ 第2次 API 调用 ────────────────────────────┐
│  你发给模型:                  │
│  messages = [                │
│    用户问题,                  │
│    模型的 tool_calls 请求,          │
│    工具返回结果(role: "tool")         │
│  ]                       │
│                         │
│  模型返回:                  │
│  "北京今天晴天,25°C,适合外出。"       │
└───────────────────────────────────────────────┘
    │
    ▼
最终回答展示给用户

“看到了没?”老张说,”第一次调用模型不回答问题,而是告诉你’我需要调 get_weather 这个函数,参数是北京’。你执行完函数,把结果塞回 messages 列表,第二次调用模型才基于真实数据生成最终回答。”

小明问:”所以 messages 列表在这两轮里一直在变?”

“对。messages 是累积的——第一次调用后,你要把模型的 tool_calls 请求追加进去;函数执行完,再把 tool 结果追加进去。第二次调用时,messages 列表比第一次多了两条消息。”

“这就是上次讲多轮对话时说的’消息列表是累积的’——Function Calling 只是多了一种 tool 角色的消息而已。”

工具定义:给模型一份”菜单”

老张说:”模型要知道有哪些工具可用,你得提前告诉它。这就是 tools 定义——用 JSON Schema 格式描述每个函数的名字、用途和参数。”

tools = [
  {
    "type": "function",
    "function": {
      "name": "get_current_weather",       # 函数名,必须与实际函数一致
      "description": "获取指定城市当前的天气情况",  # 模型靠它判断何时调用
      "parameters": {              # JSON Schema 描述参数
        "type": "object",
        "properties": {
          "location": {
            "type": "string",
            "description": "城市名称,例如:北京"
          },
          "unit": {
            "type": "string",
            "enum": ["celsius", "fahrenheit"],
            "description": "温度单位"
          }
        },
        "required": ["location"]        # 必填参数
      }
    }
  }
]

老张逐字段解释:

  1. type: "function" — 固定写法,目前只有这一种类型
  2. name — 函数名,必须和你 Python 代码里的函数名完全一致
  3. description — 最关键字段。模型靠这段话判断”什么时候该调这个函数”
  4. parameters — JSON Schema 格式,描述参数的类型、枚举值、是否必填
  5. required — 哪些参数不能省略

“重点说这个 description。”老张敲了敲白板,”很多人写 description 就一句’获取天气’——太模糊了。模型不知道什么场景该调。应该写清楚做什么 + 什么时候用。”

写法 效果
"获取天气" 太模糊,模型可能在该调时不调,不该调时乱调
"获取指定城市当前的天气情况" 清晰,模型知道用户问天气时该调
"查询天气信息,支持中国城市,返回实时数据" 更好,连适用范围都说了

“还有参数的 description 也要写好。location 写’城市名称’不如写’城市名称,例如:北京’——给个示例值,模型提取参数的准确率会高很多。”

小明问:”那如果我有多个工具呢?”

“往 tools 列表里加就行。模型会根据用户的问题,自己判断该调哪个——或者一个都不调。”

tools = [
  {"type": "function", "function": {"name": "get_current_weather", ...}},
  {"type": "function", "function": {"name": "query_order_status", ...}},
  {"type": "function", "function": {"name": "search_product", ...}},
]

“就像菜单上有多道菜,服务员根据顾客的需求决定传哪张单子到厨房。”

核心代码:两轮调用的完整实现

老张说:”概念讲完了,上代码。这是最小可运行版本,去掉注释不到 40 行。”

import json
from config import MODEL, get_client

# ===== 第一步:定义工具函数(厨房真正做的事)=====
def get_current_weather(location: str, unit: str = "celsius") -> dict:
  """模拟天气查询(实际项目里这里调真实天气API)"""
  weather_data = {
    "北京": {"weather": "晴", "temperature": "25°C"},
    "上海": {"weather": "多云", "temperature": "28°C"},
    "成都": {"weather": "阴", "temperature": "30°C"},
  }
  return weather_data.get(location, {"weather": "未知", "temperature": "未知"})

# ===== 第二步:定义工具列表(菜单)=====
tools = [
  {
    "type": "function",
    "function": {
      "name": "get_current_weather",
      "description": "获取指定城市当前的天气情况",
      "parameters": {
        "type": "object",
        "properties": {
          "location": {
            "type": "string",
            "description": "城市名称,例如:北京"
          },
          "unit": {
            "type": "string",
            "enum": ["celsius", "fahrenheit"],
            "description": "温度单位"
          }
        },
        "required": ["location"]
      }
    }
  }
]

# 函数名 → 实际函数的映射表
available_functions = {
  "get_current_weather": get_current_weather,
}

# ===== 第三步:编排两轮对话 =====
def run_conversation(user_message: str):
  client = get_client()
  messages = [{"role": "user", "content": user_message}]

  # ===== 第1次调用:模型决定是否用工具 =====
  response = client.chat.completions.create(
    model=MODEL,
    messages=messages,
    tools=tools,
    tool_choice="auto",   # 让模型自行决定
  )
  response_message = response.choices[0].message
  tool_calls = response_message.tool_calls

  if not tool_calls:
    # 模型认为不需要工具,直接回复了
    print(response_message.content)
    return

  # 把模型的 tool_calls 请求加入历史
  messages.append(response_message)

  # ===== 执行每个工具调用 =====
  for tool_call in tool_calls:
    func_name = tool_call.function.name
    func_args = json.loads(tool_call.function.arguments)

    # 找到并执行本地函数
    func_result = available_functions[func_name](**func_args)

    # 把结果以 tool 角色追加到 messages
    messages.append({
      "role": "tool",
      "tool_call_id": tool_call.id,
      "name": func_name,
      "content": json.dumps(func_result, ensure_ascii=False),
    })

  # ===== 第2次调用:基于工具结果生成最终回答 =====
  final = client.chat.completions.create(
    model=MODEL,
    messages=messages,
  )
  print(final.choices[0].message.content)

# ===== 运行 =====
run_conversation("今天北京天气怎么样?")

老张说:”看着长,拆开就三步。逐段讲。”

逐段拆解

第一步:定义工具函数

def get_current_weather(location: str, unit: str = "celsius") -> dict:
  weather_data = {
    "北京": {"weather": "晴", "temperature": "25°C"},
    ...
  }
  return weather_data.get(location, ...)

“这就是’厨房’——真正干活的函数。函数名、参数名必须和 tools 定义里完全一致。参数名不一致,模型传过来的值你接不住。”

“这里用假数据模拟。实际项目里,这个函数内部会调真实的天气 API、查数据库、或者读文件。但对外接口不变——接收参数,返回 dict。”

第二步:定义工具列表和映射表

tools = [...]  # 告诉模型有哪些工具

available_functions = {
  "get_current_weather": get_current_weather,
}

“tools 是给模型看的’菜单’,available_functions 是给你的代码用的’路由表’——模型说’调 get_current_weather’,你通过这张表找到真正的 Python 函数并执行。”

“为什么不直接用函数名调?因为模型返回的是字符串 'get_current_weather',不是函数对象。你需要一个映射把字符串转成可调用的函数。”

第三步:第一轮调用——模型决定用什么工具

response = client.chat.completions.create(
  model=MODEL,
  messages=messages,
  tools=tools,      # 把菜单传给模型
  tool_choice="auto",   # 让模型自己决定要不要调
)
response_message = response.choices[0].message
tool_calls = response_message.tool_calls

“注意这里比普通调用多了两个参数:toolstool_choicetools 是工具列表,tool_choice 控制模型的行为——auto 是让模型自己判断要不要调。”

“如果模型认为不需要工具,tool_calls 就是 Noneresponse_message.content 直接就是最终回答。如果模型认为需要工具,contentNonetool_calls 里有调用请求。”

if not tool_calls:
  print(response_message.content)  # 不需要工具,直接回答
  return

messages.append(response_message)  # 把 tool_calls 请求加入历史

这行 messages.append(response_message) 很关键。 模型的 tool_calls 请求也是一条 assistant 消息,必须存入历史。不然第二轮调用时,模型不知道自己之前要求调什么函数。”

第四步:执行函数,把结果喂回去

for tool_call in tool_calls:
  func_name = tool_call.function.name
  func_args = json.loads(tool_call.function.arguments)

  func_result = available_functions[func_name](**func_args)

  messages.append({
    "role": "tool",
    "tool_call_id": tool_call.id,
    "name": func_name,
    "content": json.dumps(func_result, ensure_ascii=False),
  })

老张指着这段说:”这是 Function Calling 最核心的四行。”

  1. func_name — 模型告诉你调哪个函数
  2. func_args — 模型给你的参数,是 JSON 字符串,json.loads() 转成 dict
  3. func_result — 通过映射表找到函数,用 **func_args 解包参数执行
  4. messages.append(...) — 把执行结果包成 tool 消息塞回历史

“重点说这个 tool 消息。”老张画了个框:

{
  "role": "tool",        # 固定值
  "tool_call_id": tool_call.id, # 关联模型的调用请求
  "name": func_name,      # 哪个函数返回的
  "content": '{"weather": "晴", "temperature": "25°C"}'  # 结果,必须是字符串
}

tool_call_id必须的——它把模型的调用请求和你的执行结果关联起来。模型可能同时调多个函数,每个函数的结果要用对应的 id 标记,模型才知道哪个结果对应哪个请求。”

content 必须是字符串。函数返回的是 dict,json.dumps() 转成 JSON 字符串。ensure_ascii=False 保证中文不被转成 \uXXXX。”

第五步:第二轮调用——模型基于真实数据生成回答

final = client.chat.completions.create(
  model=MODEL,
  messages=messages,
)
print(final.choices[0].message.content)

“第二次调用不需要传 tools——因为不需要模型再决定调不调函数了。只需要把完整的 messages 历史(包含工具结果)传过去,让模型基于真实数据生成自然语言回答。”

“这轮的 messages 列表长这样:”

[
  {"role": "user", "content": "今天北京天气怎么样?"},
  # 模型的 tool_calls 请求(第1轮返回的 assistant 消息)
  {
    "role": "assistant",
    "content": None,
    "tool_calls": [{
      "id": "call_abc123",
      "function": {
        "name": "get_current_weather",
        "arguments": '{"location": "北京"}'
      }
    }]
  },
  # 你执行函数后的结果(tool 消息)
  {
    "role": "tool",
    "tool_call_id": "call_abc123",
    "name": "get_current_weather",
    "content": '{"weather": "晴", "temperature": "25°C"}'
  }
]

“模型看到这条 tool 消息,就知道’北京天气是晴,25°C’,然后生成最终回答。这就是从’编造’到’基于真实数据’的关键跨越。

跑起来看看

小明运行了程序:

python main.py
北京今天晴天,气温25°C,适合外出活动。

小明又试了几个问题:

run_conversation("上海天气怎么样?")
# 输出:上海今天多云,气温28°C。

run_conversation("你好,请自我介绍")
# 输出:你好!我是一个AI助手,可以帮你查询天气信息……

小明注意到:”第三个问题它没调函数?”

“对。”老张说,”因为 tool_choice='auto',模型自己判断——用户问天气就调函数,用户打招呼就不调,直接回复。这就是’智能’的地方。”

“如果你问’成都和北京哪个热’呢?”

run_conversation("成都和北京哪个热?")

“模型会同时调两次函数——一次查成都,一次查北京。tool_calls 里有两条,你的 for 循环分别执行,两个结果都追加到 messages,第二轮调用模型综合两个结果回答。”

小明瞪大眼睛:”一个 for 循环就搞定了并行调用?”

“对。tool_calls 是个数组,里面有几条就调几次。你循环执行就行。模型自己决定调几次、调哪个。”

tool_choice:控制模型的行为

老张说:”tool_choice 这个参数控制模型’要不要调工具’。四个选项:”

含义 使用场景
"auto" 模型自行决定(默认) 通用对话,让模型自己判断
"none" 禁止调用任何工具 纯聊天模式,模型只回答不调函数
"required" 强制必须调用工具 业务流程必须走工具,不允许直接回答
{"type":"function","function":{"name":"xxx"}} 强制调用指定函数 明确知道要用哪个工具

小明问:”autorequired 有什么区别?”

auto 时模型可能不调——用户说’你好’,模型直接回复,不调函数。required 时模型必须调——不管用户问什么,都要调一个函数。”

“什么时候用 required?”

“比如你的业务流程必须走工具——用户问任何问题都必须先查知识库再回答。或者你在调试阶段,想强制模型调某个函数看看参数对不对。”

# 强制调 get_current_weather
response = client.chat.completions.create(
  model=MODEL,
  messages=messages,
  tools=tools,
  tool_choice={"type": "function", "function": {"name": "get_current_weather"}},
)

“入门阶段用 auto 就够了。等你对模型行为有更高要求时再调。”

四个必踩的坑

坑一:函数名不匹配

tools 定义里写 "name": "get_weather",Python 函数写 def get_current_weather()——名字不一样。

KeyError: 'get_weather'

模型返回 'get_weather',你的 available_functions 里没有这个键,直接炸了。

解法:tools 里的 name 必须和 available_functions 的键完全一致。建议用常量管理,避免手写拼错:

FUNC_WEATHER = "get_current_weather"

tools = [{"function": {"name": FUNC_WEATHER, ...}}]
available_functions = {FUNC_WEATHER: get_current_weather}

坑二:参数 JSON 解析失败

func_args = json.loads(tool_call.function.arguments)

模型返回的 arguments 是 JSON 字符串,99% 的情况没问题。但偶尔模型会返回不合法的 JSON——少了引号、多了逗号、中文没转义。

json.JSONDecodeError: Expecting property name enclosed in double quotes

解法:加 try-except,解析失败时给个默认值或提示用户换种问法:

try:
  func_args = json.loads(tool_call.function.arguments)
except json.JSONDecodeError:
  print("参数解析失败,请换种方式提问。")
  return

坑三:忘记把 tool_calls 追加到 messages

“这个坑最常见。”老张说,”第一轮调用拿到 tool_calls 后,直接去执行函数了,忘了把 response_message 追加到 messages。结果第二轮调用时,messages 里只有 user 消息和 tool 消息——缺了中间那条 assistant 消息。

Error: An assistant message with 'tool_calls' must be followed by tool messages

API 直接报错——tool 消息前面必须有对应的 assistant 消息(带 tool_calls 的那条)。

解法:记住顺序——先追加 assistant 消息,再追加 tool 消息:

# ✅ 正确顺序
messages.append(response_message)  # 1. 先追加模型的 tool_calls 请求
messages.append({...})       # 2. 再追加工具执行结果

# ❌ 错误——漏了第一步
messages.append({...})  # 直接追加 tool 结果,缺 assistant 消息

坑四:tool_call_id 不匹配

“模型同时调两个函数时,每个 tool_call 有不同的 id。你执行完函数,返回结果时 tool_call_id 必须对应——”

“如果你把北京的结果标记了成都的 id,模型就以为’北京的结果是成都的’,回答就全乱了。”

解法:for 循环里用当前 tool_call.id,别用别的:

for tool_call in tool_calls:
  # 每个 tool_call 有自己的 id,用它
  messages.append({
    "role": "tool",
    "tool_call_id": tool_call.id,  # ← 用当前的 id
    ...
  })

消息列表在 Function Calling 中的变化

老张说:”上次讲多轮对话时,messages 列表只有 system/user/assistant 三种角色。Function Calling 引入了第四种——tool。整个消息流转是这样的:”

初始状态:
[system, user]              ← 你构造的

第1次调用后(模型决定调函数):
[system, user, assistant(tool_calls)]   ← 追加模型的调用请求

执行函数后:
[system, user, assistant(tool_calls), tool(结果)]  ← 追加工具结果

第2次调用后(模型生成最终回答):
[system, user, assistant(tool_calls), tool(结果), assistant(最终回答)]  ← 追加最终回复

“注意第一次的 assistant 消息,contentNone,但多了 tool_calls 字段。第二次的 assistant 消息,content 才是真正的文字回复。”

“如果你在这个基础上继续多轮对话,后面的消息追加逻辑跟上次讲的一模一样——user 追加问题,assistant 追加回复。Function Calling 只是在中间插了一组 assistant(tool_calls) + tool(结果) 消息。”

小明恍然大悟:”所以 Function Calling 并没有改变消息列表的底层逻辑,只是多了一种消息类型。”

“完全正确。messages 列表是整个大模型开发的地基,Function Calling 只是在上面加了一层楼。

这篇文章的核心

老张最后在白板上写了一句话:

Function Calling = 模型点菜 + 代码上菜 + 模型转述。

小明看着白板,把它拍了下来。

概念 一句话
本质 模型不执行函数,只输出结构化调用指令,你的代码真正执行
两轮对话 第1次决定用什么工具,第2次基于工具结果生成回答
tools 定义 name + description + parameters(JSON Schema)
description 最关键字段,决定模型何时调用,写清用途+示例值
tool_calls 模型返回的调用请求,含函数名和参数(JSON 字符串)
tool 消息 role: "tool" + tool_call_id + content(字符串)
tool_call_id 关联模型的调用请求和你的执行结果,不能搞混
tool_choice auto(自动)/ none(禁止)/ required(必须)/ 指定函数
available_functions 函数名→实际函数的映射表,模型给字符串你给执行
messages 变化 多了 assistant(tool_calls) + tool(结果) 两种消息

“大模型从’会说’变成’会干’,靠的不是自己变聪明了,而是你给它配了一双手。Function Calling 就是这双手的接口协议。”

下一步

  1. 多个工具组合 — 让模型在一个对话里调多个不同函数
  2. 嵌套调用 — 函数 A 的结果作为函数 B 的参数,模型自己编排
  3. 并行调用 — 模型同时调多个函数,你的 for 循环处理
  4. 错误处理 — 函数执行失败时怎么告诉模型,让它换个策略
  5. Agent 工作流 — Function Calling 是 Agent 的基础,Agent 就是在这个循环上加上任务规划和自主决策

“每一步都是在 tools 列表和 messages 列表上做加法。两轮调用的骨架从来没变过。”


总结

上篇你学会了让 AI 记住对话历史。这篇你学会了让 AI 调用外部工具——核心就是两轮 API 调用:第一轮模型决定用什么工具,你的代码执行函数,第二轮模型基于真实结果生成回答。

大模型本身不会查天气、不会查订单、不会发邮件。但通过 Function Calling,它能”意识到”自己需要调一个函数,告诉你调哪个、参数是什么。你执行完把结果喂回去,它就能基于真实数据回答用户。

这就是从”聊天机器人”到”AI 助手”的分水岭。聊天机器人只会说,AI 助手会干。

小明把 Function Calling 跑通后,给老板演示了 AI 客服查订单。老板问:”它是怎么知道订单系统的?”

小明说:”它不知道。它只是告诉我’调 query_order_status 函数,参数是订单号’,真正查数据库的是我的代码。它只是个翻译官。”

老板愣了一下,然后笑了:”那这个翻译官还挺靠谱。”

老张听说后跟小明说:”记住,Function Calling 的本质不是让 AI 变强了,是让 AI 变诚实了——它不再编造,而是承认’我需要查一下’,然后让你去查。

转载请注明来源:Function-Calling让大模型从会说到会干
本文链接地址:https://ai.zhousir.top/?p=3610
回复 取消