Function Calling 到底是什么

Function Calling 是很多 AI 应用从“聊天玩具”变成“软件系统”的关键一步。

在普通 Chat 里,模型只返回一段文本。用户问“帮我查一下北京明天的天气”,模型如果没有外部信息,只能基于已有知识猜测或拒绝。但在 Function Calling 里,应用可以提前告诉模型:这里有一个 getWeather 函数,参数需要城市和日期。模型判断需要查询天气时,不直接回答,而是返回“我想调用这个函数,以及这些参数”。

这听起来像模型会写代码、会执行函数,但工程上要先把边界说清楚:模型不会真的执行你的 JavaScript 函数。它只是根据函数说明生成一个结构化的调用请求。真正执行函数的是你的应用。

它解决什么问题

Function Calling 解决的是模型和确定性系统之间的接口问题。

传统程序擅长确定性动作:查数据库、请求 API、计算价格、创建订单、读取文件、发通知。模型擅长理解自然语言、抽取意图、补全参数、组织回答。Function Calling 把两者接起来:模型负责把用户语言翻译成函数调用,应用负责执行真实动作。

对前端工程师来说,可以把它类比成“模型版事件分发”:

  • 用户在 UI 里表达意图。
  • 模型判断需要哪个 action。
  • 模型给出 action payload。
  • 应用校验 payload。
  • 应用执行 action。
  • 应用把执行结果交回模型或直接渲染到界面。

区别在于,普通事件分发来自按钮点击和表单输入,而 Function Calling 的触发条件来自自然语言理解。

Function Calling 不是远程代码执行

很多初学者会误解:既然叫 Function Calling,是不是模型可以直接调用后端函数?

不是。

模型输出的通常是一段结构化数据,表示它“建议调用某个函数”。例如:

{
  "name": "getWeather",
  "arguments": {
    "city": "北京",
    "date": "tomorrow"
  }
}

这段数据本身不会访问天气服务。你的后端代码需要拿到它,判断函数名是否合法,校验参数是否符合 schema,确认用户是否有权限,再调用真实 API。

所以 Function Calling 的安全模型应该是:

模型在图里只做“选择”和“生成参数”,不拥有执行权。

一个前端熟悉的类比

假设你有一个搜索页面,用户可以输入筛选条件。传统实现可能是:

type SearchOrdersInput = {
  status?: 'pending' | 'paid' | 'shipped';
  keyword?: string;
  page: number;
};

async function searchOrders(input: SearchOrdersInput) {
  return fetch('/api/orders/search', {
    method: 'POST',
    body: JSON.stringify(input),
  });
}

用户需要通过表单选择状态、输入关键词、点击搜索。

如果加上 Function Calling,用户可以说:

帮我找一下最近还没发货、包含 iPhone 的订单。

模型把自然语言转换成:

{
  "status": "paid",
  "keyword": "iPhone",
  "page": 1
}

然后应用调用同一个搜索接口。

这不是让模型替代业务逻辑,而是让模型成为一种更灵活的输入层。真正的订单查询、权限判断、分页限制、字段过滤,仍然属于应用。

工程流程

一次完整 Function Calling 通常包含六步。

第一步,定义函数清单。你需要告诉模型有哪些函数可用,每个函数的名字、用途、参数结构、必填字段和枚举值是什么。

第二步,把用户消息和函数定义一起发给模型。模型会根据用户意图决定是直接回答,还是返回函数调用请求。

第三步,解析模型返回的调用请求。此时不要信任参数,要像处理普通外部输入一样处理它。

第四步,校验函数名和参数。函数名必须在白名单里,参数必须通过 Zod、JSON Schema 或后端 DTO 校验。

第五步,执行真实函数。这里可以是调用内部服务、请求第三方 API、查数据库、计算价格,也可以只是读取缓存。

第六步,把函数结果回填给模型。模型拿到结构化结果后,再生成适合用户阅读的回答。

前端不一定直接接触模型返回的函数调用,但前端会受到它的影响:消息流里可能出现“正在查询订单”“工具调用失败”“需要用户确认”等中间状态。

函数定义怎么设计

函数定义不是随便写几句描述。它会直接影响模型是否选对工具、是否生成正确参数。

好的函数名应该表达清晰动作,例如 searchOrdersgetInvoiceByIdcreateCalendarEvent。不要使用太宽泛的名字,比如 handleDataprocesscallApi

函数描述要说明什么时候使用,而不只是重复函数名。

参数 schema 要尽量收敛。能用枚举就不要用任意字符串,能拆字段就不要让模型生成一整段查询语句。

例如订单搜索工具可以这样设计:

const searchOrdersTool = {
  name: 'searchOrders',
  description: '当用户想按订单状态、关键词或时间范围查找订单时使用。',
  parameters: {
    type: 'object',
    properties: {
      status: {
        type: 'string',
        enum: ['pending', 'paid', 'shipped', 'cancelled'],
        description: '订单状态',
      },
      keyword: {
        type: 'string',
        description: '订单号、商品名或客户名关键词',
      },
      limit: {
        type: 'number',
        description: '最多返回多少条,最大 20',
      },
    },
    required: ['limit'],
  },
};

注意这里没有让模型生成 SQL。模型只生成受控字段,数据库查询由应用拼装。

返回值也要结构化

很多项目只约束输入参数,却忽略工具返回值。结果工具执行成功后,模型拿到一大段混乱文本,又开始自由发挥。

更稳的方式是让工具返回结构化结果:

type SearchOrdersResult = {
  orders: Array<{
    id: string;
    status: 'pending' | 'paid' | 'shipped' | 'cancelled';
    totalAmount: number;
    createdAt: string;
  }>;
  total: number;
  hasMore: boolean;
};

这样模型可以基于明确字段生成总结,前端也可以选择直接渲染订单列表,而不是只展示模型转述。

真实产品里,最终响应不一定完全由模型生成。对于高风险、强结构化场景,前端可以直接渲染工具结果,模型只负责解释、摘要或提示下一步。

常见坑

第一个坑:把模型参数当可信输入。

Function Calling 返回的 arguments 仍然来自模型输出,必须校验。不要因为它看起来符合 JSON,就跳过权限、类型和业务规则检查。

第二个坑:工具太大,职责太宽。

一个 manageUser 工具同时创建、删除、修改权限,会让模型难以判断,也会扩大风险。工具更适合按明确动作拆分。

第三个坑:函数描述写得像内部文档。

模型需要知道“什么时候调用”,不是只知道“这个函数实现了什么”。描述应该面向意图匹配。

第四个坑:让模型生成危险参数。

不要让模型直接生成 SQL、Shell 命令、文件路径或任意 URL。应该让它填受控字段,再由应用映射到安全实现。

第五个坑:忽略失败路径。

工具可能超时、无权限、查不到结果、第三方 API 报错。失败也应该有结构化返回,让模型或前端知道如何继续。

第六个坑:前端只设计最终消息,不设计中间状态。

工具调用会引入排队、执行中、失败、重试、需要确认等状态。聊天界面需要能表达这些状态,否则用户会觉得“AI 卡住了”。

小结

Function Calling 的本质不是让模型获得执行能力,而是把自然语言意图转换成受控的函数调用请求。

模型负责理解用户、选择函数、生成参数;应用负责校验、授权、执行、记录和回填结果。前端工程师理解这一点后,就能把 AI 调用看成一种新的输入协议,而不是不可控的黑盒。

真正可靠的 Function Calling,不靠模型“乖”,而靠清晰的工具定义、严格的 schema、明确的权限边界和完整的错误状态。