《60天AI学习计划启动 | Day 07:Function Calling - 让 AI 调用外部工具》

Day 07: Function Calling - 让 AI 调用外部工具

学习目标


核心学习内容

1. Function Calling 基础

什么是 Function Calling?

  • 让 AI 决定何时调用外部函数
  • AI 分析用户意图,选择合适函数
  • 返回函数调用参数,由应用执行

工作流程:

用户提问 → AI 分析意图 → 决定调用函数 → 返回参数 → 执行函数 → 返回结果 → AI 生成回答

应用场景:

  • 天气查询
  • 数据库查询
  • API 调用
  • 计算任务
  • 文件操作

2. 函数定义(Schema)

基本结构:

{
  name: "get_weather",           // 函数名
  description: "获取指定城市的天气信息",  // 函数描述
  parameters: {                    // 参数定义
    type: "object",
    properties: {
      city: {
        type: "string",
        description: "城市名称"
      },
      unit: {
        type: "string",
        enum: ["celsius", "fahrenheit"],
        description: "温度单位"
      }
    },
    required: ["city"]            // 必填参数
  }
}

3. 函数调用流程

步骤:

  1. 定义函数列表
  2. 发送给 AI(包含函数定义)
  3. AI 决定是否调用函数
  4. 解析函数调用参数
  5. 执行函数
  6. 将结果返回给 AI
  7. AI 生成最终回答

实践作业

作业1:定义函数 Schema

src/services/functions.js:

/**
 * 函数定义列表
 */
export const functionDefinitions = [
  {
    name: "get_weather",
    description: "获取指定城市的当前天气信息",
    parameters: {
      type: "object",
      properties: {
        city: {
          type: "string",
          description: "城市名称,例如:北京、上海、New York"
        },
        unit: {
          type: "string",
          enum: ["celsius", "fahrenheit"],
          description: "温度单位,celsius 表示摄氏度,fahrenheit 表示华氏度",
          default: "celsius"
        }
      },
      required: ["city"]
    }
  },
  {
    name: "calculate",
    description: "执行数学计算",
    parameters: {
      type: "object",
      properties: {
        expression: {
          type: "string",
          description: "数学表达式,例如:2+2, 10*5, sqrt(16)"
        }
      },
      required: ["expression"]
    }
  },
  {
    name: "get_current_time",
    description: "获取当前时间",
    parameters: {
      type: "object",
      properties: {
        timezone: {
          type: "string",
          description: "时区,例如:Asia/Shanghai, America/New_York",
          default: "Asia/Shanghai"
        },
        format: {
          type: "string",
          enum: ["12h", "24h"],
          description: "时间格式",
          default: "24h"
        }
      }
    }
  },
  {
    name: "search_web",
    description: "搜索网络信息(模拟)",
    parameters: {
      type: "object",
      properties: {
        query: {
          type: "string",
          description: "搜索关键词"
        }
      },
      required: ["query"]
    }
  }
];

作业2:实现函数执行器

src/services/function-executor.js:

import axios from 'axios';

/**
 * 函数执行器
 */
export class FunctionExecutor {
  /**
   * 执行函数
   */
  async execute(functionName, parameters) {
    switch (functionName) {
      case 'get_weather':
        return await this.getWeather(parameters);
      case 'calculate':
        return this.calculate(parameters);
      case 'get_current_time':
        return this.getCurrentTime(parameters);
      case 'search_web':
        return this.searchWeb(parameters);
      default:
        throw new Error(`未知函数: ${functionName}`);
    }
  }

  /**
   * 获取天气(使用 OpenWeatherMap API)
   */
  async getWeather({ city, unit = 'celsius' }) {
    try {
      // 注意:需要注册 OpenWeatherMap 获取 API Key
      const apiKey = process.env.WEATHER_API_KEY || 'your-api-key';
      const url = `https://api.openweathermap.org/data/2.5/weather`;
      
      const response = await axios.get(url, {
        params: {
          q: city,
          appid: apiKey,
          units: unit === 'celsius' ? 'metric' : 'imperial',
          lang: 'zh_cn'
        }
      });

      const data = response.data;
      return {
        city: data.name,
        country: data.sys.country,
        temperature: Math.round(data.main.temp),
        unit: unit === 'celsius' ? '°C' : '°F',
        description: data.weather[0].description,
        humidity: data.main.humidity,
        windSpeed: data.wind.speed,
        feelsLike: Math.round(data.main.feels_like)
      };
    } catch (error) {
      // 如果 API 不可用,返回模拟数据
      return {
        city: city,
        temperature: unit === 'celsius' ? 22 : 72,
        unit: unit === 'celsius' ? '°C' : '°F',
        description: '晴天',
        humidity: 60,
        windSpeed: 10,
        note: '(模拟数据,实际需要配置天气 API)'
      };
    }
  }

