对接 Tavily Search
由于 Tavily Search API 能够返回更详细的内容,我们选择将 Tavily Search 作为数据问答的数据来源。下面文档介绍了如何使用 Tavily Search 获取搜索结果,并将结果用于后续的问答推理流程。本文档分为两个部分:
- MaxSearchSearchService:封装了对 Tavily Search(使用 SearxNG 客户端实现)的调用,并将搜索结果转换为 WebPageContent 对象列表。
- AiSearchService:在原有的搜索流程中集成 Tavily Search,获取搜索结果后进一步进行排序、过滤、提示词生成,并调用预测服务生成回答。
1. MaxSearchSearchService
该服务类主要用于调用 Tavily Search API 获取搜索结果。这里我们提供了两种调用方式:
- useTavilySearch:直接使用 Tavily Search(接口实现使用 SearxNG 客户端),返回更详细的内容。
- useSearchNg:使用 SearxNG 搜索参数进行调用,作为备用实现,两种方式代码类似。
下面是完整代码及详细说明:
package com.litongjava.max.search.services;
import java.util.ArrayList;
import java.util.List;
import com.litongjava.model.web.WebPageContent;
import com.litongjava.searxng.SearxngResult;
import com.litongjava.searxng.SearxngSearchClient;
import com.litongjava.searxng.SearxngSearchParam;
import com.litongjava.searxng.SearxngSearchResponse;
import com.litongjava.tavily.TavilyClient;
import com.litongjava.tavily.TavilySearchResponse;
import com.litongjava.tavily.TavilySearchResult;
public class MaxSearchSearchService {
/**
* 对外暴露的搜索接口,使用 Tavily Search 进行搜索
* @param quesiton 用户输入的问题或查询内容
* @return 搜索到的网页内容列表
*/
public List<WebPageContent> search(String quesiton) {
return useTavilySearch(quesiton);
}
/**
* 使用 Tavily Search API 进行搜索,返回详细内容
* 该方法内部调用 SearxngSearchClient.search() 接口,
* 将返回的 SearxngResult 结果转换为 WebPageContent 对象
*
* @param quesiton 用户查询的关键词
* @return 包含标题、链接以及详细内容的网页内容列表
*/
public List<WebPageContent> useTavilySearch(String quesiton) {
TavilySearchResponse searchResponse = TavilyClient.search(quesiton);
List<TavilySearchResult> results = searchResponse.getResults();
List<WebPageContent> webPageContents = new ArrayList<>();
for (TavilySearchResult tavilySearchResult : results) {
String title = tavilySearchResult.getTitle();
String url = tavilySearchResult.getUrl();
String content = tavilySearchResult.getContent();
String raw_content = tavilySearchResult.getRaw_content();
// 构造 WebPageContent 对象,并设置详细内容
WebPageContent webpageContent = new WebPageContent(title, url, content, raw_content);
webPageContents.add(webpageContent);
}
return webPageContents;
}
/**
* 使用 SearxNG 搜索参数方式进行搜索
* 可作为备用实现,此方法先设置搜索格式和查询关键词,再调用搜索接口
*
* @param quesiton 用户查询的关键词
* @return 搜索到的网页内容列表
*/
public List<WebPageContent> useSearchNg(String quesiton) {
SearxngSearchParam searxngSearchParam = new SearxngSearchParam();
searxngSearchParam.setFormat("json");
searxngSearchParam.setQ(quesiton);
SearxngSearchResponse searchResponse = SearxngSearchClient.search(searxngSearchParam);
List<SearxngResult> results = searchResponse.getResults();
List<WebPageContent> webPageContents = new ArrayList<>();
for (SearxngResult searxngResult : results) {
String title = searxngResult.getTitle();
String url = searxngResult.getUrl();
WebPageContent webpageContent = new WebPageContent(title, url);
webpageContent.setContent(searxngResult.getContent());
webPageContents.add(webpageContent);
}
return webPageContents;
}
}
说明:
- 方法
search(String quesiton)
对外提供统一接口,内部调用useTavilySearch
实现。 useTavilySearch
方法直接调用 SearxngSearchClient 接口获得搜索响应,然后遍历结果,提取标题、URL 以及内容,并封装成WebPageContent
对象。useSearchNg
方法展示了另一种调用方式,先设置搜索参数再调用接口,效果与前者类似。
2. AiSearchService
在原有的搜索处理流程中,我们将搜索结果数据来源切换为 Tavily Search。该服务类的主要职责如下:
- 获取用户查询、判断是否启用了 copilot(即是否需要自动调用搜索服务)。
- 根据用户输入和历史记录决定是否进行问题重写,调用相应服务获取重写结果。
- 使用
MaxSearchSearchService
进行搜索,获得搜索结果列表(List<WebPageContent>
)。 - 根据优化模式(balanced 或 quality)对搜索结果进行排序、过滤或补充(调用 VectorRankerService、JinaReaderService 或 AiRankerService)。
- 将搜索结果保存到数据库,并通过 WebSocket 返回给客户端引用信息。
- 利用模板引擎生成提示词(inputPrompt),并最终调用 GeminiPredictService 进行回答生成。
下面是完整代码及详细说明:
package com.litongjava.max.search.services;
import java.time.Instant;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import org.postgresql.util.PGobject;
import com.jfinal.kit.Kv;
import com.litongjava.db.activerecord.Db;
import com.litongjava.jfinal.aop.Aop;
import com.litongjava.kit.PgObjectUtils;
import com.litongjava.max.search.consts.OptimizationMode;
import com.litongjava.max.search.vo.ChatParamVo;
import com.litongjava.max.search.vo.ChatWsReqMessageVo;
import com.litongjava.max.search.vo.ChatWsRespVo;
import com.litongjava.max.search.vo.WebPageSource;
import com.litongjava.model.web.WebPageContent;
import com.litongjava.template.PromptEngine;
import com.litongjava.tio.core.ChannelContext;
import com.litongjava.tio.core.Tio;
import com.litongjava.tio.http.common.RequestHeaderKey;
import com.litongjava.tio.http.common.sse.SsePacket;
import com.litongjava.tio.utils.hutool.StrUtil;
import com.litongjava.tio.utils.json.FastJson2Utils;
import com.litongjava.tio.websocket.common.WebSocketResponse;
import lombok.extern.slf4j.Slf4j;
import okhttp3.Call;
@Slf4j
public class AiSearchService {
public PredictService predictService = Aop.get(PredictService.class);
private AiRankerService aiRankerService = Aop.get(AiRankerService.class);
private MaxSearchSearchService maxSearchSearchService = Aop.get(MaxSearchSearchService.class);
private VectorRankerService vectorRankerService = Aop.get(VectorRankerService.class);
public boolean spped = true;
/**
* 处理搜索请求
* 根据当前用户的设置(copilotEnabled、optimizationMode)决定是否进行搜索,
* 然后调用 Tavily Search(通过 MaxSearchSearchService)获取网页内容,
* 并进一步生成提示词供后续回答生成使用。
*
* @param channelContext 通道上下文,用于返回消息
* @param reqMessageVo 用户请求消息对象,包含消息内容、用户设置及历史记录
* @param chatParamVo 对话参数对象,保存问题重写结果、提示词、搜索结果等信息
* @return 返回用于流式处理回答的 Call 对象
*/
public Call search(ChannelContext channelContext, ChatWsReqMessageVo reqMessageVo, ChatParamVo chatParamVo) {
String optimizationMode = reqMessageVo.getOptimizationMode();
Boolean copilotEnabled = reqMessageVo.getCopilotEnabled();
String content = reqMessageVo.getMessage().getContent();
Long questionMessageId = reqMessageVo.getMessage().getMessageId();
long answerMessageId = chatParamVo.getAnswerMessageId();
String inputPrompt = null;
if (copilotEnabled != null && copilotEnabled) {
String quesiton = null;
// 如果有问题重写,则优先使用重写后的问题,否则直接使用原始内容
if (chatParamVo.getRewrited() != null) {
quesiton = chatParamVo.getRewrited();
} else {
quesiton = content;
}
// 使用 MaxSearchSearchService 对 Tavily Search API 进行调用
List<WebPageContent> webPageContents = maxSearchSearchService.search(quesiton);
// 根据优化模式对搜索结果进行处理
JinaReaderService jinaReaderService = Aop.get(JinaReaderService.class);
if (OptimizationMode.balanced.equals(optimizationMode)) {
List<WebPageContent> rankedWebPageContents = vectorRankerService.filter(webPageContents, quesiton, 1);
rankedWebPageContents = jinaReaderService.spider(webPageContents);
webPageContents.set(0, rankedWebPageContents.get(0));
} else if (OptimizationMode.quality.equals(optimizationMode)) {
// 质量模式下先过滤,再异步补全页面内容
webPageContents = aiRankerService.filter(webPageContents, quesiton, 6);
webPageContents = jinaReaderService.spiderAsync(webPageContents);
}
chatParamVo.setSources(webPageContents);
// 将搜索结果转换为 JSON 格式保存到数据库中(便于记录历史消息)
PGobject pgObject = PgObjectUtils.json(webPageContents);
Db.updateBySql("update max_search_chat_message set sources=? where id=?", pgObject, questionMessageId);
List<WebPageSource> sources = new ArrayList<>();
for (WebPageContent webPageConteont : webPageContents) {
sources.add(new WebPageSource(webPageConteont.getTitle(), webPageConteont.getUrl(), webPageConteont.getContent()));
}
String host = channelContext.getString(RequestHeaderKey.Host);
if (host == null) {
host = "//127.0.0.1";
} else {
host = "//" + host;
}
sources.add(new WebPageSource("All Sources", host + "/sources/" + questionMessageId));
// 返回 sources 数据给客户端
ChatWsRespVo<List<WebPageSource>> chatRespVo = new ChatWsRespVo<>();
chatRespVo.setType("sources").setData(sources).setMessageId(answerMessageId);
// 通过 WebSocket or sse 返回搜索结果引用信息给客户端
if (channelContext != null) {
byte[] jsonBytes = FastJson2Utils.toJSONBytes(chatRespVo);
if (reqMessageVo.isSse()) {
Tio.bSend(channelContext, new SsePacket(jsonBytes));
} else {
Tio.bSend(channelContext, new WebSocketResponse(jsonBytes));
}
}
// 拼接所有搜索结果内容,用于生成提示词
StringBuffer markdown = new StringBuffer();
for (int i = 0; i < webPageContents.size(); i++) {
WebPageContent webPageContent = webPageContents.get(i);
String sourceContent = webPageContent.getContent();
if (StrUtil.isBlank(sourceContent)) {
sourceContent = webPageContent.getDescription();
}
String sourceFormat = "source %d %s %s ";
markdown.append(String.format(sourceFormat, (i + 1), webPageContent.getUrl(), sourceContent));
}
// 使用模板引擎生成提示词,提示词中包含当前日期和搜索结果上下文
String isoTimeStr = DateTimeFormatter.ISO_INSTANT.format(Instant.now());
Kv kv = Kv.by("date", isoTimeStr).set("context", markdown);
inputPrompt = PromptEngine.renderToString("WebSearchResponsePrompt.txt", kv);
log.info("deepkseek:{}", inputPrompt);
}
chatParamVo.setSystemPrompt(inputPrompt);
return predictService.predict(channelContext, reqMessageVo, chatParamVo);
}
}
说明:
- 在
search
方法中,首先判断是否启用了 copilot 模式。如果启用,则:- 根据用户输入或重写后的问题作为查询内容。
- 使用
MaxSearchSearchService.search()
获取搜索结果。 - 根据不同的优化模式对搜索结果进行过滤、排序或内容补全。
- 将搜索结果保存到数据库,并构造返回给客户端的引用信息(通过 WebSocket)。
- 利用搜索结果内容和当前时间生成提示词(inputPrompt)。
- 最后,将生成的提示词设置到
chatParamVo
中,并调用 GeminiPredictService 进行回答生成。
总结
本文档详细介绍了如何将 Tavily Search API 对接到我们的问答系统中,作为数据来源获取更详细的网页内容。主要内容包括:
- MaxSearchSearchService:封装了调用 Tavily Search(基于 SearxNG 实现)的接口,将搜索结果转换为
WebPageContent
对象列表。提供了两种调用方式(useTavilySearch
和useSearchNg
),以便灵活选择。 - AiSearchService:在搜索流程中集成 Tavily Search 作为数据来源。通过调用
MaxSearchSearchService.search()
获取搜索结果,并对结果进行排序、过滤和提示词生成,最终调用 GeminiPredictService 进行后续回答生成。同时,该服务将搜索结果保存到数据库,并通过 WebSocket 将结果引用返回给客户端,保证整个问答流程的连贯性与实时性。