🤖 AI Agent 实战 · 第 1 篇

Function Calling 从原理到实现:模型并不会执行代码

作者:VillaB · 发布:2026-10-27 · 更新:2026-10-27 · 分类:AI Agent

最大的误解:模型不执行任何东西

第一次接触 Function Calling 的人常有一种错觉:给模型一堆函数,它就能自己去查数据库、发邮件。

不是的。 模型唯一能做的是输出一段结构化文本,声明「我想调用 query_order,参数是 {"order_id": "A123"}」。真正执行的是你的代码,执行完把结果再交回给模型,它才能继续往下说。

完整闭环是四步:

1. 你:带上 tools 定义 + 用户消息,请求模型
2. 模型:返回 tool_calls(意图),不返回自然语言答案
3. 你:按意图执行真实函数,拿到结果
4. 你:把结果以 role="tool" 追加进消息,再请求一次模型
5. 模型:基于工具结果生成最终回答

第 4 步最容易被漏掉——很多人拿到 tool_calls 执行完就直接把结果返回给用户,于是用户看到的是原始 JSON 而不是人话。

一、工具定义长什么样

以 OpenAI 兼容格式为例(国内多数厂商也用这套):

tools = [{
    "type": "function",
    "function": {
        "name": "query_order",
        "description": "查询订单状态与金额。当用户询问订单进度、是否发货、金额明细时使用。"
                       "不要用于查询他人订单。返回字段:order_id, status, amount, created_at。",
        "parameters": {
            "type": "object",
            "properties": {
                "order_id": {
                    "type": "string",
                    "description": "订单号,形如 A123456,必填"
                }
            },
            "required": ["order_id"]
        }
    }
}]

三个关键点:

  1. description 决定模型会不会正确选工具。写「查询订单」太短;要写清「做什么 / 什么时候用 / 什么时候不要用 / 返回什么」
  2. 参数的 description 同样重要,模型靠它判断该填什么格式
  3. 工具越少越准。超过 10 个高度相似的工具,误调用率会明显上升——按场景拆分不同的工具集

二、执行循环的代码骨架

import json
from openai import OpenAI

client = OpenAI(api_key=KEY, base_url=BASE_URL)

def run(messages, tools, handlers, max_steps=6):
    for step in range(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)                      # 关键:把 assistant 的 tool_calls 原样放回

        for call in msg.tool_calls:
            fn = handlers.get(call.function.name)
            try:
                args = json.loads(call.function.arguments or "{}")
                result = fn(**args) if fn else {"error": f"unknown tool {call.function.name}"}
            except Exception as e:
                result = {"error": str(e), "retryable": True}

            messages.append({
                "role": "tool",
                "tool_call_id": call.id,          # 必须对上,对不上会报错
                "content": json.dumps(result, ensure_ascii=False),
            })

    return "(已达到最大步骤数,任务未完成)"

四个工程细节,漏一个就跑不通:

  • messages.append(msg):assistant 那条带 tool_calls 的消息必须原样回传,否则模型不知道自己在回应什么
  • tool_call_id 必须匹配:每条 tool 结果都要对应一个 call id
  • content 必须是字符串:不要直接塞 dict,要 json.dumps
  • max_steps 必须有:模型可能反复调用同一工具陷入循环

三、并行工具调用

模型可以一次返回多个 tool_calls(比如同时查订单和物流)。这是性能优化点:不要串行执行,用并发。

import asyncio

async def run_parallel(calls, handlers):
    async def one(call):
        args = json.loads(call.function.arguments or "{}")
        return call.id, await handlers[call.function.name](**args)
    return await asyncio.gather(*[one(c) for c in calls])

注意:并行只在工具之间无依赖时安全。有依赖(先查订单号、再查物流)时,模型通常会自己分两轮调用。

四、tool_choice:控制调不调

取值 行为 用在哪
auto 模型自己决定 默认
required 必须调用至少一个工具 强制走工具链,防止它直接瞎答
none 禁止调用 只想要纯文本回答时
{"type":"function","function":{"name":"x"}} 强制调用指定工具 固定流程的第一步

实用场景:先 required 逼它查数据,再 auto 让它组织答案。可以显著减少"不查就编"。

五、strict 模式 / 结构化输出

即使给了 JSON Schema,模型仍可能少字段、类型不对。启用严格模式(OpenAI 的 strict: true 或 response_format 的 json_schema)后,模型输出会被约束到 schema 上。

限制要知道:

  • 通常要求 additionalProperties: false,且所有字段列入 required
  • 嵌套深度、枚举值数量有上限
  • 不是所有厂商都支持,跨厂商时要有降级方案

六、常见故障对照表

现象 根因 修法
该调工具时不调 description 太笼统 补「什么时候用」;或 tool_choice="required"
调错工具(相近功能) 工具太多/描述重叠 拆分工具集,在描述里写清与相邻工具的区别
参数缺字段 参数无 description、未设 required 补描述 + strict 模式 + 执行前校验
报错 tool_call_id 不匹配 结果没带 id 或顺序错 严格按 call.id 回传
无限循环调用 结果没让模型满足 设 max_steps,并在 prompt 里写「拿到 X 就停止」
用户看到 JSON 漏了最后一轮 执行完必须再请求一次模型生成自然语言

七、什么时候不该用 Function Calling

  • 只有一种固定操作、参数简单 → 直接规则解析更快更便宜
  • 需要严格事务一致性 → 让模型选工具可以,但执行与校验必须在你自己的代码里
  • 高风险不可逆操作(转账、删除)→ 必须加人工确认环节,不能交给 Agent 自主决定

相关技能

返回文章列表