  /**
   * 数学计算
   */
  calculate({ expression }) {
    try {
      // 安全计算:只允许数学表达式
      const sanitized = expression.replace(/[^0-9+\-*/().\s]/g, '');
      const result = Function(`"use strict"; return (${sanitized})`)();
      
      return {
        expression: expression,
        result: result,
        type: typeof result
      };
    } catch (error) {
      return {
        error: '计算失败,请检查表达式格式'
      };
    }
  }

  /**
   * 获取当前时间
   */
  getCurrentTime({ timezone = 'Asia/Shanghai', format = '24h' }) {
    const now = new Date();
    const options = {
      timeZone: timezone,
      year: 'numeric',
      month: '2-digit',
      day: '2-digit',
      hour: '2-digit',
      minute: '2-digit',
      second: '2-digit',
      hour12: format === '12h'
    };

    const formatted = new Intl.DateTimeFormat('zh-CN', options).format(now);
    
    return {
      time: formatted,
      timezone: timezone,
      format: format,
      timestamp: now.getTime()
    };
  }

  /**
   * 网络搜索(模拟)
   */
  searchWeb({ query }) {
    // 模拟搜索结果
    return {
      query: query,
      results: [
        {
          title: `关于"${query}"的搜索结果1`,
          url: 'https://example.com/1',
          snippet: `这是关于${query}的相关信息...`
        },
        {
          title: `关于"${query}"的搜索结果2`,
          url: 'https://example.com/2',
          snippet: `更多关于${query}的内容...`
        }
      ],
      note: '这是模拟搜索结果,实际需要集成真实搜索 API'
    };
  }
}

export const functionExecutor = new FunctionExecutor();

作业3:实现 Function Calling 服务

src/services/function-calling.js:

import OpenAI from 'openai';
import { functionDefinitions } from './functions.js';
import { functionExecutor } from './function-executor.js';
import { logger } from '../utils/logger.js';

const openai = new OpenAI({
  apiKey: process.env.OPENAI_API_KEY
});

/**
 * Function Calling 聊天服务
 */
export async function chatWithFunctionCalling(
  message,
  conversationHistory = [],
  options = {}
) {
  try {
    const messages = [
      {
        role: 'system',
        content: `你是一个智能助手,可以使用工具来帮助用户。
当用户需要查询天气、计算、获取时间等信息时,你应该调用相应的函数。
调用函数后,根据返回的结果生成友好的回答。`
      },
      ...conversationHistory,
      { role: 'user', content: message }
    ];

    // 第一次调用:让 AI 决定是否调用函数
    const completion = await openai.chat.completions.create({
      model: process.env.OPENAI_MODEL || 'gpt-3.5-turbo',
      messages: messages,
      functions: functionDefinitions,
      function_call: 'auto', // 让 AI 自动决定
      temperature: 0.7
    });

    const assistantMessage = completion.choices[0].message;
    messages.push(assistantMessage);

    // 检查是否有函数调用
    if (assistantMessage.function_call) {
      const functionName = assistantMessage.function_call.name;
      const functionArgs = JSON.parse(assistantMessage.function_call.arguments);

      logger.info(`调用函数: ${functionName}`, functionArgs);

      // 执行函数
      const functionResult = await functionExecutor.execute(
        functionName,
        functionArgs
      );

      logger.info(`函数结果:`, functionResult);

      // 将函数结果添加到消息中
      messages.push({
        role: 'function',
        name: functionName,
        content: JSON.stringify(functionResult)
      });

      // 第二次调用:让 AI 根据函数结果生成回答
      const finalCompletion = await openai.chat.completions.create({
        model: process.env.OPENAI_MODEL || 'gpt-3.5-turbo',
        messages: messages,
        temperature: 0.7
      });

      return {
        content: finalCompletion.choices[0].message.content,
        functionCalled: functionName,
        functionResult: functionResult,
        usage: finalCompletion.usage
      };
    } else {
      // 没有函数调用,直接返回回答
      return {
        content: assistantMessage.content,
        functionCalled: null,
        usage: completion.usage
      };
    }
  } catch (error) {
    logger.error('Function Calling 失败:', error);
    throw error;
  }
}

/**
 * 流式 Function Calling(高级)
 */
