From 2ba64e6d78e411f214e6cb8a2667a645e06f4055 Mon Sep 17 00:00:00 2001
From: GlassCat
Date: Sat, 7 Jun 2025 22:42:50 +0800
Subject: [PATCH 01/31] feat: add vector store redis simple
---
spring-ai-vector/pom.xml | 1 +
.../spring-ai-vector-redis/pom.xml | 27 +++++++++
.../ai/vector/RedisVectorApplication.java | 16 +++++
.../ai/vector/storage/VectorStoreStorage.java | 42 +++++++++++++
.../src/main/resources/application.yml | 26 ++++++++
.../java/storage/VectorStoreStorageTest.java | 59 +++++++++++++++++++
.../src/test/resources/application-test.yml | 26 ++++++++
7 files changed, 197 insertions(+)
create mode 100644 spring-ai-vector/spring-ai-vector-redis/pom.xml
create mode 100644 spring-ai-vector/spring-ai-vector-redis/src/main/java/com/glmapper/ai/vector/RedisVectorApplication.java
create mode 100644 spring-ai-vector/spring-ai-vector-redis/src/main/java/com/glmapper/ai/vector/storage/VectorStoreStorage.java
create mode 100644 spring-ai-vector/spring-ai-vector-redis/src/main/resources/application.yml
create mode 100644 spring-ai-vector/spring-ai-vector-redis/src/test/java/storage/VectorStoreStorageTest.java
create mode 100644 spring-ai-vector/spring-ai-vector-redis/src/test/resources/application-test.yml
diff --git a/spring-ai-vector/pom.xml b/spring-ai-vector/pom.xml
index 35e7aac..27930c1 100644
--- a/spring-ai-vector/pom.xml
+++ b/spring-ai-vector/pom.xml
@@ -12,6 +12,7 @@
spring-ai-embedding
spring-ai-vector-milvus
+ spring-ai-vector-redis
diff --git a/spring-ai-vector/spring-ai-vector-redis/pom.xml b/spring-ai-vector/spring-ai-vector-redis/pom.xml
new file mode 100644
index 0000000..1786114
--- /dev/null
+++ b/spring-ai-vector/spring-ai-vector-redis/pom.xml
@@ -0,0 +1,27 @@
+
+
+ 4.0.0
+
+ com.glmapper
+ spring-ai-vector
+ 0.0.1
+
+
+ spring-ai-vector-redis
+ jar
+
+
+ UTF-8
+
+
+
+
+
+ org.springframework.ai
+ spring-ai-starter-vector-store-redis
+
+
+
+
\ No newline at end of file
diff --git a/spring-ai-vector/spring-ai-vector-redis/src/main/java/com/glmapper/ai/vector/RedisVectorApplication.java b/spring-ai-vector/spring-ai-vector-redis/src/main/java/com/glmapper/ai/vector/RedisVectorApplication.java
new file mode 100644
index 0000000..bf7876b
--- /dev/null
+++ b/spring-ai-vector/spring-ai-vector-redis/src/main/java/com/glmapper/ai/vector/RedisVectorApplication.java
@@ -0,0 +1,16 @@
+package com.glmapper.ai.vector;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+/**
+ * Hello world!
+ *
+ * @author GlassCat
+ */
+@SpringBootApplication
+public class RedisVectorApplication {
+ public static void main(String[] args) {
+ SpringApplication.run(RedisVectorApplication.class, args);
+ }
+}
diff --git a/spring-ai-vector/spring-ai-vector-redis/src/main/java/com/glmapper/ai/vector/storage/VectorStoreStorage.java b/spring-ai-vector/spring-ai-vector-redis/src/main/java/com/glmapper/ai/vector/storage/VectorStoreStorage.java
new file mode 100644
index 0000000..5888356
--- /dev/null
+++ b/spring-ai-vector/spring-ai-vector-redis/src/main/java/com/glmapper/ai/vector/storage/VectorStoreStorage.java
@@ -0,0 +1,42 @@
+package com.glmapper.ai.vector.storage;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.ai.document.Document;
+import org.springframework.ai.vectorstore.SearchRequest;
+import org.springframework.ai.vectorstore.VectorStore;
+import org.springframework.stereotype.Component;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * @author GlassCat
+ * @since 2025/6/7
+ */
+@Component
+@RequiredArgsConstructor
+public class VectorStoreStorage {
+
+ private final VectorStore vectorStore;
+
+
+ public void delete(Set ids) {
+ vectorStore.delete(new ArrayList<>(ids));
+ }
+
+ public void store(List documents) {
+ if (documents == null || documents.isEmpty()) {
+ return;
+ }
+ vectorStore.add(documents);
+ }
+
+ public List search(String query) {
+ return vectorStore.similaritySearch(SearchRequest.builder()
+ .query(query)
+ .topK(5)
+ .similarityThreshold(0.7)
+ .build());
+ }
+}
diff --git a/spring-ai-vector/spring-ai-vector-redis/src/main/resources/application.yml b/spring-ai-vector/spring-ai-vector-redis/src/main/resources/application.yml
new file mode 100644
index 0000000..49c7096
--- /dev/null
+++ b/spring-ai-vector/spring-ai-vector-redis/src/main/resources/application.yml
@@ -0,0 +1,26 @@
+server:
+ port: 8080
+
+spring:
+ application:
+ name: redis-vector-store
+ data:
+ redis:
+ host: ${REDIS_HOST:localhost}
+ port: ${REDIS_PORT:6379}
+ username: ${REDIS_USERNAME:}
+ password: ${REDIS_PASSWORD:123456}
+ ai:
+ openai:
+ api-key: ${QWEN_API_KEY}
+ embedding:
+ # doc reference: https://bailian.console.aliyun.com/?switchAgent=12095181&productCode=p_efm&switchUserType=3&tab=api#/api/?type=model&url=https%3A%2F%2Fhelp.aliyun.com%2Fdocument_detail%2F2712515.html&renderType=iframe
+ base-url: https://dashscope.aliyuncs.com/compatible-mode/v1
+ embeddings-path: /embeddings
+ options:
+ model: text-embedding-v4
+ vectorstore:
+ redis:
+ initialize-schema: true
+ index-name: glmapper
+ prefix: glmapper
diff --git a/spring-ai-vector/spring-ai-vector-redis/src/test/java/storage/VectorStoreStorageTest.java b/spring-ai-vector/spring-ai-vector-redis/src/test/java/storage/VectorStoreStorageTest.java
new file mode 100644
index 0000000..d67796f
--- /dev/null
+++ b/spring-ai-vector/spring-ai-vector-redis/src/test/java/storage/VectorStoreStorageTest.java
@@ -0,0 +1,59 @@
+package storage;
+
+import com.glmapper.ai.vector.RedisVectorApplication;
+import com.glmapper.ai.vector.storage.VectorStoreStorage;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.springframework.ai.document.Document;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.ActiveProfiles;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * 测试 基于 redis 的 VectorStoreStorage 的存储和搜索功能
+ *
+ * @author GlassCat
+ * @since 2025/6/7
+ */
+@SpringBootTest(
+ classes = RedisVectorApplication.class,
+ webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT
+)
+@ActiveProfiles("test")
+public class VectorStoreStorageTest {
+
+ @Autowired
+ private VectorStoreStorage vectorStoreStorage;
+
+ //prepare test data
+ private static final List DOCS = List.of(
+ new Document("Spring AI rocks!! Spring AI rocks!! Spring AI rocks!! Spring AI rocks!! Spring AI rocks!!", Map.of("test-data", "true")),
+ new Document("The World is Big and Salvation Lurks Around the Corner", Map.of("test-data", "true")),
+ new Document("You walk forward facing the past and you turn back toward the future.", Map.of("test-data", "true")));
+
+
+ @AfterEach
+ public void cleanUp() {
+ // clear the vector store after each test
+ Set ids = DOCS.stream().map(Document::getId)
+ .collect(Collectors.toSet());
+ vectorStoreStorage.delete(ids);
+ }
+
+ @Test
+ public void testStoreAndSearch() {
+ // store documents
+ vectorStoreStorage.store(DOCS);
+ // do search
+ String query = "Spring AI rocks!!";
+ List results = vectorStoreStorage.search(query);
+ // assertions
+ Assertions.assertFalse(results.isEmpty(), "搜索结果不应该为空");
+ }
+}
diff --git a/spring-ai-vector/spring-ai-vector-redis/src/test/resources/application-test.yml b/spring-ai-vector/spring-ai-vector-redis/src/test/resources/application-test.yml
new file mode 100644
index 0000000..49c7096
--- /dev/null
+++ b/spring-ai-vector/spring-ai-vector-redis/src/test/resources/application-test.yml
@@ -0,0 +1,26 @@
+server:
+ port: 8080
+
+spring:
+ application:
+ name: redis-vector-store
+ data:
+ redis:
+ host: ${REDIS_HOST:localhost}
+ port: ${REDIS_PORT:6379}
+ username: ${REDIS_USERNAME:}
+ password: ${REDIS_PASSWORD:123456}
+ ai:
+ openai:
+ api-key: ${QWEN_API_KEY}
+ embedding:
+ # doc reference: https://bailian.console.aliyun.com/?switchAgent=12095181&productCode=p_efm&switchUserType=3&tab=api#/api/?type=model&url=https%3A%2F%2Fhelp.aliyun.com%2Fdocument_detail%2F2712515.html&renderType=iframe
+ base-url: https://dashscope.aliyuncs.com/compatible-mode/v1
+ embeddings-path: /embeddings
+ options:
+ model: text-embedding-v4
+ vectorstore:
+ redis:
+ initialize-schema: true
+ index-name: glmapper
+ prefix: glmapper
From 6b92b99bf60782ead1b75421cd64f852c21ddc0f Mon Sep 17 00:00:00 2001
From: GlassCat
Date: Sat, 7 Jun 2025 23:14:06 +0800
Subject: [PATCH 02/31] feat: add README.md
---
.../spring-ai-vector-redis/README.md | 81 +++++++++++++++++++
.../src/main/resources/application.yml | 2 +-
.../src/test/resources/application-test.yml | 2 +-
3 files changed, 83 insertions(+), 2 deletions(-)
create mode 100644 spring-ai-vector/spring-ai-vector-redis/README.md
diff --git a/spring-ai-vector/spring-ai-vector-redis/README.md b/spring-ai-vector/spring-ai-vector-redis/README.md
new file mode 100644
index 0000000..1764e29
--- /dev/null
+++ b/spring-ai-vector/spring-ai-vector-redis/README.md
@@ -0,0 +1,81 @@
+# spring-ai-vector-redis
+
+该工程模块主要是集成 Redis 的向量存储功能,提供了一个使用 Redis 存储向量并执行相似性搜索的简单示例。
+
+[Redis Search and Query](https://redis.io/docs/interact/search-and-query/) 扩展了 Redis OSS 的核心功能,使你可以将 Redis 用作向量数据库:
+
+- 在哈希或 JSON 文档中存储向量及其关联的元数据
+- 检索向量
+- 执行向量搜索
+
+## 前提条件
+
+① 一个 Redis Stack 实例,这里使用 [Docker](https://hub.docker.com/r/redis/redis-stack) 镜像 redis/redis-stack:latest,你可以直接使用下面的命令本地启动 Redis
+
+```bash
+docker run -d \
+--name redis-stack \
+-p 6379:6379 -p 8001:8001 \
+-v /your_path/data:/data \
+-e REDIS_ARGS="--requirepass 123456" \
+redis/redis-stack:latest
+```
+
+② 一个 `EmbeddingModel` 实例,用于计算文档嵌入。有多个[选项](https://docs.spring.io/spring-ai/reference/api/embeddings.html#available-implementations)可供选择
+
+③ 一个 API 密钥,给 EmbeddingModel 用于生成向量数据
+
+
+
+
+
+这里使用的是 Redis 来存储向量数据的, 对应的依赖是 `spring-ai-starter-vector-store-redis`,如下:
+```xml
+
+
+ org.springframework.ai
+ spring-ai-starter-vector-store-redis
+
+
+```
+
+
+
+### 配置文件
+
+在你启动项目之前,你需要修改 `application.yml` 文件。
+
+```yaml
+server:
+ port: 8080
+
+spring:
+ application:
+ name: redis-vector-store
+ data:
+ redis:
+ host: ${REDIS_HOST:localhost}
+ port: ${REDIS_PORT:6379}
+ username: ${REDIS_USERNAME:}
+ password: ${REDIS_PASSWORD:123456}
+ ai:
+ openai:
+ api-key: ${YOUR_QWEN_API_KEY}
+ embedding:
+ base-url: https://dashscope.aliyuncs.com/compatible-mode/v1
+ embeddings-path: /embeddings
+ options:
+ model: text-embedding-v4
+ vectorstore:
+ redis:
+ # 是否初始化所需的索引结构
+ initialize-schema: true
+ # 用于存储向量的索引名称
+ index-name: glmapper
+ # Redis 键的前缀
+ prefix: glmapper_
+```
+修改完成之后即可以在 IDEA 中启动单元测试。
+
+
+
diff --git a/spring-ai-vector/spring-ai-vector-redis/src/main/resources/application.yml b/spring-ai-vector/spring-ai-vector-redis/src/main/resources/application.yml
index 49c7096..16e96b4 100644
--- a/spring-ai-vector/spring-ai-vector-redis/src/main/resources/application.yml
+++ b/spring-ai-vector/spring-ai-vector-redis/src/main/resources/application.yml
@@ -23,4 +23,4 @@ spring:
redis:
initialize-schema: true
index-name: glmapper
- prefix: glmapper
+ prefix: glmapper_
diff --git a/spring-ai-vector/spring-ai-vector-redis/src/test/resources/application-test.yml b/spring-ai-vector/spring-ai-vector-redis/src/test/resources/application-test.yml
index 49c7096..16e96b4 100644
--- a/spring-ai-vector/spring-ai-vector-redis/src/test/resources/application-test.yml
+++ b/spring-ai-vector/spring-ai-vector-redis/src/test/resources/application-test.yml
@@ -23,4 +23,4 @@ spring:
redis:
initialize-schema: true
index-name: glmapper
- prefix: glmapper
+ prefix: glmapper_
From 4cf14a6a4bd516d9b06ed38dca9822f3bcfe9bda Mon Sep 17 00:00:00 2001
From: GlassCat
Date: Sat, 7 Jun 2025 23:21:51 +0800
Subject: [PATCH 03/31] feat: optimiz README.md
---
spring-ai-vector/spring-ai-vector-redis/README.md | 9 +++------
1 file changed, 3 insertions(+), 6 deletions(-)
diff --git a/spring-ai-vector/spring-ai-vector-redis/README.md b/spring-ai-vector/spring-ai-vector-redis/README.md
index 1764e29..5af792a 100644
--- a/spring-ai-vector/spring-ai-vector-redis/README.md
+++ b/spring-ai-vector/spring-ai-vector-redis/README.md
@@ -26,10 +26,9 @@ redis/redis-stack:latest
③ 一个 API 密钥,给 EmbeddingModel 用于生成向量数据
+## 自动配置
+Spring AI 为 Redis 向量数据库提供了 Spring Boot 自动配置。要启用它,请将以下依赖添加到项目的 Maven pom.xml 文件中:
-
-
-这里使用的是 Redis 来存储向量数据的, 对应的依赖是 `spring-ai-starter-vector-store-redis`,如下:
```xml
@@ -39,9 +38,7 @@ redis/redis-stack:latest
```
-
-
-### 配置文件
+## 配置文件
在你启动项目之前,你需要修改 `application.yml` 文件。
From 808127633f04e20a7e2f4d5f439136bec22db1d9 Mon Sep 17 00:00:00 2001
From: Gepeng18
Date: Tue, 10 Jun 2025 19:22:41 +0800
Subject: [PATCH 04/31] feat: build_agent_with_workflow
---
pom.xml | 1 +
spring-ai-agent/pom.xml | 23 +++++
spring-ai-agent/spring-ai-workflow/README.md | 1 +
spring-ai-agent/spring-ai-workflow/pom.xml | 25 +++++
.../ai/workflow/WorkflowApplication.java | 13 +++
.../config/OpenaiChatClientConfigs.java | 30 ++++++
.../ai/workflow/core/WorkflowFactory.java | 58 +++++++++++
.../ai/workflow/core/WorkflowStepFactory.java | 48 +++++++++
.../ai/workflow/core/step/WorkflowStep.java | 27 ++++++
.../core/step/impl/DefaultWorkflowStep.java | 35 +++++++
.../step/impl/RouterSelectorWorkflowStep.java | 80 +++++++++++++++
.../ai/workflow/core/workflow/Workflow.java | 23 +++++
.../core/workflow/impl/ChainWorkflow.java | 54 +++++++++++
.../impl/ParallelizationWorkflow.java | 97 +++++++++++++++++++
.../core/workflow/impl/RoutingWorkflow.java | 68 +++++++++++++
.../ai/workflow/model/WorkflowRequest.java | 22 +++++
.../ai/workflow/model/WorkflowResponse.java | 32 ++++++
.../src/main/resources/application.yml | 14 +++
.../ai/workflow/ChainWorkflowTest.java | 64 ++++++++++++
.../workflow/ParallelizationWorkflowTest.java | 66 +++++++++++++
.../ai/workflow/RoutingWorkflowTest.java | 82 ++++++++++++++++
21 files changed, 863 insertions(+)
create mode 100644 spring-ai-agent/pom.xml
create mode 100644 spring-ai-agent/spring-ai-workflow/README.md
create mode 100644 spring-ai-agent/spring-ai-workflow/pom.xml
create mode 100644 spring-ai-agent/spring-ai-workflow/src/main/java/com/glmapper/ai/workflow/WorkflowApplication.java
create mode 100644 spring-ai-agent/spring-ai-workflow/src/main/java/com/glmapper/ai/workflow/config/OpenaiChatClientConfigs.java
create mode 100644 spring-ai-agent/spring-ai-workflow/src/main/java/com/glmapper/ai/workflow/core/WorkflowFactory.java
create mode 100644 spring-ai-agent/spring-ai-workflow/src/main/java/com/glmapper/ai/workflow/core/WorkflowStepFactory.java
create mode 100644 spring-ai-agent/spring-ai-workflow/src/main/java/com/glmapper/ai/workflow/core/step/WorkflowStep.java
create mode 100644 spring-ai-agent/spring-ai-workflow/src/main/java/com/glmapper/ai/workflow/core/step/impl/DefaultWorkflowStep.java
create mode 100644 spring-ai-agent/spring-ai-workflow/src/main/java/com/glmapper/ai/workflow/core/step/impl/RouterSelectorWorkflowStep.java
create mode 100644 spring-ai-agent/spring-ai-workflow/src/main/java/com/glmapper/ai/workflow/core/workflow/Workflow.java
create mode 100644 spring-ai-agent/spring-ai-workflow/src/main/java/com/glmapper/ai/workflow/core/workflow/impl/ChainWorkflow.java
create mode 100644 spring-ai-agent/spring-ai-workflow/src/main/java/com/glmapper/ai/workflow/core/workflow/impl/ParallelizationWorkflow.java
create mode 100644 spring-ai-agent/spring-ai-workflow/src/main/java/com/glmapper/ai/workflow/core/workflow/impl/RoutingWorkflow.java
create mode 100644 spring-ai-agent/spring-ai-workflow/src/main/java/com/glmapper/ai/workflow/model/WorkflowRequest.java
create mode 100644 spring-ai-agent/spring-ai-workflow/src/main/java/com/glmapper/ai/workflow/model/WorkflowResponse.java
create mode 100644 spring-ai-agent/spring-ai-workflow/src/main/resources/application.yml
create mode 100644 spring-ai-agent/spring-ai-workflow/src/test/java/com/glmapper/ai/workflow/ChainWorkflowTest.java
create mode 100644 spring-ai-agent/spring-ai-workflow/src/test/java/com/glmapper/ai/workflow/ParallelizationWorkflowTest.java
create mode 100644 spring-ai-agent/spring-ai-workflow/src/test/java/com/glmapper/ai/workflow/RoutingWorkflowTest.java
diff --git a/pom.xml b/pom.xml
index d2674b3..36fa265 100644
--- a/pom.xml
+++ b/pom.xml
@@ -21,6 +21,7 @@
spring-ai-evaluation
spring-ai-chat-memory
spring-ai-tool-calling
+ spring-ai-agent
diff --git a/spring-ai-agent/pom.xml b/spring-ai-agent/pom.xml
new file mode 100644
index 0000000..00c241f
--- /dev/null
+++ b/spring-ai-agent/pom.xml
@@ -0,0 +1,23 @@
+
+
+ 4.0.0
+
+ com.glmapper
+ spring-ai-summary
+ 0.0.1
+
+ spring-ai-agent
+ pom
+ spring-ai-agent
+
+
+ 21
+ 1.0.0
+
+
+
+ spring-ai-workflow
+
+
+
\ No newline at end of file
diff --git a/spring-ai-agent/spring-ai-workflow/README.md b/spring-ai-agent/spring-ai-workflow/README.md
new file mode 100644
index 0000000..4f81f1f
--- /dev/null
+++ b/spring-ai-agent/spring-ai-workflow/README.md
@@ -0,0 +1 @@
+# todo 待补充
\ No newline at end of file
diff --git a/spring-ai-agent/spring-ai-workflow/pom.xml b/spring-ai-agent/spring-ai-workflow/pom.xml
new file mode 100644
index 0000000..105ca73
--- /dev/null
+++ b/spring-ai-agent/spring-ai-workflow/pom.xml
@@ -0,0 +1,25 @@
+
+
+ 4.0.0
+
+ com.glmapper
+ spring-ai-agent
+ 0.0.1
+
+ spring-ai-workflow
+ spring-ai-workflow
+ Spring AI Workflow Module
+
+
+
+ org.springframework.ai
+ spring-ai-starter-model-openai
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
\ No newline at end of file
diff --git a/spring-ai-agent/spring-ai-workflow/src/main/java/com/glmapper/ai/workflow/WorkflowApplication.java b/spring-ai-agent/spring-ai-workflow/src/main/java/com/glmapper/ai/workflow/WorkflowApplication.java
new file mode 100644
index 0000000..2632c86
--- /dev/null
+++ b/spring-ai-agent/spring-ai-workflow/src/main/java/com/glmapper/ai/workflow/WorkflowApplication.java
@@ -0,0 +1,13 @@
+package com.glmapper.ai.workflow;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+
+@SpringBootApplication
+public class WorkflowApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(WorkflowApplication.class, args);
+ }
+}
\ No newline at end of file
diff --git a/spring-ai-agent/spring-ai-workflow/src/main/java/com/glmapper/ai/workflow/config/OpenaiChatClientConfigs.java b/spring-ai-agent/spring-ai-workflow/src/main/java/com/glmapper/ai/workflow/config/OpenaiChatClientConfigs.java
new file mode 100644
index 0000000..345fce7
--- /dev/null
+++ b/spring-ai-agent/spring-ai-workflow/src/main/java/com/glmapper/ai/workflow/config/OpenaiChatClientConfigs.java
@@ -0,0 +1,30 @@
+package com.glmapper.ai.workflow.config;
+
+import org.springframework.ai.chat.client.ChatClient;
+import org.springframework.ai.openai.OpenAiChatModel;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+
+/**
+ * @Classname OpenaiChatClientConfigs
+ * @Description 注入 ChatClient
+ *
+ * @Date 2025/6/10 09:23
+ * @Created by Gepeng18
+ */
+@Configuration
+public class OpenaiChatClientConfigs {
+
+ /**
+ * 注入ChatClient
+ *
+ * @param chatModel
+ * @return
+ */
+ @Bean
+ public ChatClient chatClient(OpenAiChatModel chatModel) {
+ return ChatClient.builder(chatModel)
+ .build();
+ }
+}
\ No newline at end of file
diff --git a/spring-ai-agent/spring-ai-workflow/src/main/java/com/glmapper/ai/workflow/core/WorkflowFactory.java b/spring-ai-agent/spring-ai-workflow/src/main/java/com/glmapper/ai/workflow/core/WorkflowFactory.java
new file mode 100644
index 0000000..ba07ee1
--- /dev/null
+++ b/spring-ai-agent/spring-ai-workflow/src/main/java/com/glmapper/ai/workflow/core/WorkflowFactory.java
@@ -0,0 +1,58 @@
+package com.glmapper.ai.workflow.core;
+
+import com.glmapper.ai.workflow.core.workflow.Workflow;
+import com.glmapper.ai.workflow.core.step.WorkflowStep;
+import com.glmapper.ai.workflow.core.workflow.impl.ChainWorkflow;
+import com.glmapper.ai.workflow.core.workflow.impl.ParallelizationWorkflow;
+import com.glmapper.ai.workflow.core.workflow.impl.RoutingWorkflow;
+import lombok.AllArgsConstructor;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * @Classname WorkflowFactory
+ * @Description 工作流工厂
+ *
+ * @Date 2025/6/10 15:40
+ * @Created by Gepeng18
+ */
+@Component
+@AllArgsConstructor
+public class WorkflowFactory {
+
+ private final WorkflowStepFactory workflowStepFactory;
+
+ /**
+ * 链式工作流实现:按顺序执行一系列工作流步骤,前一步骤的输出作为后一步骤的输入
+ *
+ * @param steps 工作流步骤
+ * @return 创建的工作流
+ */
+ public Workflow createChainWorkflow(List steps) {
+ return new ChainWorkflow(steps);
+ }
+
+ /**
+ * 并行工作流实现:同时执行多个工作流步骤,所有步骤使用相同的输入,最终结果是所有步骤结果的集合
+ *
+ * @param steps 工作流步骤
+ * @return 创建的工作流
+ */
+ public Workflow createParallelizationWorkflow(List steps) {
+ return new ParallelizationWorkflow(steps);
+ }
+
+ /**
+ * 路由工作流实现:基于路由规则选择合适的工作流步骤执行
+ *
+ * @param stepMap 工作流步骤
+ * @return 创建的工作流
+ */
+ public Workflow createRoutingWorkflow(Map stepMap) {
+ // 创建AI路由选择器
+ WorkflowStep routerSelector = workflowStepFactory.createAiRouterSelector("AI路由选择器", stepMap);
+ return new RoutingWorkflow(routerSelector, stepMap);
+ }
+}
\ No newline at end of file
diff --git a/spring-ai-agent/spring-ai-workflow/src/main/java/com/glmapper/ai/workflow/core/WorkflowStepFactory.java b/spring-ai-agent/spring-ai-workflow/src/main/java/com/glmapper/ai/workflow/core/WorkflowStepFactory.java
new file mode 100644
index 0000000..e4db288
--- /dev/null
+++ b/spring-ai-agent/spring-ai-workflow/src/main/java/com/glmapper/ai/workflow/core/WorkflowStepFactory.java
@@ -0,0 +1,48 @@
+package com.glmapper.ai.workflow.core;
+
+import com.glmapper.ai.workflow.core.step.WorkflowStep;
+import com.glmapper.ai.workflow.core.step.impl.DefaultWorkflowStep;
+import com.glmapper.ai.workflow.core.step.impl.RouterSelectorWorkflowStep;
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.ai.chat.client.ChatClient;
+import org.springframework.stereotype.Service;
+
+import java.util.Map;
+
+/**
+ * @Classname WorkflowStepFactory
+ * @Description 提供创建AI工作流步骤的功能
+ *
+ * @Date 2025/6/10 15:40
+ * @Created by Gepeng18
+ */
+@Service
+@Slf4j
+@AllArgsConstructor
+public class WorkflowStepFactory {
+
+ private final ChatClient chatClient;
+
+ /**
+ * 创建AI工作流步骤
+ *
+ * @param name 步骤名称
+ * @param promptTemplate 提示词模板
+ * @return 工作流步骤
+ */
+ public WorkflowStep createAiStep(String name, String promptTemplate) {
+ return new DefaultWorkflowStep(name, promptTemplate, chatClient);
+ }
+
+ /**
+ * 创建AI路由选择器
+ *
+ * @param name 步骤名称
+ * @param stepMap 步骤映射
+ * @return AI路由选择器
+ */
+ public RouterSelectorWorkflowStep createAiRouterSelector(String name, Map stepMap) {
+ return new RouterSelectorWorkflowStep(chatClient, stepMap, name);
+ }
+}
\ No newline at end of file
diff --git a/spring-ai-agent/spring-ai-workflow/src/main/java/com/glmapper/ai/workflow/core/step/WorkflowStep.java b/spring-ai-agent/spring-ai-workflow/src/main/java/com/glmapper/ai/workflow/core/step/WorkflowStep.java
new file mode 100644
index 0000000..f5d718e
--- /dev/null
+++ b/spring-ai-agent/spring-ai-workflow/src/main/java/com/glmapper/ai/workflow/core/step/WorkflowStep.java
@@ -0,0 +1,27 @@
+package com.glmapper.ai.workflow.core.step;
+
+
+/**
+ * @Classname WorkflowStep
+ * @Description 工作流步骤接口
+ *
+ * @Date 2025/6/10 11:40
+ * @Created by Gepeng18
+ */
+public interface WorkflowStep {
+
+ /**
+ * 执行步骤
+ *
+ * @param input 输入数据
+ * @return 步骤执行结果
+ */
+ Object execute(Object input);
+
+ /**
+ * 获取步骤名称
+ *
+ * @return 步骤名称
+ */
+ String name();
+}
\ No newline at end of file
diff --git a/spring-ai-agent/spring-ai-workflow/src/main/java/com/glmapper/ai/workflow/core/step/impl/DefaultWorkflowStep.java b/spring-ai-agent/spring-ai-workflow/src/main/java/com/glmapper/ai/workflow/core/step/impl/DefaultWorkflowStep.java
new file mode 100644
index 0000000..317a3f7
--- /dev/null
+++ b/spring-ai-agent/spring-ai-workflow/src/main/java/com/glmapper/ai/workflow/core/step/impl/DefaultWorkflowStep.java
@@ -0,0 +1,35 @@
+package com.glmapper.ai.workflow.core.step.impl;
+
+import com.glmapper.ai.workflow.core.step.WorkflowStep;
+import org.springframework.ai.chat.client.ChatClient;
+import org.springframework.ai.chat.messages.SystemMessage;
+import org.springframework.ai.chat.messages.UserMessage;
+import org.springframework.ai.chat.prompt.Prompt;
+
+
+/**
+ * @Classname DefaultWorkflowStep
+ * @Description 工作流步骤
+ *
+ * @Date 2025/6/10 17:23
+ * @Created by Gepeng18
+ */
+public record DefaultWorkflowStep(String name, String promptTemplate, ChatClient chatClient) implements WorkflowStep {
+
+ /**
+ * 执行本步骤
+ *
+ * @param input 输入数据
+ * @return 步骤执行结果
+ */
+ @Override
+ public Object execute(Object input) {
+ String inputStr = input != null ? input.toString() : "";
+ Prompt prompt = new Prompt(
+ new SystemMessage(promptTemplate),
+ new UserMessage(inputStr)
+ );
+
+ return chatClient.prompt(prompt).call().content();
+ }
+}
\ No newline at end of file
diff --git a/spring-ai-agent/spring-ai-workflow/src/main/java/com/glmapper/ai/workflow/core/step/impl/RouterSelectorWorkflowStep.java b/spring-ai-agent/spring-ai-workflow/src/main/java/com/glmapper/ai/workflow/core/step/impl/RouterSelectorWorkflowStep.java
new file mode 100644
index 0000000..3bdf58a
--- /dev/null
+++ b/spring-ai-agent/spring-ai-workflow/src/main/java/com/glmapper/ai/workflow/core/step/impl/RouterSelectorWorkflowStep.java
@@ -0,0 +1,80 @@
+package com.glmapper.ai.workflow.core.step.impl;
+
+import com.glmapper.ai.workflow.core.step.WorkflowStep;
+import com.glmapper.ai.workflow.model.WorkflowRequest;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.ai.chat.client.ChatClient;
+import org.springframework.ai.chat.messages.SystemMessage;
+import org.springframework.ai.chat.messages.UserMessage;
+import org.springframework.ai.chat.prompt.Prompt;
+
+import javax.validation.constraints.Null;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * @Classname RouterSelectorWorkflowStep
+ * @Description AI路由选择器实现WorkflowStep接口,用于根据用户问题选择合适的路由
+ *
+ * @Date 2025/6/10 16:36
+ * @Created by Gepeng18
+ */
+@Slf4j
+public class RouterSelectorWorkflowStep implements WorkflowStep {
+
+ private final ChatClient chatClient;
+ private final Map stepMap;
+ private final String name;
+
+ public RouterSelectorWorkflowStep(ChatClient chatClient, Map stepMap, String name) {
+ this.chatClient = chatClient;
+ this.stepMap = stepMap;
+ this.name = name;
+ }
+
+ /**
+ * 执行步骤
+ *
+ * @param input 输入数据
+ * @return 步骤执行结果
+ */
+ @Null
+ @Override
+ public Object execute(Object input) {
+ if (!(input instanceof WorkflowRequest)) {
+ throw new IllegalArgumentException("Input must be of type WorkflowRequest");
+ }
+
+ WorkflowRequest request = (WorkflowRequest) input;
+
+ // 构建提示文本
+ String routeInfo = stepMap.entrySet().stream()
+ .map(entry -> "- " + entry.getKey() + ": " + entry.getValue().name())
+ .collect(Collectors.joining("\n"));
+
+ String promptTemplate = "你是一个专业的路由选择器。根据用户的问题,从以下可用的路由中选择最合适的一个:\n\n" +
+ "可用路由:\n" + routeInfo + "\n\n" +
+ "请仅返回最合适的路由的键名,不要包含任何额外解释。例如,如果最合适的路由是\"technical\",只需返回\"technical\"。";
+
+ // 创建并发送提示
+ Prompt prompt = new Prompt(
+ new SystemMessage(promptTemplate),
+ new UserMessage(request.getQuestion())
+ );
+
+ String routeKey = chatClient.prompt(prompt).call().content().trim();
+
+ // 确保获取到的是有效路由
+ if (!stepMap.containsKey(routeKey)) {
+ return null;
+ }
+
+ return routeKey;
+ }
+
+ @Override
+ public String name() {
+ return name;
+ }
+
+}
\ No newline at end of file
diff --git a/spring-ai-agent/spring-ai-workflow/src/main/java/com/glmapper/ai/workflow/core/workflow/Workflow.java b/spring-ai-agent/spring-ai-workflow/src/main/java/com/glmapper/ai/workflow/core/workflow/Workflow.java
new file mode 100644
index 0000000..bf032fe
--- /dev/null
+++ b/spring-ai-agent/spring-ai-workflow/src/main/java/com/glmapper/ai/workflow/core/workflow/Workflow.java
@@ -0,0 +1,23 @@
+package com.glmapper.ai.workflow.core.workflow;
+
+import com.glmapper.ai.workflow.model.WorkflowRequest;
+import com.glmapper.ai.workflow.model.WorkflowResponse;
+
+
+/**
+ * @Classname Workflow
+ * @Description 工作流核心接口
+ *
+ * @Date 2025/6/10 10:21
+ * @Created by Gepeng18
+ */
+public interface Workflow {
+
+ /**
+ * 执行本工作流
+ *
+ * @param input 输入的请求
+ * @return 工作流执行结果
+ */
+ WorkflowResponse execute(WorkflowRequest input);
+}
\ No newline at end of file
diff --git a/spring-ai-agent/spring-ai-workflow/src/main/java/com/glmapper/ai/workflow/core/workflow/impl/ChainWorkflow.java b/spring-ai-agent/spring-ai-workflow/src/main/java/com/glmapper/ai/workflow/core/workflow/impl/ChainWorkflow.java
new file mode 100644
index 0000000..be3c2ec
--- /dev/null
+++ b/spring-ai-agent/spring-ai-workflow/src/main/java/com/glmapper/ai/workflow/core/workflow/impl/ChainWorkflow.java
@@ -0,0 +1,54 @@
+package com.glmapper.ai.workflow.core.workflow.impl;
+
+import com.glmapper.ai.workflow.model.WorkflowRequest;
+import com.glmapper.ai.workflow.model.WorkflowResponse;
+import com.glmapper.ai.workflow.core.workflow.Workflow;
+import com.glmapper.ai.workflow.core.step.WorkflowStep;
+import lombok.extern.slf4j.Slf4j;
+
+import java.util.List;
+
+
+/**
+ * @Classname ChainWorkflow
+ * @Description 链式工作流实现:按顺序执行一系列工作流步骤,前一步骤的输出作为后一步骤的输入
+ *
+ * @Date 2025/6/10 14:21
+ * @Created by Gepeng18
+ */
+@Slf4j
+public class ChainWorkflow implements Workflow {
+
+ private final List steps;
+
+ public ChainWorkflow(List steps) {
+ this.steps = steps;
+ }
+
+ @Override
+ public WorkflowResponse execute(WorkflowRequest input) {
+ Object currentInput = input.getQuestion();
+
+ try {
+ log.info("开始执行链式工作流, 步骤数量: {}", steps.size());
+
+ for (WorkflowStep step : steps) {
+ log.info("执行步骤: {}, 模型输入:{}", step.name(), currentInput);
+ currentInput = step.execute(currentInput);
+ }
+
+ log.info("链式工作流执行完成");
+ return WorkflowResponse.builder()
+ .content(currentInput != null ? currentInput.toString() : null)
+ .success(true)
+ .build();
+
+ } catch (Exception e) {
+ log.error("链式工作流执行失败", e);
+ return WorkflowResponse.builder()
+ .success(false)
+ .errorMessage("工作流执行失败: " + e.getMessage())
+ .build();
+ }
+ }
+}
\ No newline at end of file
diff --git a/spring-ai-agent/spring-ai-workflow/src/main/java/com/glmapper/ai/workflow/core/workflow/impl/ParallelizationWorkflow.java b/spring-ai-agent/spring-ai-workflow/src/main/java/com/glmapper/ai/workflow/core/workflow/impl/ParallelizationWorkflow.java
new file mode 100644
index 0000000..e9ff00e
--- /dev/null
+++ b/spring-ai-agent/spring-ai-workflow/src/main/java/com/glmapper/ai/workflow/core/workflow/impl/ParallelizationWorkflow.java
@@ -0,0 +1,97 @@
+package com.glmapper.ai.workflow.core.workflow.impl;
+
+import com.glmapper.ai.workflow.model.WorkflowRequest;
+import com.glmapper.ai.workflow.model.WorkflowResponse;
+import com.glmapper.ai.workflow.core.workflow.Workflow;
+import com.glmapper.ai.workflow.core.step.WorkflowStep;
+import lombok.extern.slf4j.Slf4j;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.stream.Collectors;
+
+
+/**
+ * @Classname ParallelizationWorkflow
+ * @Description 并行工作流实现:同时执行多个工作流步骤,所有步骤使用相同的输入,最终结果是所有步骤结果的集合
+ *
+ * @Date 2025/6/10 14:27
+ * @Created by Gepeng18
+ */
+@Slf4j
+public class ParallelizationWorkflow implements Workflow {
+
+ private final List steps;
+
+ public ParallelizationWorkflow(List steps) {
+ this.steps = steps;
+ }
+
+ @Override
+ public WorkflowResponse execute(WorkflowRequest input) {
+ try {
+ log.info("开始执行并行工作流, 步骤数量: {}", steps.size());
+
+ List>> futures = new ArrayList<>();
+
+ // 为每个步骤创建一个异步任务
+ for (WorkflowStep step : steps) {
+ CompletableFuture> future = CompletableFuture.supplyAsync(() -> {
+ log.info("执行步骤: {}", step.name());
+ Object result = step.execute(input.getQuestion());
+ return Map.entry(step.name(), result);
+ });
+ futures.add(future);
+ }
+
+ // 等待所有异步任务完成
+ CompletableFuture allFutures = CompletableFuture.allOf(
+ futures.toArray(new CompletableFuture[0])
+ );
+
+ // 收集所有结果
+ CompletableFuture
🚀🚀🚀 本项目是一个 Spring AI 快速入门的样例工程项目,旨在通过一些小的案例展示 Spring AI 框架的核心功能和使用方法。
-项目采用模块化设计,每个模块都专注于特定的功能领域,便于学习和扩展。
+项目采用模块化设计,每个模块都专注于特定的功能领域,便于学习和扩展;欢迎加群交流!
+
+
+
+
+
## 📖 关于 Spring AI
From b7d75bc032e58565d76dbe2c75fb5959a74e7d1d Mon Sep 17 00:00:00 2001
From: glmapper
Date: Mon, 9 Jun 2025 10:36:59 +0800
Subject: [PATCH 07/31] chat memory readme
---
spring-ai-chat-memory/README.md | 378 ++++++++++++++++++++++++++++++++
1 file changed, 378 insertions(+)
create mode 100644 spring-ai-chat-memory/README.md
diff --git a/spring-ai-chat-memory/README.md b/spring-ai-chat-memory/README.md
new file mode 100644
index 0000000..2c8cc96
--- /dev/null
+++ b/spring-ai-chat-memory/README.md
@@ -0,0 +1,378 @@
+# Spring AI Chat Memory 实战指南:Local 与 JDBC 存储全面解析
+
+在构建智能对话系统时,保持对话上下文的连贯性是提升用户体验的关键。Spring AI 框架提供了强大的 Chat Memory 机制,支持多种存储方式来持久化对话历史。本文将深入解析 Spring AI Chat Memory 的核心机制,并通过实际代码演示如何实现基于本地内存(Local)和数据库(JDBC)的两种存储方案。
+
+## Spring AI Chat Memory 核心机制
+
+### 架构概览 Architecture Overview
+
+Spring AI Chat Memory 采用分层架构设计:
+
+```
+┌─────────────────────────────────────┐
+│ ChatClient Layer │
+├─────────────────────────────────────┤
+│ ChatMemory Advisor │
+├─────────────────────────────────────┤
+│ ChatMemory Interface │
+├─────────────────────────────────────┤
+│ ChatMemoryRepository Layer │
+├─────────────────────────────────────┤
+│ Storage Layer (Local/JDBC) │
+└─────────────────────────────────────┘
+```
+
+### 核心组件解析
+
+1. **ChatMemory 接口**:提供统一的对话记忆管理抽象
+2. **ChatMemoryRepository**:负责底层存储操作
+3. **MessageChatMemoryAdvisor**:基于 Advisor 模式的透明化处理
+4. **MessageWindowChatMemory**:支持消息窗口限制的实现
+
+## 实现方案一:Local Memory (本地内存存储)
+
+### 依赖配置
+
+```xml
+
+
+ org.springframework.ai
+ spring-ai-starter-model-openai
+
+```
+
+### Step 1: 创建 ChatClient 配置
+
+```java
+@Configuration
+public class ChatClientConfigs {
+
+ @Bean
+ public ChatClient chatClient(OpenAiChatModel chatModel, ChatMemory chatMemory) {
+ return ChatClient.builder(chatModel)
+ .defaultAdvisors(MessageChatMemoryAdvisor.builder(chatMemory).build())
+ .defaultSystem("You are deepseek chat bot, you answer questions in a concise and accurate manner.")
+ .build();
+ }
+}
+```
+
+**关键点解析**:
+
+- `MessageChatMemoryAdvisor`:采用 Advisor 模式,自动处理消息的存储和检索
+- `defaultAdvisors`:为 ChatClient 配置默认的 advisor,使 memory 功能透明化
+
+### Step 2: 实现 ChatMemoryService
+
+```java
+@Service
+public class ChatMemoryService {
+ // 模拟一个会话 ID
+ private static final String CONVERSATION_ID = "naming-20250528";
+
+ @Autowired
+ private ChatClient chatClient;
+
+ /**
+ * 基于 Advisor 模式的聊天方法
+ * ChatClient 会自动处理消息的存储和检索
+ *
+ * @param message 用户输入消息
+ * @param conversationId 对话会话ID,如果为null则使用默认ID
+ * @return AI的响应内容
+ */
+ public String chat(String message, String conversationId) {
+ String answer = this.chatClient.prompt()
+ .user(message)
+ // 关键:通过 advisor 参数指定对话ID
+ .advisors(a -> a.param(ChatMemory.CONVERSATION_ID, conversationId == null ? CONVERSATION_ID : conversationId))
+ .call()
+ .content();
+ return answer;
+ }
+}
+```
+
+**核心机制**:
+
+- 通过 `ChatMemory.CONVERSATION_ID` 参数指定对话会话 ID
+- ChatClient 自动从 memory 中检索历史消息并添加到 prompt 中
+- 响应后自动将对话记录存储到 memory 中
+
+### Step 3: 配置应用属性
+
+```properties
+# application.properties
+spring.application.name=spring-ai-chat-memory-local
+server.port=8083
+spring.profiles.active=deepseek
+
+# DeepSeek API 配置
+spring.ai.openai.api-key=${spring.ai.openai.api-key}
+spring.ai.openai.chat.base-url=https://api.deepseek.com
+spring.ai.openai.chat.completions-path=/v1/chat/completions
+spring.ai.openai.chat.options.model=deepseek-chat
+```
+
+### Local Memory 优缺点
+
+**优点**:
+- 配置简单,开箱即用
+- 响应速度快,无网络延迟
+- 适合开发和测试环境
+
+**缺点**:
+- 数据不持久化,重启后丢失
+- 不支持多实例间共享
+- 内存使用量随对话量增长
+
+## 实现方案二:JDBC Memory (数据库存储)
+
+### 依赖配置
+
+```xml
+
+
+
+ org.springframework.ai
+ spring-ai-starter-model-chat-memory-repository-jdbc
+
+
+ mysql
+ mysql-connector-java
+ 8.0.33
+
+
+```
+
+### Step 1: 数据库表结构
+
+```sql
+-- schema-mysql.sql
+CREATE TABLE IF NOT EXISTS SPRING_AI_CHAT_MEMORY (
+ conversation_id VARCHAR(36) NOT NULL,
+ content TEXT NOT NULL,
+ type VARCHAR(10) NOT NULL,
+ `timestamp` TIMESTAMP NOT NULL,
+ CONSTRAINT TYPE_CHECK CHECK (type IN ('USER', 'ASSISTANT', 'SYSTEM', 'TOOL'))
+);
+
+CREATE INDEX IF NOT EXISTS SPRING_AI_CHAT_MEMORY_CONVERSATION_ID_TIMESTAMP_IDX
+ON SPRING_AI_CHAT_MEMORY(conversation_id, `timestamp`);
+```
+
+**表结构解析**:
+
+- `conversation_id`:对话会话标识,支持多会话隔离
+- `content`:消息内容
+- `type`:消息类型(用户、助手、系统、工具)
+- `timestamp`:时间戳,用于消息排序
+- 复合索引:优化按会话 ID 和时间的查询性能
+
+### Step 2: 实现 ChatMemoryService
+
+```java
+@Service
+public class ChatMemoryService {
+
+ @Autowired
+ private ChatModel chatModel;
+
+ @Autowired
+ private JdbcChatMemoryRepository chatMemoryRepository;
+
+ private ChatMemory chatMemory;
+
+ @PostConstruct
+ public void init() {
+ this.chatMemory = MessageWindowChatMemory.builder()
+ .chatMemoryRepository(chatMemoryRepository)
+ .maxMessages(20) // 限制消息窗口大小
+ .build();
+ }
+
+ public String call(String message, String conversationId) {
+ // 1. 创建用户消息
+ UserMessage userMessage = new UserMessage(message);
+
+ // 2. 存储用户消息到 memory
+ this.chatMemory.add(conversationId, userMessage);
+
+ // 3. 从 memory 获取对话历史
+ List messages = chatMemory.get(conversationId);
+
+ // 4. 调用 ChatModel 生成响应
+ ChatResponse response = chatModel.call(new Prompt(messages));
+
+ // 5. 存储 AI 响应到 memory
+ chatMemory.add(conversationId, response.getResult().getOutput());
+
+ return response.getResult().getOutput().getText();
+ }
+}
+```
+
+**核心机制**:
+- `MessageWindowChatMemory`:支持消息窗口限制的内存实现
+- `maxMessages`:控制保留的最大消息数量,避免 token 超限
+- 手动管理消息的存储和检索流程
+
+### Step 3: 配置数据源
+
+```properties
+# application.properties
+spring.application.name=spring-ai-chat-memory-jdbc
+server.port=8083
+
+# JDBC Memory Repository 配置
+spring.ai.chat.memory.repository.jdbc.initialize-schema=always
+spring.ai.chat.memory.repository.jdbc.schema=classpath:schema-@@platform@@.sql
+spring.ai.chat.memory.repository.jdbc.platform=mysql
+
+# MySQL 数据源配置
+spring.datasource.url=jdbc:mysql://localhost:3306/spring_ai_chat_memory?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
+spring.datasource.username=root
+spring.datasource.password=${spring.datasource.password}
+spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
+```
+
+### JDBC Memory 优缺点
+
+**优点**:
+- 数据持久化,支持服务重启
+- 支持多实例间共享对话历史
+- 可扩展性强,支持大规模应用
+- 支持复杂查询和数据分析
+
+**缺点**:
+- 配置相对复杂
+- 存在网络延迟
+- 需要维护数据库
+
+## 运行效果演示
+
+### Local Memory 运行日志
+
+```
+第一轮对话:
+用户: hello, my name is glmapper
+AI: Hello glmapper! Nice to meet you. How can I help you today?
+
+第二轮对话:
+用户: do you remember my name?
+AI: Yes, I remember! Your name is glmapper. Is there anything specific you'd like to discuss?
+```
+
+### JDBC Memory 数据库记录
+
+```sql
+SELECT * FROM SPRING_AI_CHAT_MEMORY WHERE conversation_id = 'test-naming-202505281800';
+
+| conversation_id | content | type | timestamp |
+|--------------------------|----------------------------|-----------|--------------------|
+| test-naming-202505281800 | hello, my name is glmapper | USER | 2025-01-20 10:30:15 |
+| test-naming-202505281800 | Hello glmapper! Nice to... | ASSISTANT | 2025-01-20 10:30:16 |
+| test-naming-202505281800 | do you remember my name? | USER | 2025-01-20 10:31:20 |
+| test-naming-202505281800 | Yes, I remember! Your... | ASSISTANT | 2025-01-20 10:31:21 |
+```
+
+## 实战测试验证
+
+### 测试对话连续性
+
+```java
+@Test
+@DisplayName("测试聊天记忆功能 - 上下文保持")
+void testChatMemoryContextRetention() {
+ String CONVERSATION_ID = "test-naming-202505281800";
+
+ // 第一轮对话:自我介绍
+ String firstMessage = "hello, my name is glmapper";
+ String firstResponse = chatMemoryService.call(firstMessage, CONVERSATION_ID);
+
+ // 第二轮对话:询问之前提到的信息
+ String secondMessage = "do you remember my name?";
+ String secondResponse = chatMemoryService.call(secondMessage, CONVERSATION_ID);
+
+ // 验证AI是否记住了用户的名字
+ assertTrue(secondResponse.contains("glmapper"), "AI 应该记住用户的名字");
+}
+```
+
+### 测试对话隔离性
+
+```java
+@Test
+@DisplayName("测试对话ID的非一致性")
+void testConversationIdNonConsistency() {
+ String CONVERSATION_ID1 = "test-naming-202505281801";
+ String CONVERSATION_ID2 = "test-naming-202505281802";
+
+ String message1 = "请记住这个数字:12345";
+ String message2 = "刚才我说的数字是什么?";
+
+ String response1 = chatMemoryService.call(message1, CONVERSATION_ID1);
+ String response2 = chatMemoryService.call(message2, CONVERSATION_ID2);
+
+ // 验证不同对话ID间的隔离性
+ assertFalse(response2.contains("12345"), "不同对话ID应该相互隔离");
+}
+```
+
+## 方案对比与选择建议
+
+| 特性 | Local Memory | JDBC Memory |
+|------|-------------|-------------|
+| 数据持久化 | ❌ | ✅ |
+| 配置复杂度 | 低 | 中 |
+| 性能 | 高 | 中 |
+| 多实例共享 | ❌ | ✅ |
+| 扩展性 | 低 | 高 |
+| 适用场景 | 开发/测试 | 生产环境 |
+
+**选择建议**:
+- **开发/测试阶段**:使用 Local Memory,快速验证功能
+- **生产环境**:使用 JDBC Memory,确保数据可靠性
+- **高并发场景**:考虑使用 Redis 等缓存方案
+- **企业级应用**:JDBC + 数据库集群方案
+
+## 常见问题与解决方案
+
+### Q1: 为什么 AI 记不住之前的对话?
+**A**: 检查对话 ID 是否一致,确保在同一会话中使用相同的 `conversationId`。
+
+### Q2: JDBC Memory 初始化失败?
+**A**: 确认数据库连接正常,检查 `spring.ai.chat.memory.repository.jdbc.initialize-schema=always` 配置。
+
+### Q3: 对话历史过长导致 Token 超限?
+**A**: 设置合适的 `maxMessages` 参数限制消息窗口大小。
+
+```java
+this.chatMemory = MessageWindowChatMemory.builder()
+ .chatMemoryRepository(chatMemoryRepository)
+ .maxMessages(10) // 根据模型 token 限制调整
+ .build();
+```
+
+## 最佳实践
+
+1. **会话 ID 管理**:使用 UUID 或有意义的业务标识,建议格式:`user_{userId}_{timestamp}`
+2. **消息窗口控制**:根据模型 token 限制合理设置 `maxMessages`(通常 10-20 条)
+3. **异常处理**:实现 memory 操作的容错机制,避免单点故障
+4. **性能优化**:使用数据库连接池,为高频查询字段建立索引
+5. **数据清理**:定期清理过期对话数据,避免数据库膨胀
+6. **监控告警**:监控 memory 操作的延迟和错误率
+
+## 总结
+
+Spring AI Chat Memory 提供了灵活的对话记忆管理能力,通过 Local 和 JDBC 两种存储方案,可以满足从开发测试到生产部署的不同需求。Local Memory 适合快速原型开发,而 JDBC Memory 则适合需要数据持久化的生产环境。
+
+理解其核心机制和实现细节,有助于开发者根据实际场景选择合适的方案,构建出高质量的智能对话应用。
+
+---
+
+**项目地址**: [spring-ai-summary](https://github.com/glmapper/spring-ai-summary)
+
+**相关文档**:
+- [Spring AI Documentation](https://docs.spring.io/spring-ai/reference/)
+- [ChatMemory API Reference](https://docs.spring.io/spring-ai/reference/api/chat/chat-memory.html)
\ No newline at end of file
From 74666ec050c32b0187cf44f91b8e21707529d37b Mon Sep 17 00:00:00 2001
From: glmapper
Date: Thu, 12 Jun 2025 15:32:57 +0800
Subject: [PATCH 08/31] update readme
---
README.md | 165 +++++++++++++++++++-----------------------------------
1 file changed, 58 insertions(+), 107 deletions(-)
diff --git a/README.md b/README.md
index b230fbd..f63bb16 100644
--- a/README.md
+++ b/README.md
@@ -7,14 +7,14 @@
🇺🇸 English
-🚀🚀🚀 本项目是一个 Spring AI 快速入门的样例工程项目,旨在通过一些小的案例展示 Spring AI 框架的核心功能和使用方法。
-项目采用模块化设计,每个模块都专注于特定的功能领域,便于学习和扩展;欢迎加群交流!
+🚀🚀🚀 Spring AI Summary 是一个基于原生 Spring AI 开发的一系列样例工程项目,通过这些案例展示 Spring AI 框架的核心功能和使用方法,可以帮助对 Spring AI 框架感兴趣的开发者快速上手和理解其核心概念。
+项目采用模块化设计,每个模块都专注于特定的功能领域,如聊天、RAG(检索增强生成)、文本向量化、工具函数调用、会话记忆管理等;每个模块都提供了详细的文档和示例代码。
+此外,Spring AI Summary 会持续关注 Spring AI 的最新动态和版本更新,及时更新示例代码和文档;并将一些优质的技术文章和实践经验同步到项目中,帮助开发者更好地理解和应用 Spring AI 框架。
-
## 📖 关于 Spring AI
Spring AI 项目的目标是简化集成人工智能功能的应用程序的开发过程,避免引入不必要的复杂性。关于 Spring AI 的更多信息,请访问 [Spring AI 官方文档](https://spring.io/projects/spring-ai)。
@@ -31,65 +31,50 @@ spring-ai-summary/
│ ├── spring-ai-chat-doubao/ # 豆包模型接入
│ ├── spring-ai-chat-deepseek/ # DeepSeek 模型接入
│ ├── spring-ai-chat-multi/ # 多 chat 模型调用
+│ │ spring-ai-chat-ollama/ # 接入 ollma
│ └── spring-ai-chat-multi-openai/ # 多 OpenAI 协议模型调用
├── spring-ai-rag/ # RAG 检索增强生成
-├── spring-ai-embedding/ # 文本向量化服务
+├── spring-ai-vector/ # 文本向量化服务
+ |── spring-ai-vector-milvus/ # Milvus 向量存储
+ ├── spring-ai-vector-redis/ # redis 向量存储
├── spring-ai-tool-calling/ # 工具函数调用示例
├── spring-ai-chat-memory/ # 会话记忆管理
+ ├── spring-ai-chat-memory-jdbc # 基于 jdbc 实现存储
+ ├── spring-ai-chat-memory-local # 基于 内存 实现存储
├── spring-ai-evaluation/ # AI 回答评估
└── spring-ai-mcp/ # MCP 示例
+ ├── spring-ai-mcp-server # MCP 服务器
+ ├── spring-ai-mcp-client # MCP 客户端
+└── spring-ai-agent/ # agent 示例
```
-**不同工程模块的文档列表如下:**
-
-* **spring-ai-chat-聊天模块**
- * [spring-ai-chat-openai](spring-ai-chat/spring-ai-chat-openai/README.md) - OpenAI 模型接入
- * [spring-ai-chat-qwen](spring-ai-chat/spring-ai-chat-qwen/README.md) - 通义千问模型接入
- * [spring-ai-chat-doubao](spring-ai-chat/spring-ai-chat-doubao/README.md) - 豆包模型接入
- * [spring-ai-chat-deepseek](spring-ai-chat/spring-ai-chat-deepseek/README.md) - DeepSeek 模型接入
- * [spring-ai-chat-multi](spring-ai-chat/spring-ai-chat-multi/README.md) - 多 chat 模型接入
- * [spring-ai-chat-multi-openai](spring-ai-chat/spring-ai-chat-multi-openai/README.md) - 多 OpenAI 协议模型接入
-* **[spring-ai-embedding-文本向量化服务]()** --待补充
-* **[spring-ai-rag-RAG 检索增强生成]()** --待补充
-* **[spring-ai-tool-calling-工具函数调用示例]()** --待补充
-* **[spring-ai-chat-memory-会话记忆管理]()** --待补充
-* **[spring-ai-mcp-MCP 示例]()** --待补充
-* **[spring-ai-evaluation-AI 回答评估]()** --待补充
-
-## 🧩 核心功能实现
-
-本案例工程的核心功能实现包括:
-
-- **多模型支持**:集成 OpenAI、通义千问、豆包、DeepSeek 等多种 LLM 模型
-- **RAG 实现**:完整的检索增强生成实现,支持文档向量化和语义搜索
-- **Function Calling**:支持函数调用(Function Calling)和工具集成
-- **Chat Memory**:支持多种存储方式的会话历史管理
-- **评估系统**:AI 回答质量评估工具
-- **监控统计**:Token 使用量统计和性能监控
-
-下面你可以通过快速开始部分来快速运行项目。
-
-
## 🚀 快速开始
### ⚙️ 环境要求
-- SpringBoot 3.3.6
-- Spring AI 1.0.0
-- JDK 21+
-- Maven 3.6+
-- Docker(用于运行 Milvus)
+| 依赖项 | 版本/要求 | 说明 |
+| -------------- | ---------------- | ------------------- |
+| SpringBoot | 3.3.6 | |
+| Spring AI | 1.0.0 | |
+| JDK | 21+ | |
+| Maven | 3.6+ | |
+| Docker | (用于运行 Milvus) | |
### 1. 🧬 克隆项目
```bash
-git clone https://github.com/glmapper/spring-ai-summary.git
-cd spring-ai-summary
+# 克隆项目到本地
+git clone https://github.com/java-ai-tech/spring-ai-summary.git
+# 进入项目目录并且 compile 项目
+cd spring-ai-summary && mvn clean compile -DskipTests
```
+> 如果遇到 Maven 依赖下载慢的问题,可以尝试使用国内的 Maven 镜像源,如阿里云、清华大学等;运行过程中如果有其他任何问题,可以扫码加入上面的微信群进行咨询交流~~~
+
### 2. 🛠️ 配置环境变量
对于每个模块的 resource 文件夹下的 `application.yml`/`application.properties` 文件,根据你的需求配置相应的 API 密钥。如 **spring-ai-chat-deepseek** 模块:
+
```properties
# because we do not use the OpenAI protocol
spring.ai.deepseek.api-key=${spring.ai.deepseek.api-key}
@@ -97,19 +82,9 @@ spring.ai.deepseek.base-url=https://api.deepseek.com
spring.ai.deepseek.chat.completions-path=/v1/chat/completions
spring.ai.deepseek.chat.options.model=deepseek-chat
```
-将你的 `spring.ai.deepseek.api-key` 替换为实际的 API 密钥即可启动运行。
-
-### 3. 🗄️ 启动 Milvus
-
-Milvus 是一个开源的向量数据库,用于存储和检索高维向量数据。本项目是使用 Docker 来运行 Milvus,当然你也可以选择其他方式安装 Milvus或者使用已经部署好的 Milvus 服务。
-
-> PS: 如果你不运行 spring-ai-rag 模块和 spring-ai-embedding 模块,可以跳过此步骤。
+将你的 `spring.ai.deepseek.api-key` 替换为实际的 API 密钥即可启动运行。关于如何申请 api key ,可以移步项目 Wiki 页面进行查看。
-这个项目使用的 milvus 版本是 2.5.0 版本,安装方式见:[Install Milvus in Docker](https://milvus.io/docs/install_standalone-docker.md)。
-
-⚠️本人的电脑是 Mac Air M2 芯片,使用官方文档中的 docker-compose 文件启动 Milvus 时,遇到 `milvus-standalone` 镜像不匹配问题。
-
-### 4. ▶️ 运行示例
+### 3. ▶️ 运行示例
完成上述步骤后,你可以选择运行不同的示例模块来体验 Spring AI 的功能。如启动运行 **spring-ai-chat-deepseek** 模块(具体端口可以根据你自己的配置而定):
```bash
@@ -127,16 +102,17 @@ Milvus 是一个开源的向量数据库,用于存储和检索高维向量数
2025-06-04T14:18:45.175+08:00 INFO 88446 --- [spring-ai-chat-deepseek] [on(2)-127.0.0.1] o.s.web.servlet.DispatcherServlet : Initializing Servlet 'dispatcherServlet'
2025-06-04T14:18:45.176+08:00 INFO 88446 --- [spring-ai-chat-deepseek] [on(2)-127.0.0.1] o.s.web.servlet.DispatcherServlet : Completed initialization in 1 ms
```
-启动完成后,可以通过 HTTPie 或 Postman 等工具进行测试。
+启动完成后,可以通过 cUrl、HTTPie 或 Postman 等工具进行测试。
+
```bash
-GET /api/deepseek/chatWithMetric?userInput="你是谁" HTTP/1.1
-Host: localhost:8081
-User-Agent: HTTPie
+curl localhost:8081/api/deepseek/chatWithMetric?userInput="你是谁?"
```
结果如下:
+

你可以继续使用下面的请求来查看 Token 使用情况:
+
```bash
# completion tokens
http://localhost:8081/actuator/metrics/ai.completion.tokens
@@ -159,38 +135,36 @@ http://localhost:8081/actuator/metrics/ai.total.tokens
}
```
-## 📚 模块说明
+**关于其他模块的使用方法和配置,可以查看 Wiki 页面或各模块的 `README.md` 文件。**
+
+## 📚 学习资料(持续更新中)
-### 1. 💬 聊天模块 (spring-ai-chat)
+以下是一些推荐的学习资源:
-提供多种 LLM 模型的接入实现,支持:
-- 单模型对话:支持 OpenAI、通义千问、豆包、DeepSeek 等模型
-- 多模型并行调用:支持多个模型同时调用,结果对比
-- 提示词模板:支持自定义提示词模板和变量替换
-- Token 统计:支持输入输出 Token 统计和成本估算
+> 官方也有一个[学习资料汇总](https://github.com/spring-ai-community/awesome-spring-ai),但主要是汇总的国外的一些资料,所以本项目更聚焦在汇总了一些国内的学习资源,供大家参考。
-### 2. 📖 RAG 模块 (spring-ai-rag)
+#### 技术社区
-实现检索增强生成,包含:
-- 文档向量化:支持多种文档格式的向量化处理
-- 向量存储:使用 Milvus 进行高效的向量存储和检索
-- 语义检索:支持相似度搜索和混合检索
-- 问答生成:基于检索结果生成准确的回答
+- [Spring AI 官方文档](https://spring.io/projects/spring-ai)
+- [Spring AI Alibaba 官方文档](https://github.com/alibaba/spring-ai-alibaba)
-### 3. 🛠️ 工具调用模块 (spring-ai-tool-calling)
+#### 项目系列
+- [MindMark(心印)是一款基于 SpringAI 的 RAG 系统](https://gitee.com/mumu-osc/mind-mark)
+- [My AI Agent 是一个基于 Spring Boot 和 Spring AI 框架构建的智能代理服务](https://github.com/Cunninger/my-ai-agent)
-展示如何实现工具函数调用:
-- 函数定义:使用 @Tool 注解定义工具函数
-- 工具注册:支持动态注册和配置工具
-- 动态调用:支持运行时动态调用工具
-- 结果处理:支持工具调用结果的格式化和处理
+#### 博客系列
+- [码匠的流水账--Spring AI 系列专栏](https://cloud.tencent.com/developer/column/72423) 因为作者没有进行专栏管理,所以是链接到了主页;此外这个系列的文章用来学习 Spring AI 的一些设计思路和实现方式非常不错,但是他是基于 M 系列版本写作的,所以有些内容可能会和最新版本不一致。
+- [深入解析 Spring AI 系列](https://www.cnblogs.com/guoxiaoyu/p/18666904) 作者貌似停更了...
+- [如何用Spring AI构建MCP Client-Server架构](https://spring.didispace.com/article/spring-ai-mcp.html)
+- [Building Effective Agents with Spring AI](https://spring.io/blog/2025/01/21/spring-ai-agentic-patterns) 强烈建议学习下
+- [Spring AI 大模型返回内容格式化源码分析及简单使用](https://juejin.cn/post/7378696051082199080)
+- [Spring AI EmbeddingModel 概念与源码分析](https://my.oschina.net/u/2391658/blog/18534829)
-### 4. 🧠 会话记忆模块 (spring-ai-chat-memory)
+#### 视频系列
+- [How to Build Agents with Spring AI](https://www.youtube.com/watch?v=d7m6nJxfi0g)
+- [Spring AI 系列视频教程](https://www.youtube.com/watch?v=yyvjT0v3lpY&list=PLZV0a2jwt22uoDm3LNDFvN6i2cAVU_HTH)
-提供会话历史管理:
-- JDBC 持久化:支持数据库存储会话历史
-- 本地文件存储:支持文件系统存储会话历史
-- 会话上下文管理:支持会话上下文的管理和清理
+大家如果有好的文章或资源,也欢迎提交 PR 或 Issue 进行补充和完善。下面开发和贡献指南。
## 🔧 开发指南
@@ -232,26 +206,6 @@ http://localhost:8081/actuator/metrics/ai.total.tokens
- 填写 PR 描述,说明改动内容和原因
- 等待代码审查和合并
-### 开发环境设置
-1. **IDE 配置**
- - 推荐使用 IntelliJ IDEA
- - 安装 Lombok 插件
- - 配置 Java 21 SDK
-2. **Maven 配置**
- ```xml
-
- 21
- 1.0.0
-
- ```
-3. **运行测试**
- ```bash
- # 运行所有测试
- mvn test
- # 运行特定模块的测试
- mvn test -pl spring-ai-chat
- ```
-
## 📝 注意事项
1. **API 密钥安全**
@@ -259,12 +213,7 @@ http://localhost:8081/actuator/metrics/ai.total.tokens
- 切勿在代码仓库中硬编码密钥
- 定期轮换密钥,提升安全性
-2. **Milvus 使用**
- - 创建集合时需确保向量维度与 embedding 模型一致
- - 检索前需先加载集合(load collection)
- - 创建索引后再进行检索,提升性能
-
-3. **Token 使用**
+2. **Token 使用**
- 持续监控 Token 消耗,避免超额
- 设置合理的 Token 限制,防止滥用
- 推荐实现缓存机制,提升响应速度与成本控制
@@ -285,3 +234,5 @@ http://localhost:8081/actuator/metrics/ai.total.tokens
- [通义千问](https://qianwen.aliyun.com) - 提供 Qwen 系列模型
- [豆包](https://www.volcengine.com/docs/82379) - 提供豆包系列模型
- [Milvus](https://milvus.io) - 提供向量数据库支持
+
+本项目是一个完全开源项目,主要目的是汇聚更多优质的 Spring AI 相关的学习资源,当然**相关学习资源主要来源于网络,如有侵权,请联系删除!!!**;在此也对参与开源贡献和所有在技术社区分享技术的朋友们表示衷心的感谢!
From 357c4ca899a771b345427eab2d940e3a8bb213b1 Mon Sep 17 00:00:00 2001
From: glmapper
Date: Thu, 12 Jun 2025 15:43:40 +0800
Subject: [PATCH 09/31] update readme
---
README.md | 30 ++++++++++++++++++++++--------
1 file changed, 22 insertions(+), 8 deletions(-)
diff --git a/README.md b/README.md
index f63bb16..f5300df 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,3 @@
-# Spring AI Summary

@@ -7,18 +6,33 @@
🇺🇸 English
-🚀🚀🚀 Spring AI Summary 是一个基于原生 Spring AI 开发的一系列样例工程项目,通过这些案例展示 Spring AI 框架的核心功能和使用方法,可以帮助对 Spring AI 框架感兴趣的开发者快速上手和理解其核心概念。
-项目采用模块化设计,每个模块都专注于特定的功能领域,如聊天、RAG(检索增强生成)、文本向量化、工具函数调用、会话记忆管理等;每个模块都提供了详细的文档和示例代码。
-此外,Spring AI Summary 会持续关注 Spring AI 的最新动态和版本更新,及时更新示例代码和文档;并将一些优质的技术文章和实践经验同步到项目中,帮助开发者更好地理解和应用 Spring AI 框架。
+## Spring AI Summary
+
+🚀🚀🚀 Spring AI Summary 是一个基于原生 Spring AI 开发的样例工程集合,旨在帮助开发者快速掌握 Spring AI 框架的核心功能和使用方法。通过模块化设计,每个模块专注于特定功能领域,提供清晰的代码示例和详细的文档,帮助开发者轻松上手并深入理解框架的核心概念。
+
+### 项目特点
+
+- **模块化设计**:每个模块聚焦于一个功能领域,例如聊天、RAG(检索增强生成)、文本向量化、工具函数调用、会话记忆管理等,方便开发者按需学习和应用。
+- **实用示例**:每个模块都包含完整的示例代码和文档,展示 Spring AI 的实际应用场景,帮助开发者快速构建自己的 AI 应用。
+- **持续更新**:紧跟 Spring AI 的最新动态和版本更新,及时优化示例代码和文档,确保内容始终与框架保持同步。
+- **社区支持**:同步优质技术文章和实践经验,分享最佳实践,帮助开发者更好地理解和应用 Spring AI。
+
+### 适合人群
+
+Spring AI Summary 面向对 Spring AI 框架感兴趣的开发者,无论是初学者还是有经验的工程师,都可以通过本项目快速了解框架的核心功能,并将其应用到实际项目中。
+
+通过 Spring AI Summary,您可以:
+
+- 掌握 Spring AI 的核心概念和功能。
+- 学习如何构建高效的 AI 应用。
+- 获取最新的技术动态和实践经验。
+
+欢迎您加入社区,共同探索 Spring AI 的无限可能!
-## 📖 关于 Spring AI
-
-Spring AI 项目的目标是简化集成人工智能功能的应用程序的开发过程,避免引入不必要的复杂性。关于 Spring AI 的更多信息,请访问 [Spring AI 官方文档](https://spring.io/projects/spring-ai)。
-
## 🗂️ 项目结构
本工程采用模块化设计,按照功能特性主要划分为以下几个模块:
From bf72fce3a2379ebd2cff5f04a1b94c1d940dad07 Mon Sep 17 00:00:00 2001
From: glmapper
Date: Thu, 12 Jun 2025 15:44:32 +0800
Subject: [PATCH 10/31] update readme
---
README.md | 4 +---
1 file changed, 1 insertion(+), 3 deletions(-)
diff --git a/README.md b/README.md
index f5300df..9cb2d0e 100644
--- a/README.md
+++ b/README.md
@@ -1,12 +1,10 @@
-
-
🇨🇳 中文
🇺🇸 English
-## Spring AI Summary
+## Spring AI Summary 
🚀🚀🚀 Spring AI Summary 是一个基于原生 Spring AI 开发的样例工程集合,旨在帮助开发者快速掌握 Spring AI 框架的核心功能和使用方法。通过模块化设计,每个模块专注于特定功能领域,提供清晰的代码示例和详细的文档,帮助开发者轻松上手并深入理解框架的核心概念。
From 753d20dbfe4e6b413ddb33995d58a51d100f03e5 Mon Sep 17 00:00:00 2001
From: glmapper
Date: Thu, 12 Jun 2025 16:34:01 +0800
Subject: [PATCH 11/31] update readme
---
.DS_Store | Bin 0 -> 6148 bytes
README.md | 15 ++++++++++-----
docs/.DS_Store | Bin 0 -> 6148 bytes
3 files changed, 10 insertions(+), 5 deletions(-)
create mode 100644 .DS_Store
create mode 100644 docs/.DS_Store
diff --git a/.DS_Store b/.DS_Store
new file mode 100644
index 0000000000000000000000000000000000000000..fff715019240c0b930423c8bf27933fa7dfd4c16
GIT binary patch
literal 6148
zcmeHK%SyvQ6rE|KO({Ya3SADkEm*A&iklGY4;ayfN=;1BV9b;zHH%WnT7Sqd@q4^?
zW+E2rR>a;5Gv_{MG6ymb#u)de(Jo^SW6Xwz$Wd7$=w2GCnPfzcV}wOE4ig!I{bpi+
z9q`*N7BR{0SoZb%!*LR4dAIw{YjtaFyt9M5cKVr=y7~h@%mtTwlg%BnwYY(i_
zdx!IR)85+NIX)XcCQqq+(S&l~TgisO3SL3^T+g#VNfVjggRjc3@(76mVt^PR23D5=
zb0*lW)un(|P7DwOKQMs%gMfzU8Z0%ctphr|K4aWML;)S&5{SZ}Yp~P^5fH9R0d*-i
zPYkZh!7ognYp~R)%NbWQ!#HN<>hZ$W?BEwFopDzq^~3-%u*g7Nn+~4;r|`=xedI5e
zkVOm-1OJQxZVmi_2a7Ui>$m0MSu3F3LPNp45)}~87cK!{;6Bn;K^+&UL!N7})QF>?
SUzG#WML-ck9Wn3=4155*4N6@A
literal 0
HcmV?d00001
diff --git a/README.md b/README.md
index 9cb2d0e..b046be1 100644
--- a/README.md
+++ b/README.md
@@ -1,10 +1,15 @@
+## Spring AI Summary
+
+
+
+
- 🇨🇳 中文
- 🇺🇸 English
+
+
+
-## Spring AI Summary 
🚀🚀🚀 Spring AI Summary 是一个基于原生 Spring AI 开发的样例工程集合,旨在帮助开发者快速掌握 Spring AI 框架的核心功能和使用方法。通过模块化设计,每个模块专注于特定功能领域,提供清晰的代码示例和详细的文档,帮助开发者轻松上手并深入理解框架的核心概念。
@@ -94,7 +99,7 @@ spring.ai.deepseek.base-url=https://api.deepseek.com
spring.ai.deepseek.chat.completions-path=/v1/chat/completions
spring.ai.deepseek.chat.options.model=deepseek-chat
```
-将你的 `spring.ai.deepseek.api-key` 替换为实际的 API 密钥即可启动运行。关于如何申请 api key ,可以移步项目 Wiki 页面进行查看。
+将你的 `spring.ai.deepseek.api-key` 替换为实际的 API 密钥即可启动运行。关于如何申请 api key ,可以移步项目 [Wiki 页面](https://github.com/java-ai-tech/spring-ai-summary/wiki)进行查看。
### 3. ▶️ 运行示例
@@ -147,7 +152,7 @@ http://localhost:8081/actuator/metrics/ai.total.tokens
}
```
-**关于其他模块的使用方法和配置,可以查看 Wiki 页面或各模块的 `README.md` 文件。**
+**关于其他模块的使用方法和配置,可以查看 [Wiki 页面](https://github.com/java-ai-tech/spring-ai-summary/wiki)或各模块的 `README.md` 文件。**
## 📚 学习资料(持续更新中)
diff --git a/docs/.DS_Store b/docs/.DS_Store
new file mode 100644
index 0000000000000000000000000000000000000000..52c92129abc19a025a49a4003f1f44574b4aa51c
GIT binary patch
literal 6148
zcmeHK%}T>S5Z-O8-BN@c6nYGJEm*BT6fYsx7cim+m718M!I&*gY7V84v%Zi|;`2DO
zy8){?coMNQF#FB!%Q6V^7>8hDvM6TwD*d0rQJe>q_
z#YBH`2*16_QkF3v^wamt06cHOB#zRo*L&x+dSh+9DVm}!ZoCJXdzqijQrDkeVeeeZ
zI4EsDxQNF2$l5%SN#;k%c&ZYja10^Wmr)YR+?BH=OjWL@9ik;#Bdar?_Xh`Edw+1W
z=-TtaUJva4;bPGeTiZLwXQRjXDUmOlN)D_m*)UkbD;Qs@diJJCER%cilv!moAu&J<
z5Cg=(YB6BW0nuKqvZ-2PfEf4z1GqofpoosaLZjL`puy`i`WuKSVB=c?(KhH9EHr`#
zgzHp5oyyG_wk`$$JQwckb?
Z;v9p8Mw|u5RXQMD1Qa3E5Cgx!zz1CFOuYaA
literal 0
HcmV?d00001
From 37bab4787325d5a7d89c278551071e01d8b77e89 Mon Sep 17 00:00:00 2001
From: glmapper
Date: Thu, 12 Jun 2025 16:51:34 +0800
Subject: [PATCH 12/31] update readme_en
---
README_EN.md | 191 ++++++++++++++++++++++-----------------------------
1 file changed, 82 insertions(+), 109 deletions(-)
diff --git a/README_EN.md b/README_EN.md
index 00c6f80..23c1502 100644
--- a/README_EN.md
+++ b/README_EN.md
@@ -5,18 +5,37 @@
🇨🇳 中文
🇺🇸 English
+ 📖 Wiki
+🚀🚀🚀 Spring AI Summary is a collection of sample projects based on native Spring AI, designed to help developers quickly master the core features and usage of the Spring AI framework. With a modular design, each module focuses on a specific functional area, providing clear code examples and detailed documentation to help you get started easily and deeply understand the core concepts of the framework.
-🚀🚀🚀 This project is a quick-start sample for Spring AI, designed to demonstrate the core features and usage of the Spring AI framework through practical mini-cases. The project adopts a modular design, with each module focusing on a specific functional area for easy learning and extension.
+### Project Features
-## 📖 About Spring AI
+- **Modular Design**: Each module focuses on a functional area, such as chat, RAG (Retrieval Augmented Generation), text embedding, tool function calling, chat memory management, etc., making it easy for developers to learn and apply as needed.
+- **Practical Examples**: Each module contains complete sample code and documentation, demonstrating real-world application scenarios of Spring AI, helping you quickly build your own AI applications.
+- **Continuous Updates**: The project keeps up with the latest developments and version updates of Spring AI, optimizing sample code and documentation in a timely manner to ensure content is always up-to-date.
+- **Community Support**: High-quality technical articles and practical experience are shared, offering best practices to help developers better understand and apply Spring AI.
-The goal of the Spring AI project is to simplify the development of applications that integrate AI capabilities, avoiding unnecessary complexity. For more information, please visit the [Spring AI official documentation](https://spring.io/projects/spring-ai).
+### Who Is This For?
+
+Spring AI Summary is for developers interested in the Spring AI framework. Whether you are a beginner or an experienced engineer, you can quickly learn the core features of the framework and apply them to real projects through this project.
+
+With Spring AI Summary, you can:
+
+- Master the core concepts and features of Spring AI.
+- Learn how to build efficient AI applications.
+- Get the latest technical trends and practical experience.
+
+Welcome to join the community and explore the infinite possibilities of Spring AI together!
+
+
+
+
## 🗂️ Project Structure
-This project uses a modular design, divided by feature as follows:
+This project adopts a modular design, mainly divided into the following modules by feature:
```
spring-ai-summary/
@@ -25,65 +44,51 @@ spring-ai-summary/
│ ├── spring-ai-chat-qwen/ # Qwen integration
│ ├── spring-ai-chat-doubao/ # Doubao integration
│ ├── spring-ai-chat-deepseek/ # DeepSeek integration
-│ ├── spring-ai-chat-multi/ # Multi-model parallel
-│ └── spring-ai-chat-multi-openai/ # OpenAI multi-model parallel
+│ ├── spring-ai-chat-multi/ # Multi chat model
+│ │ spring-ai-chat-ollama/ # Ollama integration
+│ └── spring-ai-chat-multi-openai/ # Multi OpenAI protocol models
├── spring-ai-rag/ # RAG (Retrieval Augmented Generation)
-├── spring-ai-embedding/ # Text embedding service
+├── spring-ai-vector/ # Text embedding service
+│ ├── spring-ai-vector-milvus/ # Milvus vector storage
+│ ├── spring-ai-vector-redis/ # Redis vector storage
├── spring-ai-tool-calling/ # Tool/function calling examples
├── spring-ai-chat-memory/ # Chat memory management
+│ ├── spring-ai-chat-memory-jdbc # JDBC-based storage
+│ ├── spring-ai-chat-memory-local # In-memory storage
├── spring-ai-evaluation/ # AI answer evaluation
└── spring-ai-mcp/ # MCP examples
+ ├── spring-ai-mcp-server # MCP server
+ ├── spring-ai-mcp-client # MCP client
+└── spring-ai-agent/ # Agent examples
```
-**The documentation list for different project modules is as follows.:**
-
-* **spring-ai-chat-chat module**
- * [spring-ai-chat-openai](spring-ai-chat/spring-ai-chat-openai/README.md) - OpenAI Model access
- * [spring-ai-chat-qwen](spring-ai-chat/spring-ai-chat-qwen/README.md) - Qwen Model access
- * [spring-ai-chat-doubao](spring-ai-chat/spring-ai-chat-doubao/README.md) - Doubao Model access
- * [spring-ai-chat-deepseek](spring-ai-chat/spring-ai-chat-deepseek/README.md) - DeepSeek Model access
- * [spring-ai-chat-multi](spring-ai-chat/spring-ai-chat-multi/README.md) - multi chat Model access
- * [spring-ai-chat-multi-openai](spring-ai-chat/spring-ai-chat-multi-openai/README.md) - multi OpenAI protocol Model access
-* **[spring-ai-embedding-文本向量化服务]()** --to be added
-* **[spring-ai-rag-RAG 检索增强生成]()** --to be added
-* **[spring-ai-tool-calling-工具函数调用示例]()** --to be added
-* **[spring-ai-chat-memory-会话记忆管理]()** --to be added
-* **[spring-ai-mcp-MCP 示例]()** --to be added
-* **[spring-ai-evaluation-AI 回答评估]()** --to be added
-*
-## 🧩 Core Features
-
-This sample project implements the following core features:
-
-- **Multi-model support**: Integrates OpenAI, Qwen, Doubao, DeepSeek, and more LLMs
-- **RAG implementation**: Complete retrieval-augmented generation, supporting document embedding and semantic search
-- **Function Calling**: Supports function calling and tool integration
-- **Chat Memory**: Multiple storage options for chat history
-- **Evaluation System**: AI answer quality evaluation tools
-- **Monitoring & Stats**: Token usage and performance monitoring
-
-You can quickly get started by following the steps below.
-
## 🚀 Quick Start
### ⚙️ Requirements
-- SpringBoot 3.3.6
-- Spring AI 1.0.0
-- JDK 21+
-- Maven 3.6+
-- Docker (for running Milvus)
+| Dependency | Version/Requirement | Note |
+| -------------- | ------------------ | ------------------- |
+| SpringBoot | 3.3.6 | |
+| Spring AI | 1.0.0 | |
+| JDK | 21+ | |
+| Maven | 3.6+ | |
+| Docker | (for running Milvus)| |
### 1. 🧬 Clone the Project
```bash
-git clone https://github.com/glmapper/spring-ai-summary.git
-cd spring-ai-summary
+# Clone the project
+git clone https://github.com/java-ai-tech/spring-ai-summary.git
+# Enter the project directory and compile
+cd spring-ai-summary && mvn clean compile -DskipTests
```
+> If you encounter slow Maven dependency downloads, try using a domestic Maven mirror (e.g., Aliyun, Tsinghua). For any other issues, feel free to join the WeChat group above for discussion and support.
+
### 2. 🛠️ Configure Environment Variables
For each module, configure the required API keys in the `application.yml`/`application.properties` file under the `resources` folder. For example, in **spring-ai-chat-deepseek**:
+
```properties
# because we do not use the OpenAI protocol
spring.ai.deepseek.api-key=${spring.ai.deepseek.api-key}
@@ -91,35 +96,27 @@ spring.ai.deepseek.base-url=https://api.deepseek.com
spring.ai.deepseek.chat.completions-path=/v1/chat/completions
spring.ai.deepseek.chat.options.model=deepseek-chat
```
-Replace `spring.ai.deepseek.api-key` with your actual API key to start the service.
-
-### 3. 🗄️ Start Milvus
-
-Milvus is an open-source vector database for storing and retrieving high-dimensional vector data. This project uses Docker to run Milvus, but you can use other installation methods or an existing Milvus service.
-
-> PS: If you do not use the spring-ai-rag or spring-ai-embedding modules, you can skip this step.
+Replace `spring.ai.deepseek.api-key` with your actual API key to start the service. For how to apply for an API key, see the project [Wiki page](https://github.com/java-ai-tech/spring-ai-summary/wiki).
-The project uses Milvus version 2.5.0. See the [Install Milvus in Docker](https://milvus.io/docs/install_standalone-docker.md) guide.
-
-⚠️ On Mac Air M2, there may be issues with the `milvus-standalone` image when using the official docker-compose file.
-
-### 4. ▶️ Run Examples
+### 3. ▶️ Run Examples
After the above steps, you can run different modules to experience Spring AI features. For example, to start **spring-ai-chat-deepseek** (port may vary):
+
```bash
2025-06-04T14:18:43.939+08:00 INFO 88446 --- [spring-ai-chat-deepseek] [ main] c.g.ai.chat.deepseek.DsChatApplication : Starting DsChatApplication using Java 21.0.2 with PID 88446 (/Users/glmapper/Documents/projects/glmapper/spring-ai-summary/spring-ai-chat/spring-ai-chat-deepseek/target/classes started by glmapper in /Users/glmapper/Documents/projects/glmapper/spring-ai-summary)
...
```
-Once started, you can test with HTTPie or Postman:
+Once started, you can test with cUrl, HTTPie, or Postman:
+
```bash
-GET /api/deepseek/chatWithMetric?userInput="Who are you?" HTTP/1.1
-Host: localhost:8081
-User-Agent: HTTPie
+curl localhost:8081/api/deepseek/chatWithMetric?userInput="Who are you?"
```
Result:
+

You can also check token usage:
+
```bash
# completion tokens
http://localhost:8081/actuator/metrics/ai.completion.tokens
@@ -142,38 +139,36 @@ Example response for `ai.completion.tokens`:
}
```
-## 📚 Module Overview
+**For usage and configuration of other modules, see the [Wiki page](https://github.com/java-ai-tech/spring-ai-summary/wiki) or each module's `README.md`.**
+
+## 📚 Learning Resources (Continuously Updated)
-### 1. 💬 Chat Module (spring-ai-chat)
+Here are some recommended learning resources:
-Provides integration with multiple LLMs:
-- Single-model chat: OpenAI, Qwen, Doubao, DeepSeek, etc.
-- Multi-model parallel: Call multiple models and compare results
-- Prompt templates: Customizable prompt templates and variable replacement
-- Token stats: Input/output token statistics and cost estimation
+> The official [Awesome Spring AI](https://github.com/spring-ai-community/awesome-spring-ai) list is also available, but it mainly collects overseas resources. This project focuses on aggregating domestic learning resources for your reference.
-### 2. 📖 RAG Module (spring-ai-rag)
+#### Technical Community
-Implements retrieval-augmented generation:
-- Document embedding: Supports various document formats
-- Vector storage: Efficient storage and retrieval with Milvus
-- Semantic search: Similarity and hybrid search
-- Answer generation: Generate accurate answers based on retrieval
+- [Spring AI Official Documentation](https://spring.io/projects/spring-ai)
+- [Spring AI Alibaba Official Documentation](https://github.com/alibaba/spring-ai-alibaba)
-### 3. 🛠️ Tool Calling Module (spring-ai-tool-calling)
+#### Project Series
+- [MindMark: A RAG system based on SpringAI](https://gitee.com/mumu-osc/mind-mark)
+- [My AI Agent: An intelligent agent service based on Spring Boot and Spring AI](https://github.com/Cunninger/my-ai-agent)
-Demonstrates tool/function calling:
-- Function definition: Use @Tool annotation to define tools
-- Tool registration: Dynamic registration and configuration
-- Dynamic invocation: Call tools at runtime
-- Result handling: Format and process tool results
+#### Blog Series
+- [MaJiang's Spring AI Series](https://cloud.tencent.com/developer/column/72423) (Chinese, some content may be outdated)
+- [In-depth Spring AI Series](https://www.cnblogs.com/guoxiaoyu/p/18666904) (Chinese, discontinued)
+- [How to Build MCP Client-Server Architecture with Spring AI](https://spring.didispace.com/article/spring-ai-mcp.html)
+- [Building Effective Agents with Spring AI](https://spring.io/blog/2025/01/21/spring-ai-agentic-patterns)
+- [Spring AI Large Model Output Formatting and Simple Usage](https://juejin.cn/post/7378696051082199080)
+- [Spring AI EmbeddingModel Concept and Source Code Analysis](https://my.oschina.net/u/2391658/blog/18534829)
-### 4. 🧠 Chat Memory Module (spring-ai-chat-memory)
+#### Video Series
+- [How to Build Agents with Spring AI (YouTube)](https://www.youtube.com/watch?v=d7m6nJxfi0g)
+- [Spring AI Video Tutorials (YouTube)](https://www.youtube.com/watch?v=yyvjT0v3lpY&list=PLZV0a2jwt22uoDm3LNDFvN6i2cAVU_HTH)
-Provides chat history management:
-- JDBC persistence: Store chat history in a database
-- Local file storage: Store chat history in the file system
-- Context management: Manage and clean up chat context
+If you have good articles or resources, feel free to submit a PR or Issue to supplement and improve this list. See below for development and contribution guidelines.
## 🔧 Development Guide
@@ -189,6 +184,7 @@ Provides chat history management:
2. **Create a feature branch**
```bash
+ # Create and switch to a new feature branch
git checkout -b feature/your-feature-name
```
@@ -211,26 +207,6 @@ Provides chat history management:
- Describe your changes and reasons
- Wait for review and merge
-### Development Environment Setup
-1. **IDE Setup**
- - Recommend IntelliJ IDEA
- - Install Lombok plugin
- - Configure Java 21 SDK
-2. **Maven Setup**
- ```xml
-
- 21
- 1.0.0
-
- ```
-3. **Run Tests**
- ```bash
- # Run all tests
- mvn test
- # Run tests for a specific module
- mvn test -pl spring-ai-chat
- ```
-
## 📝 Notes
1. **API Key Security**
@@ -238,12 +214,7 @@ Provides chat history management:
- Never hardcode keys in the codebase
- Rotate keys regularly for better security
-2. **Milvus Usage**
- - Ensure vector dimension matches the embedding model when creating collections
- - Load the collection before retrieval (load collection)
- - Create indexes before retrieval for better performance
-
-3. **Token Usage**
+2. **Token Usage**
- Monitor token consumption to avoid overuse
- Set reasonable token limits to prevent abuse
- Implement caching to improve response speed and cost control
@@ -264,3 +235,5 @@ Provides chat history management:
- [Qwen](https://qianwen.aliyun.com) - Qwen series models
- [Doubao](https://www.volcengine.com/docs/82379) - Doubao series models
- [Milvus](https://milvus.io) - Vector database support
+
+This project is fully open source, aiming to aggregate more high-quality Spring AI learning resources. Most resources are collected from the internet. If there is any infringement, please contact for removal. Special thanks to all open source contributors and everyone who shares technology in the community!
From 9dad16da3fca1227d03510c9dad84dd4a26fcec0 Mon Sep 17 00:00:00 2001
From: glmapper
Date: Thu, 12 Jun 2025 18:22:09 +0800
Subject: [PATCH 13/31] add doc for tool-calling and evaluation
---
docs/statics/tc-ToolCallingManager.png | Bin 0 -> 131476 bytes
docs/statics/tc-core-component.png | Bin 0 -> 115026 bytes
docs/statics/tc-returnDirect.png | Bin 0 -> 106108 bytes
docs/statics/tool-context.png | Bin 0 -> 124510 bytes
spring-ai-evaluation/README.md | 102 ++++
spring-ai-tool-calling/README.md | 629 +++++++++++++++++++++++++
6 files changed, 731 insertions(+)
create mode 100644 docs/statics/tc-ToolCallingManager.png
create mode 100644 docs/statics/tc-core-component.png
create mode 100644 docs/statics/tc-returnDirect.png
create mode 100644 docs/statics/tool-context.png
create mode 100644 spring-ai-evaluation/README.md
create mode 100644 spring-ai-tool-calling/README.md
diff --git a/docs/statics/tc-ToolCallingManager.png b/docs/statics/tc-ToolCallingManager.png
new file mode 100644
index 0000000000000000000000000000000000000000..ea42c46a539bd906597c5701062b795525b5f2c5
GIT binary patch
literal 131476
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
z
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