Function Calling

函数调用与工具执行闭环

模型只负责提出结构化工具调用。参数校验、权限判断、实际执行、副作用确认和结果回传必须由你的服务端代码完成。

Scope & Evidence

工具能力需要逐模型验证

本教程使用 OpenAI 兼容 Chat Completions 的 toolstool_calls 结构。模型具有 openai 标签,并不自动证明它能可靠生成工具调用。

E1 运行态证据MuxLLM 公开模型信息可确认 OpenAI 兼容端点标签;当前公开标签不能单独确认工具调用质量、并行能力或严格 Schema 支持。
E2 官方证据工具声明、助手工具调用消息和 tool 结果消息遵循 OpenAI 兼容 Chat Completions 结构。
E3 接入实测没有使用真实 Key 请求模型,也没有执行任何外部工具。示例中的天气结果为本地固定演示数据。
最后核验
模型不是执行器模型输出的函数名和参数全部是不可信数据。即使 JSON 符合 Schema,也必须继续做身份、权限、资源归属、业务状态和副作用检查。

执行受控验证时,用模型广场中的精确 ID 替换 MODEL_NAME,用本站专用低额度凭证替换 YOUR_API_KEY;正式代码应从服务端环境变量读取凭证。

Workflow

一次调用可能需要多次模型请求

  1. 01声明工具

    向模型提供名称、用途和尽量严格的 JSON Schema,不提供内部凭证或实现细节。

  2. 02发送用户消息

    messagestools 和选定模型发送到 Chat Completions。

  3. 03检查助手消息

    若存在 tool_calls,先保存整条助手消息;若只有文本,则本轮结束。

  4. 04校验并执行

    解析参数,验证 Schema、权限和业务规则,只调用服务端白名单函数。

  5. 05回传每个结果

    为每个调用追加一条 role: "tool" 消息,并绑定原始 tool_call_id

  6. 06再次请求模型

    提交完整消息历史。模型可能给出最终回答,也可能提出下一轮工具调用。

  7. 07强制停止条件

    达到最大轮数、预算、超时或风险阈值时停止,不允许无限工具循环。

Tool Definition

使用收敛的 JSON Schema 声明参数

工具描述应说明“做什么”,而不是暴露后端实现。参数 Schema 应限制类型、枚举、必需字段和额外属性。

tools 字段
[
  {
    "type": "function",
    "function": {
      "name": "get_weather",
      "description": "读取指定城市的当前天气摘要",
      "parameters": {
        "type": "object",
        "properties": {
          "city": {
            "type": "string",
            "description": "城市名称,例如上海"
          },
          "unit": {
            "type": "string",
            "enum": ["celsius", "fahrenheit"],
            "description": "温度单位"
          }
        },
        "required": ["city", "unit"],
        "additionalProperties": false
      }
    }
  }
]
名称稳定函数名是协议标识。修改名称会影响提示、日志、指标和服务端分发器。
描述明确写清适用条件和结果语义,减少模型选择错误工具的概率。
枚举优先有限选项使用 enum,避免服务端收到无法处理的自由文本。
拒绝额外字段Schema 支持时设置 additionalProperties: false;服务端仍需独立拒绝未知键。

Assistant Tool Calls

工具调用位于助手消息中

模型可能返回一项或多项工具调用。arguments 通常是 JSON 字符串,不是已经可信的对象;必须捕获解析错误。

助手消息结构示例
{
  "role": "assistant",
  "content": null,
  "tool_calls": [
    {
      "id": "call_example_001",
      "type": "function",
      "function": {
        "name": "get_weather",
        "arguments": "{\"city\":\"上海\",\"unit\":\"celsius\"}"
      }
    }
  ]
}
必须保存整条消息把这条助手消息原样加入会话历史,再追加工具结果。只保存函数名或只把结果写成普通用户消息,会破坏调用关联。

Validation & Authorization

Schema 校验只是第一层

校验层检查内容失败处理
JSON 解析是否为合法 JSON 对象,而不是数组、字符串或截断内容返回可控错误结果,不执行函数
工具白名单函数名是否存在于服务端注册表拒绝未知名称并记录脱敏事件
Schema类型、必填、枚举、长度、格式和未知字段返回最少必要的验证错误
身份与权限当前用户是否可以读取或修改目标资源拒绝,不允许模型提升权限
业务状态订单状态、余额、库存、时间窗口和并发版本返回冲突或要求重新确认
副作用确认支付、删除、发布、发信等是否取得明确确认暂停执行,向真实用户确认

不要把数据库连接、Shell、任意 URL 抓取或通用代码执行器直接暴露为宽泛工具。工具范围越具体,授权和审计越容易正确实现。

Tool Result

用 tool_call_id 回传结构化结果

每个工具调用都需要一条匹配的结果消息。content 是字符串,可以放置序列化后的紧凑 JSON;只提供模型生成最终回答所需的字段。

