本文覆盖 System Prompt 的设计方法、Spring AI 中三种 Prompt 管理方式、实用 Prompt 技巧,以及如何让大模型直接返回 Java 对象。面向正在用 Spring AI 做开发、想把 Prompt 写得更好并让 AI 输出可直接消费的结构化数据的 Java 开发者。
一、Prompt 工程:让 AI 听懂你的话
先搞清楚一件事------大模型的能力边界是固定的,但同一个模型,Prompt 写得好不好,输出质量差别非常大。Prompt Engineering 不是"锦上添花",而是拿到好结果的基本功。
1.1 System Prompt vs User Prompt
上一篇我们提到过消息结构里有 system、user、assistant 三种角色。在 Prompt 工程中最关键的就是前两个:
| 角色 | 定位 | 类比 |
|---|---|---|
| System Prompt | 全局行为设定,整个对话过程生效 | 员工入职培训手册 |
| User Prompt | 每次具体的任务请求 | 用户每次下达的具体指令 |
System Prompt 基本决定了一个 AI 应用的"调性"。同样的模型,有没有写好 System Prompt,效果差别很大。
1.2 好的 System Prompt 五要素
一个高质量的 System Prompt 通常包含五个要素:
| 要素 | 说明 | 示例 |
|---|---|---|
| 角色(Role) | 告诉模型"你是谁" | "你是一个专业的 Java 技术助手" |
| 任务(Task) | 你要做什么 | "回答技术问题、帮助 debug 代码" |
| 约束(Constraints) | 什么能做、什么不能做 | "不确定的内容要说明,不要编造" |
| 输出格式(Format) | 用什么格式返回 | "使用 Markdown 格式,代码用代码块包裹" |
| 示例(Examples) | 给一两个示例让模型参考 | Few-shot Prompting |
不是每次都要五个全写齐,但至少要有角色 + 任务 + 约束,输出质量就能上一个台阶。
1.3 Spring AI 中的三种 Prompt 管理方式
方式一:defaultSystem ------ 在 ChatClient 构建时设定
适合全局固定的系统提示词,整个 Service 的所有调用都共享同一个 System Prompt:
java
@Service
public class JavaTechService {
private final ChatClient chatClient;
public JavaTechService(ChatClient.Builder builder) {
this.chatClient = builder
.defaultSystem("""
你是一个专业的 Java 技术助手。
职责:
- 回答 Java、Spring Boot、Spring AI 相关的技术问题
- 帮助用户 debug 代码
- 提供最佳实践建议
规则:
- 代码示例使用 Java 17+ 语法
- 回答简洁,不要过度解释
- 不确定的内容要说明,不要编造
- 非技术问题礼貌拒绝
输出格式:
- 使用 Markdown 格式
- 代码用代码块包裹
""")
.build();
}
public String ask(String question) {
return chatClient.prompt()
.user(question)
.call()
.content();
}
}
这个例子把五要素中的四个都写进去了------角色、任务、约束、输出格式,写得清清楚楚。
方式二:动态 .system() ------ 每次调用动态设定
适合需要根据业务参数动态组装 System Prompt 的场景。注意 .param() 方法可以做变量替换:
java
@Service
public class InlineTemplateService {
private final ChatClient chatClient;
public InlineTemplateService(ChatClient.Builder builder) {
this.chatClient = builder.build();
}
public String ask(String role, String domain, String concept) {
return chatClient.prompt()
.system(s -> s.text("你是一个 {role},擅长 {domain} 领域的问题。")
.param("role", role)
.param("domain", domain))
.user(u -> u.text("请解释 {concept} 的工作原理")
.param("concept", concept))
.call()
.content();
}
}
调用时传入不同的 role 和 domain,同一个 Service 就能变成不同领域的专家。
方式三:外部模板文件 ------ .st 文件 + PromptTemplate
适合 Prompt 较长、需要版本管理、希望非开发人员也能修改的场景。Spring AI 使用 StringTemplate(.st 文件)作为模板引擎。
先看模板文件 prompts/code-review.st:
你是一个资深 {language} 工程师,有 {years} 年开发经验。
请 review 以下代码,找出:
1. Bug 和潜在风险
2. 性能优化点
3. 代码风格问题
代码:
{code}
请用中文回答,格式:每个问题独立列出,标注严重程度(高/中/低)。
然后在 Service 中加载模板:
java
@Service
public class CodeReviewService {
private final ChatClient chatClient;
public CodeReviewService(ChatClient.Builder builder) {
this.chatClient = builder.build();
}
public String review(String language, String code) {
PromptTemplate template = new PromptTemplate(
new ClassPathResource("prompts/code-review.st")
);
Prompt prompt = template.create(Map.of(
"language", language,
"years", "10",
"code", code
));
return chatClient.prompt(prompt)
.call()
.content();
}
}
也可以不用外部文件,直接在代码里用 PromptTemplate:
java
public String translate(String text, String targetLanguage) {
PromptTemplate template = new PromptTemplate("""
请将下面这段文字翻译成 {targetLanguage},
保持原文的语气和风格,不要意译:
{text}
""");
Prompt prompt = template.create(Map.of(
"targetLanguage", targetLanguage,
"text", text
));
return chatClient.prompt(prompt).call().content();
}
1.4 三种方式怎么选
| 方式 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
defaultSystem |
全局固定角色 | 简单,一处设定全局生效 | 不够灵活,改了要重启 |
动态 .system() |
角色/参数随请求变化 | 灵活,支持变量替换 | Prompt 散落在代码里 |
.st 模板文件 |
Prompt 较长、需要版本管理 | Prompt 和代码分离,好维护 | 多了一层文件管理 |
生产建议:简单场景用
defaultSystem,需要动态参数用.system()配合.param(),Prompt 较复杂或需要多人协作时用.st模板文件。
二、Prompt 实践技巧
掌握了基本用法,接下来看几个让 Prompt 效果显著提升的实践技巧。
2.1 角色扮演 vs 裸指令
对比两种写法:
裸指令:
"帮我 review 这段代码"
角色扮演:
"你是一个资深 Java 工程师,做 code review。
找出 Bug、性能问题、代码风格问题,每个问题注明严重程度(高/中/低)。"
效果差别很大。角色扮演让模型的注意力集中在对应领域的知识上,回答的专业度和针对性明显更好。
看实际代码中的做法:
java
public String codeReview(String language, String code) {
return chatClient.prompt()
.system(s -> s.text("""
你是一个资深 {language} 工程师,做 code review。
找出 Bug、性能问题、代码风格问题,每个问题注明严重程度(高/中/低)。
""")
.param("language", language))
.user(u -> u.text("请 review 这段代码:\n```\n{code}\n```")
.param("code", code))
.call()
.content();
}
2.2 "先思考再回答"------提升推理质量
对于需要推理的任务,在 Prompt 中加一句"请先分析再给出结论",效果会好不少。这就是 Chain-of-Thought(思维链)的简化用法。
差的写法:
"这段代码有什么问题?"
好的写法:
"请先逐行分析这段代码的执行逻辑,然后指出存在的问题。"
原理是:让模型在生成最终答案之前先生成推理过程,推理 Token 会影响后续 Token 的概率分布,相当于给了模型一个"打草稿"的机会。
2.3 知识库场景:教模型说"我不知道"
做企业知识库问答时,一个常见问题是模型在知识库里找不到答案时会"编"一个。解决方法很简单------在 System Prompt 里明确告诉模型:不确定就说不知道。
java
public String domainQA(String domain, String question) {
return chatClient.prompt()
.system(s -> s.text("""
你是一个 {domain} 领域的专家顾问。
只回答和 {domain} 相关的问题,其他问题拒绝回答。
不确定的内容要明确说明,不要编造。
""")
.param("domain", domain))
.user(question)
.call()
.content();
}
这三条约束缺一不可:限定领域、拒绝越界、禁止编造。
2.4 Prompt 版本管理建议
| 实践 | 说明 |
|---|---|
| 存文件,不硬编码 | 长 Prompt 用 .st 文件管理,改 Prompt 不需要改代码 |
| 有意义的命名 | code-review.st、translate.st,一看就知道干什么 |
| 纳入版本控制 | Prompt 文件放 resources/prompts/ 目录,跟代码一起进 Git |
| 分环境管理 | 开发环境可以输出详细推理过程,生产环境只要最终结果 |
三、结构化输出:让 AI 返回 Java 对象
3.1 为什么需要结构化输出
大模型默认返回的是自然语言文本------对人类友好,但对程序不友好。当 AI 的输出需要被下游系统消费(存数据库、调接口、做聚合展示)时,我们需要的是结构化数据,而不是一段文字。
// 我们不想要这个:
"这部电影是《星际穿越》,导演是克里斯托弗·诺兰,2014年上映,属于科幻片..."
// 我们想要这个:
{
"title": "星际穿越",
"director": "克里斯托弗·诺兰",
"year": 2014,
"genre": "科幻"
}
3.2 Spring AI 的结构化输出原理
Spring AI 的做法非常巧妙,整个过程是自动的:
① 分析你传入的 Java 类(Record / POJO)
② 根据字段名、类型、注解,自动生成 JSON Schema
③ 把 JSON Schema 附加到发送给模型的 Prompt 中
④ 模型按照 Schema 返回 JSON
⑤ Spring AI 自动反序列化为 Java 对象
开发者只需要做一件事:定义好 Java 类,调用 .entity(YourClass.class)。

