1. 接入大语言模型之前先想清楚这三件事1.1 为什么后端接入优先走 HTTP 接口而不是本地推理Java 后端接入大语言模型这话听着好像很新但落到实现上本质就是“发一个 HTTP 请求、收一段 JSON 或者流式数据”。我刚接触这个领域的时候也犯过贪心的毛病觉得既然模型是开源的干脆自己拉权重到服务器上跑省得按 token 付费。结果折腾一周之后发现本地推理这套链路对后端团队来说远没有看上去那么友好。大语言模型接入分成两条路一条是本地部署自己拉开源权重用 llama.cpp、vLLM 这类推理框架把模型跑起来另一条是直接调用云厂商的模型网关像 OpenAI、谷歌 Gemini、Anthropic以及国内的智谱 GLM、阿里通义千问、百度文心等都已经把模型封装成了标准 HTTP 接口后端只需要关注发请求、收响应。我自己在本地跑过 7B 量级的量化模型当时测试用的是 4bit 量化显存占用大概在 6GB 左右单看占用好像能接受可一旦有几个人同时发起请求推理队列立刻被拉长响应时间从两三秒直接飙到十几秒。模型不是部署完就结束的你还要处理热更新、显存释放、请求排队、GPU 卡死恢复这些问题任何一个环节都会变成一个长期运维负担。想一下如果只是给内部工具加一个智能问答功能这投入产出比实在太低。所以我的结论很直接产品还在验证阶段、请求并发不高、团队又没有专门的大模型基础设施同学就走云端 HTTP 接口省下来的时间全部花在业务逻辑和产品体验上。等到请求量稳定上来了能算出模型成本和并发需求再着手私有化部署用 vLLM 这类框架做服务化那时候的投入才算花在刀刃上。1.2 同步调用、异步调用、流式调用场景怎么选HTTP 接入看着只是一个请求实际上有同步、异步、流式三种姿势选错了方案后面改起来会很难受。同步调用是最简单的后端发起请求一直阻塞到模型返回完整响应。适合的典型场景是后台任务比如批量生成商品摘要、情感分类、关键词抽取。这类场景不在乎首字多快返回反而更关心响应是不是完整可靠同步阻塞写起来也最简单。异步调用适合实时性要求不高、任务量大、需要做削峰填谷的场景。比如企业知识库每天凌晨批量给几百篇文档生成摘要直接把所有请求丢进消息队列消费端逐个向模型网关发请求失败还能自动重试。异步化把模型接口的压力和业务主链路完全隔离开主流程不会因为模型超时而被拖垮。流式调用则主要面向聊天类和交互式场景。现在各家模型网关都支持 Server-Sent EventsSSE也就是响应内容以事件流的形式一块一块返回。用户在聊天窗口里看到的“打字机效果”本质就是前端逐字渲染这些分块内容。流式的好处是首字延迟明显降低用户等待焦虑被极大缓解。三种方式不必互相排斥。我实际在项目里是同步和流式并存的管理端报表走同步调用用户聊天页面走流式调用。接入之前先弄清楚业务落在哪个象限再决定代码怎么写一个项目内完全可以混合使用。1.3 API Key 管理和成本预估是第一道坎很多后端同学上手第一件事就是打开代码直接把 API Key 写进常量里。这种做法在本地联调时没人说你代码一旦提交到 Git 仓库任何能拿到代码的人都能看到你的密钥轻则被盗刷重则泄露到公网变成“AI 代付钱包”。API Key 必须走环境变量或者配置中心。本地开发放.env文件测试和生产环境放 K8s Secret 或公司的配置中心启动时通过System.getenv()或配置管理 SDK 读取。Spring Boot 下习惯用Value(${llm.api-key})读取配置但要注意这个值不能写在 application.yml 里提交到仓库必须用环境变量占位符覆盖比如${LLM_API_KEY}。成本预估也要在接入前算一笔账。模型报价按 token 计费而 token 和字符数不是一回事。一个汉字大约占 1 到 2 个 token英文单词大约占 1.5 个 token。单次请求的整体 token 消耗等于系统提示词 多轮历史消息 当前输入 最大输出长度。如果对话历史不裁剪每一轮都会把之前所有内容重新发给模型成本是线性往上走的。后面我单独会讲上下文管理和裁剪策略这里先说结论接入第一天就要做调用量和费用监控模型网关一般都有用量明细接口后端定时拉取做展示和告警别等月底账单出来再拍大腿。2. 环境准备与选型搭出能上线的骨架2.1 JDK 与 Spring Boot 版本怎么选Java 接入大语言模型对 JDK 版本的要求其实没那么苛刻。JDK 8 也能写无非就是用 OkHttp 或 RestTemplate 发起 HTTP 请求但如果你是新建项目我的建议是直接上 JDK 17 及以上。原因很实际JDK 17 之后java.net.http.HttpClient已经稳定可用而且 Spring Boot 3.x 之后全面拥抱 Jakarta EE很多新特性只在新版本里支持。我是 2023 年下半年开始把项目从 Spring Boot 2.7 升级到 3.2 的升级的动机不是赶时髦而是 Spring 6 的响应式 WebClient 和ConfigurationProperties在很多细节上更顺手。当然老项目继续用 Spring Boot 2.x 也完全没有问题只是要注意 Javax 和 Jakarta 的包名替换风险以及 Part 类方法签名变化这些属于技术债最好提前排期处理。2.2 HTTP 客户端横向对比RestTemplate、WebClient、OkHttp 怎么选HTTP 客户端是接入大语言模型的“运输工具”。我见过有人用 RestTemplate 写同步调用也见过直接把 Apache HttpClient 搬出来封装一层。工具本身没有绝对的好坏关键看场景匹配。RestTemplate 是 Spring 自带的同步客户端配置简单用来发普通 JSON 请求够用。但它在 Spring 官方文档里已经标记为维护模式新代码不建议优先使用。WebClient 是 Spring WebFlux 里的响应式客户端既能同步阻塞又能异步响应式最关键的是它对流式响应支持非常好能直接以Flux的方式接收 SSE 数据流。OkHttp 则是一个轻量、稳定的同步/异步 HTTP 客户端拦截器生态好很多 Java 技术栈的老项目里都在用它和 Retrofit 搭配做接口封装时体验特别好。给一个参考意见新项目用 WebClient老项目如果不想引入 WebFlux 依赖就继续用 OkHttp。没有标准答案只要能处理好超时、重试、连接池这三个问题选谁都不会错。客户端风格流式支持适用场景RestTemplate同步一般老项目的最小改动方案WebClient响应式/同步原生支持 SSE新项目、高并发、流式输出OkHttp同步/异步可用需自行处理既有的 OkHttp 技术栈2.3 配置模型和接口地址方便切换和灰度接入大语言模型时一个容易忽略的设计点是模型名称和网关地址的配置化。你不能在前端硬编码模型名更不能在后端代码里把模型名写死。原因有两个第一是模型版本迭代很快厂商可能半个月就退役旧版本上线新版本要能灰度切换第二是不同环境可能需要用不同模型测试环境没必要用生产规格的高价模型。我习惯把模型配置全部收敛到一份配置类里Spring Boot 用ConfigurationProperties绑定一个llm前缀的配置组包含provider、model、api-key、endpoint、timeout、max-tokens这些字段。模型名可以通过环境变量覆盖部署到测试环境时指定LLM_MODELglm-4-flash生产环境再切到更强的模型代码一行都不用改。更进阶一点的做法是维护一个模型租户概念同一个接口层定义好多个 Provider 实现比如 OpenAI 兼容协议实现、Azure OpenAI 实现、国内厂商原生 SDK 实现启动时根据配置按需装配。我自己就是用一个LlmClient接口封装所有 Provider内部再分包实现上层业务只认这个接口换厂商只需要改配置业务代码零改动。3. 实操过程从零实现一个可用的 LLM 调用模块3.1 建项目、引依赖、备好基础类这部分直接给步骤。我用 Maven 建一个 Spring Boot 3.2 项目Java 版本设 17核心依赖只需要三样dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-webflux/artifactId /dependency dependency groupIdcom.fasterxml.jackson.core/groupId artifactIdjackson-databind/artifactId /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependencies引入 WebFlux 只是为了用 WebClient并不代表整个项目就变成响应式了。Spring MVC 项目里一样可以引入 WebFlux 依赖并使用 WebClient两者不冲突。接着定义一个最简洁的请求对象字段对齐 OpenAI 风格的请求格式这是国内绝大多数模型的兼容基准Data public class ChatRequest { private String model; private ListChatMessage messages; private Double temperature 0.7; private Integer maxTokens; } Data public class ChatMessage { private String role; // system / user / assistant private String content; }响应对象只需要关心几个关键字段choices[0].message.content是文本结果usage.prompt_tokens是输入 token 数usage.total_tokens是总消耗。把这些字段解析到ChatResponseDTO 里后面做费用统计时直接读取。3.2 同步调用先跑通整条链路同步调用是后面一切的基础。用 WebClient 构建一个单例 Bean设置好连接超时和读取超时然后写一个发送方法Service public class LlmService { private final WebClient webClient; public LlmService(Value(${llm.endpoint}) String endpoint, Value(${llm.api-key}) String apiKey) { this.webClient WebClient.builder() .baseUrl(endpoint) .defaultHeader(Authorization, Bearer apiKey) .defaultHeader(Content-Type, application/json) .build(); } public String chat(String systemPrompt, String userMessage) { ListChatMessage messages List.of( new ChatMessage(system, systemPrompt), new ChatMessage(user, userMessage) ); ChatRequest request new ChatRequest(); request.setModel(gpt-4o-mini); request.setMessages(messages); ChatResponse response webClient.post() .uri(/chat/completions) .bodyValue(request) .retrieve() .bodyToMono(ChatResponse.class) .block(Duration.ofSeconds(30)); return response.getChoices().get(0).getMessage().getContent(); } }block(Duration.ofSeconds(30))可以直接把响应式调用转成同步阻塞在普通 Spring MVC 的 Controller 里用完全没问题。第一次跑通这个链路你的后端就成功咬住了大语言模型的接口剩下的都是优化。3.3 流式调用用 SSE 实现逐字输出同步调用虽然简单但聊天场景里用户体验太差。一个完整响应可能需要 5 到 10 秒页面上一片空白用户早走了。所以聊天功能必须走流式输出。WebClient 对 SSE 的原生支持让这事变得很干净。把接口的Accept头设置为text/event-stream然后直接用bodyToFlux(String.class)收数据FluxString streamChat(String systemPrompt, String userMessage) { ListChatMessage messages List.of(...); ChatRequest request new ChatRequest(); request.setStream(true); request.setMessages(messages); return webClient.post() .uri(/chat/completions) .bodyValue(request) .accept(MediaType.TEXT_EVENT_STREAM) .retrieve() .bodyToFlux(String.class) .filter(line - line.startsWith(data:) !line.contains([DONE])) .map(line - parseDelta(line)); }注意几个容易踩的点SSE 返回的数据里每一行都以data:开头连续多个事件之间用空行分隔最后以[DONE]标志结束。解析时先过滤非data:前缀的行再跳过[DONE]然后把 JSON 对象反序列化取出choices[0].delta.content字段这个就是增量文本。把这些增量流收集起来就是完整结果。Flux可以直接在 Spring MVC 的接口里作为返回值使用框架自动把流转换成 SSE 推给前端。Controller 写成这样前端用 EventSource 或者 fetch 的分块读取就能实现打字机效果GetMapping(value /chat-stream, produces MediaType.TEXT_EVENT_STREAM_VALUE) public FluxString chatStream(RequestParam String message) { return llmService.streamChat(你是一个智能助手, message); }3.4 超时、重试、熔断一个都不能少接模型的接口稳定性往往比你想象得差偶发的 429、502、网络抖动太常见了。如果后端不做超时控制一个接口卡在那里占用线程资源并发一高整个服务就被拖垮。超时配置分三处连接超时、读取超时、请求整体超时分别对应 TCP 建立连接、等待响应首字节、拿到完整响应三个阶段。WebClient 里通过HttpClient配置HttpClient httpClient HttpClient.create() .connectTimeout(Duration.ofSeconds(5)) .responseTimeout(Duration.ofSeconds(60));重试策略上我最推崇用 Resilience4j 而不是自己写循环。自己写重试常见的问题是把 4xx 错误也当成故障去重试而 4xx 是请求参数问题重试多少次都不会成功只会加重网关压力。使用 Resilience4j 的Retry配置时要明确忽略 429、401、400 这些状态码只对 5xx 和网络异常做退避重试。熔断的意义在于防止模型网关故障时整个后端所有请求都堆积在等待里。用CircuitBreaker设置滑动窗口和失败阈值触发熔断后快速失败返回一个友好提示给用户而不是让用户白等 30 秒。4. 常见问题与排查技巧实录4.1 高频故障排查速查表接大语言模型最容易遇到的就是下面这些毛病我把高频问题和对应处理方式整理成了一张表遇到事直接对号入座现象可能原因快速处理401 / 403API Key 过期或不具备模型权限检查密钥、核对账号开通范围408 / 504模型响应慢、网关排队加大 timeout、改流式调用响应中文乱码编码未强制 UTF-8手动设置请求头 charsetUTF-8内容被截断max_tokens 设置太小调大输出上限或拆分为两段connection reset连接池耗尽或客户端空闲过久调整连接池参数、开启 keep-alive429 太多请求超出网关 QPS 配额客户端限流、退避、降级到小模型这里必须单独强调 401 和 403 的区别401 是“你是谁”的问题密钥不对403 是“你能不能访问这个模型”的问题可能是当前账号没有开通某个模型的调用权限。排查 401 时用 curl 独立验证能最快定位到底是密钥不对还是代码封装出错。4.2 上下文 Token 超限怎么办做多轮对话最典型的问题就是聊着聊着报 “maximum context length exceeded”。模型一次性能够接收的 token 有上限比如有些模型上下文在 128k token但那是把系统提示、历史消息、当前输入、模型输出全部加在一起的总额。我处理这个问题有三个办法按优先级排第一是历史裁剪。每轮对话给历史消息设置一个预算上限比如最多保留最近 10 轮超过就丢最旧的。这个办法简单直接适合大部分内部工具场景。第二是摘要压缩。当历史消息已经很长且用户明确需要保留更多上下文时把早先的全部对话交给模型做一次摘要再把摘要作为系统提示注入后续对话基于摘要继续。第三是分段续写。如果是一次性长文本生成超过输出长度把目标拆成多个子任务每个子任务单独请求最后把结果拼接。裁剪和摘要要配合使用摘要本身也是有 cost 的不能每轮都触发。我在实际项目里的经验值是历史消息不超过 20 轮时只裁剪超过 20 轮且预算紧张时才做摘要。4.3 并发上限与限流实践模型网关不会让你无限量调用。有的厂商按账号维度设置每分钟请求次数上限有的按 token 吞吐量限制超了就返回 429。后端不做限流的话网关的 429 会像洪峰一样打过来而且客户端白白浪费连接资源。后端侧我建议做两级限流。一级是全局限流基于 Redis 或者网关组件比如每秒钟最多放行 N 个 LLM 请求超出的请求直接排队或返回友好提示。另一级是用户级限流针对自己的业务场景比如每个用户每分钟最多调用 5 次对话接口防止个别用户刷爆模型费用。限流算法用令牌桶最合适令牌以固定速率放入桶中每次请求从桶里取一个令牌桶空了就拒绝请求。和漏桶相比令牌桶能容忍突发流量更符合对话类产品的节奏。用一个成熟中间件或者 Redis Lua 脚本都能实现不建议自己在 Java 内存里用 ConcurrentHashMap 手写限流器多节点部署时内存方案会失效。4.4 快速调试技巧调试大语言模型接口我不会直接断点调试。最稳的方式是先用 curl 把接口链路验证清楚再回来看代码curl -X POST https://api.example.com/v1/chat/completions \ -H Authorization: Bearer $LLM_API_KEY \ -H Content-Type: application/json \ -d { model: glm-4-flash, messages: [{role: user, content: 你好}], max_tokens: 100 }curl 能通说明密钥、网络、参数都没问题问题就在代码封装。此时再把请求体和响应体日志打印出来对照看某个字段是不是解析不到位。一个新接手的项目最容易出的问题其实是响应结构变化。厂商更新版本后可能在 choices 数组里追加新字段或者调整嵌套结构老代码按固定字段解析就报错。应对办法是在 DTO 解析时保持兼容性未知字段要忽略。Jackson 里配置JsonIgnoreProperties(ignoreUnknown true)是必不可少的否则厂商在响应里多一个字段你的反序列化直接抛异常。5. 进阶实践把 LLM 能力真正融入到业务中5.1 多轮对话怎么维持上下文很多人以为多轮对话就是把用户消息和历史回复一起发给模型实际上没那么简单。模型本身是无状态的每轮对话都需要把完整上下文通过 messages 数组带过去但带多少、怎么带直接影响质量和成本。我维护上下文的做法是抽一个ChatSession对象里面存一个按时间排序的消息列表。每次用户发言后把用户消息和模型回复同时追加进列表下一次请求前从列表里构建 messages 数组并发给模型。系统提示词固定放在列表第一位用于约束模型的回答风格、语言、行动范围。上下文长度增长以后不能单纯无脑裁剪。裁剪策略我前面提过这里再补充一个细节有些历史的中间轮次包含了关键用户指令比如“用中文回复”“请总结一下”如果被裁剪掉模型的回答风格就可能跑偏。所以我给消息增加一个pinned标志位标记这类指令性消息不允许被裁剪。5.2 结合知识库的 RAG 落地真正把大语言模型用出业务价值绕不开 RAG检索增强生成。如果不接知识库只靠模型预训练知识回答遇到企业内部文档、最新产品信息、私有数据时基本都是胡编。RAG 的核心思路很简单先检索出最相关的文档片段再拼到提问里喂给模型让模型基于这些片段生成答案。后端实现 RAG 需要三步。第一步是文档预处理把 PDF、Word、Markdown 切分成固定长度的 chunk比如每 500 到 800 个字符一段相邻块保留一点重叠避免切断语义。第二步是向量化通过 Embedding 接口把每个 chunk 转成向量存进向量数据库比如可以用 Milvus 或 Elasticsearch 的向量检索能力。第三步是问答时先向量检索 Top K 相关文档片段拼进 prompt再调用 ChatGPT 这类模型做生成。这里面最容易被忽略的是检索质量而不是大模型本身。文档切分的颗粒度、向量化的模型选择、检索召回数量都会直接影响最终回答准确率。我做过一轮对比实验同样的问题二次检索比单次检索准确率明显高。如果项目预算允许先做“检索-重排-生成”三层架构效果提升最稳定。5.3 成本管控与配额管理模型调用成本和数据库查询成本不在一个量级一个长回答可能就消耗几万 token相当于几分钱量大了之后月度账单很可观。所以我建议后端从接入第一天就把成本监控做起来。在调用链路上记录每个请求的模型名称、输入 token、输出 token、耗时、用户标识落库或者推送到监控系统。有了这些数据可以回答三个关键问题谁是消耗大户、哪些功能最烧钱、是否需要换更便宜的模型。我见过一个团队把高并发低要求的场景从大模型降级成小模型后费用直接砍了 60% 以上效果却没受影响。更进一步的管控手段是配额。比如内部系统给每个部门设置月预算超了自动切换成轻量模型或直接停用防住“测试同学随手写个定时任务半夜批量调用”这种事。配额控制放网关层最合适业务代码不需要感知这些逻辑。6. 写在最后几个我踩过之后才理解的细节这套接入方式我已经在项目里跑了一年多踩坑踩多了之后回头看看最值钱的经验反而不是代码本身而是一些“看似无关紧要、实际上决定成败”的细节。第一模型网关的接口兼容性远没有想象中好。虽然大家都在宣传兼容 OpenAI 协议但真实接入时你会发现有的厂商要加额外的头有的厂商对某些字段会校验得更严格有的流式返回格式就是不一样。所以在封装层做一层适配不要直接裸调厂商 SDK是值得的。第二给自己留一条快速降级的退路。模型网关可能在任意时刻出现故障或限流接入时就要想好降级策略是切到备用模型还是返回缓存答案还是直接走人工处理通道。我见过最靠谱的方案是双供应商配置主用 A 厂商检测到 Hystrix 熔断后自动切 B 厂商用户完全无感。第三不要迷信“大模型能解决一切”。有些问题用词向量检索加规则就能处理得更好成本还低。我最终把一个知识库问答系统的很多入口改成先走 Elasticsearch 精确匹配匹配不上才交给大模型兜底整体准确率反而提升了。技术选型永远是从业务出发不是从热点出发。如果你现在正准备在 Java 后端里接入大语言模型我的建议是先按这篇文章把同步调用和流式调用跑通再补上超时、重试、熔断、限流四件套最后再考虑上下文管理和 RAG。别一上来就搭复杂架构先让一条链路稳定跑起来再逐步扩展。等到哪一天你发现自己的关注点从“怎么发请求”变成了“怎么控制成本、怎么保证响应质量”那就算真正把大语言模型接入这件事拿捏住了。