首页/新闻资讯/正文详情

用 Java 接入大模型 API 的工程实践:Function Calling 配置与完整代码

发布时间:2026/9/27 18:55:09 来源:云帆数科 栏目:资讯中心
用 Java 接入大模型 API 的工程实践:Function Calling 配置与完整代码
1. 为什么 Java 后端接大模型 API卡点从来不在“调通”很多 Java 同学第一次接大模型 API五分钟就能发出一个请求然后觉得这事很简单。但真把它放进订单、客服、审核这类业务里问题会一个接一个冒出来模型返回的不是纯 JSON、流式响应读成了整段、超时之后不知道该不该重试、多轮对话把 Token 越堆越高、并发一上来 Tomcat 线程全被占满。这篇聚焦一个具体场景在 Java 后端项目里用 Function Calling 让模型调用你自己的方法。我会给你一套能直接复制的 Maven 依赖、HTTP 客户端骨架、JSON 解析配置以及一次完整的工具调用请求与响应验证。跑通之后你手里就有一个可以往业务里塞的最小可用版本。适合谁看写过 Spring Boot、知道HttpClient或 RestTemplate 怎么用、但还没把大模型 API 真正工程化落地的后端开发。如果你只是想先感受一下模型对话是什么样可以先去 模型对话 页面手动发几条消息观察一下返回结构再回来看代码会更有感觉。下面所有代码都基于 OpenAI 兼容的/v1/chat/completions格式换厂商只需要改baseUrl、apiKey、model三个配置不动业务代码。2. 前置准备TaoToken 的 Key、地址与依赖2.1 拿到可用的 API Key接入的第一步是有一个能用的 Key。打开 API Keys 页面创建一个新 Key复制出来先放到环境变量里别写进代码export TAOTOKEN_API_KEYsk-你的key export TAOTOKEN_BASE_URLhttps://taotoken.net/api/v1注意Key 一旦出现在 Git 提交、日志、异常堆栈里就等于泄露。后面第九节会讲怎么脱敏。2.2 Maven 依赖HTTP 客户端用 JDK 11 自带的java.net.http.HttpClient不额外引 HTTP 库JSON 用 Jackson。pom.xml里加这几项就够properties maven.compiler.source17/maven.compiler.source maven.compiler.target17/maven.compiler.target jackson.version2.17.2/jackson.version /properties dependencies dependency groupIdcom.fasterxml.jackson.core/groupId artifactIdjackson-databind/artifactId version${jackson.version}/version /dependency dependency groupIdcom.fasterxml.jackson.datatype/groupId artifactIdjackson-datatype-jdk8/artifactId version${jackson.version}/version /dependency /dependenciesJackson 的jdk8模块是为了让Optional、LocalDateTime这类类型能正常序列化工具调用的参数里经常会用到。2.3 配置骨架用一个配置类把三个变量收口方便灰度切换厂商public record LlmConfig(String apiKey, String baseUrl, String model) { public static LlmConfig fromEnv() { return new LlmConfig( System.getenv(TAOTOKEN_API_KEY), System.getenv(TAOTOKEN_BASE_URL), System.getenv().getOrDefault(TAOTOKEN_MODEL, gpt-4o-mini) ); } }baseUrl指向https://taotoken.net/api/v1请求路径拼上/chat/completions就是完整地址。这样设计的好处是哪天要换模型或换厂商只改环境变量代码零改动。3. 可复制配置HttpClient 与 JSON 解析骨架3.1 复用 HttpClient别每次 new生产上每次请求新建HttpClient会浪费连接池。正确做法是把它做成单例配好连接超时public class LlmHttpClient { private final HttpClient http; private final ObjectMapper mapper; public LlmHttpClient() { this.http HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(10)) .version(HttpClient.Version.HTTP_1_1) .build(); this.mapper new ObjectMapper() .registerModule(new Jdk8Module()) .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); } public HttpClient http() { return http; } public ObjectMapper mapper() { return mapper; } }FAIL_ON_UNKNOWN_PROPERTIES关掉很重要模型返回的字段经常比你声明的多不关掉会直接抛异常。3.2 请求体构造用Map拼请求体比字符串拼接安全也避免手写转义MapString, Object body new LinkedHashMap(); body.put(model, config.model()); body.put(messages, messages); body.put(temperature, 0.2); body.put(tools, tools); // Function Calling 时带上 body.put(tool_choice, auto); // 让模型自己决定是否调用 String json mapper.writeValueAsString(body);temperature在工具调用场景建议调低0.1~0.3因为你要的是稳定的参数抽取不是创意发挥。3.3 发送请求HttpRequest req HttpRequest.newBuilder() .uri(URI.create(config.baseUrl() /chat/completions)) .timeout(Duration.ofSeconds(60)) .header(Authorization, Bearer config.apiKey()) .header(Content-Type, application/json) .POST(HttpRequest.BodyPublishers.ofString(json, StandardCharsets.UTF_8)) .build(); HttpResponseString resp http.send(req, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); if (resp.statusCode() ! 200) { throw new LlmException(HTTP resp.statusCode() : resp.body()); }到这里一个可复用的请求骨架就搭好了。接下来是 Function Calling 的核心部分。4. Function Calling 完整代码声明工具、解析调用、回传结果4.1 工具声明工具用 JSON Schema 描述。假设你要让模型查订单声明一个query_order_by_idString toolsJson [ { type: function, function: { name: query_order_by_id, description: 根据订单号查询订单状态订单号格式为纯数字, parameters: { type: object, properties: { orderId: { type: string, description: 订单号例如 20240517001 } }, required: [orderId] } } } ] ; JsonNode tools mapper.readTree(toolsJson);description写得越具体模型抽参数越准。我试过把“订单号格式为纯数字”写进去之后模型不再把“我的订单”这种词当订单号传进来。4.2 解析 tool_calls模型决定调用工具时返回的message里会带tool_calls数组注意arguments是 JSON 字符串不是对象JsonNode root mapper.readTree(resp.body()); JsonNode message root.path(choices).path(0).path(message); JsonNode toolCalls message.path(tool_calls); if (toolCalls.isArray() !toolCalls.isEmpty()) { JsonNode call toolCalls.get(0); String callId call.path(id).asText(); String fnName call.path(function).path(name).asText(); String argsRaw call.path(function).path(arguments).asText(); // arguments 是字符串要再解析一层 JsonNode args mapper.readTree(argsRaw); String orderId args.path(orderId).asText(); String result queryOrderById(orderId); // 真正调你的内部服务 // 把执行结果作为 tool 角色消息回传 messages.add(Map.of( role, tool, tool_call_id, callId, content, result )); // 再发一次请求让模型组织最终答复 String finalAnswer chat(messages); }这里有两个容易踩的点一是arguments必须二次readTree直接当对象用会拿到空二是回传消息必须带tool_call_id否则模型不知道这是哪次调用的结果。4.3 完整方法串起来public String askWithTools(ListMapString, Object messages, JsonNode tools) throws Exception { MapString, Object body new LinkedHashMap(); body.put(model, config.model()); body.put(messages, messages); body.put(tools, tools); body.put(tool_choice, auto); String respBody post(/chat/completions, mapper.writeValueAsString(body)); JsonNode root mapper.readTree(respBody); JsonNode message root.path(choices).path(0).path(message); if (message.has(tool_calls) message.path(tool_calls).isArray()) { // 处理工具调用见 4.2处理完递归再问一次 return handleToolCalls(message, messages, tools); } return message.path(content).asText(); }handleToolCalls里执行完工具后把结果塞回messages再调一次askWithTools模型就会基于工具结果生成自然语言答复。5. 验证请求一次完整的工具调用与成功结果5.1 准备一个假的订单服务为了本地能跑通先用一个内存 Map 模拟private String queryOrderById(String orderId) { MapString, String fakeDb Map.of( 20240517001, 已发货预计明天送达, 20240517002, 待付款 ); return fakeDb.getOrDefault(orderId, 未找到该订单); }5.2 发起请求并观察ListMapString, Object messages new ArrayList(); messages.add(Map.of(role, system, content, 你是订单助手需要查订单时调用工具。)); messages.add(Map.of(role, user, content, 帮我看看订单 20240517001 到哪了)); String answer askWithTools(messages, tools); System.out.println(answer);5.3 预期结果第一次请求返回的message里没有content只有tool_calls形如{ role: assistant, tool_calls: [ { id: call_abc123, type: function, function: { name: query_order_by_id, arguments: {\orderId\:\20240517001\} } } ] }你的代码执行queryOrderById拿到“已发货预计明天送达”回传后再请求一次最终content会是类似“您的订单 20240517001 已发货预计明天送达”的自然语言。看到这个输出说明整条链路通了。提示如果第一次返回直接是content而没有tool_calls说明模型判断不需要调工具。可以换一句更明确的用户输入比如“调用工具查一下订单 20240517001”。6. 本篇常见错排查6.1 报 401 或 403先确认Authorization头是不是Bearer加 Key中间有空格。再确认 Key 没有多余换行——从网页复制时经常带一个尾部换行trim()一下。如果还不行去 API Keys 页面确认 Key 状态正常、额度没用完。6.2 报 400提示 tools 格式错误大概率是tools传成了字符串而不是 JSON 数组。用mapper.readTree(toolsJson)转成JsonNode再放进请求体别直接put(tools, toolsJson)。6.3 arguments 解析出来是空arguments是 JSON 字符串必须mapper.readTree(argsRaw)再取字段。直接args.path(orderId)在字符串节点上取不到东西。6.4 模型不调用工具直接回答检查tool_choice是不是auto以及工具description是否足够清楚。如果模型总是自己编答案把tool_choice临时设成{type:function,function:{name:query_order_by_id}}强制调用一次验证链路再改回auto。6.5 流式场景读不到内容如果你开了stream: true响应体必须用BodyHandlers.ofInputStream()逐行读不能用ofString()否则会等整个响应结束才拿到流式就失去意义了。逐行解析时只处理以data:开头的行遇到[DONE]结束。6.6 超时后不知道该不该重试超时重试要谨慎如果这次调用已经消耗了 Token 但响应超时重试就是再花一次钱。建议只对 429限流做指数退避重试超时直接降级到兜底逻辑返回“模型繁忙请稍后再试”。具体接入细节可以对照 接入文档 里的错误码说明处理。7. 工程化收尾并发、成本与安全7.1 别阻塞 Tomcat 线程大模型调用是慢 IO直接在 Controller 里同步调会把 Tomcat 线程占满。用自定义线程池 CompletableFutureprivate final ExecutorService pool new ThreadPoolExecutor( 8, 16, 60, TimeUnit.SECONDS, new ArrayBlockingQueue(200), new ThreadPoolExecutor.CallerRunsPolicy() ); public CompletableFutureString answerAsync(String userMsg) { return CompletableFuture.supplyAsync(() - { try { return askWithTools(buildMessages(userMsg), tools); } catch (Exception e) { return 抱歉暂时无法处理请稍后再试。; } }, pool); }有界队列 CallerRunsPolicy是背压的关键队列满了让调用方自己跑避免无限堆积。7.2 上下文裁剪多轮对话别无限累加messages超过一定轮数就裁掉中间的if (messages.size() 20) { int keep 8; messages.subList(1, messages.size() - keep).clear(); // 保留 system 最近 8 条 }7.3 密钥脱敏异常信息里如果出现 Key打日志前先替换String safe raw.replaceAll(sk-[A-Za-z0-9], sk-***);7.4 长期编码场景如果你是要把 Function Calling 做成 Agent 骨架、长期跑在编码或自动化任务里单次调用成本会累积建议看一下 Coding Plan 的额度方案比按次调用更适合高频场景。控制台里也能看到每次调用的 Token 消耗方便做成本核算。8. 下一步把工具调用做成可注册的骨架上面这套代码跑通之后你会发现每加一个工具就要改一次toolsJson和handleToolCalls里的 switch很别扭。下一步可以做一个工具注册表用注解标记方法启动时扫描生成 JSON Schema调用时按function.name反射分发。这样新增工具只需要写一个方法加一个注解不用动核心逻辑。如果你还没跑通基础对话建议先去 模型对话 手动发几条带工具的请求看看原始返回长什么样再回来对照代码会快很多。跑通之后把queryOrderById换成你真实的内部服务调用注意加超时和幂等就可以往业务里灰度了。

