☰
基于Spring Boot的人工智能机器人:统一接入多种OpenAI大模型的服务设计
2026/10/18 13:16:03 网站建设 项目流程

简介:基于Spring Boot构建的人工智能机器人项目,已成功对接GPT-3.5、GPT-4.0、Kimi、百度文心一言、Stable Diffusion与Midjourney等主流大模型,覆盖智能对话、文本生成与AI绘图等典型应用场景,既可作为技术学习范例,也适合计算机、电子信息、数学等专业学生用于课程设计、期末大作业或毕业设计前的参考资料。压缩包共包含1157个文件,以Java源码(584个)为主体,辅以Vue前端组件(104个)、JavaScript脚本(108个)以及大量PNG/JPG图片素材,形成完整的前后端分离结构,整体压缩后仅26.84MB,大小适中,便于下载与部署。发布至今已有1235人浏览学习,在同类毕业设计资源中关注度较高,具备良好的参考价值。包内提供了工程核心代码、SQL数据库脚本、Docker部署配置、Maven构建文件以及若干说明文档,能够帮助使用者快速梳理Spring Boot与大模型的整合流程、理解接口调用的鉴权与参数设计,并掌握前端展示和运维部署的关键思路,是一份结构完整、实践性强的优质资源。

1. 基于Spring Boot的人工智能机器人:把多种OpenAI大模型收编到一个服务里

如果你接触过人工智能聊天机器人项目,大概率见过这样的窘境:代码里写死了一个模型,换个模型就要改接口、改返回格式、改上下文处理逻辑,一个毕业设计或者小项目能折腾出好几个版本的屎山。我这次拆的这个Spring Boot人工智能机器人项目,正好解决了这个痛点:它把多种主流OpenAI大模型统一封装到一个服务里,上层只暴露一个HTTP接口,通过配置就能切换对话模型。无论是ChatGPT系列还是其他兼容OpenAI协议的模型,都能接入进来,而且保留了历史上下文、流式输出这些聊天机器人必备的能力。对于正在找Spring Boot毕业设计题目的同学来说,这是一个特别合适的参考项目——工程结构清晰,代码量不大,但涉及的模块覆盖面很广,从Web层、服务层到外部API集成都有完整实现。我把它完整跑了一遍,还把关键代码逐一拆开看过,下面会从架构设计、运行方式到二次开发思路一并讲清楚。

2. 项目准备与技术选型:不只是“调API”这么简单

2.1 这个AI机器人到底能干什么

先把它当成一个黑盒来看:启动服务后,你用Postman或者浏览器向某个接口发一段用户消息,它就能返回大模型生成的回答。这个回答不是简单的一次性应答,而是支持多轮对话——当你把当前问题和历史消息一起传进去时,模型会结合上下文进行回复。项目内部维护了多个模型供应商的配置,比如OpenAI官方接口、微软Azure的OpenAI服务、以及国内常见的兼容OpenAI协议的模型网关。每种模型都对应一个独立的客户端类,但它们都实现同一个接口。这样一来,切换模型不需要改动任何业务代码,只需要修改配置文件里的模型类型或者通过请求参数指定模型名称。

给我留下比较深的一点是,这个项目没有像很多“玩具AI机器人”那样直接在前端拼一个API Key调往常的“非流式”对话,而是把流式输出也做了。所谓流式输出,就是模型一边生成一边返回,客户端像看字一句一句蹦出来,体验接近真实聊天。实现上用到了Spring的异步请求处理和响应式编程思想,底层是Java原生HttpClient或OkHttp的流式响应处理。拆这种代码时最大的感觉是:它把“API调用”这件小事拆成了模型接口、路由策略、流式解析、上下文管理这几个独立模块,每个模块都能单独替换。

2.2 为什么是Spring Boot,以及大模型客户端怎么选

