Spring with AI (3): 定制对话——Prompt模板引入

本文代码:https://github.com/JunTeamCom/ai-demo/tree/release-3.0
Spring with AI系列,只关注上层AI的应用程序(基于JAVA搭建),不关注底层的LLM原理、搭建等技术。

通过简单的自定义Prompt模板,即可定制一个AI,专注某一领域的知识回答。

1 创建模板

先在pom.xml引入验证Starter:

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-validation</artifactId>
</dependency>

我们定义一个关于“世界各国地理历史知识”的AI,模板也简单明了:
实体定义:

package com.junteam.ai.demo.model;

import jakarta.validation.constraints.NotBlank;

public record ChatQuestion(
        @NotBlank(message = "标题不能为空") String title,
        @NotBlank(message = "问题不能为空") String question) {
}

模板文件resources/promptTemplates/questionPromptTemplate.st定义:

你是一个有用的助手,负责回答有关“代码编程题”的问题。
如果你对这个编程语言一无所知或不知道答案,请回答“我不知道”。

只给出实现代码。

编程语言是 {title}。

问题是:
{question}

2 实现逻辑

package com.junteam.ai.demo.service.impl;

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Service;

import com.junteam.ai.demo.model.ChatAnswer;
import com.junteam.ai.demo.model.ChatQuestion;
import com.junteam.ai.demo.service.ChatService;

@Service
public class OpenAIChatServiceImpl implements ChatService {

    private final ChatClient chatClient;

    public OpenAIChatServiceImpl(ChatClient.Builder chatClientBuilder) {
        this.chatClient = chatClientBuilder.build();
    }

    @Value("classpath:/promptTemplates/questionPromptTemplate.st")
    Resource questionPromptTemplate;

    @SuppressWarnings("null")
    @Override
    public ChatAnswer ask(ChatQuestion chatQuestion) {
        var answer = chatClient.prompt()
                .user(userSpec -> userSpec
                    .text(questionPromptTemplate)
                    .param("title", chatQuestion.title())
                    .param("question", chatQuestion.question())
                )
                .call();
        var answerText = answer.content();
        return new ChatAnswer(chatQuestion.title(), answerText);
    }
}

3 运行效果

测试用例:

curl http://localhost:8080/web/ask \
  -X POST \
  -H "Content-Type: application/json" \
  -d '{"title": "java", "question": "给定一个非递减排序的整数数组 nums 和一个目标值 target,请编写一个函数,返回 target 在数组中出现的第一个位置和最后一个位置(下标从 0 开始)。​\n  - 如果 target 未在数组中出现,返回 [-1, -1];​\n  - 要求:时间复杂度不超过 O(logn),空间复杂度 O(1)。​\n示例​\n  1. 输入:nums = [5,7,7,8,8,10], target = 8 → 输出:[3,4]​\n  2. 输入:nums = [5,7,7,8,8,10], target = 6 → 输出:[-1,-1]​\n  3. 输入:nums = [], target = 0 → 输出:[-1,-1]​\n  4. 输入:nums = [2,2], target = 2 → 输出:[0,1]"}' 

返回结果(为了排版,笔者进行了截断):

{
  "title":"JAVA",
  "answer":"```java\nclass Solution {\n public int findMin(int[] nums) {\n ...```"
}

整理出来结果如下:

class Solution {
    public int findMin(int[] nums) {
        int left = 0;
        int right = nums.length - 1;
        while (left < right) {
            int mid = left + (right - left) / 2;
            if (nums[mid] > nums[right]) {
                left = mid + 1;
            } else {
                right = mid;
            }
        }
        return nums[left];
    }
}

4 更多补充

4.1 基于模板扩展内容

再引入RAG之前,可以用简单的模板填充、来实现一些扩展内容。
比如再加一个langRules/java.txt,内容形如:

- java中尽量使用基本数据、而非封装类型。
- java中尽量使用静态方法实现代码。

这样可以再模板可以修改为:

你是一个有用的助手,负责回答有关“代码编程题”的问题。
如果你对这个编程语言一无所知或不知道答案,请回答“我不知道”。
如果可能,使用规则:{rules}。

只给出实现代码。

编程语言是 {title}。

问题是:
{question}

4.2 大模型选项

4.2.1 大模型类型

在配置讲解中已经提到,不再赘述

4.2.2 大模型温度

temperature参数是生成策略中的核心参数,直接影响输出的随机性与创造性。

  • 低Temperature(0.3-0.7):适用于客服机器人等需要精准回答的场景,减少错误信息
  • 中Temperature(0.7-1.2):适合创意写作,平衡逻辑性与多样性
  • 高Temperature(1.2-2.0):用于头脑风暴工具,激发非常规创意
ChatOptions chatOptions = ChatOptions.builder()
  .temperature(0.7)
  .build();

String answerText = chatClient.prompt()
  .user(question.question())
  .options(chatOptions)
  .call()
  .content();

4.2.3 其他选项

  • topP拣选答案的比例,比如.topP(0.8)从排名前80%的结果中拣选
  • topK排除答案的比例,比如.topP(0.2)排名后20%的结果排除

4.3 格式化

例如前面所提的,在Prompt中,指定输出格式为json、或者只保留java代码。
这样可以快速实现热门歌单等json接口

另外可以把输出格式就设置为流式,这样客户端或网页前端,可以使用SSE协议接收结果、逐个Token显示。

return chatClient.prompt()
  .system(systemSpec -> systemSpec
    .text(promptTemplate)
    .param("title", question.title())
    .param("rules", langRules))
    .user(question.question())
  .stream() // 流式
  .content();

4.4 响应元数据

LLM返回的内容,例如OpenAI,包含了Token使用相关的数据(元数据),形如:

{
  "token_usage": {
    "completion_tokens": 164,
    "prompt_tokens": 17,
    "total_tokens": 181
  },
  "model_name": "gpt-4-turbo",
  "system_fingerprint": "fp_76f018034d",
  "finish_reason": "stop",
  "logprobs": null
}

这样可以再代码中获取和记录:

var responseEntity = chatClient.prompt()
  .system(systemSpec -> systemSpec
    .text(promptTemplate)
    .param("gameTitle", question.gameTitle())
    .param("rules", gameRules))
    .user(question.question())
  .call()
.responseEntity(Answer.class);
var response = responseEntity.response();
var metadata = response.getMetadata();
log.info(metadata.getUsage()); // 获得Token使用量
return responseEntity.entity();
posted @ 2026-03-17 15:32  gujunge  阅读(39)  评论(0)    收藏  举报