一文带你彻底掌握Function Call 的使用(nodejs版)

前言

本文使用的模型是阿里云百炼的模型 直通阿里云百炼Function Calling

要求: 需要的为nodejs 做服务器 、会基础的sse(不会也没事,会cv就完了)

记得自己配一下apiKey~

简介

AI 根据任务需求 主动触发对外部工具/服务/代码逻辑的调用
Function Call 的核心逻辑不是AI自己执行函数,而是:

  • 判断是否需要调用:AI 先分析用户需求 ------ 如果靠自身知识库能回答(比如 "地球半径多少"),就不调用;如果需要外部数据 / 操作(比如 "实时股票价格"),就决定调用。
  • 正确构造函数参数: AI 需要按照外部函数的格式要求,自动填充参数(比如调用天气API的时候,正确传入"城市","日期",避免参数缺失)
  • 处理返回结果:外部函数返回数据后,AI需要理解结果并转化为用户能懂的自然语言(而不是直接丢出API原始响应)

预备函数

js 复制代码
// 工具执行映射
export const toolExecutors = {
  get_current_weather: getCurrentWeather,
  // 未来可以添加更多工具
  // get_stock_price: getStockPrice,
  // search_web: searchWeb,
};
// 执行工具函数
export const executeTool = (functionName, args) => {
  const executor = toolExecutors[functionName];
  if (!executor) {
    throw new Error(`未找到工具: ${functionName}`);
  }
  return executor(args);
};
js 复制代码
router.post("/stream", async (req, res) => {
  console.log(req.body, "req.body");

  try {
    const {
      message,
      model = "qwen-plus",
      sessionId = null,
      systemPrompt = null,
    } = req.body;

    if (!message) {
      return res.status(400).json({ error: "message 参数不能为空" });
    }

    // 设置 SSE 头
    res.setHeader("Content-Type", "text/event-stream");
    res.setHeader("Cache-Control", "no-cache");
    res.setHeader("Connection", "keep-alive");
    res.flushHeaders();

    await streamChatWithTools(message, model, {
      onToken: (delta) => {
        console.log(delta, "token delta");
        //给前端逐字发送消息
        res.write(
          `data: ${JSON.stringify({ type: "token", content: delta })}\n\n`
        );
      },
      onToolCall: (name, args, result) => {
        console.log({ name, args, result }, "tool delta");

        res.write(
          `data: ${JSON.stringify({ type: "tool", name, args, result })}\n\n`
        );
      },
      onDone: ({ sessionId: sid, stats }) => {
        console.log("onDone");
        res.write(
          `data: ${JSON.stringify({ type: "done", sessionId: sid, stats })}\n\n`
        );
        res.end();
      },
      onError: (error) => {
        res.write(
          `data: ${JSON.stringify({
            type: "error",
            message: error.message,
          })}\n\n`
        );
        res.end();
      },
      sessionId,
      systemPrompt,
    });
  } catch (error) {
    console.error("❌ 工具流式聊天错误:", error);
    if (!res.headersSent) {
      res.status(500).json({
        success: false,
        error: error.message || "服务器内部错误",
      });
    }
  }
});

初步使用function call(非流式)

定义工具

js 复制代码
export const weatherTool = {
  type: "function",
  function: {
    name: "get_current_weather",
    description: "当你想查询指定城市的天气时非常有用。",
    parameters: {
      type: "object",
      properties: {
        location: {
          type: "string",
          description: "城市或县区,比如北京市、杭州市、余杭区等。",
        },
      },
      required: ["location"],
    },
  },
};

注意:使用的时候是一个tools数组 export const tools = [weatherTool];

创建聊天函数

