用 Java 接入大模型(十九):Function Calling 原理与 Java 实现

简介: 本文详解Function Calling核心原理:模型仅输出调用意图(JSON),不执行任何操作;执行权完全在开发者代码中。重点厘清常见误解,强调安全边界——参数不可信、需严格校验、结果须回灌、轮次需限制,并以Java实现为例说明完整流程与五大关键处理点。

阶段三收尾之后,进入阶段四:让模型会做事。 前面十八篇里,模型始终是个"说话的角色"——你给它文本,它还你文本。但有一类需求它靠说话解决不了:查数据库、调接口、改状态。这一篇讲 Function Calling,也是整个阶段里最需要先讲清原理的一环,因为很多人对它的第一个理解就是错的。


一、模型做不到的三件事

先把需求讲清楚。有这么三类事,模型自己办不到:

  • 内部数据:某个订单的当前状态、公司内部的库存数。这些不在训练数据里,也不该在。给模型看就等于泄密。
  • 实时状态:现在几点、今天的汇率、服务器是否在报警。模型的知识有截止时间,它不知道当下。
  • 执行动作:发一封邮件、创建一条工单、扣减库存。模型只能输出文本,它没法真的动手。

这三类的共同点是:信息或能力在模型之外,在你自己的系统里。

而传统的做法是让用户"多问一句"——先问模型,模型说"我查不了",再让用户自己去系统里查。体验很差。

Function Calling 解决的正是这件事:让模型有能力"申请"调用你的程序,由你的程序去取数据或执行动作,再把结果交回给模型继续回答。


二、最重要的一条认知:模型不执行任何东西

这一节是整篇的核心。如果只记一句话,记这句:模型不调用函数,它只是告诉你想调用哪个函数。

拆开看一次完整交互:

  1. 你把"用户的问题"和"有哪些函数可用"一起发给模型;
  2. 模型返回的不是答案,而是一个结构化的调用意图:想调用 get_weather,参数是 {"city": "北京"};
  3. 你的程序去执行 get_weather("北京");
  4. 你把执行结果回填进对话,再发给模型;
  5. 这一次模型才给出最终的自然语言回答。

执行那一步完全在你的代码里。 模型没有网络访问能力、没有数据库连接、没有你的凭证——它只输出了一段"我想干什么"的 JSON。

这个认知为什么重要?因为它直接推出两件事:

第一,安全责任在你这边。 模型的"申请"是不是合理、参数能不能信、这个用户有没有权限执行这个操作——全都要你的代码来判断。模型不会替你把关。

第二,一切本质上是文本往返。 理解了这一点,后面所有细节(消息结构、参数解析、失败处理)都会变得自然。


三、工具描述怎么写:它本身就是提示词

模型怎么知道有哪些工具可用、什么时候该用哪个?靠你给的描述。 所以工具描述不是文档,它是提示词的一部分——第七篇讲的那些原则,在这里同样适用。

一个够用的定义长这样:

{
   
  "type": "function",
  "function": {
   
    "name": "get_weather",
    "description": "查询指定城市的当前天气。当用户询问某地天气、气温、是否下雨时使用。",
    "parameters": {
   
      "type": "object",
      "properties": {
   
        "city": {
   
          "type": "string",
          "description": "城市名称,例如:北京、杭州"
        }
      },
      "required": ["city"]
    }
  }
}

三个要点值得单独说:

描述要写"什么时候用",而不只是"是什么"。 description 里如果只写"查询天气",模型在遇到"今天要带伞吗"这种问题时可能想不起来用它。把那句"当用户询问某地天气、气温、是否下雨时使用"加上,命中率会明显不同。这跟第七篇说的"把要求写具体"是同一件事。

参数也要有描述。 city 这个字段,模型需要知道格式是什么样——写"城市名称,例如:北京、杭州"比什么都不写要好,尤其当参数格式有约定时(比如日期要 YYYY-MM-DD、ID 要是纯数字)。

