Java后端接入大模型:HTTP、SDK与Spring AI三层技术选型 上周有个老同事问我你是不是天天在用大模型Java 后端到底怎么接他以为我要么甩给他一个框架地址要么丢一段 HttpClient 代码。我说两种都对但真正的答案是一层一层往上搭——先明白 HTTP 层面发生了什么再去碰 SDK最后用框架解放双手。这个顺序很重要倒过来先跑框架 demo 的人遇到流式报错、超时、内容过滤这类问题时多半会非常痛苦因为根本不知道请求长什么样也不知道错误到底是从哪一层冒出来的。这篇文章就沿着这条路线展开先用原生 HTTP 请求把大模型接口的本质讲透再拆解手写请求时的隐藏难点然后看 SDK 层帮你封装了什么、代价又是什么最后聊到开发框架层以 Spring AI 为例给工程化带来的质变。全文核心解决一个问题Java 项目里接入大模型到底该从哪一层入手各层之间怎么取舍。想直接抄代码的可以跳着看但建议先把前两章读完后面所有框架 API 你都会知道它背后在做什么。1. 大模型 API 的本质先搞懂它在传输层发生了什么1.1 一个 POST 请求加一段 JSON这就是全部现在市面上主流的大模型服务几乎都默认提供 OpenAI 兼容协议也就是/v1/chat/completions这个端点。不管底层是哪个团队训出来的模型对外暴露的接口长得都差不多你提交一段 JSON里面包含模型名称、消息列表、生成参数服务端处理完再返回一段 JSON。这个过程在传输层上就是一个普通的 HTTPS POST和其他业务接口没有本质区别。也就是说Java 里调用大模型的最小单位不是某个神奇的框架而是你手里本来就有的java.net.http.HttpClient。Java 11 开始 JDK 自带这个客户端不需要任何第三方依赖就能发请求。很多人一听接入大模型就觉得很高端其实第一步是把心态摆正这就是一次带鉴权头的网络调用。请求报文的核心字段如下{ model: 模型名称, messages: [ {role: system, content: 你是一个后端开发助手}, {role: user, content: 用 Java 实现一个单例类} ], temperature: 0.7, max_tokens: 1024, stream: false }model决定你调用的是哪个具体模型messages是对话上下文temperature控制随机性max_tokens限制生成长度stream决定是否流式返回。响应报文则长这样{ choices: [ { message: {role: assistant, content: ...}, finish_reason: stop } ], usage: { prompt_tokens: 32, completion_tokens: 64, total_tokens: 96 } }1.2 字段背后要命的细节token 与生成陷阱这一段必须展开讲因为很多人栽在这里。第一是max_tokens的估算。不要把这个值理解成字符数它是 token 数。在中文场景下一个汉字大约占 1.5 到 2 个 token英文单词平均 1.3 个 token 左右。如果你让模型写一篇长文但max_tokens只给了 256输出会被硬生生截断finish_reason会变成length而不是stop。业务上如果没判断finish_reason就可能把半截话当成完整答案返回给用户这是非常典型的低级线上事故。第二是temperature。这个参数看似简单实际影响极大。temperature0时模型行为接近贪心解码输出高度确定适合抽取实体、生成 JSON 结构这类场景。temperature0.7以上时随机性明显增加适合文案、创意写作。如果你的业务对一致性有要求比如给下游系统直接吃输出务必把 temperature 压低否则线上会出现同样的入参、每次输出字段结构都不同的情况。第三是usage。不要忽略响应里的 token 统计它直接关系到你的成本账单。实际项目里建议每次调用后把usage落日志按天按业务线汇总。别等月底账单出来才发现某条链路的调用量异常膨胀。2. 用 Java 原生能力完成一次对话从连接参数到解析策略2.1 搭建 HttpClient 的注意点两个超时不是一回事先给出一段可以直接跑通的最小实现然后讲里面的门道。import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.time.Duration; public class ChatClientDemo { private static final String ENDPOINT https://api.example.com/v1/chat/completions; private static final String API_KEY System.getenv(LLM_API_KEY); public static void main(String[] args) throws Exception { HttpClient client HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(10)) .version(HttpClient.Version.HTTP_2) .build(); String body { model: gpt-4o-mini, messages: [ {role: system, content: 你是Java技术专家}, {role: user, content: 用一句话解释什么是对象池} ], temperature: 0.3, max_tokens: 256 } ; HttpRequest request HttpRequest.newBuilder() .uri(URI.create(ENDPOINT)) .header(Content-Type, application/json) .header(Authorization, Bearer API_KEY) .timeout(Duration.ofSeconds(120)) .POST(HttpRequest.BodyPublishers.ofString(body)) .build(); HttpResponseString response client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); } }这里最关键的一点是HttpClient.newBuilder().connectTimeout()只控制建立 TCP 连接的超时而HttpRequest.Builder.timeout()控制的是整个请求的完成时间包括服务端生成内容的耗时。大模型接口非常慢非流式模式下生成几百 token 可能需要十几秒甚至更久如果你把 request timeout 设置成 30 秒高峰期大概率直接超时。我的经验是普通对话请求给 60 秒长文档生成给 120 秒以上。千万记住这两个超时的区别否则你会看到一堆莫名其妙的HttpTimeoutException实际原因只是你超时太短。2.2 响应解析防御式处理 choices 空数组拿到响应字符串之后下一步是解析。大多数人会用 Jackson 或 Gson 直接把字符串转成JsonNode然后取choices[0].message.content。这在正常情况下没问题但一旦出问题就很隐蔽——比如请求触发了内容安全过滤接口返回 HTTP 200 但choices是空数组又比如content字段在某些情况下是null。所以我的建议是写一个专门的响应解析类统一做类型兜底ObjectNode root (ObjectNode) objectMapper.readTree(response.body()); JsonNode choices root.path(choices); if (choices.isArray() choices.size() 0) { String content choices.get(0).path(message).path(content).asText(null); String finishReason choices.get(0).path(finish_reason).asText(); // 注意content可能为nullfinishReason可能是stop/length/content_filter等 }把这段逻辑收敛到一个地方不要散落到各个业务代码里。后续如果要从直连升级到 SDK 或者框架你只需要改这一层。3. 裸写 HTTP 必然要撞上的四个坑与我的填坑方案这一段是我的血泪经验。HTTP 直连的代码写出来容易但凡是上过生产环境的下面四个坑一定至少踩过其中一个。3.1 慢响应与超时抖动别把超时参数全部一刀切大模型服务的响应时间方差极大。同样一个接口空闲时段可能 3 秒返回晚高峰可能 20 秒。如果你对所有调用都用同一个 timeout就会出现白天好好的晚上大面积超时的诡异现象。更隐蔽的是服务的网关往往有负载均衡不同节点处理速度不同抖动根本没法预测。我的方案是分级超时普通短文本请求 60 秒长文档或复杂推理请求 120 秒。同时请求头带上重试标识如果服务端支持request_id追踪务必把每个请求的request_id和业务单号关联起来。这样排查超时的时候才能定位到具体是哪个业务、哪次调用、卡在哪个环节。3.2 SSE 流式解析手写时最容易丢掉帧边界非流式响应虽然简单但用户体验差。一个长回答可能要等 15 秒才整体渲染出来这在聊天场景里是完全不可接受的。所以生产环境必然要上流式。流式接口的协议是 SSEtext/event-stream响应体不是一次性 JSON而是一串以data:开头的事件帧最后以data: [DONE]结束。手写 SSE 解析器的核心难点不在网络层而在帧边界的处理。服务端推送数据时可能把一条消息拆成多个 TCP 分片也可能一次推送多行事件。你需要按行读取忽略以冒号开头的注释行把data:前缀去掉然后用空行作为事件的结束标记。我见过不少初版实现直接按\n切分字符串结果流式数据一多就丢消息。正确做法是基于缓冲区的逐行扫描而不是一次性把整个流读进内存再 split。Java 的HttpResponse.BodyHandlers.ofLines()可以拿到一个StreamString配合过滤逻辑就能比较稳妥地处理 SSE。但注意如果用的是client.send同步调用流式返回会一直占用连接直到流结束其间要持续读取别在读取线程里做耗时操作。3.3 限流 429 与重试风暴指数退避是底线大模型服务几乎都有 QPS 和并发限制一旦超过阈值就返回 429同时响应头里可能有Retry-After告诉你多久后重试。很多团队的做法是捕获到 429 就立刻循环重试结果限流没有缓解反而因为重试风暴把服务打得更死。正确做法是重试前必须退避而且要用指数退避加抖动。1 秒 - 2 秒 - 4 秒这种固定倍数还不够同一时刻所有请求如果都按相同节奏重试退避完又会同时涌上来。所以要加随机抖动比如每次退避时间乘以一个 0.5 到 1 之间的随机系数。另外重试不能无限执行最多 3 次超过后直接降级到缓存或提示用户稍后再试。3.4 JSON 结构漂移错误响应和成功响应的形状不同最后一个坑是响应结构不稳定。服务端出错时经常返回{error: {message: ..., type: ...}}没有choices。如果你在解析代码里默认choices一定存在就会在错误处理逻辑里再抛一个空指针异常真正的错误信息反而被掩盖掉。我习惯的做法是先判断 HTTP 状态码非 200 一律按错误响应解析200 再按成功响应解析但choices依然要做空数组兜底。这个习惯救过我很多次尤其是在对接那些文档不完整、行为不规范的模型网关时。4. SDK 层再近一步封装协议细节之后你还得盯住三件事4.1 SDK 到底帮你做了什么从连接管理到 SSE 自动化手写 HTTP 虽然能跑但代码量不小尤其流式、重试、限流这些逻辑每个项目都重写一遍很傻。所以各家官方和社区都推出了 Java SDK。SDK 的核心价值是替你把协议细节包起来统一的请求构造你不再需要手写 JSON 字符串而是构造类型安全的参数对象流式响应封装SSE 解析被封装成了异步流你在回调里拿到的直接是增量文本片段而不是原始字节流重试与错误映射常见的 429、500、连接异常被统一映射成可捕获的异常类型工具调用参数生成函数调用function calling的参数 schema 可以通过注解自动生成不用手写 JSON Schema4.2 但 SDK 不是银弹版本滞后和依赖膨胀SDK 有它的麻烦。最头疼的是版本滞后模型服务的接口更新往往比 SDK 发布快新功能上线后你可能等一到两周才能用上对应版本的 SDK。遇到这种情况要么升级 SDK 并承担 API 变更的迁移成本要么临时用底层 HTTP 请求补位这时候你前面手写的 HTTP 封装反而成了救命稻草。另一个问题是依赖膨胀。一个官方的完整 SDK 会引入各种模块包括重试库、HTTP 客户端实现、核心模型层加起来可能让你的服务新增十几 MB 依赖拉长启动时间。对资源敏感、启动时长长的服务来说这不是一个小问题。我建议先看 SDK 的模块化程度按需引入别直接拖全量依赖。4.3 黑盒化以后排查问题反而变难了SDK 把底层封得越严实出了问题你就越难定位。有一次线上流式输出卡住我查看日志时发现 SDK 的日志只记录到请求发送成功之后就没有任何输出因为内部把网络层错误吞掉了。后来我只好在 SDK 外面再加一层监听器监控流式回调的完成状态。所以用 SDK 的时候务必提前确认三件事是否支持自定义日志级别、是否暴露底层HttpClient以便注入拦截器、序列化是否可自定义。如果不能就保持 SDK 版本在可控范围内并且准备一条绕过 SDK 的 HTTP 直连通道作为紧急排查手段。5. 到了框架层Spring AI 如何把调大模型变成业务开发的一部分5.1 脱离 HTTP 思维转向对话即服务如果你在一个 Spring Boot 项目里工作直接上 SDK 依然是偏底层的做法。你还需要自己管理 prompt 模板、处理多轮对话状态、做输出解析、对接异常处理。这些工作重复且没有技术含量于是到了框架层用 Spring AI 统一解决。Spring AI 的核心抽象是ChatClient。最直观的对比是用 SDK 时你写构造请求对象、发送、接收、解析、转成 DTO这五步用 Spring AI 你写的是给大模型下指令让它直接产出业务需要的结果。比如你想让模型从一段文本里抽取结构化信息Spring AI 通过StructuredOutputConverter帮你省掉手工 JSON 解析这一层让大模型输出直接反序列化成 POJO。第一次用到的同事几乎都会感叹一句这才是该有的开发体验。5.2 Prompt 模板与提示词管理的工程化业务系统里 prompt 往往不是一句固定的话而是带参数的模板。Spring AI 的PromptTemplate提供了类似字符串模板的能力把变量代入模板再交给模型。这个功能看似简单但工程价值很大它让你的提示词和 Java 代码解耦可以放到配置中心甚至数据库里运营同学不用改代码就能调整话术。我在项目里会把所有提示词集中到一个策略类里统一管理禁止业务代码里直接拼字符串这样到了上线后期改提示词的自由度会大很多。5.3 工具调用Function Calling的正确打开方式Spring AI 最有吸引力的能力之一是把 Java 方法暴露给大模型调用。它的原理是你标注一个Tool注解的方法框架启动时根据方法签名自动生成 JSON Schema放到请求的tools参数里大模型看完用户问题后如果认为自己需要调用某个工具会在响应里返回一个函数调用请求框架再帮你路由到对应的 Java 方法把方法返回值作为新的消息回传给模型完成下一轮生成。这个过程的工程价值在思维层面你不再把大模型当成一个孤立的文本生成器而是让它成为能调用后端服务、查询数据库、操作文件的协调者。在实现企业级 Agent 场景时这个能力几乎不可替代。但代价是链路变长一轮问答可能要经历HTTP 请求 - 模型判断需要调工具 - 框架反射调用方法 - 结果回传 - 模型再次生成这样的多轮交互单次用户感知的延迟会明显上升。上线前要做好超时和交互轮数上限的设计防止模型陷入工具调用的死循环。5.4 框架层最大的坑抽象漂亮但调试艰难用框架最舒服的是开发阶段最难受的也是开发阶段。因为链路被层层封装出问题时你面对的往往是一个高层的异常对象底层原因被吞掉了。比如一次超时可能是模型服务本身慢可能是网络问题也可能是框架内部的重试逻辑卡住了。你不得不在框架日志之外再接入链路追踪把每次外部调用的耗时和结果单独记录。我的处理方式是在框架外统一加一层 AOP 切面拦截所有 ChatClient 调用记录请求参数、耗时、错误类型和返回摘要。这样虽然绕了一圈但线上问题能快速定位到是框架层、网络层还是模型本身的问题。记住一个原则无论用了多高级的框架你都得保留一条最底层、最简单的排查通道。6. 三层方案怎么选我的选型思路与适用边界6.1 三层对比从响应速度、开发效率到调试成本当你知道三层各是什么之后真正的工程问题变成我的项目该用哪一层下面这张表是我自己的经验总结维度原生 HTTP 层SDK 层框架层Spring AI开发效率低大量重复代码中协议细节已封装高业务即代码可控性最高请求/响应完全透明中部分细节被隐藏低链路深且抽象调试成本低日志即真相中需熟悉 SDK 内部高需额外埋点学习曲线平缓Java 基础即可中需读 SDK 文档较陡需理解抽象概念适用场景网关、协议敏感的基础设施常规后端服务业务系统、Agent 类应用对模型迭代的适配速度最快改 JSON 即可受 SDK 发版节奏影响受框架支持度影响我不太认同无脑上框架或者框架都是过度设计这两种极端。三层不是替代关系而是分层关系。6.2 混合架构底层一条 HTTP 通道上层一套业务抽象我在实际项目里长期使用的架构是混合的最底层维护一个极简 HTTP 调用模块只负责发请求、鉴权、超时、基础重试不掺任何业务逻辑业务层在它之上建立自己的服务抽象对外暴露的方法只接受业务参数内部决定用直连还是 SDK。这样做有几个直接收益第一切换模型服务商时只需要改底层配置因为大家基本都兼容 OpenAI 协议第二做统一的降级开关、限流、审计都很方便不需要为了一个业务功能去翻框架源码第三出问题的时候永远有一条最原始的调试路径。6.3 别盯着一个模型协议意识比模型本身更值钱最后建议培养一个习惯写代码的时候不要跟某个具体模型深度绑定。模型本身会快速迭代今天的最优解几个月后就过时了但调用的协议结构相对稳定。无论是直连还是框架都要保证换模型不改代码的能力。做法上注意三点模型名称配置化不要硬编码请求参数统一走配置中心响应解析统一收敛不要把大模型的原始输出直接透传到业务下游。保持这一层的灵活性比纠结用哪个模型更重要。毕竟模型是别人的而工程架构是你自己的。7. 我在实际项目中的最终沉淀折腾了几年之后我办公室里留了一张便利贴上面只写了四行先发一个原始请求看返回、再拆流式、再上重试、最后才谈抽象。不管以后技术栈怎么变这个顺序我基本没变过。对于刚开始接触 Java 接入大模型的团队我的建议是别急着把框架塞给每个新人先让他们手写两次 HTTP 调用亲手处理一次超时和流式然后再去看 Spring AI 的封装会有截然不同的理解深度。我自己目前的主力方案是基础设施层用原生 HTTP 做统一网关入口业务侧用框架做快速迭代两者之间用一层薄薄的适配器连接。这套组合不算优雅但应付线上问题最稳。如果你想参考可以从正文里那几段最小代码开始改起把鉴权、超时、重试一点点加上去跑通了再决定要不要引入 SDK 或框架。