LangChain Function Call 实战:让 AI 调用自定义工具

为什么需要 Function Call?

传统的 AI 对话存在一个巨大的局限:大模型无法获取实时信息,也无法执行具体操作

场景 传统 AI 支持 Function Call 的 AI
查询天气 "我无法获取实时天气信息" 调用天气 API,返回"北京今天晴天,25°C"
发送邮件 "请手动发送" 调用邮件工具,自动发送
数据库查询 无法操作 调用查询工具,返回数据

某电商平台的实践数据显示,引入 Function Calling 后,查询订单物流等操作的响应时间从 12.7 秒缩短至 3.2 秒。

Function Call 核心原理

Function Call 的核心机制是将自然语言请求转换为结构化函数调用:

复制代码
用户输入 → AI分析 → 决定调用工具 → 执行工具 → 返回结果 → AI生成最终回答

整个流程分为四个关键步骤:

  1. 意图识别:AI 理解用户想要做什么
  2. 参数提取:从用户输入中提取函数所需的参数
  3. 工具执行:系统调用对应的函数
  4. 结果增强:AI 将工具返回的结果组织成自然语言回复

阿里云百炼 Function Call 配置

确认模型支持

首先,需要确认使用的模型支持 Function Call 功能。阿里云百炼的以下模型均支持:

模型 说明 适用场景
qwen-turbo 轻量快速 简单任务,性价比最高
qwen-plus 均衡型 大多数场景,推荐使用
qwen-max 旗舰型 复杂推理任务

环境配置

创建项目并安装依赖:

bash 复制代码
mkdir langchain-function-call
cd langchain-function-call
npm init -y

# 安装依赖
npm install langchain @langchain/core @langchain/openai dotenv zod
npm install -D typescript @types/node tsx

创建 .env 文件:

bash 复制代码
BAILIAN_API_KEY=你的API Key
BAILIAN_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1

注意compatible-mode 是阿里云百炼的 OpenAI 兼容模式入口,LangChain 可以通过这个地址无缝对接。

前端常用工具函数封装

LangChain 中创建工具的标准方式是使用 tool 函数,它需要定义名称、描述和参数 Schema。

工具 1:文本格式化工具

typescript 复制代码
import { tool } from "@langchain/core/tools";
import { z } from "zod";

// 文本格式化工具 - 支持大小写转换、去除空格等
const textFormatter = tool(
  async ({ text, action }: { text: string; action: string }) => {
    switch (action) {
      case "uppercase":
        return text.toUpperCase();
      case "lowercase":
        return text.toLowerCase();
      case "trim":
        return text.trim();
      case "reverse":
        return text.split('').reverse().join('');
      default:
        return `未知操作: ${action},支持的操作: uppercase, lowercase, trim, reverse`;
    }
  },
  {
    name: "text_formatter",
    description: "对文本进行格式化处理,支持大小写转换、去除空格、反转字符串等操作",
    schema: z.object({
      text: z.string().describe("需要处理的原始文本"),
      action: z.enum(["uppercase", "lowercase", "trim", "reverse"]).describe("要执行的操作类型"),
    }),
  }
);

工具 2:数据计算工具(JSON 计算器)

