向量检索+Skywalking+Nacos配置中心+Sentinel持久化(8.25)

一、向量检索

​ 我们之前用的是 multi_match + ik_max_word 关键词检索,本质是按字面搜,关键词没命中就搜不到;现在替换成向量检索,本质是按意思搜,通过 Embedding 模型将文本转为向量,用余弦相似度匹配语义最接近的文档,即使用户输入和文章内容字面不同,只要语义相关就能召回。

1、添加配置

在travel-modules-search模块的application.yml下添加向量的配置。

# Embedding 模型配置
embedding:
  # 模型名称:使用阿里云的 qwen3.7-text-embedding 模型
  model: qwen3.7-text-embedding
  
  # 模型调用地址:阿里云 DashScope 兼容 OpenAI 格式的 API 端点
  url: https://dashscope.aliyuncs.com/compatible-mode/v1/embeddings
  
  # API 密钥:替换为你在阿里云 DashScope 控制台申请的真实 Key
  api-Key: 你的Apikey
  
  # 向量维度:该模型输出 768 维向量,ES 索引的 dims 配置必须与此一致
  dimensions: 768

2、添加请求、应答和处理向量化的类

​ 在travel-modules-search模块的business包下,新建embedding包,包含EmbeddingReq、EmbeddingResp、EmbeddingService。

(1)EmbeddingReq请求类

@Data
@AllArgsConstructor
@NoArgsConstructor
public class EmbeddingReq {
    // 模型名称
    private String model;
    // 要向量化的文本
    private String input;
    // 向量维度
    private Integer dimensions;
}

(2)EmbeddingResp应答类

@Data                    
@AllArgsConstructor      
@NoArgsConstructor       
public class EmbeddingResp {

    // 响应数据列表,通常只取第一个元素 data.get(0)
    // 接口返回多条是因为支持批量输入(一次传多段文本,返回多个向量)
    private List<EmbeddingData> data;


    /**
     * 单条 Embedding 结果
     * 包含模型输出的向量数组,如 [0.12, -0.34, 0.56, ...](768维)
     */
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public static class EmbeddingData {
        // 模型输出的浮点数向量,维度与配置中的 dimensions 一致(768维)
        private List<Float> embedding;
    }
}

(3)EmbeddingService类

@Service
public class EmbeddingService {

    // 从配置文件中读取 Embedding API 的调用地址
    @Value("${embedding.url}")
    private String embeddingUrl;

    // 从配置文件中读取阿里云 DashScope 的 API 密钥
    @Value("${embedding.api-key}")
    private String apiKey;

    // 从配置文件中读取使用的 Embedding 模型名称(如 qwen3.7-text-embedding)
    @Value("${embedding.model}")
    private String model;

    // 从配置文件中读取向量维度(768),用于指定模型输出向量的长度
    @Value("${embedding.dimensions}")
    private Integer dimensions;

    // 注入 RestTemplate,用于发送 HTTP POST 请求调用远程 Embedding API
    @Autowired
    private RestTemplate restTemplate;

    /**
     * 将输入文本通过 Embedding 模型转换为向量
     *
     * @param text 待向量化的文本(如用户问题或攻略文章内容)
     * @return 768维的浮点数向量,可直接存入 ES 的 dense_vector 字段
     */
    public List<Float> getEmbedding(String text) {

        // ========== 1. 组装请求头 ==========
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);  // 声明请求体格式为 JSON
        headers.setAccept(Stream.of(MediaType.APPLICATION_JSON).collect(Collectors.toList())); // 声明期望响应格式为 JSON
        headers.setBearerAuth(apiKey);  // 设置 Bearer Token 鉴权,使用阿里云 API Key

        // ========== 2. 组装请求体 ==========
        EmbeddingReq req = new EmbeddingReq();
        req.setDimensions(dimensions);  // 指定输出向量维度为 768
        req.setInput(text);             // 设置待向量化的输入文本
        req.setModel(model);            // 指定使用的 Embedding 模型

        // ========== 3. 封装 HTTP 请求实体(请求体 + 请求头) ==========
        HttpEntity<EmbeddingReq> entity = new HttpEntity<>(req, headers);

