《前端 Function Calling 调度器:让 AI 自己调用你的函数》
一、问题引入:为什么前端也需要 Function Calling?
想象这个场景:用户在你的聊天框里输入"帮我查一下北京和上海的天气,然后比较一下哪边更适合周末出游"。
如果只是普通 LLM,它会基于训练数据瞎编一个答案。但如果你的前端能直接调用天气 API,然后把真实天气数据塞回给 LLM 分析——就能给出真实的、有用的答案。
这就是 Function Calling 在前端的价值:让 AI 成为"指挥官",前端成为"执行者"。
| 场景 | 无 Function Calling | 有 Function Calling |
|---|---|---|
| 查天气 | LLM 编造数据 | 前端调真实 API,LLM 分析 |
| 查时间 | LLM 不知道当前时间 | 前端直接返回 new Date() |
| 操作 DOM | 用户手动操作 | LLM 发出指令,前端执行 |
二、核心概念:三步走模型
Function Calling 的本质是 Agent 的"手",分三步:
- 工具注册:前端声明"我能做什么"(函数名 + 参数 schema)
- AI 决策:LLM 判断"该调用哪个工具"并返回 JSON
- 前端执行 + 回传:前端执行函数,结果塞回对话上下文
用户提问 "北京天气?"
│
▼
AI 返回: { tool_calls: [{ name: "getWeather", args: { city: "北京" } }] }
│
▼
前端: await getWeather("北京") → "北京今天晴天,25℃"
│
▼
再次请求 AI(带上工具结果)
│
▼
AI 最终回答: "北京今天晴天 25℃,非常适合出游……"
三、完整可运行代码
以下是原生 JavaScript 实现,不依赖任何框架,可以直接在浏览器控制台或 Node.js 环境测试:
// ============ 第一步:定义工具函数(前端能力) ============
const TOOLS = {
/** 查询指定城市天气 */
async getWeather({ city }) {
// 模拟调用天气 API
const mockWeather = {
'北京': '晴天 25℃,微风',
'上海': '多云 22℃,阵雨概率 40%',
'深圳': '阴天 28℃,湿度 85%',
}
return mockWeather[city] || `未找到 ${city} 的天气数据`
},
/** 获取当前时间 */
async getTime() {
return new Date().toLocaleString('zh-CN', {
timeZone: 'Asia/Shanghai',
})
},
/** 获取设备信息 */
async getDeviceInfo() {
return {
platform: navigator.platform,
language: navigator.language,
screenSize: `${window.screen.width}x${window.screen.height}`,
}
},
}
// ============ 第二步:构建工具定义(OpenAI 格式) ============
function buildToolDefinitions(toolMap) {
const definitions = []
for (const [name, fn] of Object.entries(toolMap)) {
definitions.push({
type: 'function',
function: {
name,
description: extractDescription(fn),
parameters: extractParameters(fn),
},
})
}
return definitions
}
// 从函数注释或推断参数
function extractDescription(fn) {
const match = fn.toString().match(/\/\*\*([^@]*)\*\//)
// 简化版:返回函数名作为描述
return `${fn.name} 工具函数`
}
function extractParameters(fn) {
const match = fn.toString().match(/\(\s*\{\s*(\w+)\s*\}\s*\)/)
if (match) {
const paramName = match[1]
return {
type: 'object',
properties: {
[paramName]: { type: 'string', description: `调用 ${fn.name} 所需参数` },
},
required: [paramName],
}
}
// 无参数函数
return { type: 'object', properties: {} }
}
// ============ 第三步:Function Calling 调度器核心 ============
async function functionCallScheduler(userMessage, maxRounds = 3) {
// 构建对话上下文(带工具定义)
const messages = [{ role: 'user', content: userMessage }]
// 最多循环 maxRounds 次,防止死循环
for (let round = 0; round < maxRounds; round++) {
// 请求 AI(这里用 fetch 示例,实际替换为你的 API)
const response = await fetch('/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
messages,
tools: buildToolDefinitions(TOOLS),
tool_choice: 'auto', // AI 自动决定是否调用工具
}),
})
const data = await response.json()
const aiMessage = data.choices[0].message
// 情况 A:AI 直接返回了答案(不需要调用工具)
if (!aiMessage.tool_calls) {
return {
finalAnswer: aiMessage.content,
rounds: round + 1,
}
}
// 情况 B:AI 要求调用工具
messages.push(aiMessage) // 保存 AI 的 tool_calls 消息
// 逐一执行 AI 要求的工具调用
for (const toolCall of aiMessage.tool_calls) {
const funcName = toolCall.function.name
const args = JSON.parse(toolCall.function.arguments || '{}')
// 安全检查:函数是否存在
if (!TOOLS[funcName]) {
console.error(`未知工具: ${funcName}`)
continue
}
try {
// 执行前端函数
const result = await TOOLS[funcName](args)
console.log(`[工具调用] ${funcName}(${JSON.stringify(args)}) → ${result}`)
// 把结果塞回对话
messages.push({
role: 'tool',
tool_call_id: toolCall.id,
name: funcName,
content: JSON.stringify(result),
})
} catch (err) {
// 工具执行失败,告诉 AI 错误信息
messages.push({
role: 'tool',
tool_call_id: toolCall.id,
name: funcName,
content: JSON.stringify({ error: err.message }),
})
}
}
// 循环继续:带上工具结果再请求 AI
}
// 超过最大轮次,返回最后一次消息
return {
finalAnswer: messages[messages.length - 1]?.content || '处理超时',
rounds: maxRounds,
note: '达到最大调度轮次',
}
}
// ============ 使用示例 ============
// 在浏览器控制台运行:
// functionCallScheduler('北京今天天气如何?适合出门吗?')
// .then(r => console.log(r))
四、逐行精讲
4.1 防死循环:maxRounds 限制
// 关键设计:必须限制最大轮次
for (let round = 0; round < maxRounds; round++) {
如果 AI 反复要求调用同一个工具但没有进展(比如天气 API 一直返回空),调度器会无限循环。GPT 的 tool_choice 设为 auto 有时也会"卡住"——一直要求调用同一函数。设置 maxRounds=3 是最低成本的保底。
4.2 工具执行失败的回传策略
catch (err) {
messages.push({
role: 'tool',
content: JSON.stringify({ error: err.message }),
})
}
错误回传比吞掉错误更好:告诉 AI "执行失败了,原因是 XXX",AI 会自己调整策略(比如换一个参数重试、或者直接承认当前无法完成)。
4.3 为什么 arguments 是 JSON 字符串
const args = JSON.parse(toolCall.function.arguments || '{}')
OpenAI 返回的 tool_calls[].function.arguments 永远是 JSON 字符串,不是对象。很多人在这里踩坑:直接用 toolCall.function.arguments.city 得到 undefined。
五、常见问题与踩坑记录
Q1:AI 调用了不存在的工具怎么办?
A:检查 TOOLS[funcName] 是否存在,不存在时跳过或返回错误给 AI。同时在 tool definitions 里把 required 参数写清楚,减少 AI 的"脑补"。
Q2:工具返回数据太长,超过 Token 限制?
A:对工具结果做截断。例如天气数据只返回关键字段(温度、天气状况、时间戳),省略原始 API 的完整 JSON。
Q3:如何支持并行调用多个工具?
A:AI 一次可以返回多个 tool_calls,前端用 Promise.all 并行执行:
const results = await Promise.all(
aiMessage.tool_calls.map(tc => TOOLS[tc.function.name](JSON.parse(tc.function.arguments)))
)
六、决策框架
你的 AI 聊天需要操作真实世界吗?
├── 不需要 → 普通对话即可
│
└── 需要
├── 操作是前端能完成的(查时间/操作 DOM/本地搜索)
│ └── 用 Function Calling 调度器(本文方案)
│
├── 操作需要后端支持(查数据库/发邮件/调第三方 API)
│ └── 前端透传 tool_calls → 后端执行 → 后端回传结果
│
└── 操作是复杂的多步骤任务(多步骤依赖)
└── 用 Agent 框架(LangChain / XState 状态机编排)
七、面试速记
Q:简述前端 Function Calling 的实现流程?
A:1. 注册工具函数(名称 + 参数 schema)→ 2. 请求 AI 时带上 tools 字段 → 3. 解析返回的 tool_calls → 4. 前端执行函数 → 5. 将结果作为 tool 消息追加到对话 → 6. 继续请求 AI 直到获得最终答案。
Q:如何防止 Function Calling 死循环?
A:设置 maxRounds 最大循环次数(通常 3-5 次)。同时监控"AI 连续调用同一工具 2 次以上仍然没有进展"的情况,强制终止。
八、总结
- Function Calling = Agent 的手:把前端能力封装成 tool,AI 决策何时调用。
- 必加
maxRounds:无上限循环 = 生产事故。 - 错误回传:工具执行失败不要吞掉,告诉 AI 更好。
arguments是 JSON 字符串:必须JSON.parse,不能直接.访问。
Comments 留言讨论
还没有评论,来抢个沙发,聊聊你的看法~