typescript 复制代码
// 数据计算工具 - 支持基本的数学运算和数组操作
const dataCalculator = tool(
  async ({ expression, data }: { expression: string; data?: any }) => {
    try {
      // 处理数学表达式
      if (expression === "sum" && Array.isArray(data)) {
        return data.reduce((acc, val) => acc + Number(val), 0).toString();
      }
      if (expression === "average" && Array.isArray(data)) {
        const sum = data.reduce((acc, val) => acc + Number(val), 0);
        return (sum / data.length).toString();
      }
      if (expression === "max" && Array.isArray(data)) {
        return Math.max(...data.map(Number)).toString();
      }
      if (expression === "min" && Array.isArray(data)) {
        return Math.min(...data.map(Number)).toString();
      }
      
      // 简单数学运算
      const allowedChars = /[0-9+\-*/().\s]/;
      if (allowedChars.test(expression)) {
        // 安全地执行数学运算
        const result = Function('"use strict";return (' + expression + ')')();
        return result.toString();
      }
      
      return `无法计算表达式: ${expression}`;
    } catch (error) {
      return `计算错误: ${error}`;
    }
  },
  {
    name: "data_calculator",
    description: "执行数据计算,包括数学运算、数组求和、平均值、最大值、最小值等",
    schema: z.object({
      expression: z.string().describe("计算表达式,如 'sum', 'average', 'max', 'min' 或数学公式如 '2+3*4'"),
      data: z.array(z.number()).optional().describe("当表达式为 sum/average/max/min 时,需要提供数据数组"),
    }),
  }
);

工具 3:时间日期工具

typescript 复制代码
// 时间日期工具 - 获取当前时间、日期计算等
const dateTimeTool = tool(
  async ({ action, date, days }: { action: string; date?: string; days?: number }) => {
    const now = new Date();
    
    switch (action) {
      case "current":
        return {
          date: now.toLocaleDateString("zh-CN"),
          time: now.toLocaleTimeString("zh-CN"),
          weekday: ["周日", "周一", "周二", "周三", "周四", "周五", "周六"][now.getDay()],
          timestamp: now.getTime()
        };
      case "format":
        if (date) {
          const d = new Date(date);
          return d.toLocaleDateString("zh-CN");
        }
        return now.toLocaleDateString("zh-CN");
      case "addDays":
        if (days) {
          const future = new Date(now.getTime() + days * 24 * 60 * 60 * 1000);
          return future.toLocaleDateString("zh-CN");
        }
        return "请提供天数参数";
      default:
        return `未知操作: ${action}`;
    }
  },
  {
    name: "date_time",
    description: "获取当前日期时间、格式化日期、计算未来/过去日期",
    schema: z.object({
      action: z.enum(["current", "format", "addDays"]).describe("操作类型"),
      date: z.string().optional().describe("需要格式化的日期字符串"),
      days: z.number().optional().describe("添加的天数(正数为未来,负数为过去)"),
    }),
  }
);

Function Call 参数详解

工具定义的关键要素

创建工具时,以下三个要素至关重要:

要素 说明 示例
name 工具的唯一标识,AI 通过名称选择工具 "text_formatter"
description 工具功能描述,AI 据此判断何时调用 "对文本进行格式化处理"
schema 参数结构定义,使用 Zod 描述 z.object({ text: z.string() })

Description 编写技巧

Description 是 AI 理解工具用途的关键,编写时要注意:

typescript 复制代码
// ❌ 不好的 description - 信息不足
description: "处理文本"

// ✅ 好的 description - 清晰说明功能和使用场景
description: "对文本进行格式化处理,支持大小写转换(uppercase/lowercase)、去除空格(trim)、反转字符串(reverse)。当用户要求修改文本格式时使用此工具。"

// ✅ 更好的 description - 包含触发条件
description: "数据计算工具。当用户需要:1) 计算数学表达式如'2+3*4';2) 对数组求和/求平均/找最大最小值时使用。"

Schema 参数描述技巧

typescript 复制代码
schema: z.object({
  text: z.string().describe("需要处理的原始文本,必填"),
  action: z.enum(["uppercase", "lowercase", "trim", "reverse"])
    .describe("要执行的操作类型:uppercase转大写、lowercase转小写、trim去除首尾空格、reverse反转字符串"),
})

AI 调用工具函数完整实战

绑定工具到模型

typescript 复制代码
import { ChatOpenAI } from "@langchain/openai";
import dotenv from "dotenv";

dotenv.config();

