记一次 Spring AI Alibaba + 百炼的踩坑:结构化输出与联网搜索的冲突
背景
项目使用阿里云百炼(DashScope)作为 LLM 后端,通过 spring-ai-alibaba-starter-dashscope(版本 1.1.2.2)集成。业务上需要两个能力同时工作:
- 联网搜索 (
enable_search):让模型在回答前先检索实时网络信息,提升答案的时效性和准确度 - 结构化输出 (
response_format):让模型返回严格 JSON,便于后端代码解析和处理
两个功能各自都很常用,百炼文档里也都有说明。但当你尝试同时开启它们时,坑就来了。
问题:联网搜索静默失效
预期行为
使用 Spring AI Alibaba 的标准写法,开启结构化输出 + 联网搜索:
java
// ❌ 这种写法下,联网搜索会失效
ChatResponse response = chatClient.prompt()
.system("你是产品技术支持专家。根据用户问题,返回JSON格式的意图分类结果。")
.user(userQuestion)
.options(DashScopeChatOptions.builder()
.withEnableSearch(true) // 开启联网搜索
.build())
.call()
.entity(IntentClassification.class); // Spring AI 原生结构化输出
期望模型结合网络搜索结果,返回一个标准的 IntentClassification JSON 对象。
实际行为
模型确实返回了格式正确的 JSON,但联网搜索没有被触发。
现象是:
- 返回的答案里没有任何来自网络的实时信息
- 问题涉及一些需要最新数据才能回答的内容,但模型用的全是训练数据
- 在百炼控制台查看调用日志,
enable_search参数虽然传了true,但搜索实际未生效 - 把
.entity()换成.content()并去掉结构化输出约束后,联网搜索恢复了
根因
这是百炼 API 层面的一个限制:
当设置 response_format 为 json_object 类型时,enable_search 参数会被忽略。
具体来说,Spring AI Alibaba 的 DashScopeChatOptions 在底层映射到百炼的请求参数:
| Spring AI Alibaba | 百炼 API 参数 | 说明 |
|---|---|---|
.withEnableSearch(true) |
enable_search: true |
开启联网搜索 |
.entity(Class) / 原生结构化输出 |
result_format: "message" + chat_template 中的 JSON 约束,或 response_format |
要求模型返回结构化 JSON |
问题出在百炼服务端的处理逻辑上:当模型被要求严格按 JSON schema 输出时,联网搜索返回的网页片段无法被可靠地嵌入到结构化响应中,因此搜索被服务端静默跳过------不会报错,也不会在响应中提示,只是搜索结果没有被纳入。
这个行为在百炼的官方文档中有提及,但很容易被忽略。而且 Spring AI Alibaba 的封装层并没有对此做任何校验或警告------你传了 enableSearch=true,它默默地给你加上,但实际不生效。
项目中的 Workaround
既然原生结构化输出和联网搜索不能共存,那就二选一 。项目中选择了保留联网搜索,用基于 prompt 的结构化约束替代原生结构化输出。
方案:Prompt 约束 + 手动 JSON 解析
Step 1:在 System Prompt 中明确 JSON 格式要求
不要用 Spring AI 的 .entity() 转换,而是在 prompt 中直接写死 JSON 模板:
java
// Phase 1 意图识别的 system prompt(节选)
String systemPrompt = """
你是产品技术支持领域的意图识别专家。
根据用户问题,识别意图类型。
# 意图类型
1. FIND_COMPONENT ------ 用户查询具体产品的使用/故障/维护问题
2. GENERAL_CONSULT ------ 一般性咨询、闲聊、问候
3. VIEW_RECENT_FOLLOWUP_TASKS ------ 查看历史记录
# 返回 JSON(不要 markdown 围栏)
1)FIND_COMPONENT:仅 {"intent":"FIND_COMPONENT"}
2)GENERAL_CONSULT:{"intent":"GENERAL_CONSULT","content":"...","needSync":true或false}
3)VIEW_RECENT_FOLLOWUP_TASKS:{"intent":"VIEW_RECENT_FOLLOWUP_TASKS","content":"..."}
""";
Step 2:用 .content() 获取原始文本,手动提取 JSON
java
// ✅ 这种写法联网搜索正常工作
private IntentClassification invokeIntentClassifier(String userMessage) {
var prompt = chatClient
.prompt()
.system(buildIntentSystemPrompt())
.user(userMessage);
// 只在需要时加上联网搜索
if (dashScopeProperties.isWebSearchEnabled()) {
prompt = prompt.options(
DashScopeChatOptions.builder()
.withEnableSearch(true)
.build()
);
}
String rawContent = prompt.call().content();
String json = extractJson(rawContent); // 手动提取 {...} 块
return objectMapper.readValue(json, IntentClassification.class);
}
Step 3:写一个健壮的 JSON 提取方法
模型偶尔会在 JSON 前后加上说明文字甚至 markdown 围栏,需要一个容错性好的提取逻辑:
java
private String extractJson(String text) {
if (text == null || text.isBlank()) {
return null;
}
// 定位 "intent" 关键字段
int intentKey = text.indexOf("\"intent\"");
if (intentKey == -1) {
return null;
}
// 找到包围它的最外层大括号
int braceBeforeIntent = text.lastIndexOf('{', intentKey);
int end = text.lastIndexOf('}');
if (braceBeforeIntent != -1 && end != -1 && end > braceBeforeIntent) {
return text.substring(braceBeforeIntent, end + 1);
}
return null;
}
这样就实现了"结构化的输出内容 + 联网搜索正常工作",两全其美。
两种方案对比
| 维度 | 原生结构化输出 + 联网搜索 | Prompt 约束 + 联网搜索 |
|---|---|---|
| 联网搜索 | 失效 | 正常 |
| JSON 格式可靠性 | 高(API 层面强制) | 中(依赖 prompt 质量,可加 fallback) |
| 额外代码 | 少(Spring AI 自动转换) | 多(需手动 extractJson + Jackson) |
| 灵活性 | 低(schema 需要在代码中定义) | 高(prompt 可以随时调整格式) |
| 适用场景 | 不需要联网搜索的结构化任务 | 需要联网搜索 + 结构化输出的场景 |
其他相关踩坑
1. Spring AI Alibaba 1.1.2 的 WebClient 依赖
spring-ai-alibaba-starter-dashscope 内部依赖了 WebClient,但不会自动引入 spring-boot-starter-webflux。如果你的项目只用 spring-boot-starter-web(Servlet 容器),启动时会报 WebClient 找不到。
解决:显式加上 webflux 依赖:
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
这与问题本身无关,但经常是集成 DashScope starter 时遇到的第一个坑。
2. API Key 不要写在 application.yml 的默认值里
yaml
# ❌ 避免这样写
spring:
ai:
dashscope:
api-key: sk-xxxxxxxxxxxxxxxxxxxx
application.yml 通常会被提交到 Git,API Key 会泄露。即使设了环境变量覆盖,默认值本身也是安全隐患。正确的做法是只用环境变量,配置文件里不留 fallback:
yaml
# ✅ 正确
spring:
ai:
dashscope:
api-key: ${AI_DASHSCOPE_API_KEY}
3. 联网搜索会产生额外费用
百炼的联网搜索是独立计费项,不是免费的。每次调用如果开启了 enable_search,无论是否实际返回了搜索结果,都可能产生额外费用。
建议:
- 开发环境设
web-search-enabled: false - 生产环境通过配置中心动态切换,必要时可以随时关闭降级
- 监控调用量,确认搜索相关费用在预算内
总结
核心结论 :阿里云百炼中,原生结构化输出(response_format)和联网搜索(enable_search)不能同时生效。如果需要两者兼具,用 prompt 约束 + 手动解析的方式替代原生结构化输出,是一个验证可行的方案。
关键代码片段(来自实际项目):
java
// 在 prompt 中约束 JSON 格式,不使用 .entity() 转换
var prompt = chatClient.prompt()
.system("...返回 JSON(不要 markdown 围栏){\"intent\":\"...\"}...")
.user(userMessage);
// 有条件地开启联网搜索
if (webSearchEnabled) {
prompt = prompt.options(
DashScopeChatOptions.builder().withEnableSearch(true).build()
);
}
// 手动解析
String raw = prompt.call().content();
String json = extractJson(raw);
IntentClassification result = objectMapper.readValue(json, IntentClassification.class);
适用范围:这个问题的根因是百炼 API 层面的限制,与 Spring AI Alibaba 版本无关。如果你使用的是其他语言 SDK 或直接调用百炼 REST API,只要同时启用了结构化输出和联网搜索,同样会遇到这个问题。