工具结果消息
{
  "role": "tool",
  "tool_call_id": "call_example_001",
  "content": "{\"ok\":true,\"city\":\"上海\",\"temperature\":26,\"unit\":\"celsius\"}"
}
结果也不可信外部 API、网页和数据库字段可能包含提示注入文本。回传前裁剪无关内容;模型看到工具结果不等于它获得执行额外操作的权限。

Python Complete Loop

完整处理多项工具调用和最终回答

下面的 get_weather 返回固定演示数据,不访问外部天气服务。真正接入时,应把分发器放在受控服务端,并保留相同的校验与最大轮数。

Python · 完整闭环
import json
import os
from openai import OpenAI

MAX_TOOL_ROUNDS = 4
ALLOWED_UNITS = {"celsius", "fahrenheit"}

TOOLS = [
    {
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "读取指定城市的当前天气摘要",
            "parameters": {
                "type": "object",
                "properties": {
                    "city": {"type": "string"},
                    "unit": {
                        "type": "string",
                        "enum": ["celsius", "fahrenheit"],
                    },
                },
                "required": ["city", "unit"],
                "additionalProperties": False,
            },
        },
    }
]


def dispatch_tool(name, arguments):
    if name != "get_weather":
        raise ValueError("不允许的工具")
    if not isinstance(arguments, dict):
        raise ValueError("参数必须是对象")
    if set(arguments) != {"city", "unit"}:
        raise ValueError("参数字段不匹配")

    city = arguments["city"]
    unit = arguments["unit"]
    if not isinstance(city, str) or not city.strip() or len(city) > 80:
        raise ValueError("city 无效")
    if unit not in ALLOWED_UNITS:
        raise ValueError("unit 无效")

    # 固定演示数据:实际项目应在此调用受控后端。
    return {
        "ok": True,
        "city": city.strip(),
        "temperature": 26 if unit == "celsius" else 79,
        "unit": unit,
        "source": "demo_fixture",
    }


client = OpenAI(
    api_key=os.environ["MUXLLM_API_KEY"],
    base_url="https://api.muxllm.com/v1",
)
messages = [
    {"role": "user", "content": "查询上海天气,并用一句话建议穿着。"}
]
executed_call_ids = set()

for _ in range(MAX_TOOL_ROUNDS):
    response = client.chat.completions.create(
        model=os.environ["MUXLLM_MODEL"],
        messages=messages,
        tools=TOOLS,
        tool_choice="auto",
    )
    assistant = response.choices[0].message
    calls = assistant.tool_calls or []

    assistant_message = {"role": "assistant"}
    if assistant.content is not None:
        assistant_message["content"] = assistant.content
    if calls:
        assistant_message["tool_calls"] = [
            {
                "id": call.id,
                "type": call.type,
                "function": {
                    "name": call.function.name,
                    "arguments": call.function.arguments,
                },
            }
            for call in calls
        ]
    messages.append(assistant_message)

    if not calls:
        print(assistant.content or "")
        break

    for call in calls:
        if call.id in executed_call_ids:
            result = {"ok": False, "error": "duplicate_tool_call"}
        else:
            executed_call_ids.add(call.id)
            try:
                arguments = json.loads(call.function.arguments)
                result = dispatch_tool(call.function.name, arguments)
            except (json.JSONDecodeError, KeyError, TypeError, ValueError):
                result = {"ok": False, "error": "invalid_tool_request"}

        messages.append(
            {
                "role": "tool",
                "tool_call_id": call.id,
                "content": json.dumps(result, ensure_ascii=False),
            }
        )
else:
    raise RuntimeError("达到最大工具轮数,已停止")

示例捕获并归一化校验错误,避免把内部堆栈、数据库结构或凭证返回给模型。生产代码还应增加超时、审计、用户授权和幂等键。

Node.js Complete Loop

在 Node.js 中保持同一消息链

以下示例同样只返回固定演示数据。它处理多个调用、重复调用 ID 和最大轮数,但不会替代业务权限校验。

Node.js · 完整闭环
import OpenAI from "openai";

const client = new OpenAI({
  apiKey: process.env.MUXLLM_API_KEY,
  baseURL: "https://api.muxllm.com/v1",
});

const tools = [{
  type: "function",
  function: {
    name: "get_weather",
    description: "读取指定城市的当前天气摘要",
    parameters: {
      type: "object",
      properties: {
        city: { type: "string" },
        unit: {
          type: "string",
          enum: ["celsius", "fahrenheit"],
        },
      },
      required: ["city", "unit"],
      additionalProperties: false,
    },
  },
}];