async function functionCallDemo() {
  // 1. 初始化模型(选择支持 function calling 的模型)
  const model = new ChatOpenAI({
    apiKey: process.env.DASHSCOPE_API_KEY,
    configuration: { 
      baseURL: process.env.DASHSCOPE_API_URL 
    },
    model: "qwen-plus",  // 推荐使用 qwen-plus,功能更完善
    temperature: 0.3,
  });

  // 2. 定义工具列表
  const tools = [textFormatter, dataCalculator, dateTimeTool];
  
  // 3. 绑定工具到模型
  const modelWithTools = model.bindTools(tools);

  // 4. 用户请求
  const userRequests = [
    "把 'hello world' 转成大写",
    "帮我计算 25 * 4 + 10 等于多少",
    "今天是几号?",
    "计算数组 [10, 25, 35, 40, 15] 的平均值",
    "把 '  JavaScript is awesome  ' 两边的空格去掉"
  ];

  for (const request of userRequests) {
    console.log(`\n📝 用户: ${request}`);
    
    // 5. AI 决定是否调用工具
    const response = await modelWithTools.invoke(request);
    
    // 6. 检查是否有工具调用
    if (response.tool_calls && response.tool_calls.length > 0) {
      console.log(`🔧 AI 决定调用工具: ${response.tool_calls[0].name}`);
      console.log(`📋 参数: ${JSON.stringify(response.tool_calls[0].args)}`);
      
      // 7. 执行工具调用
      for (const toolCall of response.tool_calls) {
        let result;
        switch (toolCall.name) {
          case "text_formatter":
            result = await textFormatter.invoke(toolCall.args);
            break;
          case "data_calculator":
            result = await dataCalculator.invoke(toolCall.args);
            break;
          case "date_time":
            result = await dateTimeTool.invoke(toolCall.args);
            break;
          default:
            result = "未知工具";
        }
        console.log(`✅ 工具返回: ${typeof result === 'object' ? JSON.stringify(result) : result}`);
      }
    } else {
      console.log(`💬 AI 直接回复: ${response.content}`);
    }
  }
}

functionCallDemo();

预期输出:

text 复制代码
📝 用户: 帮我计算 25 * 4 + 10 等于多少
🔧 AI 决定调用工具: data_calculator
📋 参数: {"expression":"25 * 4 + 10"}
✅ 工具返回: 110

📝 用户: 今天是几号?
🔧 AI 决定调用工具: date_time
📋 参数: {"action":"current"}
✅ 工具返回: {"date":"2026/6/3","time":"14:11:45","weekday":"周三","timestamp":1780467105818}

📝 用户: 计算数组 [10, 25, 35, 40, 15] 的平均值
🔧 AI 决定调用工具: data_calculator
📋 参数: {"expression":"average","data":[10,25,35,40,15]}
✅ 工具返回: 25

📝 用户: 把 '  JavaScript is awesome  ' 两边的空格去掉
🔧 AI 决定调用工具: text_formatter
📋 参数: {"action":"trim","text":"  JavaScript is awesome  "}
✅ 工具返回: JavaScript is awesome

完整对话式调用(带工具结果回传)

在实际应用中,工具执行的结果需要返回给 AI,让 AI 生成最终的自然语言回复:

typescript 复制代码
import { ChatOpenAI } from "@langchain/openai";
import { tool } from "@langchain/core/tools";
import { z } from "zod";
import { HumanMessage, ToolMessage, AIMessage } from "@langchain/core/messages";
import dotenv from "dotenv";

dotenv.config();