Spring Boot在这类项目里几乎是固定答案。原因不只是它启动快、配置简单,更关键的是Spring Boot对Web层、异步任务、线程池、配置管理都有很成熟的支持。比如后面要说的流式输出,Spring Boot的DeferredResult或者ResponseBodyEmitter可以天然对接大模型API的流式响应,不需要额外引入响应式框架。另外,这种毕业设计项目的运行环境大概率是Windows或Linux服务器,Spring Boot打包成可执行JAR就能跑,不用配置外部Tomcat,部署成本非常低。

大模型客户端的选型是个值得停下来想一想的问题。很多开发者第一反应是引入OpenAI官方提供的Java SDK,这个项目里其实没有这么做,而是直接基于HTTP调用协议封装。原因在于:OpenAI各家API返回结构不一定完全兼容,官方SDK通常绑定某一类模型,如果后续想接入国内那些“OpenAI兼容”的第三方网关,反而会增加适配成本。我自己的习惯是优先用轻量级HTTP客户端,自己抓JSON,自己把返回对象映射成自己的领域模型。尤其是设计成“对接多种主流大模型”的项目,用统一的“HTTP请求 + JSON解析”方式写适配器,是最灵活的做法。项目里用到的是OkHttp,连接超时、读写超时都可以单独配置。

2.3 目录结构与关键配置

整个项目的包结构是这样的:

src/main/java/com/example/airobot/ ├── config/ // 配置类,如WebMvcConfig、AsyncConfig ├── controller/ // 对外HTTP接口 ├── service/ // 业务逻辑层 │ ├── ChatService.java // 聊天主服务 │ ├── ModelRouter.java // 模型路由 │ └── impl/ ├── model/ // 实体类与DTO │ ├── ChatRequest.java │ ├── ChatResponse.java │ └── Message.java ├── openai/ // 各模型适配器 │ ├── OpenAiClient.java │ ├── AzureOpenAiClient.java │ └── CompatibleClient.java └── interceptor/ // 过滤器或拦截器

这个结构最直观的好处是:controller不直接依赖某个具体模型实现,而是依赖ChatService,ChatService通过ModelRouter去选择具体客户端。model包里定义的ChatRequest和ChatResponse是服务内部统一的数据模型,不管背后是哪个模型,对外接口参数始终一致。配置主要集中在application.yml里,包括各平台的API密钥、模型名称、超时时间、上下文最大轮数等。

spring: application: name: ai-robot ai: robot: default-model: openai-gpt-4o max-history-messages: 20 max-tokens: 1024 temperature: 0.7 timeout: connect: 5000 write: 10000 read: 30000 providers: openai: base-url: https://api.openai.com/v1 api-key: ${OPENAI_API_KEY} models: - text-embedding-3-small - gpt-4o - gpt-4o-mini azure-openai: base-url: ${AZURE_OPENAI_ENDPOINT} api-key: ${AZURE_OPENAI_KEY} api-version: "2024-02-15-preview" models: - gpt-4o compatible: base-url: https://your-gateway.example.com/v1 api-key: ${COMPATIBLE_API_KEY} models: - qwen-plus - deepseek-chat

配置里不少值都用环境变量引用了,这样做的好处是敏感信息不落在代码仓库里。如果本地跑,可以在IDEA里配置环境变量;如果部署到服务器,直接在启动脚本里export就行。对于刚上手Spring Boot的同学,我强烈建议别把API Key写死在yaml里,万一代码发到一个开源仓库,几秒钟你的Key就被爬走了。

3. 核心模块拆解:一个请求从进入到返回的完整链路

3.1 统一请求入口:Controller层设计

外部请求进来先经过ChatController,它只做两件事:接收请求体、调用ChatService,然后把结果包装成统一JSON返回。为了支持流式输出,这个Controller并没有直接返回ChatResponse对象,而是把响应交给Spring的SseEmitter或者ResponseBodyEmitter,这样在响应过程中可以不断向客户端推送数据。

