一、核心需求
- 功能需求:用戶可以通過語音與AI對話,并實現類似ChatGPT的實時交互(流式響應,打字機效果)
- 技術需求:在現有微服務架構中進行擴展(SpringCloud)
二、技術盲點
- 陌生領域
- 語音錄入
- 語音流傳輸
- Java對接大模型API
- 技術選型難點
- WebSocket框架選擇
- Spring WebSocket、t-io、Java-WebSocket、Socket.io等
- 流式編程 or 響應式編程 (本質是分塊傳輸)
- Rxjava:UnicastProcessor、Flowable
- Spring WebFlux:Flux、Mono、WebClient
- Java9 Reactive Stream API:Publisher、Subscriber、Subscription、Processor
- Spring MVC:StreamingResponseBody、SseEmitter 、Servlet3.0
三、技術選型與驗證
- 第一步,技術選型
- 已驗證:Spring WebSocket、t-io、spring-ai-alibaba、百煉靈積等等
- 未驗證:Java-WebSocket、Socket.io
- 第二步,驗證流程
- 單點功能驗證:
- 第三方大模型API對接
- WebSocket服務端與客戶端通信
- 功能集成驗證:
- 語音錄入 + WebSocket傳輸 + 服務端 + 第三方大模型API對接
- 整合現有微服務架構(語音錄入 → 網關 → WebSocket鏈路驗證)
四、AI編程中的卡點問題與解決
- 問題1:消息大小限制的坑:WebSocket 消息未處理(二進制流)
- 現象:
- 服務端無日志、自定義處理器未觸發(該問題最大的坑是,無從下手的感覺)
- 分析:
- AI解決:先問AI,效果不佳
- 百度解決:再查百度,效果仍然不佳(有找到正確的解決方案,由于并未真正理解,仍然卡點了很久)
- Debug源碼:此時已無從下手,因此只能Debug源碼,逐步排查(逐步理解WebSocket協議的實現原理)
- Debug源碼+AI提示:隨著了解的越深,逐步建立手感,對問題原因有了大致的判斷(驗證判斷是否正確)
- 根因:
- 消息大小超過限制
- 解決:
- 最終方案:調整消息大小,避免超出websocket server端的消息大小限制
# application.yml
spring:servlet:multipart:# 整個請求大小限制max-request-size: 10MB# 上傳單個文件大小限制max-file-size: 20MB
- 問題2:網關Filter劫持WebSocket的101響應:網關轉發WebSocket請求,導致消息未處理的異常
- 現象:該問題最大的坑是,無從下手的感覺
- 客戶端:報錯 “java.io.IOException: 你的主機中的軟件中止了一個已建立的連接”
- 服務端:握手請求成功,有打印握手日志(但數據請求的處理器沒有執行)
- 網關:有打印請求日志,但沒有打印響應日志
- 分析:由于無從下手,只能逐步排除各個環節的問題
- 限制排查:下意識的認為還是消息大小超過限制的問題(未觸發消息大小限制)
- 網關配置檢查:確認網關配置與請求轉發的正確性(請求轉發正常)
- 握手請求檢查:確認握手請求是否正常被執行(自定義攔截器,握手請求正常執行)
- 網關請求頭檢查:檢查握手請求頭是否正常透傳(websocket請求header正常透傳)
- 握手請求的響應檢查:到這一步基本確定是response未被正確處理
- 定位到根因:此處結合AI提示,定位到是網關中的過濾器,未正確處理握手請求的response
- AI修復問題:定位到問題根因,此時讓AI給出方案,但效果不佳,借助該思路,手動修正代碼,解決問題
- 根因:
- 網關過濾器對Response進行了包裝,導致握手請求的101狀態碼未被正確處理,最終導致WebSocket連接建立失敗。
- 解決:
- 最終方案:優化網關AccessLogFilter過濾器代碼,對websocket請求直接放行
public class AccessLogGlobalFilter implements GlobalFilter, Ordered {@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {StopWatch stopWatch = new StopWatch();stopWatch.start();ServerHttpRequest httpRequest = exchange.getRequest();ServerHttpResponse httpResponse = exchange.getResponse();// WebSocket 握手請求,直接進入下一個Filter,并返回原始response,確保 WebSocket 握手正常處理101狀態碼// 說明:WebSocket Client 通過Http請求與WebSocket Server握手建立長連接,后續的通信都是該長連接進行通信,不會再被filter攔截if (checkWebSocketHandShake(httpRequest)) {return filter("websocket handshake", exchange, chain, stopWatch, httpResponse, httpRequest);}// 網關:判斷是否為流式請求,支持ResponseBodyEmitter、SseEmitter的流式響應String acceptHeader = httpRequest.getHeaders().getFirst(HttpHeaders.ACCEPT);boolean isStreamingRequest = acceptHeader != null && acceptHeader.contains(MediaType.TEXT_EVENT_STREAM_VALUE);if (isStreamingRequest) {return filter("stream request", exchange, chain, stopWatch, httpResponse, httpRequest);}// 省略相關代碼 ... ...}/*** 執行攔截器鏈(不包裝request和response)*/private Mono<Void> filter(String logPrefix, ServerWebExchange exchange, GatewayFilterChain chain, StopWatch stopWatch, ServerHttpResponse httpResponse, ServerHttpRequest httpRequest) {if (log.isInfoEnabled() && configProperties.isLogEnabled()) {log.info("請求參數 {} [{}] [{}] query:{}, header:{}", logPrefix, httpRequest.getURI().getPath(), httpRequest.getMethod(), httpRequest.getURI().getRawQuery(), exchange.getRequest().getHeaders());}return chain.filter(exchange).doFinally(s -> MdcUtil.removeTraceId())// 清除MDC.then(Mono.fromRunnable(() -> {stopWatch.stop();// 為了方便排查問題,還是打印一個簡單的日志if (log.isInfoEnabled()) {log.info("響應參數 {} {} time: {} ms", logPrefix, httpResponse.getRawStatusCode(), stopWatch.getTotalTimeMillis());}}));}/*** 檢查是否為WebSocket握手請求** @return true 表示是WebSocket握手請求*/public boolean checkWebSocketHandShake(ServerHttpRequest httpRequest) {// 從請求頭中獲取 Upgrade 標志,若為websocket,則表示將普通http請求,升級為websocket請求String upgradeHeader = httpRequest.getHeaders().getFirst(HttpHeaders.UPGRADE);if ("websocket".equalsIgnoreCase(upgradeHeader)) {return true;}return false;}
}
五、實踐總結
僅針對上述場景的實踐總結
- 技術實踐經驗
- 需深入理解WebSocket協議原理(消息限制、握手流程)
- 網關需特殊處理WebSocket協議(避免過濾器干擾)
- AI工具定位
- 搜索引擎:替代傳統搜索,大部分時候,AI的搜索效果,比百度的效果要好
- 輔助神器:明確的問題,AI效果好;未知或復雜的問題,AI效果差,但用AI輔助理解技術原理效果好,可將未知或復雜的問題,轉換為明確的問題,再讓AI解決
- AI工具效果
- 通義靈碼(Chat模式):生成代碼效果一般,解決問題效果一般
- Trae(Builder模式):效果較好(有時也會抽瘋,需要多輪對話)
- AI工具局限性
- 生成代碼需人工校驗(無法直接投產)
- 復雜問題需多輪對話(效率較低,很容易放棄)
- AI開發心得
- 復雜場景,需分步拆解驗證,再集成整合
- 頑固問題,目前AI無法替代人工,需手動介入處理(給AI明確的問題點,AI才會有更好的效果)
- 陌生領域,對于不了解的技術,上手變得容易很多(如流式編程)
- 開發習慣,過往的開發習慣較難改變,存在習慣性忽略使用AI的情況(需多用多練)
六、開放性思考
- 普通人,使用AI編程,對代碼的要求是什么?
- 生成簡單的驗證性代碼,保證功能實現即可,不關注代碼質量
- 程序員,使用AI編程,對代碼的要求是什么?
- 生成可投產的生產級別代碼,保證功能實現的同時,也關注代碼質量和測試質量(要求完全自主可控)
- 面對AI編程效果不佳,尤其經過多輪對話,問題還未解決時,你是否萌生退意??
- 期望過高:通過一次性的需求描述,AI就能自動生成完整的代碼或解決問題。
- 實際拉垮:AI生成的代碼,或用AI解決問題時,困難重重,始終無法達到預期。
- 捫心自問:使用過程中,面對AI也解決不了的問題時,你有產生過,被勸退的感覺嗎?
七、WebSocket相關原理
WebSocket握手請求與數據請求的區別
一、握手請求(Handshake Request):
- 本質:握手請求是HTTP協議的升級請求,用于建立WebSocket連接
- 說明:服務器需響應HTTP 101狀態碼,確認協議升級,并返回Sec-WebSocket-Accept驗證字段
二、數據請求(Message Frame)
- 本質:WebSocket連接建立后,客戶端和服務端通過數據幀(Frame)進行雙向通信。
- 數據以幀格式傳輸,包含操作碼(Opcode)、掩碼(Mask)、載荷長度(Payload Length)等信息
- 常見幀類型:文本幀(Opcode=1)、二進制幀(Opcode=2)、控制幀(如Ping/Pong)。