export async function streamChatWithFunctionCalling(
  message,
  conversationHistory = [],
  callbacks = {}
) {
  const { onChunk, onFunctionCall, onComplete, onError } = callbacks;

  try {
    const messages = [
      {
        role: 'system',
        content: `你是一个智能助手,可以使用工具来帮助用户。`
      },
      ...conversationHistory,
      { role: 'user', content: message }
    ];

    // 第一次调用
    const completion = await openai.chat.completions.create({
      model: 'gpt-3.5-turbo',
      messages: messages,
      functions: functionDefinitions,
      function_call: 'auto'
    });

    const assistantMessage = completion.choices[0].message;
    messages.push(assistantMessage);

    if (assistantMessage.function_call) {
      const functionName = assistantMessage.function_call.name;
      const functionArgs = JSON.parse(assistantMessage.function_call.arguments);

      if (onFunctionCall) {
        onFunctionCall(functionName, functionArgs);
      }

      // 执行函数
      const functionResult = await functionExecutor.execute(
        functionName,
        functionArgs
      );

      messages.push({
        role: 'function',
        name: functionName,
        content: JSON.stringify(functionResult)
      });

      // 流式生成最终回答
      const stream = await openai.chat.completions.create({
        model: 'gpt-3.5-turbo',
        messages: messages,
        stream: true
      });

      let fullContent = '';

      for await (const chunk of stream) {
        const content = chunk.choices[0]?.delta?.content || '';
        if (content) {
          fullContent += content;
          if (onChunk) {
            onChunk(content);
          }
        }
      }

      if (onComplete) {
        onComplete({
          content: fullContent,
          functionCalled: functionName,
          functionResult: functionResult
        });
      }
    } else {
      // 没有函数调用,直接流式返回
      if (onChunk) {
        onChunk(assistantMessage.content);
      }
      if (onComplete) {
        onComplete({ content: assistantMessage.content });
      }
    }
  } catch (error) {
    if (onError) {
      onError(error);
    }
    throw error;
  }
}

作业4:创建 Function Calling 路由

src/routes/function-calling.js:

import express from 'express';
import { 
  chatWithFunctionCalling,
  streamChatWithFunctionCalling 
} from '../services/function-calling.js';
import { logger } from '../utils/logger.js';

export const functionCallingRouter = express.Router();

// POST /api/function-calling/chat
functionCallingRouter.post('/chat', async (req, res) => {
  try {
    const { message, conversationHistory = [] } = req.body;

    if (!message) {
      return res.status(400).json({
        success: false,
        error: '消息内容不能为空'
      });
    }

    const result = await chatWithFunctionCalling(
      message,
      conversationHistory
    );

    res.json({
      success: true,
      data: {
        message: result.content,
        functionCalled: result.functionCalled,
        functionResult: result.functionResult,
        usage: result.usage
      }
    });
  } catch (error) {
    logger.error('Function Calling 接口错误:', error);
    res.status(500).json({
      success: false,
      error: error.message || '服务异常'
    });
  }
});

// POST /api/function-calling/stream
functionCallingRouter.post('/stream', async (req, res) => {
  try {
    const { message, conversationHistory = [] } = req.body;

    if (!message) {
      return res.status(400).json({
        success: false,
        error: '消息内容不能为空'
      });
    }

    res.setHeader('Content-Type', 'text/event-stream');
    res.setHeader('Cache-Control', 'no-cache');
    res.setHeader('Connection', 'keep-alive');

    await streamChatWithFunctionCalling(
      message,
      conversationHistory,
      {
        onFunctionCall: (functionName, args) => {
          res.write(`data: ${JSON.stringify({
            type: 'function_call',
            functionName,
            args
          })}\n\n`);
        },
        onChunk: (content) => {
          res.write(`data: ${JSON.stringify({ content })}\n\n`);
        },
        onComplete: (result) => {
          res.write(`data: ${JSON.stringify({
            done: true,
            functionCalled: result.functionCalled,
            functionResult: result.functionResult
          })}\n\n`);
          res.end();
        },
        onError: (error) => {
          res.write(`data: ${JSON.stringify({
            error: error.message
          })}\n\n`);
          res.end();
        }
      }
    );
  } catch (error) {
    logger.error('流式 Function Calling 错误:', error);
    if (!res.headersSent) {
      res.status(500).json({
        success: false,
        error: error.message
      });
    }
  }
});

作业5:前端集成 Function Calling

utils/api.js(添加 Function Calling 方法):

/**
 * Function Calling 聊天
 */
export async function chatWithFunctions(message, conversationHistory = []) {
  try {
    const response = await api.post('/function-calling/chat', {
      message,
      conversationHistory
    });
    
    if (response.success) {
      return response.data;
    } else {
      throw new Error(response.error);
    }
  } catch (error) {
    throw error;
  }
}

/**
 * 流式 Function Calling
 */