相关推荐

3个坑避开,市级部门网站建设自评报告最佳实践指南
3个坑避开,市级部门网站建设自评报告最佳实践指南

3个坑避开,市级部门网站建设自评报告最佳实践指南 做政府信息化项目的老哥,是不是经常卡在“域名服务器搞不懂”这一步?很多市级部门在搞网站建设自评报告时,光盯着页面好不好看,结果一查后台,SSL证书快过期了,服务器IP还乱跳,直接导致评分扣光… · 2026/9/27 18:55:09

爆火的MCP!手把手教你用langchain打造自己的AI服务并接入TaoToken
爆火的MCP!手把手教你用langchain打造自己的AI服务并接入TaoToken

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/27 18:55:09

uniapp+koa+mongoose 增删改查实战:TaoToken 统一 Key 打通 SSL 配置
uniapp+koa+mongoose 增删改查实战:TaoToken 统一 Key 打通 SSL 配置

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/27 18:55:09

二进制文件编译器汇总:用 TaoToken 统一 Key 打通 Cline 与 CC Switch 配置
二进制文件编译器汇总:用 TaoToken 统一 Key 打通 Cline 与 CC Switch 配置

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/27 19:25:54

3个免费工具搞定网页版崩坏星穹铁道,拒绝建站公司拖一周
3个免费工具搞定网页版崩坏星穹铁道,拒绝建站公司拖一周

