| | |
| | | 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 / 获取索引连接时会同步调用控制面接口, |
| | | * 偶发握手失败(java.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(); |
| | | } |
| | | } |
| | | } |