js 复制代码
export const chatWithTools = async (userMessage, model = "qwen-plus") => {
  //1.-----------------------------发起请求--------------------------------

  const messages = [
    {
      role: "user",
      content: userMessage,
    },
  ];
  //模型返回
  let response = await openai.chat.completions.create({
    model,
    messages,
    tools,//工具数组
  });
  let assistantMessage = response.choices[0].message;
  //2.-------------------------判断是否需要工具----------------------------
  //
  // 确保 AI 回复不是null
  //// 兜底:如果 content 是 null,置为空字符串
  if (!assistantMessage.content) {
    assistantMessage.content = "";
  }
  // 把模型的回复加入对话历史
  messages.push(assistantMessage);
  // 判断:模型有没有要求调用工具?
  if (!assistantMessage.tool_calls) {
    console.log(`无需调用天气查询工具,直接回复:${assistantMessage.content}`);
    return {
      needToolCall: false,
      finalResponse: assistantMessage.content,
      messages,
    };
  }
  
  //3 --------------------执行工具-------------------------------------
  // 工具调用循环
  const toolCallLogs = []; // 记录所有工具日志
  while (assistantMessage.tool_calls) {
    // 取第一个工具  因为现在只能调用一个 不支持多个
    const toolCall = assistantMessage.tool_calls[0];
    const toolCallId = toolCall.id; // "call_abc123"
    const funcName = toolCall.function.name;  // "get_current_weather"
    const funcArgs = JSON.parse(toolCall.function.arguments); // { location: "北京" }
    console.log(`🔧 正在调用工具 [${funcName}],参数:`, funcArgs);
    // 执行工具函数(下方给)  返回:"北京今天是晴天,温度25°C。"
    const toolResult = await executeTool(funcName, funcArgs);
   // 记录日志
    toolCallLogs.push({
      tool: funcName,
      args: funcArgs,
      result: toolResult,
    });

   // 构造工具返回消息(符合 OpenAI 规范)
    const toolMessage = {
      role: "tool",
      tool_call_id: toolCallId, // 必须对应上面的 id
      content: toolResult,   // 工具执行结果
    };
    console.log(`工具返回:${toolMessage.content}`);
    // 把工具结果加入对话历史
    messages.push(toolMessage);
    
    
    
    
// 4. ----------------------模型总结成自然语言-----------------------------
    response = await openai.chat.completions.create({
      model,
      messages,
      tools,
    });
    assistantMessage = response.choices[0].message;
     // 兜底
    if (!assistantMessage.content) {
      assistantMessage.content = "";
    }
    
   // 把模型的总结加入对话历史
    messages.push(assistantMessage);
  }
  console.log(`助手最终回复:${assistantMessage.content}`);
  return {
    needToolCall: true,
    finalResponse: assistantMessage.content,
    toolCallLogs,
    messages,
  };
};

代码逻辑(注释有讲解,数字对应每个步骤)

1. 发起请求

创建用户信息,发起请求

模型返回 数据格式如下:

js 复制代码
{
  choices: [{
    message: {
      role: "assistant",
      content: null,  // 没有文本内容
      tool_calls: [    // 决定调用工具
        {
          id: "call_abc123",
          type: "function",
          function: {
            name: "get_current_weather",
            arguments: '{"location":"北京"}'  // 完整的 JSON 字符串
          }
        }
      ]
    }
  }]
}

2.是否需要工具

执行后的message状态

3. 执行工具

message状态

4.模型总结

执行后的模型返回的repsonse结果

此时message的最终形态

代码解惑

  • 为什么要确保content不是null

因为在"function call" 的场景下 模型返回的assisant 通常只有tool_calls(或function_call),规范允许省略文本内容,此时message.content 可能是null/undefined,

如果后续: 把这条消息直接messages.push(assistantOutput)再继续对话或在日志/前端 里做字符串,渲染文都可能因为content 为空而报错 或显示异常。

  • tool_call_id 的作用

模型返回的每个工具调用都有唯一 ID,工具结果必须带上对应的 ID,模型才能匹配"这是哪个工具的返回值"。

流程

接口请求

js 复制代码
router.post("/", async (req, res) => {
  try {
    const { message, model = "qwen-plus" } = req.body;

    if (!message) {
      return res.status(400).json({
        success: false,
        error: "message 参数不能为空",
      });
    }

    console.log(`🤖 收到工具调用请求: ${message}`);

    const result = await chatWithTools(message, model);

    console.log(`✅ 工具调用完成,最终回复: ${result.finalResponse}`);

    res.json({
      success: true,
      response: result.finalResponse,
      needToolCall: result.needToolCall,
      toolCallLogs: result.toolCallLogs || [],
      model,
    });
  } catch (error) {
    console.error("❌ 工具调用错误:", error);
    res.status(500).json({
      success: false,
      error: error.message || "服务器内部错误",
    });
  }
});