工具变多之后,描述之间的边界必须清晰。 这是实践里最容易出问题的地方:你有 get_user_info 和 get_user_profile 两个函数,描述里都写着"查询用户信息"——模型只能随机选。描述重叠就是让模型猜,而猜错的后果是调用了一个不该调的函数。工具数量上百时,这件事的优先级会高于模型能力本身。


四、一次调用的完整流程

把第二节那五步落到消息结构上,会看到这样的往返。

第一次请求:带上用户消息和工具列表。

{
   
  "model": "qwen3.8-max",
  "messages": [
    {
   "role": "user", "content": "北京今天天气怎么样"}
  ],
  "tools": [ /* 上面那个函数定义 */ ]
}

第一次响应:模型不回答,而是给出调用意图。

{
   
  "choices": [{
   
    "message": {
   
      "role": "assistant",
      "content": null,
      "tool_calls": [{
   
        "id": "call_abc123",
        "type": "function",
        "function": {
   
          "name": "get_weather",
          "arguments": "{\"city\": \"北京\"}"
        }
      }]
    }
  }]
}

注意两个细节:content 是 null(这一轮它不说话,只给意图),arguments 是一个 JSON 字符串,不是对象。

第二次请求:你执行完函数,把结果回填,再发一次。

{
   
  "model": "qwen3.8-max",
  "messages": [
    {
   "role": "user", "content": "北京今天天气怎么样"},
    {
   
      "role": "assistant",
      "content": null,
      "tool_calls": [{
   
        "id": "call_abc123",
        "type": "function",
        "function": {
   "name": "get_weather", "arguments": "{\"city\": \"北京\"}"}
      }]
    },
    {
   
      "role": "tool",
      "tool_call_id": "call_abc123",
      "content": "晴,18℃,东南风 2 级"
    }
  ],
  "tools": [ /* 还是那份定义 */ ]
}

第二次响应:这次才是自然语言答案。

这里有个结构上的硬要求,第五篇讲多轮对话时提过:assistant 里带 tool_calls 的那条消息,和紧随其后的 tool 结果消息必须成对出现。 而且 tool 消息里的 tool_call_id 必须和 assistant 里的 id 对应上——它是模型把"结果"和"请求"对起来的唯一依据。 少了或者对错了,模型会把结果当成一段无关的文本。

这也意味着一件事:如果你在中间对历史做了截断,必须保证这两个消息一起留或一起删,不能只留一个。这是第五篇那条"别把成对结构拆散"在工具调用场景下的具体体现。


五、Java 侧的最小实现

先手写一遍,看清每一步;理解了之后再用框架也不迟。这里只给函数调用的核心逻辑,HTTP 请求的封装沿用第四篇那套客户端。

/** 一次工具调用循环:模型可能连续多轮申请调用,所以用循环包起来 */
public String chatWithTools(List<Message> history, String userQuestion) {
   
    history.add(Message.user(userQuestion));

    // 循环上限:防止模型反复申请调用停不下来
    for (int round = 0; round < MAX_ROUNDS; round++) {
   
        ChatResponse resp = client.chat(history, TOOLS);
        Message msg = resp.firstMessage();

        // 没有工具调用意图,说明它要直接回答
        if (msg.toolCalls() == null || msg.toolCalls().isEmpty()) {
   
            return msg.content();
        }

        // 先把 assistant 这条(带 tool_calls)原样放进历史
        history.add(msg);

        // 可能一次申请多个,逐个执行
        for (ToolCall call : msg.toolCalls()) {
   
            String result;
            try {
   
                // 1. 解析参数:arguments 是 JSON 字符串,要自己 parse
                JsonNode args = mapper.readTree(call.function().arguments());
                // 2. 校验参数:不能直接信模型给的值
                validate(call.function().name(), args);
                // 3. 执行真正的业务逻辑
                result = toolRegistry.execute(call.function().name(), args);
            } catch (Exception e) {
   
                // 执行失败不是异常终止,而是把失败信息回灌给模型
                result = "调用失败:" + e.getMessage();
            }
            // 4. 结果必须带上对应的 tool_call_id
            history.add(Message.tool(call.id(), result));
        }
    }

    // 超出轮数上限,兜底:不再允许调用工具,直接让它回答
    return client.chat(history, List.of()).firstMessage().content();
}

