java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > Spring AI Playwright联网检索工具

Spring AI 2.0.0 + Playwright实现联网检索工具

作者:xixingzhe2

本文详细讲解如何利用SpringAI 2.0的ToolCallback机制和Playwright实现联网问答,你将学会自定义联网检索工具,抓取动态 网页内容,并集成到大模型中,轻松应对需要实时网络资讯的场景,需要的朋友可以参考下

一、整体方案说明

二、依赖引入(Maven)

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-model-ollama</artifactId>
    <version>${spring-ai.version}</version>
</dependency>
<dependency>
    <groupId>com.microsoft.playwright</groupId>
    <artifactId>playwright</artifactId>
    <version>${playwright.version}</version>
</dependency>

三、Spring AI 自定义 ToolCallback 联网检索工具

Spring AI 2.0 工具标准:实现 ToolCallback,搭配 @Tool 注解定义工具入参描述,大模型自动识别调用。

浏览器搜索服务统一接口

package com.ybw.tool.search;

import com.ybw.model.SearchResult;

import java.util.List;

/**
 * 浏览器搜索服务统一接口
 */
public interface BrowserSearchService {

    /**
     * 执行搜索并返回结果列表
     *
     * @param query 搜索关键词
     * @return 搜索结果列表
     */
    List<SearchResult> search(String query);
}

无头浏览器搜索工具(后台运行,不显示窗口)

package com.ybw.tool.search;

import com.microsoft.playwright.Browser;
import com.microsoft.playwright.Page;
import com.ybw.model.SearchResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.stereotype.Component;

import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;

/**
 * 无头浏览器搜索工具(后台运行,不显示窗口)
 */
@Slf4j
@Component("browserSearch")
public class BrowserSearchTool implements BrowserSearchService {

    private final Browser browser;

    public BrowserSearchTool(Browser browser) {
        this.browser = browser;
    }

    @Override
    @Tool(description = "使用浏览器进行网络搜索,获取最新信息")
    public List<SearchResult> search(String query) {
        log.info("开始浏览器搜索: {}", query);
        List<SearchResult> results = new ArrayList<>();

        try (Page page = browser.newPage()) {
            String encodedQuery = URLEncoder.encode(query, StandardCharsets.UTF_8);
            String searchUrl = "https://www.baidu.com/s?wd=" + encodedQuery;

            page.navigate(searchUrl);
            page.waitForLoadState();
            page.waitForTimeout(3000);

            List<String> titles = page.locator("h3").allTextContents();

            List<String> links = page.locator("h3 > a").all().stream()
                    .map(element -> element.getAttribute("href"))
                    .toList();

            List<String> summaries = page.locator("div.c-abstract").allTextContents();
            if (summaries.isEmpty()) {
                summaries = page.locator("div.result-op").allTextContents();
            }

            int count = Math.min(titles.size(), Math.min(links.size(), summaries.size()));
            for (int i = 0; i < count && i < 5; i++) {
                results.add(new SearchResult(
                        titles.get(i),
                        links.get(i),
                        summaries.get(i)
                ));
            }

            log.info("搜索完成,共找到 {} 条结果", results.size());
        } catch (Exception e) {
            log.error("浏览器搜索失败: {}", e.getMessage(), e);
        }

        return results;
    }
}

可见浏览器搜索工具(显示浏览器窗口,用户可观察搜索过程)

package com.ybw.tool.search;

import com.microsoft.playwright.Browser;
import com.microsoft.playwright.Page;
import com.ybw.model.SearchResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;

import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;

/**
 * 可见浏览器搜索工具(显示浏览器窗口,用户可观察搜索过程)
 */
@Slf4j
@Component("browserVisibleSearch")
public class BrowserVisibleSearchTool implements BrowserSearchService {

    private final Browser visibleBrowser;

    public BrowserVisibleSearchTool(@Qualifier("visibleBrowser") Browser visibleBrowser) {
        this.visibleBrowser = visibleBrowser;
    }

    @Override
    @Tool(description = "使用可见浏览器窗口进行网络搜索,用户可以看到搜索过程")
    public List<SearchResult> search(String query) {
        log.info("开始可见浏览器搜索: {}", query);
        List<SearchResult> results = new ArrayList<>();

        try (Page page = visibleBrowser.newPage()) {
            page.navigate("https://www.baidu.com");
            page.waitForLoadState();
            page.waitForTimeout(2000);

            try {
                page.evaluate("document.getElementById('kw').value = '" + escapeQuery(query) + "'");
                page.click("input#su", new Page.ClickOptions().setForce(true));
            } catch (Exception e) {
                log.warn("表单方式搜索失败,切换到URL直接导航: {}", e.getMessage());
                String encodedQuery = URLEncoder.encode(query, StandardCharsets.UTF_8);
                page.navigate("https://www.baidu.com/s?wd=" + encodedQuery);
            }

            page.waitForLoadState();
            page.waitForTimeout(3000);

            List<String> titles = page.locator("h3").allTextContents();

            List<String> links = page.locator("h3 > a").all().stream()
                    .map(element -> element.getAttribute("href"))
                    .toList();

            List<String> summaries = page.locator("div.c-abstract").allTextContents();
            if (summaries.isEmpty()) {
                summaries = page.locator("div.result-op").allTextContents();
            }

            int count = Math.min(titles.size(), Math.min(links.size(), summaries.size()));
            for (int i = 0; i < count && i < 5; i++) {
                results.add(new SearchResult(
                        titles.get(i),
                        links.get(i),
                        summaries.get(i)
                ));
            }

            page.waitForTimeout(2000);

            log.info("可见浏览器搜索完成,共找到 {} 条结果", results.size());
        } catch (Exception e) {
            log.error("可见浏览器搜索失败: {}", e.getMessage(), e);
        }

        return results;
    }

