在上一小节中,我们学习了 Spring AI 最基础的使用方法,但是大家可以思考一个问题,目前我们使用的是智谱提供的 AI 大模型,如果我想切换成最近最火的 DeepSeek
大模型,那应该怎么处理呢?所以这一小节,我们将学习 Spring AI 中推荐使用的 ChatClient
来完成其他大模型的接入,并完成另外一个新的案例:智能健康助手。
我们先来看下最终的效果:
这个智能健康管理助手有以下功能:
首先我们来看一下整个系统的流程设计图:
与第一章相比,区别主要有两点:
1、Controller
调用的不再是 ChatModel
,而变成了 ChatClient
。ChatClient
和 ChatModel
区别有如下几点:
简单来说他们的区别如下:
功能定位
适用范围
编程灵活性与复杂度
适用场景
ChatClient:
ChatModel:
看完这些内容,这个项目的选择显然呼之欲出,我们不需要针对特定模型进行定制化,所以将会使用 ChatClient
因为其更加简单,功能也更加强大。
创建一个新的 Controller
用于处理智能健康助手的接口。
@RestController
@RequestMapping("/health")
public class HealthController {
private final ChatClient chatClient;
_/**_
_ * 构造函数_
_ * _
_ * @param chatClientBuilder ChatClient构建器_
_ */_
public HealthController(ChatClient.Builder chatClientBuilder) {
this.chatClient = chatClientBuilder.build();
}
}
在 HealthController
构造方法中,通过 ChatClient
提供的建造者设计模式,使用 Builder
创建了一个新的 ChatClient
对象。
这里可能会产生一个疑问,这 ChatClient
也没有指定是哪个大模型啊?它是怎么知道这次我们是要用智谱 AI 还是 DeepSeek 呢?
通过 Debug 的方式,在构造方法中看到其实在获得 chatClientBuilder
时,它已经自动从当前的 Spring
环境中获取到当前使用的 ChatModel
也就是智谱大语言模型,从而实现了解耦,用户无需关心真正使用的大模型,只需要根据 Spring AI
提供的接口,进行大模型的访问即可。通过这种机制,可以实现代码无关的大模型切换,这个一会儿我们再来尝试。
接下来我们给这个 ChatClient
设置上默认的 Prompt:
1、定义 System Prompt:
其实有很多人使用大模型没有达到自己预期的效果,本质上就是没有定义好很好的 system prompt
。
在与大语言模型交互的场景中,system prompt
(系统提示)扮演着至关重要的角色,首先一起来学习下它的主要作用:
根据以上的经验,我们来设计一个比较完善的 promt,当然这个事完全可以交给 AI 来做:
private static final String _SYSTEM_PROMPT _= """
你是一个专业的健康管理AI助手。请仔细分析用户的问题,并严格按照以下JSON格式回复。
注意:必须确保返回的是合法的JSON字符串,不要添加任何其他内容。
示例格式:
{
"title": "简短的总结标题,不超过20字",
"analysis": "详细的分析说明,包括可能的原因和影响",
"suggestions": [
"具体的建议1",
"具体的建议2",
"具体的建议3"
],
"severity": 1到5之间的整数,含义如下:
1: 表示普通的健康咨询
2: 表示需要稍加注意
3: 表示需要重点关注
4: 表示应该尽快就医
5: 表示建议立即就医,
"tags": [
"相关领域标签1",
"相关领域标签2"
],
"nextSteps": [
"下一步具体行动建议1",
"下一步具体行动建议2"
],
"timestamp": "使用ISO 8601格式的时间戳"
}
请注意:
1. 返回内容必须是合法的JSON格式
2. 所有字段都必须存在且格式正确
3. severity必须是1-5之间的整数
4. timestamp使用ISO 8601格式
5. suggestions和nextSteps至少包含一项建议
你擅长:
1. 提供健康生活方式建议
2. 解答基础医疗保健问题
3. 进行初步症状评估
4. 提供饮食和营养建议
5. 推荐适合的运动方案
6. 进行心理健康咨询
注意事项:
1. 不做具体疾病诊断
2. 遇到严重症状时建议就医
3. 保持专业、友善的沟通态度
4. 注重隐私保护
5. 基于科学依据提供建议
请始终以专业、谨慎的态度回答问题。如果问题超出你的能力范围,请礼貌地建议用户咨询专业医生。
""";
这段提示词是 AI 帮我生成的,我们可以学习一下它编写的 Prompt 的优秀之处:
2、修改ChatClient:
_/**_
_ * 构造函数_
_ * _
_ * @param chatClientBuilder ChatClient构建器_
_ */_
public HealthController(ChatClient.Builder chatClientBuilder) {
this.chatClient = chatClientBuilder
.defaultSystem(_SYSTEM_PROMPT_)
.build();
}
这里使用了 defaultSystem
设置 System Prompt
。后续所有和大模型的对话,这一段 prompt 都会携带在会话内容中,这样大模型会以一种更合理的方式回答你的问题。
Zero - shot
、One - shot
、Few - shot
在与大语言模型交互时,借助 System Prompt
(系统提示)能够实现零样本学习(Zero - shot)、单样本学习(One - shot)和少样本学习(Few - shot),下面详细介绍一下:
含义:不提供任何具体示例,模型仅依据 System Prompt
描述的任务要求和相关背景知识生成回复。
实现方式:在 System Prompt
中详细清晰地描述任务的定义、目标、规则以及期望的输出格式等信息。
System Prompt
可以这样设定:“你是一位专业的健康问题分类助手。请根据以下规则对用户提出的健康问题进行分类:若问题涉及运动健身方面,分类为‘运动健康’;若问题与饮食营养相关,分类为‘饮食营养’;若问题是关于心理健康的,分类为‘心理健康’。请直接回复分类结果。”效果:让模型基于自身预训练所积累的广泛知识,结合系统提示中的规则,对未见过的任务进行处理,体现出一定的泛化能力。不过,由于缺乏具体示例,模型在面对复杂任务时可能准确性欠佳。
含义:仅提供一个示例,让模型学习该示例特征并应用到新任务中。
实现方式:System Prompt
除描述任务基本信息外,还给出一个具体示例。
System Prompt
可以表述为:“你是一位专业的健康顾问,根据用户描述的症状给出合理建议。以下是一个示例:症状描述:‘最近总是失眠,晚上很难入睡’,建议:‘睡前避免使用电子设备,营造安静舒适的睡眠环境,可尝试喝一杯温牛奶助眠’。现在请根据用户提供的症状给出建议。”效果:通过一个示例,模型能更快地理解任务的具体要求和操作方式。相较于零样本学习,生成的结果可能更符合预期,但单个示例可能不足以让模型充分掌握复杂任务的各种变化。
含义:提供少量(通常几个到几十个)示例,让模型学习并完成特定任务。
实现方式:在 System Prompt
中明确任务基本信息,同时给出多个具体示例。
System Prompt
可以这样设置:“你是一位专业的医疗辅助诊断师,根据用户描述的症状初步判断可能的情况,以‘可能情况:[具体情况]’的格式回复。示例 1:症状描述:‘咳嗽、流鼻涕、喉咙痛’,回复:‘可能情况:感冒’;示例 2:症状描述:‘腹痛、腹泻、呕吐’,回复:‘可能情况:肠胃炎’;示例 3:症状描述:‘头痛、眩晕、视力模糊’,回复:‘可能情况:高血压或脑部供血不足’。现在请对用户描述的症状进行初步判断。”效果:多个示例能让模型更全面地理解任务的具体要求和不同情况下的处理方式,提高回复的准确性和泛化能力。尤其适用于复杂或规则不明确的任务,但示例数量有限时仍可能存在一定的局限性。
综上所述,零样本、单样本和少样本学习在 System Prompt
中的应用各有特点,可根据任务的复杂程度和数据可获取性选择合适的方式,以引导模型高效准确地完成任务。
我们使用 Few - shot
优化一下刚才的 prompt:
private static final String SYSTEM_PROMPT = """
你是一个专业的健康管理AI助手。请仔细分析用户的问题,并严格按照以下JSON格式回复。
注意:必须确保返回的是合法的JSON字符串,不要添加任何其他内容。
示例格式:
{
"title": "简短的总结标题,不超过20字",
"analysis": "详细的分析说明,包括可能的原因和影响",
"suggestions": [
"具体的建议1",
"具体的建议2",
"具体的建议3"
],
"severity": 1到5之间的整数,含义如下:
1: 表示普通的健康咨询
2: 表示需要稍加注意
3: 表示需要重点关注
4: 表示应该尽快就医
5: 表示建议立即就医,
"tags": [
"相关领域标签1",
"相关领域标签2"
],
"nextSteps": [
"下一步具体行动建议1",
"下一步具体行动建议2"
],
"timestamp": "使用ISO 8601格式的时间戳"
}
以下是几个具体问题及对应回复的示例:
问题:“我最近总是感觉很疲劳,没什么力气,这是怎么回事呀?”
回复:{
"title": "疲劳乏力原因及建议",
"analysis": "感到疲劳没力气可能是由于睡眠不足、缺乏运动、营养不良等原因引起的。长期疲劳可能会影响日常生活和工作效率。",
"suggestions": [
"保证每天7 - 8小时的充足睡眠",
"每周进行至少三次有氧运动,如散步、慢跑等",
"注意饮食均衡,多摄入富含蛋白质、维生素的食物"
],
"severity": 2,
"tags": [
"疲劳",
"健康咨询"
],
"nextSteps": [
"观察一周内疲劳症状是否有改善",
"若症状持续或加重,考虑去医院进行全面体检"
],
"timestamp": "2024 - 12 - 01T12:00:00Z"
}
问题:“我最近吃饭没胃口,看到食物也不想吃,该怎么办呢?”
回复:{
"title": "食欲不振问题分析",
"analysis": "吃饭没胃口可能是因为天气炎热、情绪不佳、肠胃不适等因素。长期食欲不振可能导致营养摄入不足。",
"suggestions": [
"饮食尽量清淡,避免油腻和辛辣食物",
"保持心情舒畅,可以通过听音乐、散步等方式缓解压力",
"适当吃一些开胃的食物,如山楂、话梅等"
],
"severity": 2,
"tags": [
"食欲不振",
"饮食健康"
],
"nextSteps": [
"记录自己的饮食情况和食欲变化",
"若一周后症状仍未改善,咨询医生"
],
"timestamp": "2024 - 12 - 02T13:30:00Z"
}
请注意:
1. 返回内容必须是合法的JSON格式
2. 所有字段都必须存在且格式正确
3. severity必须是1 - 5之间的整数
4. timestamp使用ISO 8601格式
5. suggestions和nextSteps至少包含一项建议
你擅长:
1. 提供健康生活方式建议
2. 解答基础医疗保健问题
3. 进行初步症状评估
4. 提供饮食和营养建议
5. 推荐适合的运动方案
6. 进行心理健康咨询
注意事项:
1. 不做具体疾病诊断
2. 遇到严重症状时建议就医
3. 保持专业、友善的沟通态度
4. 注重隐私保护
5. 基于科学依据提供建议
请始终以专业、谨慎的态度回答问题。如果问题超出你的能力范围,请礼貌地建议用户咨询专业医生。
""";
接下来的接口开发就变得很容易了,整理一下步骤:
ChatClient
的 prompt
方法设置 prompt,最后通过 call
方法调用大模型,获得返回的结果。jackson
框架将返回 json
结果转换成 java 对象返回给前端,这一步其实有更简单的方法,我们在后续课程中再展开学习。参考代码如下:
_/**_
_ * 生成健康咨询回复_
_ * _
_ * @param message 用户输入的消息_
_ * @return 结构化的健康建议响应_
_ */_
@GetMapping("/generate")
public HealthResponse generate(@RequestParam(value = "message") String message) {
try {
// 构建提示词,加入系统角色定义
String prompt = "\n用户问题:" + message;
ChatClient.CallResponseSpec response = this.chatClient.prompt(prompt).call();
// 解析AI返回的JSON响应
JsonNode jsonResponse = objectMapper.readTree(response.content());
Map<String, Object> data = objectMapper.convertValue(jsonResponse, Map.class);
_// 使用系统当前时间替换AI返回的时间戳_
data.put("timestamp", LocalDateTime.now().format(java.time.format.DateTimeFormatter.ISO_DATE_TIME));
// 返回成功响应
return HealthResponse._success_(data);
} catch (Exception e) {
// 构建错误响应数据
Map<String, Object> errorData = new HashMap<>();
errorData.put("title", "处理请求时发生错误");
errorData.put("analysis", "系统暂时无法处理您的请求,请稍后重试");
errorData.put("suggestions", new String[] { "请重新提交您的问题", "如果问题持续,请联系系统管理员" });
errorData.put("severity", 1);
errorData.put("tags", new String[] { "系统错误" });
errorData.put("nextSteps", new String[] { "刷新页面重试", "尝试重新描述您的问题" });
errorData.put("timestamp", LocalDateTime._now_().toString());
// 返回错误响应
return HealthResponse._error_("AI服务调用失败: " + e.getMessage());
}
}
如果想要用流式方式进行返回,可以参考如下写法,这个就留为作业大家自行完成。
Flux<String> output = chatClient.prompt()
.user("Tell me a joke")
.stream()
.content();
别忘了在构造方法中,创建 objectMapper
:
private final ObjectMapper objectMapper;
_/**_
_ * 构造函数_
_ * _
_ * @param chatClientBuilder ChatClient构建器_
_ */_
public HealthController(ChatClient.Builder chatClientBuilder) {
this.chatClient = chatClientBuilder
.defaultSystem(_SYSTEM_PROMPT_)
.build();
this.objectMapper = new ObjectMapper();
}
页面效果就不在这里赘述了,重点来看一下接口调用的相关代码:
// 提交健康咨询
async function submitQuery() {
const userId = _document_.getElementById('userId').value;
const query = _document_.getElementById('healthQuery').value.trim();
if (!userId) {
showError('请先输入用户ID');
return;
}
if (!query) {
showError('请输入健康问题描述');
return;
}
try {
const response = await fetch(`/api/health/query`, {
method: 'POST',
headers: {
'User-Id': userId,
'Content-Type': 'text/plain'
},
body: query
});
const report = await response.json();
displayReport(report);
} catch (error) {
_console_.error('提交咨询失败:', error);
showError('提交咨询失败,请稍后重试');
}
}
这段代码区别于上一小节的代码主要在于返回结果的处理上:
await response.json()
:将响应对象的内容解析为 JSON 格式,并等待解析完成,然后将解析后的结果赋值给 report
。displayReport(report)
:调用 displayReport
函数,将解析后的报告数据作为参数传入,用于在页面上展示服务器返回的健康咨询报告。如下代码所示:
// 显示健康报告
function displayReport(report) {
const reportArea = _document_.getElementById('reportArea');
const reportContent = _document_.getElementById('reportContent');
reportContent.innerHTML = `
主要症状
${report.mainComplaint}
初步诊断
${report.diagnosis}
建议
${report.recommendation}
紧急程度
${report.urgencyLevel}">
级别 ${report.urgencyLevel}
响应时间: ${report.responseTimeMs}ms
`;
reportArea.style.display = 'block';
}
这段时间,DeepSeek 那可是在 AI 江湖里火得一塌糊涂,在技术社区 GitHub 上,DeepSeek 就像一位自带光芒的流量明星。DeepSeek Coder 一开源,那小星星(Star)就跟不要钱似的疯狂往上冒,眨眼间就几千个。
我们一起来尝一把鲜,让智能健康助手接入 DeepSeek
。
Spring AI
是如何接入大模型的呢?首先大家应该还记得,我们引入了一个依赖:
<dependency>
<groupId>org.springframework.aigroupId>
<artifactId>spring-ai-zhipuai-spring-boot-starterartifactId>
dependency>
这里包含两个关键性的依赖:
<dependency>
<groupId>org.springframework.aigroupId>
<artifactId>spring-ai-spring-boot-autoconfigureartifactId>
<version>1.0.0-SNAPSHOTversion>
<scope>compilescope>
dependency>
<dependency>
<groupId>org.springframework.aigroupId>
<artifactId>spring-ai-zhipuaiartifactId>
<version>1.0.0-SNAPSHOTversion>
<scope>compilescope>
dependency>
spring-ai-spring-boot-autoconfigure
Spring Boot 自动配置:这个依赖的核心功能是为 Spring AI 提供 Spring Boot 自动配置能力。Spring Boot 的自动配置机制可以根据类路径中的依赖、配置文件中的属性等信息,自动为应用程序配置所需的 Bean。spring-ai-spring-boot-autoconfigure
会自动检测应用程序中与 Spring AI 相关的环境,并进行相应的配置。spring-ai-zhipuai
集成智谱 AI:该依赖提供了 Spring AI 与智谱 AI 大模型的集成能力。通过这个依赖,开发者可以在 Spring 应用程序中方便地调用智谱 AI 的 API 来实现各种 AI 相关的功能,如文本生成、问答系统、对话交互等。同时对智谱 AI 的 API 进行了封装,隐藏了底层的 HTTP 请求、JSON 数据处理等复杂细节。开发者只需要使用 Spring AI 提供的高层抽象接口,就可以轻松地与智谱 AI 进行交互,降低了开发难度。我们来一探究竟,找到 spring-ai-spring-boot-autoconfigure
中的 ZhiPuAiAutoConfiguration
,这里我给每一行代码加上了注释:
// 自动配置类,用于配置与智谱AI相关的组件
// 该配置会在RestClientAutoConfiguration和SpringAiRetryAutoConfiguration之后执行
@AutoConfiguration(
after = {RestClientAutoConfiguration.class, SpringAiRetryAutoConfiguration.class}
)
// 当类路径中存在ZhiPuAiApi类时,才会进行此配置
@ConditionalOnClass({ZhiPuAiApi.class})
// 启用与智谱AI相关的配置属性类,允许从配置文件中读取配置
@EnableConfigurationProperties({ZhiPuAiConnectionProperties.class, ZhiPuAiChatProperties.class, ZhiPuAiEmbeddingProperties.class, ZhiPuAiImageProperties.class})
public class ZhiPuAiAutoConfiguration {
// 构造函数,目前为空,可能用于初始化一些必要的资源
public ZhiPuAiAutoConfiguration() {
}
/**
* 创建智谱AI聊天模型的Bean
*/
@Bean
// 当容器中不存在ZhiPuAiChatModel类型的Bean时,才会创建该Bean
@ConditionalOnMissingBean
// 当配置文件中spring.ai.zhipuai.chat.enabled属性为true(默认值为true)时,才会创建该Bean
@ConditionalOnProperty(
prefix = "spring.ai.zhipuai.chat",
name = {"enabled"},
havingValue = "true",
matchIfMissing = true
)
public ZhiPuAiChatModel zhiPuAiChatModel(ZhiPuAiConnectionProperties commonProperties, ZhiPuAiChatProperties chatProperties, ObjectProvider<RestClient.Builder> restClientBuilderProvider, List<FunctionCallback> toolFunctionCallbacks, FunctionCallbackResolver functionCallbackResolver, RetryTemplate retryTemplate, ResponseErrorHandler responseErrorHandler, ObjectProvider<ObservationRegistry> observationRegistry, ObjectProvider<ChatModelObservationConvention> observationConvention) {
// 创建智谱AI的API客户端实例
ZhiPuAiApi zhiPuAiApi = this.zhiPuAiApi(chatProperties.getBaseUrl(), commonProperties.getBaseUrl(), chatProperties.getApiKey(), commonProperties.getApiKey(), (RestClient.Builder)restClientBuilderProvider.getIfAvailable(RestClient::builder), responseErrorHandler);
// 创建智谱AI聊天模型实例
ZhiPuAiChatModel chatModel = new ZhiPuAiChatModel(zhiPuAiApi, chatProperties.getOptions(), functionCallbackResolver, toolFunctionCallbacks, retryTemplate, (ObservationRegistry)observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP));
// 确保聊天模型实例不为空
Objects.requireNonNull(chatModel);
// 如果存在观测约定,则设置到聊天模型中
observationConvention.ifAvailable(chatModel::setObservationConvention);
return chatModel;
}
/**
* 创建智谱AI嵌入模型的Bean
*/
@Bean
// 当容器中不存在ZhiPuAiEmbeddingModel类型的Bean时,才会创建该Bean
@ConditionalOnMissingBean
// 当配置文件中spring.ai.zhipuai.embedding.enabled属性为true(默认值为true)时,才会创建该Bean
@ConditionalOnProperty(
prefix = "spring.ai.zhipuai.embedding",
name = {"enabled"},
havingValue = "true",
matchIfMissing = true
)
public ZhiPuAiEmbeddingModel zhiPuAiEmbeddingModel(ZhiPuAiConnectionProperties commonProperties, ZhiPuAiEmbeddingProperties embeddingProperties, RestClient.Builder restClientBuilder, RetryTemplate retryTemplate, ResponseErrorHandler responseErrorHandler, ObjectProvider<ObservationRegistry> observationRegistry, ObjectProvider<EmbeddingModelObservationConvention> observationConvention) {
// 创建智谱AI的API客户端实例
ZhiPuAiApi zhiPuAiApi = this.zhiPuAiApi(embeddingProperties.getBaseUrl(), commonProperties.getBaseUrl(), embeddingProperties.getApiKey(), commonProperties.getApiKey(), restClientBuilder, responseErrorHandler);
// 创建智谱AI嵌入模型实例
ZhiPuAiEmbeddingModel embeddingModel = new ZhiPuAiEmbeddingModel(zhiPuAiApi, embeddingProperties.getMetadataMode(), embeddingProperties.getOptions(), retryTemplate, (ObservationRegistry)observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP));
// 确保嵌入模型实例不为空
Objects.requireNonNull(embeddingModel);
// 如果存在观测约定,则设置到嵌入模型中
observationConvention.ifAvailable(embeddingModel::setObservationConvention);
return embeddingModel;
}
/**
* 创建智谱AI的API客户端实例
*/
private ZhiPuAiApi zhiPuAiApi(String baseUrl, String commonBaseUrl, String apiKey, String commonApiKey, RestClient.Builder restClientBuilder, ResponseErrorHandler responseErrorHandler) {
// 确定最终使用的基础URL,如果特定功能的URL不为空,则使用该URL,否则使用通用URL
String resolvedBaseUrl = StringUtils.hasText(baseUrl) ? baseUrl : commonBaseUrl;
// 确保基础URL不为空
Assert.hasText(resolvedBaseUrl, "ZhiPuAI base URL must be set");
// 确定最终使用的API密钥,如果特定功能的密钥不为空,则使用该密钥,否则使用通用密钥
String resolvedApiKey = StringUtils.hasText(apiKey) ? apiKey : commonApiKey;
// 确保API密钥不为空
Assert.hasText(resolvedApiKey, "ZhiPuAI API key must be set");
// 创建智谱AI的API客户端实例
return new ZhiPuAiApi(resolvedBaseUrl, resolvedApiKey, restClientBuilder, responseErrorHandler);
}
/**
* 创建智谱AI图像模型的Bean
*/
@Bean
// 当容器中不存在ZhiPuAiImageModel类型的Bean时,才会创建该Bean
@ConditionalOnMissingBean
// 当配置文件中spring.ai.zhipuai.image.enabled属性为true(默认值为true)时,才会创建该Bean
@ConditionalOnProperty(
prefix = "spring.ai.zhipuai.image",
name = {"enabled"},
havingValue = "true",
matchIfMissing = true
)
public ZhiPuAiImageModel zhiPuAiImageModel(ZhiPuAiConnectionProperties commonProperties, ZhiPuAiImageProperties imageProperties, RestClient.Builder restClientBuilder, RetryTemplate retryTemplate, ResponseErrorHandler responseErrorHandler) {
// 确定最终使用的API密钥,如果图像相关的密钥不为空,则使用该密钥,否则使用通用密钥
String apiKey = StringUtils.hasText(imageProperties.getApiKey()) ? imageProperties.getApiKey() : commonProperties.getApiKey();
// 确定最终使用的基础URL,如果图像相关的URL不为空,则使用该URL,否则使用通用URL
String baseUrl = StringUtils.hasText(imageProperties.getBaseUrl()) ? imageProperties.getBaseUrl() : commonProperties.getBaseUrl();
// 确保API密钥不为空
Assert.hasText(apiKey, "ZhiPuAI API key must be set");
// 确保基础URL不为空
Assert.hasText(baseUrl, "ZhiPuAI base URL must be set");
// 创建智谱AI图像API客户端实例
ZhiPuAiImageApi zhiPuAiImageApi = new ZhiPuAiImageApi(baseUrl, apiKey, restClientBuilder, responseErrorHandler);
// 创建智谱AI图像模型实例
return new ZhiPuAiImageModel(zhiPuAiImageApi, imageProperties.getOptions(), retryTemplate);
}
/**
* 创建函数回调解析器的Bean
*
* @param context 应用上下文
* @return 函数回调解析器实例
*/
@Bean
// 当容器中不存在FunctionCallbackResolver类型的Bean时,才会创建该Bean
@ConditionalOnMissingBean
public FunctionCallbackResolver springAiFunctionManager(ApplicationContext context) {
// 创建默认的函数回调解析器实例
DefaultFunctionCallbackResolver manager = new DefaultFunctionCallbackResolver();
// 设置应用上下文到解析器中
manager.setApplicationContext(context);
return manager;
}
}
就以目前用到的聊天模型为例,zhiPuAiChatModel
方法将智谱的聊天模型对象放入了 Spring
容器中。我们来看一下它的实现;
public class ZhiPuAiChatModel extends AbstractToolCallSupport implements ChatModel, StreamingChatModel {
private static final Logger logger = LoggerFactory.getLogger(ZhiPuAiChatModel.class);
private static final ChatModelObservationConvention DEFAULT_OBSERVATION_CONVENTION = new DefaultChatModelObservationConvention();
public final RetryTemplate retryTemplate;
private final ZhiPuAiChatOptions defaultOptions;
private final ZhiPuAiApi zhiPuAiApi;
private final ObservationRegistry observationRegistry;
private ChatModelObservationConvention observationConvention;
...
/**
* 同步调用智谱 AI 进行聊天对话,获取聊天响应。
*
* @param prompt 包含用户输入和相关配置的提示信息
* @return 包含生成回复信息的聊天响应对象
*/
public ChatResponse call(Prompt prompt) {
// 创建智谱 AI 聊天完成请求对象,第二个参数 false 表示非流式请求
ZhiPuAiApi.ChatCompletionRequest request = this.createRequest(prompt, false);
// 创建聊天模型观测上下文,用于记录和监控请求的相关信息
ChatModelObservationContext observationContext = ChatModelObservationContext.builder()
.prompt(prompt)
.provider(ZhiPuApiConstants.PROVIDER_NAME)
.requestOptions(this.buildRequestOptions(request))
.build();
// 使用观测器对请求过程进行监控,记录性能指标等信息
ChatResponse response = (ChatResponse) ChatModelObservationDocumentation.CHAT_MODEL_OPERATION
.observation(this.observationConvention, DEFAULT_OBSERVATION_CONVENTION, () -> observationContext, this.observationRegistry)
.observe(() -> {
// 使用重试模板执行请求,处理可能的重试逻辑
ResponseEntity<ZhiPuAiApi.ChatCompletion> completionEntity = (ResponseEntity) this.retryTemplate.execute((ctx) -> this.zhiPuAiApi.chatCompletionEntity(request));
// 从响应实体中获取聊天完成结果
ZhiPuAiApi.ChatCompletion chatCompletion = (ZhiPuAiApi.ChatCompletion) completionEntity.getBody();
// 如果没有返回聊天完成结果,记录警告日志并返回空的聊天响应
if (chatCompletion == null) {
logger.warn("No chat completion returned for prompt: {}", prompt);
return new ChatResponse(List.of());
} else {
// 获取聊天完成结果中的选项列表
List<ZhiPuAiApi.ChatCompletion.Choice> choices = chatCompletion.choices();
// 将每个选项转换为生成对象,并添加元数据
List<Generation> generations = choices.stream()
.map((choice) -> {
Map<String, Object> metadata = Map.of(
"id", chatCompletion.id(),
"role", choice.message().role() != null ? choice.message().role().name() : "",
"finishReason", choice.finishReason() != null ? choice.finishReason().name() : ""
);
return buildGeneration(choice, metadata);
})
.toList();
// 创建聊天响应对象,包含生成对象列表和从聊天完成结果转换而来的信息
ChatResponse chatResponse = new ChatResponse(generations, this.from((ZhiPuAiApi.ChatCompletion) completionEntity.getBody()));
// 将聊天响应设置到观测上下文中
observationContext.setResponse(chatResponse);
return chatResponse;
}
});
// 检查是否需要处理工具调用
if (!this.isProxyToolCalls(prompt, this.defaultOptions) && this.isToolCall(response, Set.of(ChatCompletionFinishReason.TOOL_CALLS.name(), ChatCompletionFinishReason.STOP.name()))) {
// 处理工具调用,生成新的消息列表
List<Message> toolCallConversation = this.handleToolCalls(prompt, response);
// 递归调用 call 方法,继续处理新的提示信息
return this.call(new Prompt(toolCallConversation, prompt.getOptions()));
} else {
// 不需要处理工具调用,直接返回响应
return response;
}
}
/**
* 异步流式调用智谱 AI 进行聊天对话,返回一个包含聊天响应的 Flux 流。
*
* @param prompt 包含用户输入和相关配置的提示信息
* @return 包含聊天响应的 Flux 流,用于流式处理响应
*/
public Flux<ChatResponse> stream(Prompt prompt) {
return Flux.deferContextual((contextView) -> {
// 创建智谱 AI 聊天完成请求对象,第二个参数 true 表示流式请求
ZhiPuAiApi.ChatCompletionRequest request = this.createRequest(prompt, true);
// 使用重试模板执行流式请求,获取聊天完成结果的 Flux 流
Flux<ZhiPuAiApi.ChatCompletionChunk> completionChunks = (Flux) this.retryTemplate.execute((ctx) -> this.zhiPuAiApi.chatCompletionStream(request));
// 用于存储每个聊天会话的角色信息
ConcurrentHashMap<String, String> roleMap = new ConcurrentHashMap<>();
// 创建聊天模型观测上下文,用于记录和监控请求的相关信息
ChatModelObservationContext observationContext = ChatModelObservationContext.builder()
.prompt(prompt)
.provider(ZhiPuApiConstants.PROVIDER_NAME)
.requestOptions(this.buildRequestOptions(request))
.build();
// 创建观测器,用于监控流式请求过程
Observation observation = ChatModelObservationDocumentation.CHAT_MODEL_OPERATION
.observation(this.observationConvention, DEFAULT_OBSERVATION_CONVENTION, () -> observationContext, this.observationRegistry);
// 设置父观测器并启动观测
observation.parentObservation((Observation) contextView.getOrDefault("micrometer.observation", (Object) null)).start();
// 将聊天完成结果的 Flux 流转换为聊天响应的 Flux 流
Flux<ChatResponse> chatResponse = completionChunks.map(this::chunkToChatCompletion)
.switchMap((chatCompletion) -> Mono.just(chatCompletion)
.map((chatCompletion2) -> {
try {
// 获取聊天完成结果的 ID
String id = chatCompletion2.id();
// 将每个选项转换为生成对象,并添加元数据
List<Generation> generations = chatCompletion2.choices().stream()
.map((choice) -> {
if (choice.message().role() != null) {
roleMap.putIfAbsent(id, choice.message().role().name());
}
Map<String, Object> metadata = Map.of(
"id", chatCompletion2.id(),
"role", roleMap.getOrDefault(id, ""),
"finishReason", choice.finishReason() != null ? choice.finishReason().name() : ""
);
return buildGeneration(choice, metadata);
})
.toList();
// 创建聊天响应对象
return new ChatResponse(generations, this.from(chatCompletion2));
} catch (Exception e) {
// 处理异常,记录错误日志并返回空的聊天响应
logger.error("Error processing chat completion", e);
return new ChatResponse(List.of());
}
}));
// 处理工具调用逻辑
Flux<ChatResponse> flux = chatResponse.flatMap((response) -> {
if (!this.isProxyToolCalls(prompt, this.defaultOptions) && this.isToolCall(response, Set.of(ChatCompletionFinishReason.TOOL_CALLS.name(), ChatCompletionFinishReason.STOP.name()))) {
// 处理工具调用,生成新的消息列表
List<Message> toolCallConversation = this.handleToolCalls(prompt, response);
// 递归调用 stream 方法,继续处理新的提示信息
return this.stream(new Prompt(toolCallConversation, prompt.getOptions()));
} else {
// 不需要处理工具调用,直接返回响应
return Flux.just(response);
}
});
// 处理错误和结束事件,停止观测
Objects.requireNonNull(observation);
Flux<ChatResponse> finalFlux = flux.doOnError(observation::error).doFinally((s) -> observation.stop())
.contextWrite((ctx) -> ctx.put("micrometer.observation", observation));
// 创建消息聚合器,用于聚合聊天响应
MessageAggregator messageAggregator = new MessageAggregator();
Objects.requireNonNull(observationContext);
// 聚合聊天响应并设置到观测上下文中
return messageAggregator.aggregate(finalFlux, observationContext::setResponse);
});
}
...
}
很容易就能看到其实代码里就是使用了 RetryTemplate
使用 HttpClient
进行了支持重试的 http
调用。
有聪明的读者可能想到了,那我们只要把 spring-ai-zhipuai-spring-boot-starter
替换成 spring-ai-deepseek-spring-boot-starter
,不就可以直接接入 DeepSeek
了吗?理想很美好,现实很骨感。目前 Spring AI
没有提供 DeepSeek
的起步依赖。所以只能通过曲线救国的手段来完成这个操作。
如果 DeepSeak
的 API 兼容 OpenAI
的格式,包含如下内容:
那么就可以使用 OpenAI
对应的 Spring
依赖,通过修改参数的方式进行接入。
先做一下准备工作,去 DeepSeek
申请 api-key
:
官方文档
获取 API-KEY
测试一下:
curl https://api.deepseek.com/v1/chat/completions \
-H "Authorization: Bearer $DEEPSEEK_API_KEY" \
-d '{"model":"deepseek-chat", "messages":[{"role":"user","content":"Hello"}]}'
如果可以正常返回结果,就说明 DeepSeek
对应的 api-key
是可用的,接下来我们在项目中进行接入。
OpenAI
的依赖:
<dependency>
<groupId>org.springframework.aigroupId>
<artifactId>spring-ai-openai-spring-boot-starterartifactId>
dependency>
2、添加配置:
spring:
ai:
_# zhipuai:_
_ # api-key: ${ZHIPU_API_KEY}_
_ # chat:_
_ # options:_
_ # model: GLM-4-Flash_
_ _openai:
api-key: ${DEEPSEEK_API_KEY}
base-url: https://api.deepseek.com/
chat:
options:
model: deepseek-chat
3、运行程序,进行测试,是不是非常轻松非常 easy?
小建议,在页面上显著位置添加免责信息 :
⚠️ 本系统是基于大语言模型(智谱 AI、DeepSeek 等)的技术演示项目,不构成任何形式的医疗建议、诊断或治疗方案。系统生成内容仅用于展示 AI 技术能力,不可替代专业医疗机构提供的医疗服务。
这个交给大家自行去完成。
本小节主要聚焦于利用 Spring AI 搭建智能健康助手,涵盖后端、前端开发及模型接入,要点如下:
ChatClient
接入不同大模型,实现智能健康助手开发,该助手具备问题咨询、示例参考和症状处理等功能。ChatClient
和 ChatModel
,鉴于无需特定模型定制,选择更便捷强大的 ChatClient
。设置并优化 System Prompt
引导模型回复,进行少样本学习提升准确性。开发接口处理用户请求,调用大模型并处理异常。fetch
方法调用后端接口,将响应解析为 JSON 后展示健康咨询报告。