@RestController @RequestMapping("/api/chat") public class ChatController { private final ChatService chatService; public ChatController(ChatService chatService) { this.chatService = chatService; } @PostMapping("/completions") public ResponseEntity<ChatResponse> completions(@RequestBody ChatRequest request) { ChatResponse response = chatService.chat(request); return ResponseEntity.ok(response); } @PostMapping(value = "/completions/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE) public SseEmitter streamCompletions(@RequestBody ChatRequest request) { SseEmitter emitter = new SseEmitter(60_000L); // 这个executor由Spring的AsyncConfig统一管理 chatService.streamChat(request, emitter); return emitter; } }

先说非流式接口。completions方法里直接同步调用服务层,同步的好处是逻辑简单、方便在测试阶段排查问题。流式接口使用SseEmitter,超时时间设成60秒。为什么用SseEmitter而不是WebFlux?因为SseEmitter更轻量,而且和Spring MVC无缝集成,对于不使用响应式编程思维的毕业设计来说,理解成本更低。这里有个容易被忽略点:SseEmitter需要配合异步线程去写入数据,否则会在HTTP连接建立之后阻塞请求线程。所以后面会看到chatService.streamChat里是用线程池来执行实际请求的。

3.2 模型路由:根据配置选择不同大模型

模型路由是整个项目里最有设计感的一部分。它本质上是一个策略模式:把每一种模型封装成一个ModelClient接口实现类,然后ModelRouter根据请求参数或者配置去选出具体实现。这个设计避免了在每个业务方法里写switch-case或者if-else去判断应该调用哪个模型。

public interface ModelClient { ChatResponse chat(List<Message> messages, ChatOptions options); void streamChat(List<Message> messages, ChatOptions options, SseEmitter emitter); }

不同的实现类放到openai包下面。例如OpenAiClient负责openai官方接口,AzureOpenAiClient负责微软Azure接口,CompatibleClient负责兼容协议网关。每个客户端内部做的事情非常相似:把ChatRequest转换成该平台要求的HTTP请求体,发送请求,解析响应。区别只是请求地址、鉴权方式、以及部分字段叫法不同。

ModelRouter里维护了一个Map<String, ModelClient>,key就是配置文件里定义的模型名。当请求没有指定模型时,使用default-model配置。

@Service public class ModelRouter { private final Map<String, ModelClient> clientMap; public ModelRouter(List<ModelClient> clients, @Value("${ai.robot.default-model}") String defaultModel) { this.defaultModel = defaultModel; clientMap = clients.stream() .collect(Collectors.toMap(ModelClient::modelName, client -> client)); } public ModelClient route(String model) { if (model == null || model.isBlank()) { model = defaultModel; } ModelClient client = clientMap.get(model); if (client == null) { throw new IllegalArgumentException("未找到模型: " + model); } return client; } }

这一套方式最大的好处是新增模型只需要实现ModelClient接口并注册成Spring Bean,ModelRouter会自动把所有实现类收集进Map里。注意modelName()是接口里约定的标记方法,每个实现类要返回自己支持的模型名。实际场景里,一个实现类可以支持多个同名模型,比如Azure那边可能多个部署名称不一样,但都走同一个客户端逻辑。

3.3 对话历史与上下文管理

大模型本身不记事儿,要做出“多轮对话”效果只能把历史消息一起发给模型。这个项目里用一个ConversationManager管理会话历史,它维护一个Map<ConversationId, List<Message>>,每个会话最多保留max-history-messages条,超过就丢弃最旧的消息,保证请求包体不会无限变大。

@Component public class ConversationManager { private final Map<String, Deque<Message>> conversations = new ConcurrentHashMap<>(); private final int maxHistoryMessages; public ConversationManager(@Value("${ai.robot.max-history-messages}") int maxHistoryMessages) { this.maxHistoryMessages = maxHistoryMessages; } public List<Message> buildMessages(String conversationId, String currentInput) { Deque<Message> deque = conversations.computeIfAbsent(conversationId, k -> new ArrayDeque<>()); synchronized (deque) { deque.addLast(new Message("user", currentInput)); while (deque.size() > maxHistoryMessages) { deque.pollFirst(); } return new ArrayList<>(deque); } } public void appendResponse(String conversationId, String response) { Deque<Message> deque = conversations.get(conversationId); if (deque != null) { synchronized (deque) { deque.addLast(new Message("assistant", response)); } } } }

这里用Deque是因为需要在尾部添加消息,同时在头部移除旧消息。ConcurrentHashMap保证了对不同会话的并发读写不互相阻塞。实际使用中需要注意:这个内存版的管理方式有个小问题——如果服务重启,所有会话历史就全丢了。对于演示和常用毕业设计场景是够用的,如果后续要部署到生产环境,可以考虑换成Redis。不过这个项目对应的核心价值是“对接大模型”,而不是保存聊天记录,内存版反而更简洁,读者可以按需替换。

3.4 流式输出与非流式输出的实现对比

非流式实现比较直白:构建好请求体,调用HTTP接口,拿到完整响应,解析choices[0].message.content后返回。这块代码不需要多绕。流式输出就复杂一些,需要处理大模型返回的分片数据,最常见的格式是data: {json}这种Server-Sent Events格式,每个分片里都包含一个增量token。

我在streamChat实现里,会把OkHttp的ResponseBody转成一个BufferedSource,然后循环读取每一行,解析出增量的content,通过SseEmitter.send()推给客户端。这里放一段核心逻辑:

public void streamChat(List<Message> messages, ChatOptions options, SseEmitter emitter) { executor.execute(() -> { try { RequestBody body = buildStreamBody(messages, options); Request request = new Request.Builder() .url(baseUrl + "/chat/completions") .post(body) .build(); try (Response response = client.newCall(request).execute()) { if (!response.isSuccessful()) { emitter.send(SseEmitter.event() .name("error") .data("HTTP " + response.code())); emitter.complete(); return; } ResponseBody responseBody = response.body(); if (responseBody == null) { emitter.complete(); return; } String line; while ((line = responseBody.source().readUtf8Line()) != null) { if (line.startsWith("data:")) { String json = line.substring(5).trim(); if ("[DONE]".equals(json)) { break; } // 从分片json中提取增量内容 String delta = parseDelta(json); if (!delta.isEmpty()) { emitter.send(SseEmitter.event().data(delta)); } } } emitter.complete(); } } catch (Exception e) { try { emitter.send(SseEmitter.event().name("error").data(e.getMessage())); emitter.complete(); } catch (IOException ex) { // ignore } } }); }

代码里readUtf8Line()每一次读取的都是一个SSE数据块,也就是一行的data:开头字符串。parseDelta(json)负责把增量内容从JSON中抠出来。注意流式响应的结束标志是data: [DONE],这个标志在OpenAI协议里是固定的。不少人在写流式代码时踩坑,就是因为忘了处理这个结束标志,导致连接迟迟不关闭。

4. 跑通项目:本地启动与配置说明

4.1 环境要求与准备

这个项目的运行前提是已经安装了JDK 17和Maven(或者直接使用IDEA自带的Maven)。我建议用JDK 17以上版本,因为项目里用了不少新语法特性,比如List.of、record等,老JDK编译不过去。你需要准备一个能访问大模型API的网络环境和API Key。这里说的网络环境是指目标API是否在你的网络条件下可达,如果使用的第三方模型网关已经配置好内网地址,那就不存在外网访问问题。准备步骤就三步:安装JDK、装Maven(或用IDE)、把API Key配好。

如果是第一次运行,我建议先跑非流式接口,把基本的连通性打通,再去调流式。因为非流式的问题比较好排查,最多就是请求超时或者401。流式一旦出问题,还要看SSE解析逻辑,头大得多。

4.2 配置OpenAI密钥与模型参数

在application.yml里把ai.providers部分的api-key替换成你自己的密钥,或者更推荐的方式是设置环境变量,比如在启动脚本里加上:

export OPENAI_API_KEY=sk-xxx...

这里有个很常见的坑:YAML里如果写成${OPENAI_API_KEY},而当前环境变量不存在,Spring Boot启动时会报错Could not resolve placeholder。如果你拿到的项目里是这种写法,又暂时不想配环境变量,可以直接把值改成明文Key。但请注意,一旦改成明文就不要把代码提交到公开仓库。

模型参数上,重点调三个值:max-tokens、temperature和max-history-messages。max-tokens决定了单次回复最长能有多长,设小一点能省token费用,也避免个别模型因为超长输出导致网络超时。temperature是采样温度,用于控制回答的随机性,0到1之间,越大越发散,越小越稳定。做知识问答建议0.3左右,做闲聊可以设0.8。max-history-messages控制历史消息条数,如果每条消息都很长,注意总token会指数级增长,建议先设10~20条观察一下费用曲线。

4.3 启动与测试

直接运行主类里的main方法,Spring Boot就会启动内嵌Tomcat,默认端口是8080。启动成功后,先用curl打一发最简单的请求看看通不通:

curl -X POST http://localhost:8080/api/chat/completions \ -H "Content-Type: application/json" \ -d '{"conversationId":"test","message":"你好","model":"openai-gpt-4o-mini"}'

如果返回的是一个包含content字段的JSON,说明整个链路已经通了。想测流式输出,用curl加上-N参数避免缓存,肉眼就能看到字符代理0.97/秒地蹦出来:

curl -N -X POST http://localhost:8080/api/chat/completions/stream \ -H "Content-Type: application/json" \ -d '{"conversationId":"teststream","message":"给我讲个笑话","model":"qwen-plus"}'

我在实际跑的时候,更喜欢直接用IDEA的HTTP Client或者Postman。如果不想装额外工具,直接用项目里自带的src/main/resources/http/chat.http这类文件(如果有的话),点一下发送就能看到结果。测试时留意一下日志,项目里配置了敏感信息脱敏,日志不会直接打印完整API Key,这个细节对安全比较友好。

5. 避坑与常见问题排查

5.1 现象:请求返回401 Unauthorized

原因:API Key配置错误或者环境变量没生效。特别是当你用${OPENAI_API_KEY}这种占位符时,如果IDEA启动时的环境变量没同步系统环境变量,就会读取失败。解决:先在启动类里打日志确认配置值,或者在application.yml里临时换成明文Key进行验证。如果你用的模型网关需要的是Bearer token,务必确认请求头里写的是Authorization: Bearer <token>,而不是Basic。Authorization头不对时,Spring的日志里会给出非常明显的认证失败信息,多看一眼就能定位。

5.2 现象:流式输出全是乱码,或者中文字符被截成一段段破碎字符

原因:HTTP响应的字符编码没设置成UTF-8。OkHttp默认使用UTF-8处理response body,但部分模型网关返回的Content-Type里没有显式标注charset,被误判成ISO-8859-1。解决:在读取responseBody之前,手动指定responseBody.source().readUtf8Line()确实走的是UTF-8解码,但如果你的代码里用了body.string(),建议先判断Content-Type。另一种情况是前端EventSource解析时,SSE数据中如果包含换行符,会被拆成多行,造成“截断”的错觉。解决是把parseDelta里的内容做一下replace("\n", "\\n"),避免把模型生成的多行文本打散。

5.3 现象:随着对话轮次增加,请求速度越来越慢,甚至HTTP 400

原因:历史消息不断累积,超出了模型的上下文窗口。max-history-messages虽然控制了条数,但每条消息的长度可能不一致。比如某次用户粘贴了一万字文章进来,即使只保留5条历史,也会撑爆上下文。解决:在ConversationManager里不只是限制条数,还要统计总字符数或估算token数。一个更简单的方法是在buildMessages时,如果总字符数超过阈值,就只保留从最近N条消息中按比例截断。我一般实现一个estimateTokens方法,用字符串长度除以4做一个粗估,超过阈值就切断历史。这条如果不处理,生产环境经常会半夜报警。

5.4 现象:高并发下一堆线程阻塞,服务卡死

原因:streamChat里自己创建了线程池,或者直接使用了Spring默认的SimpleAsyncTaskExecutor。这个执行器每次都会创建新线程,并发高时直接把内存打爆。解决:在AsyncConfig里定义一个自定义线程池,核心线程数10、最大线程数20、队列长度200,拒绝策略设为CallerRunsPolicy。这样当线程池满时,请求会退化为同步执行,至少不会直接丢弃。需要注意线程池的队列如果选无限队列,最大线程数参数就形同虚设。

5.5 现象:调第三方网关时总是报“400 Bad Request”

原因:兼容OpenAI协议的网关,未必兼容OpenAI的完整请求结构。最常见的是stream参数、tools参数、response_format参数不被目标网关支持。解决:把请求体简化成最基础的三要素:model、messages、stream。如果网关文档里说支持temperature,再逐步加参数,而不是一次全塞进去。我在拆解这个项目时看到CompatibleClient实现里就专门做了字段过滤,只把非空字段放到请求体里,这一点非常值得借鉴。

6. 二次开发:从演示项目到可落地的机器人服务

6.1 怎么新增一个模型供应商

如果你手上又多了一个“OpenAI兼容”的模型渠道,照着三步走就行。第一步,在application.yml的providers里加一段配置,把自己平台的base-url和api-key补上。第二步,新建一个类实现ModelClient接口,最省力的办法是复制CompatibleClient然后改动请求封装里的model字段默认值。第三步,在实现类的modelName()方法里返回你支持的模型标识,Spring扫描到Bean后,ModelRouter会自动把它注册进路由表。整个过程不用改Controller,也不用改ChatService。

6.2 把机器人接上微信公众号或钉钉

公众号接入的基本思路是把Spring Boot应用当作回调服务器。微信公众号要求你提供一个HTTP接口来接收用户消息,并返回回复内容。你只需要开发一个Controller,把ChatService的调用包进去。这里要注意两个问题:第一是超时限制,公众号要求5秒内回复,所以你建议只走非流式接口,而且把max-tokens调低到512以下,保证回复速度。第二是消息去重,微信会重试你的响应,所以需要维护messageId维度去重表。钉钉群机器人则是另一种方式,更像Webhook,你需要先把接收到的消息解析出来,再调用大模型,最后POST回钉钉的Webhook地址,这个过程反而比公众号更简单,只要注意钉钉的限流频率(每分钟不超过20次)就行。

6.3 安全加固与部署建议

这类对接大模型API的项目,最怕的就是别人盗用你的后端接口去白嫖token。我在改造时会加上一个简单的接口鉴权,在请求头里要求携带一个自定义token,在拦截器中校验。同时,给用户提交的内容做一个长度校验,前端先校验一次,后端再校验一次,防止有人绕过前端直接提交超大文本,打爆你的token消耗。部署时推荐打成Docker镜像,日志里把所有Prompt和回复完整打印出来。不要嫌日志麻烦,出了生产和部署问题,日志是唯一的后悔药。

使用这个项目时,我习惯在每一步改动后都强制走一遍完整测试:先请求非流式接口,再请求流式接口,最后用自己新加的模型供应商跑一条历史对话。因为模型路由一旦因为map初始化顺序错乱导致bean找不到,启动时不会报错,运行到对应分支才会炸。从那以后我每次接到这类对接大模型的工程,都会先把路由表打出来确认模型名对得上,再往下写代码。希望这份拆解能帮你在Spring Boot和AI大模型的结合上少踩几个坑,顺利把项目跑起来。

本文还有配套的精品资源,点击获取

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

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

立即咨询