Spring AI(3)- Prompt Template - 今日运势
目录
什么是 Prompt Template?#
Prompt Template 允许开发者定义一个包含占位符 (placeholders) 的结构化字符串。运行时,这些占位符会被实际的数据(如用户输入、数据库内容、应用上下文等)填充,从而生成一个完整、复杂且动态的提示,发送给 LLM 模型。
为什么使用 Prompt Template?#
使用 Prompt Template 有助于:
- 提高一致性:确保所有生成的提示遵循相同的格式和结构。
- 简化复杂提示的创建:通过模板化的方式,轻松创建复杂的提示,而无需手动拼接字符串。
- 增强可维护性:当需要修改提示结构时,只需更新模板,而无需更改所有使用该提示的代码
Prompt Engineering#
提示工程 (Prompt Engineering)有专门的指南, 如https://www.promptingguide.ai/,来帮助用户掌握prompt的设计技巧。
基本上任何AI入门书籍都会提到prompt设计的重要性。一个好的prompt可以显著提升AI生成内容的质量。
Spring AI 中的 Prompt Template#

可以让用户来输入自己的生日,然后通过 Prompt Template 来生成一个完整的算运势的prompt,并发送给AI模型进行处理。
import java.time.LocalDate;
import java.util.Objects;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.stereotype.Service;
@Service
public class SpringAiFortuneTodayService implements FortuneTodayService {
private final ChatClient chatClient;
private static final String TEMPLATE = """
你是一位专业的星座和生肖运势分析师。请根据以下信息生成今日运势:
用户姓名:{name}
出生日期:{birthday}
请按以下格式输出:
1. **基本信息**
- 姓名:{name}
- 星座:[根据出生日期计算]
- 生肖:[根据出生年份计算]
2. **今日综合运势** ( 今天是{today_date} )
- 整体运势:[1-5颗星,用⭐表示]
- 运势简评:[一句话概括今日运势]
3. **详细运势分析**
- 爱情运势:[50-80字的详细分析]
- 事业运势:[50-80字的详细分析]
- 财运:[50-80字的详细分析]
- 健康运势:[50-80字的详细分析]
4. **幸运元素**
- 幸运颜色:
- 幸运数字:
- 幸运方位:
5. **今日建议**
[给出2-3条针对性的建议]
请用温暖、积极的语气,让运势分析既专业又富有启发性。
""";
public SpringAiFortuneTodayService(ChatClient.Builder chatClientBuilder) {
this.chatClient = chatClientBuilder.build();
}
@Override
public FortuneTodayResponse getFortuneToday(UserInfo userInfo) {
var fortuneText = chatClient.prompt()
.user(userSpec -> userSpec.text(TEMPLATE).param("name", userInfo.fullName())
.param("birthday", userInfo.birthDate()).param("today_date", Objects.requireNonNull(String.valueOf(LocalDate.now()))))
.call()
.content();
return new FortuneTodayResponse(fortuneText);
}
}
在上面的代码中,我们定义了一个包含占位符的Prompt模板 (TEMPLATE)。在运行时,我们使用 chatClient.prompt().user(...) 方法来填充这些占位符,生成最终的提示内容,并将其发送给AI模型进行处理。
这里的模版符合Prompt Engineering的一些最佳实践:
- 明确角色设定:指定AI模型扮演专业的星座和生肖运势分析师角色。
- 结构化输出:要求AI模型按照特定的格式输出结果,确保信息的完整性和可读性。
- 具体细节:提供了详细的运势分析要求,确保生成内容的深度和广度。
- 积极语气:要求使用温暖、积极的语气,使内容更具吸引力。
- 动态参数:通过占位符动态传递用户信息和当前日期,使提示更加个性化。
- 清晰指令:明确告诉AI模型需要生成哪些具体内容,避免模糊不清的回答。
- 任务分解:将复杂的任务分解为多个具体部分,帮助AI模型更好地组织回答内容。
- 上下文提供:通过提供用户的姓名和出生日期,帮助AI模型生成更相关的内容。
总结: 这个模版是一个典型的 “指令+结构+约束+上下文+风格”的组合。

优化#
外部化 Prompt Template#
如果Prompt template的内容比较多,还可以将其放到一个单独的文件中,比如fortune_today_prompt.txt,然后通过Spring的资源加载机制来加载这个文件的内容。
private final ChatClient chatClient;
private final @NonNull Resource templateResource;
public SpringAiFortuneTodayService(ChatClient.Builder chatClientBuilder,
@Value("classpath:/promptTemplates/fortune_today.st") Resource templateResource) {
this.chatClient = chatClientBuilder.build();
this.templateResource = Objects.requireNonNull(templateResource, "templateResource must not be null");
}
@Override
public FortuneTodayResponse getFortuneToday(UserInfo userInfo) {
var fortuneText = chatClient.prompt()
.user(userSpec -> userSpec.text(templateResource).param("name", userInfo.fullName())
.param("birthday", userInfo.birthDate()).param("today_date", Objects.requireNonNull(String.valueOf(LocalDate.now()))))
.call()
.content();
return new FortuneTodayResponse(fortuneText);
}
使用context来避免幻觉(hallucinations)#
上面的执行结果中,有个问题,1990-05-15这个生日信息,应该是金牛座,而不是天秤座。本地的大模型mistral:7b不清楚什么原因出现了幻觉。
ollama run mistral:7b
>>> 1990年5月15日出生的人是什么星座?
根据天文学计算,1990年5月15日是白羊座
处理幻觉的方式有几种,包括
- 训练你自己的模型
- 对已有的模型进行微调(fine-tuning)
- 在prompt中提供附加的上下文(context)
虽然训练或微调模型可以说是避免幻觉的最佳方法(更不用说它们允许你基于专有信息创建模型),但它们很困难,需要数据科学领域的技能,而不是软件开发领域的技能。此外,为了正确地完成训练和微调,需要大量的训练数据。它们也非常耗时,可能需要几个小时、几天甚至几周。
相比之下,在prompt中添加一些上下文与添加各种条件限制本身没有太大区别,它是在提交prompt时即时发生的。因此,它比训练和微调模型要简单得多。
针对上面的星座错误的问题,我门不要让 AI 去算星座(它不擅长数学和日期逻辑),直接在 Java 里算好,把星座作为参数传给 AI。
public class ZodiacUtils {
private ZodiacUtils() {
throw new IllegalStateException("Utility class");
}
public static String getZodiacSign(LocalDate date) {
MonthDay md = MonthDay.from(date);
Object[][] zodiacSigns = {
{MonthDay.of(3, 21), MonthDay.of(4, 19), "白羊座"},
{MonthDay.of(4, 20), MonthDay.of(5, 20), "金牛座"},
{MonthDay.of(5, 21), MonthDay.of(6, 20), "双子座"},
{MonthDay.of(6, 21), MonthDay.of(7, 22), "巨蟹座"},
{MonthDay.of(7, 23), MonthDay.of(8, 22), "狮子座"},
{MonthDay.of(8, 23), MonthDay.of(9, 22), "处女座"},
{MonthDay.of(9, 23), MonthDay.of(10, 22), "天秤座"},
{MonthDay.of(10, 23), MonthDay.of(11, 21), "天蝎座"},
{MonthDay.of(11, 22), MonthDay.of(12, 21), "射手座"},
{MonthDay.of(12, 22), MonthDay.of(1, 19), "摩羯座"},
{MonthDay.of(1, 20), MonthDay.of(2, 18), "水瓶座"},
{MonthDay.of(2, 19), MonthDay.of(3, 20), "双鱼座"}
};
for (Object[] zodiac : zodiacSigns) {
MonthDay start = (MonthDay) zodiac[0];
MonthDay end = (MonthDay) zodiac[1];
String sign = (String) zodiac[2];
if ((md.isAfter(start) || md.equals(start)) && (md.isBefore(end) || md.equals(end))) {
return sign;
}
}
return "";
}
}
@Override
public FortuneTodayResponse getFortuneToday(UserInfo userInfo) {
// 解析日期
LocalDate birthDate = LocalDate.parse(userInfo.birthDate());
// 计算星座
String zodiacSign = Objects.requireNonNull(ZodiacUtils.getZodiacSign(birthDate), "zodiacSign must not be null");
var fortuneText = chatClient.prompt()
.user(userSpec -> userSpec.text(templateResource)
.param("name", userInfo.fullName())
.param("birthday", userInfo.birthDate())
.param("zodiac", zodiacSign)// 传入计算好的星座
.param("today_date", Objects.requireNonNull(String.valueOf(LocalDate.now()))))
.call()
.content();
return new FortuneTodayResponse(fortuneText);
}

