简介:这一项目基于Spring Boot框架构建人工智能机器人,接入GPT-3.5、GPT-4.0、Kimi、百度文心一言、Stable Diffusion、Midjourney等主流大模型,支持多轮对话、知识问答和AI绘图。整体采用前后端分离结构,后端统一封装模型调用接口,前端以Vue搭建操作界面,逻辑层次清楚,适合计算机、电子信息工程、数学等专业学生作为课程设计、期末大作业或毕业设计参考资料。资源包大小26.84MB,文件总数1157个,包括Java后端源码584个、Vue页面104个、JavaScript脚本108个、XML配置文件73个,并有SQL初始化脚本、Dockerfile部署文件及YML环境配置等,便于从数据层、服务层到展示层完整学习。目前已有1234人浏览学习,项目完整度和示范性较好。通过研读代码,可以理解Spring Boot对接多个大模型平台时的接口鉴权、参数构造、响应解析与异常兜底方案;同时也能直接运行调试,用于功能演示或二次开发,为新模型接入和功能扩展提供清晰基础。
1. 这不是套壳机器人,而是一个模型网关层
很多人看到「基于 Spring Boot 的 AI 机器人」第一反应是:这不就是调 API 套壳吗?实际接手过这类项目的人会告诉你,把多家兼容 OpenAI 接口的大模型接进一个 Spring Boot 服务,难点从来不在发 HTTP 请求,而在接口设计、密钥管理、超时控制、上下文裁剪、降级切换这些绕不开的工程细节。我维护过一个对接三家模型服务的机器人项目,最深的体会是:模型能力是厂商的,稳定运行才是你自己的。这篇文章从工程视角拆解这个方向的完整落地路径,适合准备把 AI 能力接进 Java 服务、正在选型或已经遇到「换模型就翻车」的开发者。读完你能得到一套可以直接抄的接口设计、调用代码和避坑清单。
2. Spring Boot 机器人骨架:先定接口再选模型
2.1 为什么适配层必须放在业务外层
我见过的失败项目大多长一个样:业务代码里直接 new 一个 OpenAiClient,然后满仓都是厂商 SDK 的类型。这种写法在只接一家模型、永远不换的时候没问题,但只要你动过「再对比一家模型」的念头,就会陷入改一处崩十处的泥潭。
正确的做法是把模型调用当成一个外部依赖,在业务层和厂商 SDK 之间加一层你自己的适配接口。业务侧只认你的接口和你的 DTO,模型厂商的差异全部收敛在适配器实现里。这样做的好处有三个:一是换模型时业务代码零改动,二是测试时可以拿 Mock 适配器代替真实调用,三是团队内部可以并行开发——一个人写业务,一个人接新模型,互不阻塞。
这层适配接口的粒度很关键。我一般会把它设计成「一次对话请求」的语义,而不是把厂商 SDK 里几十个方法全部包一遍。你只需要两个方法:一个同步的完整回答,一个流式的增量响应。后者在下一章细讲。
2.2 最小工程结构与第一个适配器接口
先看工程骨架。这里用的是 Spring Boot 3.x + Java 17,如果你还在 Spring Boot 2.x,注意 WebClient 和 Jakarta 包名这些差异,思路不变。
ai-robot/ ├── pom.xml ├── src/main/java/com/example/robot/ │ ├── RobotApplication.java │ ├── chat/ │ │ ├── ChatModelAdapter.java │ │ ├── dto/ChatRequest.java │ │ ├── dto/ChatResponse.java │ │ ├── openai/OpenAiChatAdapter.java │ │ ├── router/ModelRouter.java │ │ └── config/ChatModelProperties.java │ └── web/ChatController.java └── src/main/resources/application.ymlpom.xml里核心依赖就四个:Spring Web(提供 WebClient 和 Controller)、Spring Boot Configuration Processor(配置提示)、Lombok(减少样板代码)、以及你实际要对接的模型厂商 SDK。如果你走 HTTP 接口而不是 SDK,连最后一个都不需要。
适配器接口是整个设计的锚点:
public interface ChatModelAdapter { /** * 同步获取完整回复 */ ChatResponse complete(ChatRequest request); /** * 流式获取回复,按增量回调 */ void stream(ChatRequest request, Consumer<String> onDelta); /** * 返回该适配器支持的模型标识,用于路由匹配 */ String supportedModel(); }这里有一个容易被忽略的设计细节:stream方法用Consumer<String>回调而不是返回Flux<String>。原因在于适配层要同时兼容「SDK 自带流式回调」和「HTTP 流式响应」两种实现方式。SDK 派的实现通常在内部发起异步回调,你返回 Flux 反而要再做一层转换;回调模型对两种实现都友好。
ChatRequest和ChatResponse是你自己定义的对象,相当于给厂商 SDK 的类型做了一层防腐。ChatRequest至少包含模型名、系统提示词、消息列表、温度、最大 token 数。ChatResponse至少包含回复正文和本次消耗的 token 数——后者对成本核算非常有用。
2.3 application.yml 里的多模型配置:别把密钥写死在代码里
多模型项目的配置天然比单模型复杂:每家厂商的 base-url、api-key、默认模型、超时时间都不一样,有的还要配多个密钥做负载均衡。把这些全部塞进application.yml是可行的,但要注意结构。
ai: chat: default-model: gpt-4o-mini models: - id: openai-a provider: openai base-url: ${OPENAI_BASE_URL} api-key: ${OPENAI_API_KEY} model: gpt-4o-mini timeout: 30s max-tokens: 2048 - id: openai-b provider: openai base-url: ${OPENAI_BASE_URL_2} api-key: ${OPENAI_API_KEY_2} model: gpt-4o timeout: 60s max-tokens: 4096 - id: domestic-c provider: openai-compatible base-url: ${MODEL_C_BASE_URL} api-key: ${MODEL_C_API_KEY} model: model-c-pro timeout: 30s max-tokens: 2048对应的配置类用@ConfigurationProperties绑定,不要手动一个个@Value读。注意api-key的值用${...}占位符引用环境变量,这是防止密钥进 Git 仓库的第一道防线。
@Component @ConfigurationProperties(prefix = "ai.chat") @Data public class ChatModelProperties { private String defaultModel; private List<ModelConfig> models = new ArrayList<>(); @Data public static class ModelConfig { private String id; private String provider; private String baseUrl; private String apiKey; private String model; private Duration timeout = Duration.ofSeconds(30); private Integer maxTokens = 2048; } }有一点要提醒:@ConfigurationProperties类一定要写@Data或在类上标注@Component,否则 Spring Boot 不会自动绑定。另一个常见坑是Duration类型的绑定,application.yml里写30s是合法的,写30会被解析成 30 纳秒——这个问题我见过不止一次,接口全部超时,排查了半天发现是单位问题。
配置完成后,做一件小事:启动时打印当前加载了哪些模型配置。不要打印 api-key 全文,只打印 ID、模型名和 base-url 的域名部分。这个日志在排查「为什么我配的模型没生效」时能节省大量时间。
3. 统一调用层:一个接口切换多家 OpenAI 系大模型
3.1 请求与响应 DTO:把差异挡在最外层
适配器接口有了,接下来定义 DTO。这一步决定了你的业务代码长什么样子,值得花心思设计。我的习惯是 DTO 里只放「业务真正关心的字段」,不要试图覆盖厂商 API 的全部参数——参数越多,适配越累,意义越小。
@Data @Builder public class ChatRequest { private String model; // 模型标识,由路由层解析 private String systemPrompt; // 系统提示词 private List<Message> messages; // 对话历史 private Double temperature; // 采样温度,默认 0.7 private Integer maxTokens; // 上限 token 数 @Data @Builder public static class Message { private String role; // system / user / assistant private String content; } }ChatResponse更简单,三个字段足够:
@Data @Builder public class ChatResponse { private String content; // 回复正文 private Integer promptTokens; // 请求消耗 token private Integer completionTokens; // 回复消耗 token private String model; // 实际使用的模型 }很多新手会把厂商返回的完整 JSON 直接透传给前端,这是错误的设计。透传意味着你的业务代码被厂商数据结构绑架,前端也拿不到统一格式。正确做法是适配器内部完成解析,只吐这三个字段。model字段特别有用——当你在做模型对比评估时,能知道每条回复来自哪个模型,评估数据才不会混。
3.2 模型路由:按场景而不是按厂商选模型
多模型的意义不是「把所有模型列出来炫」,而是「不同场景用不同模型」。最常见的是成本策略:简单分类任务用便宜的小模型,复杂推理任务用贵的大模型。我在项目里把它做成了路由规则。
@Component public class ModelRouter { private final ChatModelProperties properties; public ModelRouter(ChatModelProperties properties) { this.properties = properties; } /** * 解析请求中的模型标识,返回真实的模型配置 */ public ChatModelProperties.ModelConfig route(String modelHint, String scene) { if (modelHint != null && !modelHint.isBlank()) { return properties.getModels().stream() .filter(m -> m.getId().equals(modelHint)) .findFirst() .orElseThrow(() -> new IllegalArgumentException("未知模型: " + modelHint)); } // 场景兜底:summary 走便宜模型,chat 走默认模型 if ("summary".equals(scene)) { return properties.getModels().stream() .filter(m -> m.getId().equals("openai-a")) .findFirst() .orElseGet(this::defaultModel); } return defaultModel(); } private ChatModelProperties.ModelConfig defaultModel() { return properties.getModels().stream() .filter(m -> m.getId().equals(properties.getDefaultModel())) .findFirst() .orElseThrow(() -> new IllegalStateException("默认模型未配置")); } }路由规则不要写得太复杂,一开始就搞「用户等级 × 场景 × 模型」的矩阵,大概率会过度设计。先用「场景 → 模型」一级规则跑起来,后面真需要再往上加。路由的结果只返回配置对象,不在这里发起调用,保持职责单一。
3.3 用 OpenAI 兼容接口实现一个真实适配器
现在写最核心的适配器实现。很多模型厂商都提供 OpenAI 兼容的 HTTP 接口,这意味着一个适配器实现可以同时对接多家服务——只要把 base-url、api-key、model 名换成对应厂商的即可。这比给每家都引入 SDK 清爽得多。
@Component public class OpenAiChatAdapter implements ChatModelAdapter { private final ChatModelProperties properties; private final WebClient webClient; public OpenAiChatAdapter(ChatModelProperties properties, WebClient.Builder webClientBuilder) { this.properties = properties; this.webClient = webClientBuilder .baseUrl(properties.getDefaultModelConfig().getBaseUrl()) .defaultHeader("Authorization", "Bearer " + properties.getDefaultModelConfig().getApiKey()) .build(); } @Override public ChatResponse complete(ChatRequest request) { ChatModelProperties.ModelConfig config = findConfig(request.getModel()); Map<String, Object> body = buildBody(config, request, false); JsonNode resp = webClient.post() .uri("/chat/completions") .bodyValue(body) .retrieve() .bodyToMono(JsonNode.class) .block(Duration.ofSeconds(config.getTimeout().toSeconds())); return parseResponse(resp); } @Override public void stream(ChatRequest request, Consumer<String> onDelta) { ChatModelProperties.ModelConfig config = findConfig(request.getModel()); Map<String, Object> body = buildBody(config, request, true); webClient.post() .uri("/chat/completions") .bodyValue(body) .retrieve() .bodyToFlux(JsonNode.class) .doOnNext(node -> { // 逐段解析 delta 内容 String delta = node.path("choices").path(0) .path("delta").path("content").asText(null); if (delta != null) { onDelta.accept(delta); } }) .blockLast(); } @Override public String supportedModel() { return "openai-compatible"; } private Map<String, Object> buildBody(ChatModelProperties.ModelConfig config, ChatRequest request, boolean stream) { Map<String, Object> body = new HashMap<>(); body.put("model", config.getModel()); body.put("messages", buildMessages(request)); body.put("temperature", request.getTemperature() != null ? request.getTemperature() : 0.7); body.put("max_tokens", config.getMaxTokens()); body.put("stream", stream); return body; } private List<Map<String, String>> buildMessages(ChatRequest request) { List<Map<String, String>> messages = new ArrayList<>(); if (request.getSystemPrompt() != null && !request.getSystemPrompt().isBlank()) { messages.add(Map.of("role", "system", "content", request.getSystemPrompt())); } request.getMessages().forEach(m -> messages.add(Map.of("role", m.getRole(), "content", m.getContent()))); return messages; } private ChatResponse parseResponse(JsonNode resp) { String content = resp.path("choices").path(0) .path("message").path("content").asText(""); int promptTokens = resp.path("usage").path("prompt_tokens").asInt(0); int completionTokens = resp.path("usage").path("completion_tokens").asInt(0); String model = resp.path("model").asText(""); return ChatResponse.builder() .content(content) .promptTokens(promptTokens) .completionTokens(completionTokens) .model(model) .build(); } private ChatModelProperties.ModelConfig findConfig(String modelId) { // 从 properties.models 里按 id 精确查找,找不到就回退到默认模型 return properties.getModels().stream() .filter(m -> m.getId().equals(modelId)) .findFirst() .orElseGet(() -> properties.getModels().get(0)); } }这段代码有几个参数值得单独说明。
temperature控制采样随机性,取值范围 0 到 2。做客服机器人建议调低到 0.3 以下,回答更稳定;做文案生成可以拉到 0.9。这个参数不是越高越好,超过 1 之后回答会明显散漫,不是所有场景都适合。
max_tokens要特别小心:它限制的是「回复的最大 token 数」,不是「上下文总量」。很多报错「maximum context length exceeded」其实是 messages 里历史太长,而不是 max_tokens 设小了。上下文裁剪的策略在第 4 章讲。
stream字段是流式开关。注意同样的请求,流式和非流式返回的 JSON 结构不一样:非流式是完整choices[0].message.content,流式是多个choices[0].delta.content片段。你如果拿非流式的解析逻辑去解析流式响应,什么都拿不到。
WebClient的block和blockLast是阻塞调用,在 Spring MVC 的同步 Controller 里没有问题。如果用的是 Spring WebFlux 的响应式 Controller,这两个方法不能用,会让整个链路退化成同步,白白损失 WebFlux 的优势。
4. 流式输出与上下文管理:让对话真正像「机器人」
4.1 SSE 流式输出:把增量推到前端
文字生成类的模型响应时间通常几秒到几十秒不等,如果前端一直转圈等完整响应,体验非常差。流式输出的价值在于:模型每生成一小段,客户端就能立刻看到,配合打字机效果,用户感知到的等待时间大幅缩短。
实现流式输出,后端要对外暴露 SSE(Server-Sent Events)接口,把适配器回调里的增量内容逐个推给前端。Spring MVC 里最简单的做法是用SseEmitter。
@RestController @RequestMapping("/api/chat") public class ChatController { private final ChatService chatService; public ChatController(ChatService chatService) { this.chatService = chatService; } @PostMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE) public SseEmitter stream(@RequestBody ChatRequest request) { SseEmitter emitter = new SseEmitter(120_000L); // 2 分钟超时 ExecutorService executor = Executors.newSingleThreadExecutor(); executor.execute(() -> { try { chatService.stream(request, delta -> { try { emitter.send(SseEmitter.event() .name("message") .data(delta)); } catch (IOException e) { emitter.completeWithError(e); } }); emitter.complete(); } catch (Exception e) { emitter.completeWithError(e); } }); executor.shutdown(); return emitter; } }这里有三处关键点。
SseEmitter的构造参数是超时时间,单位毫秒。设太短会导致长回答生成到一半连接被切断,设太长又会占用连接资源。我的经验是 120 秒起步,如果对接的模型偶尔生成很慢,放到 180 秒。
ExecutorService是必须的。SseEmitter的发送线程和接收请求的线程不是同一个,如果直接在 Controller 方法里同步调用流式接口,响应会被阻塞,SSE 就退化成了一次性返回。
异常处理要谨慎:流式发送过程中一旦抛异常,不能只记日志,必须调用completeWithError,否则前端连接会挂到超时才断,用户看到的就是界面一直没反应。
4.2 上下文窗口裁剪:超过 token 上限的兜底策略
对话类机器人最隐蔽的坑是上下文长度。模型对一次请求能处理的 token 总量有硬上限,超出直接报错。很多项目跑着跑着突然报错,就是因为对话历史越来越长,累计 token 超过了模型的上限。
public class ContextWindowManager { private final int maxTokens; // 模型上下文上限,比如 8000 private final int reservedTokens; // 给回复预留的 token,比如 1000 public ContextWindowManager(int maxTokens, int reservedTokens) { this.maxTokens = maxTokens; this.reservedTokens = reservedTokens; } /** * 裁剪对话历史,保证历史不超过可用窗口 */ public List<ChatRequest.Message> trim(List<ChatRequest.Message> history) { // 估算 token 数:英文约 1 token/3 字符,中文约 1 token/1 字符 int available = maxTokens - reservedTokens; List<ChatRequest.Message> result = new ArrayList<>(); int used = 0; // 从最晚的消息往前遍历,保留最近的消息 for (int i = history.size() - 1; i >= 0; i--) { ChatRequest.Message msg = history.get(i); int estimated = estimateTokens(msg.getContent()); if (used + estimated > available) { break; } result.add(0, msg); used += estimated; } // 如果裁剪后连第一条 user 消息都放不下,至少保留最后一条 if (result.isEmpty() && !history.isEmpty()) { result.add(history.get(history.size() - 1)); } return result; } private int estimateTokens(String text) { int chineseChars = 0; int otherChars = 0; for (char c : text.toCharArray()) { if (c >= 0x4e00 && c <= 0x9fff) { chineseChars++; } else { otherChars++; } } return (int) Math.ceil(chineseChars * 1.0 + otherChars / 3.0); } }maxTokens的取值要留足裕量,不能等于模型上下文上限,必须给回复和 system prompt 留位置。比如模型的上下文是 8000 token,history 最多占 6500,回复预留 1000,system prompt 预留 500。宁可多裁剪,不要碰上限——一旦碰上限,整次请求直接失败,那才是真正的损失。
estimateTokens的估算方法不精确,但对裁剪场景已经够用。中文按 1 字符 ≈ 1 token,英文按 3 字符 ≈ 1 token,准确度在大多数场景能接受。如果你追求精确,可以引入分词器库做 token 计数,但成本偏高,一般不建议。
4.3 会话存储:内存还是 Redis
有了裁剪逻辑,还要有地方存对话历史。最简单的方案是ConcurrentHashMap<String, List<Message>>按会话 ID 存内存里。适合单机部署、会话量不大、重启不心疼的场景。缺点很明显:重启丢历史、多实例之间不共享、内存占用不可控。
生产环境我建议直接用 Redis 存,List 结构天然适合消息队列,每次追加消息后顺手把超长的历史裁掉。
@Service public class ChatHistoryService { private final StringRedisTemplate redis; private final ContextWindowManager windowManager; public ChatHistoryService(StringRedisTemplate redis, ContextWindowManager windowManager) { this.redis = redis; this.windowManager = windowManager; } public void appendMessage(String sessionId, String role, String content) { String key = sessionKey(sessionId); List<String> messages = redis.opsForList().range(key, 0, -1); // 解析历史,裁剪后重新写入 List<ChatRequest.Message> history = parseMessages(messages); history.add(new ChatRequest.Message(role, content)); List<ChatRequest.Message> trimmed = windowManager.trim(history); redis.delete(key); trimmed.forEach(msg -> redis.opsForList().rightPush(key, msg.getRole() + ":" + msg.getContent())); redis.expire(key, Duration.ofHours(24)); } public List<ChatRequest.Message> getHistory(String sessionId) { List<String> messages = redis.opsForList().range(sessionKey(sessionId), 0, -1); return parseMessages(messages); } private String sessionKey(String sessionId) { return "chat:history:" + sessionId; } }存储格式用简单的role:content拼接,中间用冒号分隔风险很低,因为消息内容里极少出现裸冒号。如果你担心,改成 JSON 序列化更稳妥,代价是每次读写多一点开销。expire记得设置,防止会话历史无限堆积。
5. 对接多模型避坑指南:密钥、超时与厂商差异
5.1 密钥泄露:貌似配好了,实际已经裸奔
现象:项目推送到远程仓库后,某天收到安全告警,说 api-key 泄露,账单被刷爆。
原因:application.yml里把密钥写成了明文,代码库同步后密钥进入 Git 历史。很多人以为「私有仓库没事」,但团队协作、离职人员、第三方工具扫描都可能让仓库内容外泄。
解决:密钥只从环境变量或配置中心读取,application.yml里只留${VAR}占位符。给每个模型单独配一个密钥,一个泄露可以单独吊销,不用全部换。我习惯在代码里加一层校验——启动时检查密钥是否仍为占位符格式,如果是就启动失败并提示,防止有人图省事硬编码。
5.2 超时设置不当:长回答总是莫名中断
现象:短问题回答正常,长问题答到一半报超时异常,前端显示「连接断开」。
原因:WebClient默认的超时时间可能不够,也可能你只设置了连接超时,没设置读取超时。模型生成一个长回答可能持续几十秒,这期间如果读取超时设短了,连接就会被掐断。
解决:超时拆分三层来设:连接超时(一般 5~10 秒)、读取超时(按模型最大回复时长估算,我给 60~120 秒)、整体请求超时(SseEmitter的场景要单独设)。正确的做法是先量化自己业务的真实耗时,再定超时值,不要拍脑袋。排查时看日志里异常类型:ConnectTimeoutException和ReadTimeoutException指向的病因完全不同。
5.3 上下文窗口差异:同样的代码换个模型就报错
现象:代码完全没动,只把 model 从 A 换成 B,请求频繁报「maximum context length exceeded」。
原因:不同模型的上下文上限不一样,你按 A 模型的窗口裁剪历史,换到 B 模型后历史长度超了。更隐蔽的是同一家厂商不同版本模型的窗口也在变。
解决:把「每个模型的上下文上限」作为配置项维护,裁剪逻辑按当前模型的配置动态计算,不要写死一个全局值。换模型时随手查一下目标模型的窗口参数,改配置比改代码安全得多。这个坑属于典型的「不做配置抽象就要付学费」。
5.4 流式响应解析不到内容:JSON 结构不一样
现象:非流式调用一切正常,切到流式后onDelta回调里拿到的 content 永远是 null。
原因:流式和非流式的 JSON 结构不同。非流式的内容在choices[0].message.content,流式在choices[0].delta.content。用同一套路径去解析,流式肯定拿不到。
解决:断点看一眼返回的 JSON 长什么样,再写对应的解析路径。这种「看着差不多实际差一层」的问题最好别靠猜,直接打印响应体看结构。血泪经验:接模型接口的第一件事,不要写业务代码,先把原始 JSON 打出来。
5.5 并发限流:接口看来正常,就是频繁 429
现象:并发稍微上来,厂商接口开始返 429,日志里一堆 Too Many Requests。重试后偶尔成功,但整体失败率居高不下。
原因:所有请求都走同一个 api-key,且没有在本地做限流。厂商的速率限制是按 key + 时间窗口计算的,你不控制自己的流量,就只能被对方强制限流。
解决:本地加一层限流,最简单的方案是用 Guava 的RateLimiter或 Resilience4j 的RateLimiter,按每秒请求数配置。另一个是退避重试——429 返回时等一段时间再重试,等待时间按指数退避,不要暴力重试。这里不建议每次都重试 3 次以上,因为生成类接口的调用成本高,重试多了账单也难看。
6. 验证与进阶:灰度切换模型之前先做这几件事
流式接口写完,第一件事不是写前端,而是先验证 SSE 链路。用 curl 直接测,能看到增量推送的全过程,排查问题比前端工具直观得多。
curl -N -X POST http://localhost:8080/api/chat/stream \ -H "Content-Type: application/json" \ -d '{ "model": "openai-a", "systemPrompt": "你是一个简洁的助手", "messages": [{"role": "user", "content": "用一句话介绍 Spring Boot"}], "temperature": 0.3 }'看到data:分段返回而不是一次性出结果,说明流式链路通了。这时再检查三件事:一是每条 SSE 消息的id是否递增,二是连接断开后有没有完整触发complete,三是换一个不存在的模型名看路由是否正确报错。
流式通了之后,务必要做「双模型对比」。把同一个问题分别发给两个模型,对比回复质量和响应时间差。这一步很多人偷懒跳过,结果上生产后才发现新模型在某些类型的问题上表现远差于旧模型。对比时注意控制变量:同样的 system prompt、同样的 temperature、同样的 max tokens,否则对比结果没有参考价值。
最后是降级策略和成本控制。多模型系统的核心价值就是「挂了能切,贵了能省」。我的习惯做法是给每个业务场景配一个主模型和一个备用模型,主模型连续失败 N 次后自动切到备用模型,同时把这次降级记录到日志里,方便事后评估是临时抖动还是主模型确实不稳。成本控制方面,能走小模型的不要上大模型,历史记录超过 30 分钟的就裁剪,同一个问题短期内重复提问命中缓存就直接返回——这些都是实际能省钱的细节。
我之前在一个对接三家模型的项目上吃过亏:当时只配了主模型,没有降级链路,结果主模型服务升级导致全线瘫痪两小时。后来补上了备用模型和自动切换,类似的故障再没影响过线上。从那以后我养成一个习惯:任何模型接入,先部署降级策略,再谈优化效果——这是架构问题,不是运气问题。希望这篇笔记能帮你在做类似项目时少走几步弯路。
本文还有配套的精品资源,点击获取