一、为什么 Prompt 必须"结构化"
Prompt 的本质是"自然语言版的接口契约"。如果不结构化,会出现 4 个真实的工程问题:
|-----------|------------------------|------------------|
| 问题 | 表现 | 根因 |
| 解析失败 | 下游解析 JSON / Bean 时格式抖动 | 模型输出的边界不规范 |
| 难以 review | PR 里 5000 字没人看得完 | 没有分块、没有角色标签 |
| 改动难追溯 | 不记得当时为什么这么写 | 改动位置和意图没标记 |
| 多语言模型切换 | GPT-4o 好用换 Claude 翻车 | 不同模型对格式的"理解偏好"不同 |
结论:把 Prompt 当代码写,需要 module、role、constraint 三件套------这正是结构化方法的共同特征。
下面三种方法,分别对应"读起来舒服"、"人类写起来高效"、"机器解析最稳"三个目标。
二、XML 标签法:最稳的结构化方案
XML 标签是 Anthropic 官方推荐的方式(《Claude Prompt Engineering Guide》明确提到 XML 在 Claude 上表现优于 Markdown)。它的核心思想是把 Prompt 切成有名字的分区。
2.1 一个完整的 XML Prompt 模板
XML
<role>
你是一名资深 Java 性能调优工程师,专精 JVM GC 和 Arthas 诊断。
</role>
<context>
应用的 JVM 参数:-Xms4g -Xmx4g -XX:+UseG1GC
应用的 QPS:2000,平均 RT:50ms
线上出现的问题:每 6 小时一次 Full GC,每次 STW 800ms
</context>
<task>
根据 <context> 提供的信息,分析 Full GC 频繁的根因,并给出可落地的调优方案。
</task>
<constraints>
- 回答必须用中文
- 长度控制在 500 字以内
- 必须按"根因分析 → 调优建议 → 验证步骤"三段式输出
- 不要列超过 5 条建议,每条都要量化收益
</constraints>
<output_format>
## 根因分析
- (3~5 条)
## 调优建议
- 建议 1:(具体参数 + 预期效果)
- 建议 2:...
## 验证步骤
1. ...
</output_format>
为什么用 XML 而不是 Markdown?
- 首尾匹配的语义清晰 :
<context>...</context>一对标签就是一块语义完整的区域,模型不会被同级的标题层级搞混。
- 可嵌套 :
<context>里可以再嵌<jvm_params>、<qps>,适合多源信息融合。
- Claude 模型显著更稳:Anthropic 官方基准测试中,XML 标签法在 Claude 3.5/4 上的指令遵循率高 8%~15%。
2.2 配合 Spring AI 的 StringTemplate 落地
java
// XML 标签法的 Spring AI 落地------用 StringTemplate 把 XML 模板化
// 依赖:spring-boot-starter-ai-openai-spring-boot-starter 1.0.0+
@Component
public class XmlPromptJVMAdvisor {
private final ChatClient chatClient;
// 推荐:把 XML 模板写在独立的 .st 文件里,这里为演示内联在代码中
private static final String XML_TEMPLATE = """
<role>
你是一名资深 Java 性能调优工程师,专精 JVM GC 和 Arthas 诊断。
</role>
<context>
JVM 参数:{jvmArgs}
QPS:{qps},平均 RT:{rtMs}ms
现象:{symptom}
</context>
<task>
根据 <context> 给出 GC 调优方案,要求可落地。
</task>
<constraints>
- 中文输出,长度 ≤ 500 字
- 按"根因 → 建议 → 验证"三段式
- 建议不超过 5 条,每条量化收益
</constraints>
""";
public XmlPromptJVMAdvisor(ChatClient.Builder builder) {
this.chatClient = builder.build();
}
public String advise(String jvmArgs, int qps, int rtMs, String symptom) {
String prompt = XML_TEMPLATE
.replace("{jvmArgs}", jvmArgs)
.replace("{qps}", String.valueOf(qps))
.replace("{rtMs}", String.valueOf(rtMs))
.replace("{symptom}", symptom);
return chatClient.prompt()
.user(prompt)
.call()
.content();
}
}
老梁踩坑 :模板里如果想强调某个变量,不能用 <b> 标签------模型会原样输出 <b>。强调用"重要"二字包一层,或用 <critical> 这种语义化标签,不要用 HTML 的 b/strong/i。
三、Markdown 法:人类阅读体验最好
Markdown 法是 LLM 训练数据里最常见的格式(GitHub、知乎、技术博客全是 Markdown),模型对它天然友好,适合给人类同事看的 Prompt 文档。
3.1 一个完整的 Markdown Prompt
java
````markdown
# 角色
你是一名 MySQL DBA,熟悉 InnoDB 锁机制。
# 背景信息
- 数据库版本:MySQL 8.0.32
- 表结构:`orders(id, user_id, status, created_at)`,无索引
- 当前 SQL:
```sql
SELECT * FROM orders WHERE user_id = 123 AND status = 'PAID';
````
任务
分析这条 SQL 的性能问题,给出索引优化方案。
输出要求
- 列出 2~3 个根因
- 给出完整的 DDL 语句
- 用 EXPLAIN 验证优化后的执行计划
- 输出长度 ≤ 300 字
java
**Markdown 优势**:
- **代码块天然支持**:用 ` ```sql ` 包 SQL,模型不会"误读"为自然语言。
- **层级清晰**:# / ## / ### 对应"角色→任务→细节"的认知层级。
- **写起来快**:VSCode、Jupyter、`resources/prompts/*.md` 文件都好管理。
### 3.2 配合 Spring AI 资源文件管理
把模板外置到 `src/main/resources/prompts/order_optimizer.md`,代码里读文件渲染:
//java
// Markdown 模板 + Spring AI 的标准用法
// 文件位置:src/main/resources/prompts/order_optimizer.md
@Component
public class MarkdownPromptSqlAdvisor {
private final ChatClient chatClient;
private final Resource templateResource;
public MarkdownPromptSqlAdvisor(
ChatClient.Builder builder,
@Value("classpath:prompts/order_optimizer.md") Resource templateResource) {
this.chatClient = builder.build();
this.templateResource = templateResource;
}
public String advise(String mysqlVersion, String tableSchema, String sql) {
// Spring AI 的 PromptTemplate 自动加载 .md 文件,{var} 占位符替换
PromptTemplate template = new PromptTemplate(templateResource);
Prompt prompt = template.create(Map.of(
"mysqlVersion", mysqlVersion,
"tableSchema", tableSchema,
"sql", sql
));
return chatClient.prompt(prompt).call().content();
}
}
对应的 order_optimizer.md 内容(用 {``{var}} 占位):
java
# 角色
你是一名 MySQL {{mysqlVersion}} DBA,熟悉 InnoDB 锁机制。
# 背景信息
表结构:`{{tableSchema}}`
当前 SQL:
```sql
{{sql}}
任务
分析这条 SQL 的性能问题,给出索引优化方案。
输出要求
- 2~3 个根因
- 完整 DDL
- EXPLAIN 验证计划对比
- 长度 ≤ 300 字
java
**Markdown vs XML 怎么选?**
- **给模型看 + 强约束输出** → XML(标签首尾匹配,模型不易漏字段)
- **给同事 review + 跨模型兼容** → Markdown(人人能读,所有模型都认)
- **多个变量 + 需要代码块** → Markdown(XML 包代码块需要 CDATA,麻烦)
- **Claude 模型 + 多层语义嵌套** → XML(Claude 的最优解)
## 四、JSON Schema 法:机器解析最稳
当 Prompt 的输出要被下游系统**直接消费**(入库、渲染、写代码)时,必须用 JSON Schema 约束输出。这是结构化输出的"终极方案"。
Spring AI 提供了 `BeanOutputConverter` 和 `ListOutputConverter`,把 Java Bean 直接作为输出契约:
### 4.1 定义强类型的输出契约
```java
// 用 Java Bean 直接定义大模型的输出契约
// 依赖:spring-boot-starter-ai-openai-spring-boot-starter 1.0.0+
public record SqlOptimizationReport(
List<String> rootCauses, // 根因列表
List<String> ddlStatements, // DDL 语句列表
String explainExpectedPlan, // 优化后 EXPLAIN 预期
List<String> validationSteps // 验证步骤
) {}
4.2 用 BeanOutputConverter 一行搞定
java
// Spring AI 的 BeanOutputConverter 自动把 Bean 描述转成 JSON Schema
@Component
public class JsonSchemaSqlAdvisor {
private final ChatClient chatClient;
private final BeanOutputConverter<SqlOptimizationReport> converter;
public JsonSchemaSqlAdvisor(ChatClient.Builder builder) {
// 关键:把转换器声明出来,Spring AI 会自动生成 JSON Schema
this.converter = new BeanOutputConverter<>(SqlOptimizationReport.class);
this.chatClient = builder.build();
}
public SqlOptimizationReport advise(String sql, String tableSchema) {
// 把 JSON Schema 注入 system 消息,告诉模型必须按这个格式输出
String systemPrompt = """
你是 MySQL 8.0 性能优化专家。
%s
""".formatted(this.converter.getFormat());
String userPrompt = """
表结构:%s
待优化 SQL:%s
""".formatted(tableSchema, sql);
// chatClient 返回的是 String,需要 converter 转成 Bean
String rawJson = chatClient.prompt()
.system(systemPrompt)
.user(userPrompt)
.call()
.content();
// 直接反序列化为强类型 Bean,下游代码无 if-else
return this.converter.convert(rawJson);
}
}
converter.getFormat() 会自动生成类似下面的指令注入到 system 消息里:
java
Your response should be a JSON object with the following structure:
{
"rootCauses": ["string"],
"ddlStatements": ["string"],
"explainExpectedPlan": "string",
"validationSteps": ["string"]
}
4.3 列表场景用 ListOutputConverter
如果只要一个字符串列表,更轻量:
java
// ListOutputConverter 用于"输出必须是 List<String>"的简单场景
@Component
public class BugRootCauseExtractor {
private final ChatClient chatClient;
private final ListOutputConverter converter;
public BugRootCauseExtractor(ChatClient.Builder builder) {
this.converter = new ListOutputConverter(new DefaultConverterService());
this.chatClient = builder.build();
}
public List<String> extract(String stackTrace) {
String systemPrompt = """
你是 Java 异常分析专家。请从堆栈中提取 N 个独立根因。
%s
""".formatted(converter.getFormat());
return (List<String>) converter.convert(chatClient.prompt()
.system(systemPrompt)
.user("堆栈:%s".formatted(stackTrace))
.call()
.content());
}
}
JSON Schema 的两条血泪经验:
- Bean 的字段加 JSR-380 注解 :
@NotBlank、@Size(max=200)不会自动约束大模型,但能让 Bean 校验失败时立刻抛错,比让模型给你一个空字符串好排查得多。
- 大模型 100% 不会严格遵循 JSON Schema :实测 GPT-4o 的 JSON 格式正确率约 95%、Qwen 约 88%。下游必须 有容错(
@JsonIgnoreProperties(ignoreUnknown = true)+ 字段默认值),不能假设模型给的就是合规 JSON。
五、三种结构化方案对比
|--------------|-------------------|---------------------------|----------------------------|
| 维度 | XML 标签法 | Markdown 法 | JSON Schema 法 |
| 人类可读 | 中 | 优 | 差 |
| 模型可读 | 优(Claude 首选) | 优 | 中 |
| 机器解析 | 中(需 regex 提取) | 中 | 优(强类型映射) |
| 多变量模板 | 中 | 优 | 中 |
| 嵌套语义 | 优 | 良 | 良 |
| 跨模型兼容 | 良 | 优 | 优 |
| 适用输出 | 长文本、结构化文本 | 长文本、代码块 | 强类型数据 |
| Spring AI 支持 | 手动 StringTemplate | PromptTemplate + Resource | BeanOutputConverter 原生 |
老梁的项目用法 :团队协作的场景统一用 Markdown ;调用 Claude API 的场景叠一层 XML 标签 ;输出要入库的场景用 JSON Schema。三者不是互斥,是组合拳。
六、建议
-
团队统一一套结构化规范
别让团队里有的同事写 XML、有的写 Markdown、有的写大段散文。建议制定一个 30 行以内的"团队 Prompt 规范"塞进 wiki------只规定「角色 / 背景 / 任务 / 约束 / 输出」五个分区,强制每条 Prompt 都按这个分区写。可以参考我团队的简化版:
java[角色] 一句话定义专家身份 [背景] 关键参数、上下文 [任务] 用动词开头的一句话目标 [约束] 长度、格式、语言 [输出] Markdown / JSON / 文本写完后强制走 PR review,Prompt 跟代码一样要 review。
-
JSON Schema 输出必须有兜底解析
即使 95% 准确率,在 100 万次调用里也有 5 万次格式错误。建议用 Spring AI 的
BeanOutputConverter+ Jackson 容错配置:javaObjectMapper mapper = new ObjectMapper() .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) .configure(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT, true);或者外加一层"AI 输出→Java Bean"的包装,把解析失败统一转成
Optional.empty()抛给上层重试。 -
跨模型切换要做 Prompt 兼容性测试集
我团队的踩坑:同一份 Prompt 从 DeepSeek 切到 GPT-4o 后,JSON 格式正确率从 92% 跳到 97%(好),但 XML 标签下的语义遵循率从 95% 跌到 88%(差,因为 GPT-4o 对 XML 不如 Claude 敏感)。结论:每个模型至少备 50 个标注样本做格式回归测试,别凭感觉切。
结构化 Prompt 不是为了好看,是为了让你半夜被叫起来排查时,能 30 秒看清这份 Prompt 在干什么。
下篇预告 :Prompt 写出来不是结束------你在生产里会改它 A/B、把它回滚、用 Git 管它。下一篇我们聊 《Prompt 调试与版本管理:像管理代码一样管理 Prompt》,教你用 Langfuse + Git 做 Prompt 的灰度发布和回滚。
往期回顾: