From 873adcf1933989d46ffa199f24b59a8f5aa476d3 Mon Sep 17 00:00:00 2001
From: yuan <123@>
Date: 星期二, 22 九月 2026 13:39:16 +0800
Subject: [PATCH] fix: 添加对 Pinecone 启动期连接的重试机制,处理瞬时网络异常

---
 src/main/java/com/ruoyi/ai/config/EmbeddingStoreConfig.java |  114 +++++++++++++++++++++++++++++++++++++++++++++++++-------
 1 files changed, 99 insertions(+), 15 deletions(-)

diff --git a/src/main/java/com/ruoyi/ai/config/EmbeddingStoreConfig.java b/src/main/java/com/ruoyi/ai/config/EmbeddingStoreConfig.java
index ca5156d..a31c540 100644
--- a/src/main/java/com/ruoyi/ai/config/EmbeddingStoreConfig.java
+++ b/src/main/java/com/ruoyi/ai/config/EmbeddingStoreConfig.java
@@ -5,32 +5,116 @@
 import dev.langchain4j.store.embedding.EmbeddingStore;
 import dev.langchain4j.store.embedding.pinecone.PineconeEmbeddingStore;
 import dev.langchain4j.store.embedding.pinecone.PineconeServerlessIndexConfig;
-import org.springframework.beans.factory.annotation.Autowired;
+import io.pinecone.clients.Index;
+import io.pinecone.clients.Pinecone;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 
+import java.io.IOException;
+import java.util.function.Supplier;
+
 /**
- * @author :yys
- * @date : 2025/5/2 21:07
+ * 鍚戦噺瀛樺偍閰嶇疆
  */
+@Slf4j
 @Configuration
 public class EmbeddingStoreConfig {
 
-    @Autowired
-    private EmbeddingModel embeddingModel;
+    /** Pinecone 杩炴帴閲嶈瘯娆℃暟锛氭瀯閫犳湡浼氬悓姝ュ彂璧� HTTP/2 鎻℃墜锛屽伓鍙� "Required SETTINGS preface not received" */
+    private static final int CONNECT_ATTEMPTS = 3;
+
+    /** 閲嶈瘯闂撮殧鍩烘暟锛堟绉掞級锛屾寜娆℃暟绾挎�ч��閬� */
+    private static final long RETRY_BACKOFF_MILLIS = 1500L;
+
+    @Value("${pinecone.api-key:pcsk_4SJLnh_tNB3wSLJU8tc4E5P28PcXX8eCLdURqZpVhg1FMV8CRYxjneWdzqRdB5Ftqooi9}")
+    private String pineconeApiKey;
+
+    @Value("${pinecone.index:xiaozhi-index}")
+    private String indexName;
+
+    @Value("${pinecone.namespace:knowledge-base}")
+    private String namespace;
 
     @Bean
-    public EmbeddingStore<TextSegment> embeddingStore() {
-        //鍒涘缓鍚戦噺瀛樺偍
-        return PineconeEmbeddingStore.builder()
-                .apiKey("pcsk_4SJLnh_tNB3wSLJU8tc4E5P28PcXX8eCLdURqZpVhg1FMV8CRYxjneWdzqRdB5Ftqooi9")
-                .index("xiaozhi-index")//濡傛灉鎸囧畾鐨勭储寮曚笉瀛樺湪锛屽皢鍒涘缓涓�涓柊鐨勭储寮�
-                .nameSpace("xiaozhi-namespace") //濡傛灉鎸囧畾鐨勫悕绉扮┖闂翠笉瀛樺湪锛屽皢鍒涘缓涓�涓柊鐨勫悕绉� 绌洪棿
+    public Pinecone pinecone() {
+        return new Pinecone.Builder(pineconeApiKey).build();
+    }
+
+    @Bean
+    public Index pineconeIndex(Pinecone pinecone) {
+        return retry("鑾峰彇 Pinecone 绱㈠紩杩炴帴", () -> pinecone.getIndexConnection(indexName));
+    }
+
+    @Bean
+    public EmbeddingStore<TextSegment> embeddingStore(EmbeddingModel embeddingModel) {
+        return retry("鍒濆鍖� Pinecone 鍚戦噺瀛樺偍", () -> PineconeEmbeddingStore.builder()
+                .apiKey(pineconeApiKey)
+                .index(indexName)
+                .nameSpace(namespace)
                 .createIndex(PineconeServerlessIndexConfig.builder()
-                        .cloud("AWS") //鎸囧畾绱㈠紩閮ㄧ讲鍦� AWS 浜戞湇鍔′笂銆�
-                        .region("us-east-1") //鎸囧畾绱㈠紩鎵�鍦ㄧ殑 AWS 鍖哄煙涓� us-east-1銆�
-                        .dimension(embeddingModel.dimension()) //鎸囧畾绱㈠紩鐨勫悜閲忕淮搴︼紝璇ョ淮搴︿笌 embeddedModel 鐢熸垚鐨勫悜閲忕淮搴︾浉鍚屻��
+                        .cloud("AWS")
+                        .region("us-east-1")
+                        .dimension(embeddingModel.dimension())
                         .build())
-                .build();
+                .build());
+    }
+
+    /**
+     * 瀵� Pinecone 鍚姩鏈熻繛鎺ュ仛鏈夐檺娆¢噸璇曘��
+     * <p>
+     * Pinecone 瀹㈡埛绔浐瀹氳蛋 HTTP/2锛屾瀯閫� EmbeddingStore / 鑾峰彇绱㈠紩杩炴帴鏃朵細鍚屾璋冪敤鎺у埗闈㈡帴鍙o紝
+     * 鍋跺彂鎻℃墜澶辫触锛坖ava.io.IOException: Required SETTINGS preface not received锛変細浠ヨ繍琛屾椂寮傚父鎶涘嚭锛�
+     * 鐩存帴瀵艰嚧鏁翠釜 ApplicationContext 鍔犺浇澶辫触銆傝繖閲屼粎瀵硅繖绫荤灛鏃剁綉缁滃紓甯搁噸璇曪紝鍏跺畠寮傚父绔嬪嵆鎶涘嚭銆�
+     */
+    private <T> T retry(String action, Supplier<T> supplier) {
+        RuntimeException last = null;
+        for (int attempt = 1; attempt <= CONNECT_ATTEMPTS; attempt++) {
+            try {
+                return supplier.get();
+            } catch (RuntimeException e) {
+                if (!isTransientConnectFailure(e) || attempt == CONNECT_ATTEMPTS) {
+                    throw e;
+                }
+                last = e;
+                log.warn("{}澶辫触锛堢 {}/{} 娆★級锛寋}ms 鍚庨噸璇曪細{}",
+                        action, attempt, CONNECT_ATTEMPTS, RETRY_BACKOFF_MILLIS * attempt, rootMessage(e));
+                sleep(RETRY_BACKOFF_MILLIS * attempt);
+            }
+        }
+        throw last;
+    }
+
+    /** 娌� cause 閾惧垽鏂槸鍚︿负 HTTP/2 鎻℃墜绫荤殑鐬椂杩炴帴澶辫触 */
+    private boolean isTransientConnectFailure(Throwable e) {
+        for (Throwable t = e; t != null; t = t.getCause()) {
+            if (t instanceof IOException) {
+                return true;
+            }
+            String message = t.getMessage();
+            if (message != null && (message.contains("SETTINGS preface")
+                    || message.contains("Connection reset")
+                    || message.contains("timed out"))) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private String rootMessage(Throwable e) {
+        Throwable root = e;
+        while (root.getCause() != null && root.getCause() != root) {
+            root = root.getCause();
+        }
+        return root.getMessage();
+    }
+
+    private void sleep(long millis) {
+        try {
+            Thread.sleep(millis);
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+        }
     }
 }

--
Gitblit v1.9.3