SpringAi_3_提示和结构化
前言
Github:https://github.com/HealerJean
一、提示
1、核心概念梳理
Spring AI 中的 Prompt 是与 AI 模型交互的核心载体,构建一个提示词不仅仅是拼接字符串,而是构建一个包含消息集合和模型配置的结构化对象。
1)核心组件关系图
| 组件 | 作用 | 类比 (Spring 生态) |
|---|---|---|
| Prompt | 请求的顶层容器,包含消息列表和配置选项 (ChatOptions)。 |
类似于 HTTP Request |
| Message | 对话的基本单元,包含内容和角色(System/User/Assistant/Tool)。 | 类似于 Request Body |
| PromptTemplate | 带有占位符的模板,用于生成动态消息。 | 类似于 Thymeleaf 模板 |
| ChatModel | 调用大模型的客户端接口。 | 类似于 RestTemplate / JDBC |
2)消息角色
Spring AI 定义了四种关键角色,这是构建高质量对话的基础:
SYSTEM(系统):设定 AI 的行为准则、角色和风格。这是“后台指令”,用户通常不可见。USER(用户):用户的实际输入、问题或命令。ASSISTANT(助手):AI 的响应。在多轮对话中,需要将历史的 Assistant 消息传回,以维持上下文。TOOL(工具):用于响应工具调用(Function Calling)返回的数据。
2、核心 API 深度解析
Spring AI Prompt相关 API 围绕消息(Message)、提示(Prompt)、模板(PromptTemplate) 三大核心展开,以下是官方文档的核心API拆解 + 细节补充。
1)消息(Message)
消息是
Prompt的核心元素,每个消息都有角色、内容、元数据,部分消息支持多模态媒体内容(如图像、音频)。
a、核心接口
// 基础内容接口:所有消息的父接口
public interface Content {
String getContent(); // 文本内容
Map<String, Object> getMetadata(); // 元数据(如token数、消息ID)
}
// 消息核心接口:增加角色分类
public interface Message extends Content {
MessageType getMessageType(); // 消息角色
}
// 多模态扩展接口:支持图片/音频等媒体内容
public interface MediaContent extends Content {
Collection<Media> getMedia(); // 媒体内容集合
}
b、核心角色(MessageType)
SpringAI定义 4 种核心角色,角色顺序会影响 AI 模型的响应逻辑(建议按SYSTEM→USER→ASSISTANT→TOOL顺序排列):
| 角色 | 作用说明 | 适用场景 |
|---|---|---|
SYSTEM |
定义 AI 的行为、响应风格、规则,是全局指令 | 设定 AI 身份(如 “你是 Java 架构师”)、输出格式 |
USER |
代表用户的问题 / 指令,是 AI 响应的核心依据 | 用户的实际查询、需求 |
ASSISTANT |
AI 的历史响应,用于保持对话上下文连贯;可包含工具调用请求 | 多轮对话、上下文关联查询 |
TOOL |
工具 / 函数的执行结果,响应ASSISTANT的工具调用请求 |
调用外部接口(如天气、数据库)后返回结果 |
c、常用实现类
-
SystemMessage/UserMessage/AssistantMessage:对应三大核心角色的基础实现; -
ToolResponseMessage:TOOL角色的实现,封装工具调用结果; -
多模态:
UserMessage支持媒体内容(media字段),是唯一支持多模态的消息类型Spring Framework
2)提示核心(Prompt):消息集合 + 模型配置
Prompt 是传递给
ChatModel的最终对象,本质是有序的 Message 列表 + ChatOptions 模型配置,核心源码:
public class Prompt implements ModelRequest<List<Message>> {
private final List<Message> messages; // 有序消息列表,顺序影响AI响应
private ChatOptions chatOptions; // 模型配置(如最大token、温度、模型名称)
}
关键特性:
- 消息列表是有序的,AI 模型会按顺序解析上下文;
ChatOptions是可选的,用于覆盖模型的默认配置(如设置maxTokens=1024、temperature=0.7)。
3)提示词模板:提示词的动态化工具
解决硬编码提示词的问题,支持动态占位符渲染、自定义分隔符、资源文件加载,核心是通过TemplateRenderer完成模板与参数的绑定。
a、核心渲染器
| 渲染器实现类 | 作用说明 | 适用场景 |
|---|---|---|
StTemplateRenderer |
默认实现,基于 StringTemplate 引擎,默认分隔符{} |
大部分常规场景 |
NoOpTemplateRenderer |
空实现,不做渲染,直接返回原模板 | 模板无动态占位符的场景 |
自定义 Renderer |
实现TemplateRenderer接口,自定义渲染逻辑 |
特殊语法(如 JSON 占位符)、复杂渲染规则 |
b、核心接口能力
PromptTemplate实现了三大接口,覆盖字符串渲染、消息创建、Prompt 生成全流程:
PromptTemplateStringActions:渲染为字符串(render()/render(Map));PromptTemplateMessageActions:创建Message对象(createMessage());PromptTemplateActions:生成最终的Prompt对象(create()/create(Map, ChatOptions))。
4)核心交互流程
- 构建
PromptTemplate(模板字符串/资源文件) -
绑定动态参数渲染模板
- 生成
Message/Prompt对象(指定角色/模型配置 - 调用
ChatModel.call(Prompt) - 解析
ChatResponse获取AI响应
3、实战案例
1)基础动态提示词
/**
* 案例 1:基础动态提示词
* <p>
* 场景描述:根据用户输入的形容词和话题,让 AI 生成一个笑话
* 接口地址:GET /ai/prompt/joke?adjective=幽默的&topic=程序员
*
* @param adjective 形容词(例如:幽默的、讽刺的)
* @param topic 话题(例如:程序员、Java)
* @return AI 生成的笑话内容
*/
@GetMapping("/joke")
public String generateJoke(@RequestParam String adjective, @RequestParam String topic) {
// 1. 定义提示词模板:使用 {} 作为占位符
String templateStr = "给我讲一个关于 {topic} 的 {adjective} 笑话";
// 2. 创建模板对象
PromptTemplate promptTemplate = new PromptTemplate(templateStr);
// 3. 准备参数模型
Map<String, Object> model = Map.of("adjective", adjective, "topic", topic);
// 4. 渲染模板并构建 Prompt
Prompt prompt = promptTemplate.create(model);
// 5. 调用 AI 模型并返回内容
return chatClient.prompt(prompt).call().content();
}
2)多角色对话与系统指令
/**
* 案例 2:多角色对话与系统指令
* <p>
* 场景描述:通过系统提示词设定 AI 的角色(如海盗风格),并让其以特定名字回复用户
* 接口地址:GET /ai/prompt/chat?userName=杰克&userQuestion=宝藏在哪里
*
* @param userName 用户的名字(用于 AI 称呼用户)
* @param userQuestion 用户的问题
* @return AI 以特定角色风格回复的内容
*/
@GetMapping("/chat")
public String roleplayChat(@RequestParam String userName, @RequestParam String userQuestion) {
// 1. 构建用户消息
Message userMessage = new UserMessage(userQuestion);
// 2. 构建系统消息:定义 AI 的角色、名字和说话风格
String systemTemplate = """
你是一个乐于助人的 AI 助手。
你的名字是 {name}。
你应该用 {voice} 的风格回复用户的请求,并在回复中提到你的名字。
""";
SystemPromptTemplate systemPromptTemplate = new SystemPromptTemplate(systemTemplate);
Message systemMessage = systemPromptTemplate.createMessage(
Map.of("name", userName, "voice", "海盗")
);
// 3. 组合提示词:系统消息在前,用户消息在后
Prompt prompt = new Prompt(List.of(systemMessage, userMessage));
// 4. 调用 AI 模型
ChatResponse response = chatClient.prompt(prompt).call().chatResponse();
// 5. 提取并返回文本内容
return response.getResult().getOutput().getText();
}
二、结构化输出
1、核心概念
1)为什么需要它
- 旧痛点:以前 AI 返回
"用户ID: 123, 姓名: 张三",你需要写正则表达式去提取,一旦 AI 发挥失常,代码就报错。 - 新方案:现在 AI 返回
{"userId": 123, "name": "张三"},直接映射到UserDTO对象,安全且稳定。
2)核心工作流程
结构化输出转换器在 AI 调用前和后均发挥作用,形成完整的结构化输出链路:
-
调用前:通过
FormatProvider生成格式指令(如JSON Schema、输出规则),附加到Prompt中,指导AI模型按指定格式生成 -
调用后:通过
Converter<String, T>将 AI 返回的文本输出,转换为目标结构化类型(Bean/Map/List)。
3)核心特性
-
模型无关:一套代码兼容 OpenAI、Claude 3、Ollama、Mistral AI 等主流模型;
-
尽力而为转换:AI 不保证 100% 按格式输出,需自行实现验证机制;
-
非工具调用专用:工具调用默认返回结构化输出,无需使用此转换器;
-
Spring 体系对齐:继承 Spring
Converter接口,符合 Spring 开发习惯。
2、核心 API 深度解析
1)顶层核心接口
StructuredOutputConverter<T>是所有转换器的顶层接口,结合了格式提供和文本转换两大核心能力,源码如下:
-
Converter<String, T>:Spring核心接口,实现convert(String source)方法,将 AI 文本输出转为 T 类型; -
FormatProvider:为 AI 模型提供明确的格式指导,避免 AI 返回非结构化文本。
// 泛型T为目标结构化类型
public interface StructuredOutputConverter<T> extends Converter<String, T>, FormatProvider {
}
// 提供AI模型的格式指令(如JSON Schema、输出规则)
public interface FormatProvider {
String getFormat(); // 返回格式指令字符串
}
2)内置转换器实现
Spring AI提供 3 种常用的具体转换器,覆盖 99% 的业务场景,均继承自抽象基类,预配置了转换服务 / 消息转换器:
| 转换器类 | 父类 | 目标类型 | 核心功能 | 格式指令特点 |
|---|---|---|---|---|
BeanOutputConverter<T>默认 |
AbstractMessageOutputConverter |
自定义 Java Bean/Record | 基于 JSON Schema 指导 AI 生成 JSON,通过 ObjectMapper 反序列化为目标 Bean/Record | 生成符合 DRAFT_2020_12 的 JSON Schema |
MapOutputConverter |
AbstractMessageOutputConverter |
Map<String, Object> |
指导 AI 生成 RFC8259 标准 JSON,转换为 Map 对象 | 要求返回纯 JSON,无多余解释 |
ListOutputConverter |
AbstractConversionServiceOutputConverter |
List<?> |
指导 AI 生成逗号分隔的列表文本,转换为 List 对象 | 要求返回纯列表,无序号 / 多余描述 |
3)关键辅助类
-
ParameterizedTypeReference:解决泛型类型擦除问题,支持List<Bean>、Map<String, List<Bean>>等复杂泛型的转换; -
DefaultConversionService:Spring默认转换服务,为ListOutputConverter提供基础类型转换能力。
3、实战案例
1)案例 1:BeanOutputConverter - 基础 Java Bean 转换
/**
* 案例1:基础Bean转换 - 获取城市天气信息
* @param city 城市名称
* @return 结构化WeatherInfo对象
*/
@GetMapping("/ai/weather")
public WeatherInfo getWeather(@RequestParam String city) {
// ChatClient自 动使用 BeanOutputConverter,无需手动创建
return chatClient.prompt()
.system("你是专业的天气查询助手,严格按指定JSON格式返回天气信息,无多余解释")
.user(u -> u.text("请查询{city}的当前天气信息,包含温度、天气状况、风向风力、出行建议")
.param("city", city))
.call()
.entity(WeatherInfo.class); // 直接转换为目标Bean
}
@Data
public class WeatherInfoVO {
/**
* 城市名称
*/
private String city;
/**
* 温度
*/
private String temperature;
/**
* 天气状况(晴/雨/多云)
*/
private String weather;
/**
* 风向风力
*/
private String wind;
/**
* 出行建议
*/
private String tips;
}
GET http://localhost:8080/ai/weather?city=北京
{
"city": "北京",
"temperature": "18℃",
"weather": "晴",
"wind": "北风3级",
"tips": "天气晴朗,适合外出,注意做好防晒"
}
2)案例 2:BeanOutputConverter - 泛型 List Bean 转换
场景:让 AI 返回指定数量的热门旅游城市信息,转换为List<CityInfo>,解决泛型类型擦除问题,适用于集合对象的结构化输出。
/**
* 案例2:泛型List Bean转换 - 获取热门旅游城市
*
* @param count 城市数量
* @return 结构化List<CityInfo>对象
*/
@GetMapping("/ai/travel/cities")
public List<CityInfoVO> getTravelCities(@RequestParam(defaultValue = "3") Integer count) {
// 使用ParameterizedTypeReference解决泛型擦除
ParameterizedTypeReference<List<CityInfoVO>> typeRef = new ParameterizedTypeReference<List<CityInfoVO>>() {
};
return chatClient.prompt()
.system("你是专业的旅游顾问,严格按指定JSON格式返回旅游城市信息,无多余解释,返回数量严格为{count}个")
.user(u -> u.text("推荐{count}个国内热门旅游城市,包含所属省份、著名景点、最佳旅游季节")
.param("count", count))
.call()
.entity(typeRef);
}
GET http://localhost:8080/ai/travel/cities?count=2
[
{
"cityName": "北京",
"province": "北京市",
"famousScenic": "故宫、长城",
"travelSeason": "春秋季(4月-10月)"
},
{
"cityName": "杭州",
"province": "浙江省",
"famousScenic": "西湖、灵隐寺",
"travelSeason": "春季(3月-5月)"
}
]
3)案例3:MapOutputConverter - Map 类型转换
场景:无需定义自定义 Bean,直接将 AI 输出转换为Map<String, Object>,适用于临时 / 动态数据场景,无需创建实体类。
/**
* 案例3:MapOutputConverter - 动态Map转换 - 获取水果营养信息
* @param fruit 水果名称
* @return 结构化Map<String, Object>对象
*/
@GetMapping("/ai/fruit/nutrition")
public Map<String, Object> getFruitNutrition(@RequestParam String fruit) {
ParameterizedTypeReference<Map<String, Object>> typeRef = new ParameterizedTypeReference<Map<String, Object>>() {};
return chatClient.prompt()
.system("你是专业的营养师,严格按RFC8259标准返回JSON格式的水果营养信息,无多余解释,key为营养成分,value为含量/作用")
.user(u -> u.text("请介绍{fruit}的主要营养成分和含量").param("fruit", fruit))
.call()
.entity(typeRef); // 转换为Map对象
}
GET http://localhost:8080/ai/fruit/nutrition?fruit=苹果
{
"carbohydrates": "约10g per 100g,提供能量",
"dietary_fiber": "约2.4g per 100g,促进消化健康",
"vitamin_c": "约8.4mg per 100g,增强免疫力",
"potassium": "约107mg per 100g,维持电解质平衡",
"polyphenols": "约450mg per 100g,抗氧化作用",
"malic_acid": "约0.3g per 100g,调节代谢功能",
"fructose": "约6.5g per 100g,天然果糖来源"
}
4)案例 4:ListOutputConverter - List 类型转换
场景:让 AI 返回纯列表数据(如成语、名言、菜品),转换为List<String>,适用于简单列表场景。
/**
* 案例4:ListOutputConverter - 简单List转换 - 获取成语列表
* @param count 成语数量
* @param theme 成语主题
* @return 结构化List<String>对象
*/
@GetMapping("/ai/idiom/list")
public List<String> getIdiomList(@RequestParam(defaultValue = "5") Integer count,
@RequestParam String theme) {
// 创建ListOutputConverter,使用Spring默认转换服务
ListOutputConverter listOutputConverter = new ListOutputConverter(new DefaultConversionService());
return chatClient.prompt()
.system("你是专业的语文老师,严格返回纯逗号分隔的列表,无序号、无多余解释,数量严格为{count}个")
.user(u -> u.text("推荐{count}个关于{theme}的成语").param("count", count).param("theme", theme))
.call()
.entity(listOutputConverter); // 转换为List<String>
}
GET http://localhost:8080/ai/idiom/list?theme=坚持
[
"持之以恒",
"锲而不舍",
"坚持不懈",
"百折不挠",
"水滴石穿"
]
5)手动创建转换器
官方推荐使用
ChatClient(高级 API),但部分场景需要手动控制转换流程,以下以BeanOutputConverter为例,实现低级 API(ChatModel) 的手动调用,适用于定制化格式指令、复杂 Prompt 构建场景。
/**
* 案例5:手动创建转换器
*
* @param city 城市名称
* @return 结构化WeatherInfo对象
*/
@GetMapping("/ai/manualWeather")
public WeatherInfoVO getManualWeather(@RequestParam String city) {
BeanOutputConverter<WeatherInfoVO> converter = new BeanOutputConverter<>(WeatherInfoVO.class);
// 2. 创建模板对象
PromptTemplate promptTemplate = new PromptTemplate("请查询{city}的当前天气信息,包含温度、天气状况、风向风力、出行建议");
// 3. 准备参数模型
Map<String, Object> model = Map.of("city", city);
// 4. 渲染模板并构建 Prompt
Prompt prompt = promptTemplate.create(model);
String context = chatClient.prompt(prompt).call().chatResponse().getResults().get(0).getOutput().getText();
return converter.convert(context);
}
4、高级用法与优化技巧
1)提升结构化输出准确性的核心技巧
AI 不保证 100% 按格式输出,通过以下方式可将准确性提升至 95% 以上:
- 降低
temperature:配置temperature: 0.1~0.3,减少 AI 的随机性,强制按规则输出; - 明确格式指令:在 system 消息中强调 “无多余解释、纯 JSON / 纯列表、严格按数量返回”;
- 开启模型原生结构化配置:如
OpenAI的response-format: json_schema,让模型原生支持结构化输出; - 添加示例:在 Prompt 中添加目标格式的示例,如
示例:{"city":"北京","temperature":"18℃"}; - 使用短 Prompt:精简 Prompt 内容,避免 AI 忽略格式指令。
2)自定义格式指令
默认的格式指令可满足大部分场景,若需定制,可继承抽象转换器重写
getFormat()方法:
import org.springframework.ai.converter.BeanOutputConverter;
/**
* 自定义格式指令的Bean转换器
*/
public class CustomBeanOutputConverter<T> extends BeanOutputConverter<T> {
public CustomBeanOutputConverter(Class<T> targetType) {
super(targetType);
}
// 重写格式指令,添加更严格的规则
@Override
public String getFormat() {
return super.getFormat() + "\n注意:1. 所有字段不能为空 2. 数值类型必须为数字 3. 无任何多余文字和注释";
}
}
3)结构化输出验证
实现后置验证,确保转换后的结构化数据符合业务规则,避免 AI 返回无效数据:
// 以WeatherInfo为例,添加验证逻辑
public void validateWeatherInfo(WeatherInfo weatherInfo) {
if (weatherInfo.getCity() == null || weatherInfo.getCity().isEmpty()) {
throw new IllegalArgumentException("城市名称不能为空");
}
if (weatherInfo.getTemperature() == null || !weatherInfo.getTemperature().contains("℃")) {
throw new IllegalArgumentException("温度格式无效,必须包含℃");
}
// 其他业务规则验证
}
// 在Controller中调用
@GetMapping("/ai/weather")
public WeatherInfo getWeather(@RequestParam String city) {
WeatherInfo weatherInfo = chatClient.prompt()...entity(WeatherInfo.class);
validateWeatherInfo(weatherInfo); // 验证结构化数据
return weatherInfo;
}