        // ========== 4. 发送 POST 请求,调用远程 Embedding API,并将响应自动反序列化为 EmbeddingResp ==========
        EmbeddingResp resp = restTemplate.postForObject(embeddingUrl, entity, EmbeddingResp.class);

        // ========== 5. 校验响应结果,防止空指针异常 ==========
        // 校验整体响应是否为空(接口调用失败、网络异常等情况)
        if(resp == null || resp.getData() == null || resp.getData().isEmpty()) {
            throw new ServiceException("获取嵌入向量失败");
        }

        List<EmbeddingResp.EmbeddingData> data = resp.getData();
        // 校验第一条数据及其向量是否为空(模型未正常返回结果)
        if(data.get(0) == null || data.get(0).getEmbedding() == null) {
            throw new ServiceException("向量化失败");
        }

        // ========== 6. 返回第一条向量结果 ==========
        // 因为每次只传入一段文本,所以取 data 列表的第一个元素即可
        return data.get(0).getEmbedding();
    }
}

3、修改攻略数据初始化到Es库的initEs()方法

​ 在StrategyEsServiceImpl中修改:

​ ①映射中新增 embedding 向量字段

​ ②调用 Embedding 模型生成向量,再将文本和向量一起写入 ES

@Override
    public void initEs() throws IOException {
        // 删库
        BooleanResponse exists = client.indices().exists(e -> e.index(INDEX_NAME));
        if(exists.value()){
            client.indices().delete(e -> e.index(INDEX_NAME));
        }
        // 建库、建立映射
        CreateIndexRequest createReq = CreateIndexRequest.of(r -> r.index(INDEX_NAME)
                .settings(s -> s.numberOfShards("1").numberOfReplicas("1"))
                .mappings(m -> m.properties("id", p -> p.long_(l -> l))
                        .properties("title", p -> p.text(t -> t.analyzer("ik_max_word")))
                        .properties("subTitle", p -> p.text(t -> t.analyzer("ik_max_word")))
                        .properties("summary", p -> p.text(t -> t.analyzer("ik_max_word")))
                          // 定义一个名为 "embedding" 的字段,类型为 dense_vector(密集向量)
                      	.properties("embedding", p -> p.denseVector(    
   		   d -> d.dims(dimensions)  // 指定向量维度(如 768),必须与 Embedding 模型输出维度严格一致
          .index(true)       // 启用向量索引(底层使用 HNSW 算法),开启后才能进行近似最近邻(kNN)检索
          .similarity(DenseVectorSimilarity.Cosine) // 指定相似度计算方式为余弦相似度,值越接近 1 表示语义越相似
			))))));
        client.indices().create(createReq);
        System.out.println("建库成功");
        // 查询mysql
        // 同步mysql数据到es库
        List<Strategy> list = remoteStrategyService.list("inner").getData();
        for (Strategy strategy : list) {
            StrategyEs es = new StrategyEs();
            // 同名属性拷贝
            BeanUtils.copyProperties(strategy, es);
            // ========== 向量化处理 ==========
            // 使用 StringJoiner 将多个文本字段用空格拼接成一段完整文本
            // 拼接后的文本将作为 Embedding 模型的输入,生成能代表这篇攻略整体语义的向量
            StringJoiner joiner = new StringJoiner(" ");
            joiner.add(strategy.getTitle())       // 拼接标题
                  .add(es.getSubTitle())          // 拼接副标题
                  .add(es.getSummary());          // 拼接摘要

            // 调用 Embedding 服务,将拼接后的文本送入模型,返回 768 维浮点数向量
            List<Float> embedding = embeddingService.getEmbedding(joiner.toString());

            // 将生成的向量设置到 ES 文档对象的 embedding 字段中
            es.setEmbedding(embedding);

            // ========== 写入 ES 索引 ==========
            // 将完整的 StrategyEs 文档(含文本字段 + 向量字段)写入 ES
            // index:指定目标索引名称
            // id:使用攻略主键作为 ES 文档的 _id,保证幂等性(重复执行不会产生重复数据)
            // document:要写入的文档对象
            client.index(i -> i.index(INDEX_NAME)
                               .id(es.getId().toString())
                               .document(es));

        }
    }

​ 在StrategyEs中添加embedding字段。

@Data
@AllArgsConstructor
@NoArgsConstructor
public class StrategyEs {

    private Long id;
    private String title;
    private String subTitle;
    private String summary;
    private List<Float> embedding;
}

4、修改 AIController 类,将用户输入的问题也通过 Embedding 模型转为向量,与 ES 中已存储的攻略向量进行语义匹配检索。

(1)在 travel-modules-ai 模块下添加 embedding 配置

embedding:
  model: qwen3.7-text-embedding
  url: https://dashscope.aliyuncs.com/compatible-mode/v1/embeddings
  api-Key: 你的Apikey
  dimensions: 768

(2)拷贝 embedding 包

(3)修改 getAnswer 方法实现向量化 kNN 查询。

  @RequestMapping("/chat")
    public Object getAnswer(@RequestBody ChatRequest request) throws IOException {

//        // 精准匹配
//        SearchResponse<StrategyEs> resp = client.search(sh -> sh.index("strategy")
//                .from(0).size(request.getTopK())
//                .query(q -> q.multiMatch(m -> m.query(request.getQuestion())
//                        .fields("title", "subtitle", "summary")
//                        .analyzer("ik_max_word"))), StrategyEs.class);

        // 将用户的问题向量化
        List<Float> embedding = embeddingService.getEmbedding(request.getQuestion());
        // 使用向量方式查询es
        SearchResponse<StrategyEs> resp = client.search(sh -> sh.index("strategy")
                .knn(k -> k.field("embedding")
                        .queryVector(embedding)
                        .k(request.getTopK())), StrategyEs.class);

        HitsMetadata<StrategyEs> hits = resp.hits();
        TotalHits total = hits.total();
        List<Hit<StrategyEs>> hits2 = hits.hits();
        // 私库数据
        List<StrategyEs> strategies = new ArrayList<>();
        for (Hit<StrategyEs> strategyEsHit : hits2) {
            StrategyEs es = strategyEsHit.source();
            strategies.add(es);
        }

        // 发送请求到deepseek处理
        String answer = askAI(request.getQuestion(), strategies);

        ChatResponse chatResponse = new ChatResponse(answer, strategies);
        return chatResponse;
    }

二、Skywalking集成

1、Skywalking简介

(1)Skywalking 和 Sleuth + Zipkin 对比

  • 功能一致:SkyWalking 和 Sleuth + Zipkin 都是做链路追踪的。

  • 维护方不同:Sleuth 已停止维护,SkyWalking 目前已由 Apache 软件基金会维护。

  • 侵入性不同:Sleuth + Zipkin 有侵入,需要在代码中写日志来采集数据;SkyWalking 无侵入,通过 Agent 程序直接监控,不需要改业务代码。

(2)SkyWalking 特点

  • 多语言支持:提供自动探针,支持 Java、.NET Core、Node.js 等多种语言。
  • 多监控手段:同时支持语言探针和 Service Mesh(服务网格)两种监控方式。
  • 轻量高效:架构轻量,不需要额外搭建复杂的大数据平台即可运行。
  • 模块化架构:UI 界面和存储后端(如集群管理)均支持多种机制可选,灵活度高。
  • 功能完善:内置支持告警功能,并具备优秀的可视化展示效果。

2、Skywalking集成

​ (1)双击bin下的startup.bat。

​ (2)浏览器访问localhost:8080

​ (3)在 IDEA 中配置 Spring Boot 启动项(VM options)。

​ 点击编辑配置,找到对应的Spring Boot ,alt+v,把文件拷贝进去。

  • 指定 Agent 路径

  • 设置服务名称

  • 指定后端服务地址

-javaagent:D:\ideaWorkSpace\work73\tools\tools\skywalking\apache-skywalking-apm-8.9.1\agent\skywalking-agent.jar
-Dskywalking.agent.service_name=travel-XXX
-Dskywalking.collector.backend_service=localhost:11800

三、Nacos配置中心

​ 之前项目没有使用配置中心,所有配置都写在本地 application.yml 中。现在需要将这些配置迁移到 Nacos 配置中心统一管理。

以 travel-modules-strategy 模块为例:

1、将本地配置迁移到 Nacos

​ 将 application.yml 中的内容拷贝到 Nacos 中已有的 travel-strategy-dev.yml 文件里,然后将本地 application.yml 中的配置注释掉。

server:
  port: 8089