流式调用(单个工具)

注意: 工具函数名称仅在第一个流式返回的对象(delta) 中出现 核心设置:stream:true

函数

js 复制代码
export const streamChatWithTools = async (
  userMessage,
  model = "qwen-plus",
  { onToken, onToolCall, onDone, onError } = {}
) => {
 // 1. ----------------------发起请求---------------------------
  const messages = [{ role: "user", content: userMessage }];
  console.log("streamChatWithTools");

  try {
    while (true) {
      const stream = await openai.chat.completions.create({
        model,
        messages,
        tools,
        stream: true,
      });

      // 单个工具调用
      let toolCall = null;  // 存储工具调用信息
      let contentBuffer = "";  //累积文本内容
      let finishReason = null; // 记录结束原因
      
      
  //2.-------------逐块处理流式数据(目的就是拼接成完整信息)-------------------

      for await (const chunk of stream) {
        const choice = chunk.choices?.[0];
        if (!choice) continue;

        // 文本增量
        const delta = choice.delta?.content || "";
        console.log(choice.delta, "choice.delta");

        if (delta) {
          contentBuffer += delta;
          onToken?.(delta);
        }

        // 工具调用增量(单个工具)
        const deltaToolCalls = choice.delta?.tool_calls || [];
        for (const tc of deltaToolCalls) {
          // 🔴 第一次出现时初始化
          if (!toolCall) {
            toolCall = {
              id: tc.id || "",
              type: "function",
              function: { name: "", arguments: "" },
            };
          }

          // 🔴 累积拼接
          if (tc.id) toolCall.id = tc.id;
          if (tc.function?.name) toolCall.function.name += tc.function.name;
          if (tc.function?.arguments) toolCall.function.arguments += tc.function.arguments;
        }

        // 结束原因
        if (choice.finish_reason) {
          finishReason = choice.finish_reason;
        }
      }

      //3.--------------- 构造标准assistant 消息 ---------------------
      const assistantMessage = {
        role: "assistant",
        content: contentBuffer || null,
      };
      if (toolCall && toolCall.id) {
        assistantMessage.tool_calls = [toolCall]; // 🔴 单个工具也要用数组
      }
      messages.push(assistantMessage);

      // 没有工具调用,流程结束
      if (!toolCall || !toolCall.id) {
        break;
      }

      // 执行单个工具
      const funcName = toolCall.function.name;
      const funcArgs = (() => {
        try {
          return JSON.parse(toolCall.function.arguments || "{}");
        } catch {
          return {};
        }
      })();

      const result = await executeTool(funcName, funcArgs);
      const resultStr = typeof result === "string" ? result : JSON.stringify(result);

      onToolCall?.(funcName, funcArgs, resultStr);

      // 🔴 添加工具消息
      messages.push({
        role: "tool",
        tool_call_id: toolCall.id,
        content: resultStr,
      });

      // 如果模型明确结束,不再继续
      if (finishReason && finishReason !== "tool_calls") {
        break;
      }
    }

    onDone?.();
  } catch (error) {
    onError?.(error);
  }
};

代码逻辑

建议结合完整代码看 截个图放旁边最佳

处理流式数据

单工具调用时,无论返回了多少个chunk -->index 的值始终为0

chunk1(告诉你函数名,没返回任何文本信息 delta为空):

js 复制代码
{
  choices: [{
    delta: {
      role: "assistant",
      tool_calls: [{
        index: 0,
        id: "call_abc123",
        type: "function",
        function: { name: "get_current_weather", arguments: "" }
      }]
    },
    finish_reason: null
  }]
}

这个chunk可以知道deltaToolCalls

然后因为一开始toolCall是个null 所以要进行初始化 然后拼接

chunk2:

js 复制代码
{
  choices: [{
    delta: {
      tool_calls: [{
        index: 0,
        function: { arguments: "{\"location\":" }
      }]
    }
  }]
}

chunk3:

js 复制代码
{
  choices: [{
    delta: {
      tool_calls: [{
        index: 0,
        function: { arguments: "\"北京\"}" }
      }]
    },
    finish_reason: "tool_calls"
  }]
}

拼接后 :

js 复制代码
toolCall = {
  id: "call_abc123",
  type: "function",
  function: {
    name: "get_current_weather",
    arguments: "{\"location\":\"北京\"}"  // 完整的 JSON 字符串
  }
}

3.构造标准assistant信息

!!! toolCall 是有固定格式的

此时的message :

js 复制代码
messages = [
  { role: "user", content: "北京天气怎么样?" },
  { 
    role: "assistant",
    content: null,
    tool_calls: [{
      id: "call_abc123",
      type: "function",
      function: {
        name: "get_current_weather",
        arguments: "{\"location\":\"北京\"}"
      }
    }]
  }
]

将工具信息添加到messages

此时的messages:

js 复制代码
 [
      { role: "user", content: "北京天气怎么样?" },
      { 
        role: "assistant",
        content: null,
        tool_calls: [{ id: "call_abc123", ... }]
      },
      {
        role: "tool",
        tool_call_id: "call_abc123",
        content: "{\"temperature\":15,\"weather\":\"晴天\"}"
      }
]

进入二次循环

再次循环调用AI 但这次messages包含了工具结果 AI看到工具结果后开始生成自然语言服务

这次delta有数据了 就开始调用onToken每次触发 前端可以逐字显示 最后的contentBuffer就是一个完整句子:"北京今天天气晴朗,温度15度。"


最终的messages:

js 复制代码
messages = [
  { role: "user", content: "北京天气怎么样?" },
  { role: "assistant", content: null, tool_calls: [...] },
  { role: "tool", tool_call_id: "call_abc123", content: "{...}" },
  { role: "assistant", content: "北京今天天气晴朗,温度15度。" }
]

循环结束 触发onDone 对话结束

流式调用(并行工具)

核心参数 parallel_tool_calls:true

并行工具实际上是为了支持询问多次

以北京上海天气为例

单个工具只会接收北京的天气 因为目前只能执行天气函数 location 是北京 并行工具就可以调用多次

函数

js 复制代码
/**
 * 流式工具调用(支持多工具并行)
 * @param {string} userMessage - 用户消息
 * @param {string} model - 模型名称
 * @param {function} onToken - 文本增量回调 (delta: string) => void
 * @param {function} onToolCall - 工具调用回调 (toolName, args, result) => void
 * @param {function} onDone - 完成回调 () => void
 * @param {function} onError - 错误回调 (error) => void
 */
export const streamChatWithTools = async (
  userMessage,
  model = "qwen-plus",
  { onToken, onToolCall, onDone, onError } = {}
) => {
  const messages = [{ role: "user", content: userMessage }];
  console.log("streamChatWithTools");

  try {
    // 可能发生多轮:模型产生 tool_calls -> 执行工具 -> 继续流式总结
    while (true) {
      const stream = await openai.chat.completions.create({
        model,
        messages,
        tools,
        tool_choice: "auto",
        parallel_tool_calls: true, // 支持并行工具调用
        stream: true,
      });

      // 聚合流式增量:按 index 累积 tool_calls
      const toolCallsMap = new Map(); // 用来拼接工具调用的增量 index -> { id, type, function: { name, arguments } }
      let contentBuffer = ""; //用来拼接文本的增量
      let finishReason = null; // 用来记录结束原因

      for await (const chunk of stream) {
        // 工具函数名称:仅在第一个流式返回的对象(delta)中出现。
        const choice = chunk.choices?.[0];
        if (!choice) continue;
        // 文本增量
        const delta = choice.delta?.content || "";
        console.log(choice.delta, "choice.delta");

        if (delta) {
          contentBuffer += delta;
          onToken?.(delta);
        }

        // 工具调用增量(OpenAI 风格:按 index 累积)
        const deltaToolCalls = choice.delta?.tool_calls || [];
        for (const tc of deltaToolCalls) {
          const idx = tc.index ?? 0;

          //   先创建后赋值
            // 如果是这个工具的第一次出现,初始化空对象
          if (!toolCallsMap.has(idx)) {
            toolCallsMap.set(idx, {
              id: tc.id || "",
              type: "function",
              function: { name: "", arguments: "" },
            });
          }
           // 获取当前工具的累积对象
          const current = toolCallsMap.get(idx);
             // 拼接 ID(通常只在第一个 chunk 有)
          if (tc.id) current.id = tc.id;
           // 拼接函数名(可能分多次推送:"get" + "_current" + "_weather")
          if (tc.function?.name) current.function.name += tc.function.name;
             // 拼接参数(JSON 字符串分多次推送:'{"loc' + 'ation":"北京"}')
          if (tc.function?.arguments)
            current.function.arguments += tc.function.arguments;
        }

        // 结束原因
        if (choice.finish_reason) {
          finishReason = choice.finish_reason;
        }
      }

      // 本轮流式结束,若有内容则加入消息
      const assistantMessage = {
        role: "assistant",
        content: contentBuffer || null,
      };
      const toolCalls = Array.from(toolCallsMap.values()).filter((tc) => tc.id);
      if (toolCalls.length > 0) {
        assistantMessage.tool_calls = toolCalls;
      }
      messages.push(assistantMessage);

      // 没有工具调用,流程结束
      if (toolCalls.length === 0) {
        break;
      }

      // 并行执行所有工具
      const toolMessages = await Promise.all(
        toolCalls.map(async (tc) => {
          const funcName = tc.function.name;
          const funcArgs = (() => {
            try {
              return JSON.parse(tc.function.arguments || "{}");
            } catch {
              return {};
            }
          })();

          const result = await executeTool(funcName, funcArgs);
          const resultStr =
            typeof result === "string" ? result : JSON.stringify(result);

          // 通知前端工具调用结果
          onToolCall?.(funcName, funcArgs, resultStr);

          return {
            role: "tool",
            tool_call_id: tc.id,
            content: resultStr,
          };
        })
      );

      // 批量加入工具消息
      messages.push(...toolMessages);

      // 如果模型明确 finish_reason !== 'tool_calls',不再继续
      if (finishReason && finishReason !== "tool_calls") {
        break;
      }
    }

    onDone?.();
  } catch (error) {
    onError?.(error);
  }
};

这个我就不多说了 中间部分的实际上核心还是进行拼接 用map 数组都可以

然后使用Promise.all并行调用 拿到所有结果再给AI处理

官方的单(多)个工具通用的方法:

js 复制代码
const toolCalls = {};
for await (const responseChunk of stream) {
  const deltaToolCalls = responseChunk.choices[0]?.delta?.tool_calls;
  if (deltaToolCalls) {
    for (const toolCallChunk of deltaToolCalls) {
      const index = toolCallChunk.index;
      if (!toolCalls[index]) {
        toolCalls[index] = { ...toolCallChunk };
        if (!toolCalls[index].function) {
            toolCalls[index].function = { name: '', arguments: '' };
        }
      } 
      else if (toolCallChunk.function?.arguments) {
        toolCalls[index].function.arguments += toolCallChunk.function.arguments;
      }
    }
  }
}
相关推荐
新智元2 天前
AI 科学家登场!12 小时抵人类科学家半年工作量,已有 7 项大成果
人工智能·openai
新智元2 天前
PyTorch 之父闪电离职,AI 半壁江山集体致敬!
人工智能·openai
安思派Anspire3 天前
构建一个自主深度思考的RAG管道以解决复杂查询--分析最终的高质量答案(8)
aigc·openai·agent
Geo_V3 天前
OpenAI 大模型 API 使用示例
python·chatgpt·openai·大模型应用·llm 开发
新智元4 天前
全球十大AI杀入美股!最新战况曝光,第一名太意外
人工智能·openai
新智元4 天前
ICML 2026史上最严新规:LLM不得列为作者,滥用AI直接退稿
人工智能·openai
未来智慧谷4 天前
OpenAI押注的NEO人形机器人:技术拆解与消费级人形机器人落地启示
机器人·openai·人形机器人neo
Moment4 天前
Cursor 2.0 支持模型并发,我用国产 RWKV 模型实现了一模一样的效果 🤩🤩🤩
前端·后端·openai
mortimer5 天前
视频翻译中的最后一公里:口型匹配为何如此难
openai·音视频开发·视频编码
安思派Anspire5 天前
构建一个自主深度思考的RAG管道以解决复杂查询--通过网络搜索扩充知识(6)
aigc·openai·agent