【Spring AI 实战】三、Prompt 工程:模板化、结构化输出与 Advisors 顾问模式

大家好,我是冰点,今天我们继续Spring AI实战系列

所属阶段:第一阶段·核心基础

前置知识:建议先完成第一、二篇,熟悉 ChatClient 的基本调用链路。

适用版本:本文按 Spring AI 1.0.x 的 Prompt 与 Advisor 能力组织内容,不同版本的 Builder 与 OutputParser 细节可能略有差异。


1. Prompt 工程为什么重要

Prompt 工程(Prompt Engineering)不是玄学,它本质上是如何清晰地表达你的意图给模型。在 Spring AI 体系中,Prompt 不仅仅是传给模型的字符串,它被封装为完整的对象体系:

Prompt = messages(List<Message>) + parameters(Map<String, Object>)
Message = content + type(SYSTEM/USER/ASSISTANT) + media(可选)

一个设计良好的 Prompt 可以:

  • 降低 Token 消耗:减少无效 token,节省 API 调用成本
  • 提高输出质量:格式稳定、内容准确、逻辑清晰
  • 实现复杂业务:多轮对话、条件分支、工具调用

1.1 Prompt 的三个层次

层次 描述 示例
零样本 直接给任务描述 “把这段中文翻译成英文”
少样本 提供参考示例 “参考以下例子:中文→英文,给出下一个翻译”
思维链 引导模型逐步推理 “先分析问题,再给出解答”

2. Prompt 模板体系

在这里插入图片描述

2.1 PromptTemplate 核心用法

Spring AI 的 PromptTemplate 借鉴了 Spring MVC 的模板思想,支持占位符渲染

// 定义模板
PromptTemplate template = PromptTemplate.from(
    "请将以下{language}代码转换为{targetLanguage}," +
    "只输出代码,不要解释:\n\n```{language}\n{code}\n```"
);

// 渲染变量
Prompt prompt = template.render(Map.of(
    "language", "Python",
    "targetLanguage", "Java",
    "code", "print('Hello World')"
));

// 调用
String javaCode = chatClient.prompt(prompt).call().content();

2.2 模板文件管理

在真实项目中,Prompt 模板应从文件加载,而不是硬编码在代码中:

src/main/resources/prompts/
├── system-code-review.st
├── user-translation.st
├── system-summarizer.st
└── user-sql-generator.st
// 从文件加载模板
PromptTemplate codeReviewTemplate = new PromptTemplate(
    new ClassPathResource("prompts/system-code-review.st")
);

// 支持 Placeholder 语法
PromptTemplate promptTemplate = PromptTemplate.builder()
    .resource(new ClassPathResource("prompts/user-sql-generator.st"))
    .build();

// 渲染
Prompt prompt = promptTemplate.render(Map.of(
    "schema", "CREATE TABLE users (id BIGINT, name VARCHAR(100), email VARCHAR(200))",
    "question", "查询所有邮箱以 @company.com 结尾的用户"
));

resources/prompts/system-code-review.st 示例

你是一位资深代码审查专家,负责审查以下{language}代码。

审查维度:
1. 安全性:是否存在 SQL 注入、XSS、敏感信息泄露风险?
2. 性能:是否有 N+1 查询、死循环、大对象内存风险?
3. 可维护性:命名是否清晰、是否有重复代码?
4. 最佳实践:是否符合{framework}开发规范?

请按以下 JSON 格式输出审查结果:
{
  "security": { "issues": [], "level": "LOW|MEDIUM|HIGH", "summary": "..." },
  "performance": { "issues": [], "level": "LOW|MEDIUM|HIGH", "summary": "..." },
  "maintainability": { "issues": [], "level": "LOW|MEDIUM|HIGH", "summary": "..." },
  "bestPractices": { "issues": [], "level": "LOW|MEDIUM|HIGH", "summary": "..." },
  "overallLevel": "LOW|MEDIUM|HIGH",
  "suggestions": []
}

