Skip to content

零基础实战:用 Spring AI 写一个会“思考”的狗狗领养助手

2025-11-13

你是否想过,如何让 AI 不仅仅是陪聊,而是真正变成一个**“有记性、懂知识、能办事”**的智能助手?

今天,我们将一起构建一个**“狗狗领养助手”**。别被“AI”这个词吓到,其实它的原理就像教一个小孩子做事一样简单。

我们将用最通俗易懂的方式,带你从零开始,一步步把这个助手跑起来,并理解它背后的“大脑构造”。

核心概念

在开始写代码之前,我们先打个比方。一个能干活的 AI 助手,其实是由三个部分组成的:

  1. 记忆 (Memory):就像人的海马体

    • 问题:如果你问 AI “它几岁了?”,AI 得知道“它”指的是上一句聊到的那只狗。
    • 解法:我们需要把聊天记录存起来,每次说话时都带上之前的上下文。
  2. 知识 (RAG - 检索增强生成):就像人的大脑皮层(存书本知识)。

    • 问题:AI 的模型是半年前训练的,它不知道今天刚来了一只叫 Prancer 的新狗。
    • 解法:我们把新狗的资料存在数据库里。当用户问起时,先去数据库查,查到了再告诉 AI。
  3. 手脚 (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.git
cd spring-ai-dog-adoption-showcase

2. 启动数据库

本项目默认使用 PostgreSQL + pgvector 作为向量数据库,同时也支持可选的 PostgresML

cd adoptions
docker-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: 如果你用 OpenAI
export 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 服务。

  1. 你会看到 Scheduler 的终端里打印出:Scheduling adoption for dog Prancer
  2. 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

代码配置如下:

@Bean
PromptChatMemoryAdvisor 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 adoptions
docker-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 工具:

@Component
class 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 将其注册:

@Bean
MethodToolCallbackProvider methodToolCallbackProvider(DogAdoptionScheduler scheduler) {
return MethodToolCallbackProvider.builder().toolObjects(scheduler).build();
}

也可以使用 McpSyncServer 动态注册新工具,可以参考文章《Spring AI 模型上下文协议中的动态工具更新》

@Bean
public 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:

@Bean
McpSyncClient 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 提供了检测工具变更并做出反应的机制:

@Bean
McpSyncClientCustomizer customizeMcpClient() {
return (name, mcpClientSpec) -> {
mcpClientSpec.toolsChangeConsumer(tv -> {
logger.info("\nMCP TOOLS CHANGE: " + tv);
latch.countDown();
});
};
}

客户端注册一个监听器,当服务器的可用工具发生变化时,该监听器会被调用。这使得客户端可以:

  1. 工具添加或移除时收到通知
  2. 相应地更新其内部状态
  3. 立即将新工具提供给人工智能模型

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 URL
spring.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 能不能学会新技能!