☰
Java后端接入大语言模型:从API调用到SSE流式与Nginx部署
2026/9/26 7:51:16 网站建设 项目流程

最近好几个朋友问我同一个问题:Java后端到底怎么接入大语言模型?有人以为用RestTemplate调一下接口就算完事,结果流式输出一到生产就卡住,JSON解析连着报错,重试机制还导致账单翻倍。也有人直接在前端把模型API的Key填进去,上线第二天Key就被刷爆了。我统一回复:Java后端接入大语言模型,不是“调一个接口”这么简单,它是一个涉及接口设计、流式传输、结构化解析、限流降级、部署代理的完整工程问题。

这篇文章我打算把自己做过的项目经验完整写出来,包括技术选型、核心代码、流式SSE怎么落到前端、本地部署怎么接入、Nginx怎么配、生产环境有哪些坑。不管你是刚接触LLM的Java后端,还是已经在接但总出问题的老手,都值得花几分钟看完。

1. 先把“接入大语言模型”这件事拆清楚:Java后端到底在接什么

很多团队立项时说的是“接入大模型”,但实际做的时候才发现,大模型本身只是一个“会生成文本的远程服务”,Java后端在这里面扮演的角色是翻译官、调度者和守门员。你不仅要能调通它,还要让它在你的业务系统里稳定、安全、可控地跑起来。

1.1 三种接入方式,别一上来就选最重的

我见过不少团队上来就要自建GPU集群,结果模型还没跑通,运维先崩溃了。其实Java后端接入大语言模型,主流就三条路:

  • 方式一:直接调用云端模型API。比如OpenAI、通义千问、文心一言、Kimi、智谱等,它们都提供了兼容的HTTP接口。你构造一个JSON请求,带上Key,就能拿到结果。优点是接入快、效果稳定、不需要GPU;缺点是数据要出域,按token计费,长期用成本不低。
  • 方式二:走云厂商的模型托管网关。比如阿里云百炼、腾讯云混元、Azure OpenAI等,相当于在模型API和你之间加了一层企业级网关。好处是支持私有网络访问、统一账单、有内容审核,适合中大型企业。
  • 方式三:本地部署开源模型。用Ollama、vLLM、llama.cpp等工具,把Qwen、Llama、ChatGLM这类开源模型跑在自己的服务器上,然后提供一个OpenAI兼容的接口给Java调用。数据完全不出域,可控性最强,但硬件投入大,维护复杂度高。

选择标准其实就三条:数据能不能出域、预算有多少、团队有没有GPU运维能力。我个人的建议是,先用云端API把业务跑通,等量大了再考虑本地部署。反过来一上来就啃私有化,大概率会卡在“显存不够”“推理速度慢”这些硬件问题上。

1.2 无论哪种方式,Java后端都要做的四件事

不管选哪条路,Java后端的工作可以抽象成四件事:

  1. 构造请求与鉴权:把用户的输入拼成模型需要的messages格式,带上API Key或Token。
  2. 控制生成参数:model、temperature、max_tokens、top_p、stream等参数的配比,直接影响回答质量和响应速度。
  3. 解析响应:非流式请求拿到完整JSON,流式请求要处理SSE协议,逐段解析增量内容。
  4. 错误处理与补偿:模型接口不是“银弹”,超时、限流、空响应、内容审核拦截都会发生,必须有兜底方案。

这四件事听着简单,但每一件都有很多细节。比如参数temperature设置太高,模型会“胡言乱语”;max_tokens设太短,回答会戛然而止;stream开了之后,前端收不到EOF标志就会一直转圈。

1.3 大模型接口的“反直觉”点:它不是普通HTTP接口