这段代码里每一处都对应一个必须处理的点,下面逐个说明为什么。


六、五个必须处理的点

第一,arguments 是字符串,不是对象。

上面响应里的 "arguments": "{\"city\": \"北京\"}" —— 注意它整体是一个字符串。直接当成对象用会失败,必须自己 readTree 或者反序列化一次。这是个很常见的低级错误,但刚接触时几乎人人都踩。

第二,绝不能把模型给的参数当成可信数据。

这是本篇第二个重要认知。模型的参数从哪来?是从用户的自然语言里抽出来的。 也就是说,它的本质是"用户输入经过一层转述"——用户输入不可信,那么经过转述的它同样不可信。

具体后果很实在。假设你有一个 get_order_detail(order_id) 工具,用户说"帮我查一下订单 10086,不对,是别人的订单 10087"——模型完全可能把 10087 传进来。如果你的工具直接按 ID 查库返回,用户就读到了别人的订单。

所以参数校验必须在你的代码里做,而且要做到和"用户在网页表单里直接提交"一样严格:该判归属判归属、该判权限判权限、该判格式判格式。工具函数应该是"可以被互联网直接调用"的安全级别,而不是"反正只有模型会调"。

第三,工具执行失败要回灌,不要抛异常。

上面代码里 catch 之后把失败信息写进 result 而不是往外抛。原因是:失败信息对模型是有用信息。 如果查天气的接口超时了,把"调用超时"告诉模型,它可以选择重试、换一个工具,或者如实告诉用户"暂时查不到"。

如果直接把异常抛出去结束整个请求,用户看到的就是一句"系统繁忙"——而模型其实有能力给出更好的回应。

第四,必须有轮数上限。

模型可能反复申请调用:调用 A 发现不够,再调用 B,又回头调用 A。没有上限就是一个死循环,而且每一轮都在烧 token。上面代码里的 MAX_ROUNDS 不是防御性编程,是必需品。

超出上限之后的兜底也要想好:通常是不再给它工具列表,直接要求它基于已有信息作答。这样至少能返回一个结果,而不是报错。

第五,并行调用要全部执行、全部回灌。

一条 assistant 消息里可能带多个 tool_calls(比如用户一次问了两个城市的天气)。这时要注意:每个都要执行,每个结果都要作为独立的 tool 消息回填,且各自的 tool_call_id 不能错位。 只回填一个、或者把 id 对错了,模型会认为"另一个调用没有结果"。

如果几个调用之间没有依赖关系,执行可以并行以降低延迟——但回填时要保证 id 对应正确,别让并发的顺序打乱了对应关系。


七、安全边界:把工具当成对外接口来设计

这一节把前面的安全问题收一下,同时预告下一篇的关系。

Function Calling 的安全要求,和把接口暴露给外部调用者是同一级别的。 因为从你的工具代码的视角看,那个"调用者"的意图来自一个不可信的来源(用户输入 → 模型转述)。

几条落实原则:

  • 只读优先。 查询类工具的风险远低于写入类。如果一个需求能用只读工具满足,就不要给它写的能力。写操作如果必须做,就该走确认流程。
  • 白名单。 工具注册表是白名单,只暴露明确允许的函数。不要做"根据名字动态反射调用"这种设计——那等于把内部方法全暴露了。
  • 参数严格校验。 前面讲过:类型、范围、归属、权限,一样都不能少。尤其要校验"这个资源是否属于当前用户",这是最容易漏、后果最严重的一条。
  • 限制返回内容。 工具返回给模型的内容也会进入上下文。如果它返回了敏感字段(比如完整的手机号、内部备注),这些内容会跟着进入后续所有请求。返回前过滤掉不该出现给模型看的字段。

下一篇会讲多工具编排(串行、并行、失败重试),再往后一篇讲怎么让模型安全地操作数据库——那是这条安全线上最需要谨慎的一环。