function dispatchTool(name, args) {
  if (name !== "get_weather") throw new Error("tool_not_allowed");
  if (!args || typeof args !== "object" || Array.isArray(args)) {
    throw new Error("invalid_arguments");
  }
  if (Object.keys(args).sort().join(",") !== "city,unit") {
    throw new Error("invalid_arguments");
  }
  if (typeof args.city !== "string" || !args.city.trim() || args.city.length > 80) {
    throw new Error("invalid_city");
  }
  if (!["celsius", "fahrenheit"].includes(args.unit)) {
    throw new Error("invalid_unit");
  }
  return {
    ok: true,
    city: args.city.trim(),
    temperature: args.unit === "celsius" ? 26 : 79,
    unit: args.unit,
    source: "demo_fixture",
  };
}

const messages = [{
  role: "user",
  content: "查询上海天气,并用一句话建议穿着。",
}];
const executedCallIds = new Set();
const maxToolRounds = 4;

for (let round = 0; round < maxToolRounds; round += 1) {
  const response = await client.chat.completions.create({
    model: process.env.MUXLLM_MODEL,
    messages,
    tools,
    tool_choice: "auto",
  });
  const assistant = response.choices[0].message;
  const calls = assistant.tool_calls || [];
  messages.push(assistant);

  if (calls.length === 0) {
    console.log(assistant.content || "");
    break;
  }

  for (const call of calls) {
    let result;
    if (executedCallIds.has(call.id)) {
      result = { ok: false, error: "duplicate_tool_call" };
    } else {
      executedCallIds.add(call.id);
      try {
        const args = JSON.parse(call.function.arguments);
        result = dispatchTool(call.function.name, args);
      } catch {
        result = { ok: false, error: "invalid_tool_request" };
      }
    }

    messages.push({
      role: "tool",
      tool_call_id: call.id,
      content: JSON.stringify(result),
    });
  }

  if (round === maxToolRounds - 1) {
    throw new Error("max_tool_rounds_reached");
  }
}

Parallel & Repeated Calls

不要假设每轮只有一个调用

遍历全部调用tool_calls 数组逐项处理,并为每项返回匹配的 tool_call_id
并行需受控只并发执行彼此独立、无冲突且受并发上限保护的只读工具。
写操作串行会修改同一资源的调用应在事务或版本校验下串行执行,避免竞态覆盖。
调用去重用调用 ID 和业务幂等键防止网络重试或重复消息造成重复扣款、发信或创建资源。
限制轮数模型收到工具结果后仍可能继续调用;设置最大轮数、总耗时、总费用和结果大小。

Security Boundary

高风险动作必须脱离自动闭环

风险示例最低控制
越权访问读取其他用户订单服务端按当前身份重新校验资源归属
提示注入网页内容要求泄露系统提示工具结果视为不可信数据,权限与数据分离
任意网络访问模型提供内网或云元数据 URL目标域名白名单、DNS/IP 校验、禁止私网与重定向绕过
命令或查询注入参数拼接到 Shell、SQL 或模板参数化 API、固定操作映射、最小系统权限
不可逆副作用付款、删除、发布、发送消息展示精确影响并要求真实用户二次确认
敏感数据外泄把 Key、完整数据库行回传模型字段白名单、脱敏、最小结果与日志审计
禁止直接执行永远不要把模型生成的函数名映射为 eval、Shell 字符串、任意模块导入或无限制 HTTP 请求。模型不能授予自己权限。

Failure Handling

工具失败也要形成可控消息

失败点对模型返回对系统记录
JSON 无法解析{"ok":false,"error":"invalid_arguments"}模型、工具名、调用 ID 和解析类别;不记原始敏感参数
权限不足{"ok":false,"error":"not_authorized"}真实身份、资源、策略结果和审计事件
外部服务超时{"ok":false,"error":"upstream_timeout"}耗时、有限重试次数和脱敏上游标识
业务冲突{"ok":false,"error":"state_conflict"}版本、当前状态和拒绝原因
达到工具上限停止继续调用并给出安全失败说明总轮数、总耗时和停止条件

给模型的错误信息应稳定、简短且不包含堆栈。给运维系统的日志应足以定位问题,但必须脱敏并设置合理保留周期。

Acceptance

在沙箱工具上完成受控验收

  1. 01选定候选模型

    确认 MODEL_NAMEopenai 标签,并另行确认工具能力证据。

  2. 02使用无副作用工具

    先采用返回固定数据的沙箱函数,不连接生产数据库、支付、邮件或 Shell。

  3. 03覆盖三条路径

    验证无需工具、单个工具和多个工具调用,检查最终回答和消息顺序。

  4. 04主动发送错误参数

    确认未知工具、非法 JSON、额外字段、越权和重复调用都会被拒绝。

  5. 05验证停止条件

    确认最大轮数、总超时、取消、重试和幂等策略生效。

  6. 06保存脱敏证据

    记录模型、日期、状态码、调用序列和验证结论,再决定是否开放真实工具。

当前状态本页没有执行上述 E3 验收,不能据此声称某个具体模型已经在 MuxLLM 上通过函数调用测试。

Back to Reference

将工具能力纳入 API 接入检查

回到 API 总览,继续核对鉴权、端点标签、错误处理和对应客户端配置。