    private String escapeQuery(String query) {
        return query.replace("\\", "\\\\").replace("'", "\\'").replace("\"", "\\\"");
    }
}

浏览器搜索工具工厂

package com.ybw.tool.factory;

import com.ybw.tool.search.BrowserSearchService;
import com.ybw.tool.search.BrowserSearchTool;
import com.ybw.tool.search.BrowserVisibleSearchTool;
import org.springframework.stereotype.Component;

/**
 * 浏览器搜索工具工厂
 * <p>
 * 根据搜索模式(无头/可见)返回对应的 BrowserSearchService 实现。
 * 使用方式:
 * <pre>{@code
 *   BrowserSearchService service = factory.createSearchTool(BrowserSearchMode.HEADLESS);
 *   List<SearchResult> results = service.search("关键词");
 * }</pre>
 */
@Component
public class BrowserSearchToolFactory {

    private final BrowserSearchTool headlessTool;
    private final BrowserVisibleSearchTool visibleTool;

    public BrowserSearchToolFactory(BrowserSearchTool headlessTool,
                                    BrowserVisibleSearchTool visibleTool) {
        this.headlessTool = headlessTool;
        this.visibleTool = visibleTool;
    }

    /**
     * 根据搜索模式创建对应的搜索工具实例
     *
     * @param mode 搜索模式:HEADLESS(无头模式)或 VISIBLE(可见浏览器模式)
     * @return 对应的 BrowserSearchService 实现
     */
    public BrowserSearchService createSearchTool(BrowserSearchMode mode) {
        return switch (mode) {
            case HEADLESS -> headlessTool;
            case VISIBLE -> visibleTool;
        };
    }

    /**
     * 浏览器搜索模式枚举
     */
    public enum BrowserSearchMode {
        /** 无头模式:后台运行,不显示浏览器窗口 */
        HEADLESS,
        /** 可见模式:显示浏览器窗口,用户可观察搜索过程 */
        VISIBLE
    }
}

搜索结果数据模型

package com.ybw.model;

import lombok.AllArgsConstructor;
import lombok.Data;

/**
 * 搜索结果数据模型
 */
@Data
@AllArgsConstructor
public class SearchResult {
    private String title;
    private String url;
    private String summary;

    @Override
    public String toString() {
        return "标题: " + title + "\n链接: " + url + "\n摘要: " + summary;
    }
}

四、封装 Playwright 网页抓取工具类

Spring AI 2.0 工具标准:实现 ToolCallback,搭配 @Tool 注解定义工具入参描述,大模型自动识别调用。

package com.ybw.config;

import com.microsoft.playwright.Browser;
import com.microsoft.playwright.BrowserType;
import com.microsoft.playwright.Playwright;
import jakarta.annotation.PreDestroy;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Slf4j
@Configuration
public class BrowserSearchConfig {

    private Playwright playwright;

    @Value("${spring.ai.browser.timeout:30000}")
    private int timeout;

    /**
     * 初始化 Playwright
     *
     * @methodName: playwright
     * @return: com.microsoft.playwright.Playwright
     * @author: ybw
     * @date: 2026/7/26
     **/
    @Bean(destroyMethod = "")
    public Playwright playwright() {
        log.info("初始化 Playwright");
        this.playwright = Playwright.create();
        return playwright;
    }

    /**
     * 创建浏览器实例-无头模式
     *
     * @param playwright Playwright 实例
     * @methodName: browser
     * @return: com.microsoft.playwright.Browser
     * @author: ybw
     * @date: 2026/7/26
     **/
    @Bean
    public Browser browser(Playwright playwright) {
        log.info("创建浏览器实例(headless={})", true);
        return playwright.chromium().launch(new BrowserType.LaunchOptions()
                .setHeadless(true)
                .setTimeout(timeout)
        );
    }

    /**
     * 创建浏览器实例-可见模式
     *
     * @param playwright Playwright 实例
     * @methodName: visibleBrowser
     * @return: com.microsoft.playwright.Browser
     * @author: ybw
     * @date: 2026/7/26
     **/
    @Bean("visibleBrowser")
    public Browser visibleBrowser(Playwright playwright) {
        log.info("创建可见浏览器实例(headless=false)");
        return playwright.chromium().launch(new BrowserType.LaunchOptions()
                .setHeadless(false)
                .setTimeout(timeout)
        );
    }

    @PreDestroy
    public void destroy() {
        if (playwright != null) {
            log.info("关闭 Playwright");
            playwright.close();
        }
    }
}