认真看的话,生肖也有一样的问题,正确的生肖应该属马。可以用类似的方法来解决。
Prompt消息角色#
许多 LLM,包括来自 OpenAI、MistralAI 和 Anthropic 的模型,都支持将一个提示拆分成多条消息,每条消息都属于一个特定的角色。常用的角色包括:
- User (用户): 消息包含由应用程序用户(或代表用户)提出的问题或陈述。
- System (系统): 消息包含由应用程序本身提供给 LLM 的指令。
- Assistant (助手): 消息包含来自 LLM 的回复。
- Tool (工具): 消息包含调用工具执行某些操作或获取额外上下文信息的指令。
| 角色 (Role) | 目的/用途 | 谁发送 (Sender) |
|---|---|---|
| System | 设置模型身份、行为、约束和提供全局上下文(如规则)。 | 应用程序/开发者 |
| User | 提出实际问题、发起对话或提供数据。 | 最终用户 |
| Assistant | LLM 的历史回复。用于多轮对话中保持上下文。 | LLM/AI 模型 |
| Tool | 用于函数调用(Function Calling)或 RAG 场景中,提供工具的输出或检索到的数据。 | 应用程序/工具 |
在 Spring AI 中,可以使用 chatClient.prompt().user(...)、chatClient.prompt().system(...) 和 chatClient.prompt().assistant(...) 方法来创建不同角色的消息。
interface ChatClientRequestSpec {
/**
* Return a {@link ChatClient.Builder} to create a new {@link ChatClient} whose
* settings are replicated from this {@link ChatClientRequest}.
*/
Builder mutate();
ChatClientRequestSpec advisors(Consumer<AdvisorSpec> consumer);
ChatClientRequestSpec advisors(Advisor... advisors);
ChatClientRequestSpec advisors(List<Advisor> advisors);
ChatClientRequestSpec messages(Message... messages);
ChatClientRequestSpec messages(List<Message> messages);
<T extends ChatOptions> ChatClientRequestSpec options(T options);
ChatClientRequestSpec toolNames(String... toolNames);
ChatClientRequestSpec tools(Object... toolObjects);
ChatClientRequestSpec toolCallbacks(ToolCallback... toolCallbacks);
ChatClientRequestSpec toolCallbacks(List<ToolCallback> toolCallbacks);
ChatClientRequestSpec toolCallbacks(ToolCallbackProvider... toolCallbackProviders);
ChatClientRequestSpec toolContext(Map<String, Object> toolContext);
ChatClientRequestSpec system(String text);
ChatClientRequestSpec system(Resource textResource, Charset charset);
ChatClientRequestSpec system(Resource text);
ChatClientRequestSpec system(Consumer<PromptSystemSpec> consumer);
ChatClientRequestSpec user(String text);
ChatClientRequestSpec user(Resource text, Charset charset);
ChatClientRequestSpec user(Resource text);
ChatClientRequestSpec user(Consumer<PromptUserSpec> consumer);
ChatClientRequestSpec templateRenderer(TemplateRenderer templateRenderer);
CallResponseSpec call();
StreamResponseSpec stream();
}
注意: 并非所有 LLM API 都支持相同的消息角色。当 API 不支持 System 角色时,Spring AI 会简单地将原本用于 System 角色的文本添加到 User 消息中。