手搓RAG系统 – Ragent AI(6)

eve2333 发布于 8 小时前 4 次阅读


理解函数调用Function Call

RAG 只能查知识库的局限

回顾一下 RAG 的工作流程:用户提问 → 向量检索 → 召回相关文档 → 生成答案。整个流程的数据来源是知识库(向量数据库里存的文档 chunk),所以 RAG 只能回答“知识库里有什么”。

但企业场景下,用户的需求远不止查文档:

  • 查实时数据:我还剩几天年假?我这个月的考勤记录?我的销售业绩排名?
  • 查业务状态:订单 #12345 的物流到哪了?工单 #678 处理到哪一步了?
  • 执行操作:帮我请 3 天年假、帮我提交报销申请、帮我发一条通知给项目组

这些需求的共同点是:答案不在知识库里,需要调用业务系统的接口或数据库查询。

2. 传统方案的问题

面对这种需求,传统的做法有两种:

2.1 方案一:在 Prompt 里写死兜底回复

如果用户问年假余额,请回复:"您可以访问 HR 系统查询年假余额,地址:https://hr.company.com"
如果用户问订单物流,请回复:"您可以在订单详情页查看物流信息"

这种方案太死板,用户体验差。用户问“我还剩几天年假”,系统回复“请访问 HR 系统查询”,用户心里想:你不就是个智能助手吗,为什么不能直接告诉我?

2.2 方案二:用规则匹配用户意图,然后调接口

if (userQuestion.contains("年假")) {
    int days = hrSystem.getAnnualLeave(userId);
    return "您还剩 " + days + " 天年假";
}
if (userQuestion.contains("订单") && userQuestion.contains("物流")) {
    String status = logisticsSystem.getOrderStatus(orderId);
    return "订单物流状态:" + status;
}

这种方案的问题是:规则写不完,维护成本高。用户可能问“我的假期余额”、“还有多少天假”、“年假还剩多少”,你要为每种表达都写一条规则吗?而且用户问题里可能没有明确的关键词,比如“我想请假,但不知道还有没有额度”,这种问题用规则很难匹配。

更关键的是,这种方案把判断用户意图的逻辑硬编码在代码里,每次新增一个工具(比如新增查考勤记录的功能),你都要改代码、加规则、重新部署。

我们需要一种更灵活的方案:让模型自己判断什么时候该调工具、该调哪个工具、该传什么参数。这就是 Function Call。

Function Call 是什么

1. Function Call 的本质:模型输出调用意图

先说结论:模型不是真的调函数,它只是输出一个 JSON,告诉你“我觉得应该调某个函数,参数是这些”。真正执行函数的是你的代码。

打个比方:你去餐厅吃饭,服务员(模型)拿着菜单(工具列表)问你想吃什么(用户问题)。你说“我想吃点清淡的”,服务员根据菜单推荐“那来一份清蒸鲈鱼吧”(输出调用意图)。但服务员不会做菜,做菜的是厨房(你的代码)。服务员只是把你的需求翻译成厨房能理解的订单(JSON 格式的函数调用)。

具体来说,Function Call 的流程是这样的:

  1. 1.你定义一些工具(函数),每个工具有名字、描述、参数定义
  2. 2.把工具列表和用户问题一起发给模型
  3. 3.模型判断需要调用哪个工具,输出一个 JSON(叫 tool_calls),里面包含函数名和参数
  4. 4.你解析这个 JSON,执行对应的函数,拿到结果
  5. 5.把函数执行结果返回给模型
  6. 6.模型基于结果生成最终答案

注意第 3 步和第 4 步的区别:模型只是输出“应该调 getUserAnnualLeave 函数,参数是 userId=12345”,但不会真的去调这个函数。你的代码拿到这个 JSON 后,才去执行 getUserAnnualLeave(12345),拿到结果(比如:剩余 5 天),然后把结果返回给模型。

场景:用户问“我还剩几天年假”

第一轮:发送工具列表和用户问题

你的代码构建一个请求,包含:

  • 用户问题:我还剩几天年假
  • 工具列表:getUserAnnualLeave(查询用户年假余额)

发送给模型。

第一轮响应:模型输出调用意图

模型分析用户问题,发现需要查年假余额,于是输出:

{"tool_calls":[{"id":"call_abc123","type":"function","function":{"name":"getUserAnnualLeave","arguments":"{\"userId\": \"12345\"}"}}]}