3.3 基础用法:返回单个对象
最简单的例子------让 AI 推荐一部电影并返回结构化数据:
java
@RestController
@RequestMapping("/movie")
public class MovieController {
record MovieRecommendation(
String title,
String director,
int year,
String genre,
String reason
) {
}
private final ChatClient chatClient;
public MovieController(ChatClient.Builder builder) {
this.chatClient = builder.build();
}
@GetMapping("/recommend")
public MovieRecommendation recommend() {
return chatClient.prompt()
.user("推荐一部经典科幻电影")
.call()
.entity(MovieRecommendation.class);
}
}
关键就一行:.entity(MovieRecommendation.class)。Spring AI 会根据 Record 的字段名自动生成 JSON Schema,模型返回的 JSON 直接变成 Java 对象。
3.4 返回列表:ParameterizedTypeReference
当需要模型返回多个对象时,用 ParameterizedTypeReference 来处理泛型类型擦除的问题:
java
@RestController
@RequestMapping("/book")
public class BookController {
record BookSummary(String title, String author, String oneLinerSummary) {}
private final ChatClient chatClient;
public BookController(ChatClient.Builder builder) {
this.chatClient = builder.build();
}
@GetMapping("/list")
public List<BookSummary> list() {
return chatClient.prompt()
.user("列出 5 本经典的 Java 技术书籍")
.call()
.entity(new ParameterizedTypeReference<List<BookSummary>>() {});
}
}
注意:这里不能直接写
.entity(List.class),因为 Java 泛型擦除后 Spring AI 不知道 List 里装的是什么类型。必须用ParameterizedTypeReference来保留泛型信息。
3.5 用注解增强 Schema 描述
字段名有时不够表意,模型可能猜不准你想要什么。用 Jackson 注解可以给 Schema 加上更精确的描述:
java
record ProductReview(
@JsonProperty("product_name")
@JsonPropertyDescription("商品名称,从评论中提取")
String productName,
@JsonProperty("sentiment")
@JsonPropertyDescription("情感倾向:POSITIVE(正面)、NEGATIVE(负面)、NEUTRAL(中性)")
String sentiment,
@JsonProperty("score")
@JsonPropertyDescription("评分,1-5分,根据评论语气推断")
int score,
@JsonProperty("key_points")
@JsonPropertyDescription("评论中提到的关键点,最多3条")
List<String> keyPoints,
@JsonProperty("improvement_suggestions")
@JsonPropertyDescription("改进建议,如果没有则为空列表")
List<String> improvementSuggestions
) {
}
| 注解 | 作用 |
|---|---|
@JsonProperty |
指定 JSON 字段名(解决 Java 驼峰 vs JSON 下划线的问题) |
@JsonPropertyDescription |
给字段加描述,模型能"看懂"你要什么 |
@JsonPropertyDescription 的内容会被写进 JSON Schema,发送给模型。描述越清晰,模型输出越准确。
3.6 用 Enum 约束分类字段
当某个字段只能是有限的几个值时,用 Enum 比用 String 好------Spring AI 会把 Enum 的所有值写进 Schema,模型只会从这些值里选:
java
enum Priority {LOW, MEDIUM, HIGH, CRITICAL}
enum Category {BUG, FEATURE, IMPROVEMENT, DOCUMENTATION}
record IssueClassification(
String title,
Category category,
Priority priority,
String assignTo,
String reason
) {
}
调用时配合 System Prompt 效果更好:
java
@PostMapping("/classify")
public IssueClassification classify(@RequestBody IssueRequest request) {
return chatClient.prompt()
.system("你是项目经理,负责对 Issue 进行分类和优先级评估。")
.user("请对这个 Issue 进行分类:" + request.description())
.call()
.entity(IssueClassification.class);
}
3.7 复杂嵌套结构
结构化输出支持嵌套对象和 List,比如简历分析:
java
record SkillLevel(String name, String level) {
}
record ResumeAnalysis(
String name,
String email,
String summary,
List<SkillLevel> technicalSkills,
List<String> workHistory,
String overallAssessment
) {
}
@PostMapping("/analyze")
public ResumeAnalysis analyze(@RequestBody ResumeRequest request) {
return chatClient.prompt()
.system("你是一个专业的HR,帮助分析候选人简历。字段为空时填null,技能等级只能是:入门/熟练/精通。")
.user("分析这份简历:\n" + request.content())
.call()
.entity(ResumeAnalysis.class);
}
这里有几个值得注意的点:
List<SkillLevel>嵌套对象列表,Spring AI 能正确处理- System Prompt 里约束了"技能等级只能是:入门/熟练/精通",配合结构化输出效果更可控
- "字段为空时填 null"------这个约束很重要,否则模型可能编一个值出来
四、手动转换:BeanOutputConverter
4.1 什么时候需要手动
前面的 .entity() 方法是全自动的------生成 Schema、附加到 Prompt、反序列化一条龙。但有些场景你需要更多控制权:
| 场景 | 建议 |
|---|---|
| 标准场景,快速出活 | 用 .entity() 自动方式 |
| 需要自定义 Prompt 格式 | 用 BeanOutputConverter 手动方式 |
| 需要在 Prompt 中精确控制 Schema 位置 | 用 BeanOutputConverter 手动方式 |
| 需要调试或查看生成的 Schema | 用 BeanOutputConverter 手动方式 |
4.2 手动方式三步走
java
@PostMapping("/analyze")
public ResumeAnalysis analyze(@RequestBody ResumeRequest request) {
// 第一步:创建转换器,自动生成 JSON Schema
BeanOutputConverter<ResumeAnalysis> converter =
new BeanOutputConverter<>(ResumeAnalysis.class);
// 第二步:手动拼装 Prompt,把 Schema 放进去
String prompt = """
分析这份简历,按照以下 JSON 格式输出:
%s
简历内容:%s
""".formatted(converter.getFormat(), request.content());
// 第三步:调用模型,拿到纯文本 JSON,手动转换
String jsonResponse = chatClient.prompt()
.user(prompt)
.call()
.content();
return converter.convert(jsonResponse);
}
三个关键方法:
| 方法 | 作用 |
|---|---|
converter.getFormat() |
获取自动生成的 JSON Schema 字符串 |
chatClient...content() |
拿到模型返回的原始 JSON 文本 |
converter.convert() |
把 JSON 文本反序列化为 Java 对象 |
开发调试时可以打印
converter.getFormat()看看生成的 Schema 长什么样,有助于理解 Spring AI 在背后做了什么。
五、实战:文章智能分析
把前面学的 System Prompt + 结构化输出组合起来,看一个贴近真实业务的例子------文章智能分析接口。
5.1 定义分析结果结构
java
record ArticleAnalysis(
@JsonPropertyDescription("文章标题,如果没有则根据内容生成")
String title,
@JsonPropertyDescription("文章类型:NEWS/OPINION/TUTORIAL/RESEARCH/OTHER")
String type,
@JsonPropertyDescription("100字以内的摘要")
String summary,
@JsonPropertyDescription("关键词列表,最多5个")
List<String> keywords,
@JsonPropertyDescription("文章的主要观点,最多3条")
List<String> mainPoints,
@JsonPropertyDescription("情感倾向:POSITIVE/NEGATIVE/NEUTRAL")
String sentiment,
@JsonPropertyDescription("可读性评分,1-10分,10分最易读")
int readabilityScore
) {
}
每个字段都用 @JsonPropertyDescription 写清楚了约束------类型范围、长度限制、数量上限。这些描述会变成 JSON Schema 的一部分发给模型。
5.2 文章分析接口
java
@RestController
@RequestMapping("/api/article")
public class ArticleAnalysisController {
private final ChatClient chatClient;
public ArticleAnalysisController(ChatClient.Builder builder) {
this.chatClient = builder
.defaultSystem("""
你是一个专业的文章分析助手。
分析准确,不要添加原文没有的内容。
""")
.build();
}
@PostMapping("/analyze")
public ArticleAnalysis analyze(@RequestBody ArticleRequest request) {
return chatClient.prompt()
.user("请分析以下文章:\n\n" + request.content())
.call()
.entity(ArticleAnalysis.class);
}
record ArticleRequest(String content) {
}
}
System Prompt 里的"不要添加原文没有的内容"这条约束很关键------防止模型在分析时"发挥想象力"。
5.3 关键词提取接口
同一个 Controller 里还可以做关键词提取,直接返回 List<String>:
java
@PostMapping("/keywords")
public List<String> extractKeywords(@RequestBody ArticleRequest request) {
return chatClient.prompt()
.user("从以下文章中提取5个最重要的关键词:\n\n" + request.content())
.call()
.entity(new ParameterizedTypeReference<List<String>>() {
});
}
这个例子展示了结构化输出不一定要用复杂对象------简单的 List<String> 也行。
六、写在最后
API 速查表
| 需求 | API | 示例 |
|---|---|---|
| 全局 System Prompt | builder.defaultSystem(...) |
构建时设定角色 |
| 动态 System Prompt | .system(s -> s.text(...).param(...)) |
按请求动态调整 |
| 外部模板文件 | new PromptTemplate(new ClassPathResource(...)) |
加载 .st 文件 |
| 内联模板 | new PromptTemplate("...") + .create(Map.of(...)) |
代码内模板 |
| 结构化输出(单对象) | .entity(SomeClass.class) |
返回一个 Java 对象 |
| 结构化输出(列表) | .entity(new ParameterizedTypeReference<List<T>>() {}) |
返回对象列表 |
| 手动转换 | BeanOutputConverter |
需要自定义 Prompt 格式时 |
| 字段描述增强 | @JsonPropertyDescription |
让 Schema 更精确 |
| 字段名映射 | @JsonProperty |
驼峰转下划线等 |
| 约束分类值 | enum 字段 |
限定取值范围 |
生产环境的几点建议
- Prompt 和代码分离 :长 Prompt 用
.st文件管理,放在resources/prompts/目录下,纳入 Git 版本控制。 - 结构化输出优先用
.entity():自动方式覆盖 90% 的场景,只有需要精细控制时才用BeanOutputConverter。 - 善用
@JsonPropertyDescription:字段名不够表意时,描述越详细,模型输出越准确。 - Enum 约束分类:凡是"只能从几个选项里选"的字段,用 Enum 而不是 String,能避免模型输出意料之外的值。
- System Prompt 加防御性约束:尤其是知识库场景,"不确定就说不知道""不要添加原文没有的内容"这类约束必须加上。
- 注意 Token 消耗:结构化输出会在 Prompt 中附加 JSON Schema,复杂结构的 Schema 本身就会占用不少 Token,设计结构时要考虑这个成本。