Spring AI 2.0.0 + Playwright实现联网检索工具
作者: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 工具机制关键点
- 使用
@Tool+@ToolParam注解,自动生成工具元数据,大模型自动识别何时调用; defaultTools()全局注册工具,所有对话均可使用联网检索;- 工具返回纯文本,过滤 HTML 标签,减少 Token 消耗。
3. 扩展:搜索引擎前置(可选)
当前示例仅支持传入 URL 爬取,如需输入关键词自动搜网页,可增加一层搜索引擎接口(百度 / 谷歌搜索 API):
- AI 传入关键词 → 调用搜索 API 返回多条网页 URL;
- 循环调用 Playwright 抓取每个页面内容;
- 汇总所有网页文本给大模型总结。
八、适用场景 & 局限性
适用
- 需要实时网络资讯、动态渲染网页(前端 JS 页面);
- Spring AI 2.0 工具调用联网增强 RAG;
局限
- 仅支持传入 URL 抓取,需额外对接搜索引擎实现关键词搜索;
- Playwright 占用内存较高,高并发场景建议浏览器池化管理;
- 部分网站存在反爬(UA、验证码、IP 封禁),可补充代理池、验证码识别。
以上就是Spring AI 2.0.0 + Playwright实现联网检索工具的详细内容,更多关于Spring AI Playwright联网检索工具的资料请关注脚本之家其它相关文章!
