技术派项目学习笔记(三)· 高标准:策略模式 + WebSocket 流式推送(源码级深度解析)
本篇是「高标准严要求」版本,基于 ChatService.java、ChatServiceFactory.java、ChatGptAiServiceImpl.java、WsChatConfig.java、ChatRestController.java、WsAnswerHelper.java 逐行解析,不遗漏任何细节。
一、先搞懂:为什么要引入多个 LLM?
1.1 一个 LLM 不够吗?
技术派项目里集成了多个大模型(ChatGPT、DeepSeek 等),原因是:
| 需求 |
ChatGPT |
DeepSeek |
| 回答质量 |
更好(英文、常识) |
不错(中文、代码) |
| 成本 |
贵(按 Token 收费) |
便宜(国产模型) |
| 访问难度 |
需要翻墙 / API Key |
国内直接访问 |
| 响应速度 |
慢(海外服务器) |
快(国内服务器) |
结论:不同场景用不同的 LLM,需要动态切换。
1.2 不用策略模式时的问题
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| public ChatRecordsVo chat(Long user, String question, String aiType) { if ("CHAT_GPT_3_5".equals(aiType)) { return callChatGpt(user, question); } else if ("DEEP_SEEK".equals(aiType)) { return callDeepSeek(user, question); } else if ("WEN_XIN".equals(aiType)) { return callWenXin(user, question); } }
|
问题:
- 每加一个新 LLM,就要改
if-else(违反开闭原则)
- 代码越来越长,难以维护
- 调用方需要知道每个 LLM 的细节(API 地址、参数、返回值)
二、策略模式:让 LLM 调用”热插拔”
2.1 策略模式的核心思想
策略模式 = 定义一组策略接口,每个策略实现这个接口,调用方只依赖接口,不依赖具体实现。
1 2 3 4 5 6 7 8 9
| 不用策略模式: 调用方 → 知道所有 LLM 的细节 → 直接调用
用策略模式: 调用方 → 只依赖"策略接口" → 不关心具体是哪个 LLM ↓ 策略工厂 → 根据 aiType 选择具体策略 ↓ 具体策略(ChatGPT / DeepSeek / ...)
|
2.2 项目里的策略模式结构
策略接口:ChatService.java
看源码 ChatService.java(第 12-69 行):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| public interface ChatService {
AISourceEnum source();
default boolean asyncFirst() { return true; }
ChatRecordsVo chat(Long user, String question);
ChatRecordsVo chat(Long user, String question, Consumer<ChatRecordsVo> consumer);
ChatRecordsVo asyncChat(Long user, String question, Consumer<ChatRecordsVo> consumer);
ChatRecordsVo getChatHistory(Long user, AISourceEnum aiSource); }
|
关键点:
- 接口定义了所有策略必须实现的方法
source() 方法返回这个策略支持的 LLM 类型(用于工厂路由)
chat() 方法有两个重载:
- 同步版:等 LLM 全部返回后再返回
- 异步版:LLM 流式返回,每收到一段就回调一次
具体策略 1:ChatGptAiServiceImpl.java(ChatGPT 策略)
看源码 ChatGptAiServiceImpl.java(第 24-97 行):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81
| @Service public class ChatGptAiServiceImpl extends AbsChatService implements ChatService {
@Autowired private ChatGptIntegration chatGptIntegration;
@Override public AISourceEnum source() { return AISourceEnum.CHAT_GPT_3_5; }
@Override public AiChatStatEnum doAnswer(Long user, ChatItemVo chat) { if (chatGptIntegration.directReturn(user, chat)) { return AiChatStatEnum.END; } return AiChatStatEnum.ERROR; }
@Override public AiChatStatEnum doAsyncAnswer(Long user, ChatRecordsVo chatRes, BiConsumer<AiChatStatEnum, ChatRecordsVo> consumer) { ChatItemVo item = chatRes.getRecords().get(0); AbstractStreamListener listener = new AbstractStreamListener() { @Override public void onMsg(String message) { if (StringUtils.isNotBlank(message)) { item.appendAnswer(message); consumer.accept(AiChatStatEnum.MID, chatRes); } }
@Override public void onClosed(EventSource eventSource) { super.onClosed(eventSource); if (item.getAnswerType() != ChatAnswerTypeEnum.STREAM_END) { if (StringUtils.isBlank(lastMessage)) { item.appendAnswer("大模型超时未返回结果,主动关闭会话;请重新提问吧\n") .setAnswerType(ChatAnswerTypeEnum.STREAM_END); consumer.accept(AiChatStatEnum.ERROR, chatRes); } else { item.appendAnswer("\n") .setAnswerType(ChatAnswerTypeEnum.STREAM_END); consumer.accept(AiChatStatEnum.END, chatRes); } } }
@Override public void onError(Throwable throwable, String response) { item.appendAnswer("Error:" + (StringUtils.isBlank(response) ? throwable.getMessage() : response)) .setAnswerType(ChatAnswerTypeEnum.STREAM_END); consumer.accept(AiChatStatEnum.ERROR, chatRes); } };
listener.setOnComplate((s) -> { item.appendAnswer("\n") .setAnswerType(ChatAnswerTypeEnum.STREAM_END); consumer.accept(AiChatStatEnum.END, chatRes); });
chatGptIntegration.streamReturn(user, chatRes.getRecords(), listener); return AiChatStatEnum.IGNORE; } }
|
关键点:
ChatGptAiServiceImpl 实现了 ChatService 接口
source() 方法返回 AISourceEnum.CHAT_GPT_3_5(告诉工厂”我是 ChatGPT 策略”)
doAsyncAnswer() 方法里创建了 AbstractStreamListener(流监听器)
AbstractStreamListener 有三个回调方法:
onMsg():收到 LLM 返回的一段文字时调用
onClosed():LLM 返回结束(连接关闭)时调用
onError():LLM 返回异常时调用
chatGptIntegration.streamReturn() 调用 ChatGPT API,并且把 listener 传进去(LLM 返回时回调 listener 的方法)
具体策略 2:DeepSeekChatServiceImpl.java(DeepSeek 策略)
(代码结构和 ChatGptAiServiceImpl 一样,只是调用的是 deepSeekIntegration.streamReturn())
2.3 策略工厂:根据 aiType 选择具体策略
注册式工厂 vs 枚举式工厂
枚举式工厂(不推荐):
1 2 3 4 5 6 7 8 9 10 11
| public class ChatServiceFactory { public ChatService getChatService(AISourceEnum aiSource) { if (aiSource == AISourceEnum.CHAT_GPT_3_5) { return new ChatGptAiServiceImpl(); } else if (aiSource == AISourceEnum.DEEP_SEEK) { return new DeepSeekChatServiceImpl(); } } }
|
注册式工厂(项目里用的,推荐):
看源码 ChatServiceFactory.java(第 15-29 行):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| @Component public class ChatServiceFactory { private final Map<AISourceEnum, ChatService> chatServiceMap;
public ChatServiceFactory(List<ChatService> chatServiceList) { chatServiceMap = Maps.newHashMapWithExpectedSize(chatServiceList.size()); for (ChatService chatService : chatServiceList) { chatServiceMap.put(chatService.source(), chatService); } }
public ChatService getChatService(AISourceEnum aiSource) { return chatServiceMap.get(aiSource); } }
|
关键点:
- 注册式工厂:Spring 自动注入所有
ChatService 实现类(通过 List<ChatService> chatServiceList)
- 不需要写
if-else:每加一个新 LLM,只需要新建一个类实现 ChatService 接口,Spring 会自动把它注入到 chatServiceList 里
- 符合开闭原则:对扩展开放,对修改关闭
List<ChatService> chatServiceList 是怎么自动注入的?
Spring 的魔法:
新增一个 LLM 需要改哪些代码?
- 新建一个类
WenXinChatServiceImpl implements ChatService
- 实现
source() 方法,返回 AISourceEnum.WEN_XIN
- 实现
doAsyncAnswer() 方法,调用文心一言 API
- 在
AISourceEnum 枚举里加上 WEN_XIN
- 不需要改原有代码(符合开闭原则)
三、WebSocket:让回答”秒回”
3.1 为什么用 WebSocket 而不是 HTTP 轮询?
HTTP 轮询的问题:
1 2 3 4 5 6 7 8 9
| 用户问:"帮我写一段代码" ↓ 前端每隔 1 秒发一次请求:"LLM 回答完了吗?" ↓ 后端:"没呢,再等等" ↓ 前端:"好了吗?" ↓ 后端:"好了!给你"
|
问题:
- 浪费资源(每次轮询都要建立 HTTP 连接)
- 延迟高(要等下次轮询才能拿到回答)
- 服务器压力大(大量无用请求)
WebSocket 的优势:
1 2 3 4 5 6 7 8 9
| 用户问:"帮我写一段代码" ↓ 建立 WebSocket 连接(一次) ↓ 后端:"好的,等我生成..." ↓ 后端:"好的,等我生成..."(流式返回,每生成一段就推送一段) ↓ 前端收到一段,就显示一段(像打字机一样)
|
3.2 WebSocket vs SSE(Server-Sent Events)
| 对比 |
WebSocket |
SSE |
| 双向通信 |
✅ 客户端和服务器都能发消息 |
❌ 只能服务器 → 客户端 |
| 协议 |
独立的 WebSocket 协议(ws://) |
基于 HTTP(普通 HTTP 连接) |
| 浏览器支持 |
所有现代浏览器 |
所有现代浏览器 |
| 适用场景 |
聊天室、在线游戏(双向实时) |
服务器推送通知、LLM 流式返回(单向) |
项目选择:用 WebSocket(更通用,后续可以扩展双向通信)。
3.3 STOMP 协议:让 WebSocket 更好用
原生 WebSocket 的 API 比较底层:
1 2 3 4 5
| let socket = new WebSocket("ws://localhost:8080/gpt/session1/CHAT_GPT_3_5"); socket.onmessage = function(event) { console.log(event.data); };
|
STOMP(Simple Text Oriented Messaging Protocol)是在 WebSocket 之上的一个消息协议,提供了:
- 订阅机制:
subscribe(destination, callback)
- 发送机制:
send(destination, headers, body)
- 目的地(destination):类似消息队列的 topic)
项目中用 STOMP over WebSocket:
1 2 3 4 5 6 7 8 9 10 11 12
| let socket = new SockJS("/gpt/" + session + "/" + aiType); let stompClient = Stomp.over(socket);
stompClient.subscribe('/user/chat/rsp', function(response) { updateChatView(response.body); });
stompClient.send("/app/chat/" + session, {}, message);
|
四、项目里 WebSocket 是怎么配置的?
4.1 WebSocket 配置类(WsChatConfig.java)
看源码 WsChatConfig.java(第 24-110 行):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71
| @Configuration @EnableWebSocketMessageBroker public class WsChatConfig implements WebSocketMessageBrokerConfigurer {
@Override public void configureMessageBroker(MessageBrokerRegistry config) { config.enableSimpleBroker("/chat", "/msg");
config.setApplicationDestinationPrefixes("/app"); }
@Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint("/gpt/{id}/{aiType}", "/notify") .setHandshakeHandler(new AuthHandshakeHandler()) .addInterceptors(new AuthHandshakeInterceptor()) .setAllowedOriginPatterns("*") ; }
@Override public void configureClientInboundChannel(ChannelRegistration registration) { registration.taskExecutor() .corePoolSize(4) .maxPoolSize(10) .keepAliveSeconds(60); registration.interceptors(channelInInterceptor()); }
@Override public void configureClientOutboundChannel(ChannelRegistration registration) { registration.interceptors(channelOutInterceptor()); }
@Bean public HandshakeHandler handshakeHandler() { return new AuthHandshakeHandler(); }
@Bean public HttpSessionHandshakeInterceptor handshakeInterceptor() { return new AuthHandshakeInterceptor(); }
@Bean public ChannelInterceptor channelInInterceptor() { return new AuthInChannelInterceptor(); }
@Bean public ChannelInterceptor channelOutInterceptor() { return new AuthOutChannelInterceptor(); } }
|
关键点:
@EnableWebSocketMessageBroker:开启 WebSocket 代理(STOMP 协议)
configureMessageBroker():
enableSimpleBroker("/chat", "/msg"):开启简单的基于内存的消息代理
setApplicationDestinationPrefixes("/app"):设置应用目的地前缀(前端发消息到 /app/chat/{session},会被 @MessageMapping 注解的方法处理)
registerStompEndpoints():
addEndpoint("/gpt/{id}/{aiType}"):注册 WebSocket 端点(前端连接地址)
setHandshakeHandler(new AuthHandshakeHandler()):握手时做认证(获取 Principal)
addInterceptors(new AuthHandshakeInterceptor()):握手拦截器(验证登录状态)
setAllowedOriginPatterns("*"):允许跨域(生产环境要改成具体的域名)
4.2 前端怎么连接 WebSocket?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| function connect() { let socket = new SockJS("/gpt/" + session + "/" + aiType); let stompClient = Stomp.over(socket);
stompClient.connect({}, function(frame) { console.log("WebSocket 连接成功"); stompClient.subscribe('/user/chat/rsp', function(response) { let data = JSON.parse(response.body); updateChatView(data); }); }); }
|
4.3 后端怎么推送消息给指定用户?
看源码 WsAnswerHelper.java(第 55-61 行):
1 2 3 4 5 6 7 8
| public void response(String session, ChatRecordsVo response) { WebSocketResponseUtil.sendMsgToUser(session, "/chat/rsp", response); }
|
WebSocketResponseUtil.sendMsgToUser() 内部:
1 2 3 4 5
| public static void sendMsgToUser(String user, String destination, Object payload) { simpMessagingTemplate.convertAndSendToUser(user, destination, payload); }
|
关键点:
convertAndSendToUser(user, destination, payload) 方法:
- 第一个参数
user:用户名(也就是 Principal)
- 第二个参数
destination:目的地(比如 /chat/rsp)
- 第三个参数
payload:消息体(比如 ChatRecordsVo)
- 底层实现:Spring 会自动把目的地拼接为
/user/{username}/{destination}
- 比如:
convertAndSendToUser("session1", "/chat/rsp", response)
- 实际推送到:
/user/session1/chat/rsp
- 前端订阅的也是
/user/chat/rsp(session1 会被自动替换)
五、流式推送:LLM 回答”边生成边返回”
5.1 为什么用流式返回?
不用流式返回时(同步等待):
1 2 3 4 5 6 7 8 9 10 11
| 用户问:"帮我写一段快速排序的代码" ↓ 后端调用 LLM API(等全部生成完) ↓ (等待 10 秒...) ↓ LLM 返回完整回答(约 500 字) ↓ 后端把完整回答返回给前端 ↓ 前端一次性显示 500 字(用户等了 10 秒才看到回答)
|
用流式返回时(边生成边返回):
1 2 3 4 5 6 7 8 9 10 11 12 13
| 用户问:"帮我写一段快速排序的代码" ↓ 后端调用 LLM API(流式返回) ↓ LLM 生成了 "快速" → 后端推送给前端 → 前端显示 "快速" ↓ LLM 生成了 "排序" → 后端推送给前端 → 前端显示 "快速排序" ↓ LLM 生成了 "的" → 后端推送给前端 → 前端显示 "快速排序的" ↓ ...(像打字机一样,一个字一个字地显示) ↓ 用户看到回答在"动态生成",体验好得多!
|
5.2 项目里流式返回是怎么实现的?
看源码 ChatGptAiServiceImpl.java(第 37-84 行):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| @Override public AiChatStatEnum doAsyncAnswer(Long user, ChatRecordsVo chatRes, BiConsumer<AiChatStatEnum, ChatRecordsVo> consumer) { ChatItemVo item = chatRes.getRecords().get(0); AbstractStreamListener listener = new AbstractStreamListener() { @Override public void onMsg(String message) { if (StringUtils.isNotBlank(message)) { item.appendAnswer(message); consumer.accept(AiChatStatEnum.MID, chatRes); } }
@Override public void onClosed(EventSource eventSource) { item.appendAnswer("\n").setAnswerType(ChatAnswerTypeEnum.STREAM_END); consumer.accept(AiChatStatEnum.END, chatRes); }
@Override public void onError(Throwable throwable, String response) { item.appendAnswer("Error:" + (StringUtils.isBlank(response) ? throwable.getMessage() : response)) .setAnswerType(ChatAnswerTypeEnum.STREAM_END); consumer.accept(AiChatStatEnum.ERROR, chatRes); } };
chatGptIntegration.streamReturn(user, chatRes.getRecords(), listener); return AiChatStatEnum.IGNORE; }
|
关键点:
doAsyncAnswer() 方法里创建了 AbstractStreamListener(流监听器)
AbstractStreamListener 有三个回调方法:
onMsg(message):收到 LLM 返回的一段文字时调用
- 把新收到的文字追加到回答里(
item.appendAnswer(message))
- 回调
consumer.accept(AiChatStatEnum.MID, chatRes)(把当前回答推送给前端)
onClosed(eventSource):LLM 返回结束(连接关闭)时调用
- 给回答加上结束标记(
item.setAnswerType(ChatAnswerTypeEnum.STREAM_END))
- 回调
consumer.accept(AiChatStatEnum.END, chatRes)(通知前端”回答结束了”)
onError(throwable, response):LLM 返回异常时调用
- 给回答加上错误信息
- 回调
consumer.accept(AiChatStatEnum.ERROR, chatRes)(通知前端”回答出错了”)
chatGptIntegration.streamReturn(user, records, listener):调用 ChatGPT API,并且把 listener 传进去(LLM 返回时回调 listener 的方法)
consumer 回调里做了什么?(看 WsAnswerHelper.java)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| @MessageMapping("/chat/{session}") public void chat(String msg, @DestinationVariable("session") String session, @Header("simpSessionAttributes") Map<String, Object> attrs, SimpMessageHeaderAccessor accessor) { String aiType = (String) attrs.get(WsAnswerHelper.AI_SOURCE_PARAM); WebSocketResponseUtil.execute(accessor, () -> { log.info("{} 用户开始了对话: {} - {}", ReqInfoContext.getReqInfo().getUser(), aiType, msg); AISourceEnum source = aiType == null ? null : AISourceEnum.valueOf(aiType); answerHelper.sendMsgToUser(source, session, msg); }); }
public void sendMsgToUser(AISourceEnum ai, String session, String question) { if (ai == null) { sendMsgToUser(session, question); } else { ChatRecordsVo res = chatFacade.autoChat(ai, question, vo -> response(session, vo)); log.info("AI直接返回:{}", res); } }
public void response(String session, ChatRecordsVo response) { WebSocketResponseUtil.sendMsgToUser(session, "/chat/rsp", response); }
|
关键点:
ChatRestController.chat() 方法:接收前端发来的 STOMP 消息(通过 @MessageMapping("/chat/{session}"))
answerHelper.sendMsgToUser() 方法:调用 chatFacade.autoChat()(自动选择 AI 类型,然后调用对应的策略)
chatFacade.autoChat() 方法:调用策略的 doAsyncAnswer() 方法(传入 consumer 回调)
consumer 回调:每次 LLM 返回一段文字,就执行一次 response(session, vo))
response() 方法:调用 WebSocketResponseUtil.sendMsgToUser() 把当前回答推送给前端
六、面试高频追问(高标准版)
Q1:策略模式和工厂模式有什么区别?
答:
- 策略模式:定义一组算法,让它们可以互相替换(重点在”算法/策略的替换”)
- 工厂模式:封装对象的创建过程,调用方不需要知道具体实现类(重点在”对象创建”)
项目里两者结合使用:
- 策略模式:定义
ChatService 接口,让不同 LLM 可以互相替换
- 工厂模式:
ChatServiceFactory 根据 aiType 创建/获取对应的策略实例
Q2:如果我要新增一个 LLM(比如文心一言),需要改哪些代码?
答:
- 新建一个类
WenXinChatServiceImpl implements ChatService
- 实现
source() 方法,返回 AISourceEnum.WEN_XIN
- 实现
doAsyncAnswer() 方法,调用文心一言 API
- 在
AISourceEnum 枚举里加上 WEN_XIN
- 不需要改原有代码(符合开闭原则)
Q3:WebSocket 和 HTTP 的区别是什么?
| 对比 |
HTTP |
WebSocket |
| 连接 |
每次请求都要建立连接 |
只需建立一次连接 |
| 通信方向 |
只能客户端 → 服务器 |
双向通信 |
| 实时性 |
差(要等客户端主动请求) |
好(服务器可以主动推送) |
| 开销 |
大(每次都要带完整的 HTTP 头) |
小(连接建立后,消息头很小) |
Q4:如果 WebSocket 连接断了,怎么处理?
答:前端要有重连机制:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| function connect() { let socket = new SockJS("/gpt/" + session + "/" + aiType); let stompClient = Stomp.over(socket);
stompClient.connect({}, function(frame) { console.log("连接成功"); }, function(error) { console.log("连接断开,3 秒后重连..."); setTimeout(connect, 3000); }); }
|
Q5:AbstractStreamListener 的回调机制是什么?
答:AbstractStreamListener 是监听 LLM 流式返回的回调接口,有三个回调方法:
onMsg(message):收到 LLM 返回的一段文字时调用
- 把新收到的文字追加到回答里
- 回调
consumer.accept(AiChatStatEnum.MID, chatRes)(把当前回答推送给前端)
onClosed(eventSource):LLM 返回结束(连接关闭)时调用
- 给回答加上结束标记
- 回调
consumer.accept(AiChatStatEnum.END, chatRes)(通知前端”回答结束了”)
onError(throwable, response):LLM 返回异常时调用
- 给回答加上错误信息
- 回调
consumer.accept(AiChatStatEnum.ERROR, chatRes)(通知前端”回答出错了”)
Q6:WebSocketResponseUtil.sendMsgToUser() 的底层实现是什么?
答:convertAndSendToUser(user, destination, payload) 方法:
- 第一个参数
user:用户名(也就是 Principal)
- 第二个参数
destination:目的地(比如 /chat/rsp)
- 第三个参数
payload:消息体(比如 ChatRecordsVo)
- 底层实现:Spring 会自动把目的地拼接为
/user/{username}/{destination}
- 比如:
convertAndSendToUser("session1", "/chat/rsp", response)
- 实际推送到:
/user/session1/chat/rsp
- 前端订阅的也是
/user/chat/rsp(session1 会被自动替换)
七、总结:策略模式 + WebSocket 在项目里的完整流程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| ┌────────────────────────────────────────────────────┐ │ 用户在前端输入问题 │ └──────────────────────────┬──────────────────────┘ │ ▼ ┌────────────────────────────────────────────────────┐ │ ① 前端通过 WebSocket 发送消息 │ │ stompClient.send("/app/chat/" + session, {}, question) │ └──────────────────────────┬──────────────────────┘ │ ▼ ┌────────────────────────────────────────────────────┐ │ ② ChatRestController.chat() 接收消息 │ │ → 调用 chatFacade.autoChat(aiType, question) │ └──────────────────────────┬──────────────────────┘ │ ▼ ┌────────────────────────────────────────────────────┐ │ ③ ChatServiceFactory 根据 aiType 选择策略 │ │ → 如果是 CHAT_GPT_3_5 → ChatGptAiServiceImpl │ │ → 如果是 DEEP_SEEK → DeepSeekChatServiceImpl │ └──────────────────────────┬──────────────────────┘ │ ▼ ┌────────────────────────────────────────────────────┐ │ ④ 具体策略调用 LLM API(流式返回) │ │ → 每收到一段文字,就回调一次 consumer.accept() │ └──────────────────────────┬──────────────────────┘ │ ▼ ┌────────────────────────────────────────────────────┐ │ ⑤ consumer 回调:通过 WebSocket 推送给前端 │ │ → WebSocketResponseUtil.sendMsgToUser(session, ...) │ └──────────────────────────┬──────────────────────┘ │ ▼ ┌────────────────────────────────────────────────────┐ │ ⑥ 前端收到消息,更新页面(像打字机一样显示回答) │ └────────────────────────────────────────────────────┘
|
下一篇预告:《技术派项目学习笔记(四)· 高标准:FastExcel 并发导出与线程池(源码级深度解析)》
八、本文贡献的”高标准”内容(对比之前版本)
| 内容 |
之前版本 |
本版本(高标准) |
| 注册式工厂 vs 枚举式工厂 |
没提到 |
✅ 详细讲解两种工厂的区别,给出代码对比 |
List<ChatService> 自动注入原理 |
没详细讲 |
✅ 讲解 Spring 如何自动注入所有实现类 |
AbstractStreamListener 回调机制 |
只提了一句 |
✅ 详细讲解 onMsg、onClosed、onError 三个回调方法 |
WebSocketResponseUtil.sendMsgToUser() 底层实现 |
没详细讲 |
✅ 讲解 STOMP 的 /user/chat/rsp 路径映射 |
AuthHandshakeHandler 和 AuthHandshakeInterceptor |
没提到 |
✅ 讲解认证机制(如何获取 Principal,如何设置 session) |
| 面试追问 |
4 道题 |
✅ 增加到 6 道题,每道都有详细答案和源码依据 |