3个免费工具搞定网页版崩坏星穹铁道,拒绝建站公司拖一周 改个需求建站公司拖一周,这种憋屈事谁没经历过?明明只是想在官网加个“网页版崩坏星穹铁道”的游戏入口或者资讯页,对方却要排期、改合同,最后交付的页面还卡顿得要命。别等了,自己动手才能掌握… · 2026/9/27 19:25:54

【安装包】2026 最新版 OpenClaw 接入 DeepSeek V4 完整配置 + 避坑指南:TaoToken 统一 Key 通道实战
【安装包】2026 最新版 OpenClaw 接入 DeepSeek V4 完整配置 + 避坑指南:TaoToken 统一 Key 通道实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/27 19:25:54

从原理到示例:Java开发玩转MCP,用TaoToken统一Key打通Spring AI Alibaba
从原理到示例:Java开发玩转MCP,用TaoToken统一Key打通Spring AI Alibaba

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/27 19:25:54

不会代码也能搞:一站式wordpress性能优化实战
不会代码也能搞:一站式wordpress性能优化实战

不会代码也能搞:一站式wordpress性能优化实战 自己完全不懂代码,却想给公司或项目搞个靠谱的网站,是不是经常对着屏幕发呆?别慌,这太正常了。其实你需要的不是从零学编程,而是一套能跑起来的【一站式wordpress】解决方案。但光有方案… · 2026/9/27 19:25:48

