diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..fff7150 Binary files /dev/null and b/.DS_Store differ diff --git a/README.md b/README.md index 62b3a9c..3d45e0b 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,41 @@ -# Spring AI Summary + +## Spring AI Summary ![Spring AI Summary](https://img.shields.io/badge/spring--ai--summary-v1.0.0-blue.svg) +![Visitors](https://visitor-badge.laobi.icu/badge?page_id=java-ai-tech.spring-ai-summary)

- 🇨🇳 中文 - 🇺🇸 English + 中文 + English + doc

-🚀🚀🚀 本项目是一个 Spring AI 快速入门的样例工程项目,旨在通过一些小的案例展示 Spring AI 框架的核心功能和使用方法。 -项目采用模块化设计,每个模块都专注于特定的功能领域,便于学习和扩展。 -## 📖 关于 Spring AI -Spring AI 项目的目标是简化集成人工智能功能的应用程序的开发过程,避免引入不必要的复杂性。关于 Spring AI 的更多信息,请访问 [Spring AI 官方文档](https://spring.io/projects/spring-ai)。 +🚀🚀🚀 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)! + +

+ image +

## 🗂️ 项目结构 @@ -26,65 +49,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} @@ -92,19 +100,14 @@ 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 页面](https://github.com/java-ai-tech/spring-ai-summary/wiki)进行查看。 -这个项目使用的 milvus 版本是 2.5.0 版本,安装方式见:[Install Milvus in Docker](https://milvus.io/docs/install_standalone-docker.md)。 +有一个一劳永逸的办法,将对应的spring.ai.deepseek.api-key添加到对应环境变量中,后续启动时会带进来,不用再去修改代码了对应的application.yml,不用担心提交代码泄露key +将IDEA启动项中的环境变量添加spring.ai.openai.api-key=sk-***************(你自己对应的 key),运行项目时,会自动带入环境变量。 +不过对应的子module每个模块都需要配置 -⚠️本人的电脑是 Mac Air M2 芯片,使用官方文档中的 docker-compose 文件启动 Milvus 时,遇到 `milvus-standalone` 镜像不匹配问题。 -### 4. ▶️ 运行示例 +### 3. ▶️ 运行示例 完成上述步骤后,你可以选择运行不同的示例模块来体验 Spring AI 的功能。如启动运行 **spring-ai-chat-deepseek** 模块(具体端口可以根据你自己的配置而定): ```bash @@ -122,16 +125,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="你是谁?" ``` 结果如下: + ![chat-ds-metrics.png](docs/statics/chat-ds-metrics.png) 你可以继续使用下面的请求来查看 Token 使用情况: + ```bash # completion tokens http://localhost:8081/actuator/metrics/ai.completion.tokens @@ -154,38 +158,38 @@ http://localhost:8081/actuator/metrics/ai.total.tokens } ``` -## 📚 模块说明 +**关于其他模块的使用方法和配置,可以查看 [Wiki 页面](https://github.com/java-ai-tech/spring-ai-summary/wiki)或各模块的 `README.md` 文件。** -### 1. 💬 聊天模块 (spring-ai-chat) +## 📚 学习资料(持续更新中) -提供多种 LLM 模型的接入实现,支持: -- 单模型对话:支持 OpenAI、通义千问、豆包、DeepSeek 等模型 -- 多模型并行调用:支持多个模型同时调用,结果对比 -- 提示词模板:支持自定义提示词模板和变量替换 -- Token 统计:支持输入输出 Token 统计和成本估算 +以下是一些推荐的学习资源: -### 2. 📖 RAG 模块 (spring-ai-rag) +> 官方也有一个[学习资料汇总](https://github.com/spring-ai-community/awesome-spring-ai),但主要是汇总的国外的一些资料,所以本项目更聚焦在汇总了一些国内的学习资源,供大家参考。 -实现检索增强生成,包含: -- 文档向量化:支持多种文档格式的向量化处理 -- 向量存储:使用 Milvus 进行高效的向量存储和检索 -- 语义检索:支持相似度搜索和混合检索 -- 问答生成:基于检索结果生成准确的回答 +#### 技术社区 -### 3. 🛠️ 工具调用模块 (spring-ai-tool-calling) +- [Spring AI 官方文档](https://spring.io/projects/spring-ai) +- [Spring AI Alibaba 官方文档](https://github.com/alibaba/spring-ai-alibaba) -展示如何实现工具函数调用: -- 函数定义:使用 @Tool 注解定义工具函数 -- 工具注册:支持动态注册和配置工具 -- 动态调用:支持运行时动态调用工具 -- 结果处理:支持工具调用结果的格式化和处理 +#### 项目系列 +- [MindMark(心印)是一款基于 SpringAI 的 RAG 系统](https://gitee.com/mumu-osc/mind-mark) +- [My AI Agent 是一个基于 Spring Boot 和 Spring AI 框架构建的智能代理服务](https://github.com/Cunninger/my-ai-agent) -### 4. 🧠 会话记忆模块 (spring-ai-chat-memory) +#### 博客系列 +- [码匠的流水账--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) +- [全量RAG技术:更简单、更实用的实现方法 ✨](https://www.readme-i18n.com/FareedKhan-dev/all-rag-techniques?lang=zh) +- [Spring AI 框架原理与实战](https://juejin.cn/column/7375109287716372520) 🌟🌟🌟 -提供会话历史管理: -- JDBC 持久化:支持数据库存储会话历史 -- 本地文件存储:支持文件系统存储会话历史 -- 会话上下文管理:支持会话上下文的管理和清理 +#### 视频系列 +- [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) +- [马克的技术工作坊](https://space.bilibili.com/1815948385) 🌟🌟🌟🌟🌟 +大家如果有好的文章或资源,也欢迎提交 PR 或 Issue 进行补充和完善。下面开发和贡献指南。 ## 🔧 开发指南 @@ -227,26 +231,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 密钥安全** @@ -254,12 +238,7 @@ http://localhost:8081/actuator/metrics/ai.total.tokens - 切勿在代码仓库中硬编码密钥 - 定期轮换密钥,提升安全性 -2. **Milvus 使用** - - 创建集合时需确保向量维度与 embedding 模型一致 - - 检索前需先加载集合(load collection) - - 创建索引后再进行检索,提升性能 - -3. **Token 使用** +2. **Token 使用** - 持续监控 Token 消耗,避免超额 - 设置合理的 Token 限制,防止滥用 - 推荐实现缓存机制,提升响应速度与成本控制 @@ -280,3 +259,10 @@ 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 相关的学习资源,当然**相关学习资源主要来源于网络,如有侵权,请联系删除!!!**;在此也对参与开源贡献和所有在技术社区分享技术的朋友们表示衷心的感谢! + + +## Star History + +[![Star History Chart](https://api.star-history.com/svg?repos=java-ai-tech/spring-ai-summary&type=Date)](https://www.star-history.com/#java-ai-tech/spring-ai-summary&Date) \ No newline at end of file diff --git a/README_EN.md b/README_EN.md index 00c6f80..b7ee5c1 100644 --- a/README_EN.md +++ b/README_EN.md @@ -1,22 +1,43 @@ # Spring AI Summary ![Spring AI Summary](https://img.shields.io/badge/spring--ai--summary-v1.0.0-blue.svg) +![Visitors](https://visitor-badge.laobi.icu/badge?page_id=java-ai-tech.spring-ai-summary)

- 🇨🇳 中文 - 🇺🇸 English + 中文 + English + doc

-🚀🚀🚀 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. +🚀🚀🚀 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. -## 📖 About Spring AI +### Project Features -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). +- **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. + +### 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! + +

+ image +

