Skip to content

对接 Tavily Search ​

由于 Tavily Search API 能够返回更详细的内容,我们选择将 Tavily Search 作为数据问答的数据来源。下面文档介绍了如何使用 Tavily Search 获取搜索结果,并将结果用于后续的问答推理流程。本文档分为两个部分:

  1. MaxSearchSearchService:封装了对 Tavily Search(使用 SearxNG 客户端实现)的调用,并将搜索结果转换为 WebPageContent 对象列表。
  2. AiSearchService:在原有的搜索流程中集成 Tavily Search,获取搜索结果后进一步进行排序、过滤、提示词生成,并调用预测服务生成回答。

1. MaxSearchSearchService ​

该服务类主要用于调用 Tavily Search API 获取搜索结果。这里我们提供了两种调用方式:

  • useTavilySearch:直接使用 Tavily Search(接口实现使用 SearxNG 客户端),返回更详细的内容。
  • useSearchNg:使用 SearxNG 搜索参数进行调用,作为备用实现,两种方式代码类似。

下面是完整代码及详细说明:

java
package nexus.io.max.search.services;

import java.util.ArrayList;
import java.util.List;

import nexus.io.model.web.WebPageContent;
import nexus.io.searxng.SearxngResult;
import nexus.io.searxng.SearxngSearchClient;
import nexus.io.searxng.SearxngSearchParam;
import nexus.io.searxng.SearxngSearchResponse;
import nexus.io.tavily.TavilyClient;
import nexus.io.tavily.TavilySearchResponse;
import nexus.io.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 进行回答生成。

下面是完整代码及详细说明:

java
package nexus.io.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 nexus.io.db.activerecord.Db;
import nexus.io.jfinal.aop.Aop;
import nexus.io.kit.PgObjectUtils;
import nexus.io.max.search.consts.OptimizationMode;
import nexus.io.max.search.vo.ChatParamVo;
import nexus.io.max.search.vo.ChatWsReqMessageVo;
import nexus.io.max.search.vo.ChatWsRespVo;
import nexus.io.max.search.vo.WebPageSource;
import nexus.io.model.web.WebPageContent;
import nexus.io.template.PromptEngine;
import nexus.io.tio.core.ChannelContext;
import nexus.io.tio.core.Tio;
import nexus.io.tio.http.common.RequestHeaderKey;
import nexus.io.http.common.sse.SsePacket;
import nexus.io.tio.utils.hutool.StrUtil;
import nexus.io.tio.utils.json.FastJson2Utils;
import nexus.io.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 将结果引用返回给客户端,保证整个问答流程的连贯性与实时性。

MIT Licensed