一、为什么 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 模板
<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 落地
// 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
````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 字
**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}}占位):
# 角色 你是一名 MySQL {{mysqlVersion}} DBA,熟悉 InnoDB 锁机制。 # 背景信息 表结构:`{{tableSchema}}` 当前 SQL: ```sql {{sql}}任务
分析这条 SQL 的性能问题,给出索引优化方案。
输出要求
- 2~3 个根因
- 完整 DDL
- EXPLAIN 验证计划对比
- 长度 ≤ 300 字
**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 一行搞定
// 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 消息里:
Your response should be a JSON object with the following structure: { "rootCauses": ["string"], "ddlStatements": ["string"], "explainExpectedPlan": "string", "validationSteps": ["string"] }4.3 列表场景用 ListOutputConverter
如果只要一个字符串列表,更轻量:
// 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 都按这个分区写。可以参考我团队的简化版:
[角色] 一句话定义专家身份 [背景] 关键参数、上下文 [任务] 用动词开头的一句话目标 [约束] 长度、格式、语言 [输出] Markdown / JSON / 文本写完后强制走 PR review,Prompt 跟代码一样要 review。
JSON Schema 输出必须有兜底解析
即使 95% 准确率,在 100 万次调用里也有 5 万次格式错误。建议用 Spring AI 的
BeanOutputConverter+ Jackson 容错配置:ObjectMapper 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 的灰度发布和回滚。
往期回顾:
- 90篇JAVA高级深度分析与实践文章:做AI的主人,代码直接可用,附完整目录
- Day67-Prompt工程核心技巧:从零样本到思维链(CoT)
- Day66-开源模型vs闭源模型:技术决策框架