## 🗂️ 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 +46,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 +98,32 @@ 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. +There's a one-time setup solution: add the spring.ai.deepseek.api-key to your environment variables. This will be automatically loaded during subsequent application startups without needing code modifications in application.yml, eliminating concerns about accidentally committing the code and exposing the key. -⚠️ On Mac Air M2, there may be issues with the `milvus-standalone` image when using the official docker-compose file. +In IntelliJ IDEA's launch configuration, add the environment variable spring.ai.openai.api-key=sk-***************(your_actual_key). The project will automatically include this environment variable when running. -### 4. ▶️ Run Examples +Note: Each submodule requires separate configuration for this setting. +### 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: + ![chat-ds-metrics.png](docs/statics/chat-ds-metrics.png) You can also check token usage: + ```bash # completion tokens http://localhost:8081/actuator/metrics/ai.completion.tokens @@ -142,38 +146,37 @@ 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`.** -### 1. 💬 Chat Module (spring-ai-chat) +## 📚 Learning Resources (Continuously Updated) -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 +Here are some recommended learning resources: -### 2. 📖 RAG Module (spring-ai-rag) +> 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. -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 +#### Technical Community -### 3. 🛠️ Tool Calling Module (spring-ai-tool-calling) +- [Spring AI Official Documentation](https://spring.io/projects/spring-ai) +- [Spring AI Alibaba Official Documentation](https://github.com/alibaba/spring-ai-alibaba) -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 +#### 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) -### 4. 🧠 Chat Memory Module (spring-ai-chat-memory) +#### 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) -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 +#### 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) +- [马克的技术工作坊](https://space.bilibili.com/1815948385) 🌟🌟🌟🌟🌟 + +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 +192,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 +215,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 +222,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 +243,10 @@ 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! + + +## Star History + +[![Star History Chart](https://api.star-history.com/svg?repos=java-ai-tech/spring-ai-summary&type=Date)](https://www.star-history.com/#java-ai-tech/spring-ai-summary&Date) \ No newline at end of file diff --git a/docs/.DS_Store b/docs/.DS_Store new file mode 100644 index 0000000..52c9212 Binary files /dev/null and b/docs/.DS_Store differ diff --git a/docs/statics/my_chat.png b/docs/statics/my_chat.png new file mode 100644 index 0000000..87a25fc Binary files /dev/null and b/docs/statics/my_chat.png differ diff --git a/docs/statics/tc-ToolCallingManager.png b/docs/statics/tc-ToolCallingManager.png new file mode 100644 index 0000000..ea42c46 Binary files /dev/null and b/docs/statics/tc-ToolCallingManager.png differ diff --git a/docs/statics/tc-core-component.png b/docs/statics/tc-core-component.png new file mode 100644 index 0000000..486727c Binary files /dev/null and b/docs/statics/tc-core-component.png differ diff --git a/docs/statics/tc-returnDirect.png b/docs/statics/tc-returnDirect.png new file mode 100644 index 0000000..6edaade Binary files /dev/null and b/docs/statics/tc-returnDirect.png differ diff --git a/docs/statics/tool-context.png b/docs/statics/tool-context.png new file mode 100644 index 0000000..73977af Binary files /dev/null and b/docs/statics/tool-context.png differ diff --git a/pom.xml b/pom.xml index d2674b3..cc59094 100644 --- a/pom.xml +++ b/pom.xml @@ -21,6 +21,8 @@ spring-ai-evaluation spring-ai-chat-memory spring-ai-tool-calling + spring-ai-agent + spring-ai-observability diff --git a/spring-ai-agent/pom.xml b/spring-ai-agent/pom.xml new file mode 100644 index 0000000..fcdd806 --- /dev/null +++ b/spring-ai-agent/pom.xml @@ -0,0 +1,24 @@ + + + 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 + spring-ai-agent-orchestrator + + + \ No newline at end of file diff --git a/spring-ai-agent/spring-ai-agent-orchestrator/README.md b/spring-ai-agent/spring-ai-agent-orchestrator/README.md new file mode 100644 index 0000000..00b0dea --- /dev/null +++ b/spring-ai-agent/spring-ai-agent-orchestrator/README.md @@ -0,0 +1,197 @@ + +# 基于大模型的 OrchestratorWorkersWorkflow 智能编排 + +## 一、背景与目标 + +在智能体(AI Agent)和自动化编排领域,复杂任务往往需要拆解为多个子任务,并由不同的“worker”并行处理。本文将介绍如何基于 Spring AI 和大模型(LLM),实现一个通用的 OrchestratorWorkersWorkflow,自动完成任务拆解、分发与结果汇总,并通过单元测试进行验证和评估。 + +--- + +## 二、核心设计思路 + +### 1. 任务拆解(Orchestration) + +- 利用大模型(如 OpenAI、Qwen 等)对用户输入的复杂任务进行智能拆解,输出结构化的子任务列表。 +- 拆解 prompt 采用系统提示词,要求模型以 JSON 数组返回所有可独立执行的子任务。 + +### 2. 子任务处理(Workers) + +- 对每个子任务,worker 再次调用大模型,让其“执行”或“回答”该子任务,实现通用型处理。 +- 支持并行处理,提升效率。 + +### 3. 结果汇总 + +- 汇总所有 worker 的输出,形成最终的 content。 +- 同时保留原始拆解、子任务列表、各 worker 输出,便于溯源和分析。 + +--- + +## 三、核心代码实现 + +### 1. WorkflowResponse 结构 + +```java +@Data +@Builder +public class WorkflowResponse { + private String analysis; // 大模型原始拆解输出 + private List subtasks; // 拆解出的所有子任务 + private List workerResponses; // 每个 worker 的输出 + private String content; // 最终合成内容 + private boolean success; // 是否成功 + private String errorMessage; // 错误信息 +} +``` + +### 2. OrchestratorWorkersWorkflow 主要逻辑 + +```java +@Component +public class OrchestratorWorkersWorkflow { + + @Autowired + private ChatClient chatClient; + + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + + public WorkflowResponse process(String taskDescription) { + try { + // 1. 用大模型拆解任务 + LlmSubtaskResult subtaskResult = callLlmForSubtasks(taskDescription); + List subtasks = subtaskResult.subtasks; + String analysis = subtaskResult.analysis; + if (subtasks == null || subtasks.isEmpty()) { + return WorkflowResponse.builder() + .success(false) + .errorMessage("大模型未能拆解出子任务") + .analysis(analysis) + .subtasks(subtasks) + .build(); + } + + // 2. Workers process subtasks in parallel + List workerResponses = subtasks.stream() + .map(subtask -> CompletableFuture.supplyAsync(() -> workerProcess(subtask))) + .collect(Collectors.toList()) + .stream() + .map(CompletableFuture::join) + .collect(Collectors.toList()); + + // 3. Results are combined into final response + String combined = String.join("\n", workerResponses); + return WorkflowResponse.builder() + .content(combined) + .success(true) + .analysis(analysis) + .subtasks(subtasks) + .workerResponses(workerResponses) + .build(); + } catch (Exception e) { + return WorkflowResponse.builder() + .success(false) + .errorMessage("Orchestrator/Worker 执行失败: " + e.getMessage()) + .build(); + } + } + + // 用大模型拆解任务 + private LlmSubtaskResult callLlmForSubtasks(String taskDescription) throws Exception { + List messages = List.of( + new SystemMessage("你是一个任务拆解专家。请将用户输入的复杂任务描述拆解为若干可以独立执行的子任务,输出格式为 JSON 数组,每个元素为一个子任务字符串。"), + new UserMessage(taskDescription) + ); + Prompt prompt = new Prompt(messages); + String modelResult = chatClient.prompt(prompt).call().content(); + List subtasks; + try { + subtasks = OBJECT_MAPPER.readValue(modelResult, new TypeReference>() {}); + } catch (Exception e) { + subtasks = Arrays.stream(modelResult.split("[。,.,\n]")).map(String::trim).filter(s -> !s.isEmpty()).collect(Collectors.toList()); + } + return new LlmSubtaskResult(modelResult, subtasks); + } + + // 通用 worker 处理逻辑:再次用大模型处理子任务 + private String workerProcess(String subtask) { + try { + String systemPrompt = "你是一个高效的AI助手,请认真完成以下子任务:"; + List messages = List.of( + new SystemMessage(systemPrompt), + new UserMessage(subtask) + ); + Prompt prompt = new Prompt(messages); + return chatClient.prompt(prompt).call().content(); + } catch (Exception e) { + return "[Worker Error] " + e.getMessage(); + } + } + + // 内部类:封装大模型分析结果 + private static class LlmSubtaskResult { + final String analysis; + final List subtasks; + LlmSubtaskResult(String analysis, List subtasks) { + this.analysis = analysis; + this.subtasks = subtasks; + } + } +} +``` + +--- + +## 四、测试用例与验证 + +### 1. 测试用例设计 + +- 输入任务描述:"Generate both technical and user-friendly documentation for a REST API endpoint" +- 期望:大模型能拆解出合理的文档编写子任务,worker 能对每个子任务给出专业输出。 + +### 2. 测试结果(test_result.md 摘要) + +- **Analysis**(大模型拆解原始输出): + ```json + [ + "Identify the REST API endpoint to be documented", + "Gather technical specifications of the endpoint (e.g., HTTP method, URL, request/response formats)", + "Document the endpoint's purpose and functionality", + ... + "Format the documentation for readability and consistency" + ] + ``` +- **Worker Outputs**(每个子任务的处理结果): + - 针对“Identify the REST API endpoint to be documented”,worker 会主动询问 API 细节,体现了智能 agent 的交互性。 + - 针对“Gather technical specifications...”,worker 会列举常见的技术规格项,并给出格式建议。 + - 针对“Write clear, step-by-step usage instructions for developers”,worker 会输出结构化的开发者指引。 + - 其他子任务也都能得到合理、专业的响应。 + +- **最终 content**:为所有 worker 输出的合成文本,便于直接展示或下游处理。 + +--- + +## 五、结果评估与客观分析 + +### 1. 优点 + +- **高度自动化**:无需手写拆解规则,完全依赖大模型智能分析。 +- **通用性强**:worker 逻辑可适配各种类型的子任务,适合多场景复用。 +- **可扩展性好**:支持并行处理,易于横向扩展。 +- **结构化输出**:便于前端展示、流程追踪和后续分析。 + +### 2. 局限与改进空间 + +- **大模型输出稳定性**:部分模型可能输出非严格 JSON,需做好容错处理。 +- **worker 处理深度**:如需更专业的子任务处理,可为不同类型子任务定制专属 worker。 +- **性能优化**:大规模并发时可引入线程池、限流等机制。 + +### 3. 适用场景 + +- 智能文档生成、API 文档自动化 +- 复杂任务的自动拆解与执行 +- AI Agent、RPA、智能问答等 + +--- + +## 六、结语 + +通过 OrchestratorWorkersWorkflow 的实现与验证,我们可以看到大模型驱动的智能编排在实际业务中的巨大潜力。只需简单 prompt,即可实现复杂任务的自动拆解与高效执行。未来,结合更细粒度的 worker 能力和更强的模型,智能体编排将更加智能和实用。 \ No newline at end of file diff --git a/spring-ai-agent/spring-ai-agent-orchestrator/pom.xml b/spring-ai-agent/spring-ai-agent-orchestrator/pom.xml new file mode 100644 index 0000000..b35e089 --- /dev/null +++ b/spring-ai-agent/spring-ai-agent-orchestrator/pom.xml @@ -0,0 +1,26 @@ + + 4.0.0 + + com.glmapper + spring-ai-agent + 0.0.1 + + + spring-ai-agent-orchestrator + jar + + spring-ai-agent-orchestrator + + + + org.springframework.ai + spring-ai-starter-model-openai + + + org.springframework.boot + spring-boot-starter-test + test + + + diff --git a/spring-ai-agent/spring-ai-agent-orchestrator/src/main/java/com/glmapper/ai/workflow/OrchestratorWorkersWorkflowApplication.java b/spring-ai-agent/spring-ai-agent-orchestrator/src/main/java/com/glmapper/ai/workflow/OrchestratorWorkersWorkflowApplication.java new file mode 100644 index 0000000..edda90f --- /dev/null +++ b/spring-ai-agent/spring-ai-agent-orchestrator/src/main/java/com/glmapper/ai/workflow/OrchestratorWorkersWorkflowApplication.java @@ -0,0 +1,14 @@ +package com.glmapper.ai.workflow; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * Hello world! + */ +@SpringBootApplication +public class OrchestratorWorkersWorkflowApplication { + public static void main(String[] args) { + SpringApplication.run(OrchestratorWorkersWorkflowApplication.class, args); + } +} diff --git a/spring-ai-agent/spring-ai-agent-orchestrator/src/main/java/com/glmapper/ai/workflow/config/OpenaiChatClientConfigs.java b/spring-ai-agent/spring-ai-agent-orchestrator/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-agent-orchestrator/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-agent-orchestrator/src/main/java/com/glmapper/ai/workflow/workflow/OrchestratorWorkersWorkflow.java b/spring-ai-agent/spring-ai-agent-orchestrator/src/main/java/com/glmapper/ai/workflow/workflow/OrchestratorWorkersWorkflow.java new file mode 100644 index 0000000..91a86db --- /dev/null +++ b/spring-ai-agent/spring-ai-agent-orchestrator/src/main/java/com/glmapper/ai/workflow/workflow/OrchestratorWorkersWorkflow.java @@ -0,0 +1,110 @@ +package com.glmapper.ai.workflow.workflow; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.glmapper.ai.workflow.workflow.model.WorkflowResponse; +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 org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; + +/** + * @Classname OrchestratorWorkersWorkflow + * @Description Orchestrator 用大模型拆解任务,Worker 并行处理,合并结果 + * @Date 2025/6/12 20:19 + * @Created by glmapper + */ + +@Component +public class OrchestratorWorkersWorkflow { + + @Autowired + private ChatClient chatClient; + + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + + public WorkflowResponse process(String taskDescription) { + try { + // 1. 用大模型拆解任务 + LlmSubtaskResult subtaskResult = callLlmForSubtasks(taskDescription); + List subtasks = subtaskResult.subtasks; + String analysis = subtaskResult.analysis; + if (subtasks == null || subtasks.isEmpty()) { + return WorkflowResponse.builder() + .success(false) + .errorMessage("大模型未能拆解出子任务") + .analysis(analysis) + .subtasks(subtasks) + .build(); + } + + // 2. Workers process subtasks in parallel + List workerResponses = subtasks.stream() + .map(subtask -> CompletableFuture.supplyAsync(() -> workerProcess(subtask))) + .collect(Collectors.toList()) + .stream() + .map(CompletableFuture::join) + .collect(Collectors.toList()); + + // 3. Results are combined into final response + String combined = String.join("\n", workerResponses); + return WorkflowResponse.builder() + .content(combined) + .success(true) + .analysis(analysis) + .subtasks(subtasks) + .workerResponses(workerResponses) + .build(); + } catch (Exception e) { + return WorkflowResponse.builder() + .success(false) + .errorMessage("Orchestrator/Worker 执行失败: " + e.getMessage()) + .build(); + } + } + + // 用大模型拆解任务,返回原始分析和子任务列表 + private LlmSubtaskResult callLlmForSubtasks(String taskDescription) throws Exception { + List messages = List.of(new SystemMessage("你是一个任务拆解专家。请将用户输入的复杂任务描述拆解为若干可以独立执行的子任务,输出格式为 JSON 数组,每个元素为一个子任务字符串。"), new UserMessage(taskDescription)); + Prompt prompt = new Prompt(messages); + String modelResult = chatClient.prompt(prompt).call().content(); + // 解析为 List + List subtasks; + try { + subtasks = OBJECT_MAPPER.readValue(modelResult, new TypeReference>() { + }); + } catch (Exception e) { + // 容错:如果模型返回不是严格 JSON 数组,尝试简单分割 + subtasks = Arrays.stream(modelResult.split("[。,.,\n]")) + .map(String::trim) + .filter(s -> !s.isEmpty()) + .collect(Collectors.toList()); + } + return new LlmSubtaskResult(modelResult, subtasks); + } + + // 内部类:封装大模型分析结果 + private record LlmSubtaskResult(String analysis, List subtasks) { + } + + private String workerProcess(String subtask) { + try { + // 你可以根据业务自定义 system prompt + String systemPrompt = "你是一个高效的AI助手,请认真完成以下子任务:"; + List messages = List.of(new SystemMessage(systemPrompt), new UserMessage(subtask)); + Prompt prompt = new Prompt(messages); + // 直接用 chatClient 让大模型"执行"子任务 + return chatClient.prompt(prompt).call().content(); + } catch (Exception e) { + // 失败时返回错误信息,便于排查 + return "[Worker Error] " + e.getMessage(); + } + } +} diff --git a/spring-ai-agent/spring-ai-agent-orchestrator/src/main/java/com/glmapper/ai/workflow/workflow/model/WorkflowRequest.java b/spring-ai-agent/spring-ai-agent-orchestrator/src/main/java/com/glmapper/ai/workflow/workflow/model/WorkflowRequest.java new file mode 100644 index 0000000..1fe2e58 --- /dev/null +++ b/spring-ai-agent/spring-ai-agent-orchestrator/src/main/java/com/glmapper/ai/workflow/workflow/model/WorkflowRequest.java @@ -0,0 +1,22 @@ +package com.glmapper.ai.workflow.workflow.model; + +import lombok.Builder; +import lombok.Data; + +/** + * @Classname WorkflowRequest + * @Description 用户请求 + * + * @Date 2025/6/10 11:23 + * @Created by Gepeng18 + */ +@Data +@Builder +public class WorkflowRequest { + + /** + * 请求内容 + */ + private String question; + +} \ No newline at end of file diff --git a/spring-ai-agent/spring-ai-agent-orchestrator/src/main/java/com/glmapper/ai/workflow/workflow/model/WorkflowResponse.java b/spring-ai-agent/spring-ai-agent-orchestrator/src/main/java/com/glmapper/ai/workflow/workflow/model/WorkflowResponse.java new file mode 100644 index 0000000..9804132 --- /dev/null +++ b/spring-ai-agent/spring-ai-agent-orchestrator/src/main/java/com/glmapper/ai/workflow/workflow/model/WorkflowResponse.java @@ -0,0 +1,47 @@ +package com.glmapper.ai.workflow.workflow.model; + +import lombok.Builder; +import lombok.Data; + +/** + * @Classname WorkflowResponse + * @Description 工作流响应 + * + * @Date 2025/6/10 14:32 + * @Created by Gepeng18 + */ +@Data +@Builder +public class WorkflowResponse { + + /** + * 大模型对任务的分析/拆解原始输出 + */ + private String analysis; + + /** + * 拆解出的所有子任务 + */ + private java.util.List subtasks; + + /** + * 每个 worker 的输出结果 + */ + private java.util.List workerResponses; + + /** + * 最终合成的内容(如汇总、总结、最终答案等) + */ + private String content; + + /** + * 总体是否成功 + */ + private boolean success; + + /** + * 错误信息(如有) + */ + private String errorMessage; + +} \ No newline at end of file diff --git a/spring-ai-agent/spring-ai-agent-orchestrator/src/main/resources/application.yml b/spring-ai-agent/spring-ai-agent-orchestrator/src/main/resources/application.yml new file mode 100644 index 0000000..b7b491c --- /dev/null +++ b/spring-ai-agent/spring-ai-agent-orchestrator/src/main/resources/application.yml @@ -0,0 +1,14 @@ +server: + port: 8080 +spring: + application: + name: spring-ai-workflow-orchestrator + ai: + openai: + api-key: ${OPENAI_API_KEY} + base-url: https://api.deepseek.com + chat: + options: + model: deepseek-chat + temperature: 0.7 + completions-path: /v1/chat/completions \ No newline at end of file diff --git a/spring-ai-agent/spring-ai-agent-orchestrator/src/test/java/com/glmapper/ai/workflow/OrchestratorWorkersWorkflowTest.java b/spring-ai-agent/spring-ai-agent-orchestrator/src/test/java/com/glmapper/ai/workflow/OrchestratorWorkersWorkflowTest.java new file mode 100644 index 0000000..0c2404a --- /dev/null +++ b/spring-ai-agent/spring-ai-agent-orchestrator/src/test/java/com/glmapper/ai/workflow/OrchestratorWorkersWorkflowTest.java @@ -0,0 +1,25 @@ +package com.glmapper.ai.workflow; + +import com.glmapper.ai.workflow.workflow.OrchestratorWorkersWorkflow; +import com.glmapper.ai.workflow.workflow.model.WorkflowResponse; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +/** + * Unit test for simple App. + */ +@SpringBootTest(classes = OrchestratorWorkersWorkflowApplication.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +public class OrchestratorWorkersWorkflowTest { + + @Autowired + private OrchestratorWorkersWorkflow orchestratorWorkersWorkflow; + + @Test + public void test() { + WorkflowResponse response = orchestratorWorkersWorkflow.process("Generate both technical and user-friendly documentation for a REST API endpoint"); + // 执行结果见 resources/test_result.md + System.out.println("Analysis: " + response.getAnalysis()); + System.out.println("Worker Outputs: " + response.getWorkerResponses()); + } +} diff --git a/spring-ai-agent/spring-ai-agent-orchestrator/src/test/resources/test_result.md b/spring-ai-agent/spring-ai-agent-orchestrator/src/test/resources/test_result.md new file mode 100644 index 0000000..905a9d2 --- /dev/null +++ b/spring-ai-agent/spring-ai-agent-orchestrator/src/test/resources/test_result.md @@ -0,0 +1,1075 @@ +Analysis: ```json +[ +"Identify the REST API endpoint to be documented", +"Gather technical specifications of the endpoint (e.g., HTTP method, URL, request/response formats)", +"Document the endpoint's purpose and functionality", +"List all required and optional parameters with their data types and descriptions", +"Provide example request payloads for different scenarios", +"Document all possible response codes and their meanings", +"Include example response payloads for different scenarios", +"Write clear, step-by-step usage instructions for developers", +"Create user-friendly explanations of technical concepts for non-technical users", +"Add any necessary warnings or best practices for using the endpoint", +"Include troubleshooting tips for common issues", +"Review and validate the documentation for accuracy and completeness", +"Format the documentation for readability and consistency" +] +``` +Worker Outputs: [```json +{ + "response": "I am ready to assist you with any tasks or questions you have. Please provide the details of the sub-tasks you'd like me to complete, and I will handle them efficiently." +} +```, 我是一个高效的AI助手,很高兴为您服务!我会认真完成您分配的任何任务。请告诉我您需要帮助的具体内容,我会以最专业、最有效的方式为您提供支持。 + +无论您需要: +- 信息查询 +- 问题解答 +- 文本处理 +- 数据分析 +- 创意生成 +- 或其他任何任务 + +我都会以清晰、准确、高效的方式完成。请随时告诉我您的具体需求,让我们开始工作吧!, To identify the REST API endpoint that needs to be documented, I would need the following details: + +1. **Base URL**: The root path of the API (e.g., `https://api.example.com/v1`) +2. **Endpoint Path**: The specific resource path (e.g., `/users`, `/orders/{id}`) +3. **HTTP Method**: The type of request (`GET`, `POST`, `PUT`, `DELETE`, etc.) +4. **Parameters**: Any query, path, or request body parameters +5. **Purpose**: A brief description of what the endpoint does + +### Example Format: +```plaintext +Endpoint: GET /api/v1/users/{id} +Description: Retrieves user details by ID. +Parameters: + - Path: `id` (required, integer) + - Query: `fields` (optional, string) +``` + +If you provide the API details, I can help structure the documentation effectively. Let me know how you'd like to proceed!, It seems like your request got cut off. Could you please provide the complete details of what you're looking for regarding the endpoint's technical specifications? + +For example, are you looking for: +1. API endpoint specifications (URL, methods, parameters, etc.)? +2. Hardware specifications of a device endpoint? +3. Network endpoint details (IP, ports, protocols)? +4. Something else entirely? + +Please provide more context so I can give you the most accurate and helpful response., 你好!我是一个高效的AI助手,随时准备帮助你完成各种任务。无论是解答问题、提供建议、处理文本,还是协助规划,我都会尽力提供准确、快速的支持。请告诉我你需要什么帮助,我会立即开始工作!, HTTP (Hypertext Transfer Protocol) methods, also known as HTTP verbs, indicate the desired action to be performed on a resource. Here are the most common HTTP methods: + +### **Core Methods** +1. **GET** + - Requests data from a specified resource (should only retrieve data, no side effects). + - Example: Fetching a webpage or API data. + +2. **POST** + - Submits data to be processed to a specified resource (often creates new records). + - Example: Submitting a form or uploading a file. + +3. **PUT** + - Updates an existing resource or creates it if it doesn’t exist (idempotent). + - Example: Updating a user’s profile. + +4. **DELETE** + - Removes a specified resource. + - Example: Deleting a blog post. + +5. **PATCH** + - Partially updates a resource (only sends changes, not the full entity). + - Example: Updating a single field in a user profile. + +### **Additional Methods** +6. **HEAD** + - Similar to GET but returns only headers (no body), useful for checking resource existence. + +7. **OPTIONS** + - Describes communication options for the target resource (used in CORS). + +8. **CONNECT** + - Establishes a tunnel to the server (used for HTTPS proxies). + +9. **TRACE** + - Echoes back the received request (used for debugging). + +### **Idempotency & Safety** +- **Safe Methods** (GET, HEAD, OPTIONS) – Do not modify the server state. +- **Idempotent Methods** (GET, PUT, DELETE, HEAD, OPTIONS, TRACE) – Repeated requests have the same effect as a single request. + +Would you like details on REST API best practices or specific use cases?, 您似乎只输入了“URL”这个词。如果您需要帮助解析、缩短或处理某个URL,请提供具体的网址或详细说明您的需求。例如: + +1. **解析URL**:我可以帮您分解网址的各个部分(协议、域名、路径等)。 +2. **缩短URL**:提供长链接,我可以生成短链接建议。 +3. **检查安全性**:需要验证某个URL是否安全?请提供具体链接。 +4. **其他操作**:如编码/解码、参数提取等。 + +请补充信息,我会全力协助! 🔍, Here are common request/response formats used in AI interactions, APIs, and web services: + +### 1. **Plain Text** +- **Request:** Simple text input +- **Response:** Plain text output +- **Example:** + *User:* "What's the weather today?" + *AI:* "Sunny, 75°F" + +### 2. **JSON (API Standard)** +- **Request:** Structured JSON with parameters + ```json + { + "query": "weather in Tokyo", + "user_id": "12345", + "language": "en" + } + ``` +- **Response:** + ```json + { + "response": "Partly cloudy, 22°C", + "source": "WeatherAPI", + "timestamp": "2023-11-15T09:30:00Z" + } + ``` + +### 3. **XML (Legacy Systems)** +- **Request:** + ```xml + + Define AI + short + + ``` +- **Response:** + ```xml + + Artificial Intelligence + Machine simulation of human intelligence. + + ``` + +### 4. **Chat Markdown (LLMs like ChatGPT)** +- **Request:** + ```markdown + Summarize this in 3 bullet points: + - AI assistants can process natural language. + - They leverage machine learning models. + - Responses may include formatted text. + ``` +- **Response:** + ```markdown + - Process natural language input + - Use ML models (e.g., transformers) + - Support formatted outputs (tables, code) + ``` + +### 5. **GraphQL (Flexible API Queries)** +- **Request:** + ```graphql + query { + book(id: "101") { + title + author + pages + } + } + ``` +- **Response:** + ```json + { + "data": { + "book": { + "title": "AI Fundamentals", + "author": "Jane Doe", + "pages": 300 + } + } + } + ``` + +### 6. **gRPC (Binary Protocol)** +- **Protocol Buffers Definition:** + ```proto + message Request { + string question = 1; + } + message Response { + string answer = 1; + } + ``` +- **Binary payloads** (not human-readable) + +### Key Considerations: +- **APIs:** JSON/GraphQL dominate modern systems +- **AI Assistants:** Often use markdown with embedded JSON/HTML for rich responses +- **Legacy:** XML still used in banking/healthcare +- **Performance:** gRPC for microservices, JSON for web + +Need a specific format explained further?, Certainly! Below is a structured documentation template for describing an endpoint's purpose and functionality. Replace the placeholders (e.g., ``) with the relevant details. + +--- + +### **Endpoint Documentation** +**Endpoint**: `` `` +**Version**: `` (e.g., `v1`) +**Authentication**: `` (e.g., `Bearer Token`, `API Key`, `None`) + +--- + +#### **Purpose** + +"This endpoint allows authenticated users to `` (e.g., retrieve a list of resources, submit data, delete a record)." + +--- + +#### **Functionality** + +1. **Input Parameters**: + - **Query Parameters**: (e.g., `?filter=active&limit=10`) + - **Path Parameters**: (e.g., `/users/{id}`) + - **Request Body**: (e.g., JSON payload for `POST/PUT` requests) + - **Headers**: (e.g., `Authorization: Bearer `, `Content-Type: application/json`) + +2. **Behavior**: + - Success scenarios (e.g., "Returns `200 OK` with a list of items"). + - Error scenarios (e.g., "Returns `404 Not Found` if the resource doesn’t exist"). + +3. **Response Format**: + - Example success response (JSON/XML): + ```json + { + "status": "success", + "data": [ /* array of items */ ] + } + ``` + - Example error response: + ```json + { + "status": "error", + "code": 404, + "message": "Resource not found." + } + ``` + +--- + +#### **Examples** +**Request**: +```http +GET /api/v1/users?role=admin HTTP/1.1 +Authorization: Bearer +``` + +**Response**: +```json +{ + "data": [ + { "id": 1, "name": "Admin User", "role": "admin" } + ] +} +``` + +--- + +#### **Notes** +- +- + +--- + +Let me know if you'd like to refine this for a specific endpoint (e.g., REST, GraphQL, RPC)!, Here is a structured template for listing required and optional parameters with their data types and descriptions: + +--- + +### **Parameters** + +#### **Required Parameters** +*(Must be provided for the function/API to work)* + +| Parameter | Data Type | Description | +|-------------|-----------|-----------------------------------------------------------------------------| +| `param1` | `string` | Explanation of what this parameter does (e.g., "Username for authentication"). | +| `param2` | `integer` | Description (e.g., "Age of the user, must be a positive number"). | + +#### **Optional Parameters** +*(Can be omitted; default values may apply)* + +| Parameter | Data Type | Default Value | Description | +|-------------|-------------|---------------|-----------------------------------------------------------------------------| +| `optParam1` | `boolean` | `true` | Description (e.g., "If enabled, sends notifications"). | +| `optParam2` | `string[]` | `[]` | Description (e.g., "List of tags for filtering results"). | + +--- + +### **Notes** +- Replace placeholders (`param1`, `optParam1`, etc.) with actual parameter names. +- Adjust **data types** (e.g., `number`, `object`, `array`) as needed. +- Specify **constraints** (e.g., "Must be a valid email") if applicable. +- Mention **default values** for optional parameters. + +Would you like this tailored for a specific API, function, or programming language?, Here are example request payloads for different scenarios across common API use cases: + +### 1. User Authentication (Login) +```json +{ + "username": "johndoe", + "password": "securePassword123!", + "remember_me": true +} +``` + +### 2. E-commerce Product Creation +```json +{ + "name": "Wireless Bluetooth Headphones", + "description": "Noise-cancelling with 30hr battery life", + "price": 129.99, + "stock_quantity": 150, + "categories": ["electronics", "audio"], + "sku": "WBH-2023-XL" +} +``` + +### 3. Weather Data Query +```json +{ + "location": { + "city": "Seattle", + "country": "US", + "coordinates": { + "lat": 47.6062, + "lon": -122.3321 + } + }, + "date_range": { + "start": "2023-11-15", + "end": "2023-11-20" + }, + "units": "metric" +} +``` + +### 4. Bank Transaction +```json +{ + "transaction_id": "TX987654321", + "from_account": "ACC123456", + "to_account": "ACC789012", + "amount": 2500.00, + "currency": "USD", + "description": "Invoice #INV-2023-456", + "metadata": { + "invoice_id": "INV-2023-456", + "client_id": "CLT-789" + } +} +``` + +### 5. IoT Device Telemetry +```json +{ + "device_id": "thermo-7X9B2", + "timestamp": "2023-11-15T08:23:45Z", + "readings": { + "temperature": 23.4, + "humidity": 45, + "power_consumption": 2.34, + "battery_level": 87 + }, + "status": "normal" +} +``` + +### 6. Calendar Event Creation +```json +{ + "title": "Team Sync Meeting", + "description": "Weekly project status update", + "start_time": "2023-11-20T14:00:00-08:00", + "end_time": "2023-11-20T15:00:00-08:00", + "attendees": [ + {"email": "alice@example.com", "rsvp": true}, + {"email": "bob@example.com", "rsvp": null} + ], + "reminders": [ + {"method": "email", "minutes": 30}, + {"method": "popup", "minutes": 10} + ] +} +``` + +### 7. AI Image Generation Request +```json +{ + "prompt": "A futuristic cityscape at sunset with flying cars", + "style": "cyberpunk", + "resolution": "1024x768", + "num_images": 3, + "advanced": { + "seed": 42, + "steps": 50, + "cfg_scale": 7.5 + } +} +``` + +### 8. Support Ticket Submission +```json +{ + "subject": "Login issues after recent update", + "description": "Unable to login since v2.3 update - getting 403 error", + "user": { + "id": "USR-7890", + "email": "user@example.com", + "device_info": "iPhone 13, iOS 16.4" + }, + "attachments": ["error_screenshot.jpg"], + "priority": "high", + "category": "authentication" +} +``` + +Each payload demonstrates: +- Proper structure for the domain +- Common required fields +- Example data formats +- Optional parameters where relevant + +Would you like me to provide payload examples for any specific scenario or industry?, Here is a comprehensive list of common HTTP response status codes, grouped by their categories, along with their meanings: + +--- + +### **1xx: Informational Responses** +*(Request received, continuing process)* +- **100 Continue** + - The server acknowledges the initial part of the request; the client should proceed. +- **101 Switching Protocols** + - The server agrees to switch protocols (e.g., upgrading to WebSocket). +- **102 Processing** (WebDAV) + - The server is processing the request but has not yet completed it. +- **103 Early Hints** + - Used to return some response headers before the final response. + +--- + +### **2xx: Success Responses** +*(Request successfully received, understood, and processed)* +- **200 OK** + - The request succeeded. +- **201 Created** + - A new resource was successfully created (e.g., after a POST request). +- **202 Accepted** + - The request was accepted but not yet processed. +- **203 Non-Authoritative Information** + - The response is a modified version of the origin server’s response. +- **204 No Content** + - The request succeeded, but there is no content to return. +- **205 Reset Content** + - The client should reset the document view (e.g., clear a form). +- **206 Partial Content** + - The server is delivering only part of the resource (used for range requests). +- **207 Multi-Status** (WebDAV) + - Contains multiple status codes for a batch request. +- **208 Already Reported** (WebDAV) + - Prevents redundant listing of resources. +- **226 IM Used** + - The server has fulfilled a request for the resource with instance manipulation. + +--- + +### **3xx: Redirection Responses** +*(Further action is needed to complete the request)* +- **300 Multiple Choices** + - The request has multiple possible responses (user/agent should choose one). +- **301 Moved Permanently** + - The resource has been permanently moved to a new URL. +- **302 Found (Previously "Moved Temporarily")** + - The resource is temporarily moved to a different URL. +- **303 See Other** + - The response can be found under a different URL (used for POST-to-GET redirects). +- **304 Not Modified** + - Indicates caching; the resource has not been modified since the last request. +- **305 Use Proxy** (Deprecated) + - The request must be accessed through a proxy. +- **307 Temporary Redirect** + - The resource is temporarily moved, but the method and body must not change. +- **308 Permanent Redirect** + - The resource is permanently moved, and the method must not change. + +--- + +### **4xx: Client Error Responses** +*(The request contains errors or cannot be fulfilled)* +- **400 Bad Request** + - The server cannot process the request due to client-side errors (e.g., malformed syntax). +- **401 Unauthorized** + - Authentication is required, and the client has not provided valid credentials. +- **402 Payment Required** (Rarely used) + - Reserved for future use (originally intended for digital payments). +- **403 Forbidden** + - The server refuses to authorize the request (even if authenticated). +- **404 Not Found** + - The requested resource does not exist. +- **405 Method Not Allowed** + - The HTTP method is not supported for the resource. +- **406 Not Acceptable** + - The server cannot produce a response matching the client’s `Accept` headers. +- **407 Proxy Authentication Required** + - The client must authenticate with the proxy first. +- **408 Request Timeout** + - The server timed out waiting for the request. +- **409 Conflict** + - The request conflicts with the current state of the server (e.g., edit collisions). +- **410 Gone** + - The resource is permanently deleted (unlike 404, this is intentional). +- **411 Length Required** + - The server requires a `Content-Length` header. +- **412 Precondition Failed** + - A client-side condition (e.g., `If-Match`) failed. +- **413 Payload Too Large** + - The request exceeds the server’s size limits. +- **414 URI Too Long** + - The URL is longer than the server can process. +- **415 Unsupported Media Type** + - The payload format is unsupported by the server. +- **416 Range Not Satisfiable** + - The requested range (e.g., bytes) is invalid. +- **417 Expectation Failed** + - The server cannot meet the `Expect` header’s requirements. +- **418 I’m a Teapot** (RFC 2324, April Fools’ joke) + - The server refuses to brew coffee because it is a teapot. +- **421 Misdirected Request** + - The request was sent to a server unable to respond (e.g., wrong TLS config). +- **422 Unprocessable Entity** (WebDAV) + - The request was well-formed but semantically incorrect (e.g., validation errors). +- **423 Locked** (WebDAV) + - The resource is locked and cannot be modified. +- **424 Failed Dependency** (WebDAV) + - The request failed because it depended on another failed request. +- **425 Too Early** + - The server is unwilling to process a request that might be replayed. +- **426 Upgrade Required** + - The client must switch to a different protocol (e.g., TLS). +- **428 Precondition Required** + - The server requires conditional requests (e.g., `If-Match`). +- **429 Too Many Requests** + - The client has exceeded rate limits. +- **431 Request Header Fields Too Large** + - The headers exceed the server’s size limit. +- **451 Unavailable For Legal Reasons** + - The resource is blocked due to legal restrictions. + +--- + +### **5xx: Server Error Responses** +*(The server failed to fulfill a valid request)* +- **500 Internal Server Error** + - A generic server-side error occurred. +- **501 Not Implemented** + - The server lacks the functionality to fulfill the request. +- **502 Bad Gateway** + - The server acting as a gateway received an invalid response. +- **503 Service Unavailable** + - The server is temporarily unable to handle requests (e.g., overloaded). +- **504 Gateway Timeout** + - The gateway server did not receive a timely response. +- **505 HTTP Version Not Supported** + - The server does not support the HTTP version used in the request. +- **506 Variant Also Negotiates** + - The server has a configuration error in content negotiation. +- **507 Insufficient Storage** (WebDAV) + - The server cannot store the representation needed to complete the request. +- **508 Loop Detected** (WebDAV) + - The server detected an infinite loop while processing the request. +- **510 Not Extended** + - Further extensions to the request are required. +- **511 Network Authentication Required** + - The client must authenticate to access the network (e.g., captive portals). + +--- + +### **Unofficial / Non-Standard Codes** +- **418 I’m a Teapot** (Kept for historical reasons) +- **420 Enhance Your Calm** (Twitter API rate-limiting) +- **450 Blocked by Windows Parental Controls** (Microsoft) +- **509 Bandwidth Limit Exceeded** (Apache/cPanel) + +--- + +This list covers the most widely recognized HTTP status codes. Let me know if you'd like further details on any specific code!, Certainly! Below are example response payloads for different scenarios across various contexts (e.g., REST APIs, webhooks, or event-driven systems). Each example includes a brief description of the scenario and the corresponding payload in JSON format. + +--- + +### **1. Successful API Response (e.g., Fetching User Data)** +**Scenario**: A GET request to `/users/123` returns the details of user with ID `123`. +**Response**: +```json +{ + "status": "success", + "data": { + "id": 123, + "name": "John Doe", + "email": "john.doe@example.com", + "role": "admin", + "created_at": "2023-01-15T10:30:00Z" + } +} +``` + +--- + +### **2. Error Response (e.g., Invalid Request)** +**Scenario**: A POST request to `/users` fails due to missing required fields. +**Response**: +```json +{ + "status": "error", + "code": 400, + "message": "Validation failed", + "errors": [ + { + "field": "email", + "message": "Email is required" + }, + { + "field": "name", + "message": "Name must be at least 3 characters" + } + ] +} +``` + +--- + +### **3. Paginated List Response** +**Scenario**: A GET request to `/products` returns a paginated list of products. +**Response**: +```json +{ + "status": "success", + "data": [ + { + "id": 1, + "name": "Laptop", + "price": 999.99 + }, + { + "id": 2, + "name": "Smartphone", + "price": 699.99 + } + ], + "pagination": { + "total_items": 42, + "current_page": 1, + "per_page": 10, + "total_pages": 5 + } +} +``` + +--- + +### **4. Webhook Notification (e.g., Payment Processed)** +**Scenario**: A payment service sends a webhook notification after a successful transaction. +**Payload**: +```json +{ + "event_id": "evt_123456789", + "type": "payment.succeeded", + "data": { + "payment_id": "pay_987654321", + "amount": 2500, + "currency": "USD", + "customer_id": "cus_abc123", + "timestamp": "2023-05-20T14:22:10Z" + } +} +``` + +--- + +### **5. Empty Response (e.g., No Results Found)** +**Scenario**: A search query for `/orders?status=cancelled` returns no matching orders. +**Response**: +```json +{ + "status": "success", + "data": [], + "message": "No orders found with the specified criteria." +} +``` + +--- + +### **6. Authentication Token Response** +**Scenario**: A successful login request to `/auth/login` returns a JWT token. +**Response**: +```json +{ + "status": "success", + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", + "expires_in": 3600, + "user": { + "id": 123, + "name": "John Doe" + } +} +``` + +--- + +### **7. Event-Driven System (e.g., Order Shipped)** +**Scenario**: A message queue publishes an event when an order is shipped. +**Payload**: +```json +{ + "event": "order.shipped", + "event_id": "evt_789012345", + "timestamp": "2023-05-20T09:15:33Z", + "payload": { + "order_id": "ord_456789", + "customer_id": "cust_123", + "tracking_number": "UPS123456789", + "shipping_address": "123 Main St, City, Country" + } +} +``` + +--- + +### Key Takeaways: +- Use consistent structures (e.g., `status`, `data`, `errors`). +- Include metadata (e.g., pagination, timestamps) where relevant. +- For errors, provide actionable details (e.g., field-specific validation errors). +- Use standard HTTP status codes (e.g., `200`, `400`, `404`). + +Let me know if you'd like examples for other scenarios!, Here’s a clear and concise response to your request: + +--- +**"Write clear"** + +To write clearly: +1. **Be concise** – Use simple words and avoid unnecessary jargon. +2. **Structure logically** – Organize ideas with headings, bullet points, or short paragraphs. +3. **Focus on the reader** – Tailor language to their level of understanding. +4. **Edit ruthlessly** – Remove redundancies and tighten sentences. +5. **Use active voice** – E.g., "The team completed the project" (not "The project was completed"). + +Example of clear writing: +> *"Submit the report by Friday. Include budget details and risks. Contact me if you need help."* + +Let me know if you'd like help with a specific text! + +--- +Would you like me to refine or expand on any part?, Here’s a clear, step-by-step guide for developers to effectively use your AI assistant: + +--- + +### **Step-by-Step Usage Instructions for Developers** + +#### **1. Access the AI Assistant** +- **Via API**: + - Obtain your API key (if authentication is required). + - Use the endpoint: `POST https://api.example.com/v1/assistant` + - Include headers: + ```json + { + "Authorization": "Bearer YOUR_API_KEY", + "Content-Type": "application/json" + } + ``` + +- **Via SDK (if available)**: + ```python + from ai_assistant import Assistant + assistant = Assistant(api_key="YOUR_API_KEY") + ``` + +#### **2. Define Your Task** +Structure your request with: +- **Input**: Clearly describe the task (e.g., "Debug this Python code" or "Generate a SQL query"). +- **Context**: Provide relevant code snippets, error logs, or constraints. + +**Example API Request**: +```json +{ + "task": "Optimize this Python function for speed", + "context": "def calculate(...): ..." +} +``` + +#### **3. Submit the Request** +- **API**: + ```bash + curl -X POST https://api.example.com/v1/assistant \ + -H "Authorization: Bearer YOUR_API_KEY" \ + -d '{"task":"...", "context":"..."}' + ``` +- **SDK**: + ```python + response = assistant.ask(task="...", context="...") + ``` + +#### **4. Process the Response** +- The AI will return structured output (e.g., code, explanations, or steps). +- **Example Response**: + ```json + { + "response": "Here’s the optimized function...", + "suggestions": ["Use list comprehensions", "Avoid nested loops"] + } + ``` + +#### **5. Iterate (If Needed)** +- Refine your query based on initial results: + - Add more context (e.g., "The function must handle edge cases like X"). + - Ask follow-ups (e.g., "Explain why you chose this approach"). + +#### **6. Integrate into Workflow** +- **Automation**: Schedule tasks via cron jobs/webhooks. +- **Error Handling**: Check for `status_code` or `error` fields in responses. + +#### **7. Best Practices** +- **Be Specific**: Narrow tasks yield better results (e.g., "Fix this null pointer exception" vs. "Debug my app"). +- **Secure Data**: Avoid sharing sensitive info; use placeholders if needed. +- **Rate Limits**: Check API docs for call limits. + +#### **8. Troubleshooting** +- **Common Issues**: + - `401 Unauthorized`: Verify your API key. + - `400 Bad Request`: Check input JSON formatting. + - Timeouts: Reduce payload size or retry. + +--- + +**Example Use Case**: +```python +# Ask the AI to generate a React component +response = assistant.ask( + task="Create a React button component with hover effects", + context="Use Tailwind CSS for styling." +) +print(response["code"]) +``` + +Let me know if you’d like details for a specific language/platform!, Certainly! Here’s a structured approach to creating user-friendly explanations of technical concepts for non-technical audiences: + +### **1. Simplify the Language** +- **Avoid jargon**: Replace technical terms with everyday words. + - *Example*: Instead of "The API fetches data," say, "The system retrieves information from another service." +- **Use analogies**: Compare the concept to something familiar. + - *Example*: "A **firewall** is like a security guard that checks who can enter a building." + +### **2. Focus on the "Why" Before the "How"** +- Explain the **purpose** first, not the mechanics. + - *Example*: "**Encryption** scrambles your messages so only the intended person can read them—like sending a locked box instead of a postcard." + +### **3. Break It Down Step-by-Step** +- Use short, clear sentences. + - *Example*: + 1. You type a website name (e.g., "google.com"). + 2. Your browser finds the correct address (like GPS for the internet). + 3. The website loads on your screen. + +### **4. Visual Aids (When Possible)** +- Simple diagrams, icons, or metaphors help. + - *Example*: "Cloud storage is like a virtual filing cabinet—you store files online instead of on your computer." + +### **5. Relate to Real-Life Scenarios** +- Tie the concept to daily experiences. + - *Example*: "A **VPN** is like a tunnel that keeps your internet activity private, even on public Wi-Fi." + +### **6. Anticipate Questions** +- Address common concerns naturally. + - *Example*: "Yes, **two-factor authentication** adds an extra step, but it’s like a second lock on your door—it’s worth it!" + +### **7. Keep It Concise** +- Avoid overwhelming details. Stick to what’s relevant. + +--- +### **Example: Explaining "Machine Learning"** +**Technical**: "Machine learning uses algorithms to parse data, learn patterns, and make predictions." +**User-Friendly**: +> "Machine learning helps computers learn from experience, like how a child improves at catching a ball over time. The more examples it sees (e.g., spam emails), the better it gets at spotting similar things." + +Would you like me to adapt a specific technical concept for you? 😊, Here are some essential warnings and best practices for using the endpoint effectively and safely: + +### **Warnings:** +1. **Rate Limiting** – Avoid excessive requests to prevent being throttled or blocked. Check the API documentation for rate limits. +2. **Data Privacy** – Do not send sensitive, personally identifiable information (PII) unless the endpoint is explicitly designed for it. +3. **Error Handling** – Always implement proper error handling for failed requests (e.g., timeouts, invalid inputs). +4. **Authentication** – Keep API keys and credentials secure. Never expose them in client-side code. +5. **Deprecation Risks** – Endpoints may change or be deprecated; monitor API updates to avoid disruptions. + +### **Best Practices:** +✅ **Validate Inputs** – Ensure request payloads match the expected format before sending. +✅ **Use Retries with Backoff** – Implement exponential backoff for transient failures. +✅ **Monitor Usage** – Track API consumption to stay within limits and optimize costs. +✅ **Cache Responses** – Store frequent, non-changing responses to reduce unnecessary calls. +✅ **Follow API Documentation** – Adhere to the provider’s guidelines for optimal performance. + +Would you like me to tailor these further for a specific use case?, Certainly! Below is a structured troubleshooting guide for common issues, which you can adapt to your specific needs (e.g., software, hardware, or general problems). + +--- + +### **Troubleshooting Tips for Common Issues** + +#### **1. Device/Application Not Responding** +- **Fix 1**: Force-close the app/restart the device. +- **Fix 2**: Check for updates (OS, firmware, or app updates). +- **Fix 3**: Clear cache/data (for apps) or free up memory/storage. + +#### **2. Slow Performance** +- **Fix 1**: Close unnecessary background apps. +- **Fix 2**: Check for malware/viruses (run a scan). +- **Fix 3**: Upgrade hardware (RAM/SSD) if applicable. + +#### **3. Connection Issues (Wi-Fi/Bluetooth)** +- **Fix 1**: Restart the router or device. +- **Fix 2**: Forget and reconnect to the network. +- **Fix 3**: Check for interference (distance, obstacles, other devices). + +#### **4. Login/Password Problems** +- **Fix 1**: Use "Forgot Password" to reset. +- **Fix 2**: Check caps lock/num lock. +- **Fix 3**: Clear browser cache or try incognito mode. + +#### **5. Error Messages/Crashes** +- **Fix 1**: Note the exact error code and search online. +- **Fix 2**: Reinstall the software/driver. +- **Fix 3**: Check system compatibility (OS version, hardware). + +#### **6. Peripheral Issues (Printer/Mouse/Keyboard)** +- **Fix 1**: Reconnect/restart the device. +- **Fix 2**: Update or reinstall drivers. +- **Fix 3**: Test on another device to isolate the issue. + +#### **7. Battery Draining Quickly** +- **Fix 1**: Reduce screen brightness/disable unused features. +- **Fix 2**: Check for battery-hogging apps. +- **Fix 3**: Replace the battery if old/degraded. + +#### **8. File Corruption/Data Loss** +- **Fix 1**: Use file recovery software. +- **Fix 2**: Restore from backup (if available). +- **Fix 3**: Check storage health (for hardware issues). + +#### **General Tips:** +- **Restart first**: Solves many temporary glitches. +- **Check logs**: System/application logs often reveal root causes. +- **Search forums**: Others may have solved the same issue. + +--- + +Would you like this tailored to a specific product or scenario? Let me know, and I can refine it further!, Certainly! Below is a structured approach to reviewing and validating documentation for accuracy and completeness: + +### **1. Define the Scope and Objectives** +- Identify the type of documentation (e.g., technical, user manuals, API docs, process guides). +- Clarify the purpose (e.g., compliance, training, operational reference). + +### **2. Check for Completeness** +- Verify all required sections are included (e.g., introduction, procedures, troubleshooting, references). +- Ensure no missing steps, diagrams, or critical details. +- Cross-check against a checklist or template (if available). + +### **3. Validate Accuracy** +- **Technical Accuracy:** + - Confirm that instructions, code snippets, and configurations are correct. + - Test procedures (if applicable) to ensure they work as described. +- **Consistency:** + - Ensure terminology, formatting, and style are uniform. + - Check for contradictions between sections. +- **Up-to-date Information:** + - Verify that content aligns with the latest software version, policies, or processes. + - Remove outdated references or deprecated features. + +### **4. Review Clarity and Usability** +- Ensure language is clear, concise, and free of jargon (unless audience-specific). +- Check that diagrams, screenshots, or examples support understanding. +- Assess logical flow—can a user follow the document without confusion? + +### **5. Compliance & Standards Alignment** +- Confirm adherence to organizational/industry standards (e.g., ISO, GDPR, internal style guides). +- Validate that mandatory disclaimers, version control, and approval signatures are present. + +### **6. Peer Review & Feedback** +- Engage SMEs (Subject Matter Experts) to verify technical content. +- Conduct a walkthrough with end-users to identify gaps. + +### **7. Final Approval & Version Control** +- Document revisions and track changes. +- Obtain formal approval before publishing. + +### **Tools to Assist:** +- **Grammar/Clarity:** Grammarly, Hemingway Editor +- **Version Control:** Git, SharePoint +- **Collaboration:** Confluence, Google Docs, MS Teams + +### **Output:** +- A revised document with tracked changes. +- A validation report noting corrections, approvals, and open issues (if any). + +Would you like a template for the validation checklist or a specific focus area (e.g., API docs, SOPs)?, Certainly! Below is a structured and consistent documentation template that improves readability. You can customize it based on your specific needs. + +--- + +# **Documentation Title** +*Brief description or tagline (optional)* + +## **1. Overview** +*A concise introduction to the purpose, scope, and key features of the project/tool.* + +## **2. Installation** +### **Prerequisites** +- List any dependencies (e.g., Python 3.8+, Node.js, etc.). +- Required libraries or frameworks. + +### **Steps** +1. Step-by-step installation guide (e.g., `pip install package-name`). +2. Additional setup if needed (e.g., environment variables). + +## **3. Usage** +### **Basic Commands** +```bash +command --example "Usage syntax" +``` +- Explanation of the command. + +### **Examples** +- Example 1: Brief description. + ```bash + command --input file.txt --output results/ + ``` +- Example 2: Brief description. + +## **4. Configuration** +*Describe configuration files, flags, or options.* +- **`config.yml`** (example): + ```yaml + key: value + enabled: true + ``` + +## **5. Troubleshooting** +| Issue | Solution | +|-------|----------| +| Error XYZ | Fix steps or workaround. | + +## **6. FAQ** +**Q:** Common question? +**A:** Clear answer. + +## **7. Contributing** +- Guidelines for pull requests, issues, or code style. +- Link to a `CONTRIBUTING.md` if applicable. + +## **8. License** +*Specify the license (e.g., MIT, Apache 2.0).* + +--- + +### **Formatting Tips for Consistency** +- Use **headings** (h1/h2/h3) for hierarchy. +- **Code blocks** with syntax highlighting. +- **Bullet points** for lists. +- **Tables** for structured data (e.g., troubleshooting). +- **Bold/italic** for emphasis. + +Let me know if you'd like this adapted for a specific tool or language!, 你好!我是一个高效的AI助手,很高兴为你服务。请问有什么我可以帮助你的吗?无论是问题解答、任务处理还是创意生成,我都会尽力提供快速、准确的帮助。请告诉我你的具体需求吧!, 您好!我是一个高效的AI助手,很高兴为您服务。请问有什么我可以帮助您完成的子任务吗?我会认真、专业地协助您解决问题。] 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..6c31d9a --- /dev/null +++ b/spring-ai-agent/spring-ai-workflow/src/main/java/com/glmapper/ai/workflow/core/step/impl/RouterSelectorWorkflowStep.java @@ -0,0 +1,84 @@ +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; + } + + private static final String PROMPT_TEMPLATE = """ + 你是一个专业的路由选择器。根据用户的问题,从以下可用的路由中选择最合适的一个: + 可用路由: + %s \s + 请仅返回最合适的路由的键名,不要包含任何额外解释。例如,如果最合适的路由是"technical",只需返回"technical"。"""; + + /** + * 执行步骤 + * + * @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 = String.format(PROMPT_TEMPLATE, routeInfo); + + // 创建并发送提示 + 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> resultFuture = allFutures.thenApply(v -> + futures.stream() + .map(CompletableFuture::join) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)) + ); + + Map results = resultFuture.get(); + String content = formatResults(results); + log.info("并行工作流执行完成,结果数量: {}, 执行结果为:\n {}", results.size(), content); + + return WorkflowResponse.builder() + .success(true) + .content(content) + .build(); + + } catch (InterruptedException | ExecutionException e) { + log.error("并行工作流执行失败", e); + return WorkflowResponse.builder() + .success(false) + .errorMessage("工作流执行失败: " + e.getMessage()) + .build(); + } + } + + /** + * 格式化结果为可读字符串 + * + * @param results 结果映射 + * @return 格式化的结果字符串 + */ + private String formatResults(Map results) { + StringBuilder sb = new StringBuilder(); + sb.append("并行工作流执行结果:\n"); + + results.forEach((key, value) -> { + sb.append("步骤 [").append(key).append("]: \n"); + sb.append(value != null ? value.toString() : "null").append("\n\n"); + }); + + return sb.toString(); + } +} \ 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/RoutingWorkflow.java b/spring-ai-agent/spring-ai-workflow/src/main/java/com/glmapper/ai/workflow/core/workflow/impl/RoutingWorkflow.java new file mode 100644 index 0000000..6c5a78c --- /dev/null +++ b/spring-ai-agent/spring-ai-workflow/src/main/java/com/glmapper/ai/workflow/core/workflow/impl/RoutingWorkflow.java @@ -0,0 +1,68 @@ +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.Map; + +/** + * @Classname RoutingWorkflow + * @Description 路由工作流实现:基于路由规则选择合适的工作流步骤执行 + * + * @Date 2025/6/10 16:36 + * @Created by Gepeng18 + */ +@Slf4j +public class RoutingWorkflow implements Workflow { + + private final WorkflowStep routerSelector; + private final Map stepMap; + + public RoutingWorkflow(WorkflowStep routerSelector, Map stepMap) { + this.routerSelector = routerSelector; + this.stepMap = stepMap; + } + + @Override + public WorkflowResponse execute(WorkflowRequest input) { + try { + log.info("开始执行路由工作流, 路由规则数量: {}", stepMap.size()); + + // 使用路由选择器确定最合适的路由 + String routeKey = (String) routerSelector.execute(input); + log.info("路由结果: {}", routeKey); + + // 查找对应的步骤 + WorkflowStep step = stepMap.get(routeKey); + + if (step == null) { + log.error("未找到路由对应的步骤: {}", routeKey); + return WorkflowResponse.builder() + .success(false) + .errorMessage("未找到路由对应的步骤: " + routeKey) + .build(); + } + + // 执行找到的步骤 + log.info("执行步骤: {}", step.name()); + Object result = step.execute(input.getQuestion()); + + log.info("路由工作流执行完成, 执行结果为: \n {}", result); + + return WorkflowResponse.builder() + .content(result != null ? result.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/model/WorkflowRequest.java b/spring-ai-agent/spring-ai-workflow/src/main/java/com/glmapper/ai/workflow/model/WorkflowRequest.java new file mode 100644 index 0000000..4c85398 --- /dev/null +++ b/spring-ai-agent/spring-ai-workflow/src/main/java/com/glmapper/ai/workflow/model/WorkflowRequest.java @@ -0,0 +1,22 @@ +package com.glmapper.ai.workflow.model; + +import lombok.Builder; +import lombok.Data; + +/** + * @Classname WorkflowRequest + * @Description 用户请求 + * + * @Date 2025/6/10 11:23 + * @Created by Gepeng18 + */ +@Data +@Builder +public class WorkflowRequest { + + /** + * 请求内容 + */ + private String question; + +} \ No newline at end of file diff --git a/spring-ai-agent/spring-ai-workflow/src/main/java/com/glmapper/ai/workflow/model/WorkflowResponse.java b/spring-ai-agent/spring-ai-workflow/src/main/java/com/glmapper/ai/workflow/model/WorkflowResponse.java new file mode 100644 index 0000000..7d60646 --- /dev/null +++ b/spring-ai-agent/spring-ai-workflow/src/main/java/com/glmapper/ai/workflow/model/WorkflowResponse.java @@ -0,0 +1,32 @@ +package com.glmapper.ai.workflow.model; + +import lombok.Builder; +import lombok.Data; + +/** + * @Classname WorkflowResponse + * @Description 工作流响应 + * + * @Date 2025/6/10 14:32 + * @Created by Gepeng18 + */ +@Data +@Builder +public class WorkflowResponse { + + /** + * 响应内容 + */ + private String content; + + /** + * 是否成功 + */ + private boolean success; + + /** + * 错误信息 + */ + private String errorMessage; + +} \ No newline at end of file diff --git a/spring-ai-agent/spring-ai-workflow/src/main/resources/application.yml b/spring-ai-agent/spring-ai-workflow/src/main/resources/application.yml new file mode 100644 index 0000000..208dcc9 --- /dev/null +++ b/spring-ai-agent/spring-ai-workflow/src/main/resources/application.yml @@ -0,0 +1,14 @@ +server: + port: 8080 + +spring: + application: + name: spring-ai-workflow + ai: + openai: + api-key: ${OPENAI_API_KEY} + base-url: ${OPENAI_BASE_URL} + chat: + options: + model: gpt-4 + temperature: 0.7 diff --git a/spring-ai-agent/spring-ai-workflow/src/test/java/com/glmapper/ai/workflow/ChainWorkflowTest.java b/spring-ai-agent/spring-ai-workflow/src/test/java/com/glmapper/ai/workflow/ChainWorkflowTest.java new file mode 100644 index 0000000..329e7a7 --- /dev/null +++ b/spring-ai-agent/spring-ai-workflow/src/test/java/com/glmapper/ai/workflow/ChainWorkflowTest.java @@ -0,0 +1,64 @@ +package com.glmapper.ai.workflow; + +import com.glmapper.ai.workflow.model.WorkflowRequest; +import com.glmapper.ai.workflow.model.WorkflowResponse; +import com.glmapper.ai.workflow.core.step.WorkflowStep; +import com.glmapper.ai.workflow.core.WorkflowFactory; +import com.glmapper.ai.workflow.core.WorkflowStepFactory; +import com.glmapper.ai.workflow.core.workflow.Workflow; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import java.util.Arrays; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@SpringBootTest +public class ChainWorkflowTest { + + @Autowired + private WorkflowStepFactory workflowStepFactory; + + @Autowired + private WorkflowFactory workflowFactory; + + @Test + @DisplayName("演示链式工作流基本用法") + void demonstrateChainWorkflowUsage() { + // 创建工作流步骤 + WorkflowStep outlineStep = workflowStepFactory.createAiStep( + "大纲生成", + "根据用户提供的主题,生成一个详细的内容大纲,包括引言、主要部分和结论。" + ); + + WorkflowStep expandStep = workflowStepFactory.createAiStep( + "内容扩写", + "根据提供的大纲,创作一篇完整的文章。" + ); + + WorkflowStep polishStep = workflowStepFactory.createAiStep( + "润色优化", + "对提供的文章进行润色和优化,改进语言表达。" + ); + + // 创建链式工作流 + List steps = Arrays.asList(outlineStep, expandStep, polishStep); + Workflow chainWorkflow = workflowFactory.createChainWorkflow(steps); + + // 执行工作流 + WorkflowRequest request = WorkflowRequest.builder() + .question("人工智能在医疗领域的应用") + .build(); + + WorkflowResponse response = chainWorkflow.execute(request); + + // 验证响应 + assertNotNull(response); + assertTrue(response.isSuccess()); + assertNotNull(response.getContent()); + } +} \ No newline at end of file diff --git a/spring-ai-agent/spring-ai-workflow/src/test/java/com/glmapper/ai/workflow/ParallelizationWorkflowTest.java b/spring-ai-agent/spring-ai-workflow/src/test/java/com/glmapper/ai/workflow/ParallelizationWorkflowTest.java new file mode 100644 index 0000000..deca1eb --- /dev/null +++ b/spring-ai-agent/spring-ai-workflow/src/test/java/com/glmapper/ai/workflow/ParallelizationWorkflowTest.java @@ -0,0 +1,66 @@ +package com.glmapper.ai.workflow; + +import com.glmapper.ai.workflow.model.WorkflowRequest; +import com.glmapper.ai.workflow.model.WorkflowResponse; +import com.glmapper.ai.workflow.core.step.WorkflowStep; +import com.glmapper.ai.workflow.core.WorkflowFactory; +import com.glmapper.ai.workflow.core.WorkflowStepFactory; +import com.glmapper.ai.workflow.core.workflow.Workflow; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import java.util.Arrays; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@SpringBootTest +public class ParallelizationWorkflowTest { + + @Autowired + private WorkflowStepFactory workflowStepFactory; + + @Autowired + private WorkflowFactory workflowFactory; + + @Test + @DisplayName("演示并行工作流基本用法") + void demonstrateParallelWorkflowUsage() { + // 创建工作流步骤 + WorkflowStep marketingStep = workflowStepFactory.createAiStep( + "市场营销评估", + "从市场营销角度对产品进行评估,包括市场定位和目标受众分析" + ); + + WorkflowStep productManagerStep = workflowStepFactory.createAiStep( + "产品经理评估", + "从产品管理角度对产品进行评估,分析产品定位和功能" + ); + + WorkflowStep uxDesignerStep = workflowStepFactory.createAiStep( + "用户体验评估", + "从用户体验角度对产品进行评估,分析界面设计和交互流程" + ); + + // 创建并行工作流 + List steps = Arrays.asList( + marketingStep, productManagerStep, uxDesignerStep + ); + Workflow parallelWorkflow = workflowFactory.createParallelizationWorkflow(steps); + + // 执行工作流 + WorkflowRequest request = WorkflowRequest.builder() + .question("智能家居控制系统,通过手机App控制家中设备") + .build(); + + WorkflowResponse response = parallelWorkflow.execute(request); + + // 验证响应 + assertNotNull(response); + assertTrue(response.isSuccess()); + assertNotNull(response.getContent()); + } +} \ No newline at end of file diff --git a/spring-ai-agent/spring-ai-workflow/src/test/java/com/glmapper/ai/workflow/RoutingWorkflowTest.java b/spring-ai-agent/spring-ai-workflow/src/test/java/com/glmapper/ai/workflow/RoutingWorkflowTest.java new file mode 100644 index 0000000..f10dadb --- /dev/null +++ b/spring-ai-agent/spring-ai-workflow/src/test/java/com/glmapper/ai/workflow/RoutingWorkflowTest.java @@ -0,0 +1,82 @@ +package com.glmapper.ai.workflow; + +import com.glmapper.ai.workflow.model.WorkflowRequest; +import com.glmapper.ai.workflow.model.WorkflowResponse; +import com.glmapper.ai.workflow.core.step.WorkflowStep; +import com.glmapper.ai.workflow.core.WorkflowFactory; +import com.glmapper.ai.workflow.core.WorkflowStepFactory; +import com.glmapper.ai.workflow.core.workflow.Workflow; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + + +@SpringBootTest +public class RoutingWorkflowTest { + + @Autowired + private WorkflowStepFactory workflowStepFactory; + + @Autowired + private WorkflowFactory workflowFactory; + + @Test + @DisplayName("演示传统路由工作流基本用法 - 使用函数进行路由") + void demonstrateLegacyRoutingWorkflowUsage() { + + // 创建各种专门的处理步骤,这里主要采用mock数据 + Map stepMap = new HashMap<>(); + + // 账单查询处理步骤 + stepMap.put("billing", workflowStepFactory.createAiStep( + "账单查询处理", + "如果用户问你关于账单的问题,你就回答:存在两个账单。其他内容不需要回答。" + )); + + // 技术支持步骤 + stepMap.put("technical", workflowStepFactory.createAiStep( + "技术支持处理", + "如果用户问你有关技术支持的问题,你就回答:电脑该重启了。其他内容不需要回答。" + )); + + // 产品信息步骤 + stepMap.put("product", workflowStepFactory.createAiStep( + "产品信息处理", + "如果用户问你关于产品信息的问题,你就回答:这个产品还不错。其他内容不需要回答。" + )); + + // 订单状态步骤 + stepMap.put("order", workflowStepFactory.createAiStep( + "订单状态处理", + "如果用户问你关于订单状态的问题,你就回答:没有订单。其他内容不需要回答。" + )); + + // 通用查询步骤 + stepMap.put("general", workflowStepFactory.createAiStep( + "通用查询处理", + "如果用户问你通用的问题,你就回答:通用问题请查询搜索引擎。其他内容不需要回答。" + )); + + // 创建路由工作流 + Workflow routingWorkflow = workflowFactory.createRoutingWorkflow(stepMap); + + // 执行工作流 + WorkflowRequest request = WorkflowRequest.builder() + .question("我想查看我的账单详情") + .build(); + + WorkflowResponse response = routingWorkflow.execute(request); + + // 验证响应 + assertNotNull(response); + assertTrue(response.isSuccess()); + assertNotNull(response.getContent()); + } +} \ No newline at end of file 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 diff --git a/spring-ai-chat-memory/spring-ai-chat-memory-local/src/main/java/com/glmapper/ai/chat/memory/local/controller/ChatMemoryController.java b/spring-ai-chat-memory/spring-ai-chat-memory-local/src/main/java/com/glmapper/ai/chat/memory/local/controller/ChatMemoryController.java new file mode 100644 index 0000000..444d5e2 --- /dev/null +++ b/spring-ai-chat-memory/spring-ai-chat-memory-local/src/main/java/com/glmapper/ai/chat/memory/local/controller/ChatMemoryController.java @@ -0,0 +1,32 @@ +package com.glmapper.ai.chat.memory.local.controller; + +import com.glmapper.ai.chat.memory.local.service.ChatMemoryService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * @Classname TestController + * @Description + *