注意模型并没有生成最终答案,而是告诉你需要调用 getUserAnnualLeave 函数。

你的代码执行函数

你解析 tool_calls,发现要调 getUserAnnualLeave,参数是 userId=12345。你执行这个函数(可能是查数据库、调 HR 系统 API),拿到结果:

{"remainingDays":5,"totalDays":10,"usedDays":5}

第二轮:把结果返回给模型

你构建第二轮请求,包含:

  • 第一轮的用户问题
  • 第一轮的模型响应(带 tool_calls
  • 函数执行结果(上面的 JSON)

发送给模型。

第二轮响应:模型生成最终答案

模型基于函数执行结果,生成最终答案:

您还剩 5 天年假(总共 10 天,已使用 5 天)。

这样有效避免了在 Prompt 里写死规则,传统 NLP 无需做意图识别,让模型在回答里生成函数调用代码

Function Call 的优势:输出格式标准化(JSON)防止模型可能生成错误不安全的代码,难以解析;Function Call 的核心优势是:把判断什么时候该调工具的逻辑交给模型,把执行工具的逻辑留给项目代码。模型擅长理解自然语言和意图识别,你的代码擅长执行具体的业务逻辑,各司其职。

OpenAI Function Call 协议详解

Function Call 最早由 OpenAI 提出,现在已经成为事实标准,协议的核心是定义工具的格式和模型响应的格式。

1. 工具定义格式(tools 数组)

你需要把工具列表以 JSON 数组的形式发给模型,每个工具的定义格式如下:

{
  "type": "function",
  "function": {
    "name": "getUserAnnualLeave",
    "description": "查询用户的年假余额,包括总天数、已使用天数、剩余天数",
    "parameters": {
      "type": "object",
      "properties": {
        "userId": {
          "type": "string",
          "description": "用户 ID"
        }
      },
      "required": ["userId"]
    }
  }
}

字段说明:

  • type:固定为 function
  • function.name:函数名,模型会在 tool_calls 里返回这个名字
  • function.description:函数的功能描述,这是模型判断是否调用该工具的关键依据
  • function.parameters:参数定义,使用 JSON Schema 格式

description 是模型判断是否调用该工具的关键。描述要清晰、具体,说明:

  • 工具的功能是什么
  • 适用于什么场景
  • 参数的含义

举个例子,如果你定义了两个工具:

[
  {
    "type": "function",
    "function": {
      "name": "getUserAnnualLeave",
      "description": "查询用户的年假余额"
    }
  },
  {
    "type": "function",
    "function": {
      "name": "getUserSickLeave",
      "description": "查询用户的病假余额"
    }
  }
]

用户问“我还剩几天年假”,模型会根据 description 判断应该调用 getUserAnnualLeave 而不是 getUserSickLeave。如果 description 写得不清楚(比如只写“查询用户信息”),模型可能会选错工具。


parameters 使用 JSON Schema 格式定义参数,常用字段:

  • type:参数类型,常用 "object"(表示参数是一个对象)
  • properties:对象的属性,每个属性有 type(类型)和 description(描述)
  • required:必填参数列表
{
  "model": "Qwen/Qwen2.5-7B-Instruct",
  "messages": [
    {
      "role": "user",
      "content": "我还剩几天年假"
    }
  ],
  "tools": [
    {
      "type": "function",
      "function": {
        "name": "getUserAnnualLeave",
        "description": "查询用户的年假余额",
        "parameters": {
          "type": "object",
          "properties": {
            "userId": {
              "type": "string",
              "description": "用户 ID"
            }
          },
          "required": ["userId"]
        }
      }
    }
  ],
  "tool_choice": "auto"
}

字段说明:

  • model:模型名称
  • messages:对话历史,格式和普通 Chat API 一样
  • tools:工具列表
  • tool_choice:控制模型是否调用工具,可选值:
    • auto:模型自己判断是否需要调用工具(默认值)
    • none:不调用工具,只生成文本回答
    • required:必须调用工具,不能只生成文本回答
    • {"type": "function", "function": {"name": "getUserAnnualLeave"}}:指定调用某个工具

大部分场景下用 auto 就行,让模型自己判断。

3. 响应格式:模型输出 tool_calls

如果模型判断需要调用工具,响应格式如下:

{
  "choices": [
    {
      "message": {
        "role": "assistant",
        "content": null,
        "tool_calls": [
          {
            "id": "call_abc123",
            "type": "function",
            "function": {
              "name": "getUserAnnualLeave",
              "arguments": "{\"userId\": \"12345\"}"
            }
          }
        ]
      },
      "finish_reason": "tool_calls"
    }
  ]
}

关键字段:

  • message.tool_calls:工具调用数组,可能包含多个工具调用(模型可能一次调用多个工具)
  • tool_calls[0].id:调用 ID,第二轮请求时需要带上这个 ID
  • tool_calls[0].function.name:要调用的函数名
  • tool_calls[0].function.arguments:函数参数,注意是 JSON 字符串,不是 JSON 对象,需要你自己解析
  • finish_reason:值为 tool_calls 表示模型输出了工具调用,而不是 stop(正常结束)

注意 message.contentnull,因为模型没有生成文本回答,而是输出了工具调用。

4. 第二轮请求:把函数执行结果返回给模型

你执行完函数后,需要把结果返回给模型,让模型生成最终答案。第二轮请求的 messages 数组要包含完整的对话历史:

{
  "model": "Qwen/Qwen2.5-7B-Instruct",
  "messages": [
    {
      "role": "user",
      "content": "我还剩几天年假"
    },
    {
      "role": "assistant",
      "content": null,
      "tool_calls": [
        {
          "id": "call_abc123",
          "type": "function",
          "function": {
            "name": "getUserAnnualLeave",
            "arguments": "{\"userId\": \"12345\"}"
          }
        }
      ]
    },
    {
      "role": "tool",
      "tool_call_id": "call_abc123",
      "content": "{\"remainingDays\": 5, \"totalDays\": 10, \"usedDays\": 5}"
    }
  ]
}

关键点:

  1. 1.第一条消息:用户的原始问题
  2. 2.第二条消息:第一轮的模型响应(带 tool_calls),roleassistant
  3. 3.第三条消息:函数执行结果,roletooltool_call_id 要和第二条消息里的 id 对应,content 是函数返回值(JSON 字符串)

模型基于这些信息生成最终答案:

{
  "choices": [
    {
      "message": {
        "role": "assistant",
        "content": "您还剩 5 天年假(总共 10 天,已使用 5 天)。"
      },
      "finish_reason": "stop"
    }
  ]
}

这次 finish_reasonstop,表示模型生成了最终答案。



Function Call 在 RAG 系统中的应用

1. 意图识别:查知识库 vs 调工具

Function Call 可以作为意图识别的手段。你可以定义一个 searchKnowledgeBase 工具:

{
  "type": "function",
  "function": {
    "name": "searchKnowledgeBase",
    "description": "在企业知识库中搜索相关文档,适用于查询公司制度、产品文档、操作指南等静态知识",
    "parameters": {
      "type": "object",
      "properties": {
        "query": {
          "type": "string",
          "description": "搜索关键词"
        }
      },
      "required": ["query"]
    }
  }
}

同时定义业务工具(如 getUserAnnualLeavegetOrderStatus)。模型根据用户问题判断:

  • 用户问“年假制度是什么”,调用 searchKnowledgeBase,你的代码执行 RAG 检索
  • 用户问“我还剩几天年假”,调用 getUserAnnualLeave,你的代码查 HR 系统

这样就实现了查知识库 vs 调工具的自动路由。

2. 混合场景:知识检索 + 工具调用

有些场景需要同时查知识库和调工具。比如用户问“我的订单 #12345 能退货吗”:

  1. 先调用 getOrderStatus 查订单信息(购买时间、商品类型)
  2. 再调用 searchKnowledgeBase 检索退货政策
  3. 模型综合两部分信息生成答案

这种场景下,模型可能一次输出多个 tool_calls(如果支持并行调用),或者分多轮调用(先调工具 A,根据结果决定是否调工具 B)。

// 第一轮:模型输出 tool_calls,可能包含多个工具
JsonArray toolCalls = ...;
for (JsonElement toolCall : toolCalls) {
    String functionName = ...;
    String result = executeFunction(functionName, arguments);
    // 把结果添加到 messages 数组
}

// 第二轮:把所有结果返回给模型
// 模型可能继续输出 tool_calls(需要调用更多工具),或者生成最终答案

需要循环处理,直到模型不再输出 tool_callsfinish_reasonstop)。

