AI 探索

《前端 Function Calling 调度器:让 AI 自己调用你的函数》

2024年7月23日◷ 6 分钟阅读
《前端 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 的"手",分三步:

  1. 工具注册:前端声明"我能做什么"(函数名 + 参数 schema)
  2. AI 决策:LLM 判断"该调用哪个工具"并返回 JSON
  3. 前端执行 + 回传:前端执行函数,结果塞回对话上下文
用户提问 "北京天气?"
   │
   ▼
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 次以上仍然没有进展"的情况,强制终止。


八、总结

  1. Function Calling = Agent 的手:把前端能力封装成 tool,AI 决策何时调用。
  2. 必加 maxRounds:无上限循环 = 生产事故。
  3. 错误回传:工具执行失败不要吞掉,告诉 AI 更好。
  4. arguments 是 JSON 字符串:必须 JSON.parse,不能直接 . 访问。

Comments 留言讨论

还没有评论,来抢个沙发,聊聊你的看法~

Michael.Meng

michaelnews@126.com
用 AI 记录,用文字沉淀

© 2026 Michael Meng · 保留所有权利 · Powered by FastAPI + Nuxt