记一次 Spring AI Alibaba + 百炼的踩坑:结构化输出与联网搜索的冲突

记一次 Spring AI Alibaba + 百炼的踩坑:结构化输出与联网搜索的冲突

背景

项目使用阿里云百炼(DashScope)作为 LLM 后端,通过 spring-ai-alibaba-starter-dashscope(版本 1.1.2.2)集成。业务上需要两个能力同时工作:

  1. 联网搜索enable_search):让模型在回答前先检索实时网络信息,提升答案的时效性和准确度
  2. 结构化输出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_formatjson_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,只要同时启用了结构化输出和联网搜索,同样会遇到这个问题。

相关推荐
欧阳天羲1 小时前
AI智能水枪完整开发攻略
人工智能·macos·xcode
逻辑君1 小时前
Foresight研究报告【20260015】
人工智能·数学建模
万粉变现经纪人1 小时前
2026最新CSDN博客质量分v6.0深度解读:从评分机制到80+实战提分指南
数据库·人工智能·深度学习·csdn·csdn博客·csdn博客质量分6.0·博客质量分
夜郎king1 小时前
告别低效单篇创作,CSDN AI 批量生成工具深度体验
大数据·人工智能·csdn ai 数字营销
星辰AI1 小时前
拒绝“祖传屎山”:用 Git Rebase 重构 Apache/GPL 许可证冲突的分支管理
人工智能·ai·语言模型
薛定猫AI1 小时前
【深度解析】Google Antigravity 2.0:多智能体协同编程、CLI 演进与工程化落地实践
人工智能
专注VB编程开发20年1 小时前
VB.NET是唯一能直接打击 Python 的语言
python
自律懒人1 小时前
AI Agent 记忆方案横评:Memoria vs OpenClaw vs MCP,让Agent记住你的3种方式
人工智能·大模型·ai编程
Allen正心正念20251 小时前
AI编程—claude code中plugin三种范围模式的配置方法
人工智能·ai编程