3. 工具调用的优先级和策略

如果定义了多个工具,模型怎么选择?

方法一:通过 description 引导

在工具描述中给出优先级提示:

{"name":"searchKnowledgeBase","description":"在企业知识库中搜索相关文档。优先使用此工具查询公司制度、产品文档等静态知识。"}

方法二:通过 tool_choice 参数控制

如果你明确知道某个场景应该调用某个工具,可以用 tool_choice 指定:

{"tool_choice":{"type":"function","function":{"name":"getUserAnnualLeave"}}}

方法三:分阶段调用

先用一个轻量级的意图识别工具判断用户需求类型,再根据类型选择具体的工具。

Function Call 的局限性与痛点

1. 工具定义的维护成本

每个工具都要手写 JSON Schema,参数多了很繁琐。比如一个工具有 10 个参数,你要写 10 个 properties,还要写 descriptiontyperequired

工具增加或修改时,要同步更新定义。如果你的工具是从数据库表自动生成的(比如每个表对应一个查询工具),手写 JSON Schema 就不现实了。

2. 跨语言和跨系统的集成问题

你的工具可能是 Java 实现的,别人的工具可能是 Python 实现的,还有的工具是 HTTP API。怎么让模型能调用不同语言、不同系统的工具?

Function Call 协议本身不解决这个问题,你需要自己实现一个工具注册和调用的框架:

  • 定义统一的工具描述格式
  • 实现跨语言的 RPC 调用(如 gRPC、HTTP)
  • 处理不同系统的认证和权限