我以前接普通第三方接口的习惯是:发起请求、等待响应、解析JSON、完事。但大模型接口有几个非常反直觉的特点,后端设计必须从一开始就考虑进去:

  • 响应时间极长:普通接口200ms算慢,大模型接口一次动辄几秒到几十秒,前端根本等不了完整响应,所以必须做流式输出。
  • 输出不稳定:同一个问题,同一个参数,两次调用结果可能不一样。这让自动化测试和断言变得很痛苦。
  • 重试有代价:普通HTTP接口重试没问题,但模型接口按token计费,重试一次就是一次钱。如果网络超时但服务端其实已经生成完了,你重试就会扣两次费。
  • token不是字符数:一个汉字可能占1到2个token,不同模型的分词器还不一样。做上下文字数限制、成本估算时,不能用length()去算。
  • 输出格式不稳定:你让模型返回JSON,它可能在JSON前后加一句“好的,这是你要的结果”,直接把你的解析器干崩。

这些“反直觉”点,决定了我们不能把大模型接口当成普通HTTP接口来设计。接下来要讲的抽象层、流式处理、结构化输出,基本都是围绕这些点展开的。

2. 设计选型:为什么我只用官方SDK + 自定义抽象层,而不是Spring AI

Java后端接入大模型,网上教程很多,但选型这块讲得很少。我直接给结论:如果你做的是生产项目,优先用官方SDK或者OpenAI兼容协议,再包一层自己的接口抽象;Spring AI和LangChain4j这类高层框架,目前更适合做原型验证,不太适合直接上生产。

2.1 Java侧接入大模型的三条路

方案优点缺点适合场景
直接HTTP调用(RestTemplate/WebClient/OkHttp)无依赖、原理透明、可控性强流式处理要自己写,代码量大想深入了解协议,团队后端能力强
官方SDK(openai-java、各云厂商Java SDK)协议封装完整、接入快、厂商持续维护各家SDK风格不统一,切换厂商成本高只打算接一家模型,不想重复造轮子
Spring AI / LangChain4j提供高层的ChatClient、PromptTemplate、向量数据库集成版本变动快、抽象重、出现问题不好排查快速做Demo,团队愿意跟着框架升级

我实际项目里选的是“官方SDK + 自定义抽象层”。原因很简单:官方SDK对SSE、错误码、限流这些细节处理得比我手写更完善,但我不想让业务代码绑定某一家厂商。所以我在SDK外面再包一层接口,业务代码只依赖我自己的接口,底层厂商随时可以换。

2.2 定义ChatClient:先把接口抽象出来

我的抽象层直接照抄了“客户端模式”,一个同步接口,一个流式接口,再加上一个响应封装:

public interface ChatClient { /** * 非流式对话:返回完整回复 */ ChatResponse chat(ChatRequest request); /** * 流式对话:返回增量内容,适合打字机效果 */ Flux<ChatChunk> chatStream(ChatRequest request); }

对应的请求对象,我一般把核心参数都放进去:

public class ChatRequest { private String model; // 模型名 private String systemPrompt; // 系统提示词 private String userMessage; // 用户输入 private Double temperature; // 随机性,默认0.7 private Integer maxTokens; // 最大输出token数 private Boolean stream; // 是否流式 }

响应对象则统一返回“内容、原始数据、token消耗”三个维度:

public class ChatResponse { private String content; private Integer promptTokens; private Integer completionTokens; private String rawResponse; }

为什么流式接口用Flux<ChatChunk>?因为Spring WebFlux的Flux天然支持异步流,配合SSE很容易把增量内容推给前端。如果你项目是Spring MVC,也可以用SseEmitter或者CompletableFuture,但抽象接口不变,只是实现不同。这样做的好处是:上层业务永远不知道底层是接的哪家模型、用的什么协议,以后换模型只改实现类。

2.3 配置项设计:多Key、多模型、动态切换

配置方面,我见过最失败的做法是把API Key硬编码在代码里,或者写死在application.yml里。生产环境至少要支持多Key、多模型、动态切换。我的做法是借助Spring的@ConfigurationProperties,把模型配置和Key池拆出来:

ai: default-model: qwen-plus models: qwen-plus: base-url: https://dashscope.aliyuncs.com/compatible-mode/v1 api-key: ${AI_QWEN_KEY} gpt-4o-mini: base-url: https://api.example.com/v1 api-key: ${AI_OPENAI_KEY} key-pool: - ${AI_KEY1} - ${AI_KEY2}

对应配置类:

@ConfigurationProperties(prefix = "ai") public class AiProperties { private String defaultModel; private Map<String, Endpoint> models = new HashMap<>(); private List<String> keyPool = new ArrayList<>(); public static class Endpoint { private String baseUrl; private String apiKey; // getter/setter省略 } }

多个Key轮询可以用一个简单的AtomicInteger自增取模,也可以在某个Key触发401或429时把它临时标记为不可用。实际项目中,多Key轮询能显著降低限流概率,尤其在高并发场景下。

3. 手写一个OpenAI兼容客户端:非流式、流式、结构化输出

选完型,接下来就是核心代码环节。我会以OpenAI兼容协议为例,因为国内外的模型绝大多数都支持这个协议,你只需要改base-url和api-key。

3.1 非流式调用:先跑通最简单的链路

非流式调用是最基础的,相当于一次普通的POST请求。我用Spring WebClient实现,因为异步和流式更好支持:

@Service public class OpenAiChatClient implements ChatClient { private final WebClient webClient; private final AiProperties aiProperties; public OpenAiChatClient(WebClient.Builder builder, AiProperties aiProperties) { this.aiProperties = aiProperties; this.webClient = builder .baseUrl(aiProperties.getModels().get(aiProperties.getDefaultModel()).getBaseUrl()) .defaultHeader("Authorization", "Bearer " + aiProperties.getModels() .get(aiProperties.getDefaultModel()).getApiKey()) .build(); } @Override public ChatResponse chat(ChatRequest request) { Map<String, Object> body = new HashMap<>(); body.put("model", request.getModel()); body.put("messages", List.of( Map.of("role", "system", "content", request.getSystemPrompt()), Map.of("role", "user", "content", request.getUserMessage()) )); body.put("temperature", request.getTemperature()); body.put("max_tokens", request.getMaxTokens()); body.put("stream", false); Map response = webClient.post() .uri("/chat/completions") .bodyValue(body) .retrieve() .bodyToMono(Map.class) .block(); Map choices = (Map) ((List) response.get("choices")).get(0); Map message = (Map) choices.get("message"); Map usage = (Map) response.get("usage"); ChatResponse chatResponse = new ChatResponse(); chatResponse.setContent((String) message.get("content")); chatResponse.setPromptTokens((Integer) usage.get("prompt_tokens")); chatResponse.setCompletionTokens((Integer) usage.get("completion_tokens")); chatResponse.setRawResponse(response.toString()); return chatResponse; } }

这段代码有几个容易踩坑的地方:

