你是否想过,如何让 AI 不仅仅是陪聊,而是真正变成一个**“有记性、懂知识、能办事”**的智能助手?
今天,我们将一起构建一个**“狗狗领养助手”**。别被“AI”这个词吓到,其实它的原理就像教一个小孩子做事一样简单。
我们将用最通俗易懂的方式,带你从零开始,一步步把这个助手跑起来,并理解它背后的“大脑构造”。
核心概念
在开始写代码之前,我们先打个比方。一个能干活的 AI 助手,其实是由三个部分组成的:
记忆 (Memory):就像人的海马体。
- 问题:如果你问 AI “它几岁了?”,AI 得知道“它”指的是上一句聊到的那只狗。
- 解法:我们需要把聊天记录存起来,每次说话时都带上之前的上下文。
知识 (RAG - 检索增强生成):就像人的大脑皮层(存书本知识)。
- 问题:AI 的模型是半年前训练的,它不知道今天刚来了一只叫 Prancer 的新狗。
- 解法:我们把新狗的资料存在数据库里。当用户问起时,先去数据库查,查到了再告诉 AI。
手脚 (Tools/MCP):就像人的双手。
- 问题:AI 只是个聊天程序,它没法帮你打电话预约,也没法操作你的日程表。
- 解法:我们给 AI 提供一些“工具”(比如一个
schedule()函数),告诉它:“如果你想预约,就调用这个函数”。
在这个项目中,我们将用 Spring AI 把这三者串联起来。
环境准备
在开始之前,请检查你的电脑是否准备好了:
- Java 21+:这是硬指标,Spring AI 需要新版本的 Java。
- Docker Desktop:我们需要用它来运行数据库(PostgreSQL + pgvector)。
- Git:用来下载代码。
- API Key:你需要一个 OpenAI 的 Key,或者国内的通义千问(DashScope)Key。
第一部分:快速上手
我们先别管代码怎么写,先把项目跑起来,感受一下它的神奇之处。
1. 下载代码
打开终端,运行:
git clone https://github.com/chensoul/spring-ai-dog-adoption-showcase.gitcd spring-ai-dog-adoption-showcase2. 启动数据库
本项目默认使用 PostgreSQL + pgvector 作为向量数据库,同时也支持可选的 PostgresML。
cd adoptionsdocker-compose up -d postgres注意:
- 运行
docker ps,看到postgres容器的状态是Up就算成功。- 本项目默认连接 5432 端口的
postgres服务。
3. 启动 Scheduler Service
这是一个独立的小程序,专门负责处理“预约”这个动作。它就像是一个专门负责排期的前台。
打开一个新的终端窗口:
cd scheduler./mvnw spring-boot:run检查点:当你看到日志最后显示
Tomcat started on port 8081,说明它准备好了。
4. 启动 Adoptions Service
这是主程序,它负责听用户的指挥,然后去调用数据库或办事处。
打开一个新的终端窗口:
cd adoptions
# 设置你的 API Key (二选一)# 选项 A: 如果你用 OpenAIexport OPENAI_API_KEY=sk-你的key
# 选项 B: 如果你用通义千问export DASHSCOPE_API_KEY=sk-你的key
./mvnw spring-boot:run
# 通义千问需要额外加参数启动:# ./mvnw spring-boot:run -Dspring-boot.run.profiles=qwen
# 如果你想切换到 PostgresML (可选):# ./mvnw spring-boot:run -Ppostgresml -Dspring-boot.run.profiles=postgresml检查点:看到
Started AdoptionsApplication且端口是8080,说明大功告成!
5. 见证奇迹
现在,我们来考考它。
场景一:测试记忆与知识 (RAG)
我们要问一只其实很凶,但 AI 应该知道详情的狗 —— Prancer。
curl -G "http://localhost:8080/alice/assistant" --data-urlencode "question=do you have any neurotic dogs?"预期结果:AI 会根据数据库里的真实资料回答你。它可能会说:“Prancer is a Chihuahua with a neurotic personality and doesn’t like men…”(这说明它真的去查了数据库,而不是瞎编)。
场景二:测试办事能力 (Tools)
curl -G "http://localhost:8080/alice/assistant" --data-urlencode "question=I think Prancer is cool, help me schedule an appointment to adopt it"预期结果:AI 不会只跟你客套,它会真的去调用 Scheduler 服务。
- 你会看到 Scheduler 的终端里打印出:
Scheduling adoption for dog Prancer。 - curl 的返回值会告诉你:“Your appointment has been successfully scheduled for three days from now…”。
第二部分:深度解析
刚才发生了什么?让我们结合 Spring AI 1.0 发布博客 中提到的关键概念,看看代码是怎么实现的。
0. 核心依赖
首先,看看 pom.xml,我们引入了哪些“魔法积木”:
<dependencies> <!-- 1. MCP 客户端:连接外部工具 --> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-mcp-client</artifactId> </dependency> <!-- 2. pgvector 向量库:存储向量 (默认) --> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-vector-store-pgvector</artifactId> </dependency> <!-- 3. JDBC 聊天记忆:持久化对话 --> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-model-chat-memory-repository-jdbc</artifactId> </dependency></dependencies>
<profiles> <profile> <id>postgresml</id> <dependencies> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-model-postgresml-embedding</artifactId> </dependency> </dependencies> </profile></profiles>1. 系统提示
概念:就像演员上台前要看剧本一样,AI 也需要一个“人设”。如果你不告诉它“你是谁”,它可能会放飞自我。
实现:在 AdoptionsApplication.java 中,我们通过 .defaultSystem() 给了它一个明确的指令:
var system = """ You are an AI powered assistant to help people adopt a dog... If there is no information, then return a polite response... """;
this.ai = ai.defaultSystem(system).build();这就保证了它始终是一个“专业的领养助手”,而不是一个会写诗的诗人。
2. 聊天记忆
概念:大模型本身是无状态的(Stateless)。它就像电影《初恋50次》里的主角,每新说一句话,就忘了上一句说了什么。为了让它能和你连续对话,我们需要把聊天记录(Transcript)保存下来,每次都重新发给它。
实现:我们使用了 JdbcChatMemory 将记忆持久化到数据库。
在 application.properties 中,我们开启了自动建表功能,这样你就不用手动写 SQL 建表了:
spring.ai.chat.memory.repository.jdbc.initialize-schema=always代码配置如下:
@BeanPromptChatMemoryAdvisor promptChatMemoryAdvisor(DataSource dataSource) { // 1. 使用 JDBC 存储库 (持久化到 Postgres) var jdbc = JdbcChatMemoryRepository.builder().dataSource(dataSource).build();
// 2. 设定“消息窗口” (避免上下文过长,只保留最近 N 条) var chatMessageWindow = MessageWindowChatMemory.builder().chatMemoryRepository(jdbc).build();
return PromptChatMemoryAdvisor.builder(chatMessageWindow).build();}3. 检索增强生成
概念:模型虽然聪明,但它不知道你的私有数据(比如 Prancer 这只狗的性格)。我们不能把所有数据都塞进提示词(Prompt Stuffing),那样太贵且受长度限制。RAG 的做法是:先检索,再生成。
实现:本项目默认使用 pgvector 配合 OpenAI (或 Qwen) 的嵌入模型,同时也支持可选的 PostgresML 方案。
方案 A:默认方案 (pgvector + OpenAI/Qwen)
这是目前最通用的做法。我们使用 OpenAI 或通义千问来将文本转换成向量,然后存入 PostgreSQL 的 pgvector 扩展中。
配置在 application.properties 中:
# 自动创建向量表spring.ai.vectorstore.pgvector.initialize-schema=true方案 B:可选方案 (PostgresML)
如果你希望实现 In-Database Machine Learning,可以使用 PostgresML。它的优势是嵌入计算直接在数据库内部完成,无需将数据传给外部 API。
首先,需要启动 postgresml:
cd adoptionsdocker-compose up -d postgresml配置在 application-postgresml.properties 中:
# 自动在数据库中创建 PostgresML 扩展spring.ai.postgresml.embedding.create-extension=true# 设置向量维度 (与模型匹配)spring.ai.vectorstore.pgvector.dimensions=768如果您希望手动管理数据库架构,或者应用程序的数据库用户没有创建扩展的权限,您可以设置 spring.ai.postgresml.embedding.create-extension 属性为false并手动运行 SQL 命令:
CREATE EXTENSION IF NOT EXISTS pgml;代码中,我们把狗狗信息存入 VectorStore,Spring AI 会自动根据当前配置生成向量:
// 初始化数据repository.findAll().forEach(dog -> { var dogument = new Document("id: %s, name: %s, description: %s".formatted( dog.id(), dog.name(), dog.description() )); vectorStore.add(List.of(dogument)); // 触发向量计算并存储});
// 查询时自动检索QuestionAnswerAdvisor.builder(vectorStore).build()小技巧:想看看向量长什么样?你可以连上数据库看看。
如果使用默认的
postgres服务:docker exec -it postgres psql -U postgres -d mydatabase -c "SELECT id, content, embedding FROM vector_store LIMIT 1;"如果使用
postgresml服务:docker exec -it postgresml psql -U postgresml -d postgresml -c "SELECT id, content, embedding FROM vector_store LIMIT 1;"你会看到
content是狗狗的描述,而embedding是一长串看不懂的数字。
4. 工具调用
概念:模型生活在沙箱里,它没法直接操作现实世界(比如修改数据库、发邮件)。工具调用就是给模型一双手,让它能通过调用函数来改变世界。
在本项目中,我们使用了最新的 MCP (Model Context Protocol) 协议。
什么是 MCP?
模型上下文协议 (MCP)是一个标准化的接口,允许 AI 应用程序和代理:访问外部工具、检索资源、使用提示模板。
MCP 采用客户端-服务器架构:MCP 服务器——提供工具、资源和提示;MCP 客户端——连接到服务器并使用其功能;AI 模型——通过这些客户端与外部世界交互。
Spring AI 提供了包含客户端和服务端组件的 MCP 全面实现,使 AI 功能能够轻松集成到 Spring 应用程序中。
架构图解
在这个项目中,我们模拟了这个架构:
- MCP Server (服务端):即
Scheduler服务。它提供了一个“预约工具”,但它不知道谁会来调它,只管把工具暴露出来。 - MCP Client (客户端):即
Adoptions服务(AI 主体)。它负责连接服务端,获取工具清单,并在 AI 决定使用工具时转发请求。
关键组件与依赖
Spring AI 提供了开箱即用的 Starter,让实现变得非常简单:
- 服务端依赖:
spring-ai-starter-mcp-server-webmvc(支持通过 HTTP/SSE 暴露工具) - 客户端依赖:
spring-ai-starter-mcp-client(支持连接远程 MCP 服务)
代码实现
A. 服务端 (SchedulerApplication.java):暴露工具
在服务端,我们只需要用 @Tool 注解标记一个方法,Spring AI 会自动把它包装成 MCP 工具:
@Componentclass DogAdoptionScheduler {
// 1. 定义工具:描述很重要,AI 靠它来决定何时调用 @Tool(description = "schedule an appointment to pickup or adopt a dog...") String schedule(int dogId, String dogName) { System.out.println("Scheduling adoption for dog " + dogName); // 2. 真正的业务逻辑:计算日期 return Instant.now().plus(3, ChronoUnit.DAYS).toString(); }}并且,通过 MethodToolCallbackProvider 将其注册:
@BeanMethodToolCallbackProvider methodToolCallbackProvider(DogAdoptionScheduler scheduler) { return MethodToolCallbackProvider.builder().toolObjects(scheduler).build();}也可以使用 McpSyncServer 动态注册新工具,可以参考文章《Spring AI 模型上下文协议中的动态工具更新》。
@Beanpublic CommandLineRunner commandRunner(McpSyncServer mcpSyncServer) { return args -> { logger.info("Server: " + mcpSyncServer.getServerInfo()); latch.await();
List<SyncToolSpecification> newTools = McpToolUtils .toSyncToolSpecifications(ToolCallbacks.from(new MathTools()));
for (SyncToolSpecification newTool : newTools) { logger.info("Add new tool: " + newTool); mcpSyncServer.addTool(newTool); } logger.info("Tools updated: "); };}该类McpSyncServer提供工具管理方法:
addTool(SyncToolSpecification)- 添加一个新工具removeTool(String)- 按名称删除工具notifyToolsListChanged()- 通知客户有关工具变更的信息
**注意:**只有在客户端/服务器连接初始化之后,才能添加和/或删除工具。
B. 客户端 (AdoptionsApplication.java):连接工具
在客户端,我们不需要写任何关于“如何预约”的代码,只需要连接到服务端的 URL:
@BeanMcpSyncClient mcpSyncClient() { // 1. 连接到 MCP Server (http://localhost:8081) // 这里使用 SSE (Server-Sent Events) 传输协议,它是 MCP 的标准传输方式之一 var mcp = McpClient.sync(HttpClientSseClientTransport.builder("http://localhost:8081").build()).build(); mcp.initialize(); return mcp;}然后把这个客户端注册给 AI:
this.ai = ai // 2. 告诉 AI:这里有一堆远程工具,你需要时尽管用 .defaultToolCallbacks(new SyncMcpToolCallbackProvider(mcpSyncClient)) .build();这意味着:Adoptions 服务(大脑)不需要知道如何预约,它只需要通过 MCP 协议告诉 Scheduler 服务(手脚):“帮我约一下”。
MCP协议包含一个通知系统,允许服务器将可用工具的变更告知客户端。该通知系统确保客户端始终能够及时了解服务器的功能。
在客户端,Spring AI 提供了检测工具变更并做出反应的机制:
@BeanMcpSyncClientCustomizer customizeMcpClient() { return (name, mcpClientSpec) -> { mcpClientSpec.toolsChangeConsumer(tv -> { logger.info("\nMCP TOOLS CHANGE: " + tv); latch.countDown(); }); };}客户端注册一个监听器,当服务器的可用工具发生变化时,该监听器会被调用。这使得客户端可以:
- 工具添加或移除时收到通知
- 相应地更新其内部状态
- 立即将新工具提供给人工智能模型
5. 结构化输出
概念:通常大模型返回的是一段文本。但在工程化开发中,我们往往需要 JSON 格式的数据(比如把狗狗信息存入前端页面)。
虽然本项目简单地返回了字符串,但 Spring AI 提供了强大的 Structured Output 支持。你可以定义一个 Java Bean(例如 Record),Spring AI 会自动提示模型生成符合该结构的 JSON,并帮你转换回 Java 对象。
// 示例代码(非本项目现有逻辑,仅作演示)record AdoptionRequest(String dogName, String ownerName, LocalDate date) {}
AdoptionRequest request = chatClient.prompt() .user("I want to schedule an appointment to see Prancer next Wednesday") .call() .entity(AdoptionRequest.class); // 直接得到对象!6. 一键切换模型与向量库
Spring AI 最强大的地方在于它的抽象能力。如果你想把 OpenAI 换成阿里的通义千问 (Qwen),或者把 pgvector 换成 PostgresML,只需要改改配置,代码一行都不用动。
场景 A:切换大模型
看看 src/main/resources/application-qwen.properties:
# 换个 Base URLspring.ai.openai.base-url=https://dashscope.aliyuncs.com/compatible-mode# 换个模型名spring.ai.openai.chat.options.model=qwen-plus启动时加上 -Dspring-boot.run.profiles=qwen 即可。
场景 B:切换向量库/嵌入模型
如果你想使用本地的 PostgresML 来计算嵌入并存储,需要先启动 postgresml 容器,让后再应用启动时指定 profile:
./mvnw spring-boot:run -Ppostgresml -Dspring-boot.run.profiles=postgresml这会激活 application-postgresml.properties 中的配置,并使用 Maven profile 中定义的 spring-ai-starter-model-postgresml-embedding。
你的“AI 管家”瞬间就换了一个“大脑”和“记忆方式”。
总结
恭喜你!你已经搭建起了一个具备记忆、知识和行动力的现代 AI 应用。
- PostgreSQL (pgvector/PostgresML) 既是数据库也是 AI 引擎,负责存记忆和算向量。
- Spring AI 像是胶水,把大模型、数据库和外部工具粘合在一起。
- MCP 让你的 AI 可以像人类一样使用工具。
接下来,你可以尝试修改 adoptions/src/main/resources/data.sql 添加更多狗狗,或者在 Scheduler 里增加一个 cancel_appointment(取消预约)的功能,看看 AI 能不能学会新技能!