本篇內容主要講前端請求(不包含)訪問后端服務接口,接口通過檢索知識庫,封裝提示詞,調用deepseek的,并返回給前端的全過程,非完整代碼,不可直接運行。
1.基于servlet封裝異步請求
為什么要進行異步分裝?因為前段需要流式輸出,以減少用戶長時間等待造成的不良體驗
集成HttpServlet 實現POST方法,get方式多倫對話有數據了限制
@WebServlet(urlPatterns = "/ds",asyncSupported = true // 啟用異步支持
)
public class DeepseekApi extends HttpServlet
2.設置跨域(如果沒有前后端分離可以忽略此步驟)
response.setHeader("Access-Control-Allow-Origin", "*");response.setHeader("Access-Control-Allow-Methods", "POST, GET, PUT, OPTIONS, DELETE");response.setHeader("Access-Control-Max-Age", "3600");response.setHeader("Access-Control-Allow-Headers", "x-requested-with, Content-Type");response.setHeader("Access-Control-Allow-Credentials", "true");// 設置SSE頭response.setContentType("text/event-stream");response.setCharacterEncoding("UTF-8");response.setHeader("Cache-Control", "no-cache");response.setHeader("Connection", "keep-alive");// 處理OPTIONS預檢請求if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {response.setStatus(HttpServletResponse.SC_OK);asyncContext.complete();return;}
3.獲取參數
我這里前段直接封裝好多輪對話參數和當前問題,當前問題為什么要分開,應為問題需要在知識庫做增強檢索,這樣好取參數
// 獲取問題參數String question = request.getParameter("question");String his = request.getParameter("his");
4.封裝異步任務
asyncContext
// 獲取異步上下文final AsyncContext asyncContext = request.startAsync();asyncContext.setTimeout(3*60*1000); // 超時 60 秒writer = response.getWriter();processRequest(asyncContext, writer, question,his);
5.設置異步事件監聽
asyncContext.addListener(new AsyncListener() {@Overridepublic void onComplete(AsyncEvent event) {// 確保資源釋放}@Overridepublic void onTimeout(AsyncEvent event) {writer.write("event:error\ndata:請求超時\n\n");writer.flush();asyncContext.complete();}@Overridepublic void onError(AsyncEvent event) {asyncContext.complete();}@Overridepublic void onStartAsync(AsyncEvent event) {}});
6.檢索向量庫
// 檢索向量庫List<Map<String, Object>> kl = KnowledgeBaseService.searchKnowledge(question, 40);
7.構建提示詞
private static String buildPrompt(String question, List<Map<String, Object>> knowledge) {System.out.println("提示詞封裝!");String txtString="";for (Map<String, Object> map : knowledge) {txtString+=map.get("title")+"\n"+map.get("text")+"\n";}return "問題:\n" + question + "\n\n參考知識:" +txtString+ "\n\n請以參考知識為主,給出簡明扼要的回復,如果參考知識與問題沒有相關性或不存在請拒絕答復:";}
8.構建請求體
// 新的對話內容JSONObject newMessage = new JSONObject();newMessage.put("role", "user");newMessage.put("content", prompt);// 插入新的對話messages.add(newMessage);System.out.println(messages.toJSONString());// 構建請求體Map<String, String> headers = new HashMap<>();headers.put("Authorization", "Bearer " + Consist.DEEPSEEK_API_KEY);headers.put("Content-Type", "application/json");JSONObject requestBody = new JSONObject();requestBody.put("model", Consist.MODEL_NAME);requestBody.put("messages", messages);requestBody.put("stream", true);requestBody.put("max_tokens", Consist.MAX_TOKENS);
9.進行異步調用
sendAsyncRequestWithCallback(Consist.DEEPSEEK_API_URL,headers,requestBody.toJSONString(),new StreamCallback() {@Overridepublic void onDataReceived(String content) {
// System.out.print(content);writer.write("data:" + content+"\n\n");writer.flush();}@Overridepublic void onComplete() {System.out.println("調用完成!");writer.write("event:done\ndata:\n\n");writer.flush();asyncContext.complete();}@Overridepublic void onError(Exception ex) {
// ex.printStackTrace();System.err.println("報錯!");writer.write("event:error\ndata:發生錯誤\n\n");writer.flush();asyncContext.complete();}});
10.監聽調用狀態,防止客戶端掉線造成的異常
if (asyncClient == null || !asyncClient.isRunning()) {synchronized (DeepseekR1WebApiPost.class) {if (asyncClient == null || !asyncClient.isRunning()) {try {if (asyncClient != null) {asyncClient.close();}asyncClient = HttpAsyncClients.createDefault();asyncClient.start();} catch (IOException e) {e.printStackTrace();}}}}
11.封裝客戶端參數,調用deepseek官網接口
HttpPost request = new HttpPost(new URI(url));request.setEntity(new StringEntity(requestBody, "UTF-8"));headers.forEach(request::setHeader);HttpHost target = new HttpHost(new URI(url).getHost(),new URI(url).getPort(),new URI(url).getScheme());
12.執行異步請求,并處理返回數據
@Overrideprotected void onContentReceived(ContentDecoder decoder, IOControl ioctrl) {try {ByteBuffer bb = ByteBuffer.allocate(Consist.MAX_TOKENS);int read;while ((read = decoder.read(bb)) > 0) {bb.flip();byte[] bytes = new byte[bb.remaining()];bb.get(bytes);buffer.write(bytes);processBuffer(callback);bb.clear();}} catch (Exception e) {e.printStackTrace();System.out.println("報錯: " + e.getMessage().toString());callback.onError(e);}}
13.解析流式數據返回給前端
private void processBuffer(StreamCallback callback) throws Exception {String chunk = buffer.toString("UTF-8");buffer.reset();// 按行分割并過濾空行String[] lines = chunk.split("\\r?\\n");for (String line : lines) {if (line.isEmpty()) continue;if (line.startsWith("data: ")) {String jsonStr = line.substring(6).trim();if ("[DONE]".equals(jsonStr)) {callback.onComplete();return; // 提前返回避免后續處理}try {if(isJsonComplete(jsonStr)) {JsonObject responseJson = JsonParser.parseString(jsonStr).getAsJsonObject();JsonArray choices = responseJson.getAsJsonArray("choices");callback.onDataReceived(choices.toString());};
// // callback.onDataReceived(jsonStr);} catch (Exception e) {callback.onError(new RuntimeException("解析 JSON 失敗: " + jsonStr, e));continue;}}}}