+ * # 你好,我是glmapper + * 1、curl -X GET http://localhost:8083/api/test/chat?message=%E4%BD%A0%E5%A5%BD%EF%BC%8C%E6%88%91%E6%98%AFglmapper&conversationId=test-1101 + * # 我是谁? + * 2、curl -X GET http://localhost:8083/api/test/chat?message=%E6%88%91%E6%98%AF%E8%B0%81%EF%BC%9F&conversationId=test-1101 + * + *

+ * @Date 2025/6/14 17:36 + * @Created by glmapper + */ +@RestController +@RequestMapping("/api/test") +public class ChatMemoryController { + + @Autowired + private ChatMemoryService chatMemoryService; + + @RequestMapping("chat") + public String test(String message, String conversationId) { + return this.chatMemoryService.chat(message, conversationId); + } +} diff --git a/spring-ai-chat/spring-ai-chat-qwen/src/main/resources/application.properties b/spring-ai-chat/spring-ai-chat-qwen/src/main/resources/application.properties index 777ea89..6adf540 100644 --- a/spring-ai-chat/spring-ai-chat-qwen/src/main/resources/application.properties +++ b/spring-ai-chat/spring-ai-chat-qwen/src/main/resources/application.properties @@ -4,7 +4,7 @@ server.port=8085 spring.profiles.active=qwen # qwen model -spring.ai.openai.chat.api-key=${spring.ai.openai.api-key} +spring.ai.openai.api-key=${spring.ai.openai.api-key} spring.ai.openai.chat.base-url=https://dashscope.aliyuncs.com/compatible-mode spring.ai.openai.chat.completions-path=/v1/chat/completions spring.ai.openai.chat.options.model=qwen-plus \ No newline at end of file diff --git a/spring-ai-evaluation/README.md b/spring-ai-evaluation/README.md new file mode 100644 index 0000000..663f7c5 --- /dev/null +++ b/spring-ai-evaluation/README.md @@ -0,0 +1,102 @@ +## 模型评估 +在测试 AI 应用时,需要对生成的内容进行评估,以避免模型输出“幻觉”信息(即虚构、不准确的内容)。一种常见的评估方式是直接借助 AI 模型本身来判断答案的质量。此时应选用一个更适合评估任务的模型,它未必和用于生成答案的模型相同。在 Spring AI 中,专门用于结果评估的接口是 `Evaluator`,它的定义如下: + +```java +@FunctionalInterface +public interface Evaluator { + EvaluationResponse evaluate(EvaluationRequest evaluationRequest); +} +``` + +评估时传入的是一个特定的请求类型 EvaluationRequest,它包括几个关键的属性,如下: + +```java +public class EvaluationRequest { + // 用户输入的原始问题 + private final String userText; + // 上下文信息,可以是文档,历史对话等等 + private final List dataList; + // 模型针对用户问题返回的结果 + private final String responseContent; + + public EvaluationRequest(String userText, List dataList, String responseContent) { + this.userText = userText; + this.dataList = dataList; + this.responseContent = responseContent; + } + ... +} +``` + + + +# 关联评估 +`RelevancyEvaluator` 是对 `Evaluator` 接口的具体实现,主要用于判断 AI 所生成的回答是否与检索到的上下文信息相关联。这项评估机制常被用来检测 RAG(检索增强生成)流程中的响应质量,确保模型的回复能贴合用户提问及相关内容。评估过程依赖三个要素:**用户输入、AI 模型的回答**,**以及检索得到的上下文内容**。系统会通过提示词模板向 AI 发出提问,然后判断模型回答是否具有上下文相关性。下面是 `RelevancyEvaluator` 默认使用的提示模板内容: + +```java +Your task is to evaluate if the response for the query +is in line with the context information provided. + +You have two options to answer. Either YES or NO. + +Answer YES, if the response for the query +is in line with context information otherwise NO. + +Query: +{query} + +Response: +{response} + +Context: +{context} + +Answer: +``` + + + +## 案例介绍 +这里我们使用 deepseek 来评估豆包的输出结果,关于如何在项目中使用多个 chatmodel 可以参考 chat client api 部分。 + +```java +/** + * 评估消息内容 + * + * @param message + * @return + */ +public EvaluationResponse evaluate(String message) { + // 使用 OpenAI 模型进行评估 + String openAiResponse = openAiChatClient.prompt().user(message).call().content(); + String question = message; + EvaluationRequest evaluationRequest = new EvaluationRequest( + // The original user question + question, + // context data + Collections.emptyList(), + // The AI model's response + openAiResponse); + RelevancyEvaluator evaluator = new RelevancyEvaluator(ChatClient.builder(this.deepSeekChatModel)); + EvaluationResponse evaluationResponse = evaluator.evaluate(evaluationRequest); + return evaluationResponse; +} +``` + + + +这里主要看下 EvaluationResponse 的几个属性 + +```java +// 表示是否通过评估 +private final boolean pass; +// 评估打分 0~1 +private final float score; +// 评估反馈 +private final String feedback; +// 一些元数据信息 +private final Map metadata; +``` + +评估结果主要是针对 pass 和 score,如果对于结果要求不是很高,直接关注 pass 即可;如果对结果有求较高,需要结合评估打分来控制。 + diff --git a/spring-ai-mcp/mcp-client/pom.xml b/spring-ai-mcp/mcp-client/pom.xml index f39a9e6..c3ad84e 100644 --- a/spring-ai-mcp/mcp-client/pom.xml +++ b/spring-ai-mcp/mcp-client/pom.xml @@ -19,6 +19,11 @@ httpclient5 5.4.3 + + org.apache.httpcomponents.core5 + httpcore5 + 5.3.4 + org.springframework.ai diff --git a/spring-ai-mcp/mcp-client/src/main/resources/application.yaml b/spring-ai-mcp/mcp-client/src/main/resources/application.yaml index 801598b..93df710 100644 --- a/spring-ai-mcp/mcp-client/src/main/resources/application.yaml +++ b/spring-ai-mcp/mcp-client/src/main/resources/application.yaml @@ -6,11 +6,14 @@ spring: name: mcp-client profiles: active: mcp-client - ai: - openai: - api-key: sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx - chat: - base-url: https://api.deepseek.com - completions-path: /v1/chat/completions - options: - model: deepseek-chat + ai: + openai: + api-key: ${spring.ai.openai.api-key} + chat: +# base-url: https://api.deepseek.com + base-url: https://dashscope.aliyuncs.com/compatible-mode +# completions-path: /v1/chat/completions + completions-path: /v1/chat/completions + options: +# model: deepseek-chat + model: qwen-plus \ No newline at end of file diff --git a/spring-ai-observability/pom.xml b/spring-ai-observability/pom.xml new file mode 100644 index 0000000..dc845ea --- /dev/null +++ b/spring-ai-observability/pom.xml @@ -0,0 +1,35 @@ + + + 4.0.0 + + com.glmapper + spring-ai-summary + 0.0.1 + + + spring-ai-observability + pom + spring-ai-observability + + spring-ai-observability-metric + spring-ai-observability-tracing + + + + + + org.springframework.ai + spring-ai-starter-model-openai + + + + + org.springframework.ai + spring-ai-starter-vector-store-redis + + + + + \ No newline at end of file diff --git a/spring-ai-observability/spring-ai-observability-metric/README.md b/spring-ai-observability/spring-ai-observability-metric/README.md new file mode 100644 index 0000000..e586046 --- /dev/null +++ b/spring-ai-observability/spring-ai-observability-metric/README.md @@ -0,0 +1,125 @@ +# Spring AI Observability Metric + +该项目是基于 [Spring AI](https://docs.spring.io/spring-ai/) 构建的可观察性追踪示例,集成了 **OpenAI 客户端**(兼容阿里云 DashScope)、**工具调用(Function/Method)** 和 **向量存储(Redis VectorStore)** 功能。 + + +通过本项目可以快速了解如何在 Spring Boot 应用中实现对 AI 模型调用的监控、追踪和可观测性管理。 + + +## 模块结构 + +``` +spring-ai-observability-metric +├── src/ +│ ├── main/ +│ │ ├── java/com/glmapper/ai/observability/tracing/ +│ │ │ ├── controller/ # 各类 REST 接口(Chat, Image, Embedding, Tools) +│ │ │ ├── tools/ # 工具函数(天气查询、时间获取) +│ │ │ ├── storage/ # Redis VectorStore 存储封装 +│ │ │ └── configs/ # 配置类 +│ │ └── resources/ +│ │ └── application.yml # 配置文件 +├── pom.xml # Maven 项目配置 +└── README.md # 当前文档 +``` + +## 🧩 核心功能 + +| 模块 | 功能描述 | +|------|----------| +| ChatController | 调用 OpenAI 兼容接口进行聊天对话 | +| ImageController | 调用图像生成 API(如 Stable Diffusion) | +| EmbeddingController | 使用文本嵌入模型生成向量表示 | +| ToolCallingController | 支持 Function 和 Method 类型工具调用(天气、当前时间) | +| VectorStoreController | 使用 Redis 作为向量数据库进行文档存储与搜索 | +| Zipkin 集成 | 所有请求链路信息上报至 Zipkin,支持全链路追踪 | + +--- + +## 🛠️ 技术栈 + +- Spring Boot 3.x +- Spring AI 1.0.x +- Redis VectorStore +- OpenAI Client(兼容 DashScope) +- Micrometer Tracing + Brave +- Zipkin 分布式追踪 + +--- + +## 🚀 快速启动 +### 1. 修改配置 + +在 [application.yml](src/main/resources) 中更新以下参数: + +```yaml +spring: + ai: + openai: + api-key: your-api-key-here +``` + +### 2. 启动应用 + +```bash +mvn spring-boot:run +``` + +或构建后运行: + +```bash +mvn clean package +java -jar target/spring-ai-observability-tracing-*.jar +``` + +--- + +## 🌐 接口列表 + +| 接口路径 | 描述 | +|---------------------------------------|------------------------| +| /observability/chat?message=xxx | 发送聊天消息给 AI 模型 | +| /observability/image | 生成一张图片 | +| /observability/embedding | 获取 "hello world" 的文本嵌入 | +| /observability/embedding/generic | 使用指定模型获取嵌入 | +| /observability/tools/function | 调用 Function 工具(天气) | +| /observability/tools/method | 调用 Method 工具(当前时间) | +| /observability/vector/store?text=xxx | 存储文档到 Redis 向量库 | +| /observability/vector/search?text=xxx | 在向量库中搜索相似内容 | +| /observability/vector/delete?id=xxx | 删除向量库中指定id内容 | + +--- + +## 📊观测指标查看 + +项目使用 Micrometer 和 Spring Boot Actuator 来暴露和管理指标数据。可以通过以下步骤查看观测指标: + +1. **启动应用**:确保应用已经成功启动。 + +2. **访问指标端点**:通过浏览器或 HTTP 客户端访问 `/actuator/metrics` 端点。 + + ```bash + curl http://localhost:8087/actuator/metrics + ``` + +3. **查看具体指标**:可以通过指定指标名称来查看具体的指标详情。 + + ```bash + curl http://localhost:8087/actuator/metrics/[metricName] + ``` + + 其中 `[metricName]` 是你想查看的具体指标名称。 + +4. **Prometheus 格式**:如果需要以 Prometheus 格式查看指标,可以访问 `/actuator/prometheus` 端点。 + + ```bash + curl http://localhost:8087/actuator/prometheus + ``` + +这些指标包括但不限于 JVM 指标、系统指标、HTTP 请求统计等。通过这些指标,可以更好地监控和诊断应用程序的运行状态。 + +## 📝 注意事项 + +- 确保已安装并运行 Redis。 +- 如需部署到生产环境,请关闭 debug 日志并调整采样率(`management.tracing.sampling.probability`)。 +- 若更换为其他 OpenAI 兼容平台,请修改对应的 `base-url` 和 `api-key`。 diff --git a/spring-ai-observability/spring-ai-observability-metric/pom.xml b/spring-ai-observability/spring-ai-observability-metric/pom.xml new file mode 100644 index 0000000..87e6a12 --- /dev/null +++ b/spring-ai-observability/spring-ai-observability-metric/pom.xml @@ -0,0 +1,16 @@ + + + 4.0.0 + + com.glmapper + spring-ai-observability + 0.0.1 + + + com.glmapper.ai.observability.metric + spring-ai-observability-metric + + + \ No newline at end of file diff --git a/spring-ai-observability/spring-ai-observability-metric/request.http b/spring-ai-observability/spring-ai-observability-metric/request.http new file mode 100644 index 0000000..cbb8435 --- /dev/null +++ b/spring-ai-observability/spring-ai-observability-metric/request.http @@ -0,0 +1,26 @@ +### chat +GET http://localhost:8087/observability/chat + +### embedding +GET http://localhost:8087/observability/embedding + +### embedding generic +GET http://localhost:8087/observability/embedding/generic + +### image +GET http://localhost:8087/observability/image + +### tools function +GET http://localhost:8087/observability/tools/function + +### tools method +GET http://localhost:8087/observability/tools/method + +### vector store +GET http://localhost:8087/observability/vector/store?text= + +### vector search +GET http://localhost:8087/observability/vector/search?text= + +### vector delete +GET http://localhost:8087/observability/vector/delete?id= diff --git a/spring-ai-observability/spring-ai-observability-metric/src/main/java/com/glmapper/ai/observability/metric/ObservabilityMetricApplication.java b/spring-ai-observability/spring-ai-observability-metric/src/main/java/com/glmapper/ai/observability/metric/ObservabilityMetricApplication.java new file mode 100644 index 0000000..2f2c25c --- /dev/null +++ b/spring-ai-observability/spring-ai-observability-metric/src/main/java/com/glmapper/ai/observability/metric/ObservabilityMetricApplication.java @@ -0,0 +1,14 @@ +package com.glmapper.ai.observability.metric; + + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class ObservabilityMetricApplication { + + public static void main(String[] args) { + SpringApplication.run(ObservabilityMetricApplication.class, args); + } + +} \ No newline at end of file diff --git a/spring-ai-observability/spring-ai-observability-metric/src/main/java/com/glmapper/ai/observability/metric/configs/WeatherToolsConfigs.java b/spring-ai-observability/spring-ai-observability-metric/src/main/java/com/glmapper/ai/observability/metric/configs/WeatherToolsConfigs.java new file mode 100644 index 0000000..977d9e4 --- /dev/null +++ b/spring-ai-observability/spring-ai-observability-metric/src/main/java/com/glmapper/ai/observability/metric/configs/WeatherToolsConfigs.java @@ -0,0 +1,25 @@ +package com.glmapper.ai.observability.metric.configs; + +import com.glmapper.ai.observability.metric.tools.function.WeatherRequest; +import com.glmapper.ai.observability.metric.tools.function.WeatherResponse; +import com.glmapper.ai.observability.metric.tools.function.WeatherService; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Description; + +import java.util.function.Function; + + +@Configuration(proxyBeanMethods = false) +public class WeatherToolsConfigs { + public static final String CURRENT_WEATHER_TOOL = "currentWeather"; + + WeatherService weatherService = new WeatherService(); + + @Bean(CURRENT_WEATHER_TOOL) + @Description("Get the weather in location") + Function currentWeather() { + return weatherService; + } + +} diff --git a/spring-ai-observability/spring-ai-observability-metric/src/main/java/com/glmapper/ai/observability/metric/controller/ChatController.java b/spring-ai-observability/spring-ai-observability-metric/src/main/java/com/glmapper/ai/observability/metric/controller/ChatController.java new file mode 100644 index 0000000..4896aa4 --- /dev/null +++ b/spring-ai-observability/spring-ai-observability-metric/src/main/java/com/glmapper/ai/observability/metric/controller/ChatController.java @@ -0,0 +1,31 @@ +package com.glmapper.ai.observability.metric.controller; + +import org.springframework.ai.chat.client.ChatClient; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/observability/chat") +public class ChatController { + + private final ChatClient chatClient; + + public ChatController(ChatClient.Builder chatClientBuilder) { + this.chatClient = chatClientBuilder.build(); + } + + @GetMapping + public String chat(@RequestParam String message) { + // 自动记录 spring.ai.chat.client 观测数据 + return chatClient.prompt() + .user(message) + .call() + .content(); + } + + + + +} diff --git a/spring-ai-observability/spring-ai-observability-metric/src/main/java/com/glmapper/ai/observability/metric/controller/EmbeddingController.java b/spring-ai-observability/spring-ai-observability-metric/src/main/java/com/glmapper/ai/observability/metric/controller/EmbeddingController.java new file mode 100644 index 0000000..2bb21ad --- /dev/null +++ b/spring-ai-observability/spring-ai-observability-metric/src/main/java/com/glmapper/ai/observability/metric/controller/EmbeddingController.java @@ -0,0 +1,39 @@ +package com.glmapper.ai.observability.metric.controller; + + +import org.springframework.ai.embedding.EmbeddingModel; +import org.springframework.ai.embedding.EmbeddingRequest; +import org.springframework.ai.openai.OpenAiEmbeddingOptions; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@RestController +@RequestMapping("/observability/embedding") +public class EmbeddingController { + + private final EmbeddingModel embeddingModel; + + public EmbeddingController(EmbeddingModel embeddingModel) { + this.embeddingModel = embeddingModel; + } + + @GetMapping + public String embedding() { + var embeddings = embeddingModel.embed("hello world."); + return "embedding vector size:" + embeddings.length; + } + + @GetMapping("/generic") + public String embeddingGenericOpts() { + + var embeddings = embeddingModel.call(new EmbeddingRequest( + List.of("hello world."), + OpenAiEmbeddingOptions.builder().model("text-embedding-v4").build()) + ).getResult().getOutput(); + return "embedding vector size:" + embeddings.length; + } + +} diff --git a/spring-ai-observability/spring-ai-observability-metric/src/main/java/com/glmapper/ai/observability/metric/controller/ImageController.java b/spring-ai-observability/spring-ai-observability-metric/src/main/java/com/glmapper/ai/observability/metric/controller/ImageController.java new file mode 100644 index 0000000..badbe7f --- /dev/null +++ b/spring-ai-observability/spring-ai-observability-metric/src/main/java/com/glmapper/ai/observability/metric/controller/ImageController.java @@ -0,0 +1,36 @@ +package com.glmapper.ai.observability.metric.controller; + +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.ai.image.ImageModel; +import org.springframework.ai.image.ImagePrompt; +import org.springframework.ai.image.ImageResponse; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.net.URL; + +@RestController +@RequestMapping("/observability/image") +public class ImageController { + + private final ImageModel imageModel; + + private static final String DEFAULT_PROMPT = "为人工智能生成一张富有科技感的图片!"; + + public ImageController(ImageModel imageModel) { + this.imageModel = imageModel; + } + + @GetMapping + public String image() { + + ImageResponse imageResponse = imageModel.call(new ImagePrompt(DEFAULT_PROMPT)); + + return imageResponse.getResult().getOutput().getUrl(); + } +} diff --git a/spring-ai-observability/spring-ai-observability-metric/src/main/java/com/glmapper/ai/observability/metric/controller/ToolCallingController.java b/spring-ai-observability/spring-ai-observability-metric/src/main/java/com/glmapper/ai/observability/metric/controller/ToolCallingController.java new file mode 100644 index 0000000..dafc0ca --- /dev/null +++ b/spring-ai-observability/spring-ai-observability-metric/src/main/java/com/glmapper/ai/observability/metric/controller/ToolCallingController.java @@ -0,0 +1,49 @@ +package com.glmapper.ai.observability.metric.controller; + +import com.glmapper.ai.observability.metric.tools.function.WeatherRequest; +import com.glmapper.ai.observability.metric.tools.function.WeatherService; +import com.glmapper.ai.observability.metric.tools.methods.DateTimeTools; +import org.springframework.ai.chat.client.ChatClient; +import org.springframework.ai.tool.ToolCallback; +import org.springframework.ai.tool.function.FunctionToolCallback; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + + +@RestController +@RequestMapping("/observability/tools") +public class ToolCallingController { + + public final ChatClient chatClient; + + public ToolCallingController(ChatClient.Builder builder) { + this.chatClient = builder.build(); + } + + @GetMapping("/function") + public String functionTools() { + ToolCallback toolCallback = FunctionToolCallback + .builder("currentWeather", new WeatherService()) + .description("Get the weather in location") + .inputType(WeatherRequest.class) + .build(); + + return chatClient + .prompt("上海的天气怎么样?") + .toolCallbacks(toolCallback) + .call() + .content(); + } + + // 记录 tools 观测数据 + @GetMapping("/method") + public String methodTools() { + return chatClient + .prompt("今天是几号?") + .tools(new DateTimeTools()) + .call() + .content(); + } + +} diff --git a/spring-ai-observability/spring-ai-observability-metric/src/main/java/com/glmapper/ai/observability/metric/controller/VectorStoreController.java b/spring-ai-observability/spring-ai-observability-metric/src/main/java/com/glmapper/ai/observability/metric/controller/VectorStoreController.java new file mode 100644 index 0000000..a22e57d --- /dev/null +++ b/spring-ai-observability/spring-ai-observability-metric/src/main/java/com/glmapper/ai/observability/metric/controller/VectorStoreController.java @@ -0,0 +1,41 @@ +package com.glmapper.ai.observability.metric.controller; + + +import com.glmapper.ai.observability.metric.storage.VectorStoreStorage; +import org.springframework.ai.document.Document; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +@RestController +@RequestMapping("/observability/vector") +public class VectorStoreController { + + @Autowired() + private VectorStoreStorage vectorStoreStorage; + + @GetMapping("/store") + public String embedding(@RequestParam String text) { + Document document = new Document(text, Map.of("test-data", "true")); + List documents = List.of(document); + vectorStoreStorage.store(documents); + return document.getId(); + } + + @GetMapping("/search") + public List search(@RequestParam String text) { + return vectorStoreStorage.search(text); + } + + @GetMapping("/delete") + public void delete(@RequestParam String id) { + vectorStoreStorage.delete(Set.of(id)); + } + +} diff --git a/spring-ai-observability/spring-ai-observability-metric/src/main/java/com/glmapper/ai/observability/metric/storage/VectorStoreStorage.java b/spring-ai-observability/spring-ai-observability-metric/src/main/java/com/glmapper/ai/observability/metric/storage/VectorStoreStorage.java new file mode 100644 index 0000000..7de3141 --- /dev/null +++ b/spring-ai-observability/spring-ai-observability-metric/src/main/java/com/glmapper/ai/observability/metric/storage/VectorStoreStorage.java @@ -0,0 +1,39 @@ +package com.glmapper.ai.observability.metric.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; + + +@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-observability/spring-ai-observability-metric/src/main/java/com/glmapper/ai/observability/metric/tools/function/Unit.java b/spring-ai-observability/spring-ai-observability-metric/src/main/java/com/glmapper/ai/observability/metric/tools/function/Unit.java new file mode 100644 index 0000000..101a032 --- /dev/null +++ b/spring-ai-observability/spring-ai-observability-metric/src/main/java/com/glmapper/ai/observability/metric/tools/function/Unit.java @@ -0,0 +1,11 @@ +package com.glmapper.ai.observability.metric.tools.function; + +/** + * @Classname Unit + * @Description TODO + * @Date 2025/5/29 17:07 + * @Created by glmapper + */ +public enum Unit { + C, F +} diff --git a/spring-ai-observability/spring-ai-observability-metric/src/main/java/com/glmapper/ai/observability/metric/tools/function/WeatherRequest.java b/spring-ai-observability/spring-ai-observability-metric/src/main/java/com/glmapper/ai/observability/metric/tools/function/WeatherRequest.java new file mode 100644 index 0000000..f40c47e --- /dev/null +++ b/spring-ai-observability/spring-ai-observability-metric/src/main/java/com/glmapper/ai/observability/metric/tools/function/WeatherRequest.java @@ -0,0 +1,10 @@ +package com.glmapper.ai.observability.metric.tools.function; + + +/** + * @Classname WeatherRequest + * @Description WeatherRequest + */ +public record WeatherRequest(String location, Unit unit) { + +} diff --git a/spring-ai-observability/spring-ai-observability-metric/src/main/java/com/glmapper/ai/observability/metric/tools/function/WeatherResponse.java b/spring-ai-observability/spring-ai-observability-metric/src/main/java/com/glmapper/ai/observability/metric/tools/function/WeatherResponse.java new file mode 100644 index 0000000..a409d4a --- /dev/null +++ b/spring-ai-observability/spring-ai-observability-metric/src/main/java/com/glmapper/ai/observability/metric/tools/function/WeatherResponse.java @@ -0,0 +1,10 @@ +package com.glmapper.ai.observability.metric.tools.function; + + + +/** + * @Classname WeatherResponse + * @Description WeatherResponse + */ +public record WeatherResponse(double temp, Unit unit) { +} diff --git a/spring-ai-observability/spring-ai-observability-metric/src/main/java/com/glmapper/ai/observability/metric/tools/function/WeatherService.java b/spring-ai-observability/spring-ai-observability-metric/src/main/java/com/glmapper/ai/observability/metric/tools/function/WeatherService.java new file mode 100644 index 0000000..fd487d5 --- /dev/null +++ b/spring-ai-observability/spring-ai-observability-metric/src/main/java/com/glmapper/ai/observability/metric/tools/function/WeatherService.java @@ -0,0 +1,15 @@ +package com.glmapper.ai.observability.metric.tools.function; + +import java.util.function.Function; + + +/** + * @Classname WeatherService + * @Description WeatherService + */ +public class WeatherService implements Function { + + public WeatherResponse apply(WeatherRequest request) { + return new WeatherResponse(30.0, Unit.C); + } +} diff --git a/spring-ai-observability/spring-ai-observability-metric/src/main/java/com/glmapper/ai/observability/metric/tools/methods/DateTimeTools.java b/spring-ai-observability/spring-ai-observability-metric/src/main/java/com/glmapper/ai/observability/metric/tools/methods/DateTimeTools.java new file mode 100644 index 0000000..c7d0105 --- /dev/null +++ b/spring-ai-observability/spring-ai-observability-metric/src/main/java/com/glmapper/ai/observability/metric/tools/methods/DateTimeTools.java @@ -0,0 +1,24 @@ +package com.glmapper.ai.observability.metric.tools.methods; + +import org.springframework.ai.chat.model.ToolContext; +import org.springframework.ai.tool.annotation.Tool; +import org.springframework.context.i18n.LocaleContextHolder; + +import java.text.SimpleDateFormat; +import java.time.LocalDateTime; + + +public class DateTimeTools { + + @Tool(description = "Get the current date and time in the user's timezone") + public String getCurrentDateTime() { + return LocalDateTime.now().atZone(LocaleContextHolder.getTimeZone().toZoneId()).toString(); + } + + + @Tool(description = "Get the current date and time in the user's timezone") + public String getFormatDateTime(ToolContext toolContext) { + return new SimpleDateFormat(toolContext.getContext().get("format").toString()) + .format(LocalDateTime.now().atZone(LocaleContextHolder.getTimeZone().toZoneId()).toInstant().toEpochMilli()); + } +} diff --git a/spring-ai-observability/spring-ai-observability-metric/src/main/resources/application.yml b/spring-ai-observability/spring-ai-observability-metric/src/main/resources/application.yml new file mode 100644 index 0000000..ef2fa65 --- /dev/null +++ b/spring-ai-observability/spring-ai-observability-metric/src/main/resources/application.yml @@ -0,0 +1,75 @@ +spring: + application: + name: spring-ai-observability + data: + redis: + host: ${REDIS_HOST:localhost} + port: ${REDIS_PORT:6379} + username: ${REDIS_USERNAME:} + password: ${REDIS_PASSWORD:} + ai: + openai: + api-key: ${spring.ai.openai.api-key} + chat: + base-url: https://dashscope.aliyuncs.com/compatible-mode + options: + model: qwen-plus + image: + api-key: ${spring.ai.openai.api-key} + base-url: https://dashscope.aliyuncs.com/api + options: + model: stable-diffusion-xl + images-path: /v1/services/aigc/text2image/image-synthesis + 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_ + observations: + # 记录向量存储查询响应内容 + log-query-response: true + + chat: + client: + observations: + # 是否记录聊天客户端提示内容 + log-prompt: true + observations: + # 记录提示内容 + log-prompt: true + # 记录完成内容 + log-completion: true + # 在观察中包含错误日志 + include-error-logging: true + image: + observations: + # 记录提示内容 + log-prompt: true + + +management: + endpoints: + web: + exposure: + include: "*" + endpoint: + health: + show-details: always + prometheus: + metrics: + export: + enabled: true + +server: + port: 8087 + servlet: + encoding: + charset: utf-8 + enabled: true + force: true \ No newline at end of file diff --git a/spring-ai-observability/spring-ai-observability-tracing/README.md b/spring-ai-observability/spring-ai-observability-tracing/README.md new file mode 100644 index 0000000..1afd53a --- /dev/null +++ b/spring-ai-observability/spring-ai-observability-tracing/README.md @@ -0,0 +1,119 @@ +# Spring AI Observability Tracing + +该项目是基于 [Spring AI](https://docs.spring.io/spring-ai/) 构建的可观察性追踪示例,集成了 **OpenAI 客户端**(兼容阿里云 DashScope)、**工具调用(Function/Method)**、**向量存储(Redis VectorStore)** 和 **分布式追踪(Zipkin)** 功能。 + + +通过本项目可以快速了解如何在 Spring Boot 应用中实现对 AI 模型调用的监控、追踪和可观测性管理。 + +--- + +## 📦 项目结构 + +``` +spring-ai-observability-tracing +├── src/ +│ ├── main/ +│ │ ├── java/com/glmapper/ai/observability/tracing/ +│ │ │ ├── controller/ # 各类 REST 接口(Chat, Image, Embedding, Tools) +│ │ │ ├── tools/ # 工具函数(天气查询、时间获取) +│ │ │ ├── storage/ # Redis VectorStore 存储封装 +│ │ │ └── configs/ # 配置类 +│ │ └── resources/ +│ │ └── application.yml # 配置文件 +│ └── ... +├── docker-compose.yaml # Zipkin 服务定义 +├── pom.xml # Maven 项目配置 +└── README.md # 当前文档 +``` + +--- + +## 🧩 核心功能 + +| 模块 | 功能描述 | +|------|----------| +| ChatController | 调用 OpenAI 兼容接口进行聊天对话 | +| ImageController | 调用图像生成 API(如 Stable Diffusion) | +| EmbeddingController | 使用文本嵌入模型生成向量表示 | +| ToolCallingController | 支持 Function 和 Method 类型工具调用(天气、当前时间) | +| VectorStoreController | 使用 Redis 作为向量数据库进行文档存储与搜索 | +| Zipkin 集成 | 所有请求链路信息上报至 Zipkin,支持全链路追踪 | + +--- + +## 🛠️ 技术栈 + +- Spring Boot 3.x +- Spring AI 1.0.x +- Redis VectorStore +- OpenAI Client(兼容 DashScope) +- Micrometer Tracing + Brave +- Zipkin 分布式追踪 + +--- + +## 🚀 快速启动 + +### 1. 启动 Zipkin(使用 Docker) + +```bash +docker-compose up -d +``` + +访问:[http://localhost:9411](http://localhost:9411) + +### 2. 修改配置 + +在 [application.yml](src/main/resources) 中更新以下参数: + +```yaml +spring: + ai: + openai: + api-key: your-api-key-here +``` + +### 3. 启动应用 + +```bash +mvn spring-boot:run +``` + +或构建后运行: + +```bash +mvn clean package +java -jar target/spring-ai-observability-tracing-*.jar +``` + +--- + +## 🌐 接口列表 + +| 接口路径 | 描述 | +|---------------------------------------|------------------------| +| /observability/chat?message=xxx | 发送聊天消息给 AI 模型 | +| /observability/image | 生成一张图片 | +| /observability/embedding | 获取 "hello world" 的文本嵌入 | +| /observability/embedding/generic | 使用指定模型获取嵌入 | +| /observability/tools/function | 调用 Function 工具(天气) | +| /observability/tools/method | 调用 Method 工具(当前时间) | +| /observability/vector/store?text=xxx | 存储文档到 Redis 向量库 | +| /observability/vector/search?text=xxx | 在向量库中搜索相似内容 | +| /observability/vector/delete?id=xxx | 删除向量库中指定id内容 | + +--- + +## 📊 监控指标 + +- Prometheus 指标地址:\`http://localhost:8088/actuator/prometheus\` +- 健康检查:\`http://localhost:8088/actuator/health\` +- 所有链路数据会自动上报至 Zipkin + +--- + +## 📝 注意事项 + +- 确保已安装并运行 Redis。 +- 如需部署到生产环境,请关闭 debug 日志并调整采样率(`management.tracing.sampling.probability`)。 +- 若更换为其他 OpenAI 兼容平台,请修改对应的 `base-url` 和 `api-key`。 diff --git a/spring-ai-observability/spring-ai-observability-tracing/docker-compose.yaml b/spring-ai-observability/spring-ai-observability-tracing/docker-compose.yaml new file mode 100644 index 0000000..ff23f8a --- /dev/null +++ b/spring-ai-observability/spring-ai-observability-tracing/docker-compose.yaml @@ -0,0 +1,5 @@ +services: + zipkin: + image: 'openzipkin/zipkin:latest' + ports: + - '9411:9411' diff --git a/spring-ai-observability/spring-ai-observability-tracing/pom.xml b/spring-ai-observability/spring-ai-observability-tracing/pom.xml new file mode 100644 index 0000000..1659b90 --- /dev/null +++ b/spring-ai-observability/spring-ai-observability-tracing/pom.xml @@ -0,0 +1,28 @@ + + + 4.0.0 + + com.glmapper + spring-ai-observability + 0.0.1 + + + com.glmapper.ai.observability.tracing + spring-ai-observability-tracing + + + + + io.micrometer + micrometer-tracing-bridge-brave + + + + io.zipkin.reporter2 + zipkin-reporter-brave + + + + \ No newline at end of file diff --git a/spring-ai-observability/spring-ai-observability-tracing/request.http b/spring-ai-observability/spring-ai-observability-tracing/request.http new file mode 100644 index 0000000..1f357e1 --- /dev/null +++ b/spring-ai-observability/spring-ai-observability-tracing/request.http @@ -0,0 +1,26 @@ +### chat +GET http://localhost:8088/observability/chat + +### embedding +GET http://localhost:8088/observability/embedding + +### embedding generic +GET http://localhost:8088/observability/embedding/generic + +### image +GET http://localhost:8088/observability/image + +### tools function +GET http://localhost:8088/observability/tools/function + +### tools method +GET http://localhost:8088/observability/tools/method + +### vector store +GET http://localhost:8088/observability/vector/store?text= + +### vector search +GET http://localhost:8088/observability/vector/search?text= + +### vector delete +GET http://localhost:8088/observability/vector/delete?id= diff --git a/spring-ai-observability/spring-ai-observability-tracing/src/main/java/com/glmapper/ai/observability/tracing/ObservabilityTracingApplication.java b/spring-ai-observability/spring-ai-observability-tracing/src/main/java/com/glmapper/ai/observability/tracing/ObservabilityTracingApplication.java new file mode 100644 index 0000000..c8ba14b --- /dev/null +++ b/spring-ai-observability/spring-ai-observability-tracing/src/main/java/com/glmapper/ai/observability/tracing/ObservabilityTracingApplication.java @@ -0,0 +1,14 @@ +package com.glmapper.ai.observability.tracing; + + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class ObservabilityTracingApplication { + + public static void main(String[] args) { + SpringApplication.run(ObservabilityTracingApplication.class, args); + } + +} \ No newline at end of file diff --git a/spring-ai-observability/spring-ai-observability-tracing/src/main/java/com/glmapper/ai/observability/tracing/configs/WeatherToolsConfigs.java b/spring-ai-observability/spring-ai-observability-tracing/src/main/java/com/glmapper/ai/observability/tracing/configs/WeatherToolsConfigs.java new file mode 100644 index 0000000..ab87367 --- /dev/null +++ b/spring-ai-observability/spring-ai-observability-tracing/src/main/java/com/glmapper/ai/observability/tracing/configs/WeatherToolsConfigs.java @@ -0,0 +1,25 @@ +package com.glmapper.ai.observability.tracing.configs; + +import com.glmapper.ai.observability.tracing.tools.function.WeatherRequest; +import com.glmapper.ai.observability.tracing.tools.function.WeatherResponse; +import com.glmapper.ai.observability.tracing.tools.function.WeatherService; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Description; + +import java.util.function.Function; + + +@Configuration(proxyBeanMethods = false) +public class WeatherToolsConfigs { + public static final String CURRENT_WEATHER_TOOL = "currentWeather"; + + WeatherService weatherService = new WeatherService(); + + @Bean(CURRENT_WEATHER_TOOL) + @Description("Get the weather in location") + Function currentWeather() { + return weatherService; + } + +} diff --git a/spring-ai-observability/spring-ai-observability-tracing/src/main/java/com/glmapper/ai/observability/tracing/controller/ChatController.java b/spring-ai-observability/spring-ai-observability-tracing/src/main/java/com/glmapper/ai/observability/tracing/controller/ChatController.java new file mode 100644 index 0000000..bfa0070 --- /dev/null +++ b/spring-ai-observability/spring-ai-observability-tracing/src/main/java/com/glmapper/ai/observability/tracing/controller/ChatController.java @@ -0,0 +1,31 @@ +package com.glmapper.ai.observability.tracing.controller; + +import org.springframework.ai.chat.client.ChatClient; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/observability/chat") +public class ChatController { + + private final ChatClient chatClient; + + public ChatController(ChatClient.Builder chatClientBuilder) { + this.chatClient = chatClientBuilder.build(); + } + + @GetMapping + public String chat(@RequestParam String message) { + // 自动记录 spring.ai.chat.client 观测数据 + return chatClient.prompt() + .user(message) + .call() + .content(); + } + + + + +} diff --git a/spring-ai-observability/spring-ai-observability-tracing/src/main/java/com/glmapper/ai/observability/tracing/controller/EmbeddingController.java b/spring-ai-observability/spring-ai-observability-tracing/src/main/java/com/glmapper/ai/observability/tracing/controller/EmbeddingController.java new file mode 100644 index 0000000..53bce9e --- /dev/null +++ b/spring-ai-observability/spring-ai-observability-tracing/src/main/java/com/glmapper/ai/observability/tracing/controller/EmbeddingController.java @@ -0,0 +1,39 @@ +package com.glmapper.ai.observability.tracing.controller; + + +import org.springframework.ai.embedding.EmbeddingModel; +import org.springframework.ai.embedding.EmbeddingRequest; +import org.springframework.ai.openai.OpenAiEmbeddingOptions; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@RestController +@RequestMapping("/observability/embedding") +public class EmbeddingController { + + private final EmbeddingModel embeddingModel; + + public EmbeddingController(EmbeddingModel embeddingModel) { + this.embeddingModel = embeddingModel; + } + + @GetMapping + public String embedding() { + var embeddings = embeddingModel.embed("hello world."); + return "embedding vector size:" + embeddings.length; + } + + @GetMapping("/generic") + public String embeddingGenericOpts() { + + var embeddings = embeddingModel.call(new EmbeddingRequest( + List.of("hello world."), + OpenAiEmbeddingOptions.builder().model("text-embedding-v4").build()) + ).getResult().getOutput(); + return "embedding vector size:" + embeddings.length; + } + +} diff --git a/spring-ai-observability/spring-ai-observability-tracing/src/main/java/com/glmapper/ai/observability/tracing/controller/ImageController.java b/spring-ai-observability/spring-ai-observability-tracing/src/main/java/com/glmapper/ai/observability/tracing/controller/ImageController.java new file mode 100644 index 0000000..9a91dc1 --- /dev/null +++ b/spring-ai-observability/spring-ai-observability-tracing/src/main/java/com/glmapper/ai/observability/tracing/controller/ImageController.java @@ -0,0 +1,29 @@ +package com.glmapper.ai.observability.tracing.controller; + +import org.springframework.ai.image.ImageModel; +import org.springframework.ai.image.ImagePrompt; +import org.springframework.ai.image.ImageResponse; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/observability/image") +public class ImageController { + + private final ImageModel imageModel; + + private static final String DEFAULT_PROMPT = "为人工智能生成一张富有科技感的图片!"; + + public ImageController(ImageModel imageModel) { + this.imageModel = imageModel; + } + + @GetMapping + public String image() { + + ImageResponse imageResponse = imageModel.call(new ImagePrompt(DEFAULT_PROMPT)); + + return imageResponse.getResult().getOutput().getUrl(); + } +} diff --git a/spring-ai-observability/spring-ai-observability-tracing/src/main/java/com/glmapper/ai/observability/tracing/controller/ToolCallingController.java b/spring-ai-observability/spring-ai-observability-tracing/src/main/java/com/glmapper/ai/observability/tracing/controller/ToolCallingController.java new file mode 100644 index 0000000..e4c3b61 --- /dev/null +++ b/spring-ai-observability/spring-ai-observability-tracing/src/main/java/com/glmapper/ai/observability/tracing/controller/ToolCallingController.java @@ -0,0 +1,49 @@ +package com.glmapper.ai.observability.tracing.controller; + +import com.glmapper.ai.observability.tracing.tools.function.WeatherRequest; +import com.glmapper.ai.observability.tracing.tools.function.WeatherService; +import com.glmapper.ai.observability.tracing.tools.methods.DateTimeTools; +import org.springframework.ai.chat.client.ChatClient; +import org.springframework.ai.tool.ToolCallback; +import org.springframework.ai.tool.function.FunctionToolCallback; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + + +@RestController +@RequestMapping("/observability/tools") +public class ToolCallingController { + + public final ChatClient chatClient; + + public ToolCallingController(ChatClient.Builder builder) { + this.chatClient = builder.build(); + } + + @GetMapping("/function") + public String functionTools() { + ToolCallback toolCallback = FunctionToolCallback + .builder("currentWeather", new WeatherService()) + .description("Get the weather in location") + .inputType(WeatherRequest.class) + .build(); + + return chatClient + .prompt("上海的天气怎么样?") + .toolCallbacks(toolCallback) + .call() + .content(); + } + + // 记录 tools 观测数据 + @GetMapping("/method") + public String methodTools() { + return chatClient + .prompt("今天是几号?") + .tools(new DateTimeTools()) + .call() + .content(); + } + +} diff --git a/spring-ai-observability/spring-ai-observability-tracing/src/main/java/com/glmapper/ai/observability/tracing/controller/VectorStoreController.java b/spring-ai-observability/spring-ai-observability-tracing/src/main/java/com/glmapper/ai/observability/tracing/controller/VectorStoreController.java new file mode 100644 index 0000000..a622c10 --- /dev/null +++ b/spring-ai-observability/spring-ai-observability-tracing/src/main/java/com/glmapper/ai/observability/tracing/controller/VectorStoreController.java @@ -0,0 +1,41 @@ +package com.glmapper.ai.observability.tracing.controller; + + +import com.glmapper.ai.observability.tracing.storage.VectorStoreStorage; +import org.springframework.ai.document.Document; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +@RestController +@RequestMapping("/observability/vector") +public class VectorStoreController { + + @Autowired() + private VectorStoreStorage vectorStoreStorage; + + @GetMapping("/store") + public String embedding(@RequestParam String text) { + Document document = new Document(text, Map.of("test-data", "true")); + List documents = List.of(document); + vectorStoreStorage.store(documents); + return document.getId(); + } + + @GetMapping("/search") + public List search(@RequestParam String text) { + return vectorStoreStorage.search(text); + } + + @GetMapping("/delete") + public void delete(@RequestParam String id) { + vectorStoreStorage.delete(Set.of(id)); + } + +} diff --git a/spring-ai-observability/spring-ai-observability-tracing/src/main/java/com/glmapper/ai/observability/tracing/storage/VectorStoreStorage.java b/spring-ai-observability/spring-ai-observability-tracing/src/main/java/com/glmapper/ai/observability/tracing/storage/VectorStoreStorage.java new file mode 100644 index 0000000..7b2f2a2 --- /dev/null +++ b/spring-ai-observability/spring-ai-observability-tracing/src/main/java/com/glmapper/ai/observability/tracing/storage/VectorStoreStorage.java @@ -0,0 +1,39 @@ +package com.glmapper.ai.observability.tracing.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; + + +@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-observability/spring-ai-observability-tracing/src/main/java/com/glmapper/ai/observability/tracing/tools/function/Unit.java b/spring-ai-observability/spring-ai-observability-tracing/src/main/java/com/glmapper/ai/observability/tracing/tools/function/Unit.java new file mode 100644 index 0000000..49bbb89 --- /dev/null +++ b/spring-ai-observability/spring-ai-observability-tracing/src/main/java/com/glmapper/ai/observability/tracing/tools/function/Unit.java @@ -0,0 +1,11 @@ +package com.glmapper.ai.observability.tracing.tools.function; + +/** + * @Classname Unit + * @Description TODO + * @Date 2025/5/29 17:07 + * @Created by glmapper + */ +public enum Unit { + C, F +} diff --git a/spring-ai-observability/spring-ai-observability-tracing/src/main/java/com/glmapper/ai/observability/tracing/tools/function/WeatherRequest.java b/spring-ai-observability/spring-ai-observability-tracing/src/main/java/com/glmapper/ai/observability/tracing/tools/function/WeatherRequest.java new file mode 100644 index 0000000..b073abe --- /dev/null +++ b/spring-ai-observability/spring-ai-observability-tracing/src/main/java/com/glmapper/ai/observability/tracing/tools/function/WeatherRequest.java @@ -0,0 +1,10 @@ +package com.glmapper.ai.observability.tracing.tools.function; + + +/** + * @Classname WeatherRequest + * @Description WeatherRequest + */ +public record WeatherRequest(String location, Unit unit) { + +} diff --git a/spring-ai-observability/spring-ai-observability-tracing/src/main/java/com/glmapper/ai/observability/tracing/tools/function/WeatherResponse.java b/spring-ai-observability/spring-ai-observability-tracing/src/main/java/com/glmapper/ai/observability/tracing/tools/function/WeatherResponse.java new file mode 100644 index 0000000..166a440 --- /dev/null +++ b/spring-ai-observability/spring-ai-observability-tracing/src/main/java/com/glmapper/ai/observability/tracing/tools/function/WeatherResponse.java @@ -0,0 +1,10 @@ +package com.glmapper.ai.observability.tracing.tools.function; + + + +/** + * @Classname WeatherResponse + * @Description WeatherResponse + */ +public record WeatherResponse(double temp, Unit unit) { +} diff --git a/spring-ai-observability/spring-ai-observability-tracing/src/main/java/com/glmapper/ai/observability/tracing/tools/function/WeatherService.java b/spring-ai-observability/spring-ai-observability-tracing/src/main/java/com/glmapper/ai/observability/tracing/tools/function/WeatherService.java new file mode 100644 index 0000000..d1cc9ed --- /dev/null +++ b/spring-ai-observability/spring-ai-observability-tracing/src/main/java/com/glmapper/ai/observability/tracing/tools/function/WeatherService.java @@ -0,0 +1,15 @@ +package com.glmapper.ai.observability.tracing.tools.function; + +import java.util.function.Function; + + +/** + * @Classname WeatherService + * @Description WeatherService + */ +public class WeatherService implements Function { + + public WeatherResponse apply(WeatherRequest request) { + return new WeatherResponse(30.0, Unit.C); + } +} diff --git a/spring-ai-observability/spring-ai-observability-tracing/src/main/java/com/glmapper/ai/observability/tracing/tools/methods/DateTimeTools.java b/spring-ai-observability/spring-ai-observability-tracing/src/main/java/com/glmapper/ai/observability/tracing/tools/methods/DateTimeTools.java new file mode 100644 index 0000000..2c1e731 --- /dev/null +++ b/spring-ai-observability/spring-ai-observability-tracing/src/main/java/com/glmapper/ai/observability/tracing/tools/methods/DateTimeTools.java @@ -0,0 +1,24 @@ +package com.glmapper.ai.observability.tracing.tools.methods; + +import org.springframework.ai.chat.model.ToolContext; +import org.springframework.ai.tool.annotation.Tool; +import org.springframework.context.i18n.LocaleContextHolder; + +import java.text.SimpleDateFormat; +import java.time.LocalDateTime; + + +public class DateTimeTools { + + @Tool(description = "Get the current date and time in the user's timezone") + public String getCurrentDateTime() { + return LocalDateTime.now().atZone(LocaleContextHolder.getTimeZone().toZoneId()).toString(); + } + + + @Tool(description = "Get the current date and time in the user's timezone") + public String getFormatDateTime(ToolContext toolContext) { + return new SimpleDateFormat(toolContext.getContext().get("format").toString()) + .format(LocalDateTime.now().atZone(LocaleContextHolder.getTimeZone().toZoneId()).toInstant().toEpochMilli()); + } +} diff --git a/spring-ai-observability/spring-ai-observability-tracing/src/main/resources/application.yml b/spring-ai-observability/spring-ai-observability-tracing/src/main/resources/application.yml new file mode 100644 index 0000000..c7034d3 --- /dev/null +++ b/spring-ai-observability/spring-ai-observability-tracing/src/main/resources/application.yml @@ -0,0 +1,82 @@ +spring: + application: + name: spring-ai-observability + data: + redis: + host: ${REDIS_HOST:localhost} + port: ${REDIS_PORT:6379} + username: ${REDIS_USERNAME:} + password: ${REDIS_PASSWORD:} + ai: + openai: + api-key: ${spring.ai.openai.api-key} + chat: + base-url: https://dashscope.aliyuncs.com/compatible-mode + options: + model: qwen-plus + image: + api-key: ${spring.ai.openai.api-key} + base-url: https://dashscope.aliyuncs.com/api + options: + model: stable-diffusion-xl + images-path: /v1/services/aigc/text2image/image-synthesis + 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_ + observations: + # 记录向量存储查询响应内容 + log-query-response: true + + chat: + client: + observations: + # 是否记录聊天客户端提示内容 + log-prompt: true + observations: + # 记录提示内容 + log-prompt: true + # 记录完成内容 + log-completion: true + # 在观察中包含错误日志 + include-error-logging: true + image: + observations: + # 记录提示内容 + log-prompt: true + + +management: + endpoints: + web: + exposure: + include: "*" + endpoint: + health: + show-details: always + prometheus: + metrics: + export: + enabled: true + # zipkin 配置 + zipkin: + tracing: + endpoint: http://localhost:9411/api/v2/spans + tracing: + sampling: + probability: 1.0 + +server: + port: 8088 + servlet: + encoding: + charset: utf-8 + enabled: true + force: true \ No newline at end of file diff --git a/spring-ai-tool-calling/README.md b/spring-ai-tool-calling/README.md new file mode 100644 index 0000000..9fd619a --- /dev/null +++ b/spring-ai-tool-calling/README.md @@ -0,0 +1,629 @@ +## Tool Calling + +工具调用(也称为函数调用)是 AI 应用程序中的一种常见模式,允许模型与一组 API 或工具交互,从而扩展其功能。工具主要用于: + ++ **信息检索。** 此类工具可用于从外部源(如数据库、Web 服务、文件系统或 Web 搜索引擎)检索信息。目标是增强模型的知识,使其能够回答原本无法回答的问题。因此,它们可用于检索增强生成(RAG)场景。例如,工具可用于检索给定位置的当前天气、检索最新新闻文章或查询数据库中的特定记录。 ++ **执行操作。** 此类工具可用于在软件系统中执行操作,如发送电子邮件、在数据库中创建新记录、提交表单或触发工作流。目标是自动化原本需要人工干预或显式编程的任务。例如,工具可用于为与聊天机器人交互的客户预订航班、填写网页表单或在代码生成场景中基于自动化测试(TDD)实现 Java 类。 + +尽管我们通常将工具调用称为模型能力,但实际上是由客户端应用程序提供工具调用逻辑。模型只能请求工具调用并提供输入参数,而应用程序负责从输入参数执行工具调用并返回结果。模型永远无法访问作为工具提供的任何 API,这是一个关键的安全考虑因素。Spring AI 提供了便捷的 API 来定义工具、解析来自模型的工具调用请求并执行工具调用。本章主要概述 Spring AI 中的工具调用功能。 + +## 快速开始 +首先看看如何在 Spring AI 中开始使用工具调用。这里我将实现两个简单的工具:一个用于信息检索,一个用于执行操作。信息检索工具将用于获取用户时区的当前日期和时间。操作工具将用于在指定时间设置闹钟。 + +### 信息检索 +AI 模型无法访问实时信息。任何假设了解当前日期或天气预报等信息的问题都无法由模型回答。但是,我们可以提供一个可以检索这些信息的工具,让模型在需要访问实时信息时调用这个工具。 + +这里使用官方文档中提供的案例, `DateTimeTools` 类中实现一个工具来获取用户时区的当前日期和时间,这个工具不需要参数。Spring Framework 的 `LocaleContextHolder` 可以提供用户的时区。`DateTimeTools` 将使用 `@Tool` 注解进行声明。为了让模型能够理解是否以及何时调用此工具,可以在 @Tool 注解的 description 属性中给出工具功能的详细描述。 + +```java +import java.time.LocalDateTime; +import org.springframework.ai.tool.annotation.Tool; +import org.springframework.context.i18n.LocaleContextHolder; + +public class DateTimeTools { + + @Tool(description = "Get the current date and time in the user's timezone") + String getCurrentDateTime() { + return LocalDateTime.now().atZone(LocaleContextHolder.getTimeZone().toZoneId()).toString(); + } + +} +``` + +下面通过 ChatClient 的 `tools()` 方法来绑定 `DateTimeTools` 这个工具。当模型需要知道当前日期和时间时,它将请求调用工具。在内部,`ChatClient` 调用工具并将结果返回给模型,然后模型将使用工具调用结果生成对原始问题的最终响应。 + +```java +ChatModel chatModel = ... + +String response = ChatClient.create(chatModel) +.prompt("What day is tomorrow?") +.defaultTools(new DateTimeTools()) +.call() +.content(); + +System.out.println(response); +``` + +输出将类似于: + +```plain +Tomorrow is 2025-5-29. +``` + +再次尝试问同样的问题。这次,不要向模型提供工具。输出将类似于: + +```plain +I am an AI and do not have access to real-time information. Please provide the current date so I can accurately determine what day tomorrow will be. +``` + +没有工具,模型不知道如何回答这个问题,因为它无法确定当前日期和时间。 + +### 执行操作 +AI 模型可用于生成实现某些目标的计划。例如,模型可以生成预订丹麦之旅的计划。但是,模型没有执行计划的能力。这就是工具的用武之地:它们可用于执行模型生成的计划。 + +在前面的示例中,我们使用工具来确定当前日期和时间。在此示例中,我们将定义第二个工具用于在特定时间设置闹钟。目标是设置从现在起 10 分钟的闹钟,因此我们需要向模型提供两个工具来完成此任务。 + +我们将新工具添加到与之前相同的 `DateTimeTools` 类中。新工具将接受一个参数,即 ISO-8601 格式的时间。然后,工具将在控制台打印一条消息,指示已为给定时间设置闹钟。与之前一样,该工具使用 `@Tool` 注解定义,我们还用它提供详细描述以帮助模型理解何时以及如何使用该工具。 + +```java +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import org.springframework.ai.tool.annotation.Tool; +import org.springframework.context.i18n.LocaleContextHolder; + +class DateTimeTools { + + @Tool(description = "Get the current date and time in the user's timezone") + String getCurrentDateTime() { + return LocalDateTime.now().atZone(LocaleContextHolder.getTimeZone().toZoneId()).toString(); + } + + @Tool(description = "Set a user alarm for the given time, provided in ISO-8601 format") + void setAlarm(String time) { + LocalDateTime alarmTime = LocalDateTime.parse(time, DateTimeFormatter.ISO_DATE_TIME); + System.out.println("Alarm set for " + alarmTime); + } + +} +``` + +接下来,让我们使两个工具对模型可用。我们将使用 `ChatClient` 与模型交互。我们将通过 `tools()` 方法传递 `DateTimeTools` 的实例来向模型提供工具。当我们要求从现在起 10 分钟设置闹钟时,模型首先需要知道当前日期和时间。然后,它将使用当前日期和时间来计算闹钟时间。最后,它将使用闹钟工具设置闹钟。在内部,`ChatClient` 将处理来自模型的任何工具调用请求并将任何工具调用执行结果发送回模型,以便模型可以生成最终响应。 + +```java +@Bean +public ChatClient chatClient(OpenAiChatModel chatModel, ChatMemory chatMemory) { + +// ToolCallback[] toolCallbacks = ToolCallbacks.from(new DateTimeTools(),new FileReaderTools()); +// ChatClient.builder(chatModel) +// .defaultToolCallbacks(toolCallbacks) +// .defaultAdvisors(MessageChatMemoryAdvisor.builder(chatMemory).build()) +// .defaultSystem("You are deepseek chat bot, you answer questions in a concise and accurate manner.") +// .build(); + + // 与上面的代码等价 + return ChatClient.builder(chatModel) + .defaultTools(new DateTimeTools(), new FileReaderTools()) + .defaultAdvisors(MessageChatMemoryAdvisor.builder(chatMemory).build()) + .defaultSystem("You are deepseek chat bot, you answer questions in a concise and accurate manner.") + .build(); +} +``` + +在应用程序日志中,你可以检查闹钟是否已在正确的时间设置。 + +## 概述 +Spring AI 提供了一套灵活的抽象机制,用于支持工具调用(tool calling),开发者可以通过这些机制以统一的方式来定义、解析并执行各类工具。本节将介绍 Spring AI 中工具调用的核心概念和主要组件。 + +![tc-core-component.png](../docs/statics/tc-core-component.png) + +1、如果我们希望某个工具可以被模型使用,就需要在对话请求中定义该工具,包括工具的名称、功能描述和输入参数的结构说明(schema)。 + +2、一旦模型决定调用某个工具,它会返回一条响应,包含工具的名称以及根据 schema 构造的输入参数。 + +3、应用程序需要根据工具名称找到并执行对应的工具,同时传入模型提供的参数。 + +4、工具的执行结果由应用程序处理。 + +5、然后应用程序将处理结果返回给模型。 + +6、模型会将工具调用的结果作为额外的上下文信息,生成最终的回复。 + + + +工具是工具调用机制中的核心组件,它们通过 `ToolCallback` 接口进行建模。Spring AI 提供了内建支持,可以将方法或函数直接注册为 `ToolCallback`,当然你也可以自己实现该接口,以适配更复杂的业务需求。 + +在实际使用中,`ChatModel` 会自动将模型发起的工具调用请求转发给相应的 `ToolCallback` 实例,并在获取结果后将其反馈给模型,由模型生成最终的回复。这一过程由 `ToolCallingManager` 接口协调完成,它负责工具调用的整个执行流程。 + +无论是 `ChatClient` 还是 `ChatModel`,都支持通过传入一组 `ToolCallback` 实例将工具注册给模型,并结合 `ToolCallingManager` 执行这些工具。 + +除了直接传入 `ToolCallback` 对象之外,还可以只传入工具的名称列表,工具会通过 `ToolCallbackResolver` 接口动态解析成具体的执行逻辑。 + + +## Methods 作为 Tools +Spring AI 内置支持通过 Methods 指定工具,可以通过两种方式实现: + ++ 在方法上声明 `@Tool 注解` ++ 通过编码方式实现 `MethodToolCallback` + +### @Tool 注解 +```java +@Tool(description = "Get the current date and time in the user's timezone") +public String getCurrentDateTime() { + return LocalDateTime.now().atZone(LocaleContextHolder.getTimeZone().toZoneId()).toString(); +} +``` + +`@Tool` 注解中几个关键方法: + ++ **name**:工具名称。如果未指定,将使用方法名。AI 模型在调用工具时会使用此名称。因此,在同一个类中不允许有两个名称相同的工具。对于特定聊天请求中可用的所有工具,其名称必须唯一。 ++ **description**:工具的描述,供模型理解何时以及如何调用该工具。如果未指定,将使用方法名作为工具描述。但强烈建议提供详细的描述,因为这是模型理解工具用途及其使用方式的关键。如果描述不清晰,可能导致模型在该使用工具时不调用它,或调用方式不正确。 ++ **returnDirect**:是否将工具结果直接返回给客户端,还是将其传回给模型。 ++ **resultConverter**:用于将工具调用结果转换为 `String` 对象,以便返回给 AI 模型的 `ToolCallResultConverter` 实现类。 + +Spring AI 会自动为带有 `@Tool` 注解的方法生成输入参数的 JSON Schema。这个 Schema 是模型识别工具使用方式的关键,它能帮助模型构造正确的调用请求。我们可以通过 `@ToolParam` 注解为每个参数补充更多信息,比如添加说明文字、标明该参数是否为必填项等。默认情况下,所有参数都被视为必填。 + +```java +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import org.springframework.ai.tool.annotation.Tool; +import org.springframework.ai.tool.annotation.ToolParam; + +public class DateTimeTools { + + @Tool(description = "Set a user alarm for the given time") + public void setAlarm(@ToolParam(description = "Time in ISO-8601 format") String time) { + LocalDateTime alarmTime = LocalDateTime.parse(time, DateTimeFormatter.ISO_DATE_TIME); + System.out.println("Alarm set for " + alarmTime); + } + +} +``` + +@ToolParam 注解允许我们为工具参数提供更多描述信息: + ++ description:参数的描述,帮助模型更好地理解如何使用该参数,比如参数应采用什么格式,允许哪些取值等。 ++ required:参数是否必填,默认情况下所有参数都被视为必填。 + +如果参数被标注为 @Nullable,则会被视为可选,除非通过 @ToolParam 注解显式声明为必填。除了 @ToolParam 注解外,我们还可以使用 Swagger 的 @Schema 注解或 Jackson 的 @JsonProperty 注解。 + +### `MethodToolCallback` + +MethodToolCallback.Builder 允许我们构建一个 MethodToolCallback 实例,并提供工具的关键信息: + ++ toolDefinition:定义工具名称、描述和输入参数结构的 ToolDefinition 实例。可以通过 ToolDefinition.Builder 类构建,必填项。 ++ toolMetadata:定义额外设置的 ToolMetadata 实例,比如结果是否直接返回给客户端,以及使用的结果转换器。可以通过 ToolMetadata.Builder 构建。 ++ toolMethod:表示工具方法的 Method 实例,必填项。 ++ toolObject:包含工具方法的对象实例。 ++ toolCallResultConverter:用于将工具调用结果转换为发送给 AI 模型的字符串的 ToolCallResultConverter 实例。如果不提供,则使用默认转换器 DefaultToolCallResultConverter。 + +ToolDefinition.Builder 允许我们构建 ToolDefinition 实例,并定义工具的名称、描述和输入参数结构: + ++ name:工具名称。如果未提供,则使用方法名。AI 模型通过此名称识别调用的工具,因此同一个类中不允许存在同名工具。该名称在特定聊天请求中所有可用工具中必须唯一。 ++ description:工具描述,帮助模型理解何时以及如何调用该工具。如果未提供,则使用方法名作为描述。但强烈建议提供详细描述,因为这对模型理解工具用途及使用方式至关重要。缺乏良好描述可能导致模型未能在应调用时使用工具,或错误调用工具。 ++ inputSchema:工具输入参数的 JSON 结构。如果未提供,系统将基于方法参数自动生成。可通过 @ToolParam 注解提供输入参数的附加信息,如描述及是否必填,默认所有参数均视为必填。 + +ToolMetadata.Builder 允许我们构建 ToolMetadata 实例,并定义工具的附加设置: + ++ returnDirect:是否将工具结果直接返回给客户端,还是传回模型。 + +```java +Method method = ReflectionUtils.findMethod(DateTimeTools.class, "getCurrentDateTime"); +ToolCallback toolCallback = MethodToolCallback.builder() + .toolDefinition(ToolDefinition.builder().name("getCurrentDateTime") + .description("Get the current date and time in the user's timezone") + .build()) + .toolMethod(method) + .build(); +ChatClient chatClient = ChatClient.builder(chatModel) + .defaultToolCallbacks(toolCallback) + .defaultAdvisors(MessageChatMemoryAdvisor.builder(chatMemory).build()) + .defaultSystem("You are deepseek chat bot, you answer questions in a concise and accurate manner.") + .build(); +``` + +### MethodTool 的限制 +以下类型目前不支持用作工具方法的参数或返回值: + +1. `Optional` +2. 异步类型(如 `CompletableFuture`、`Future`) +3. 响应式类型(如 `Flow`、`Mono`、`Flux`) +4. 函数式类型(如 `Function`、`Supplier`、`Consumer`) + + + +函数式类型可以通过 Functions as Tools 方式使用。 + +## Function 作为 Tools +Spring AI 内置支持通过 Function 指定工具,可以通过两种方式实现: + ++ 使用底层的 `FunctionToolCallback` 实现,以编程方式定义工具; ++ 也可以将函数作为 `@Bean` 动态注册,在运行时解析并使用。 + +### `FunctionToolCallback` +可以通过编程方式构建 `FunctionToolCallback`,将函数式类型(如 `Function`、`Supplier`、`Consumer` 或 `BiFunction`)转换为工具。 + +```java +public class WeatherService implements Function { + public WeatherResponse apply(WeatherRequest request) { + return new WeatherResponse(30.0, Unit.C); + } +} + +public enum Unit { C, F } +public record WeatherRequest(String location, Unit unit) {} +public record WeatherResponse(double temp, Unit unit) {} +``` + +可以通过 `FunctionToolCallback.Builder` 构建一个 `FunctionToolCallback` 实例,并为工具提供关键信息,包括: + ++ **name**:工具名称。模型依靠该名称识别并调用工具,因此同一上下文中不允许存在两个重名工具。该名称在一次对话请求中必须全局唯一(**必填**)。 ++ **toolFunction**:表示工具方法的函数式对象,支持 `Function`、`Supplier`、`Consumer` 和 `BiFunction` 类型(**必填**)。 ++ **description**:工具的描述,用于帮助模型理解该工具的用途以及何时、如何调用。如果未显式提供,则默认使用方法名作为描述。但强烈建议提供详细描述,因为这对模型理解工具的作用和用法至关重要。描述不足可能导致模型在该使用工具时未调用,或使用方式错误。 ++ **inputType**:函数输入参数的类型(**必填**)。 ++ **inputSchema**:工具输入参数的 JSON Schema。若未指定,会根据 `inputType` 自动生成。可使用 `@ToolParam` 注解补充参数的描述信息,如是否必填等。默认所有参数均为必填项。详见 JSON Schema 章节。 ++ **toolMetadata**:定义工具额外设置的对象,包括是否将结果直接返回给客户端、使用哪个结果转换器等。可通过 `ToolMetadata.Builder` 构建。 ++ **toolCallResultConverter**:用于将工具调用结果转换为字符串的 `ToolCallResultConverter` 实例,供模型接收。如果未提供,则使用默认转换器 `DefaultToolCallResultConverter`。 + +可以使用 `ToolMetadata.Builder` 构建一个 `ToolMetadata` 实例,并定义该工具的附加配置项,包括: + ++ **returnDirect**:是否将工具执行结果直接返回给客户端,而不是交由模型进一步处理。 + +```java +// 使用 FunctionToolCallback.Builder 构建 ToolCallback 实例 +ToolCallback toolCallback = FunctionToolCallback + .builder("currentWeather", new WeatherService()) + .description("Get the weather in location") + .inputType(WeatherRequest.class) + .build(); + +// 与上面的代码等价 +return ChatClient.builder(chatModel) + .defaultTools(new DateTimeTools(), new FileReaderTools()) + // defaultToolCallbacks + .defaultToolCallbacks(toolCallback) + .defaultAdvisors(MessageChatMemoryAdvisor.builder(chatMemory).build()) + .defaultSystem("You are deepseek chat bot, you answer questions in a concise and accurate manner.") + .build(); +``` + +### 使用动态注册方式 +除了通过编程方式指定工具外,也可以将工具定义为 Spring Bean,并通过 Spring AI 提供的 `ToolCallbackResolver` 接口(具体实现为 `SpringBeanToolCallbackResolver`)在运行时动态解析。这种方式允许将任何 `Function`、`Supplier`、`Consumer` 或 `BiFunction` 类型的 Bean 当作工具使用。 + +在这种模式下,**Bean 的名称会作为工具的名称**,而 Spring Framework 中的 `@Description` 注解则可用于为工具提供描述,供模型参考,以判断何时以及如何调用该工具。如果未显式提供描述,则默认使用方法名作为工具描述。**强烈建议提供详细且准确的描述**,否则可能导致模型在应使用工具时未调用,或调用方式不正确。 + +```java +@Configuration(proxyBeanMethods = false) +public class WeatherTools { + + WeatherService weatherService = new WeatherService(); + + @Bean + @Description("Get the weather in location") + Function currentWeather() { + return weatherService; + } + +} +``` + +### Function Tool 限制 + + +以下类型目前**不支持**作为函数式工具(Function、Supplier 等)中使用的输入或输出类型: + ++ 基本类型(Primitive types) ++ `Optional` 类型 ++ 集合类型(例如 `List`、`Map`、`Array`、`Set`) ++ 异步类型(例如 `CompletableFuture`、`Future`) ++ 响应式类型(例如 `Flow`、`Mono`、`Flux`) + +如需使用基本类型或集合类型,建议改用 Method-Tool 来定义工具。 + +## Tool Specification +在 Spring AI 中,工具通过 ToolCallback 接口进行建模。前面章节中,我们了解了如何利用 Spring AI 提供的内置支持,从方法和函数定义工具(参见“methods as tool”和“function as tool”)。本节将更深入地探讨工具规范,以及如何进行定制和扩展,以支持更多使用场景。 + +### Tool Callback +ToolCallback 接口提供了一种定义可被 AI 模型调用的工具的方法,包含工具的定义和执行逻辑。当需要从零开始定义一个工具时,这是主要需要实现的接口。例如,可以基于 MCP Client(使用模型上下文协议)或 ChatClient 来定义一个 ToolCallback,用于构建模块化的智能代理应用。ToolCallback 接口定义如下: + +```java +public interface ToolCallback { + + /** + * 定义由 AI 模型用来判断何时以及如何调用该工具。 + */ + ToolDefinition getToolDefinition(); + + /** + * 元数据,提供有关如何处理该工具的额外信息。 + */ + ToolMetadata getToolMetadata(); + + /** + * 使用给定的输入执行工具,并返回结果以发送回 AI 模型。 + */ + String call(String toolInput); + + /** + * 使用给定的输入和上下文执行工具,并返回结果以发送回 AI 模型。 + */ + String call(String toolInput, ToolContext tooContext); + +} +``` + +Spring AI 提供了工具方法(MethodToolCallback)和工具函数(FunctionToolCallback)的内置实现。 + +### Tool ToolDefinition +ToolDefinition 接口提供了 AI 模型了解工具可用性所需的信息,包括工具名称、描述和输入参数的模式。每个 ToolCallback 实现都必须提供一个 ToolDefinition 实例来定义该工具。ToolDefinition 接口定义如下: + +```java +public interface ToolDefinition { + + /** + * 工具名称:在提供给模型的工具集中必须唯一。 + */ + String name(); + + /** + * 工具的描述信息 + */ + String description(); + + /** + * 用于调用该工具的参数的 Schema(模式定义)。 + * 关于 json schema 可以参考:https://docs.spring.io/spring-ai/reference/api/tools.html#_json_schema + */ + String inputSchema(); + +} +``` +ToolDefinition.Builder 允许我们使用默认实现类 DefaultToolDefinition 来构建一个 ToolDefinition 实例。 + +```java +ToolDefinition toolDefinition = ToolDefinition.builder() + .name("currentWeather") + .description("Get the weather in location") + .inputSchema(""" + { + "type": "object", + "properties": { + "location": { + "type": "string" + }, + "unit": { + "type": "string", + "enum": ["C", "F"] + } + }, + "required": ["location", "unit"] + } + """) + .build(); +``` + +#### Method Tool Definition +下面示例是构建 `Method ToolDefinition` 的方式 + +```java +Method method = ReflectionUtils.findMethod(DateTimeTools.class, "getCurrentDateTime"); +ToolDefinition toolDefinition = ToolDefinition.builder(method) + .name("currentDateTime") + .description("Get the current date and time in the user's timezone") + .inputSchema(JsonSchemaGenerator.generateForMethodInput(method)) + .build(); +``` + +#### Function Tool Definition +从函数构建工具时,`ToolDefinition` 会自动为我们生成。使用 `FunctionToolCallback.Builder` 构建 `FunctionToolCallback` 实例时,可以提供工具名称、描述和输入 schema,这些信息将用于生成 ToolDefinition。 + +### Tool Context +Spring AI 支持通过 `ToolContext API` 向工具传递额外的上下文信息。该功能允许我们提供额外的用户数据,这些数据可以与 AI 模型传递的工具参数一起在工具执行过程中使用。 + +![tool-context.png](../docs/statics/tool-context.png) + +```java + @Tool(description = "Get the current date and time in the user's timezone") +public String getFormatDateTime(ToolContext toolContext) { + return new SimpleDateFormat(toolContext.getContext().get("format").toString()) + .format(LocalDateTime.now().atZone(LocaleContextHolder.getTimeZone().toZoneId()).toInstant().toEpochMilli()); +} +``` + +具体使用如下: + +```java +ChatClient.create(chatModel) + .prompt("Tell me more about the customer with ID 42") + .tools(new DateTimeTools()) + .toolContext(Map.of("format", "yyyy-MM-dd HH:mm:ss")) + .call() + .content(); +``` + + + +### Return Direct +默认情况下,工具调用的结果会作为响应发送回模型,模型随后可以利用该结果继续对话。但在某些情况下,我们希望直接将结果返回给调用方,而不是发送回模型。比如,当构建一个依赖 RAG 工具的智能体时,可能想直接将结果返回给调用者,避免模型进行不必要的后续处理。又或者,有些工具应当结束智能体的推理循环。每个 ToolCallback 实现都可以定义工具调用结果是直接返回给调用方,还是发送回模型。默认行为是将结果发送回模型,但我们可以针对每个工具调整这个行为。负责管理工具执行生命周期的 ToolCallingManager 会处理工具的 returnDirect 属性。如果该属性为 true,工具调用结果会直接返回给调用方;否则,结果将发送回模型。 + +> PS : 如果同时请求多个工具调用,必须将所有工具的 returnDirect 属性都设置为 true,结果才能直接返回给调用方。否则,结果将会发送回模型。 + + +![tc-returnDirect.png](../docs/statics/tc-returnDirect.png) + +* 1、当我们希望让模型能够调用某个工具时,会在聊天请求中包含该工具的定义。如果希望工具执行结果直接返回给调用方,需要将 returnDirect 属性设置为 true。 +* 2、当模型决定调用工具时,会发送包含工具名称和按照定义的参数 schema 组织的输入参数的响应。 +* 3、应用程序负责根据工具名称识别并使用提供的输入参数执行该工具。 +* 4、应用程序处理工具调用的结果。 +* 5、应用程序将工具调用结果直接发送给调用方,而不是返回给模型。 + +## Tool Execution + +工具执行是指使用提供的输入参数调用工具并返回结果的过程。工具执行由 ToolCallingManager 接口负责管理,该接口负责整个工具执行的生命周期。 + +```java +public interface ToolCallingManager { + + /** + * 从模型的工具调用选项中解析工具定义。 + */ + List resolveToolDefinitions(ToolCallingChatOptions chatOptions); + + /** + * 执行模型请求的工具调用。 + */ + ToolExecutionResult executeToolCalls(Prompt prompt, ChatResponse chatResponse); +} +``` + +如果使用了任何 Spring AI Spring Boot Starter,DefaultToolCallingManager 是 ToolCallingManager 接口的自动配置实现。我们可以通过提供自定义的 ToolCallingManager Bean 来定制工具执行行为。 + +```java +@Bean +ToolCallingManager toolCallingManager() { + return ToolCallingManager.builder().build(); +} +``` + +默认情况下,Spring AI 会在每个 ChatModel 实现内部为你透明地管理工具执行生命周期。但是也可以选择不使用这种默认行为,而是自行控制工具的执行。 下面将介绍这两种场景。 + +### 由框架控制的 Tool Execution +在使用默认行为时,Spring AI 会自动拦截模型发起的任何 Tool Calling 请求,执行对应工具并将结果返回给模型。所有这些操作都由各个 `ChatModel` 实现通过 `ToolCallingManager` 透明完成,无需额外配置或手动干预。 + +![tc-ToolCallingManager.png](../docs/statics/tc-ToolCallingManager.png) + +* 1、在我们希望让某个工具对模型可用时,需要在聊天请求(Prompt)中包含该工具的定义,并通过 ChatModel API 向 AI 模型发送请求。 +* 2、当模型决定调用某个工具时,会发回一个响应(ChatResponse),其中包含工具名称和根据预定义 schema 构造的输入参数。 +* 3、ChatModel 将工具调用请求发送给 ToolCallingManager API。 +* 4、ToolCallingManager 负责识别应调用的工具,并使用提供的输入参数执行该工具。 +* 5、工具调用结果返回给 ToolCallingManager。 +* 6、ToolCallingManager 将工具执行结果返回给 ChatModel。 +* 7、ChatModel 将工具执行结果作为 ToolResponseMessage 发送回 AI 模型。 +* 8、AI 模型结合工具调用结果生成最终响应,并通过 ChatClient 将 ChatResponse 返回给调用方。 + +> 当前,与模型之间就工具执行所交换的内部消息对用户来说是不可见的。如果需要访问这些消息,必须采用用户控制的工具执行方式。 + +Tool Calling 是否可以执行的判断逻辑由 `ToolExecutionEligibilityPredicate` 接口负责控制。通常情况下,工具执行的条件是 ToolCallingChatOptions 的 internalToolExecutionEnabled 属性被设置为 true(默认值),并且 ChatResponse 中包含工具调用信息。 + +```java +/** + * Default implementation of {@link ToolExecutionEligibilityPredicate} that checks whether + * tool execution is enabled in the prompt options and if the chat response contains tool + * calls. + * + * @author Christian Tzolov + */ +public class DefaultToolExecutionEligibilityPredicate implements ToolExecutionEligibilityPredicate { + + @Override + public boolean test(ChatOptions promptOptions, ChatResponse chatResponse) { + return ToolCallingChatOptions.isInternalToolExecutionEnabled(promptOptions) && chatResponse != null + && chatResponse.hasToolCalls(); + } + +} +``` + +当然,我们也可以在实际开发过程中自己实现这个接口,只要在创建 `ChatModel` Bean 时,指定自定义的 `ToolExecutionEligibilityPredicate` 实现,从而实现自定义控制是否可被调用的判断逻辑。 + +### 由用户控制的 Tool Execution + +在某些场景下,我们其实更希望自行控制工具的执行生命周期。Spring AI 中提供了这样的扩展能力。即将 `ToolCallingChatOptions` 的 `internalToolExecutionEnabled` 属性设置为 `false`。当基于这个选项调用 `ChatModel` 时,工具的执行将由调用方负责,从而实现对整个执行流程的完全控制。不过我们需要自行在 `ChatResponse` 中检查是否存在工具调用,并通过 `ToolCallingManager` 执行这些调用。 + +下面是个示例 + +```java +public String manualExecTools(String message) { + // 创建一个 ToolCallingManager 实例 + ToolCallingManager toolCallingManager = ToolCallingManager.builder().build(); + // 注册工具方法 + ToolCallback[] toolCallbacks = ToolCallbacks.from(new DateTimeTools()); + // 创建一个 ChatOptions 实例,包含工具调用选项 + ChatOptions chatOptions = ToolCallingChatOptions.builder() + .toolCallbacks(toolCallbacks) + .internalToolExecutionEnabled(false) + .build(); + // 创建一个 Prompt 实例,包含用户消息和工具调用选项 + Prompt prompt = new Prompt(message, chatOptions); + // 调用 ChatModel 进行对话 + ChatResponse chatResponse = chatModel.call(prompt); + // 如果 ChatResponse 包含工具调用,则执行工具调用 + while (chatResponse.hasToolCalls()) { + // 执行工具调用 + ToolExecutionResult toolExecutionResult = toolCallingManager.executeToolCalls(prompt, chatResponse); + // 更新 Prompt 实例,包含工具执行结果和工具调用选项 + prompt = new Prompt(toolExecutionResult.conversationHistory(), chatOptions); + // 再次调用 ChatModel 进行对话 + chatResponse = chatModel.call(prompt); + } + // 获取最终的回答 + String answer = chatResponse.getResult().getOutput().getText(); + System.out.println(answer); + return answer; +} +``` + +> 在选择用户控制的工具执行方式时,推荐使用 `ToolCallingManager` 来管理工具调用操作。这样可以充分利用 Spring AI 提供的内置支持来完成工具的执行流程。当然,你也可以自行实现工具的执行逻辑,Spring AI 并不会对此做什么限制。 + +[这个示例](https://github.com/glmapper/spring-ai-summary/blob/main/spring-ai-tool-calling/src/main/java/com/glmapper/ai/tc/memory/UserControlledMemory.java)展示了将用户控制的工具执行方式与 ChatMemory API 结合使用的实现。 + +### 异常处理 + +当工具调用失败时,会抛出一个 `ToolExecutionException` 异常,可通过捕获该异常来处理错误。可以使用 `ToolExecutionExceptionProcessor` 来处理 `ToolExecutionException`,处理结果有两种:要么生成一条错误信息返回给 AI 模型,要么抛出异常由调用方处理。 + +```java +@FunctionalInterface +public interface ToolExecutionExceptionProcessor { + + /** + * Convert an exception thrown by a tool to a String that can be sent back to the AI + * model or throw an exception to be handled by the caller. + */ + String process(ToolExecutionException exception); + +} +``` + +只要我们引入了 Spring AI 相关的 start 依赖,就会引入`ToolExecutionExceptionProcessor` 接口的自动配置实现。默认情况下,错误信息会被发送回模型。`DefaultToolExecutionExceptionProcessor` 构造函数允许设置 `alwaysThrow` 属性为 `true` 或 `false`。如果设为 `true`,则会抛出异常,而不是将错误信息返回给模型。 + +> 如果自定义了自己的 `ToolCallback` 实现,务必在 `call()` 方法中的工具执行逻辑出现错误时抛出 `ToolExecutionException`。 + +ToolCallingManager 在工具执行过程中处理异常就是 ToolExecutionExceptionProcessor 的实现。 + +## Tool Resolution +将工具传递给模型的主要方式,是在调用 `ChatClient` 或 `ChatModel` 时,提供一个或多个 `ToolCallback`,前面我已经做过相关介绍。除了这两种方式外,Spring AI 还支持通过 `ToolCallbackResolver` 接口在运行时动态解析工具。 + +```java +public interface ToolCallbackResolver { + + /** + * Resolve the {@link ToolCallback} for the given tool name. + */ + @Nullable + ToolCallback resolve(String toolName); + +} +``` + +当我们使用这种方式时: + ++ 在客户端,需要向 `ChatClient` 或 `ChatModel` 提供工具名称,而不是 `ToolCallback` 实例。 ++ 在服务端,由 `ToolCallbackResolver` 接口的实现类负责将工具名称解析为对应的 `ToolCallback` 实例。 + +Spring AI 中 ToolCallbackResolver 接口的默认实现是`DelegatingToolCallbackResolver`,其本质是将工具解析的任务委托给一组 `ToolCallbackResolver` 实例。 + ++ `SpringBeanToolCallbackResolver` 会从 Spring 容器中的 `Function`、`Supplier`、`Consumer` 或 `BiFunction` 类型的 Bean 中解析工具。 ++ `StaticToolCallbackResolver` 会从一组静态的 `ToolCallback` 实例中解析工具。在使用 Spring Boot 自动配置时,该解析器会自动加载应用上下文中定义的所有 `ToolCallback` 类型的 Bean。 + + +如果依赖 Spring Boot 自动配置,可以通过提供自定义的 `ToolCallbackResolver` Bean 来定制工具解析逻辑。代码如下: + +```java +@Bean +ToolCallbackResolver toolCallbackResolver(List toolCallbacks) { + StaticToolCallbackResolver staticToolCallbackResolver = new StaticToolCallbackResolver(toolCallbacks); + return new DelegatingToolCallbackResolver(List.of(staticToolCallbackResolver)); +} +``` + +`ToolCallbackResolver` 在运行时被 `ToolCallingManager` 使用,用于动态解析工具,同时支持框架控制的工具执行方式和用户控制的工具执行方式。 + diff --git a/spring-ai-vector/pom.xml b/spring-ai-vector/pom.xml index 35e7aac..491bb72 100644 --- a/spring-ai-vector/pom.xml +++ b/spring-ai-vector/pom.xml @@ -12,6 +12,8 @@ spring-ai-embedding spring-ai-vector-milvus + spring-ai-vector-redis + spring-ai-vector-mariadb diff --git a/spring-ai-vector/spring-ai-vector-mariadb/README.md b/spring-ai-vector/spring-ai-vector-mariadb/README.md new file mode 100644 index 0000000..7cd4b51 --- /dev/null +++ b/spring-ai-vector/spring-ai-vector-mariadb/README.md @@ -0,0 +1,226 @@ +# `spring-ai-vector-mariadb` 文档 + +## 1. 概述 + +本模块 `spring-ai-vector-mariadb` 提供了一个基于 **MariaDB** 的向量存储实现,支持将高维向量数据存入 MariaDB 数据库,并能够执行相似性搜索(如余弦相似度)。适用于需要结合关系型数据库和 AI 向量能力的场景。 + +该项目集成了 [Spring AI](https://docs.spring.io/spring-ai/reference/html/) 和 [MariaDB](https://mariadb.org/),为开发者提供了一个快速上手的向量存储解决方案。 + +--- + +## 2. 功能特性 + +- 使用 MariaDB 存储文档及其向量表示 +- 支持通过 Spring Boot 自动配置向量数据库连接 +- 提供简单的 CRUD 操作(增删查) +- 支持基于余弦相似度的向量检索 +- 集成 OpenAI 兼容的嵌入模型接口(如阿里云 DashScope) + +--- + +## 3. 技术栈 + +| 技术 | 版本/说明 | +|------|---------------------------------------------| +| Java | JDK 21+ | +| Spring Boot | 最新稳定版本 | +| Spring AI | 集成 `spring-ai-starter-vector-store-mariadb` | +| MariaDB | 使用 JDBC 客户端 `mariadb-java-client:3.5.2` | +| 嵌入模型 | 支持 OpenAI 接口兼容模型,如阿里云 DashScope | + +--- + +## 4. 项目结构 + +``` +spring-ai-vector-mariadb/ +├── src/ +│ ├── main/ +│ │ ├── java/ +│ │ │ └── com.glmapper.ai.vector/ +│ │ │ ├── MariadbVectorApplication.java # 应用启动类 +│ │ │ └── storage/ +│ │ │ └── VectorStoreStorage.java # 向量存储业务逻辑封装 +│ │ └── resources/ +│ │ └── application.yml # 主配置文件 +│ └── test/ +│ ├── java/ +│ │ └── storage/ +│ │ └── VectorStoreStorageTest.java # 单元测试类 +│ └── resources/ +│ └── application-test.yml # 测试环境配置文件 +├── README.md # 项目说明文档(当前文件) +└── pom.xml # Maven 构建配置 +``` + + +--- + +## 5. 环境依赖与准备 + +### 5.1 MariaDB 数据库部署 + +推荐使用 Docker 快速部署 MariaDB 实例: + +```bash +docker run -d \ + --name mariadb \ + -e MYSQL_ROOT_PASSWORD=root \ + -p 3306:3306 \ + mariadb:latest +``` + + +① 初始化数据库并创建用于向量存储的表结构(可由 Spring Boot 自动完成)。 + +② 一个 `EmbeddingModel` 实例,用于计算文档嵌入。有多个[选项](https://docs.spring.io/spring-ai/reference/api/embeddings.html#available-implementations)可供选择 + +③ 一个 API 密钥,给 EmbeddingModel 用于生成向量数据 +> 注意:确保在 `application.yml` 中配置正确的数据库连接地址、用户名和密码。 + +--- + +## 6. 配置说明 + +### 6.1 核心配置 (`application.yml`) + +```yaml +server: + port: 8080 + +spring: + datasource: + url: ${BASE_HOST} # 如 jdbc:mariadb://localhost:3308/vector_test + username: ${BASE_NAME} + password: ${BASE_PWD} + driver-class-name: org.mariadb.jdbc.Driver + + ai: + vectorstore: + mariadb: + initialize-schema: true # 是否自动初始化表结构 + distance-type: COSINE # 距离计算类型 + dimensions: 1536 # 向量维度 + openai: + api-key: ${API_KEY} # 嵌入模型 API Key + embedding: + base-url: https://dashscope.aliyuncs.com/compatible-mode/v1 + embeddings-path: /embeddings + options: + model: text-embedding-v4 # 使用的嵌入模型 +``` + + +### 6.2 测试环境配置 (`application-test.yml`) + +```yaml +spring: + datasource: + url: jdbc:mariadb://localhost:3308/vector_test + username: root + password: root + ai: + vectorstore: + mariadb: + initialize-schema: false # 不再初始化 schema +``` + + +--- + +## 7. 核心类说明 + +### 7.1 `MariadbVectorApplication.java` + +主应用启动类,使用 `@SpringBootApplication` 注解启用 Spring Boot 自动配置。 + +### 7.2 `VectorStoreStorage.java` + +封装了对向量存储的操作,包括: + +- `store(List)`: 将文档及向量存入数据库 +- `search(String)`: 执行相似性搜索,返回匹配结果 +- `delete(Set)`: 删除指定 ID 的向量记录 + +依赖注入 `VectorStore` 实现具体的向量操作逻辑。 + +--- + +## 8. 单元测试 + +### 8.1 `VectorStoreStorageTest.java` + +单元测试验证向量存储与搜索功能是否正常。测试步骤如下: + +1. 插入预定义文档 +2. 执行相似性搜索 +3. 验证结果非空 +4. 清理数据(每个测试后删除插入的数据) + +运行命令: + +```bash +mvn test +``` + + +--- + +## 9. 使用指南 + +### 9.1 启动应用 + +确保已正确配置数据库连接信息和 API 密钥后,直接运行: + +```bash +mvn spring-boot:run +``` + + +或在 IDE(如 IntelliJ IDEA)中运行 `MariadbVectorApplication` 类。 + +### 9.2 示例调用 + +你可以通过 REST API 或自定义服务调用 [VectorStoreStorage](spring-ai-vector/spring-ai-vector-mariadb/src/main/java/com/glmapper/ai/vector/storage/VectorStoreStorage.java) 方法进行文档存储与查询。 + +--- + +## 10. 依赖管理 + +### 10.1 Maven 依赖 ([pom.xml](spring-ai-vector/spring-ai-vector-mariadb/pom.xml)) + +```xml + + org.springframework.ai + spring-ai-starter-vector-store-mariadb + + + + org.mariadb.jdbc + mariadb-java-client + 3.5.2 + +``` + + +--- + +## 11. 注意事项 + +- 确保 MariaDB 已正确安装并启动。 +- 如果遇到 SQL 异常,请检查 `initialize-schema` 设置以及数据库权限。 +- 向量维度需与使用的 Embedding 模型一致(默认为 1536)。 +- 在生产环境中应关闭 `initialize-schema`,避免误删数据。 + +--- + +## 12. 参考资料 + +- [Spring AI 文档](https://docs.spring.io/spring-ai/reference/html/) +- [MariaDB 官方文档](https://mariadb.org/documentation/) +- [DashScope 开发者文档](https://help.aliyun.com/zh/dashscope/developer-reference/introduction) +- [Docker Hub - MariaDB](https://hub.docker.com/_/mariadb) + +--- + +如有问题或建议,请联系维护人员。 \ No newline at end of file diff --git a/spring-ai-vector/spring-ai-vector-mariadb/pom.xml b/spring-ai-vector/spring-ai-vector-mariadb/pom.xml new file mode 100644 index 0000000..9f0d162 --- /dev/null +++ b/spring-ai-vector/spring-ai-vector-mariadb/pom.xml @@ -0,0 +1,38 @@ + + + 4.0.0 + + com.glmapper + spring-ai-vector + 0.0.1 + + + spring-ai-vector-mariadb + jar + + + UTF-8 + + + + + + org.springframework.ai + spring-ai-starter-vector-store-mariadb + + + org.mariadb.jdbc + mariadb-java-client + + + + + + org.mariadb.jdbc + mariadb-java-client + 3.5.2 + + + \ No newline at end of file diff --git a/spring-ai-vector/spring-ai-vector-mariadb/src/main/java/com/glmapper/ai/vector/MariadbVectorApplication.java b/spring-ai-vector/spring-ai-vector-mariadb/src/main/java/com/glmapper/ai/vector/MariadbVectorApplication.java new file mode 100644 index 0000000..7b5eea8 --- /dev/null +++ b/spring-ai-vector/spring-ai-vector-mariadb/src/main/java/com/glmapper/ai/vector/MariadbVectorApplication.java @@ -0,0 +1,15 @@ +package com.glmapper.ai.vector; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * @author siyuan + * @since 2025/6/14 + */ +@SpringBootApplication +public class MariadbVectorApplication { + public static void main(String[] args) { + SpringApplication.run(MariadbVectorApplication.class, args); + } +} diff --git a/spring-ai-vector/spring-ai-vector-mariadb/src/main/java/com/glmapper/ai/vector/storage/VectorStoreStorage.java b/spring-ai-vector/spring-ai-vector-mariadb/src/main/java/com/glmapper/ai/vector/storage/VectorStoreStorage.java new file mode 100644 index 0000000..0bf6677 --- /dev/null +++ b/spring-ai-vector/spring-ai-vector-mariadb/src/main/java/com/glmapper/ai/vector/storage/VectorStoreStorage.java @@ -0,0 +1,38 @@ +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 siyuan + * @since 2025/6/14 + */ +@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).build()); + } +} diff --git a/spring-ai-vector/spring-ai-vector-mariadb/src/main/resources/application.yml b/spring-ai-vector/spring-ai-vector-mariadb/src/main/resources/application.yml new file mode 100644 index 0000000..42cc2e9 --- /dev/null +++ b/spring-ai-vector/spring-ai-vector-mariadb/src/main/resources/application.yml @@ -0,0 +1,27 @@ +server: + port: 8080 + +spring: + application: + name: mariadb-vector-store + datasource: + url: ${BASE_HOST} + username: ${BASE_NAME} + password: ${BASE_PWD} + driver-class-name: org.mariadb.jdbc.Driver + ai: + vectorstore: + mariadb: + # 启用模式初始化 + initialize-schema: true + # 设置距离计算类型为余弦相似度 + distance-type: COSINE + # 定义向量维度为1536 + dimensions: 1536 + openai: + api-key: ${API_KEY} + embedding: + base-url: https://dashscope.aliyuncs.com/compatible-mode/v1 + embeddings-path: /embeddings + options: + model: text-embedding-v4 diff --git a/spring-ai-vector/spring-ai-vector-mariadb/src/test/java/storage/VectorStoreStorageTest.java b/spring-ai-vector/spring-ai-vector-mariadb/src/test/java/storage/VectorStoreStorageTest.java new file mode 100644 index 0000000..43fdfa1 --- /dev/null +++ b/spring-ai-vector/spring-ai-vector-mariadb/src/test/java/storage/VectorStoreStorageTest.java @@ -0,0 +1,58 @@ +package storage; + +import com.glmapper.ai.vector.MariadbVectorApplication; +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; + +/** + * 测试 基于 mariadb 的 VectorStoreStorage 的存储和搜索功能 + * + * @author siyuan + * @since 2025/6/14 + */ +@SpringBootTest( + classes = MariadbVectorApplication.class, + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT +) +@ActiveProfiles("test") +public class VectorStoreStorageTest { + + @Autowired + private VectorStoreStorage vectorStoreStorage; + + //prepare test data + private static final List documents = List.of( + new Document("Spring AI rocks!! Spring AI rocks!! Spring AI rocks!! Spring AI rocks!! Spring AI rocks!!", Map.of("meta1", "meta1")), + new Document("The World is Big and Salvation Lurks Around the Corner"), + new Document("You walk forward facing the past and you turn back toward the future.", Map.of("meta2", "meta2"))); + + @AfterEach + public void cleanUp() { + // clear the vector store after each test + Set ids = documents.stream().map(Document::getId) + .collect(Collectors.toSet()); + vectorStoreStorage.delete(ids); + } + + @Test + public void testStoreAndSearch() { + // store documents + vectorStoreStorage.store(documents); + // 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-mariadb/src/test/resources/application-test.yml b/spring-ai-vector/spring-ai-vector-mariadb/src/test/resources/application-test.yml new file mode 100644 index 0000000..c1d9aa6 --- /dev/null +++ b/spring-ai-vector/spring-ai-vector-mariadb/src/test/resources/application-test.yml @@ -0,0 +1,28 @@ +server: + port: 8080 + +spring: + application: + name: mariadb-vector-store + datasource: + url: jdbc:mariadb://localhost:3308/vector_test + username: root + password: root + driver-class-name: org.mariadb.jdbc.Driver + ai: + vectorstore: + mariadb: + # 启用模式初始化 + initialize-schema: false + # 设置距离计算类型为余弦相似度 + distance-type: COSINE + # 定义向量维度为1536 + dimensions: 1536 + openai: + api-key: ${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 diff --git a/spring-ai-vector/spring-ai-vector-milvus/README.md b/spring-ai-vector/spring-ai-vector-milvus/README.md new file mode 100644 index 0000000..07f32e7 --- /dev/null +++ b/spring-ai-vector/spring-ai-vector-milvus/README.md @@ -0,0 +1,176 @@ +## spring-ai-vector-milvus + +该工程模块主要是集成 Milvus 的向量存储功能,提供了一个使用 Milvus 存储向量并执行相似性搜索的简单示例。 + +### 1、Milvus 数据库部署与初始化 + +#### Milvus 安装 + +Milvus 是一个开源的向量数据库,用于存储和检索高维向量数据。本项目是使用 Docker 来运行 Milvus,当然你也可以选择其他方式安装 Milvus或者使用已经部署好的 Milvus 服务。 + +> PS: 如果你不运行 spring-ai-rag 模块和 spring-ai-embedding 模块,可以跳过此步骤。 + +这个项目使用的 milvus 版本是 2.5.0 版本,安装方式见:[Install Milvus in Docker](https://milvus.io/docs/install_standalone-docker.md)。 + +> ⚠️本人的电脑是 Mac Air M2 芯片,使用官方文档中的 docker-compose 文件启动 Milvus 时,遇到 `milvus-standalone` 镜像不匹配问题。 + +#### 创建 Collection(向量集合) + +> 注意:embedding 维度需与模型一致,否则会报错。 + +* 创建 Collection 的 curl 示例 +```bash +curl -X POST "http://localhost:19530/v2/vectordb/collections/create" \ + -H "Authorization: Bearer root:Milvus" \ + -H "Content-Type: application/json" \ + -d '{ + "collectionName": "vector_store", + "schema": { + "fields": [ + { "fieldName": "embedding", "dataType": "FloatVector", "elementTypeParams": { "dim": "2048" } }, + { "fieldName": "content", "dataType": "VarChar", "elementTypeParams": { "max_length": 512000 } }, + { "fieldName": "metadata", "dataType": "JSON" }, + { "fieldName": "doc_id", "dataType": "VarChar", "isPrimary": true, "elementTypeParams": { "max_length": 512 } } + ] + }, + "indexParams": [ + { "fieldName": "embedding", "metricType": "COSINE", "indexName": "embedding_index", "indexType": "AUTOINDEX" }, + { "fieldName": "doc_id", "indexName": "doc_id_index", "indexType": "AUTOINDEX" } + ] + }' +``` +* 加载集合(load collection) + +```bash +curl -X POST "http://localhost:19530/v2/vectordb/collections/load" \ + -H "Content-Type: application/json" \ + -d '{ + "collectionName": "vector_store" + }' +``` + +* 删除 Collection + +```bash +POST /v2/vectordb/collections/drop HTTP/1.1 +Authorization: Bearer root:Milvus +Content-Length: 38 +Content-Type: application/json +Host: localhost:19530 +User-Agent: HTTPie +{ + "collectionName": "vector_store" +} +``` + +### 2、引入依赖和配置 + +* 依赖 + +```xml + + org.springframework.ai + spring-ai-starter-vector-store-milvus + +``` + +* 配置 + +```properties +# embedding model +# 这里替换成你自己的 api-key +spring.ai.openai.api-key=${spring.ai.openai.api-key} +spring.ai.openai.embedding.base-url=https://ark.cn-beijing.volces.com/api/v3 +spring.ai.openai.embedding.embeddings-path=/embeddings +spring.ai.openai.embedding.options.model=ep-20250506170049-dzjj7 + +spring.ai.vectorstore.milvus.client.host=localhost +spring.ai.vectorstore.milvus.client.port=19530 +#spring.ai.vectorstore.milvus.client.username=root +#spring.ai.vectorstore.milvus.client.password=Milvus +#spring.ai.vectorstore.milvus.databaseName="default" +spring.ai.vectorstore.milvus.collection.name=vector_store +``` + +#### 文档数据初始化与嵌入 + +按照示例,你可以将你需要存储的文件放在 `src/main/resources/files` 目录下,然后使用 `LangChainTextSplitter` 类来读取文件内容,切分为小块,并将其嵌入到 Milvus 向量库中。 + +核心代码如下: + +```java +// LangChainTextSplitter.java +/** + * 读取本地 markdown 文档,切分为小块后写入 Milvus 向量库 + */ +public void embedding() { + try { + // 1. 创建文本切分器 + TextSplitter splitter = new TokenTextSplitter(); + // 2. 读取本地 markdown 文件内容 + URL path = LangChainTextSplitter.class.getClassLoader().getResource("classpath:files/LLM-infer.md"); + String mdContent = Files.readString(Paths.get(path.toURI()), StandardCharsets.UTF_8); + // 3. 构造 Document 对象 + Document doc = new Document(mdContent); + // 4. 切分为小块 + List docs = splitter.split(doc); + // 5. 写入向量库 + this.vectorStore.add(docs); + } catch (Exception e) { + // 异常信息 + } +} +``` + +* controller 代码如下 +```java +// 初始化嵌入数据 +/** + * 触发文档嵌入,将本地文档内容写入向量库 + */ +@GetMapping("embedding_test") +public String embedding() { + langChainTextSplitter.embedding(); + return "Embedding completed successfully."; +} + +// RAG 聊天接口 +/** + * 基于用户输入,检索相关文档并拼接到系统提示词,实现 RAG 问答 + * @param userInput 用户输入 + * @return LLM 生成的答案 + */ +@GetMapping("/chat") +public String prompt(@RequestParam String userInput) { + // 1. 构造用户消息 + Message userMessage = new UserMessage(userInput); + // 2. 检索相似文档 + List similarDocuments = vectorStore.similaritySearch(userInput); + // 3. 拼接检索到的内容 + String tncString = similarDocuments.stream().map(Document::getFormattedContent).collect(Collectors.joining("\n")); + // 4. 构造系统提示词 + Message systemMessage = new SystemPromptTemplate("You are a helpful assistant. Here are some relevant documents:\n\n {documents}") + .createMessage(Map.of("documents", tncString)); + // 5. 构造 Prompt + Prompt prompt = new Prompt(List.of(systemMessage, userMessage)); + // 6. 调用 LLM 生成答案 + return chatClient.prompt(prompt).call().content(); +} +``` + +* ChatClient 配置(Advisor API 自动拼接) +```java +@Bean +public ChatClient chatClient(OpenAiChatModel chatModel, VectorStore vectorStore) { + return ChatClient.builder(chatModel) + .defaultAdvisors(new QuestionAnswerAdvisor(vectorStore)) + .defaultSystem("You are a friendly chat bot that answers question with json always") + .build(); +} +``` + +#### 效果验证 + +1. 启动 spring-ai-rag 服务(确保 Milvus 已启动并初始化好集合) +2. 先访问 `/api/qwen/embedding_test` 完成文档嵌入 +3. 再访问 `/api/qwen/chat?userInput=你的问题`,可检索并返回文档相关内容 \ No newline at end of file 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..4c4f1f9 --- /dev/null +++ b/spring-ai-vector/spring-ai-vector-redis/README.md @@ -0,0 +1,78 @@ +## 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 用于生成向量数据 + + +## 自动配置 +Spring AI 为 Redis 向量数据库提供了 Spring Boot 自动配置。要启用它,请将以下依赖添加到项目的 Maven pom.xml 文件中: + +```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/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..16e96b4 --- /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..16e96b4 --- /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_