五、Spring AI 客户端装配工具,实现联网问答

package com.ybw.config;

import com.ybw.tool.search.BrowserSearchTool;
import com.ybw.tool.search.BrowserVisibleSearchTool;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;

@Configuration
public class ChatClientConfig {

    @Bean
    @Primary
    ChatClient chatClient(ChatClient.Builder builder, BrowserSearchTool browserSearchTool) {
        return builder
                .defaultSystem("你是一个智能助手,可以调用浏览器搜索工具获取最新信息。" +
                        "当用户问的问题需要最新的信息或者你不确定答案时,请使用 search 工具进行搜索。" +
                        "工具返回的搜索结果包含标题、链接和摘要,请根据搜索结果进行回答。" +
                        "必须中文回复。")
                .defaultTools(browserSearchTool)
                .build();
    }

    @Bean("visibleChatClient")
    ChatClient visibleChatClient(ChatClient.Builder builder, BrowserVisibleSearchTool browserVisibleSearchTool) {
        return builder
                .defaultSystem("你是一个智能助手,可以调用可见浏览器搜索工具获取最新信息。" +
                        "当用户问的问题需要最新的信息或者你不确定答案时,请使用 search 工具进行搜索。" +
                        "工具返回的搜索结果包含标题、链接和摘要,请根据搜索结果进行回答。" +
                        "搜索过程会打开浏览器窗口显示。" +
                        "必须中文回复。")
                .defaultTools(browserVisibleSearchTool)
                .build();
    }
}

六、测试用例

可见浏览器搜索测试 - 显示浏览器窗口,可观察搜索过程

package com.ybw.agent.visible;

import com.ybw.tool.factory.BrowserSearchToolFactory;
import com.ybw.tool.factory.BrowserSearchToolFactory.BrowserSearchMode;
import com.ybw.model.SearchResult;
import com.ybw.tool.search.BrowserSearchService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.List;

/**
 * 可见浏览器搜索测试 - 显示浏览器窗口,可观察搜索过程
 * <p>
 * 通过工厂模式创建 {@link BrowserSearchMode#VISIBLE} 模式的搜索服务进行测试。
 */
@SpringBootTest
@Slf4j
@DisplayName("可见浏览器搜索测试")
public class VisibleBrowserSearchTest {

    @Resource(name = "visibleChatClient")
    private ChatClient visibleChatClient;

    @Resource
    private BrowserSearchToolFactory searchToolFactory;

    @Test
    @DisplayName("直接调用搜索工具 - 搜索 Spring AI 最新版本")
    void testDirectSearch() {
        log.info("========== [可见模式] 直接测试可见浏览器搜索工具 ==========");

        BrowserSearchService searchService = searchToolFactory.createSearchTool(BrowserSearchMode.VISIBLE);
        List<SearchResult> results = searchService.search("Spring AI 最新版本");

        printSearchResults(results);
    }

    @Test
    @DisplayName("AI 对话 + 可见浏览器搜索 - 2026年AI发展趋势")
    void testChatWithSearch() {
        log.info("========== [可见模式] 测试 AI 对话 + 可见浏览器搜索 ==========");
        String userQuery = "2026年人工智能最新发展趋势?";

        String response = visibleChatClient.prompt()
                .user(userQuery)
                .call()
                .content();

        log.info("用户问题: {}", userQuery);
        log.info("AI 回答: {}", response);
    }

    private void printSearchResults(List<SearchResult> results) {
        if (results.isEmpty()) {
            log.info("搜索结果为空");
            return;
        }

        for (int i = 0; i < results.size(); i++) {
            SearchResult result = results.get(i);
            log.info("结果 {}:", i + 1);
            log.info("标题: {}", result.getTitle());
            log.info("链接: {}", result.getUrl());
            log.info("摘要: {}", result.getSummary());
            log.info("----------------------------------------");
        }
    }
}

七、关键优化点与问题解决

1. Playwright 性能优化

2. Spring AI 2.0 工具机制关键点

  1. 使用 @Tool + @ToolParam 注解,自动生成工具元数据,大模型自动识别何时调用;
  2. defaultTools() 全局注册工具,所有对话均可使用联网检索;
  3. 工具返回纯文本,过滤 HTML 标签,减少 Token 消耗。

3. 扩展:搜索引擎前置(可选)

当前示例仅支持传入 URL 爬取,如需输入关键词自动搜网页,可增加一层搜索引擎接口(百度 / 谷歌搜索 API):

  1. AI 传入关键词 → 调用搜索 API 返回多条网页 URL;
  2. 循环调用 Playwright 抓取每个页面内容;
  3. 汇总所有网页文本给大模型总结。

八、适用场景 & 局限性

适用

局限

  1. 仅支持传入 URL 抓取,需额外对接搜索引擎实现关键词搜索;
  2. Playwright 占用内存较高,高并发场景建议浏览器池化管理;
  3. 部分网站存在反爬(UA、验证码、IP 封禁),可补充代理池、验证码识别。

以上就是Spring AI 2.0.0 + Playwright实现联网检索工具的详细内容,更多关于Spring AI Playwright联网检索工具的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文