3. 工具的权限和安全控制

模型可能会调用不该调用的工具,或者传入不合法的参数。比如:

  • 用户 A 问“帮我查用户 B 的年假”,模型调用 getUserAnnualLeave(userId=B),但用户 A 没有权限查用户 B 的数据
  • 用户传入恶意参数(SQL 注入、路径穿越等)

需要在执行函数前做权限校验和参数校验:

private static String executeFunction(String functionName, String arguments, String currentUserId) {
    JsonObject args = gson.fromJson(arguments, JsonObject.class);

    if ("getUserAnnualLeave".equals(functionName)) {
        String userId = args.get("userId").getAsString();
        // 权限校验:只能查自己的数据
        if (!userId.equals(currentUserId)) {
            return "{\"error\": \"无权限查询其他用户的数据\"}";
        }
        // 参数校验:userId 格式检查
        if (!userId.matches("\\d+")) {
            return "{\"error\": \"用户 ID 格式错误\"}";
        }
        // 执行函数
        return getUserAnnualLeave(userId);
    }
}

但这些逻辑都要自己写,没有统一的框架支持。

4. 工具调用的可观测性

模型调用了哪些工具、传了什么参数、执行结果是什么、耗时多少,这些信息需要自己记录和追踪:

private static String executeFunction(String functionName, String arguments) {
    long startTime = System.currentTimeMillis();
    String result = ...;
    long endTime = System.currentTimeMillis();

    // 记录日志
    logger.info("Function: {}, Arguments: {}, Result: {}, Duration: {}ms",
            functionName, arguments, result, endTime - startTime);

    return result;
}

如果工具调用链路很长(调用工具 A → 工具 A 内部调用工具 B → 工具 B 查数据库),追踪起来很麻烦。

5. 引出下一篇:MCP 协议

Function Call 解决了让模型调用工具的问题,但带来了新的问题:

  • 工具定义的维护成本高
  • 跨语言、跨系统的集成复杂
  • 权限和安全控制需要自己实现
  • 可观测性不足

业界提出了 MCP(Model Context Protocol)协议,试图解决这些问题。MCP 的核心思想是:

  • 定义统一的工具描述和调用协议
  • 支持工具的动态发现和注册
  • 提供标准的权限和安全控制机制
  • 内置可观测性支持

下一篇详细讲 MCP 是什么、怎么用、和 Function Call 的关系。


Java 实战:完整的 Function Call 流程

1. 场景设定

我们做一个企业知识库助手,支持两个工具:

  1. 1.getUserAnnualLeave:查询用户年假余额
  2. 2.getOrderStatus:查询订单状态

用户问“我还剩几天年假”,系统调用 getUserAnnualLeave,返回结果,模型生成答案。

技术栈:

  • Java + OkHttp 调用 SiliconFlow API
  • 模型:Qwen/Qwen2.5-7B-Instruct(支持 Function Call)
  • Gson 处理 JSON

完整示例可以查看 TinyRAG 项目 com.nageoffer.ai.tinyrag.function 目录下代码。