async function fullFunctionCallDemo() {
  // 定义工具
  const calculator = tool(
    async ({ expression }: { expression: string }) => {
      try {
        const result = Function('"use strict";return (' + expression + ')')();
        return result.toString();
      } catch (error) {
        return `计算错误: ${error}`;
      }
    },
    {
      name: "calculator",
      description: "执行数学计算,支持加减乘除和括号",
      schema: z.object({
        expression: z.string().describe("数学表达式,如 '2+3*4' 或 '(10-5)*2'"),
      }),
    }
  );

  const weatherTool = tool(
    async ({ city }: { city: string }) => {
      // 模拟天气查询
      const weathers: Record<string, string> = {
        "北京": "晴天,25°C,湿度40%",
        "上海": "多云,28°C,湿度65%",
        "深圳": "阵雨,30°C,湿度80%",
      };
      return weathers[city] || `${city}:晴天,22°C,湿度50%`;
    },
    {
      name: "get_weather",
      description: "获取指定城市的天气信息",
      schema: z.object({
        city: z.string().describe("城市名称,如'北京'、'上海'"),
      }),
    }
  );

  const tools = [calculator, weatherTool];
  const model = new ChatOpenAI({
    openai_api_key: process.env.BAILIAN_API_KEY,
    configuration: { baseURL: process.env.BAILIAN_BASE_URL },
    model: "qwen-plus",
    temperature: 0.3,
  });
  const modelWithTools = model.bindTools(tools);

  // 用户请求
  const userInput = "北京今天天气怎么样?另外帮我算一下 (100-25)*3+50 等于多少";

  console.log(`📝 用户: ${userInput}\n`);

  // 构建消息
  const messages = [new HumanMessage(userInput)];

  // 第一轮:AI 决定调用工具
  const aiResponse = await modelWithTools.invoke(messages);
  messages.push(aiResponse);

  console.log(`🤖 AI 决定调用 ${aiResponse.tool_calls?.length || 0} 个工具:`);
  
  // 执行工具调用
  if (aiResponse.tool_calls) {
    for (const toolCall of aiResponse.tool_calls) {
      console.log(`   🔧 ${toolCall.name}(${JSON.stringify(toolCall.args)})`);
      
      let result;
      if (toolCall.name === "calculator") {
        result = await calculator.invoke(toolCall.args);
      } else if (toolCall.name === "get_weather") {
        result = await weatherTool.invoke(toolCall.args);
      } else {
        result = "未知工具";
      }
      
      console.log(`   ✅ 返回: ${result}`);
      
      // 将工具执行结果添加到消息中
      messages.push(new ToolMessage({
        content: result,
        tool_call_id: toolCall.id,
      }));
    }
  }

  // 第二轮:AI 根据工具结果生成最终回复
  console.log(`\n🔄 AI 整合结果中...\n`);
  const finalResponse = await modelWithTools.invoke(messages);
  
  console.log(`🤖 最终回复: ${finalResponse.content}`);
}

fullFunctionCallDemo();

预期输出:

text 复制代码
[dotenv@17.3.1] injecting env (4) from .env -- tip: ⚙️  suppress all logs with { quiet: true }
📝 用户: 北京今天天气怎么样?另外帮我算一下 (100-25)*3+50 等于多少

🤖 AI 决定调用 2 个工具:
   🔧 get_weather({"city":"北京"})
   ✅ 返回: 晴天,25°C,湿度40%
   🔧 calculator({"expression":"(100-25)*3+50"})
   ✅ 返回: 275

🔄 AI 整合结果中...

🤖 最终回复: 北京今天天气晴朗,气温25°C,湿度40%。  
而计算结果:(100 - 25) × 3 + 50 = 275。

前端结合 Function Call 的业务思路

思路一:AI 辅助代码生成与优化

typescript 复制代码
// 代码分析工具
const codeAnalyzer = tool(
  async ({ code, analysisType }: { code: string; analysisType: string }) => {
    // 模拟代码分析
    const analyses = {
      complexity: `圈复杂度: 8,建议拆分为更小的函数`,
      performance: `存在不必要的重渲染,建议使用 useMemo`,
      security: `发现 XSS 风险:未对用户输入进行转义`,
    };
    return analyses[analysisType as keyof typeof analyses] || "分析完成";
  },
  {
    name: "code_analyzer",
    description: "分析前端代码的质量、性能、安全问题",
    schema: z.object({
      code: z.string().describe("需要分析的代码"),
      analysisType: z.enum(["complexity", "performance", "security"]).describe("分析类型"),
    }),
  }
);

思路二:智能表单验证助手

