一、整体方案说明
- 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联网检索工具的资料请关注代码网其它相关文章!
发表评论