基础篇 | 聊天客户端 - 智能健康助手
一、学习目标
在上一小节中,我们学习了 Spring AI 最基础的使用方法,但是大家可以思考一个问题,目前我们使用的是智谱提供的 AI 大模型,如果我想切换成最近最火的 DeepSeek
大模型,那应该怎么处理呢?所以这一小节,我们将学习 Spring AI 中推荐使用的 ChatClient
来完成其他大模型的接入,并完成另外一个新的案例:智能健康助手。
我们先来看下最终的效果:
这个智能健康管理助手有以下功能:
- 问题咨询:用户能在文本框输入健康问题或困扰,获取相应建议,且描述越详细,建议越精准。
- 示例参考:提供常见健康问题示例,如疲劳、健身、饮食计划等方面的疑问,帮助用户更好地提问。
- 症状处理:针对具体健康症状,如嗓子痒咳嗽,分析病因,给出家庭治疗等缓解方法,还提示就医时机。
二、后端开发
首先我们来看一下整个系统的流程设计图:
与第一章相比,区别主要有两点:
1、Controller
调用的不再是 ChatModel
,而变成了 ChatClient
。ChatClient
和 ChatModel
区别有如下几点:
ChatClient 和 ChatModel 的区别
简单来说他们的区别如下:
功能定位
- ChatClient:提供了与 AI 模型通信的 Fluent API,类似应用程序开发中的服务层,隐藏了与 LLM 及其他组件(如提示词模板、聊天记忆、LLM Model、输出解析器、RAG 组件等)交互的复杂性,开发者能使用其 Fluent API 快速完成一整套 AI 交互流程的组装。它支持同步和反应式编程模型,还具备定制和组装模型输入(Prompt)、格式化解析模型输出(Structured Output)、调整模型交互参数(ChatOptions)、聊天记忆(Chat Memory)、工具 / 函数调用(Function Calling)、RAG 等功能 。
- ChatModel:是针对各个模型的具体实现。不同的模型(如 OpenAI、Ollama 等)都有其对应的 ChatModel 实现。以 OpenAI 为例,通过相关依赖引入后,可使用 OpenAiChatOptions 进行模型配置,如设置使用的模型、温度、频率损失等,启动时可通过构造函数或属性配置默认选项,运行时也可在 Prompt 调用中覆盖默认选项 。
适用范围
- ChatClient:针对所有模型,提供一个共用的客户端,统一处理与不同模型的交互。
- ChatModel:针对各个具体模型,每个模型都有其独立的 ChatModel 实现,开发者需要针对不同模型进行相应的配置和调用。
编程灵活性与复杂度
- ChatClient:降低了编码的繁琐程度,无需编写大量样板代码,但相对而言在一些高度定制化需求上的灵活性稍低。
- ChatModel:为应用程序带来更多的灵活性,开发者可以针对具体模型进行更细致的控制和定制,但需要编写更多的样板代码来处理与模型的交互以及与其他组件的协调。
适用场景
-
ChatClient:
- 适用场景:当项目需要快速搭建与多种 AI 模型交互的功能,且希望简化开发过程、隐藏底层模型交互复杂性时适用。比如一个面向普通用户的健康咨询应用,快速整合多种模型能力为用户提供统一的健康建议回复,使用 ChatClient 可加快开发进度。
-
ChatModel:
- 适用场景:当项目对特定模型有深度定制化需求,需要针对模型特性进行精细控制时适用。例如在科研项目中,需要针对某特定模型微调参数以满足特定实验要求,此时使用 ChatModel 更合适。
看完这些内容,这个项目的选择显然呼之欲出,我们不需要针对特定模型进行定制化,所以将会使用 ChatClient
因为其更加简单,功能也更加强大。
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
(系统提示)扮演着至关重要的角色,首先一起来学习下它的主要作用:
System Prompt 系统提示词
角色与任务设定
- 明确角色定位:给模型指定一个特定的角色,比如说 “你是一个专业的健康管理 AI 助手”“你是一位精通历史的学者” 等。这样就可以让模型能够以特定的身份和视角来处理用户的问题,让回复更贴合相应的专业领域和情境。
- 界定任务范围:清晰地告知模型需要完成的任务,如 “请仔细分析用户的问题,并严格按照以下格式回复”,让模型明确工作的具体要求和方向。
输出格式规范
- 统一回复格式:通过给出具体的格式要求,如规定使用特定的 JSON 格式、文本排版格式等,使模型的输出具有一致性和规范性。这对于后续的数据处理、解析和展示非常有帮助,提高了信息的可读性和可操作性。
- 确保信息完整性:明确列出需要包含的字段和内容,要求所有字段都必须存在且格式正确,避免模型遗漏重要信息,保证回复内容的完整性和准确性。
能力与边界约束
- 限定知识领域:列举模型擅长的领域,如 “提供健康生活方式建议、解答基础医疗保健问题” 等,让用户清楚可以向模型咨询的问题类型,同时也让模型专注于自身擅长的领域,提高回复的质量和可信度。
- 避免不当回复:明确指出模型不应该做的事情,如 “不做具体疾病诊断”,防止模型在不具备专业资质的情况下给出不恰当或不准确的回复,降低可能产生的风险。
沟通风格与态度引导
- 塑造沟通风格:对模型的沟通风格提出要求,如 “保持专业、友善的沟通态度”,使模型在与用户交互时能够营造良好的氛围,提升用户体验。
- 引导应对策略:指导模型在遇到特定情况时的应对方式,例如 “遇到严重症状时建议就医”“如果问题超出你的能力范围,请礼貌地建议用户咨询专业医生”,确保模型能够合理、有效地处理各种问题。
根据以上的经验,我们来设计一个比较完善的 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 的优秀之处:
- 角色清晰:明确设定为专业健康管理 AI 助手,让模型清楚职责。使模型能聚焦于健康管理领域,以专业视角精准处理用户问题,避免回复偏离方向,提升回复的相关性和专业性。
- 格式规范:规定 JSON 输出格式并给出示例,还有严格规则确保回复统一规范,便于解析处理。让模型输出具有统一结构,无论是在前端展示信息,还是后端进行数据存储、分析,都能高效准确地处理,提高开发和使用效率。
- 能力界定:列举擅长领域,避免具体疾病诊断,明确知识边界。使用户清楚可咨询的问题类型,同时防止模型在不具备专业资质时进行不当诊断,降低医疗风险,保证回复的合理性。
- 应对合理:指导模型遇严重症状或超能力范围问题时,引导用户就医或咨询专业医生。体现对用户健康负责的态度,确保在模型无法有效解决问题时,用户能得到正确指引,获取更专业的医疗帮助。
- 态度要求:强调专业友善沟通、注重隐私保护,提升用户体验与信任。营造良好的交互氛围,让用户感受到优质服务,同时保护用户信息安全,增强用户对该服务的信任和使用意愿。
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),下面详细介绍一下:
零样本学习(Zero - shot)
-
含义:不提供任何具体示例,模型仅依据
System Prompt
描述的任务要求和相关背景知识生成回复。 -
实现方式:在
System Prompt
中详细清晰地描述任务的定义、目标、规则以及期望的输出格式等信息。- 示例:在一个健康问题分类任务中,
System Prompt
可以这样设定:“你是一位专业的健康问题分类助手。请根据以下规则对用户提出的健康问题进行分类:若问题涉及运动健身方面,分类为‘运动健康’;若问题与饮食营养相关,分类为‘饮食营养’;若问题是关于心理健康的,分类为‘心理健康’。请直接回复分类结果。”
- 示例:在一个健康问题分类任务中,
-
效果:让模型基于自身预训练所积累的广泛知识,结合系统提示中的规则,对未见过的任务进行处理,体现出一定的泛化能力。不过,由于缺乏具体示例,模型在面对复杂任务时可能准确性欠佳。
单样本学习(One - shot)
-
含义:仅提供一个示例,让模型学习该示例特征并应用到新任务中。
-
实现方式:
System Prompt
除描述任务基本信息外,还给出一个具体示例。- 示例:在症状建议生成任务中,
System Prompt
可以表述为:“你是一位专业的健康顾问,根据用户描述的症状给出合理建议。以下是一个示例:症状描述:‘最近总是失眠,晚上很难入睡’,建议:‘睡前避免使用电子设备,营造安静舒适的睡眠环境,可尝试喝一杯温牛奶助眠’。现在请根据用户提供的症状给出建议。”
- 示例:在症状建议生成任务中,
-
效果:通过一个示例,模型能更快地理解任务的具体要求和操作方式。相较于零样本学习,生成的结果可能更符合预期,但单个示例可能不足以让模型充分掌握复杂任务的各种变化。
少样本学习(Few - shot)
-
含义:提供少量(通常几个到几十个)示例,让模型学习并完成特定任务。
-
实现方式:在
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 = `
<div class="report-item">
<div class="report-label">主要症状</div>
<div>${report.mainComplaint}</div>
</div>
<div class="report-item">
<div class="report-label">初步诊断</div>
<div>${report.diagnosis}</div>
</div>
<div class="report-item">
<div class="report-label">建议</div>
<div>${report.recommendation}</div>
</div>
<div class="report-item">
<div class="report-label">紧急程度</div>
<div class="urgency-level urgency-${report.urgencyLevel}">
级别 ${report.urgencyLevel}
</div>
</div>
<div class="response-time">
响应时间: ${report.responseTimeMs}ms
</div>
`;
reportArea.style.display = 'block';
}
四、使用 DeepSeek
这段时间,DeepSeek 那可是在 AI 江湖里火得一塌糊涂,在技术社区 GitHub 上,DeepSeek 就像一位自带光芒的流量明星。DeepSeek Coder 一开源,那小星星(Star)就跟不要钱似的疯狂往上冒,眨眼间就几千个。
我们一起来尝一把鲜,让智能健康助手接入 DeepSeek
。
模型接入的原理
Spring AI
是如何接入大模型的呢?首先大家应该还记得,我们引入了一个依赖:
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-zhipuai-spring-boot-starter</artifactId>
</dependency>
这里包含两个关键性的依赖:
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-spring-boot-autoconfigure</artifactId>
<version>1.0.0-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-zhipuai</artifactId>
<version>1.0.0-SNAPSHOT</version>
<scope>compile</scope>
</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
:
测试一下:
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.ai</groupId>
<artifactId>spring-ai-zhipuai-spring-boot-starter</artifactId>
</dependency> -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
</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 搭建智能健康助手,涵盖后端、前端开发及模型接入,要点如下:
- 学习目标:基于之前对 Spring AI 的基础学习,探索使用
ChatClient
接入不同大模型,实现智能健康助手开发,该助手具备问题咨询、示例参考和症状处理等功能。 - 后端开发:比较
ChatClient
和ChatModel
,鉴于无需特定模型定制,选择更便捷强大的ChatClient
。设置并优化System Prompt
引导模型回复,进行少样本学习提升准确性。开发接口处理用户请求,调用大模型并处理异常。 - 前端开发:前端通过
fetch
方法调用后端接口,将响应解析为 JSON 后展示健康咨询报告。 - 接入 DeepSeek:剖析 Spring AI 接入大模型的原理,因暂无 DeepSeek 起步依赖,若其 API 与 OpenAI 兼容,可通过修改 OpenAI 依赖和配置实现接入。