待审查代码:
```{language}
{code}

### 2.3 多语言消息构建

```java
// 构建多段对话
Prompt prompt = Prompt.builder()
    .messages(
        // 系统消息
        MessageBuilder.createSystemMessage(
            "你是一位熟悉{service}领域的技术专家,使用{style}风格回答"
        ).render(
            Map.of("service", "电商", "style", "简洁专业")
        ),
        // 用户历史消息
        MessageBuilder.createUserMessage("什么是微服务架构?"),
        // 助手历史回复
        MessageBuilder.createAssistantMessage(
            "微服务架构是一种将单体应用拆分为多个小型服务的架构风格..."
        ),
        // 当前用户消息
        MessageBuilder.createUserMessage("它和SOA架构有什么区别?")
    )
    .build();

3. Few-Shot 与 Chain of Thought

3.1 Few-Shot:提供参考示例

Few-Shot 的核心思想是让模型从示例中学习模式,而不是从零理解任务:

PromptTemplate fewShotTemplate = PromptTemplate.from(
    "请根据以下示例,将中文句子转换为英文。\n" +
    "\n" +
    "示例1:\n" +
    "输入: 今天天气真好\n" +
    "输出: The weather is great today\n" +
    "\n" +
    "示例2:\n" +
    "输入: {input}\n" +
    "输出:"
);

String result = chatClient.prompt()
    .user(
        fewShotTemplate.render(Map.of("input", "我喜欢吃苹果"))
    )
    .call()
    .content();
// 输出: I like eating apples

Few-Shot 技巧

  • 示例数量:2~5 个效果最好,过多会浪费 token
  • 示例质量:覆盖主要场景,边界案例放后面
  • 示例格式:与最终任务的输入输出一致

3.2 Chain of Thought(思维链)

思维链(CoT)通过引导模型先推理再回答来提高复杂问题的准确率:

PromptTemplate cotTemplate = PromptTemplate.from(
    "请按以下步骤思考并回答问题:\n\n" +
    "第1步 - 理解题意:{question}\n" +
    "第2步 - 分析关键信息:\n" +
    "第3步 - 逐步推理(至少3步):\n" +
    "第4步 - 给出最终答案:\n\n" +
    "问题:{question}\n"
);

String answer = chatClient.prompt()
        .user(cotTemplate.render(Map.of("question", userQuestion)))
        .options(ChatOptionsBuilder.builder().withTemperature(0.0).build())
        .call()
        .content();

3.3 Few-CoT:Few-Shot + 思维链

最强组合——示例中包含推理过程:

PromptTemplate fewCotTemplate = PromptTemplate.from(
    "请根据以下示例,按推理步骤回答问题。\n\n" +
    "示例问题: 小明买了3支铅笔,每支2元,又买了1个橡皮擦3元,一共花了多少钱?\n" +
    "推理: 铅笔总价=3×2=6元,加上橡皮擦3元,总共6+3=9元\n" +
    "答案: 9元\n\n" +
    "示例问题: 小红有10个苹果,给了小明3个,又收到5个,现在有多少个?\n" +
    "推理: 10-3=7个,7+5=12个\n" +
    "答案: 12个\n\n" +
    "现在回答以下问题:\n" +
    "问题: {question}\n" +
    "推理:"
);

4. 结构化输出:让 AI 返回精确 JSON

结构化输出是 AI 应用的核心痛点之一。Spring AI 提供了**严格模式(Strict Mode)**来保证输出格式。

4.1 使用 outputParser 解析

// 定义输出结构
public record ReviewResult(
    int score,          // 1-10 分
    List<String> pros,  // 优点
    List<String> cons,  // 缺点
    String summary      // 总结
) {}

// 解析输出
String rawOutput = chatClient.prompt()
        .user("分析这部电影《肖申克的救赎》的优缺点")
        .call()
        .content();

// 手动解析(基础方式)
ObjectMapper mapper = new ObjectMapper();
ReviewResult result = mapper.readValue(rawOutput, ReviewResult.class);

4.2 使用 BeanOutputParser(推荐)

Spring AI 提供 BeanOutputParser 自动完成解析:

// 定义 POJO
public record MovieReview(
    int score,
    List<String> pros,
    List<String> cons,
    String summary
) {}

// 使用 BeanOutputParser 自动生成 Prompt + 解析
BeanOutputParser<MovieReview> parser =
    new BeanOutputParser<>(MovieReview.class);

