
场景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 直接返回 POJO1、定义输出实体package com.badao.ai.output; public class WeatherInfo { private String city; private String weather; private int temperature; // 必须有无参构造器 getter/setterSpring 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 对象。2. 在 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(); } }3. 在 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 ListDocument 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 Schemaqwen2.5:7b 对 Schema 支持有限使用BeanOutputParser 提示词约束降级处理字段嵌套复杂简单 Bean 反序列化不够使用JsonNaming或自定义反序列化器