Spring AI 结构化输出 Agent 实战:让大模型返回精准 JSON

场景

Spring AI + Ollama 深度实战:从 RAG 问答到 Graph Agent 全流程指南:

https://blog.csdn.net/BADAO_LIUMANG_QIZHI/article/details/161199169

基于上述示例代码,学习结构化输出 Agent的示例。

在构建智能体应用时,除了让模型能调用工具、规划步骤,输出结构化数据同样至关重要。

例如,我们希望天气查询工具直接返回 {"city":"北京","temperature":22} 这样的 JSON,

而不是自然语言描述。

Spring AI 提供了多种机制来约束大模型的输出格式,实现可被程序精准消费的结构化响应。

本文仍基于 Spring AI 1.1.2 + Ollama + RAG 技术栈,在之前的 Agent 项目基础上,

系统讲解结构化输出的概念、实现原理,并给出可直接运行的完整代码

一、为什么需要结构化输出?

场景 非结构化输出 结构化输出
天气查询 "北京今天天气晴朗,温度22度" {"city":"北京","weather":"晴","temperature":22}
实体抽取 "用户名叫张三,电话13800138000" {"name":"张三","phone":"13800138000"}
问答+信心评估 "根据资料,答案是A,但我不是很确定" {"answer":"A","confidence":0.7}

结构化输出让下游系统可以直接解析结果,无需再做复杂的文本抽取,是实现自动化工作流的基础

二、Spring AI 结构化输出实现原理

Spring AI 利用 Function Calling 的基础设施来约束输出:

当调用 ChatClient 时,可以将一个 Java 类作为期望返回的结构体,

Spring AI 会将其转换为 JSON Schema,并通过提示词或工具定义告知模型输出必须符合该格式。

底层机制:

JSON Schema 生成:

Spring AI 自动将 Bean 类转换为 JSON Schema,描述字段名、类型、是否必填等。

模型约束:

支持结构化输出的模型(如 qwen2.5:7b)会参考该 Schema,调整生成逻辑以匹配要求。

反序列化:

响应被自动反序列化为目标 Java 对象,处理失败时会进行重试或降级。

注意:

并非所有模型都原生支持严格的结构化输出,Ollama 的 qwen2.5:7b 对 JSON Schema 的支持良好,

但实际效果可能因模型而异,建议配合明确的指令使用。

注:

博客:
https://blog.csdn.net/badao_liumang_qizhi

实现

三、基础示例:让 ChatClient 直接返回 POJO

1、定义输出实体

复制代码
package com.badao.ai.output;

public class WeatherInfo {
    private String city;
    private String weather;
    private int temperature;

    // 必须有无参构造器 + getter/setter(Spring AI 使用 Jackson 反序列化)
    public WeatherInfo() {}
    public String getCity() { return city; }
    public void setCity(String city) { this.city = city; }
    public String getWeather() { return weather; }
    public void setWeather(String weather) { this.weather = weather; }
    public int getTemperature() { return temperature; }
    public void setTemperature(int temperature) { this.temperature = temperature; }
}

2. 修改 AgentRagConfig,配置 ChatClient 支持结构化输出

在 AgentRagConfig 中我们保留 RAG 和工具的注册,但增加一个专门用于结构化输出的 ChatClient Bean,

或者直接在 Service 中通过 call().entity() 指定目标类型。

简单起见,我们在 AgentService 中直接使用:

复制代码
package com.badao.ai.service;

import com.badao.ai.config.AgentGraphConfig;
import com.badao.ai.output.WeatherInfo;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.stereotype.Service;

@Service
public class AgentService {

    private final AgentGraphConfig.AgentWorkflow workflow;

    private final ChatClient chatClient;

    public AgentService(AgentGraphConfig.AgentWorkflow workflow, ChatClient chatClient) {
        this.workflow = workflow;
        this.chatClient = chatClient;
    }

    public String ask(String question) {
        return workflow.execute(question);
    }

    public WeatherInfo askWeatherStructured(String city) {
        String prompt = "请查询城市"%s"的天气,并以 JSON 格式返回,包含 city、weather、temperature 三个字段。".formatted(city);
        return chatClient.prompt()
                .user(prompt)
                .call()
                .entity(WeatherInfo.class);   // 核心:指定返回类型
    }
}

