使用 DeepSeek 模型和 Spring AI 构建 AI 聊天机器人
1、概览
现代 Web 应用越来越多地与大型语言模型(LLM)集成,以构建解决方案。
DeepSeek 是一家中国的 AI 研究公司,致力于开发功能强大的 LLM,最近凭借其 DeepSeek-V3 和 DeepSeek-R1 模型颠覆了人工智能世界。DeepSeek-V3 和 DeepSeek-R1 模型揭示了它的思维链(CoT),让我们了解了 AI 模型是如何解释和处理给定提示的。
本文将带你了解如何将 DeepSeek 模型与 Spring AI 集成,以构建一个能够进行多轮文本对话的简单聊天机器人。
2、依赖和配置
有多种方法可以将 DeepSeek 模型集成到我们的应用中。
2.1、使用 OpenAI
DeepSeek 模型与 OpenAI API 完全兼容,可以使用任何 OpenAI 客户端或库进行访问。
首先,在项目的 pom.xml
文件中添加 Spring AI 的 OpenAI Starter 依赖:
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
<version>1.0.0-M6</version>
</dependency>
由于当前的 1.0.0-M6 版本是里程碑版本,因此还需要在 pom.xml
中添加 Spring Milestones Repository:
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
该 Repository 是发布里程碑版本的地方,而不是标准的 Maven Central Repository。无论我们选择哪种配置选项,都需要添加这个里程碑 Repository。
接下来,在 application.yaml
文件中配置 DeepSeek API Key 和 聊天模型:
spring:
ai:
openai:
api-key: ${DEEPSEEK_API_KEY}
chat:
options:
model: deepseek-reasoner
base-url: https://api.deepseek.com
embedding:
enabled: false
此外,还指定了 DeepSeek API 的 base URL 并禁用了 embedding(嵌入),因为 DeepSeek 目前还没有提供任何与 embedding 兼容的模型。
配置上述属性后,Spring AI 会自动创建一个 ChatModel
类型的 Bean,允许我们与指定的模型交互。接下来,我们将使用它为聊天机器人定义其他几个 Bean。
2.2、使用 Amazon Bedrock Converse API
或者,也可以使用 Amazon Bedrock Converse API 将 DeepSeek R1 模型集成到我们的应用中。
要进行这一配置,我们需要一个 AWS 账户。DeepSeek-R1 模型可通过 Amazon Bedrock Marketplace 购买,并可使用 Amazon SageMaker 托管。可以参考 本部署指南 进行设置。
首先,在 pom.xml
中添加 Bedrock Converse starter 依赖:
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bedrock-converse-spring-boot-starter</artifactId>
<version>1.0.0-M6</version>
</dependency>
接下来,为了与亚马逊 Bedrock 交互,我们需要在 application.yaml
文件中配置用于身份验证的 AWS 凭证和 DeepSeek 模型所在的区域(region
):
spring:
ai:
bedrock:
aws:
region: ${AWS_REGION}
access-key: ${AWS_ACCESS_KEY}
secret-key: ${AWS_SECRET_KEY}
converse:
chat:
options:
model: arn:aws:sagemaker:REGION:ACCOUNT_ID:endpoint/ENDPOINT_NAME
使用 ${}
属性占位符从环境变量中加载属性值。
此外,我们还指定了托管 DeepSeek 模型的 SageMaker 端点 URL ARN。要记得用实际值替换 REGION、ACCOUNT_ID 和 ENDPOINT_NAME 占位符。
最后,为了与模型交互,我们需要为应用中配置的 IAM 用户分配以下 IAM 策略:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "bedrock:InvokeModel",
"Resource": "arn:aws:bedrock:REGION:ACCOUNT_ID:marketplace/model-endpoint/all-access"
}
]
}
同样,要记得用 Resource
ARN 中的实际值替换 REGION
和 ACCOUNT_ID
占位符。
2.3、使用 Ollama 进行本地设置
为了进行本地开发和测试,我们可以通过 Ollama 运行 DeepSeek 模型,这是一个开源工具,可以让我们在本地机器上运行 LLM。
在项目的 pom.xml
文件中导入必要的依赖:
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-ollama-spring-boot-starter</artifactId>
<version>1.0.0-M6</version>
</dependency>
Ollama starter 依赖 会帮助我们建立与 Ollama 服务的连接。
接下来,在 application.yaml
文件中配置聊天模型:
spring:
ai:
ollama:
chat:
options:
model: deepseek-r1
init:
pull-model-strategy: when_missing
embedding:
enabled: false
如上,我们指定了 deepseek-r1 模型,不过,你也可以尝试使用 其他可用模型 来实现这一功能。
此外,我们还将 pull-model-strategy 设置为 when_missing。这样就能确保 Spring AI 在本地没有指定模型的情况下拉取该模型。
Spring AI 在 localhost
上运行时会自动连接到 Ollama,默认端口为 11434
。不过,我们可以使用 spring.ai.ollama.base-url
属性覆盖连接 URL。或者,也可以使用 Testcontainers 来设置 Ollama 服务。
同样,Spring AI 会自动为我们创建 ChatModel
Bean。如果由于某种原因,我们的类路径上同时存在 OpenAI API、Bedrock Converse 和 Ollama 这三个依赖项,我们可以分别使用 openAiChatModel
、bedrockProxyChatModel
或 ollamaChatModel
的限定符来引用我们想要的特定 Bean。
3、构建聊天机器人
在熟悉了各种配置选项之后,让我们使用配置好的 DeepSeek 模型构建一个简单的聊天机器人。
3.1、定义 Chatbot Bean
首先,定义聊天机器人所需的 Bean:
@Bean
ChatMemory chatMemory() {
return new InMemoryChatMemory();
}
@Bean
ChatClient chatClient(ChatModel chatModel, ChatMemory chatMemory) {
return ChatClient
.builder(chatModel)
.defaultAdvisors(new MessageChatMemoryAdvisor(chatMemory))
.build();
}
首先,我们使用 InMemoryChatMemory
实现定义了一个 ChatMemory
Bean,它将聊天历史记录存储在内存中,以保持对话上下文。
接下来,使用 ChatModel
和 ChatMemory
Bean 创建一个 ChatClient
Bean。ChatClient
类是与我们配置的 DeepSeek 模型交互的主要入口。
3.2、创建一个自定义 StructuredOutputConverter
如前所述,DeepSeek-R1 模型的响应包括其 CoT,我们得到的响应格式如下:
<think>
Chain of Thought
</think>
Answer
不幸的是,由于这种独特的格式,当我们尝试将响应解析为 Java 类时,当前版本 Spring AI 中的所有结构化输出转换器(Converter)都会失败并抛出异常。
因此,需要创建自己的自定义结构化输出转换器(StructuredOutputConverter
)实现,分别解析 AI 模型的答案和 CoT:
record DeepSeekModelResponse(String chainOfThought, String answer) {
}
class DeepSeekModelOutputConverter implements StructuredOutputConverter<DeepSeekModelResponse> {
private static final String OPENING_THINK_TAG = "<think>";
private static final String CLOSING_THINK_TAG = "</think>";
@Override
public DeepSeekModelResponse convert(@NonNull String text) {
if (!StringUtils.hasText(text)) {
throw new IllegalArgumentException("Text cannot be blank");
}
int openingThinkTagIndex = text.indexOf(OPENING_THINK_TAG);
int closingThinkTagIndex = text.indexOf(CLOSING_THINK_TAG);
if (openingThinkTagIndex != -1 && closingThinkTagIndex != -1 && closingThinkTagIndex > openingThinkTagIndex) {
String chainOfThought = text.substring(openingThinkTagIndex + OPENING_THINK_TAG.length(), closingThinkTagIndex);
String answer = text.substring(closingThinkTagIndex + CLOSING_THINK_TAG.length());
return new DeepSeekModelResponse(chainOfThought, answer);
} else {
logger.debug("No <think> tags found in the response. Treating entire text as answer.");
return new DeepSeekModelResponse(null, text);
}
}
}
如上,我们的 Converter 会从 AI 模型的响应中提取思维链和答案,并将它们作为 DeepSeekModelResponse
record 返回。
如果 AI 回复不包含 <think>
标记,我们就会将整个回复视为答案。这确保了与其他不在回复中包含 CoT 的 DeepSeek 模型的兼容性。
3.3、实现 Service 层
配置就绪后,创建一个 ChatbotService
类,并注入之前定义的 ChatClient
Bean,以便与指定的 DeepSeek
模型交互。
首先,定义两个简单的 record
来表示聊天请求和响应:
record ChatRequest(@Nullable UUID chatId, String question) {}
record ChatResponse(UUID chatId, String chainOfThought, String answer) {}
ChatRequest
包含用户的问题和一个可选的 chatId
,用于识别正在进行的对话。
同样,ChatResponse
包含 chatId
以及聊天机器人的 chainOfThought
(思维链)和 answer
(回答)。
现在,让我们来实现预期的功能:
ChatResponse chat(ChatRequest chatRequest) {
UUID chatId = Optional
.ofNullable(chatRequest.chatId())
.orElse(UUID.randomUUID());
DeepSeekModelResponse response = chatClient
.prompt()
.user(chatRequest.question())
.advisors(advisorSpec ->
advisorSpec
.param("chat_memory_conversation_id", chatId))
.call()
.entity(new DeepSeekModelOutputConverter());
return new ChatResponse(chatId, response.chainOfThought(), response.answer());
}
如果收到的请求不包含 chatId
,我们就会生成一个新的 chatId
。这样,用户就可以开始新对话或继续现有对话。
我们将用户的 question
(问题)传递给 chatClient
Bean,并将 chat_memory_conversation_id
参数设置为已解析的 chatId
,以维护对话历史记录。
最后,我们创建自定义 DeepSeekModelOutputConverter
类的实例,并将其传递给 entity()
方法,以便将 AI 模型的响应解析为 DeepSeekModelResponse
记录。然后,我们从中提取 chainOfThought
(思维链)和 answer
(答案),并连同 chatId
一起返回。
3.4、与聊天机器人互动
实现了 Service 层后,创建 REST API:
@PostMapping("/chat")
ResponseEntity<ChatResponse> chat(@RequestBody ChatRequest chatRequest) {
ChatResponse chatResponse = chatbotService.chat(chatRequest);
return ResponseEntity.ok(chatResponse);
}
使用 HTTPie CLI 来调用上述 API 端点并开始新对话:
http POST :8080/chat question="What was the name of Superman's adoptive mother?"
如上,我们向聊天机器人发送了一个简单的问题,其回复如下:
{
"chatId": "1e3c15lf-cded-4f10-a5fc-c52c5952411c",
"chainOfThought": "Alright, so I need to figure out the name of Superman's adoptive mother. I'm not entirely sure, but I
remember that Superman's story involves him being found and adopted by humans. Let me try to recall the details. I
think Superman's real parents are from Krypton, and when he was a baby, they sent him to Earth in a small spaceship
before their planet was destroyed. On Earth, he was found by a couple who couldn't have children. I believe the
couple's last name is Kent, so his adoptive father is Jonathan Kent, and his mother would be Martha Kent. That sounds
familiar from the Superman lore I've come across. Wait, but I'm not 100% certain. Maybe I should think about
different versions or media where Superman appears. In the comics, movies, or TV shows, is the name consistent? For
example, in the classic Superman movies, like the ones with Christopher Reeve, I think they refer to his adoptive
parents as Jonathan and Martha Kent. In the more recent movies, like Man of Steel, they also use those names. So it
seems consistent across different media. Is there any variation where the names are different? I can't recall any.
Maybe in some alternate universe stories, but in the main DC Universe, it's Martha Kent. So I think the answer is
Martha Kent.",
"answer": "The name of Superman's adoptive mother is Martha Kent. She and her husband, Jonathan Kent, found Superman when
he was a baby and raised him on their farm in Smallville, Kansas. This is consistent across various Superman stories
in comics, movies, and TV shows."
}
回复包含一个唯一的 chatId
、聊天机器人的 chainOfThought
和对我们问题的回答。我们可以看到 AI 模型是如何使用 chainOfThought
属性推理和处理给定提示的。
继续对话,使用上述回复中的 chatId
发送一个后续问题:
http POST :8080/chat question="Which bald billionaire hates him?" chatId="1e3c151f-cded-4f10-a5fc-c52c5952411c"
看看聊天机器人能否保持我们对话的上下文,并提供相关的回复:
{
"chatId": "1e3c15lf-cded-4f10-a5fc-c52c5952411c",
"chainOfThought": "Alright, so I need to figure out the name of Superman's adoptive mother. I'm not entirely sure, but I
remember that Superman's story involves him being found and adopted by humans. Let me try to recall the details. I
think Superman's real parents are from Krypton, and when he was a baby, they sent him to Earth in a small spaceship
before their planet was destroyed. On Earth, he was found by a couple who couldn't have children. I believe the
couple's last name is Kent, so his adoptive father is Jonathan Kent, and his mother would be Martha Kent. That sounds
familiar from the Superman lore I've come across. Wait, but I'm not 100% certain. Maybe I should think about
different versions or media where Superman appears. In the comics, movies, or TV shows, is the name consistent? For
example, in the classic Superman movies, like the ones with Christopher Reeve, I think they refer to his adoptive
parents as Jonathan and Martha Kent. In the more recent movies, like Man of Steel, they also use those names. So it
seems consistent across different media. Is there any variation where the names are different? I can't recall any.
Maybe in some alternate universe stories, but in the main DC Universe, it's Martha Kent. So I think the answer is
Martha Kent.",
"answer": "The name of Superman's adoptive mother is Martha Kent. She and her husband, Jonathan Kent, found Superman when
he was a baby and raised him on their farm in Smallville, Kansas. This is consistent across various Superman stories
in comics, movies, and TV shows."
}
可以看到,聊天机器人确实保持了对话上下文。chatId
保持不变,表明后续回答是同一对话的继续。
4、总结
本文介绍了将 DeepSeek 模型集成到应用中的各种方案,首先是直接使用 OpenAI API(因为 DeepSeek 与之兼容),然后是使用亚马逊的 Bedrock Converse API,最后是使用 Ollama 建立本地测试环境。
Ref:https://www.baeldung.com/spring-ai-deepseek-cot