String result = chatClient.prompt()
    .user("分析电影《肖申克的救赎》的优缺点")
    .defaultSystem(
        "你是一位专业的影评人,用客观专业的语言评价电影。" +
        "请严格按照以下JSON格式输出:{format}"
    )
    .param("format", parser.getFormat())  // 自动注入 JSON Schema
    .call()
    .content();

// 自动解析为 Java 对象
MovieReview review = parser.parse(result);
System.out.println(review.score());  // 9
System.out.println(review.pros());   // [信念与希望, 精彩的叙事...]

4.3 严格模式 Strict Output

对于生产环境,需要确保输出的 JSON 格式绝对正确,可以使用 StringOutputParser + 正则清洗:

@Service
public class StrictJsonParser {

    private final ChatClient chatClient;

    public StrictJsonParser(ChatClient.Builder builder) {
        this.chatClient = builder.build();
    }

    public <T> T parseStrict(String question, Class<T> clazz) {
        BeanOutputParser<T> parser =
            new BeanOutputParser<>(clazz);

        // 强制要求模型在 ```json ```块中输出
        String prompt = question + "\n\n请严格按照以下JSON格式输出在 ```json 代码块中,不要有其他文字:\n" +
                "```json\n" + parser.getFormat() + "\n```";

        String raw = chatClient.prompt()
                .user(prompt)
                .options(
                    ChatOptionsBuilder.builder().withTemperature(0.0).build()
                )
                .call()
                .content();

        // 提取 JSON 内容
        String json = extractJson(raw);
        return parser.parse(json);
    }

    private String extractJson(String raw) {
        Pattern pattern = Pattern.compile(
            "```json\\s*(.*?)\\s*```", Pattern.DOTALL);
        Matcher matcher = pattern.matcher(raw);
        if (matcher.find()) {
            return matcher.group(1).trim();
        }
        return raw.trim();
    }
}

5. Advisors 顾问模式详解

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Advisors 是 Spring AI 的拦截器链,类似于 Spring AOP 的 HandlerInterceptor 或 Servlet 的 Filter,在 AI 调用前后插入自定义逻辑。

5.1 Advisor 执行时机

User Request
    │
    ▼
[Advisor 1: before() → after()]
    │
    ▼
[Advisor 2: before() → after()]
    │
    ▼
[ChatModel.call()]  ← 实际 AI 调用
    │
    ▼
[...after() 返回...]

5.2 内置 Advisors

Spring AI 提供多个开箱即用的 Advisors:

// 1. 对话记忆 Advisor(最常用)
MessageChatAdvisor.builder()
    .chatMemory(new InMemoryChatMemory())
    .build();

// 2. 信号拦截 Advisor(拦截特定信号触发动作)
SignalChatAdvisor.builder()
    .signal("继续")
    .advisorBehavior(...)
    .build();

// 3. 重试 Advisor
RetryChatAdvisor.builder()
    .maxAttempts(3)
    .retryExceptions(AIException.class)
    .build();

// 4. 限流 Advisor
RateLimitChatAdvisor.builder()
    .tokensPerMinute(60)
    .build();

5.3 自定义 Advisor

@Component
public class LoggingAdvisor implements org.springframework.ai.chat.client.advisor.Advisor {

    private static final Logger log = LoggerFactory.getLogger(LoggingAdvisor.class);

    @Override
    public org.springframework.ai.chat.messages.Message around(
            org.springframework.ai.chat.client.ChatClientRequest request,
            org.springframework.ai.chat.client.ChatClientResponse response,
            java.util.function.Supplier<org.springframework.ai.chat.client.ChatClientResponse> next) {

        // 记录请求
        log.info("【AI请求】用户输入: {}",
            request.userText());

        // 执行调用
        var result = next.get();

        // 记录响应
        log.info("【AI响应】回复长度: {} 字,Token消耗: {}",
            result.chatResponse().getResult().getOutput().getContent().length(),
            result.chatResponse().getMetadata().getUsage());

        return result;
    }

    @Override
    public String getName() {
        return "LoggingAdvisor";
    }

    @Override
    public int getOrder() {
        return 0; // 执行顺序,数字越小越先执行
    }
}

5.4 Advisor 链组合