spring:
  application:
    name: travel-strategy
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/wolf2w-cloud?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
    username: root
    password: 1234
  redis:
    password: 123456
  rabbitmq:
    addresses: 192.168.222.129:5672,192.168.222.130:5672
    username: root
    password: root
    virtual-host: /

mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

注意:本次项目各模块共用一个 MySQL 数据库,正常项目中每个服务应拥有独立的数据库,不能通过 Mapper 直接访问其他服务的数据。

2、新建bootstrap.yml

​ Nacos 配置中心的加载优先于 application.yml,因此需要通过 bootstrap.yml 来指定 Nacos 配置中心地址及当前环境。

spring:
  application:
    name: travel-strategy
  cloud:
    nacos:
      config:
        server-addr: localhost:8848
        file-extension: yml
  profiles:
    active: dev

3、优化配置——抽取共享配置

​ 观察 Nacos 中的配置可以发现,rabbitmq 和 mybatis-plus 的配置在多个服务中是相同的,可以抽取为共享配置,避免重复维护。

(1)在 Nacos 中新建两个共享配置文件

  • rabbitmq-shared-config.yml
  • mybatis-plus-config.yml

(2)将对应配置分别抽取到共享配置文件中

travel-strategy-dev.yml(精简后):

server:
  port: 8089
spring:
  application:
    name: travel-strategy
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/wolf2w-cloud?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
    username: root
    password: 1234
  redis:
    password: 123456

rabbitmq-shared-config.yml:

spring:
  rabbitmq:
    addresses: 192.168.222.129:5672,192.168.222.130:5672
    username: root
    password: root
    virtual-host: /

mybatis-plus-config.yml:

mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

(3)在 bootstrap.yml 中引入共享配置

spring:
  application:
    name: travel-strategy
  cloud:
    nacos:
      config:
        server-addr: localhost:8848
        file-extension: yml
        shared-configs:
          - data-id: rabbitmq-shared-config.yml
            refresh: true
          - data-id: mybatis-plus-config.yml
            refresh: true
  profiles:
    active: dev

四、Sentinel持久化

​ Sentinel 的限流规则默认存储在内存中,服务重启后规则会丢失。因此需要将规则持久化到 Nacos 配置中心,实现规则的长期保存和动态生效。

在 sentinel-travel-gateway 中配置 Strategy 模块的限流规则:

 {
        "resource": "travel-strategy",
        "count": 300,
        "grade": 1,
        "limitApp": "default",
        "strategy": 0,
        "controlBehavior": 0
    }

各字段含义如下:

字段 含义 当前值说明
resource 资源名称,即限流规则作用的目标 travel-strategy,表示对 Strategy 模块进行限流
count 限流阈值 300,表示单机阈值上限为 300
grade 阈值类型 1 表示按 QPS(每秒请求数)限流;0 表示按并发线程数限流
limitApp 调用来源 default 表示不区分调用来源,所有请求共享该阈值
strategy 流控模式 0 直接模式(资源自身超阈值即限流);1 关联模式;2 链路模式
controlBehavior 流控效果 0 快速失败(超阈值直接拒绝);1 Warm Up 预热;2 排队等待

实际项目中,限流阈值需要根据服务承载能力和业务重要性来设定,一般遵循核心服务阈值高、非核心服务阈值低的原则。以下是本项目各模块的限流配置参考:

模块 限流阈值(QPS) 设定依据
travel-gateway(网关) 1000 网关是所有请求的统一入口,阈值最高,承担全局流量管控
travel-strategy(攻略服务) 300 核心业务模块,查询类接口为主,承载中等流量
travel-order(订单服务) 200 涉及写操作(下单),数据库压力较大,阈值适当降低
travel-user(用户服务) 500 用户信息查询频率高,但逻辑简单,可承受较高 QPS

参考:网关层作为统一入口设置最高阈值 1000,做全局兜底限流;各微服务根据自身业务特点设置不同阈值,比如订单服务涉及写操作,阈值设为 200;攻略服务以查询为主,阈值设为 300。限流模式统一采用直接模式 + 快速失败,超阈值直接拒绝请求,避免请求堆积导致服务雪崩。

posted on 2026-08-25 22:42  冬冬咚  阅读(22)  评论(0)    收藏  举报