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();
浙公网安备 33010602011771号