3、新增Controller

复制代码
    // 新增结构化天气接口
    @PostMapping("/weather")
    public WeatherInfo weather(@RequestBody String city) {
        return agentService.askWeatherStructured(city);
    }

注意:

call().entity(Class) 会强制要求模型返回一个 JSON 对象并反序列化为指定类型。

若模型输出不是有效 JSON 或字段不匹配,Spring AI 会尝试重试(通过 RetryTemplate),默认重试 3 次后抛出异常。

4、测试效果

四、进阶示例:工具调用的结构化返回值

在实际 Agent 中,我们通常希望工具返回的结果就是结构化的,以便后续节点处理。

我们可以修改 WeatherTool 让它返回 WeatherInfo 对象,并利用 @Tool 注解的自动 Schema 生成能力。

  1. 修改 WeatherTool 返回结构化对象

    package com.badao.ai.tools;

    import com.badao.ai.output.WeatherInfo;
    import org.springframework.ai.tool.annotation.Tool;
    import org.springframework.ai.tool.annotation.ToolParam;
    import org.springframework.stereotype.Component;

    @Component
    public class WeatherTool {

    复制代码
     @Tool(name = "get_weather", description = "查询指定城市的实时天气,返回结构化数据")
     public WeatherInfo getWeather(@ToolParam(description = "城市名称") String city) {
         // 模拟天气数据,实际可接入API
         WeatherInfo info = new WeatherInfo();
         info.setCity(city);
         info.setWeather("晴");
         info.setTemperature(22);
         return info;
     }

    }

Spring AI 会自动将 WeatherInfo 类的 JSON Schema 作为工具的输出格式告知模型。

当模型决定调用此工具时,它会期望得到一个符合该 Schema 的 JSON 对象。

  1. 在 AgentRagConfig 中注册工具

已经在之前的 AgentRagConfig 中通过 defaultTools(weatherTool) 注册,无需修改。

复制代码
package com.badao.ai.config;

import com.badao.ai.tools.WeatherTool;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.vectorstore.QuestionAnswerAdvisor;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AgentRagConfig {

    @Bean
    public ChatClient chatClient(ChatModel chatModel, VectorStore vectorStore,
                                 WeatherTool weatherTool) {
        return ChatClient.builder(chatModel)
                .defaultAdvisors(
                        QuestionAnswerAdvisor.builder(vectorStore)
                                .searchRequest(SearchRequest.builder()
                                        .similarityThreshold(0.7)
                                        .topK(3)
                                        .build())
                                .build()
                )
                .defaultTools(weatherTool)  // 注册@Tool工具
                .build();
    }
}
  1. 在 Graph Agent 中使用结构化的工具返回

在 AgentGraphConfig 的 execute 方法中,我们原本拿到的是 String,现在工具返回的是 WeatherInfo,需要稍作调整。

但由于我们手动调用 weatherTool.getWeather(city) 而不是通过 ChatClient,直接就能获得对象。

复制代码
package com.badao.ai.config;

import com.badao.ai.output.WeatherInfo;
import com.badao.ai.tools.WeatherTool;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.vectorstore.QuestionAnswerAdvisor;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.document.Document;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.List;
import java.util.stream.Collectors;

@Configuration
public class AgentGraphConfig {

    private static final Logger log = LoggerFactory.getLogger(AgentGraphConfig.class);

    @Bean("agentChatClient")
    public ChatClient agentChatClient(ChatModel chatModel, VectorStore vectorStore) {
        return ChatClient.builder(chatModel)
                .defaultAdvisors(
                        QuestionAnswerAdvisor.builder(vectorStore)
                                .searchRequest(SearchRequest.builder()
                                        .similarityThreshold(0.7)
                                        .topK(3)
                                        .build())
                                .build()
                )
                .build();
    }

    @Bean
    public AgentWorkflow agentWorkflow(VectorStore vectorStore,
                                       @Qualifier("agentChatClient") ChatClient chatClient,
                                       WeatherTool weatherTool) {
        return new AgentWorkflow(vectorStore, chatClient, weatherTool);
    }

    public static class AgentWorkflow {
        private final VectorStore vectorStore;
        private final ChatClient chatClient;
        private final WeatherTool weatherTool;

