技术派面试拷打②:策略模式 + 工厂模式 + WebSocket 流式推送
技术派面试拷打②:策略模式 + 工厂模式 + WebSocket 流式推送
简历原文:「为解决多态大模型接入的扩展性问题,基于策略模式与工厂模式实现 LLM 调用链路的统一抽象与热插拔;后端结合 WebSocket 推送 Stream 流,实现前端毫秒级响应体验。」
面试口径:“我们项目要接入多个大模型(ChatGPT、Kimi、DeepSeek 等),如果写 if-else 判断类型再调用,新增一个模型就要改主流程代码,违反开闭原则。所以用策略模式:定义
ChatService接口,每个模型写一个实现类;用工厂模式(ChatServiceFactory)在启动时自动注册所有策略,调用时根据枚举类型直接拿对应的实现,新增模型只要加一个实现类,不动任何旧代码(热插拔)。前端用 WebSocket(STOMP 协议)订阅后端推送,后端用SimpMessagingTemplate逐段推送 LLM 生成的文本,前端收到一段渲染一段,不用等全部生成完才显示,实现毫秒级响应体验。”
第一层:为什么要策略模式?(设计模式基础题)
Q1:不用策略模式,直接用 if-else 不行吗?有什么区别?
答: 用 if-else 能跑,但不好维护。策略模式是为了解决”一堆 if-else 判断类型,然后执行不同逻辑“这种场景的。
if-else 版本(反面教材):
1 | |
问题:
- 违反开闭原则:新增一个模型,要改这个
if-else方法 - 代码臃肿:所有模型的逻辑都堆在一个类里,可读性差
- 无法独立测试:想单元测试 DeepSeek 的调用逻辑,但这个方法依赖所有模型
策略模式版本(项目里的写法):
1 | |
面试回答:
“不用策略模式也能跑,但维护成本高。如果项目要接入 10 个大模型,if-else 版本要写 10 个
else if,新增模型要改主流程代码,违反开闭原则。策略模式把’调用哪个模型’的判断逻辑剥离到工厂里,每个模型独立成一个实现类,新增模型只要加一个类,不动任何旧代码(热插拔)。”
Q2:策略模式的核心角色有哪些?项目里分别是谁?
答: 策略模式有三个核心角色:
| 角色 | 作用 | 项目里的类 |
|---|---|---|
| 策略接口(Strategy) | 定义所有策略的通用方法 | ChatService |
| 具体策略(ConcreteStrategy) | 实现策略接口,每个策略一种算法 | ChatGptAiServiceImpl、DeepSeekChatServiceImpl、DoubaoAiServiceImpl 等 |
| 上下文/工厂(Context/Factory) | 根据条件选择并执行对应的策略 | ChatServiceFactory |
项目里的类结构:
1 | |
面试回答:
“策略模式有三个角色:策略接口(定义通用方法)、具体策略(每种算法的实现)、上下文/工厂(选择并执行策略)。我们项目里,
ChatService是策略接口,每个大模型是一个具体策略实现类,ChatServiceFactory是工厂,启动时自动注册所有策略,调用时根据AISourceEnum直接拿实现类。”
Q3:工厂模式有哪几种?项目里用的是哪种?为什么?
答: 工厂模式有三种:简单工厂、工厂方法、抽象工厂。项目里用的是注册式工厂(属于简单工厂的变种)。
三种工厂的区别:
| 工厂类型 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| 简单工厂 | 一个工厂类,根据条件 if-else 创建对象 |
简单 | 新增产品要改工厂类,违反开闭原则 |
| 工厂方法 | 每种产品一个工厂类,实现同一个工厂接口 | 符合开闭原则 | 类爆炸(10 个产品 = 10 个工厂类) |
| 抽象工厂 | 工厂方法 + 产品族(一组相关产品) | 保证产品族兼容 | 复杂,过度设计 |
项目里的”注册式工厂”(推荐!):
1 | |
为什么用注册式工厂?
- 不用写 if-else:所有策略自动注册到 Map
- 新增策略零修改:加一个
XxxAiServiceImpl,工厂自动把它加进 Map(Spring 自动注入List<ChatService>) - 符合开闭原则:对扩展开放,对修改关闭
面试回答:
“工厂模式有三种:简单工厂(if-else 判断)、工厂方法(每种产品一个工厂类)、抽象工厂(产品族)。我们项目用的是’注册式工厂’(简单工厂的变种):工厂持有一个 Map,启动时 Spring 自动注入所有
ChatService的实现类,调用source()方法把每个策略注册到 Map 里。新增一个模型时,只要加一个实现类,工厂自动注册,不用改任何旧代码。”
第二层:Spring 自动注入的魔法(Java 基础题)
Q4:ChatServiceFactory 的构造函数里,List<ChatService> chatServiceList 是什么神器?为什么 Spring 能自动注入所有实现类?
答: 这是 Spring 的泛化注入(by-type) 特性。
原理:
1 | |
如果有两个ChatService 实现类,但我想指定注入某一个怎么办?
1 | |
面试回答:
“
List<ChatService>是 Spring 的泛化注入特性。Spring 启动时会把所有实现了ChatService接口的 Bean 都找出来,打包成 List 注入进来。这样工厂不用手动注册,新增一个实现类时 Spring 自动把它加进 List,工厂的构造函数自动把它注册到 Map 里,实现零配置扩展。”
Q5:如果 ChatService 有 10 个实现类,但我想在工厂里按”模型厂商”分组(比如 OpenAI 系、阿里系),怎么办?
答: 这是策略模式 + 工厂模式的进阶用法,可以给策略接口加一个”分组”方法。
方案 1:在 AISourceEnum 里加一个”厂商”字段
1 | |
方案 2:用多个工厂(工厂方法模式)
1 | |
面试回答:
“如果想按厂商分组,可以在
AISourceEnum里加一个vendor字段,工厂里维护一个Map<String, List<ChatService>>按厂商分组。或者升级成’工厂方法模式’:定义一个抽象工厂接口,每个厂商实现一个工厂类(OpenAI 工厂、阿里工厂),调用时根据厂商选工厂,再从工厂里拿具体的模型策略。”
第三层:WebSocket 与 STOMP 协议(网络编程题)
Q6:为什么用 WebSocket 而不是 HTTP 轮询?区别是什么?
答: HTTP 是**”问一次,答一次”,WebSocket 是“建立连接后,服务端可以主动推消息”**。
HTTP 轮询的问题(反面教材):
1 | |
问题:
- 浪费资源:每 1 秒发一次 HTTP 请求,服务器要处理大量无效请求
- 延迟高:最多有 1 秒的延迟(轮询间隔)
- 服务器压力大:1000 个用户 = 每秒 1000 个轮询请求
WebSocket 的优势:
1 | |
优势:
- 服务端可以主动推消息(HTTP 做不到)
- 延迟极低(生成一段推一段,不用等全部完成)
- 节省资源(一条 TCP 连接搞定,不用每次 HTTP 请求)
面试回答:
“HTTP 是短连接,客户端问一次服务器答一次,要实现服务端推消息只能用轮询(每隔 N 秒问一次’有新消息吗’),延迟高且浪费资源。WebSocket 是长连接,建立连接后服务端可以主动推消息,延迟低(毫秒级),适合 LLM 流式输出场景。我们项目里,AI 每生成一段文本,后端就通过 WebSocket 推一段,前端收到一段渲染一段,不用等全部生成完。”
Q7:为什么用 STOMP 协议而不是直接用原生 WebSocket?
答: 原生 WebSocket 只有最基础的消息通道(发文本/二进制),没有消息路由、订阅、ACK 确认这些高级功能。STOMP 是在 WebSocket 之上的消息协议,相当于”给 WebSocket 加上消息队列的能力”。
原生 WebSocket 的问题:
1 | |
STOMP 的优势:
1 | |
STOMP 的核心概念(类比 RabbitMQ):
| STOMP 概念 | 类比 RabbitMQ | 作用 |
|---|---|---|
| destination | routing key + queue | 消息的目的地(订阅哪个频道) |
| subscribe | consumer 订阅队列 | 客户端订阅某个 destination |
| send | producer 发消息 | 客户端/服务端向某个 destination 发消息 |
| ack | basicAck | 确认收到消息 |
面试回答:
“原生 WebSocket 只有基础的消息收发,没有消息路由、订阅、ACK 这些高级功能,所有消息都走同一个 onmessage 回调,要自己解析消息类型。STOMP 是在 WebSocket 之上的消息协议,支持 destination(消息路由)、subscribe(订阅)、ACK 确认等高级功能,代码更清晰。我们项目里,前端订阅
/user/chat/rsp接收 AI 回复,后端通过SimpMessagingTemplate.convertAndSendToUser()推消息到这个 destination。”
Q8:项目里 WebSocket 是用哪个框架实现的?STOMP 的配置在哪里?
答: 用的是 Spring WebSocket(spring-websocket) 的 STOMP 支持,配置在 WsChatConfig.java。
核心配置(WsChatConfig.java):
1 | |
前端连接的 URL:
1 | |
面试回答:
“我们项目用 Spring WebSocket 的 STOMP 支持,配置在
WsChatConfig.java。核心配置有两部分:①configureMessageBroker设置消息代理(/chat、/msg 开头的 destination 走内存代理);②registerStompEndpoints注册 WebSocket 端点(前端连接的 URL 是/gpt/{userId}/{aiType})。前端通过这个 URL 建立 STOMP 连接,然后订阅/user/chat/rsp接收 AI 回复。”
第四层:流式推送实现细节(LLM 集成题)
Q9:后端是怎么”流式”推送 LLM 返回的文本的?SSE 和 WebSocket 有什么区别?
答: 后端用 SimpMessagingTemplate.convertAndSendToUser() 逐段推送,前端收到一段渲染一段。
流式推送的原理(以 ChatGPT 为例):
1 | |
项目里的实际代码(ChatGptAiServiceImpl.java):
1 | |
SSE(Server-Sent Events)和 WebSocket 的区别:
| SSE | WebSocket | |
|---|---|---|
| 协议 | HTTP(长连接) | 独立的 WS 协议(HTTP Upgrade) |
| 方向 | 服务端 → 客户端(单向) | 双向(客户端和服务端都能发消息) |
| 浏览器支持 | 原生支持(EventSource API) |
需要 new WebSocket() |
| 适用场景 | 服务端单向推消息(比如股价推送) | 双向通信(聊天、游戏) |
面试回答:
“后端流式推送的实现:调用 LLM API 时开启 stream=true,LLM 会逐段返回文本(SSE 格式),我们在回调里每收到一段就通过
SimpMessagingTemplate.convertAndSendToUser()推给前端,前端收到一段渲染一段,不用等全部生成完。SSE 和 WebSocket 的区别是:SSE 是 HTTP 长连接,只能服务端→客户端单向推;WebSocket 是独立的协议,支持双向通信。聊天场景用 WebSocket 更合适,因为前端也要发消息给后端。”
Q10:SimpMessagingTemplate 是怎么用的?convertAndSendToUser 和 convertAndSend 有什么区别?
答: SimpMessagingTemplate 是 Spring 提供的消息推送工具类,类似 RabbitMQ 的 RabbitTemplate。
convertAndSend vs convertAndSendToUser:
1 | |
convertAndSendToUser 的魔法:
1 | |
项目里的实际代码(WsAnswerHelper.java,推测):
1 | |
面试回答:
“
SimpMessagingTemplate是 Spring 的消息推送工具。convertAndSend(destination, msg)是广播(所有订阅了这个 destination 的客户端都能收到);convertAndSendToUser(userId, destination, msg)是点对点推送(只有指定用户能收到)。我们项目里用的是后者,因为 AI 回复只能给提问的那个人看。convertAndSendToUser会自动把 destination 转换成/user/{userId}/chat/rsp,只有这个用户的 WebSocket 连接能收到。”
Q11:如果 WebSocket 连接断了(比如网络不好),前端怎么重连?
答: STOMP 客户端(比如 stompjs)支持自动重连机制。
stompjs 的重连配置:
1 | |
后端也要处理连接断开:
1 | |
面试回答:
“WebSocket 断线重连在前端做。我们用的
stompjs客户端支持自动重连,配置reconnectDelay=5000(断线后 5 秒重连)就行。同时配置heartbeatIncoming/Outgoing(心跳检测),每 4 秒检查一次连接是否存活。后端可以监听SessionDisconnectEvent事件,在用户断线时做一些清理工作(比如取消正在进行的 AI 生成任务)。”
第五层:LLM 集成与流式处理(AI 集成题)
Q12:项目里是怎么调用大模型 API 的?用的是什么 HTTP 客户端?
答: 从代码看,项目里用的是 com.plexpt.chatgpt 这个第三方库(ChatGPT 的 Java SDK)。
调用 LLM API 的两种方式:
方式 1:直接用 OkHttp / HttpClient 调用 HTTP API(底层)
1 | |
方式 2:用封装好的 SDK(项目里的方式)
1 | |
为什么用 SDK 而不是直接 HTTP 调用?
- 省代码:不用自己构造 HTTP 请求、处理 SSE 格式
- 错误处理更完善:SDK 帮你处理了重试、超时、错误码
- 维护方便:API 升级时只要升级 SDK,不用改业务代码
面试回答:
“我们项目里用的是
com.plexpt.chatgpt这个第三方 SDK(ChatGPT 的 Java 封装),底层是 OkHttp + EventSource(SSE 客户端)。调用时开启stream=true,SDK 会逐段回调AbstractStreamListener.onMsg(message),我们在回调里把每段文本通过 WebSocket 推给前端。如果不用 SDK,也可以用 OkHttp 直接调 OpenAI 的 HTTP API,但要自己处理 SSE 格式解析,比较麻烦。”
Q13:如果调用 LLM API 超时了(比如网络不好),项目里怎么处理的?
答: 从代码看,AbstractStreamListener 有 onError 回调,专门处理 API 调用失败的场景。
项目里的错误处理(ChatGptAiServiceImpl.java):
1 | |
API 超时重试策略:
1 | |
面试回答:
“我们项目里,
AbstractStreamListener有onError回调,API 调用失败时会在答案后面追加错误信息(Error: xxx),然后通过 WebSocket 推给前端。onClosed回调里会判断是不是正常结束(AI 返回完了),如果不是,会主动结束对话并提示用户’重新提问’。底层用的 OkHttp 可以配置连接超时和读取超时,也会自动重试连接失败。”
第六层:开放设计题(架构设计)
Q14:如果让你设计一个”支持 10 个大模型”的聊天系统,你会怎么设计?
答: 这是策略模式的最佳实践场景,项目里的设计已经很不错了,但可以再优化。
V1(项目当前设计):策略模式 + 注册式工厂
1 | |
优点: 新增模型零修改(只要加实现类)
缺点: 所有策略都在同一个 JVM 里,如果某个模型的 SDK 很重(比如要加载 1GB 的模型文件),会影响其他策略
V2(优化:策略模式 + 插件化)
1 | |
V3(终极:支持动态添加模型,不用重启服务)
1 | |
面试回答模板:
“如果要支持 10 个大模型,我会用策略模式 + 注册式工厂(项目里已经实现了)。进一步优化可以考虑插件化:把每个模型的 SDK 封装成独立进程(微服务),核心服务通过 gRPC 调用插件服务,这样某个模型的 SDK 崩溃了不会影响其他模型。如果要求动态添加模型(不用重启服务),可以把策略实现做成 Groovy 脚本,核心服务动态加载。”
Q15(附加):项目里 LLM 的 API Key 是怎么管理的?会不会泄露?
答: 从代码看,API Key 应该配置在 application-ai.yml 里,用 Spring 的 @Value 或 @ConfigurationProperties 注入。
API Key 管理的三种方案:
方案 1:配置文件(简单,但不安全)
1 | |
问题: 配置文件如果提交到 Git,API Key 就泄露了。
方案 2:环境变量(推荐)
1 | |
或者:
1 | |
方案 3:配置中心(生产环境推荐)
1 | |
面试回答:
“我们项目里 API Key 配置在
application-ai.yml里,通过@ConfigurationProperties注入到ChatGptIntegration类。生产环境会用环境变量或配置中心(Nacos / Apollo)管理 API Key,避免提交到 Git 导致泄露。也可以在网关层统一做 API Key 的加密解密,进一步保证安全。”
总结:简历上这句话的面试回答模板
面试官:”你的简历上写了’策略模式 + 工厂模式实现 LLM 调用链路统一抽象,WebSocket 推送 Stream 流实现毫秒级响应’,你能详细讲一下吗?”
回答模板(背下来!):
“好的。我们项目要接入多个大模型(ChatGPT、DeepSeek、豆包等),如果写 if-else 判断类型再调用,新增一个模型就要改主流程代码,违反开闭原则。
策略模式:我们定义了
ChatService接口(策略接口),每个大模型写一个实现类(具体策略),比如ChatGptAiServiceImpl、DeepSeekChatServiceImpl。每个实现类通过source()方法告诉工厂’我是哪个模型’。工厂模式:我们用的是’注册式工厂’(
ChatServiceFactory)。Spring 启动时自动注入所有ChatService的实现类(通过List<ChatService>构造函数注入),工厂在构造函数里把每个策略注册到 Map 里(key =AISourceEnum,value = 策略实现)。调用时只要factory.getChatService(AISourceEnum.CHAT_GPT_3_5)就能拿到对应的实现类,不用 if-else。新增模型只要加一个实现类,不动任何旧代码(热插拔)。WebSocket 流式推送:前端通过 STOMP over WebSocket 建立长连接(URL =
/gpt/{userId}/{aiType}),订阅/user/chat/rsp接收 AI 回复。后端调用 LLM API 时开启stream=true,LLM 逐段返回文本,我们在AbstractStreamListener.onMsg()回调里把每段文本通过SimpMessagingTemplate.convertAndSendToUser()推给前端。前端收到一段渲染一段,不用等全部生成完,实现毫秒级响应体验。”
下一篇预告: FastExcel + 线程池 + CountDownLatch 并发导出,百万级数据导出性能优化 🚀