typescript 复制代码
const formValidator = tool(
  async ({ value, ruleType }: { value: string; ruleType: string }) => {
    const rules: Record<string, (v: string) => boolean> = {
      email: (v) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v),
      phone: (v) => /^1[3-9]\d{9}$/.test(v),
      idCard: (v) => /^[1-9]\d{5}(18|19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[\dXx]$/.test(v),
    };
    
    const isValid = rules[ruleType]?.(value) ?? true;
    return JSON.stringify({ valid: isValid, message: isValid ? "验证通过" : `格式不正确` });
  },
  {
    name: "form_validator",
    description: "验证表单字段格式,支持邮箱、手机号、身份证等",
    schema: z.object({
      value: z.string().describe("需要验证的值"),
      ruleType: z.enum(["email", "phone", "idCard"]).describe("验证规则类型"),
    }),
  }
);

思路三:API 调用代理

typescript 复制代码
const apiCaller = tool(
  async ({ endpoint, params }: { endpoint: string; params: Record<string, string> }) => {
    // 模拟 API 调用
    const mockResponses: Record<string, any> = {
      "user/info": { id: 1, name: "张三", role: "admin" },
      "order/list": [{ id: "ORD001", amount: 299, status: "paid" }],
    };
    
    const response = mockResponses[endpoint] || { message: "数据获取成功", params };
    return JSON.stringify(response);
  },
  {
    name: "api_caller",
    description: "调用业务 API 获取数据,支持用户信息、订单列表等",
    schema: z.object({
      endpoint: z.enum(["user/info", "order/list"]).describe("API 端点"),
      params: z.record(z.string()).optional().describe("请求参数"),
    }),
  }
);

Function Call 常见问题排查

问题 1:模型不调用工具

现象:明明定义了工具,但 AI 直接回复而不调用

解决方案

  • 检查模型是否支持 Function Call(qwen-turbo、qwen-plus、qwen-max 均支持)
  • 优化工具的 description,让 AI 更清楚何时使用
  • 在 prompt 中明确提示 AI 可以使用哪些工具
typescript 复制代码
// 添加系统提示,引导 AI 使用工具
const systemPrompt = "你可以使用 calculator 工具进行数学计算,使用 get_weather 工具查询天气";

问题 2:参数传递错误

现象:工具被调用但参数格式不对

解决方案

  • 使用 Zod 的 describe 为每个参数添加详细说明
  • 在 schema 中明确参数类型和约束

问题 3:工具执行超时

现象:工具执行时间过长,整个流程卡住

解决方案

  • 为工具执行添加超时控制
  • 将耗时操作异步化

结语

通过这篇教程,我们学习了 LangChain Function Call 的核心概念和实践方法。Function Call 是让 AI 从"聊天"走向"干活"的关键能力,掌握它就能构建真正实用的 AI 应用。

对于文章中错误的地方或有任何疑问,欢迎在评论区留言讨论!

相关推荐
用户3499904939191 小时前
我用 AI 做了一个 PR Review 工作流
ai编程
DyLatte1 小时前
很多人把坚持,误以为成长
前端·后端·程序员
canonical_entropy1 小时前
自进化的两个尺度:RMSP Agent 与 AGE 方法论的深层结构对应
aigc·agent·ai编程
yingyima1 小时前
凌晨3点的警报声:定时任务监控与告警的最佳实践
前端
zach1 小时前
React中的兄弟通讯之发布订阅模式
前端·react.js
书中枫叶1 小时前
我用 Vue3 写了一个 Chrome 步骤录制插件:自动截图、本地存储、一键导出教程
前端·vue.js
达达尼昂1 小时前
AI Native 工程实践 : agent 自动化测试
前端·后端·架构
2501_940041742 小时前
前端工程化命题,覆盖性能/架构/交互
前端·交互
夜焱辰2 小时前
我花了3个月,把一个终端 AI Agent 搬进了浏览器——踩坑全记录
前端·agent