Call to undefined function think\captcha\imagettftext():PHP GD 扩展与 php-fpm 环境排查配置指南
Call to undefined function think\captcha\imagettftext():PHP GD 扩展与 php-fpm 环境排查配置指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/27 19:25:48

MATLAB雷达信号脉冲压缩仿真:LFM线性调频、匹配滤波与距离分辨率实现
MATLAB雷达信号脉冲压缩仿真:LFM线性调频、匹配滤波与距离分辨率实现

简介:这套Matlab仿真工具完整呈现雷达信号脉冲压缩过程,从线性调频(LFM)信号生成、目标回波仿真到匹配滤波压缩处理均有可运行代码支撑,面向电子信息工程、计算机、数学等专业学生,适用于课程设计、期末大作… · 2026/9/27 0:00:01

汕头网站建设制作厂家避坑指南:5大注意事项救急
汕头网站建设制作厂家避坑指南:5大注意事项救急

汕头网站建设制作厂家避坑指南:5大注意事项救急 改个需求建站公司拖一周,这种憋屈事我见得太多了。 很多汕头老板找本地建站团队,签合同前看着方案挺美,一上线就变脸。 今天不聊虚的,直接拆解找 汕头网站建设制作厂家 时的5个核心 注意事项… · 2026/9/27 0:00:01

多模态虚假新闻检测实战:BERT+ResNet双塔与对比学习
多模态虚假新闻检测实战:BERT+ResNet双塔与对比学习

简介:基于PyTorch的多模态虚假新闻检测项目完整代码包,面向自然语言处理与计算机视觉交叉方向的开发者、科研人员及毕业设计选题者,解决社交媒体中文本与图像联合识别虚假新闻的问题。系统以BERT预训练模型提取文本语义特征,以Res… · 2026/9/27 0:00:01

MATLAB雷达信号脉冲压缩仿真:LFM线性调频、匹配滤波与距离分辨率实现
MATLAB雷达信号脉冲压缩仿真:LFM线性调频、匹配滤波与距离分辨率实现

简介:这套Matlab仿真工具完整呈现雷达信号脉冲压缩过程,从线性调频(LFM)信号生成、目标回波仿真到匹配滤波压缩处理均有可运行代码支撑,面向电子信息工程、计算机、数学等专业学生,适用于课程设计、期末大作… · 2026/9/27 0:00:01

汕头网站建设制作厂家避坑指南:5大注意事项救急
汕头网站建设制作厂家避坑指南:5大注意事项救急

汕头网站建设制作厂家避坑指南:5大注意事项救急 改个需求建站公司拖一周,这种憋屈事我见得太多了。 很多汕头老板找本地建站团队,签合同前看着方案挺美,一上线就变脸。 今天不聊虚的,直接拆解找 汕头网站建设制作厂家 时的5个核心 注意事项… · 2026/9/27 0:00:01

多模态虚假新闻检测实战:BERT+ResNet双塔与对比学习
多模态虚假新闻检测实战:BERT+ResNet双塔与对比学习

简介:基于PyTorch的多模态虚假新闻检测项目完整代码包,面向自然语言处理与计算机视觉交叉方向的开发者、科研人员及毕业设计选题者,解决社交媒体中文本与图像联合识别虚假新闻的问题。系统以BERT预训练模型提取文本语义特征,以Res… · 2026/9/27 0:00:01

了解更多?预约专属演示

我们的顾问将为您一对一讲解产品与方案

企业微信二维码