前言

Github:https://github.com/HealerJean

博客:http://blog.healerjean.com

一、提示

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

Spring AI 定义 4 种核心角色,角色顺序会影响 AI 模型的响应逻辑(建议按SYSTEM→USER→ASSISTANT→TOOL顺序排列):

角色 作用说明 适用场景
SYSTEM 定义 AI 的行为、响应风格、规则,是全局指令 设定 AI 身份(如 “你是 Java 架构师”)、输出格式
USER 代表用户的问题 / 指令,是 AI 响应的核心依据 用户的实际查询、需求
ASSISTANT AI 的历史响应,用于保持对话上下文连贯;可包含工具调用请求 多轮对话、上下文关联查询
TOOL 工具 / 函数的执行结果,响应ASSISTANT的工具调用请求 调用外部接口(如天气、数据库)后返回结果

c、常用实现类

  • SystemMessage/UserMessage/AssistantMessage:对应三大核心角色的基础实现;

  • ToolResponseMessageTOOL 角色的实现,封装工具调用结果;

  • 多模态: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、温度、模型名称)
}

关键特性

  1. 消息列表是有序的,AI 模型会按顺序解析上下文;
  2. ChatOptions是可选的,用于覆盖模型的默认配置(如设置maxTokens=1024temperature=0.7)。

3)提示词模板:提示词的动态化工具

解决硬编码提示词的问题,支持动态占位符渲染、自定义分隔符、资源文件加载,核心是通过TemplateRenderer完成模板与参数的绑定。

a、核心渲染器

渲染器实现类 作用说明 适用场景
StTemplateRenderer 默认实现,基于 StringTemplate 引擎,默认分隔符{} 大部分常规场景
NoOpTemplateRenderer 空实现,不做渲染,直接返回原模板 模板无动态占位符的场景
自定义 Renderer 实现TemplateRenderer接口,自定义渲染逻辑 特殊语法(如 JSON 占位符)、复杂渲染规则

b、核心接口能力

PromptTemplate实现了三大接口,覆盖字符串渲染、消息创建、Prompt 生成全流程:

  1. PromptTemplateStringActions:渲染为字符串(render()/render(Map));
  2. PromptTemplateMessageActions:创建 Message 对象(createMessage());
  3. PromptTemplateActions:生成最终的 Prompt 对象(create()/create(Map, ChatOptions))。

4)核心交互流程

  1. 构建 PromptTemplate(模板字符串/资源文件)
  2. 绑定动态参数渲染模板

  3. 生成 Message/Prompt 对象(指定角色/模型配置
  4. 调用 ChatModel.call(Prompt)
  5. 解析 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 体系对齐:继承 SpringConverter接口,符合 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>>等复杂泛型的转换;

  • DefaultConversionServiceSpring 默认转换服务,为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% 以上:

  1. 降低 temperature:配置temperature: 0.1~0.3,减少 AI 的随机性,强制按规则输出;
  2. 明确格式指令:在 system 消息中强调 “无多余解释、纯 JSON / 纯列表、严格按数量返回”
  3. 开启模型原生结构化配置:如 OpenAIresponse-format: json_schema,让模型原生支持结构化输出;
  4. 添加示例:在 Prompt 中添加目标格式的示例,如示例:{"city":"北京","temperature":"18℃"}
  5. 使用短 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;
}

ContactAuthor