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 或自定义反序列化器
相关推荐
BerrySen178几秒前
KMP全栈开发:从Android到AI Agent的技术演进与实践
android·人工智能
战族狼魂6 分钟前
高频面试题精选:分治与AI Agent架构
人工智能·算法·大模型·大语言模型
zSD55rt5a1 小时前
方差在扩散模型保护中的作用
人工智能·harmonyos
Suhan421 小时前
IDEA 设置启动参数和环境变量
spring boot·spring·编辑器·idea
人生百态,人生如梦1 小时前
情感交互仿生人从技术到落地构想3——技术交流贴(2026.7)
人工智能·机器学习·人机交互·交互·具身智能
用户652238438111 小时前
为什么成熟的 LLM 应用,都把"后端地址"做成可切换的配置
人工智能
Aa99883341 小时前
AI视觉检测设备厂家的技术评估框架——以及AI视觉检测的检出边界与适用条件
人工智能
wuhanzhanhui1 小时前
当AI遇见储能!2026武汉国际数字能源产业与新型电力及储能技术展览会
人工智能·能源
Tangyuewei2 小时前
给 AI 套上缰绳:Harness Engineering 是什么
人工智能