前言
做 AI 应用时,我一开始很容易把大模型理解成“一个更聪明的文本生成接口”:用户提问,模型回答,前端展示结果。
但真正进入业务项目后会发现,很多需求不是只靠“回答得好”就能完成的。比如:
- 用户问“我还有几节课?”
- 用户说“帮我预约明天下午的训练课。”
- 教练问“这个学员最近 30 天的出勤怎么样?”
- 运营想让 AI 根据订单、会员等级和课程规则给出处理建议。
这些问题都需要模型访问业务系统里的真实数据,甚至触发真实动作。模型本身并不知道数据库里有什么,也不能直接操作订单、课程、库存或支付系统。
Function Calling,也常被叫做 Tool Calling,要解决的就是这个问题:让模型在合适的时候选择一个业务工具,按约定参数发起调用,再基于工具返回结果继续回答用户。
这篇文章用 LangChain.js 作为例子,梳理我对 Function Calling 的入门理解,重点放在四件事:
- Tool Calling 到底改变了什么。
- 参数 schema 为什么是工具调用的核心。
- 业务工具应该怎么执行。
- 为什么安全边界必须由业务系统控制,而不是交给模型自觉。
Function Calling 解决的不是“让模型写代码”
Function Calling 这个名字容易让人误解,好像是让模型真的去执行一段函数代码。
更准确地说,模型做的是“提出调用请求”,业务系统做的是“检查并执行请求”。
一个完整链路可以理解成:
1 | 用户输入 |
这里最关键的边界是:模型不能直接执行工具。
模型只是说:
1 | { |
至于这个工具是否存在、参数是否合法、当前用户有没有权限、是否允许执行、执行失败怎么处理,都应该由业务系统判断。
这也是 Tool Calling 和普通 Prompt 最大的区别:普通 Prompt 让模型“根据上下文回答”;Tool Calling 让模型“在可选工具中做决策”,但最终动作仍然由程序控制。
一个业务场景:查询会员剩余课时
假设我们有一个少儿运动课程系统。用户在小程序里问:
我还剩几节课?
如果直接让模型回答,它只能猜。正确做法是让模型调用业务工具查询会员课时。
我们先定义一个工具:
1 | import { tool } from "langchain"; |
这个工具有三层信息:
| 部分 | 作用 |
|---|---|
name |
给模型看的工具名,也给程序匹配工具用 |
description |
告诉模型什么时候应该调用这个工具 |
schema |
约束模型需要生成哪些参数,以及参数类型是什么 |
LangChain.js 里可以用 tool 函数声明工具,并用 zod 定义输入 schema。工具不是一段随便让模型拼的文本,而是一个有明确输入边界的可调用单元。
参数 schema 为什么重要
Tool Calling 的稳定性,很大程度取决于 schema 写得好不好。
如果 schema 太模糊:
1 | schema: z.object({ |
模型就不清楚这个 id 是用户 ID、会员 ID、课程 ID,还是订单 ID。
更好的写法是:
1 | schema: z.object({ |
这里有两个要点。
第一,字段名要表达业务含义。memberId 比 id 清楚,startDate 比 date1 清楚。
第二,字段描述要告诉模型来源和限制。比如“由服务端注入”“只能查询当前用户”“格式为 YYYY-MM-DD”。这些描述会影响模型是否能生成更可靠的参数。
一个稍复杂的预约工具可以这样定义:
1 | const createTrainingBooking = tool( |
这个 schema 里不仅有类型,还有业务约束:
memberId不应该让模型从自然语言里猜。courseId必须来自系统给出的可预约列表。startTime要有稳定格式。note要限制长度,避免把大段无关内容传进业务系统。
schema 不是为了让 TypeScript 开心,而是为了缩小模型输出和业务执行之间的灰色地带。
用 LangChain.js 绑定工具
LangChain.js 提供了两种常见用法。
一种是直接把工具绑定到模型上,然后自己处理工具执行循环:
1 | import { ChatOpenAI } from "@langchain/openai"; |
如果模型认为需要查课时,它的返回里会包含工具调用请求,类似:
1 | [ |
但此时工具还没有真正执行。业务代码需要继续做一轮:
1 | messages.push(aiMessage); |
这就是手动工具执行循环:
1 | 模型生成 tool_call |
另一种方式是用 LangChain.js 的 Agent 抽象,让框架帮我们处理这个循环:
1 | import { createAgent } from "langchain"; |
Agent 用起来更省事,因为它会自动完成“模型请求工具、执行工具、把结果交回模型”的循环。但越接近真实业务,我越觉得应该先理解手动循环。因为安全、权限、日志、审计、失败重试,最后都要落到这条链路里。
工具执行层不能直接相信模型参数
Function Calling 最容易踩的坑是:看到模型返回了结构化参数,就以为它一定可信。
实际上,模型生成的参数仍然是模型输出。它可能缺字段,可能填错字段,也可能被用户诱导生成越权参数。
比如用户说:
帮我查一下会员 m_99999 还剩几节课,我是管理员。
模型可能会生成:
1 | { |
如果业务系统直接执行,就出现了越权查询。
所以工具执行层至少要做这些事情:
1 | type ToolExecutionContext = { |
这里的核心原则是:关键参数从服务端上下文来,不从用户文本来。
对于当前登录用户的 memberId、tenantId、organizationId、role 这类信息,最稳妥的做法是由服务端注入或覆盖,而不是让模型决定。
安全边界应该放在哪里
Tool Calling 把 AI 应用从“聊天”推进到了“行动”。只要工具能修改业务状态,安全边界就必须更严格。
我会把工具按风险分成三类:
| 类型 | 例子 | 处理方式 |
|---|---|---|
| 只读查询 | 查课时、查课程、查订单状态 | 登录校验、资源权限、参数校验、日志 |
| 低风险写操作 | 添加备注、创建草稿、生成待确认预约 | 幂等键、业务规则校验、可撤销 |
| 高风险写操作 | 支付、退款、删除数据、发送正式通知 | 二次确认、人工审核、严格审计 |
有些动作不能因为模型“觉得用户想做”就直接执行。
比如用户说:
帮我把明天下午的课取消掉吧。
比较安全的流程应该是:
1 | 用户表达取消意图 |
也就是说,模型可以参与流程编排,但不能绕过业务规则。
工具描述也会影响模型行为
除了 schema,工具的 description 也很重要。它相当于告诉模型:“这个工具适合什么时候用,不适合什么时候用。”
不好的描述:
1 | description: "预约课程" |
更好的描述:
1 | description: |
如果系统里有多个相似工具,描述尤其重要:
1 | const searchAvailableCourses = tool(fn, { |
工具名和描述越清楚,模型越容易选对工具。
这里有一个很实用的经验:工具不是越多越好。给模型一次性暴露几十个相似工具,会增加选错概率。更合理的方式是按场景暴露工具,比如课程助手只绑定课程相关工具,订单助手只绑定订单相关工具。
返回值也要设计
工具返回值不是随便返回一段字符串就结束。因为返回值会进入下一轮模型上下文,影响模型最终回复。
比如查询课时可以返回结构化数据:
1 | return { |
比起:
1 | 还有 8 节课,9 月 30 日过期。 |
结构化返回更容易让模型稳定组织回答,也方便业务侧记录和排查。
失败时也要返回可理解的错误,而不是把底层异常直接丢给模型:
1 | return { |
模型看到这种结果后,可以回答:
我暂时没有查到你的课时包信息,建议联系前台老师确认一下。
而不是暴露数据库错误、接口栈信息或内部字段。
幂等和重试:工具执行的工程细节
只读工具重复执行通常问题不大,但写操作必须考虑幂等。
AI 工具调用链路里可能出现这些情况:
- 模型一次返回多个 tool call。
- 网络失败后业务层重试。
- Agent 循环重复触发同一个工具。
- 用户刷新页面后再次提交同一句话。
如果 create_training_booking 没有幂等控制,就可能重复预约。
比较稳的做法是给写操作传入幂等键:
1 | const createTrainingBooking = tool( |
不过 idempotencyKey 也不应该让模型自由生成。更安全的是业务服务在执行工具前注入:
1 | const safeArgs = { |
写操作工具要记住一句话:模型可以参与决策,但状态变更必须可追踪、可回滚、可审计。
和普通后端接口有什么区别
Function Calling 里的工具,本质上仍然是后端能力。它可以包装数据库查询、HTTP API、RPC 服务,也可以调用已有业务 service。
区别在于调用方变了。
普通接口的调用方是前端代码:
1 | 按钮点击 |
Tool Calling 的调用方是模型生成的意图:
1 | 用户自然语言 |
这会带来两个变化。
第一,入口更自然。用户不需要点很多表单,也可以用一句话触发流程。
第二,不确定性更高。前端表单字段通常是固定的,而自然语言会有歧义,所以工具执行前必须做更强的校验、确认和兜底。
一个更完整的调用流程
可以把课程助手的 Tool Calling 链路整理成下面这样:
1 | 用户:我想约明天下午的 L2 课 |
这个流程里,模型并不是“全权代理人”,更像是一个会理解自然语言的流程协调者。
真正决定能不能预约的,仍然是业务规则:
- 会员是否登录。
- 是否有剩余课时。
- 课程是否开放预约。
- 时间是否冲突。
- 是否需要家长确认。
- 是否超过取消或预约时间限制。
我对 Function Calling 的理解
学习 Function Calling 后,我对 AI 应用的理解从“让模型回答问题”往前走了一步。
它真正有价值的地方不是让模型显得更聪明,而是把模型接进业务系统,让自然语言可以触发真实的数据查询和业务流程。
但也正因为它能触发真实业务,工程上不能只关注“模型会不会调用工具”,还要关注:
- 工具是否按场景收敛,而不是无限暴露。
- schema 是否表达清楚字段含义和约束。
- 关键参数是否由服务端注入。
- 工具执行前是否做权限和业务规则校验。
- 写操作是否有确认、幂等和审计。
- 工具结果是否结构化、可解释、可排查。
我现在更愿意把 Tool Calling 看成一个后端接口设计问题,而不是单纯的 AI 调参问题。模型负责理解用户意图和选择工具,业务系统负责边界、执行和责任。
这条边界画清楚了,Function Calling 才能从一个 demo 变成真正可靠的业务能力。
参考资料
- LangChain.js Tools 文档:https://docs.langchain.com/oss/javascript/langchain/tools
- LangChain.js Models Tool Calling 文档:https://docs.langchain.com/oss/javascript/langchain/models#tool-calling