2. Maven 依赖

<dependencies><dependency><groupId>com.squareup.okhttp3</groupId><artifactId>okhttp</artifactId><version>4.12.0</version></dependency><dependency><groupId>com.google.code.gson</groupId><artifactId>gson</artifactId><version>2.13.1</version></dependency></dependencies>

3. 完整代码示例

publicclassRAGFunctionCallDemo{privatestaticfinalStringAPI_KEY="YOUR_API_KEY";privatestaticfinalStringAPI_URL="https://api.siliconflow.cn/v1/chat/completions";privatestaticfinalStringMODEL="deepseek-ai/DeepSeek-V3";privatestaticfinalOkHttpClient client =newOkHttpClient();privatestaticfinalGson gson =newGsonBuilder().setPrettyPrinting().create();publicstaticvoidmain(String[] args)throwsIOException{// 用户问题String userQuestion ="我还剩几天年假";System.out.println("用户问题:"+ userQuestion);System.out.println("\n"+"=".repeat(60)+"\n");// 第一轮:发送工具列表和用户问题JsonObject firstResponse =callModelWithTools(userQuestion);System.out.println("第一轮响应:");System.out.println(gson.toJson(firstResponse));System.out.println("\n"+"=".repeat(60)+"\n");// 解析 tool_callsJsonArray toolCalls = firstResponse.getAsJsonArray("choices").get(0).getAsJsonObject().getAsJsonObject("message").getAsJsonArray("tool_calls");if(CollUtil.isEmpty(toolCalls)){System.out.println("模型没有调用工具,直接返回答案");return;}// 执行函数JsonObject toolCall = toolCalls.get(0).getAsJsonObject();String functionName = toolCall.getAsJsonObject("function").get("name").getAsString();String arguments = toolCall.getAsJsonObject("function").get("arguments").getAsString();String toolCallId = toolCall.get("id").getAsString();System.out.println("模型要调用的函数:"+ functionName);System.out.println("函数参数:"+ arguments);System.out.println("\n"+"=".repeat(60)+"\n");// 执行函数(这里用 mock 数据)String functionResult =executeFunction(functionName, arguments);System.out.println("函数执行结果:"+ functionResult);System.out.println("\n"+"=".repeat(60)+"\n");// 第二轮:把结果返回给模型JsonObject secondResponse =callModelWithFunctionResult(
                userQuestion, toolCall, toolCallId, functionResult);System.out.println("第二轮响应:");System.out.println(gson.toJson(secondResponse));System.out.println("\n"+"=".repeat(60)+"\n");// 提取最终答案String finalAnswer = secondResponse.getAsJsonArray("choices").get(0).getAsJsonObject().getAsJsonObject("message").get("content").getAsString();System.out.println("最终答案:"+ finalAnswer);}/**
     * 第一轮调用:发送工具列表和用户问题
     */privatestaticJsonObjectcallModelWithTools(String userQuestion)throwsIOException{// 定义工具列表JsonArray tools =newJsonArray();// 工具 1:查询年假余额JsonObject tool1 =newJsonObject();
        tool1.addProperty("type","function");JsonObject function1 =newJsonObject();
        function1.addProperty("name","getUserAnnualLeave");
        function1.addProperty("description","查询用户的年假余额,包括总天数、已使用天数、剩余天数");JsonObject parameters1 =newJsonObject();
        parameters1.addProperty("type","object");JsonObject properties1 =newJsonObject();JsonObject userId1 =newJsonObject();
        userId1.addProperty("type","string");
        userId1.addProperty("description","用户 ID");
        properties1.add("userId", userId1);
        parameters1.add("properties", properties1);JsonArray required1 =newJsonArray();
        required1.add("userId");
        parameters1.add("required", required1);
        function1.add("parameters", parameters1);
        tool1.add("function", function1);
        tools.add(tool1);// 工具 2:查询订单状态JsonObject tool2 =newJsonObject();
        tool2.addProperty("type","function");JsonObject function2 =newJsonObject();
        function2.addProperty("name","getOrderStatus");
        function2.addProperty("description","查询订单的物流状态和详细信息");JsonObject parameters2 =newJsonObject();
        parameters2.addProperty("type","object");JsonObject properties2 =newJsonObject();JsonObject orderId =newJsonObject();
        orderId.addProperty("type","string");
        orderId.addProperty("description","订单号");
        properties2.add("orderId", orderId);
        parameters2.add("properties", properties2);JsonArray required2 =newJsonArray();
        required2.add("orderId");
        parameters2.add("required", required2);
        function2.add("parameters", parameters2);
        tool2.add("function", function2);
        tools.add(tool2);// 构建请求体JsonObject requestBody =newJsonObject();
        requestBody.addProperty("model",MODEL);JsonArray messages =newJsonArray();// ★★★ 新增:添加系统消息,带上用户ID ★★★JsonObject systemMessage =newJsonObject();
        systemMessage.addProperty("role","system");
        systemMessage.addProperty("content","当前登录用户的ID是: user_12345");
        messages.add(systemMessage);JsonObject userMessage =newJsonObject();
        userMessage.addProperty("role","user");
        userMessage.addProperty("content", userQuestion);
        messages.add(userMessage);

        requestBody.add("messages", messages);
        requestBody.add("tools", tools);
        requestBody.addProperty("tool_choice","auto");// 发送请求returnsendRequest(requestBody);}/**
     * 第二轮调用:把函数执行结果返回给模型
     */privatestaticJsonObjectcallModelWithFunctionResult(String userQuestion,JsonObject toolCall,String toolCallId,String functionResult)throwsIOException{JsonObject requestBody =newJsonObject();
        requestBody.addProperty("model",MODEL);JsonArray messages =newJsonArray();// 第一条消息:用户问题JsonObject userMessage =newJsonObject();
        userMessage.addProperty("role","user");
        userMessage.addProperty("content", userQuestion);
        messages.add(userMessage);// 第二条消息:第一轮的模型响应(带 tool_calls)JsonObject assistantMessage =newJsonObject();
        assistantMessage.addProperty("role","assistant");
        assistantMessage.add("content",JsonNull.INSTANCE);JsonArray toolCalls =newJsonArray();
        toolCalls.add(toolCall);
        assistantMessage.add("tool_calls", toolCalls);
        messages.add(assistantMessage);// 第三条消息:函数执行结果JsonObject toolMessage =newJsonObject();
        toolMessage.addProperty("role","tool");
        toolMessage.addProperty("tool_call_id", toolCallId);
        toolMessage.addProperty("content", functionResult);
        messages.add(toolMessage);

        requestBody.add("messages", messages);// 发送请求returnsendRequest(requestBody);}/**
     * 执行函数(这里用 mock 数据模拟)
     */privatestaticStringexecuteFunction(String functionName,String arguments){JsonObject args = gson.fromJson(arguments,JsonObject.class);if("getUserAnnualLeave".equals(functionName)){// 模拟查询 HR 系统String userId = args.get("userId").getAsString();JsonObject result =newJsonObject();
            result.addProperty("userId", userId);
            result.addProperty("remainingDays",5);
            result.addProperty("totalDays",10);
            result.addProperty("usedDays",5);return gson.toJson(result);}elseif("getOrderStatus".equals(functionName)){// 模拟查询订单系统String orderId = args.get("orderId").getAsString();JsonObject result =newJsonObject();
            result.addProperty("orderId", orderId);
            result.addProperty("status","运输中");
            result.addProperty("location","北京市朝阳区分拨中心");
            result.addProperty("estimatedDelivery","2026-02-28");return gson.toJson(result);}return"{\"error\": \"未知的函数\"}";}/**
     * 发送 HTTP 请求
     */privatestaticJsonObjectsendRequest(JsonObject requestBody)throwsIOException{RequestBody body =RequestBody.create(
                gson.toJson(requestBody),MediaType.parse("application/json"));Request request =newRequest.Builder().url(API_URL).addHeader("Authorization","Bearer "+API_KEY).addHeader("Content-Type","application/json").post(body).build();try(Response response = client.newCall(request).execute()){if(!response.isSuccessful()){thrownewIOException("请求失败:"+ response);}String responseBody = response.body().string();return gson.fromJson(responseBody,JsonObject.class);}}}

4. 运行效果展示

运行上面的代码,输出如下:

用户问题:我还剩几天年假

============================================================

第一轮响应:
{
  "id": "019c9e1d26c3d808e4af0dae8cb9e15a",
  "object": "chat.completion",
  "created": 1772179236,
  "model": "deepseek-ai/DeepSeek-V3",
  "choices": [
    {
      "index": 0,
      "message": {
        "role": "assistant",
        "content": "",
        "tool_calls": [
          {
            "id": "019c9e1d2c66c68e888c9086c45216e4",
            "type": "function",
            "function": {
              "name": "getUserAnnualLeave",
              "arguments": "{\"userId\":\"user_12345\"}"
            }
          }
        ]
      },
      "finish_reason": "tool_calls"
    }
  ],
  "usage": {
    "prompt_tokens": 195,
    "completion_tokens": 23,
    "total_tokens": 218,
    "completion_tokens_details": {
      "reasoning_tokens": 0
    }
  },
  "system_fingerprint": ""
}

============================================================

模型要调用的函数:getUserAnnualLeave
函数参数:{"userId":"user_12345"}

============================================================

函数执行结果:{
  "userId": "user_12345",
  "remainingDays": 5,
  "totalDays": 10,
  "usedDays": 5
}

============================================================

第二轮响应:
{
  "id": "019c9e1d2cd655432ba32666031ca899",
  "object": "chat.completion",
  "created": 1772179238,
  "model": "deepseek-ai/DeepSeek-V3",
  "choices": [
    {
      "index": 0,
      "message": {
        "role": "assistant",
        "content": "根据查询结果,你的年假使用情况如下:\n\n- **总年假天数**:10天\n- **已使用天数**:5天\n- **剩余天数**:5天\n\n你还剩5天年假可以安排使用。如果有其他需求或需要帮助规划休假,随时告诉我哦!"
      },
      "finish_reason": "stop"
    }
  ],
  "usage": {
    "prompt_tokens": 79,
    "completion_tokens": 64,
    "total_tokens": 143
  },
  "system_fingerprint": ""
}

============================================================

最终答案:根据查询结果,你的年假使用情况如下:

- **总年假天数**:10天
- **已使用天数**:5天
- **剩余天数**:5天

你还剩5天年假可以安排使用。如果有其他需求或需要帮助规划休假,随时告诉我哦!

整个流程:

  1. 1.用户问“我还剩几天年假”
  2. 2.模型判断需要调用 getUserAnnualLeave 函数,输出 tool_calls
  3. 3.代码解析 tool_calls,执行函数,拿到结果(剩余 5 天)
  4. 4.把结果返回给模型
  5. 5.模型生成最终答案:您还剩 5 天年假(总共 10 天,已使用 5 天)

5. 代码要点说明

5.1 工具定义的构建

用 Gson 构建 JSON 比较繁琐,实际项目中可以封装一个工具类:

publicclassFunctionTool{privateString name;privateString description;privateMap<String, Object> parameters;publicJsonObjecttoJson(){// 转换为 OpenAI Function Call 格式}}

或者直接用 JSON 字符串定义工具,然后用 gson.fromJson() 解析。

5.2 参数解析

function.arguments 是 JSON 字符串,不是 JSON 对象,需要先解析:

String arguments = toolCall.getAsJsonObject("function").get("arguments").getAsString();JsonObject args = gson.fromJson(arguments,JsonObject.class);String userId = args.get("userId").getAsString();

5.3 函数路由

根据 function.name 路由到对应的函数实现:

if("getUserAnnualLeave".equals(functionName)){returngetUserAnnualLeave(args);}elseif("getOrderStatus".equals(functionName)){returngetOrderStatus(args);}

实际项目中可以用策略模式或反射来实现动态路由。

5.4 第二轮请求的消息构建

第二轮请求的 messages 数组要包含完整的对话历史,顺序不能错:

  1. 1.用户问题(role=user)
  2. 2.第一轮模型响应(role=assistant,带 tool_calls)
  3. 3.函数执行结果(role=tool,带 tool_call_id)

如果顺序错了或者缺了某条消息,模型可能无法正确生成答案。

肯定一下 Function Call 的核心价值:

  • 让模型自己判断什么时候该调工具 :不用写规则匹配,模型理解自然语言,“我还剩几天年假”和“假期余额还有多少”都能识别
  • 输出格式标准化 :模型输出 JSON 格式的 tool_calls,易于解析,不会出现“模型在回答里夹了一段代码”的混乱情况
  • 多轮对话机制成熟 :定义工具 → 模型输出调用意图 → 执行函数 → 返回结果 → 生成答案,流程清晰

这些能力本身没问题,Function Call 解决了让模型调工具的核心问题。但当工具规模化之后,Function Call 协议本身没有覆盖的那些“管理层面”的问题就暴露出来了。