Spring AI 接入阿里云百炼模型内置工具:联网搜索与网页抓取实战
一、解决什么问题
项目后端(backend,Spring Boot 3 + Spring AI)接入阿里云百炼(DashScope)模型后,我们想做到这样一件事:
用户在智能体里问"查一下 2026 年 Qwen 最新版本发布了什么功能 ",模型不要靠训练数据里的旧知识硬答,而是自己上网搜索 ;如果网页里有相关文章,还能打开网页抓取正文,再基于真实内容回答。
这就涉及阿里云百炼的"模型自带工具 ":web_search(联网搜索)、web_extractor(网页抓取)等。它们不是我们自己在后端用爬虫实现的,而是百炼平台在服务端替模型执行的官方工具。
读完这篇笔记你会掌握:
- 模型自带工具和传统 Function Calling 有什么区别;
- 百炼为这类工具提供了哪几条 API 通道、各自参数怎么传;
- 先用 curl 把协议调通,再看本项目
BailianResponsesClient、DashScopeProvider等代码是怎么落地成产品功能的; - 常见坑:模型不支持、参数没生效、工具结果太大、SSE 断流、计费差异等。
二、先建立直觉:什么是"模型自带工具"
2.1 一个通俗类比
想象你雇了一个"全能助理"。传统 Function Calling 是:助理不会开飞机,但你给他一本飞行手册,并把飞机停在楼下 ,他看完手册自己按步骤操作------工具执行发生在你这边的场地。
模型自带工具则是:助理的公司本身就提供"查资料组" ,助理打个内线电话说"帮我查一下",公司后台 查完把资料送到助理桌上。助理不需要知道搜索引擎怎么写、网页怎么抓,他只需要告诉公司要开这些服务。
2.2 Function Calling 与平台内置工具的对比
| 对比项 | 自定义 Function Calling / @Tool | 平台内置工具(web_search / web_extractor) |
|---|---|---|
| 工具由谁执行 | 我们自己的后端执行 | 百炼服务端执行 |
| 怎么"告诉模型" | 随请求发送 JSON Schema 工具定义 | 平台约定好名字,直接开启(enable_search 或 tools) |
| 需要写什么代码 | 每个工具一个 Java 方法 | 基本不用实现,只做参数映射与事件解析 |
| 适用能力 | 查库、调公司 API、业务逻辑 | 联网搜索、网页抓取、代码解释器、PDF 解析等通用能力 |
| 数据可达性 | 取决于我们的执行环境 | 平台具备公网检索与抓取能力 |
| 灵活性 | 高,想干什么自己写 | 低,平台给什么用什么 |
| 计费 | 只花模型 token | 模型 token + 部分工具按次计费(见 FAQ) |
项目里的 SqlQueryTool(get_db_schema / query_database)就是第一类;本笔记的主角是第二类。
2.3 为什么优先用平台自带工具
如果自己用 Jsoup 写爬虫(项目 crawl/tool/CrawlTool.java 就是这么做的),要处理反爬、页面结构变化、正文提取、多轮搜索重试等一堆事。而百炼把"搜索 + 抓取"做成了服务端工具:
- 搜索由平台搜索引擎执行,质量稳定;
web_extractor能把网页正文抽出来喂给模型;- 模型可以在"搜索 → 读网页 → 再搜索 → 总结"之间自主规划,官方称之为 agent 式检索。
代价是:受平台支持的模型与协议限制、工具按次计费、抓取内容会占用输入 token。
三、百炼内置工具的三条 API 通道
阿里云官方把联网搜索/网页抓取分成三种调用方式,参数差异很大,新手最容易在这里搞混:
3.1 原生 DashScope 协议
地址形如 https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation,使用官方 dashscope SDK 的 Generation,通过参数开启:
json
{
"model": "qwen-plus",
"parameters": {
"enable_search": true,
"search_options": {
"search_strategy": "max",
"forced_search": true
}
}
}
这个通道功能最全(能返回搜索来源、角标引用等),但它不是 OpenAI 兼容协议。我们的项目统一用 Spring AI 的 OpenAI 兼容客户端,所以没有走这条路,作为知识了解即可。
3.2 OpenAI 兼容 - Chat Completions(老式 enable_search)
地址形如 https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions,就是 OpenAI 的 /chat/completions。由于 enable_search 不是 OpenAI 标准参数:
- Python 里要放
extra_body; - Node.js 里可以直接放顶层;
- Spring AI 里要放到
extraBody(见后文代码)。
json
{
"model": "qwen-plus",
"messages": [
{ "role": "user", "content": "杭州明天天气如何" }
],
"enable_search": true,
"search_options": {
"search_strategy": "max",
"forced_search": true
}
}
特点:
- 实现简单,在原有聊天请求上加一个参数即可;
- 适合老一代文本模型 (如
qwen-plus、qwen-max等)做普通联网搜索; - 不支持返回结构化搜索来源(兼容协议限制);
- 网页抓取若走这条路,需要用
search_strategy: agent_max且仅部分思考模式模型支持(如qwen3-max思考模式、qwen3.5-plus)。
3.3 OpenAI 兼容 - Responses API(内置工具主通道,推荐)
地址:https://dashscope.aliyuncs.com/compatible-mode/v1/responses。
Responses API 是 OpenAI 在 Chat Completions 基础上演进的"智能体原生"接口,百炼官方称它支持内置联网搜索、网页抓取、代码解释器、文搜图、图搜图 等工具。启用方式是把工具名放进 tools 数组:
json
{
"model": "qwen3.8-max",
"input": "请访问阿里云百炼代码解释器部分的官方文档,并总结主要内容",
"tools": [
{ "type": "web_search" },
{ "type": "web_extractor" }
],
"enable_thinking": true,
"stream": true
}
注意这里工具是 web_search / web_extractor,不是 enable_search------两种协议的开关长得完全不一样。
特点:
- 内置工具执行过程会以 SSE 事件流返回,我们能看到"模型决定搜索、搜索到什么、抓了哪个网页";
- 官方网页抓取文档推荐使用 Responses API;
- 本项目实测:
qwen3.8-max这类新一代模型走 Chat Completions 的enable_search不生效,走 Responses API 才可靠(代码注释里有记录); - 仅部分模型支持,且模型清单会随平台迭代变化,要以官方文档与控制台为准。
3.4 三条路怎么选
| 需求 | 推荐通道 | 说明 |
|---|---|---|
| 老模型快速加联网搜索 | Chat Completions + enable_search |
改动最小,先 curl 验证 |
| 新版 Qwen 模型 + 搜索/抓取/代码解释 | Responses API + tools |
本项目主路径 |
| 需要搜索来源、角标等丰富能力 | 原生 DashScope | 需放弃 OpenAI 兼容层,另写客户端 |
四、先动手:curl 把协议调通
不管用什么框架,先 curl 验证"参数 + 模型"组合是否生效,能省掉大量"代码没毛病但平台不认"的排查时间。
4.1 验证 Chat Completions 联网搜索
bash
curl https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions \
-H "Authorization: Bearer $DASHSCOPE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "qwen-plus",
"messages": [
{ "role": "user", "content": "杭州明天天气如何?" }
],
"enable_search": true,
"search_options": {
"search_strategy": "max",
"forced_search": true
}
}'
如果回答内容明显带出"根据搜索结果/来源"等实时信息,说明生效。
4.2 验证 Responses API 搜索 + 抓取
bash
curl https://dashscope.aliyuncs.com/compatible-mode/v1/responses \
-H "Authorization: Bearer $DASHSCOPE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "qwen3.8-max",
"input": "请打开 https://help.aliyun.com/zh/model-studio/web-extractor 并总结网页抓取功能的启用方式",
"tools": [
{ "type": "web_search" },
{ "type": "web_extractor" }
],
"enable_thinking": true,
"stream": true
}'
能观察到类似下面的 SSE 事件序列,就说明链路通了:
text
event: response.reasoning_summary_text.delta
data: {"type":"response.reasoning_summary_text.delta","delta":"用户让我访问指定网页......"}
event: response.output_item.done
data: {"type":"response.output_item.done","item":{"type":"web_search_call", ...}}
event: response.output_item.done
data: {"type":"response.output_item.done","item":{"type":"web_extractor_call", ...}}
event: response.output_text.delta
data: {"type":"response.output_text.delta","delta":"根据阿里云官方文档......"}
五、关键参数逐个解释
5.1 enable_search 与 tools:两种"开关"
- Chat Completions / 原生 DashScope:顶层布尔参数
enable_search: true; - Responses API:
tools数组里放{"type": "web_search"}/{"type": "web_extractor"}。
两者互不通用,封装适配层时要按协议分开翻译。
5.2 enable_thinking:思考模式
部分模型/工具组合要求先开思考模式(如 qwen3-max 系列用 agent_max 抓网页时)。项目里的实现把 enable_thinking: true 和内置工具一起注入,理由写在 DashScopeProvider 注释里:
内置工具要求开启思考模式,与默认关闭思考互斥。
而项目普通文本生成链路(LlmService.buildChatModel)默认是关闭思考 的(防止思考 token 挤占正文)。所以"开内置工具"必须显式把思考参数覆盖回来,并且要在合并 extraBody 时放最后,保证优先级。
5.3 search_options.search_strategy:搜索量级
不同协议能设置的策略不同,官方目前主要取值:
| 取值 | 含义 | 注意 |
|---|---|---|
turbo(默认) |
平衡速度与效果 | 大多数场景够用 |
max |
更全面、多源搜索 | 更慢、可能更贵 |
agent |
多轮搜索 + 模型整合 | 适用 Qwen3-Max/3.5/3.6/3.7 部分模型;不支持 Qwen3.8 等 |
agent_max |
agent 基础上支持网页抓取 | 旧 Chat Completions 路径开抓取用;仅个别思考模式模型 |
策略随模型系列不断变化,务必以官方页面为准,代码里不要写死"一定支持"。
5.4 forced_search:强制搜索
forced_search: true 表示用户既然勾选了联网,就必须真的搜索核实,而不是让模型"看心情"决定搜不搜。本项目在 Chat Completions 兜底参数里就注入了它。
5.5 web_extractor 必须和 web_search 搭配
网页抓取工具本身没有"抓哪个 URL"的来源,通常是模型先搜索、拿到候选 URL,再决定打开哪个。官方文档明确:Responses API 开启抓取时要同时添加 web_search 。前端只勾了 web_extractor 而漏掉 web_search,容易造成调用空转或报错。
六、项目里是怎么落地的(读代码)
仓库地址:D:\workspace\springai-web-novel。版本:Spring Boot 3.5.8 + Spring AI 1.1.2 + Spring AI Alibaba 1.1.2.2。
6.1 整体链路
用户提问:请帮我查某本书的最新豆瓣评分
│ POST /api/agent/apps/chat(SSE)
▼
BasicAgentExecutor.streamExecute
│ 模型 provider=DASHSCOPE 且 enabledTools 含 web_search/web_extractor
▼
llmService.shouldUseResponsesApi(resolvedModel) == true
▼
BailianResponsesClient.streamResponses
│ POST https://dashscope.aliyuncs.com/compatible-mode/v1/responses
│ body: model / input(system+user) / tools / enable_thinking / stream
▼
百炼服务端:思考 → 执行搜索 → 抓网页 → 组织回答
│ SSE:reasoning / *_call(done) / output_text.delta / completed
▼
事件映射为 delta / reasoning / tool_call / tool_result / done
│
▼
AgentChatController 转发 SSE → 前端打字机效果 + 工具调用气泡
6.2 第一步:模型管理页把"内置工具"勾选存下来
前端文件:frontend/src/constants/modelProviders.ts
每个服务商预设里声明了支持的工具:
ts
export const MODEL_TOOL_LABELS: Record<string, string> = {
web_search: '联网搜索',
pdf_parsing: 'PDF 解析',
web_extractor: '网页抓取',
}
{
key: 'DASHSCOPE',
label: '阿里云百炼',
baseUrl: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
tools: ['web_search', 'pdf_parsing', 'web_extractor'],
}
管理员/用户模型页(AdminModelsView.vue、UserModelsView.vue)根据 tools 渲染复选框,勾选结果存进表单的 enabledTools 数组。
小提醒:
pdf_parsing目前只出现在前端清单和后端注释里,DashScopeProvider真正映射的是web_search/web_extractor两个;想支持 PDF 解析需要继续扩展适配器,别被复选框误导。
6.3 第二步:enabledTools 落库并一路带到模型解析结果
数据库字段(schema-mysql.sql):
sql
`enabled_tools` TEXT COMMENT '启用的供应商内置工具列表(JSON数组),如 web_search/pdf_parsing/web_extractor'
对应实体(backend/src/main/java/com/zhanghui/domain/AiModel.java):
java
@Convert(converter = JsonConverter.class)
private List<String> enabledTools = new ArrayList<>();
模型服务商把解密后的模型信息解析成 ResolvedModel record,enabledTools 就在其中:
java
public record ResolvedModel(
...,
String provider, // DASHSCOPE / DEEPSEEK ...
BigDecimal topP,
List<String> enabledTools // 启用的供应商内置工具列表
) {}
6.4 第三步:DashScopeProvider 做平台差异翻译
项目把"不同模型服务商的差异"收敛到 ModelProvider 接口,每个服务商一个实现,避免在核心调用链路上到处 if-else。看 backend/src/main/java/com/zhanghui/service/provider/DashScopeProvider.java,核心是三个方法:
java
public static final String TOOL_WEB_SEARCH = "web_search";
public static final String TOOL_WEB_EXTRACTOR = "web_extractor";
// ① 内置工具 → Chat Completions 请求体扩展参数(老协议兜底)
public Map<String, Object> toolParams(ResolvedModel m) {
boolean search = m.enabledTools().contains(TOOL_WEB_SEARCH);
boolean extractor = m.enabledTools().contains(TOOL_WEB_EXTRACTOR);
...
params.put("enable_search", true);
if (extractor) {
params.put("search_options", Map.of(
"search_strategy", "agent_max",
"forced_search", true));
}
params.put("enable_thinking", true);
return params;
}
// ② 内置工具 → Responses API tools 列表
public List<String> responsesTools(ResolvedModel m) {
// 遍历 enabledTools,只挑 web_search / web_extractor
}
// ③ 是否必须走 Responses API
public boolean preferResponsesApi(ResolvedModel m) {
return m.enabledTools().contains(TOOL_WEB_SEARCH)
|| m.enabledTools().contains(TOOL_WEB_EXTRACTOR);
}
也就是说,适配器同时维护了两套翻译:
- 如果将来有人强制走 Chat Completions,就把
enable_search那套参数塞进extraBody; - 但框架判断
preferResponsesApi为 true 时,会直接切到 Responses 路径,把工具名放进tools数组。
这解释了为什么代码里既有 enable_search 又有 tools------它们是两条协议通道,不是同一件事。
6.5 第四步:执行器分流
backend/src/main/java/com/zhanghui/agent/executor/BasicAgentExecutor.java 的 streamExecute:
java
ResolvedModel resolvedModel = agentService.resolveRuntimeModel(userId, config);
// 百炼 + 启用平台内置工具时走 Responses API 专用路径
if (llmService.shouldUseResponsesApi(resolvedModel)) {
return streamViaResponsesApi(config, resolvedModel, userMessage, requestVariables);
}
streamViaResponsesApi 做的事很轻:渲染系统提示词(支持 {变量} 占位符),然后调 bailianResponsesClient.streamResponses(...),最后拼一个 done 事件。因为内置工具由百炼服务端执行,本地不需要 Spring AI 的 function calling 循环。
普通链路(不启用内置工具)仍走 ChatClient + ToolCallingManager 的本地工具循环,与内置工具路径互不干扰。
6.6 第五步:自研 Responses SSE 客户端
Spring AI 1.1.x 的 OpenAI 模块还不提供 Responses API 封装,而百炼内置工具又必须走 Responses,所以项目手写了一个轻量客户端:
backend/src/main/java/com/zhanghui/service/BailianResponsesClient.java
请求体构造(关键片段):
java
private Map<String, Object> buildRequestBody(ResolvedModel m, String systemPrompt, String userMessage) {
List<Map<String, Object>> input = new ArrayList<>();
if (systemPrompt != null && !systemPrompt.isBlank()) {
input.add(Map.of("role", "system", "content", systemPrompt));
}
input.add(Map.of("role", "user", "content", userMessage));
// 内置工具:只注入 web_search / web_extractor
List<Map<String, String>> tools = new ArrayList<>();
for (String tool : m.enabledTools()) {
if (ModelProvider.TOOL_WEB_SEARCH.equals(tool)
|| ModelProvider.TOOL_WEB_EXTRACTOR.equals(tool)) {
tools.add(Map.of("type", tool));
}
}
Map<String, Object> body = new HashMap<>();
body.put("model", m.model());
body.put("input", input);
body.put("tools", tools);
body.put("enable_thinking", true); // 内置工具要求思考模式
body.put("stream", true);
return body;
}
发送与解析用 JDK 原生 HttpClient(HTTP/1.1),原因注释写得很清楚:
Reactor Netty WebClient 与本 SSE 流式响应存在兼容问题(实测 Connection reset,curl 正常)。
逐行读取 data: 前缀的 SSE JSON,做事件映射:
java
case "response.output_text.delta" -> // 正文增量 → delta
case "response.reasoning_summary_text.delta" -> // 思考增量 → reasoning
case "response.output_item.done" -> {
// item.type 以 _call 结尾(web_search_call / web_extractor_call ...)
// goal 为搜索/抓取目标;output 或 action.sources 为结果
// → tool_call + tool_result(结果超 4000 字符截断)
}
整体超时放宽到 5 分钟 (搜索/抓取慢),工具结果默认截断为 4000 字符,防止抓来的长网页把上下文和前端撑爆。
URL 拼接细节也很关键:前端预设填的 Base URL 是 https://dashscope.aliyuncs.com/compatible-mode/v1,工具类 BaseUrlUtil.normalize 会把尾部 /v1 剥掉,再分别拼:
- Chat Completions:
.../compatible-mode/v1/chat/completions - Responses:
.../compatible-mode/v1/responses
如果直接拿带 /v1 的地址再拼一次,就会拼出 /v1/v1/responses 这类错误路径。
6.7 第六步:事件统一成 AgentChatEvent
backend/src/main/java/com/zhanghui/agent/domain/AgentChatEvent.java 定义了全项目统一的事件模型:
| 事件类型 | 含义 | data |
|---|---|---|
delta |
正文增量 | {text} |
reasoning |
思考增量 | {text} |
tool_call |
模型决定调用工具 | {id, name, arguments} |
tool_result |
工具执行结果 | {name, result} |
done |
本轮完成 | {status, model} |
AgentChatController 把事件逐个转成 SSE 推给前端,前端就能一边显示思考过程,一边把 web_search_call / web_extractor_call 渲染成"工具调用"气泡,最后把正文打字机式打出来。
七、如果自己从零接入,最小三步
不管是不是用本项目,思路都是这三步:
第一步:确认模型支持 + 用 curl 验证
打开官方"联网搜索 / 网页抓取"文档,确认你打算用的模型在支持列表里;再用第四节 curl 验证,别直接写代码。
第二步:请求里带上工具开关
- 简单联网搜索且模型支持:Chat Completions 请求加
enable_search: true; - 新版模型 + 网页抓取:Responses API 的
tools加web_search+web_extractor,并enable_thinking: true。
第三步:解析流式事件,而不是只取最终文本
内置工具的价值在于"模型自己决定搜什么、抓什么"。Responses API 会把决策过程以 response.output_item.done 等事件流出来,建议:
response.output_text.delta→ 展示正文;response.reasoning_summary_text.delta→ 展示思考;response.output_item.done且item.type以_call结尾 → 记录工具名、目标、结果;- 对抓取结果做长度截断,防止上下文爆炸。
八、在本项目里测试
- 启动后端与前端,进入"模型管理",新增/编辑百炼模型,服务商选"阿里云百炼";
- 勾选"联网搜索"和"网页抓取",模型 ID 建议填支持 Responses 内置工具的型号(如
qwen3.8-max),保存; - 确保该模型被设为默认或备用模型;
- 打开任意 Agent 应用(或新建一个),在对话里问一个时效性问题,例如"查一下阿里云百炼最新内置工具有哪些";
- 观察事件流:应出现 reasoning(思考)→
web_search_call/web_extractor_call(工具气泡)→ delta 正文; - 对照后端日志,能看到请求的 tools 与 extraBody 参数。
九、FAQ 与踩坑记录
Q1:勾了工具,但小说大纲/章节生成没联网?
本项目目前只在 Agent 应用对话链路 里做内置工具分流;LlmService.completeText / streamStory 等普通生成链路不会注入平台工具参数。如果想让写作链路也联网,需要在对应调用点把 platformToolParams 合并进 extraBody,或另走 Responses 客户端。
Q2:为什么工具选了 web_extractor 却没效果?
先确认是否同时勾了 web_search。官方要求两者配对使用;只开抓取没有搜索来源可抓。另外确认模型是 Responses API 支持列表里的型号。
Q3:为什么 qwen3.8-max 加 enable_search 没反应?
项目代码注释记录了这个实测结论:新一代模型走 Chat Completions 的 enable_search 不生效,应走 Responses API。写代码前先 curl 验证,不同模型/协议的兼容矩阵变化很快。
Q4:Spring AI 官方有没有 Responses API 客户端?
在项目使用的 Spring AI 1.1.x 中没有可用封装,所以自研了 BailianResponsesClient。实现时注意:
- 用 JDK
HttpClient+ HTTP/1.1 解析 SSE(实测 WebClient 会 Connection reset); - 整体超时放宽到 5 分钟;
- 工具结果要截断。
Q5:会不会很烧钱?
两部分费用:
- 抓取的网页内容会拼进提示词,按模型输入 token 计费;
- 工具调用费用:联网搜索按次收费(以官方页面为准,北京地域文档示例为每千次 4 元),网页抓取文档标注"限时免费"。
日常查询建议用 turbo 策略控制成本,只有研究/报告场景再上 max / agent。
Q6:工具执行结果能直接给前端看吗?
可以。Responses API 流式事件里带着工具名、目标(goal)和结果/来源,项目把它们映射为 tool_call / tool_result 事件,前端按气泡渲染。要注意个别字段(output / action.sources)不同工具不一样,解析要做兜底。
Q7:enable_thinking 和"用户要求关闭思考"冲突怎么办?
内置工具要求思考模式,就必须让平台参数最后合并并覆盖。BasicAgentExecutor.buildChatOptions 的合并顺序是:先合并用户思考开关,再把平台内置工具参数放最后,所以"启用内置工具时思考必然打开"。
Q8:为什么 Responses 路径目前"记不住"多轮对话?
streamViaResponsesApi 目前是单轮请求(系统指令 + 当前用户消息),没有把本地会话历史回传给百炼,也没有走 previous_response_id 机制。需要多轮记忆时,要么把历史消息拼进 input,要么利用 Responses API 的 previous_response_id(官方说明响应 id 7 天有效),这一点写代码时要想清楚产品取舍。
十、参考文档
- 阿里云百炼:大模型如何联网搜索
https://help.aliyun.com/zh/model-studio/web-search - 阿里云百炼:如何开启并使用网页抓取功能
https://help.aliyun.com/zh/model-studio/web-extractor - 阿里云百炼:OpenAI 兼容 - Responses
https://help.aliyun.com/zh/model-studio/compatibility-with-openai-responses-api - 阿里云百炼:创建响应(Responses API 参数与事件)
https://help.aliyun.com/zh/model-studio/qwen-api-via-openai-responses
平台支持模型、策略与价格会持续变化,落地前记得以官方文档和控制台"模型广场"为准。