  • 超时设置:WebClient默认没有读取超时,如果不配置,遇到慢模型请求会一直挂着。建议连接超时3秒,读取超时60秒。
  • 泛型擦除:直接bodyToMono(Map.class)虽然能拿到数据,但内部嵌套结构强转时容易ClassCastException。如果追求类型安全,应该定义完整的DTO。
  • block()的取舍:在Spring WebFlux环境里不应使用block()阻塞线程,但项目是Spring MVC时可以接受。更优雅的做法是返回CompletableFuture或直接用Flux。

3.2 流式输出与SSE:让AI打字机效果真正落地

流式是调用大模型最核心的交互方式。OpenAI协议中,请求体加"stream": true,响应变成text/event-stream,每一行是data: {...},最后一行是data: [DONE]。

Java后端要做的事,就是把远程的SSE流“原样”转发给前端。用WebClient实现很简单:

@Override public Flux<ChatChunk> chatStream(ChatRequest request) { Map<String, Object> body = new HashMap<>(); body.put("model", request.getModel()); body.put("messages", List.of( Map.of("role", "system", "content", request.getSystemPrompt()), Map.of("role", "user", "content", request.getUserMessage()) )); body.put("stream", true); return webClient.post() .uri("/chat/completions") .bodyValue(body) .retrieve() .bodyToFlux(ServerSentEvent.class) .mapNotNull(event -> { String data = (String) event.data(); if (data == null || "[DONE]".equals(data)) { return null; } // 解析JSON增量 JsonNode node = JsonNodeFactory.instance.decodeValue(data); JsonNode delta = node.path("choices").get(0).path("delta").path("content"); if (delta.isMissingNode() || delta.asText().isEmpty()) { return null; } return new ChatChunk(delta.asText()); }); }

这段代码里有几个细节值得说:

  • data: [DONE]:这个标志表示流结束,直接过滤掉,不要把它当内容返回给前端。
  • 增量可能为空:很多模型在第一帧会返回role信息,不包含content。需要判断delta.content是否为空,否则会给前端推一个undefined或null。
  • 错误帧:流式过程中如果模型报错,数据里会有error字段而不是content。最好在mapNode中判断node.has("error"),然后抛出异常,让全局异常处理器接管。

前端在收到这种流时,用EventSource或fetch配合ReadableStream逐个读取。这里的关键点是:后端必须保持连接直到模型完全生成结束,不能因为某个帧为空就关闭。

3.3 结构化输出:让模型返回可解析的JSON

比流式更折磨人的是结构化输出。业务系统往往需要模型返回一个JSON对象,比如“根据商品描述生成标题和卖点”,你希望拿到的是:

{ "title": "xxx", "points": ["aaa", "bbb"] }

但模型经常会这样返回:

好的,根据您的要求,我生成了以下标题和卖点: { "title": "xxx", "points": ["aaa", "bbb"] }

这时候直接用ObjectMapper.readValue解析必挂。我有三个办法应对:

  • 在系统提示词里强约束:明确要求“只输出JSON,不要输出任何多余文字,不要使用markdown代码块”。这个办法能解决80%的问题。
  • 利用模型的JSON Mode:OpenAI兼容协议支持response_format: {"type": "json_object"},国内不少模型也已支持这个参数。开启后模型会尽量输出合法JSON。
  • 解析时提取最外层JSON块:写一个工具方法,从字符串中截取第一个{到最后一个}之间的内容,再做反序列化。这个方法最笨但最可靠。

我实际用的提取工具长这样:

public static String extractJson(String text) { int start = text.indexOf('{'); int end = text.lastIndexOf('}'); if (start == -1 || end == -1 || end <= start) { throw new IllegalArgumentException("No JSON object found in text: " + text); } return text.substring(start, end + 1); }

配合Jackson反序列化:

ObjectMapper mapper = new ObjectMapper(); try { ProductIdea idea = mapper.readValue(extractJson(rawContent), ProductIdea.class); return idea; } catch (JsonProcessingException e) { // 记录原始内容,方便排查 log.error("解析模型输出失败, content={}", rawContent); // 这里可以根据业务选择重试一次,或者返回兜底默认值 }

注意结构化和流式是冲突的。如果开了流式,再要求JSON Mode,通常也能工作,但你会把多个增量拼起来后再整体解析。我一般建议:需要结构化结果的请求,不要用流式;需要打字机效果的请求,就不要要求严格JSON。两者混用容易遇到状态不一致。

3.4 超时、重试、限流与熔断降级

生产环境不能把所有问题都留给上游。我在这块踩过不少坑,总结成四条经验:

  • 超时分两层设:连接超时3秒,读取超时60秒。流式接口不要设读取超时,而是用总时长兜底,比如120秒后强制断开。
  • 重试必须幂等:模型接口天然不幂等,重试一次就是一次新请求。如果网络异常后不确定服务端是否收到,重试会导致重复扣费。我的策略是:只有连接异常(比如TCP握手失败)才重试,拿到HTTP响应后一律不重试,除非业务明确允许。
  • 后端要限流:就算模型API不限流,你自己的系统也要防刷。用Resilience4j或Guava RateLimiter对用户维度限流,比如每个用户每分钟最多20次。不然前端页面开个按钮连点,你的模型账单就爆了。
  • 降级是必须的:模型不可用时要给用户一个体面的回复,而不是让前端一直转圈。我会定义一个FallbackHandler,超时或异常时返回“当前AI服务繁忙,请稍后再试”,同时把问题记录到日志或消息队列,方便事后补偿。

4. 本地部署大语言模型:Java后端的第二接入姿势

说完云端API,再聊聊本地部署。最近“本地部署大语言模型”的热度很高,很多数据敏感的项目要求模型必须跑在内网。这块的本质是:把开源模型部署成OpenAI兼容服务,Java后端只需要改一个base-url,代码几乎不用动。

4.1 本地部署的核心动机:数据不出域,但别低估维护成本

选择本地部署通常有几个理由:数据敏感不能出域、长期token成本太高、需要深度定制模型、国家政策合规要求。但代价也很明显:你需要买GPU,需要有人会配模型服务,需要处理模型效果不如付费API的问题。

我做过的项目中,最稳妥的路径是先用Ollama把模型跑起来做技术验证,确认效果和速度之后,再上vLLM这类高性能推理框架。Ollama胜在简单,一条命令就能起服务;vLLM胜在高并发和continuous batching,适合真实生产。

4.2 硬件估算与模型选型:先用小模型把链路跑通

很多人在本地部署这一步栽在“选了个跑不动的模型”。模型显存需求可以粗略估算:显存占用 = 模型参数量 × 精度字节数。一个7B模型,FP16精度约需要14GB显存,INT4量化约需要5GB。如果是14B模型,FP16就要28GB,一块4090勉强能跑。

我的建议是:先用量化版小模型跑通链路。比如:

ollama pull qwen2.5:7b ollama run qwen2.5:7b

Ollama会默认在本地起一个OpenAI兼容服务,监听http://localhost:11434/v1。Java后端的base-url改成这个地址,api-key随便填一个非空字符串即可。也就是说,我前面写的OpenAiChatClient完全不需要改,只要配置项指向Ollama就行。

4.3 本地部署接口的兼容性坑与并发问题

本地部署看起来美好,实际用起来有几个坑,我这里集中列一下:

  • 模型名称必须匹配:Ollama的模型名是qwen2.5:7b,不是qwen-plus。请求体里的model一定得改成你在本地起的模型名,否则报404。
  • 上下文长度限制:开源模型默认上下文可能只有4096或8192,超长会被截断,业务要提前做文本切片。
  • 并发能力弱:Ollama不擅长高并发,多用户同时问会排队。生产环境如果要接多个Java后端实例,建议用vLLM,它支持连续批处理、PagedAttention,能把GPU利用率拉高。
  • 模型输出可能不稳定:本地小模型的结构化输出能力明显弱于云端大模型,JSON Mode支持也参差不齐。这时候硬解析不如在提示词里加few-shot示例。

关于本地部署,还有一个高可用问题:本地推理服务是单点,进程一挂所有AI功能就全挂了。建议至少在Nginx层对多个推理节点做负载均衡,并在Java侧配置故障转移,比如主节点超时就切换到备用云端API。

5. 前后端分离部署:接口设计、跨域与Nginx流式代理

前面讲的是怎么把模型接进来,最后这段讲怎么把模型能力安全、顺滑地暴露给前端。这步做不好,前面全白搭。因为大模型接口不是普通业务接口,它涉及跨域、鉴权、流式转发、代理缓冲,一个不注意就是线上事故。

5.1 接口设计:把LLM能力封装成业务接口,而不是直接透传

我见过最离谱的做法,是前端直接请求模型厂商的API,把Key放在前端代码里。这不叫对接,叫裸奔。正确的做法是后端封装,至少要做到:

  • Key只存在服务端,前端永远不碰模型厂商的地址和Key。
  • 接口按业务定义,比如POST /api/ai/summary、POST /api/ai/translate,而不是一个通用的/api/ai/chat透传一切。业务接口可以夹带自己的参数校验、权限校验和业务逻辑。
  • 输出要过滤:有些模型会对敏感内容做处理,后端要有一个输出审核的钩子,至少要有敏感词过滤。

一个典型接口定义大概是:

POST /api/ai/chat 请求体:{ "scene": "chat", "message": "帮我写一封请假邮件" } 响应体(非流式):{ "reply": "xxx", "requestId": "xxx" } GET /api/ai/chat/stream?message=xxx 响应:text/event-stream

5.2 跨域配置与开发环境代理

前后端分离开发时,前端在localhost:5173,后端在localhost:8080,如果不做任何跨域处理,浏览器会直接拦截。你的流式接口也逃不掉。

如果不想在Nginx上线前一直跟跨域较劲,可以在Spring Boot里加一个CORS过滤器,只放开必要的路径:

@Configuration public class CorsConfig { @Bean public CorsWebFilter corsWebFilter() { CorsConfiguration config = new CorsConfiguration(); config.addAllowedOriginPattern("*"); config.addAllowedMethod("*"); config.addAllowedHeader("*"); config.setAllowCredentials(true); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/api/ai/**", config); return new CorsWebFilter(source); } }

注意addAllowedOriginPattern("*")和setAllowCredentials(true)在旧版本会冲突,建议用Spring Boot 2.4+,并显式允许Origin模式。

但更推荐的方案是:开发环境用前端代理,生产环境用Nginx同域转发,后端本身不开启CORS。这样接口更干净,也能少暴露一些信息给外部扫描。

5.3 Nginx如何代理流式接口:关掉缓冲,否则前端等不到字

这是最容易踩的坑。SSE流式接口如果经过Nginx默认配置,Nginx会先缓冲整个响应,直到模型全部生成完才一次性发给前端。也就是说,前端看着接口一直pending,等了几十秒后突然一次性出现所有内容,“打字机”效果直接没了。

正确的Nginx配置要关掉代理缓冲,并设置合理的超时:

location /api/ai/stream { proxy_pass http://192.168.1.10:8080; proxy_http_version 1.1; proxy_set_header Connection ""; proxy_buffering off; proxy_cache off; proxy_read_timeout 120s; proxy_send_timeout 120s; chunked_transfer_encoding off; }

这里的核心是proxy_buffering off,告诉Nginx不要缓存上游响应,来一块转发一块。proxy_http_version 1.1和Connection ""是SSE长连接必须的,否则text/event-stream可能被截断。chunked_transfer_encoding off可以避免某些旧客户端解析分段编码出问题。

5.4 网关层的统一鉴权和限流

如果你的系统用了Spring Cloud Gateway或自研网关,还要额外注意两点:

  • 鉴权不要只做在后端应用:大模型接口很耗资源,暴露给外部消费前,至少要在网关层做一次JWT或Token校验。不然有人拿你的接口当免费代理,你连投诉都找不到人。
  • 网关也要限流:就算后端做了限流,网关前置限流能更早挡住恶意流量。Nginx可以用limit_req_zone,Spring Cloud Gateway可以用RedisRateLimiter。
  • 网关对流式接口的转发:如果是Spring Cloud Gateway,默认也会缓冲响应。需要配置GatewayFilter设置streamingMediaType或者在路由配置中禁用缓冲。这块文档不多,但坑很深,生产环境一定要压测过再上线。

6. 接入大模型后的几个“返工”坑:我踩过你也可能踩

最后这部分,我把这几年的真实翻车经历做个汇总。这些坑不一定写在官方文档里,但每个都能让你凌晨三点爬起来排查。

6.1 Token统计与业务计费:别在月底才发现账单爆了

模型API的token消耗是成本核心,但很多项目上线时根本没做统计,等月底账单出来才发现某用户一个人跑掉了全团队一半的预算。所以接入第一天,就必须在每次调用后把prompt_tokens和completion_tokens落库。

我的做法是建一张ai_usage_log表:

CREATE TABLE ai_usage_log ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id VARCHAR(64), scene VARCHAR(32), model VARCHAR(64), prompt_tokens INT, completion_tokens INT, total_tokens INT, request_time DATETIME );

每次调用都记录,按天/按用户汇总。别小看这张表,它是你后面做成本优化、异常排查、用户维度限流的核心依据。没有它,出了问题你就是“瞎猜”。

6.2 数据一致性:先落库再调模型,还是反过来?

这个坑非常隐蔽。比如用户上传一份文档后,系统调用模型生成摘要,摘要生成成功后存库。如果你的流程是“用户提交文档 -> 调模型 -> 写库”,用户刷新页面的时间点恰好夹在模型调用中,就会看到文档没有摘要,体验很怪。

更严重的是“先写脏数据再调模型,模型失败了脏数据还在”。把模型调用当成外部不可靠依赖处理之后,我的标准流程变成了:

  1. 先落库,状态标记为PENDING。
  2. 提交一个异步任务去调模型(线程池或消息队列)。
  3. 模型返回成功,更新状态为SUCCEEDED,写入摘要。
  4. 失败则更新为FAILED,记录错误信息,触发重试或人工补偿。

这套流程的本质是:本地数据库才是事实源,模型结果只是外部计算结果,不能因为它失败而让主链路数据不一致。如果用了MQ,还要考虑“至少一次”投递导致的重复处理,最好建一张任务去重表,以业务主键做唯一约束。

6.3 高可用场景下的连接池与线程池问题

大模型接口是IO密集型,不是CPU密集型。如果每个请求都用new RestTemplate()或者新建HttpClient,高并发下第一个扛不住的就是你的JVM。连接一定要复用,线程一定要隔离。

在Spring Boot项目里,我用的是WebClient+ConnectionProvider,连接池配置大概这样:

ConnectionProvider provider = ConnectionProvider.builder("llm") .maxConnections(200) .pendingAcquireTimeout(Duration.ofSeconds(30)) .build();

流式接口千万不要直接占满Tomcat线程。如果你用的是Spring MVC+SseEmitter,每个长连接会占一个Tomcat线程,并发一上去线程池就满了;如果业务以流式对话为主,建议用Spring WebFlux或者WebMvc.fn的异步支持,让线程数控制在合理范围。最省事的方式是@EnableAsync+CompletableFuture,把模型调用放到独立线程池,并给线程池设置合理的拒绝策略。

6.4 内容安全与合规:接入大模型不能跳过的一环

最后提醒一点:接入大模型,尤其是本地部署模型,内容审核一定要做。云端模型大多自带安全过滤,但你自己的业务输出依然可能因为用户输入触发风险内容。合规项目里,输入和输出两侧都要接内容安全服务,或者至少做一套敏感词过滤和人工复审机制。这不是技术浪漫,是业务上线的基本前提。

这块我只说一句:不要为了“用户体验”直接裸奔,该加的审核通道一定要加,否则出了问题,接锅的永远是后端负责人。

回想整个接入过程,我最大的体会是:大模型接口本质上是一个“智商很高但脾气不稳定”的外部系统。Java后端要做的不是把它当神仙供着,而是用工程手段把它约束在业务规则里——抽象接口、控制流程、记录日志、设置兜底。只要把这套骨架搭好,接哪家模型其实都不难。如果你正好在用RuoYi这类前后端分离框架做后台系统,那更简单——把上面的ChatClient实现注入进去,再配上流式接口和Nginx代理,一个AI助手模块几天就能上线。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询