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

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

一、整体方案说明

  • Spring AI 2.0.0:提供 ToolCallback 工具回调机制,将联网能力封装为 AI 可调用工具;
  • Playwright Java:浏览器爬取网页,解决 JS 动态渲染页面(普通 HttpClient 拿不到动态内容);
  • 流程:大模型判断需要联网 → 调用自定义 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 性能优化

  • 全局单例浏览器,避免每次检索启动 / 关闭浏览器(开销极大);
  • waitUntil("networkidle") 等待页面 JS、接口全部加载完毕,适配 SPA 前端页面(Vue/React 官网、资讯站)。

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

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

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

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

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

八、适用场景 & 局限性

适用

  • 需要实时网络资讯、动态渲染网页(前端 JS 页面);
  • Spring AI 2.0 工具调用联网增强 RAG;

局限

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

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

相关文章

  • mybatis-plus实现四种lambda表达式方式

    mybatis-plus实现四种lambda表达式方式

    使用了lambda表达式 可以通过方法引用的方式来使用实体字段名的操作,本文主要介绍了mybatis-plus实现四种lambda表达式方式,具有一定的参考价值,感兴趣的可以了解一下
    2024-06-06
  • 23种设计模式(21)java享元模式

    23种设计模式(21)java享元模式

    这篇文章主要为大家详细介绍了23种设计模式之java享元模式,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2018-01-01
  • JAVA中重写(Override)与重载(Overload)的相关示例

    JAVA中重写(Override)与重载(Overload)的相关示例

    这篇文章主要给大家介绍了关于JAVA中重写(Override)与重载(Overload)的相关示例,重写(override)和重载(overload)是两种不同的方法重用技术,文中通过代码介绍的非常详细,需要的朋友可以参考下
    2023-10-10
  • Java线程安全的计数器简单实现代码示例

    Java线程安全的计数器简单实现代码示例

    这篇文章主要介绍了Java线程安全的计数器简单实现代码示例,具有一定参考价值,需要的朋友可以了解下。
    2017-10-10
  • Activiti如何动态获取流程图过程详解

    Activiti如何动态获取流程图过程详解

    这篇文章主要介绍了Activiti如何动态获取流程图过程详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-03-03
  • Java import导入及访问控制权限修饰符原理解析

    Java import导入及访问控制权限修饰符原理解析

    这篇文章主要介绍了Java import导入及访问控制权限修饰符过程解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2019-11-11
  • Java数组反转方式

    Java数组反转方式

    这篇文章主要介绍了Java数组反转方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2024-06-06
  • spring的@Transactional注解用法解读

    spring的@Transactional注解用法解读

    这篇文章主要介绍了spring的@Transactional注解用法解读,具有一定借鉴价值,需要的朋友可以参考下
    2018-01-01
  • Java中常用的五种List声明并赋值操作方式

    Java中常用的五种List声明并赋值操作方式

    在Java编程中经常会使用到集合类来存储和操作多个元素,List是一种常见的集合类,它允许存储重复的元素,并且保持元素的插入顺序,这篇文章主要给大家介绍了关于Java中常用的五种List声明并赋值操作的相关资料,需要的朋友可以参考下
    2024-02-02
  • 使用lombok@Data存在extends时需要注意的问题

    使用lombok@Data存在extends时需要注意的问题

    在Java编程中,正确实现equals方法是保证对象比较一致性的关键,使用instanceof检查类型可能导致违反对称性原则,即当子类和父类都重写equals时可能出现a.equals(b)不等于b.equals(a)的情况,Lombok的@EqualsAndHashCode注解可以通过callSuper=true参数
    2024-10-10

最新评论