使用 Spring AI 的 DeepSeek 模型建立 AI 聊天機器人
1. 概述
現代網路應用程式越來越多地與大型語言模型 (LLM) 整合來建立解決方案。
DeepSeek 是一家中國人工智慧研究公司,致力於開發強大的 LLM,最近其DeepSeek-V3和DeepSeek-R1模型顛覆了人工智慧世界。後一種模型及其響應揭示了它的思路鏈(CoT),這讓我們了解了人工智慧模型如何解釋和處理給定的提示。
在本教程中,我們將探索將 DeepSeek 模型與 Spring AI 結合。我們將建立一個能夠進行多輪文字對話的簡單聊天機器人。
2.依賴和配置
有多種方法可以將 DeepSeek 模型整合到我們的應用程式中,在本節中,我們將討論一些流行的選項。我們可以選擇最適合我們要求的。
2.1.使用 OpenAI API
DeepSeek 模型與OpenAI API完全相容,可以透過任何 OpenAI 用戶端或程式庫存取。
讓我們先將 Spring AI 的OpenAI 啟動器依賴項新增到我們專案的pom.xml
檔案中:
<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
是一個里程碑版本,我們還需要將 Spring Milestones 儲存庫新增到我們的pom.xml
中:
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
此儲存庫是發布里程碑版本的地方,與標準 Maven Central 儲存庫不同。無論我們選擇哪種配置選項,我們都需要新增這個里程碑儲存庫。
接下來,讓我們在application.yaml
檔案中設定我們的DeepSeek API 金鑰和聊天模型:
spring:
ai:
openai:
api-key: ${DEEPSEEK_API_KEY}
chat:
options:
model: deepseek-reasoner
base-url: https://api.deepseek.com
embedding:
enabled: false
此外,我們指定 DeepSeek API 的基本 URL 並停用嵌入,因為 DeepSeek 目前不提供任何嵌入相容模型。
配置上述屬性後, 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託管。可以參考此部署指南進行設定。
讓我們先將Bedrock Converse 啟動器依賴項加入到我們的pom.xml
中:
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bedrock-converse-spring-boot-starter</artifactId>
<version>1.0.0-M6</version>
</dependency>
接下來,為了與 Amazon Bedrock 交互,我們需要在application.yaml
檔案中配置用於身份驗證的 AWS 憑證以及託管 DeepSeek 模型的區域:
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 啟動器依賴項可協助我們與 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
預設連接埠11434
上運作時,會自動連線到 Ollama。但是,我們可以使用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.定義聊天機器人 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 中存在的所有結構化輸出轉換器都會失敗並引發異常。
因此,讓我們創建自己的自訂StructuredOutputConverter
實作來分別解析 AI 模型的答案和 CoT :
`record DeepSeekModelResponse(String chainOfThought, String answer) {
}
class DeepSeekModelOutputConverter implements StructuredOutputConverter
private static final String OPENING_THINK_TAG = "
private static final String CLOSING_THINK_TAG = "";
@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
return new DeepSeekModelResponse(null, text);
}
}
}`
在這裡,我們的轉換器從 AI 模型的回應中提取chainOfThought
和answer
,並將它們作為DeepSeekModelResponse
記錄返回。
如果 AI 回應不包含<think>
標籤,我們會將整個回應視為答案。這確保了與其他回應中不包含 CoT 的 DeepSeek 模型的兼容性。
3.3.實現服務層
配置完成後,讓我們建立一個ChatbotService
類別。我們將注入先前定義的ChatClient
bean 來與指定的 DeepSeek 模型進行互動。
但首先,讓我們定義兩個簡單的記錄來表示聊天請求和回應:
record ChatRequest(@Nullable UUID chatId, String question) {}
record ChatResponse(UUID chatId, String chainOfThought, String answer) {}
ChatRequest
包含使用者的question
和一個可選的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
,我們將產生一個新的。這允許用戶開始新的對話或繼續現有的對話。
我們將使用者的question
傳遞給chatClient
bean,並將chat_memory_conversation_id
參數設為已解析的chatId
,以維護對話歷史記錄。
最後,我們建立自訂DeepSeekModelOutputConverter
類別的實例並將其傳遞給entity()
方法,以將 AI 模型的回應解析為DeepSeekModelResponse
記錄。然後,我們從中提取chainOfThought
和answer
,並將它們與 chatId 一起返回。
3.4.與我們的聊天機器人互動
現在我們已經實作了服務層,讓我們在其上公開一個 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?"
在這裡,我們向聊天機器人發送一個簡單的question
,讓我們看看我們收到的答案:
回應包含唯一的chatId
,以及聊天機器人的chainOfThought
和對我們的問題的answer
。我們可以看到 AI 模型如何使用chainOfThought
屬性來推理並解決給定的提示。
讓我們透過使用上述回覆中的chatId
發送後續question
來繼續此對話:
http POST :8080/chat question="Which bald billionaire hates him?" chatId="1e3c151f-cded-4f10-a5fc-c52c5952411c"
讓我們看看聊天機器人是否能保持我們談話的上下文並提供相關的回應:
我們可以看到,聊天機器人確實維持了對話脈絡。 chatId
保持不變,顯示後續answer
是同一次對話的延續。
4. 結論
在本文中,我們探索了將 DeepSeek 模型與 Spring AI 結合使用。
我們討論了將 DeepSeek 模型整合到我們的應用程式中的各種選項,其中一種是直接使用 OpenAI API,因為 DeepSeek 與它相容,另一種是使用亞馬遜的 Bedrock Converse API。此外,我們也探索了使用 Ollama 建立本機測試環境。
然後,我們建立了一個能夠進行多輪文字對話的簡單聊天機器人,並使用自訂的StructuredOutputConverter
實作從 AI 模型的回應中提取思路和答案鏈。
與往常一樣,本文使用的所有程式碼範例均可在 GitHub 上找到。