八、小结与检查清单

  • 核心认知:模型不执行函数,只输出"要调用哪个函数 + 什么参数"的意图,执行权在你手里
  • 工具描述是提示词:description 要写"什么时候用";工具变多后描述边界必须清晰,重叠就是让模型猜
  • 消息结构:assistant(tool_calls) 与 tool 结果必须成对,tool_call_id 必须对应;截断历史时这两个一起留或一起删
  • arguments 是 JSON 字符串,要自己解析
  • 参数绝不可信:它本质是"用户输入经模型转述",校验标准要和对外接口一样严,尤其要判资源归属
  • 失败要回灌,不要抛异常结束——失败信息对模型是有用信息
  • 必须有轮数上限,超限后去掉工具列表让它直接作答
  • 并行调用:全部执行、全部回填、id 不错位
  • 安全设计:只读优先、注册表白名单、参数严格校验、返回内容过滤

一道判断标准:如果你的工具函数被直接暴露到公网上,会不会出问题? 会的话,那它就不该交给模型调用——因为模型传进来的参数,本质上和你从一个陌生请求里收到的参数一样不可信。


下一篇

单个工具的调用跑通了。但真实需求很少这么简单:模型可能要先查用户、再查订单、最后算价格,中间某一步还可能失败。下一篇讲多工具编排——串行与并行的选择、调用链上的参数校验放在哪、中途失败怎么重试或改道,以及为什么"让模型自己决定调用顺序"这件事需要格外谨慎。


本文是「用 Java 接入大模型」系列第十九篇,阶段四开篇。上一篇讲了把 RAG 接进已有 Java 服务,建议按顺序看。文中消息结构与字段名以 OpenAI 兼容接口为例,具体以你使用的模型文档为准。

目录
相关文章
|
14天前
|
Linux 开发工具 iOS开发
Codex保姆级教程(下载+安装+登录+使用,含所有版本,2026最新)
Codex是OpenAI推出的开源AI编程智能体,本地运行,可理解整个代码库,支持自然语言指令改代码、执行命令、跑测试、修Bug,兼容Win/macOS/Linux。(239字)
5971 44
|
14天前
|
JavaScript API iOS开发
DeepSeek Harness下载、安装、配置一篇搞定(2026整理,送常用插件)
DeepSeek Harness(DSH)是DeepSeek于2026年开源的MIT协议AI Agent框架,支持插件化组装模型、工具、沙箱与UI,兼容主流大模型。本文提供免Node.js的桌面端安装包(Win/macOS),含详细图文教程及热门插件推荐。
5039 60
|
9天前
|
Linux 开发工具 iOS开发
2026最新codex下载安装保姆级使用教程(下载+安装+登录+使用,含所有历史版本)
本文详解OpenAI Codex国内快速上手指南:提供Win/macOS/Linux全平台免安装包、npm/Homebrew等多方式安装、账号/API Key双登录、简体中文切换,覆盖下载、安装、汉化、使用及高频避坑(如微软商店失败、npm超时等),10分钟即可跑通。
2387 6
|
6天前
|
人工智能 安全 API
DeepSeek Harness下载、安装、配置一篇搞定(2026整理,送常用插件)
DeepSeek Harness(dsh)是DeepSeek AI于2026年开源的轻量级Agent运行框架,基于Cordis内核,践行“一切皆插件”理念。支持Web/桌面双端,开箱即用;内置多模型接入、丰富插件生态(超3.5万),适配编程、办公、创作等场景。(239字)
1487 0
|
9天前
|
云安全 人工智能 安全
|
2天前
|
云安全 人工智能 安全
|
8天前
|
人工智能 并行计算 调度
本地 AI 一键部署工具 LocModel:Windows 端集成画图、视频生成、Agent 的开源模型运行方案
LocModel 是专为 Windows 设计的一站式本地 AI 工具,支持零配置启动、自动硬件检测、GGUF 模型一键加载、文生图/视频生成及 Agent 自动化任务。适配 8G 显存,内置显存分片调度,轻松实现 AI 漫剧批量创作与离线大模型调用。(239 字)

热门文章

最新文章