场景
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 生成能力。
-
修改 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 对象。
- 在 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();
}
}
- 在 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 或自定义反序列化器 |