export function streamChatWithFunctions(
  message,
  conversationHistory = [],
  callbacks = {}
) {
  const { onFunctionCall, onChunk, onComplete, onError } = callbacks;

  return new Promise((resolve, reject) => {
    fetch('http://localhost:3000/api/function-calling/stream', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        message,
        conversationHistory
      })
    })
    .then(response => {
      if (!response.ok) {
        throw new Error('请求失败');
      }

      const reader = response.body.getReader();
      const decoder = new TextDecoder();
      let buffer = '';

      function readStream() {
        reader.read().then(({ done, value }) => {
          if (done) {
            resolve();
            return;
          }

          buffer += decoder.decode(value, { stream: true });
          const lines = buffer.split('\n');
          buffer = lines.pop() || '';

          for (const line of lines) {
            if (line.startsWith('data: ')) {
              try {
                const data = JSON.parse(line.substring(6));
                
                if (data.error) {
                  if (onError) onError(new Error(data.error));
                  reject(new Error(data.error));
                  return;
                }

                if (data.done) {
                  if (onComplete) onComplete(data);
                  resolve(data);
                  return;
                }

                if (data.type === 'function_call' && onFunctionCall) {
                  onFunctionCall(data.functionName, data.args);
                }

                if (data.content && onChunk) {
                  onChunk(data.content);
                }
              } catch (e) {
                console.error('解析失败:', e);
              }
            }
          }

          readStream();
        }).catch(error => {
          if (onError) onError(error);
          reject(error);
        });
      }

      readStream();
    })
    .catch(error => {
      if (onError) onError(error);
      reject(error);
    });
  });
}

作业6:测试 Function Calling

test-function-calling.js:

import { chatWithFunctionCalling } from './src/services/function-calling.js';

async function testWeather() {
  console.log('测试:天气查询');
  const result = await chatWithFunctionCalling('北京今天天气怎么样?');
  console.log('回答:', result.content);
  console.log('调用的函数:', result.functionCalled);
  console.log('函数结果:', result.functionResult);
}

async function testCalculate() {
  console.log('\n测试:数学计算');
  const result = await chatWithFunctionCalling('帮我计算 123 * 456 + 789');
  console.log('回答:', result.content);
  console.log('调用的函数:', result.functionCalled);
}

async function testTime() {
  console.log('\n测试:获取时间');
  const result = await chatWithFunctionCalling('现在几点了?');
  console.log('回答:', result.content);
  console.log('调用的函数:', result.functionCalled);
}

async function runTests() {
  await testWeather();
  await testCalculate();
  await testTime();
}

runTests();

遇到的问题

问题1:函数参数解析失败

错误: JSON.parse 失败

解决方案:

try {
  const functionArgs = JSON.parse(assistantMessage.function_call.arguments);
} catch (error) {
  // 尝试修复常见问题
  const fixed = assistantMessage.function_call.arguments
    .replace(/'/g, '"')  // 单引号转双引号
    .replace(/(\w+):/g, '"$1":');  // 添加引号
  const functionArgs = JSON.parse(fixed);
}

问题2:AI 不调用函数

解决方案:

// 1. 改进函数描述,更清晰
description: "获取指定城市的当前天气信息。当用户询问天气时,必须调用此函数。"

// 2. 在系统提示词中强调
content: "当用户询问天气、时间、计算等问题时,你必须调用相应的函数。"

// 3. 使用 function_call: 'required'(强制调用)

问题3:函数执行错误

解决方案:

try {
  const result = await functionExecutor.execute(functionName, functionArgs);
  return result;
} catch (error) {
  // 返回错误信息给 AI
  return {
    error: error.message,
    functionName: functionName,
    args: functionArgs
  };
}

学习总结

今日收获

  1. ✅ 理解 Function Calling 概念
  2. ✅ 掌握函数 Schema 定义
  3. ✅ 实现函数执行器
  4. ✅ 集成外部 API
  5. ✅ 实现完整 Function Calling 流程

关键知识点

  • Function Calling 让 AI 更强大,可以调用外部工具
  • 函数定义要清晰,描述和参数要准确
  • 两阶段调用:先决定调用,再生成回答
  • 错误处理重要,函数执行失败要有降级方案
  • 实际应用广泛:天气、计算、搜索等

Function Calling 优势

  • ✅ 扩展 AI 能力
  • ✅ 实时数据获取
  • ✅ 精确计算
  • ✅ 外部服务集成

明日计划

明天将学习:

期待明天的学习! 🚀


参考资源


代码仓库

项目已更新:

  • ✅ Function Calling 服务
  • ✅ 函数执行器
  • ✅ 天气查询功能
  • ✅ 测试示例

GitHub 提交: Day 07 - Function Calling 实现


标签: #AI学习 #FunctionCalling #工具调用 #API集成 #学习笔记


写在最后

今天学习了 Function Calling,这是让 AI 更强大的关键技术。
通过函数调用,AI 可以获取实时数据、执行计算、调用外部服务。
明天将学习向量数据库,为 RAG 应用打下基础!

继续加油! 💪


快速检查清单

完成这些,第七天就达标了!

posted @ 2025-12-16 15:02  XiaoZhengTou  阅读(1)  评论(0)    收藏  举报