发布攻略存放及ai向量搜索流程梳理
用向量的模式,实现ai问答
第一步需要先在es库里增加一列向量,所以我们先去es里改es库,
//建库 建立映射
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")))
.properties("embedding", p -> p.denseVector(
d -> d.dims(dimensions)
.index(true)
.similarity(DenseVectorSimilarity.Cosine))
))
);
库里存的是id,title,subtitle,summary四个字段的数据转换为的向量
//查询mysql
List<Strategy> list = remoteStrategyService.list("inner").getData();
for (Strategy strategy : list) {
StrategyES strategyES = new StrategyES();
//同名属性拷贝,strategyES其实相当于strategy的子集了,只是比他少俩属性
BeanUtils.copyProperties(strategy, strategyES);
//向量化 转换之后,es库里就有向量embedding这个字段
StringJoiner joiner = new StringJoiner(" ");
joiner.add(strategyES.getTitle());
joiner.add(strategyES.getSubTitle());
joiner.add(strategyES.getSummary());
List<Float> embeddingResp = embeddingService.getEmbedding(joiner.toString());
strategyES.setEmbedding(embeddingResp);
//同步mysql数据到es
client.index(i -> i.index(INDEX_NAME).id(strategyES.getId().toString()).document(strategyES));
}
接下来,到ai流程里,首先先把用户请求向量化
//把用户问题向量化
List<Float> embedding = embeddingService.getEmbedding(request.getQuestion());
依旧是拼接请求体(要把前端传回来的请求数据叶转换成向量),请求头,把请求转换成向量,组装去es里找相似的,
//使用向量方式查询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> strategyES : hits2) {
strategies.add(strategyES.source());
}
//发送请求到deepseek处理
String answer = askAI(request.getQuestion(), strategies);
ChatResponse chatResponse = new ChatResponse(answer, strategies);
return chatResponse;
实现ai的向量搜索
private String askAI(String question, List<StrategyES> strategies) {
StringJoiner stringJoiner = new StringJoiner("\n\n");
for (StrategyES strategy : strategies) {
String jsonString = JSON.toJSONString(strategy);
stringJoiner.add(jsonString);
}
String strategiesStr = stringJoiner.toString();
//私库数据
OpenAIMessage ragMessage = new OpenAIMessage("system", strategiesStr);
//系统消息
OpenAIMessage sysMessage = new OpenAIMessage("system", "你是一名资深导游,请结合用户问题和私库数据,并根据互联网信息给出旅游建议。规划处路线图,必坑指南及规划条件。回答小于200字");
//用户消息
OpenAIMessage userMessage = new OpenAIMessage("user", question);
List<OpenAIMessage> messages = new ArrayList<>();
messages.add(ragMessage);
messages.add(sysMessage);
messages.add(userMessage);
OpenAIChatRequest requestBody = new OpenAIChatRequest();
requestBody.setMessages(messages);
requestBody.setModel(model);
requestBody.setTemperature(temperature);
Map<String, Object> map = new HashMap<>();
map.put("type", thinking);
requestBody.setThinking(map);
requestBody.setTop_p(top_p);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
List<MediaType> mediaTypeList = new ArrayList<>();
mediaTypeList.add(MediaType.APPLICATION_JSON);
headers.setAccept(mediaTypeList);
headers.setBearerAuth(apiKey);
//等于header和body
HttpEntity<OpenAIChatRequest> httpEntity = new HttpEntity<>(requestBody, headers);
String url = baseUrl + "/chat/completions";
OpenAIChatResponse response = restTemplate.postForObject(url, httpEntity, OpenAIChatResponse.class);
if (response == null || response.getChoices() == null ||
response.getChoices().isEmpty()){
return "AI没有返回合适的结果";
}
OpenAIChatResponse.Choice choice = response.getChoices().get(0);
if(choice==null||choice.getMessage()==null||choice.getMessage().getContent()==null){
return "AI没有返回合适的结果";
}
return choice.getMessage().getContent();
}
先做了一个es库初始化,从mysql中查数据,把查到的数据转换成向量数据,一起存到es库中,在这里设定按照余弦方式查找.
发布攻略时,canal一直在监控,生成的消息,告诉redis,放到redis里,然后告诉rabbitmq这个中间件,rabbitmq收到消息会直接消费,拿到canal的message数据,转成向量数据,和message一起存到es库里,当es存完数据,在执行把数据从redis里删掉的操作.同时,这里有一个兜底机制(rabbitmq的超时检查),是一个定时任务的周期性运行.根据定时任务调用strategyserverimpl里的方法检查有没有超时,如果超时定时任务会通过远程调用让rabbitmq重发消息.
ai模块,先把用户消息转换成向量化数据,查出es库数据,拼接请求体请求头,比对,找出最相近的向量数据返回给deepseek,deepseek处理完之后,带着返回值返回给前端,梳理流程是否正确,哪里有偏差
┌─────────────────────────────────────────────────────────────────┐
│ 阶段1:初始化(手动触发,一次性) │
│ MySQL查数据 → 转向量 → 存ES → mapping设定余弦查找 │
└─────────────────────────────────────────────────────────────────┘
│
▼ 发布攻略
┌─────────────────────────────────────────────────────────────────┐
│ 阶段2:实时同步 + 兜底机制 │
│ │
│ ① Canal监控MySQL binlog → 构造JSON message │
│ ② 写Redis ZSet(暂存) → 发RabbitMQ │
│ │
│ ┌─ 正常路径(立即)────────────────────────────────┐ │
│ │ ③ RabbitMQ直接推送 → Listener.receive() │ │
│ │ ④ JSON→Strategy→BeanUtils.copy→StrategyES │ │
│ │ ⑤ getEmbedding(title+subTitle+summary) │ │
│ │ ⑥ setEmbedding → client.index 写ES │ │
│ │ ⑦ 删Redis ZSet对应消息 │ │
│ └────────────────────────────────────────────────┘ │
│ │
│ ┌─ 兜底路径(周期并行)──────────────────────────────┐ │
│ │ A. 定时任务按cron周期触发 │ │
│ │ B. Feign远程调StrategyServiceImpl │ │
│ │ C. 检查Redis ZSet里score<now-60000的消息 │ │
│ │ D. 超时则补发给RabbitMQ重发 │ │
│ │ E. 没超时则跳过 │ │
│ └────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
│
▼ 用户提问
┌─────────────────────────────────────────────────────────────────┐
│ 阶段3:AI向量检索问答 │
│ ① getEmbedding(question) 用户问题向量化 │
│ ② client.search(knn) 余弦比对,找最相近向量 │
│ ③ 取hits → List
│ ④ 拼OpenAIChatRequest + HttpHeaders │
│ ⑤ restTemplate → deepseek API │
│ ⑥ choices[0].message.content → answer │
│ ⑦ ChatResponse(answer, strategies) → 前端 │
└─────────────────────────────────────────────────────────────────┘
超时兜底机制
Canal → Redis + RabbitMQ
│
┌─────────────────┴─────────────────┐
│ │
【正常路径】 【兜底路径】
│ │
RabbitMQ立即推送 定时任务周期跑(独立)
│ │
TravelRabbitListener收到 扫Redis ZSet
│ │
立即处理(不用等超时) 找score<now-60s的消息
│ │
写ES → 删Redis 补发RabbitMQ
│ │
正常完成 消息卡住才介入

浙公网安备 33010602011771号