// 组合多个 Advisor
String result = chatClient.prompt()
    .user(question)
    .advisors(
        // 1. 日志记录
        new LoggingAdvisor(),
        // 2. 对话记忆(session 隔离)
        MessageChatAdvisor.builder()
            .chatMemory(new InMemoryChatMemory())
            .build(),
        // 3. 重试策略
        RetryChatAdvisor.builder()
            .maxAttempts(3)
            .build()
    )
    .call()
    .content();

6. 实战:构建一个智能代码审查 Agent

结合 Prompt 模板 + 结构化输出 + Advisors,构建完整的代码审查服务:

@Service
@RequiredArgsConstructor
public class CodeReviewAgent {

    private final ChatClient chatClient;

    // 代码审查输出结构
    public record CodeReviewResult(
        String severity,      // CRITICAL / HIGH / MEDIUM / LOW / INFO
        String category,     // SECURITY / PERFORMANCE / STYLE / ...
        String description,
        String location,
        String suggestion
    ) {}

    public List<CodeReviewResult> review(String code, String language) {
        BeanOutputParser<List<CodeReviewResult>> parser =
            new BeanOutputParser<>(
                new ParameterizedTypeReference<>() {}
            );

        String systemPrompt = """
            你是一位资深代码审查专家。审查{language}代码,识别以下类型的问题:
            - SECURITY: 安全漏洞(SQL注入、XSS、敏感信息泄露等)
            - PERFORMANCE: 性能问题(N+1查询、内存泄漏等)
            - STYLE: 代码风格问题
            - BEST_PRACTICE: 违反最佳实践

            请输出一个JSON数组,每个问题一个对象,字段为:
            severity, category, description, location, suggestion
            如果没有问题,返回空数组 []。
            """;

        String raw = chatClient.prompt()
            .system(sp)
            .user("```" + language + "\n" + code + "\n```")
            .param("format", parser.getFormat())
            .options(
                ChatOptionsBuilder.builder()
                    .withTemperature(0.1)  // 降低随机性
                    .build()
            )
            .call()
            .content();

        return parser.parse(raw);
    }
}

7. 常见 Prompt 反模式与优化策略

7.1 反模式

反模式 问题 优化方式
模糊指令 “帮我看看代码” 明确任务、输出格式、约束条件
过长 Prompt 一股脑塞入所有上下文 拆分成多步,或用 RAG 检索
温度过高 随机性太强,输出不稳定 设置 temperature=0.0
无示例 模型理解偏差大 添加 Few-Shot 示例
指令与示例冲突 示例优先于指令 示例与指令保持一致

7.2 优化策略

策略一:角色定位 + 分段指令

// ❌ 模糊
"You are an assistant. Help me."

// ✅ 精确
"""
你是一位拥有10年经验的高级Java架构师。
第一步:根据业务场景,分析是否适合微服务架构;
第二步:给出架构设计图(ASCII格式);
第三步:提供核心代码示例。
"""

策略二:输出格式约束

"""
回答必须满足以下格式(严格遵守):
【结论】:一行话总结
【理由】:不超过3点
【代码示例】:Java代码(如涉及)
【风险提示】:如存在潜在问题
"""

策略三:分步验证

// 让模型先验证再执行
"""
任务:{user_task}
请先验证以下条件是否满足:
1. [条件1]
2. [条件2]
如果条件不满足,返回"INVALID: [原因]";
如果条件满足,执行任务并返回结果。
"""

8. 小结

本文系统讲解了 Spring AI 中的 Prompt 工程体系:

  1. Prompt 模板:从占位符渲染到模板文件管理,从单段落到多消息构建
  2. 高级技巧:Few-Shot 提供参考示例、Chain of Thought 引导推理
  3. 结构化输出:BeanOutputParser + 严格模式,保证 JSON 格式绝对正确
  4. Advisors 拦截器链:对话记忆、重试、限流、自定义逻辑的组合方式
  5. 实战:构建了一个完整的代码审查 Agent

下一篇预告:【Spring AI 实战】四、OpenAI / Anthropic / Azure——多模型适配与自动配置原理——我们将深入 Spring AI 的自动配置机制,理解如何通过配置文件切换不同 AI 服务商,以及多模型 A/B 测试、模型热切换等生产级实践。


📌 系列导航

📎 示例说明:本文以 Prompt 设计思路和代码片段为主,建议与后续 RAG 和 Function Calling 篇配合阅读。

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