news 2026/9/30 10:10:08

Spring Boot接入OpenAI API:从同步调用到流式输出的完整实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Spring Boot接入OpenAI API:从同步调用到流式输出的完整实践

先说个结论:用 Spring Boot 去接 OpenAI API,本质上不是“调一个接口”的事,而是“设计一个服务”的事。标题里的“从零搭建AI对话服务”,我理解成三层:第一层是把 API 调通,第二层是让接口能扛住真实业务流的各种意外,第三层是留出上下文、限流、日志这些生产级细节的扩展余地。这篇文章会按这个思路逐步拆解,所有代码基于 Spring Boot 3.2+(Java 17),同步和流式两种方式都会覆盖,最后附上我实际踩过的坑和排查思路。

1. 动手之前:先把技术选型和整体架构想清楚

1.1 为什么用 Spring Boot 做 AI 集成层

很多人的第一反应是“直接用 Python 调 OpenAI 不就行了”,这话没错,但放到真实业务场景里,你的 AI 对话能力往往不是孤立存在的。它要接入现有的用户体系、权限控制、业务数据库、日志监控,甚至要跟订单、客服工单、CRM 这类系统联动。如果你的技术栈本来就是 Java/Spring 全家桶,那用 Spring Boot 做一层 AI 集成服务,是最自然的选择——你可以复用已有的认证鉴权、配置中心、注册发现、熔断限流这些基础设施,而不是在 Python 服务里再单独搞一套。

Spring Boot 本身不关心你调的是 OpenAI 还是别的模型,它只负责把 HTTP 客户端、JSON 序列化、异步处理、配置管理这些琐事处理干净。你需要关心的,只有业务逻辑和接口设计。这也是我这篇文章一直想传达的观点:不要被“AI”这个词唬住,它对你来说就是一个对外部 HTTP 服务的封装与适配。

1.2 整体架构设计:调用方、服务层与 OpenAI API 的关系

一个比较清爽的架构是这样的:前端或上游服务 → 你自己的 Spring Boot Controller → Service 层 → OpenAI Client 封装 → OpenAI API。

Controller 负责接收请求参数、校验输入、把会话 ID 和用户消息传下去;Service 层负责拼装消息历史、控制上下文长度、决定要不要截断或摘要;OpenAI Client 封装负责真正发 HTTP 请求、处理超时重试、解析响应、抛出业务异常。这样做的好处是每一层边界清晰,后续哪怕你要把 OpenAI 换成其他模型服务,只需要替换最底层那个 Client 封装,上层业务代码几乎不用动。

我的经验是:不要把 OpenAI 相关的代码散落在各个业务 Service 里。一定单独抽一个openai包或者独立模块,里面只放请求 DTO、响应 DTO、Client 封装和配置类,对外暴露的是一个干净的chat(...)方法。这样做的好处,在你上线三个月后要加审计日志、要统计 token 消耗、要接新模型时,会体现得非常明显。

1.3 直接复用现成框架,还是自己封装?我的取舍

现在市面上有一些现成的 Java 框架,比如 LangChain4j、Spring AI,它们把模型调用、Prompt 模板、对话记忆都封装好了,开箱即用。我自己也试用过,怎么说呢,如果只是想快速做个 Demo,它们确实省时间。但放到生产环境,我最终还是选择了自己封装,原因有三个。

第一个是可控性。现成框架为了兼容各种模型和场景,往往做了很重的抽象,一旦遇到特殊需求——比如流式输出要拼自定义格式、上下文要按业务规则做不同粒度的摘要——你就要去读框架源码、覆写它的逻辑,反而更费劲。第二个是版本适配。OpenAI API 本身也在演进,框架的更新不一定跟得上你的节奏,比如模型参数、响应字段变了,自己封装只需改一个 DTO,框架可能要等社区发版。第三个是依赖体积。为了一个对话功能引入整个 AI 框架,连带一堆传递依赖,对很多中大型项目来说是不必要的负担。

所以,我的建议是:如果你只是验证想法,用框架;如果你要做长期维护的产品服务,自己封装并不复杂,反而更踏实。下面的内容就按“自己封装”这条路来讲。

2. 准备工作:API Key、项目初始化和依赖配置

2.1 OpenAI API Key 的获取与存储方式

这个步骤本身不复杂:登录 OpenAI 平台,进入 API keys 页面,点击创建新的密钥,复制保存。但有几个细节一定要留意。

第一,新生成的 API Key 通常只会完整显示一次,关掉弹窗后就再也看不到了。我没少吃这个亏,现在养成习惯:创建之后立刻存到密码管理器里,同时填到本地环境变量中测试一遍再继续。

第二,不要直接把 API Key 硬编码在代码里,也不要提交到 Git 仓库。Spring Boot 项目我一般这样处理:本地开发时放在application-local.yml里,并且把application-local.yml加进.gitignore;测试或生产环境用环境变量注入。具体做法:

# Linux / macOS 临时设置 export OPENAI_API_KEY=sk-xxxxxx # Windows PowerShell $env:OPENAI_API_KEY="sk-xxxxxx"

然后在application.yml里只留占位符:

openai: api-key: ${OPENAI_API_KEY} base-url: https://api.openai.com model: gpt-4o-mini max-tokens: 1024 temperature: 0.7

这样做的好处是代码库里不出现任何真实密钥,换环境部署也不需要改代码,只要在部署平台(K8s Secret、环境变量面板、配置中心)里维护好OPENAI_API_KEY即可。

2.2 快速创建 Spring Boot 项目

我习惯用 Spring Initializr(start.spring.io)生成基础工程,选型如下:

  • Java 版本:17(Spring Boot 3.x 的最低要求)
  • 依赖:Spring Web、Validation、Lombok(可选)
  • 额外依赖:spring-boot-starter-webflux(后面流式输出要用 WebClient)

如果只是同步调用,spring-boot-starter-web就够。但考虑到 SSE 流式输出是对话服务的刚需,我建议直接加上 webflux 的 starter——它不会和 MVC 冲突,反而让你多一个选择。

生成完工程,检查一下 pom.xml 里 Spring Boot 父版本是不是 3.2 以上。3.2 之后 Spring 官方提供了RestClient,它比 RestTemplate 更现代、更好用,我后面同步请求就用它。

2.3 关键依赖与配置类

除了基础 web 依赖,我一般还会加这几个:

  • spring-boot-starter-validation:参数校验,避免把空消息发给 API 浪费 token
  • spring-boot-starter-data-redis:如果要多轮对话记忆,用它存会话历史(可选)
  • resilience4j-spring-boot2或spring-retry:重试与熔断(可选,但建议生产加)

配置类方面,我会用@ConfigurationProperties来绑定 OpenAI 相关配置,比@Value分散在多个类里清晰得多:

@ConfigurationProperties(prefix = "openai") @Component public class OpenAiProperties { private String apiKey; private String baseUrl = "https://api.openai.com"; private String model = "gpt-4o-mini"; private int maxTokens = 1024; private double temperature = 0.7; // getter / setter 省略(Lombok @Data 可代替) }

然后在使用处注入这个 Properties 对象,就不会到处写@Value("${openai.api-key}")这种散装代码了。这点在类多、参数多的时候尤其重要。

3. 核心代码实现:从同步请求到流式输出

3.1 先定义好请求与响应实体

OpenAI 的 Chat Completions 接口,请求体和响应体字段不算多,但最好严格按官方格式定义,避免漏字段或类型不匹配。我一般建这几个类:

// 消息:角色 + 内容 public record ChatMessage(String role, String content) { public static ChatMessage system(String content) { return new ChatMessage("system", content); } public static ChatMessage user(String content) { return new ChatMessage("user", content); } public static ChatMessage assistant(String content) { return new ChatMessage("assistant", content); } }
// 请求体 public record ChatRequest( String model, List<ChatMessage> messages, Double temperature, Integer max_tokens, Boolean stream ) { public static ChatRequest of(List<ChatMessage> messages, String model, double temperature, int maxTokens, boolean stream) { return new ChatRequest(model, messages, temperature, maxTokens, stream); } }
// 响应体中的 choice public record Choice( int index, ChatMessage message, String finish_reason ) {} // 用量统计 public record Usage( int prompt_tokens, int completion_tokens, int total_tokens ) {} // 完整响应 public record ChatResponse( String id, String object, long created, String model, List<Choice> choices, Usage usage ) {}

用 Java record 写 DTO 非常简洁,配合 Jackson 的默认属性映射就能直接完成反序列化。注意max_tokens这种下划线命名的字段,Jackson 在 Spring Boot 默认配置下能正确处理 JSON 属性名和 Java 字段名的映射——record 组件名是max_tokens,JSON 键也是max_tokens,天然对齐。

3.2 封装 OpenAI 客户端:同步调用

Spring Boot 3.2 推荐的RestClient,链式调用很直观。核心代码如下:

@Component public class OpenAiClient { private final RestClient restClient; private final OpenAiProperties properties; public OpenAiClient(OpenAiProperties properties) { this.properties = properties; this.restClient = RestClient.builder() .baseUrl(properties.getBaseUrl()) .defaultHeader(HttpHeaders.AUTHORIZATION, "Bearer " + properties.getApiKey()) .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) .build(); } public ChatResponse chat(ChatRequest request) { return restClient.post() .uri("/v1/chat/completions") .body(request) .retrieve() .body(ChatResponse.class); } // 方便业务层调用:自动填充模型、温度等默认参数 public ChatResponse chat(List<ChatMessage> messages) { ChatRequest request = ChatRequest.of( messages, properties.getModel(), properties.getTemperature(), properties.getMaxTokens(), false ); return chat(request); } }

写到这里,有几点经验分享:

  • 不要在每次请求时都new RestClient。把它做成单例 Bean,连接复用,性能和资源占用都会好很多。
  • 默认 Header 里已经带了鉴权,业务层完全不用关心。
  • 用.retrieve()拿响应体最省事。如果你需要拿到原始响应做处理(比如流式 SSE 场景),再用.exchange()。

3.3 流式输出(SSE)实现:从 WebClient 到前端 EventSource

同步请求返回完整结果,体验上的缺点是大段文字要等全部生成完才能看到。真实对话服务几乎都要求“打字机效果”,也就是流式输出。OpenAI API 支持stream: true,会通过 SSE(Server-Sent Events)逐段推送数据。

Spring 这边,我推荐用WebClient来做流式请求,因为它天然支持Flux<ServerSentEvent>。代码大致这样:

@Component public class OpenAiStreamClient { private final WebClient webClient; private final OpenAiProperties properties; public OpenAiStreamClient(OpenAiProperties properties) { this.properties = properties; this.webClient = WebClient.builder() .baseUrl(properties.getBaseUrl()) .defaultHeader(HttpHeaders.AUTHORIZATION, "Bearer " + properties.getApiKey()) .build(); } public Flux<String> streamChat(List<ChatMessage> messages) { ChatRequest request = ChatRequest.of( messages, properties.getModel(), properties.getTemperature(), properties.getMaxTokens(), true ); return webClient.post() .uri("/v1/chat/completions") .bodyValue(request) .retrieve() .bodyToFlux(String.class) .map(this::parseDeltas) .filter(content -> !content.isBlank()); } // 解析 SSE 数据行:data: {...} private List<String> parseDeltas(String rawLine) { if (rawLine == null || !rawLine.startsWith("data:")) { return List.of(); } String jsonStr = rawLine.substring(5).trim(); if ("[DONE]".equals(jsonStr)) { return List.of(); } // 用 Jackson 解析 JSON,提取 choices[0].delta.content JsonNode node = objectMapper.readTree(jsonStr); JsonNode delta = node.path("choices").path(0).path("delta").path("content"); if (delta.isMissingNode() || delta.isNull()) { return List.of(); } return List.of(delta.asText()); } }

这里有几个坑,必须说一下:

  • SSE 数据是一行一行推过来的,WebClient的bodyToFlux(String.class)拿到的是每一行的文本。它是一个事件一行的粒度,不是完整 JSON 对象。
  • 每一行的格式是data: {"choices":[{"delta":{"content":"你好"}}]},所以要手动去掉data:前缀再解析。
  • 流结束时会有一个data: [DONE]标记,要过滤掉。
  • 不是每条 SSE 都包含delta.content,有些事件是空的,或者只带finish_reason,所以解析逻辑里一定要做空值判断,否则你会看到一堆空字符串被推给前端。

然后 Controller 层把它转成 Spring 的 SSE 输出:

@RestController @RequestMapping("/api/chat") public class ChatController { private final OpenAiStreamClient streamClient; public ChatController(OpenAiStreamClient streamClient) { this.streamClient = streamClient; } @GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE) public Flux<ServerSentEvent<String>> stream(@RequestParam("message") String message) { List<ChatMessage> messages = List.of( ChatMessage.system("你是一个乐于助人的助手。"), ChatMessage.user(message) ); return streamClient.streamChat(messages) .map(content -> ServerSentEvent.builder(content) .event("message") .build()) .doOnError(e -> log.error("stream error", e)); } }

前端的注意点:EventSource标准只支持 GET 请求,所以我上面的接口用的是@GetMapping。如果你的业务要求必须 POST(比如传很长的消息或复杂参数),用EventSource会受限,此时可以改用 fetch + ReadableStream 来读取 SSE 数据流,这也是我在实际项目里更常用的方式。需要我可以单独写一篇前端读取 SSE 的细节,这里不展开。

3.4 Controller 层设计:一个能用的同步接口

同步接口就简单多了,适合内部服务调用或不需要流式的场景。我通常会做一个 POST 接口,接收用户消息和会话 ID,返回完整回答:

@PostMapping("/sync") public ChatResponse syncChat(@RequestBody @Valid ChatRequestDto dto) { List<ChatMessage> messages = List.of( ChatMessage.system(dto.systemPrompt() == null ? "你是一个乐于助人的助手。" : dto.systemPrompt()), ChatMessage.user(dto.message()) ); return openAiClient.chat(messages); }

ChatRequestDto可以这样定义:

public record ChatRequestDto( @NotBlank(message = "消息不能为空") String message, String sessionId, String systemPrompt ) {}

加上@Valid校验后,空消息根本进不到后面的逻辑,省了很多无意义的 API 调用费用。

4. 真实项目中绕不开的细节:上下文、限流与异常处理

4.1 多轮对话的上下文管理

只传当前一条消息,模型是“失忆”的。真实对话服务必须把历史消息一起发给 API。常规做法是:每次请求时,把该会话的历史消息取出,拼上当前消息,再按 token 预算截断。

我用的方案是按 sessionId 把消息列表存 Redis,每次请求先把用户消息追加进去,然后从消息列表尾部往前截取。为什么从尾部截?因为离当前问题越近的消息相关性越高,System Prompt 要保留,最早期的寒暄可以丢。

private static final int MAX_HISTORY_MESSAGES = 20; public List<ChatMessage> buildMessages(String sessionId, String userMessage) { List<ChatMessage> history = redisTemplate.opsForList() .range("chat:history:" + sessionId, 0, -1); List<ChatMessage> messages = new ArrayList<>(); messages.add(ChatMessage.system("你是一个乐于助人的助手。")); if (history != null) { messages.addAll(history); } messages.add(ChatMessage.user(userMessage)); // 简单截断:控制消息数量 if (messages.size() > MAX_HISTORY_MESSAGES) { int start = messages.size() - MAX_HISTORY_MESSAGES; // 保留第一条 system,截断后面的 List<ChatMessage> kept = new ArrayList<>(); kept.add(messages.get(0)); kept.addAll(messages.subList(start, messages.size())); return kept; } return messages; }

这只是“数量截断”的简易做法。更进一步的做法是按 token 数控制,比如只保留最近 2000 个 token。因为不同模型的 token 计算逻辑不一样,我用的是 OpenAI 官方提供的 tiktoken 换算思路:中文大约 1 个汉字占 1 到 2 个 token,英文约 0.75 个 token 一个单词。你可以粗略估算,把超过预算的旧消息丢给一个“摘要模型”压缩成一句话。这个方法我用过,效果不错,但成本会增加,需权衡。

4.2 超时、重试、429 与失败降级

外部 API 调用最怕三件事:超时、限流、服务端异常。我处理的方式如下。

超时方面,RestClient底层默认用的是SimpleClientHttpRequestFactory,但如果你像我一样希望精细控制连接和读取超时,建议显式配置:

@Bean public RestClient.Builder restClientBuilder() { SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory(); factory.setConnectTimeout(10_000); factory.setReadTimeout(60_000); return RestClient.builder().requestFactory(factory); }

为什么读超时要设这么长?因为模型的生成时间跟输出长度强相关,输出 1000 个 token 可能需要几十秒。如果你设 5 秒超时,稍微长一点的回答就会失败。

针对 429 限流,我的重试策略是:遇到 429 或 5xx,退避重试。Spring 里可以这样写:

@Retryable( retryFor = {RateLimitException.class, ServerException.class}, maxAttempts = 3, backoff = @Backoff(delay = 1000, multiplier = 2) ) public ChatResponse chatWithRetry(ChatRequest request) { return chat(request); }

也可以用 Resilience4j,配置更灵活。注意:重试只对幂等或可重复的请求有意义。如果是流式响应已经发了一部分给前端,此时重试会导致前端收到重复内容,所以我的流式接口不做自动重试,宁可快速失败,让前端提示用户重新发送。

失败降级方面,至少要做“返回友好错误提示”,不要让 500 直接暴露给用户。我会自定义异常类型,比如OpenAiApiException,在全局异常处理器里统一转成 HTTP 502 或 503,并附上一个业务可读的 message。

4.3 日志脱敏与 API Key 安全

这是我最想强调的一点。日志里绝对不能出现 API Key,也不能出现完整的用户敏感输入。我在实际项目中遇到过一次线上事故排查,发现日志里打印了请求体,包含 Bearer Token,当场吓出一身冷汗。处理方案:

  • 全局日志切面里对 Header 中的 Authorization 字段做掩码。
  • OpenAiClient内部不打印完整请求,只打印日志级别为 DEBUG 且脱敏后的摘要,比如“调用模型 gpt-4o-mini,消息数 5”。
  • 不要把用户原始输入打到 info 日志,最多打到 DEBUG,且确认日志平台访问权限受控。

这里给一个简单的脱敏工具方法示例:

public static String maskToken(String token) { if (token == null || token.length() < 10) return "***"; return token.substring(0, 3) + "..." + token.substring(token.length() - 4); }

4.4 CORS 与前端联调

对话服务大概率要被浏览器直接访问。如果前端域名和后端不同,就要处理跨域。我的做法是在 Controller 或全局配置中允许指定来源,而不是无脑allowedOrigins("*")。流式 SSE 接口同样受 CORS 约束,需要显式把Access-Control-Allow-Origin带上。Spring 的@CrossOrigin在 Controller 上配置后,SSE 响应也会自动带上 CORS 头,实际上我测试下来是生效的,放心用。

如果前端用 POST + fetch 流式读取,记得还需要允许Access-Control-Allow-Headers: Content-Type, Authorization,否则预检请求会失败。

5. 排坑实录:我踩过的这些 OpenAI 集成问题

5.1 常见问题速查表

我把自己在开发和线上环境中遇到的高频问题整理成了一张表,按“现象、原因、解决方式”来列:

现象可能原因排查与解决
返回 401 认证失败API Key 错误或未生效检查 API Key 是否复制完整,注意有没有空格;确认环境变量已正确注入;到 OpenAI 后台验证该 key 是否有效
返回 404 model not found模型名称写错或模型不兼容确认 model 参数为官方全名,例如gpt-4o-mini、gpt-4o;换新模型前先在官方文档确认可用
返回 429 限流触发速率限制或额度不足查看账号剩余额度;降低请求频率;实现退避重试;必要时换max_tokens更小的模型
请求超时响应生成时间超过读超时调大读取超时到 60 秒以上;改用流式接口提升体感
流式接口拿到空字符串未过滤空 delta 事件检查解析逻辑,忽略delta为空和finish_reason不为 null 的事件
SSE 中文乱码响应未声明 UTF-8确保produces = MediaType.TEXT_EVENT_STREAM_VALUE,同时让框架使用 UTF-8 编码;必要时设置ServerSentEvent的编码
日志中出现完整 Key请求体或 Header 被日志打印加日志脱敏切面;不要在 DEBUG 级别打印完整 Authorization

这张表我建议直接贴到项目 Wiki 里,团队其他人遇到类似问题可以快速定位。

5.2 几个容易被忽视的细节

细节一:max_tokens的设置直接影响回答长度和成本。很多人不管这个参数,结果模型默认生成长度偏长,费用超预算。我的一般取值区间是 512~1024,配合业务需要调整。如果回答被截断了,前端会看到最后finish_reason为length,此时提示用户“继续”或自动拼接下一轮,而业务后端也可以据此判断是否需要分段。

细节二:temperature的语义。它控制随机性,0 到 2 之间,过高的值在客服、问答等确定性场景会跑偏。我做过测试,同样的 Prompt,temperature=0.3和temperature=1.2的输出风格差距非常明显。做知识库问答,建议 0.2~0.4;做创意文案,可以 0.8~1.0。

细节三:OpenAI 的响应里choices可能不止一个。如果请求里传了n参数(生成多个候选),choices就是数组。我们默认只取第一个,但代码里别写死get(0)而不判断空列表,否则极端情况下容易索引越界。

细节四:连接池管理。如果你的对话服务是单点,多个并发请求同时进来,底层 HTTP 连接如果默认是短连接,性能和可靠性都会有问题。我用的是RestClient和WebClient共用同一个连接池配置思路——Apache HttpClient 或 JDK HttpClient 的连接池大小建议设为 50 到 200,具体看你的 QPS。不能忽略这个,否则压测时会看到大量Connection refused。

细节五:SSE 响应结束后的连接释放。流式接口如果前端中断连接,后端要能感知并取消上游请求。WebClient 的Flux天然支持取消传播,但如果你在 Controller 里用了doOnFinally做清理,记得别把响应数据也吞掉。我踩过一回:前端断连后,后端还在继续调 OpenAI API,白白烧了 token。后来在doOnCancel里加了一行日志和计数,才发现问题。

6. 上线之前我建议你做的几件小事

到这里,一个可用的 AI 对话服务已经跑通了。但上线之前,我建议你把下面这几件事也落实了。

第一,加一个简单的“请求日志表”。每次对话请求记录会话 ID、模型、输入 token 数、输出 token 数、耗时、错误码。这些数据不仅能帮你排查问题,还能用来分析用户使用模式和成本趋势。我当时用一张 MySQL 表记录这些信息,每天跑个统计,对优化模型选型和提示词非常有帮助。

第二,给接口加一个健康的超时保护。除了客户端超时,服务端也要有超时中断机制。比如用Mono.timeout(Duration.ofSeconds(60))或Flux.timeout包裹调用,防止极端情况下请求挂死。

第三,考虑要不要做内容安全过滤。OpenAI API 本身有 content policy 检查,但应用层最好也做一层基础的关键词过滤或人审接口。这个在这里不展开,但做 B 端产品时这往往是个硬需求。

第四,预留多模型切换能力。把OpenAiProperties.model做成运行时可切换的配置,比如从gpt-4o-mini切换到gpt-4o,或者切换到其他兼容 OpenAI 协议的模型服务,只需要改配置或加一个路由策略,业务代码不用动。这个扩展点在设计ChatRequest时就留好了——model字段本身就是参数化的。

我个人在实际操作中的体会是:把 AI 集成做成一个独立服务模块,比把 OpenAI 调用散落在业务代码里要省心得多。你只需要维护一个OpenAiClient,全项目的 AI 调用都走它,后续要加缓存、加熔断、加审计日志,都是一处改动,全局生效。

最后再分享一个小技巧:上线后给流式接口加一个“首字节时间”监控。也就是从请求发出到前端收到第一个内容分片的时间。这个指标比平均延迟更敏感,能第一时间反映 OpenAI API 侧的抖动或你服务端网络的问题。我就是在加了首字节监控之后,才发现某个机房的出网链路存在偶发高延迟,才针对性做了优化。做 AI 对话服务,细节决定体验,体验决定留存。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/30 10:07:37

2026 上半年居家食记,一盘水饺抚慰三餐

2026 上半年居家的细碎日子里&#xff0c;三餐总盼着些不用费神的妥帖——一盘水饺便成了不少时候的选择。此前整理囤货时&#xff0c;留意到山东鲁香源食品股份有限公司推出的大肚水饺&#xff0c;便试着煮了两袋。 这款水饺的个头确实比市面常规款饱满&#xff0c;下锅后形态…

作者头像 李华
网站建设 2026/9/30 10:07:34

OpenMAIC零代码搭建AI互动课堂:多智能体协作教学实战指南

1. 从零上手OpenMAIC&#xff1a;一堂AI互动课到底是怎么搭出来的 第一次看到“多智能体课堂”这个词&#xff0c;我脑子里蹦出来的画面是几个AI角色在讲台上你一言我一语地给学生上课。实际跑通OpenMAIC之后才发现&#xff0c;它的核心逻辑比想象中更接地气——本质上是把一堂…

作者头像 李华
网站建设 2026/9/30 10:04:46

OpenStack私有云搭建实战:Kolla-Ansible从零部署到虚拟机运行

简介&#xff1a;这份文档面向云计算初学者与运维人员&#xff0c;系统讲解如何从零搭建一套 OpenStack 私有云平台&#xff0c;帮助读者理解开源云平台各组件的部署逻辑与配置要点。资源包为单个 doc 文档&#xff0c;约 119KB&#xff0c;内容以文字步骤与命令示例为主&#…

作者头像 李华
网站建设 2026/9/30 10:04:28

Qt 按钮类控件详解

&#x1f4d1; 目录 &#x1f3a8; 1.按钮类控件 &#x1f518; 1.1 Push Button&#x1f518; 1.2 Radio Button1.3 Check Box&#x1f6e0;️ 1.4 Tool Button &#x1f4dd; 小结经典面试题 &#x1f3a8; 1.按钮类控件 &#x1f518; 1.1 Push Button QPushButton 继承…

作者头像 李华