        public AgentWorkflow(VectorStore vectorStore, ChatClient chatClient, WeatherTool weatherTool) {
            this.vectorStore = vectorStore;
            this.chatClient = chatClient;
            this.weatherTool = weatherTool;
        }

        public String execute(String query) {
            // 1. 检索
            // 使用 Builder 模式正确构建 SearchRequest
            List<Document> docs = vectorStore.similaritySearch(
                    SearchRequest.builder()
                            .query(query)                 // 设置查询文本(公开方法)
                            .similarityThreshold(0.7)
                            .topK(3)
                            .build()
            );
            System.out.println("检索到文档数量;"+docs.size());
            log.info("🔍 检索到 {} 篇相关文档", docs.size());

            // 2. 条件调用工具
            String toolResult = null;
            if (docs.isEmpty()) {
                log.info("调用天气工具...");
                String city = extractCity(query);
                WeatherInfo info = weatherTool.getWeather(city);  // 返回结构化对象
                toolResult = String.format("城市:%s,天气:%s,温度:%d℃",
                        info.getCity(), info.getWeather(), info.getTemperature());
            }

            // 3. 生成答案
            String context = docs.stream()
                    .map(Document::getFormattedContent)
                    .collect(Collectors.joining("\n"));
            String prompt = buildPrompt(query, context, toolResult);
            return chatClient.prompt().user(prompt).call().content();
        }

        private String extractCity(String query) {
            if (query.contains("北京")) return "北京";
            if (query.contains("上海")) return "上海";
            if (query.contains("青岛")) return "青岛";
            return "未知城市";
        }

        private String buildPrompt(String query, String context, String toolResult) {
            StringBuilder sb = new StringBuilder();
            sb.append("请基于以下信息回答用户问题。\n");
            if (!context.isEmpty()) {
                sb.append("知识库相关信息:\n").append(context).append("\n");
            }
            if (toolResult != null) {
                sb.append("外部工具查询结果:").append(toolResult).append("\n");
            }
            sb.append("用户问题:").append(query);
            sb.append("\n请给出简洁专业的回答。");
            return sb.toString();
        }
    }
}

五、常见问题与注意事项

问题 原因 解决方案
entity() 调用后报 No suitable converter 模型返回的不是合法 JSON 或字段名不匹配 增加明确的格式指令,或改用 OutputParser
工具返回的结构化字段未被模型正确利用 模型可能忽略工具返回的描述 在提示词中强调"请严格按照工具返回的数据回答"
Ollama 模型不支持严格的 JSON Schema qwen2.5:7b 对 Schema 支持有限 使用 BeanOutputParser + 提示词约束,降级处理
字段嵌套复杂 简单 Bean 反序列化不够 使用 @JsonNaming 或自定义反序列化器
相关推荐
珠海西格电力2 小时前
零碳园区的能源成本优势具体体现在哪些方面
大数据·人工智能·算法·架构·能源
晚霞的不甘2 小时前
CANN 模型转换与适配:从 PyTorch 到 Ascend OM 的完整指南
人工智能·pytorch·python·深度学习
刘一说3 小时前
AI科技热点日报 | AI Tech Daily | 2026年5月20日 May 20, 2026
人工智能·科技
o丁二黄o3 小时前
Gemini镜像站办公效能深度解析:多模态链式调用与自动化工作流构建指南
运维·人工智能·自动化
Mr数据杨3 小时前
【CanMV K210】显示交互 OLED 128x64 智能状态面板设计
人工智能·交互·硬件开发·canmv k210
kyle~3 小时前
ros_gz_sim --- ROS 2 与 Gazebo 仿真的桥梁
人工智能·机器人·自动驾驶
灵机一物3 小时前
灵机一物AI原生电商小程序、PC端(已上线)-谷歌I/O 2026 All in AI:Gemini全系升级+搜索25年最大改版,AI落地进入规模化时代
人工智能
南屹川3 小时前
【容器化】Docker Compose完全指南:从入门到实战
人工智能
闵孚龙3 小时前
MSE AI任务调度爆火:Agent定时任务、Dify、OpenClaw、Hermes、百炼统一调度,如何把AI助手变成自动干活的数字员工?
人工智能