这两年我最大的一个习惯改变是:每天早上花十五分钟,把一天要做的事、最近在追的目标、脑子里反复绕弯的烦心事,全部倒出来“写完”。但时间一长,我越来越明确地感受到问题所在——信息散得到处都是,日程躺在日历App,待办在任务管理工具里,情绪和碎碎念藏在备忘录里,彼此根本不互通。直到我开始动手搭建自己的 AI 聊天机器人,才意识到完全可以换一种方式:把这些和生活管理相关的事情全部收拢到一个对话入口里,做一个“我的人生我做主”的个人助理。它会替我记着目标、帮我拆解计划、在我情绪低落的时候接住话,而不是又一个只会说“你好,请问有什么可以帮你”的陪聊壳子。
这个项目非常适合三类人:想从零入门 AI 应用开发的开发者,受够了各种效率App割裂、想自己做一套数字生活管家的人,以及单纯好奇“聊天机器人除了问答还能干嘛”的爱好者。下面我会把整个搭建过程彻底摊开讲,从定位、选型、架构到代码、部署、踩坑,尽量做到你照着走也能跑起来。
1. 项目定位:不是"又一个陪聊机器人",而是"人生掌控助理"
1.1 最初的需求:把割裂的工具收拢成一个对话入口
我大概从半年前开始记录自己的生活任务:每周有固定的学习和写作计划,每个月有两个“硬目标”,每天有临时冒出来的琐事。我试过很多工具组合,最终发现效率工具越用越多,负担反而越来越大。
打开手机就是十几个App:日历、待办、笔记、习惯打卡、目标管理、情绪日记……每个工具都解决一个问题,但每个工具都要求我单独维护一套数据。最崩溃的是周复盘的时候,要同时打开四五个窗口才能还原自己这一周到底做了什么。
所以我给这个项目定了一个非常朴素的需求:我不要更多的面板和表单,我要一个能说话的入口。 比如我对机器人说“这周有三天要写东西,帮我安排一下”,它就能直接把我日历里的空闲时间找出来、把写作任务排进去、并且回头在微信或网页上提醒我。这个入口不再是“查询工具”,而是会主动帮我做安排的存在。
1.2 能力清单:这个机器人到底会做什么
为了让项目范围可控,我没有做那种什么都会的全能机器人,而是严格限定在四个能力域里:
- 日程管理:创建、查询、调整我的日程安排,处理“下周三下午有空吗”这类时间问题。
- 目标追踪:记住我在追的年/月/周目标,帮我拆解下一步行动,定期检查进度。
- 任务待办:快速添加待办事项,按优先级排序,过期提醒。
- 情绪陪伴:在我表达焦虑、疲惫、纠结时,不灌鸡汤,而是给出具体的观察和建议。
这四个能力覆盖了“我的人生我做主”这句话最核心的日常场景:有目标、有计划、有行动、有状态波动时的自我调节。更关键的是,这些能力都藏在同一个对话界面里,我不需要记任何指令格式,用大白话就能指挥它。
1.3 一段真实的对话示例
项目跑通后,最典型的一段对话是这样:
text复制我:我这周五有个公开分享,但我现在特别慌,感觉准备时间不够。
机器人:先别急着慌,我给你拆一下:距离周五还有三天。你手头已经有PPT初稿了吗?
我:只有大纲,内容没填。
机器人:那我们把目标定为“完成内容填充+一次全流程试讲”。今天和明天的主要任务不用多,先重点把案例部分补完,周四晚上做一次计时试讲。需要我把这两件事写进日程和待办吗?
我:可以。
机器人:已安排。今天下午3点-5点,任务“补充演讲案例”,优先级高;周四20:00-21:00,任务“全流程试讲”。我会在开始前提醒你。
这段对话里,机器人做了三件事:评估时间、拆解目标、调用工具执行。整个过程没有出现“请问你想选择哪个功能”之类的废话。这就是我想要的体验——它像一个真正了解我生活节奏的助理,而不是一个呆板的命令执行器。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 技术选型:为什么是 Ollama + Spring AI + 向量库
2.1 大模型怎么选:云端 API 和本地模型的取舍
搭建 AI 聊天机器人,第一个绕不开的问题就是大模型用谁的。我当时的候选方案有三类:
- 通用云端大模型 API:比如各种国内外商业模型的 API。优点是开箱即用、模型能力强、生态完善;缺点是个人隐私数据要交给第三方,长期使用按量付费也是一笔开销。
- 本地开源模型:通过 Ollama 跑 Qwen2.5、Llama 3.1 这类开源模型。优点是离线可用、数据完全本地、免费;缺点是对电脑配置有要求,模型能力上限和商业大模型有差距。
- 混合方案:日常聊天和工具调用走本地模型,遇到复杂逻辑或长文本总结时切云端 API。
最后我选了混合方案,但主路径走本地模型。原因很简单:这个机器人会记录我的日程、目标、情绪状态,这些数据太私密了,我不希望它们离开自己的设备。同时现在本地开源模型的指令跟随和工具调用能力已经足够日常使用,没必要为每次聊天额外付费。
2.2 为什么用 Spring AI 而不是 LangChain
聊到大模型应用开发框架,很多人第一反应是 Python 的 LangChain 或 LlamaIndex。但考虑到我对 Java 技术栈更熟,而且热词里也出现了 spring ai,我仔细对比了两类框架的真实差异:
| 对比维度 | Spring AI | LangChain |
|---|---|---|
| 语言生态 | Java / Kotlin 等 JVM 语言 | Python 优先 |
| 学习曲线 | 对后端开发者友好,依赖注入、模块化成熟 | 概念多,链条抽象层次较高 |
| 工具调用(Function Calling) | 通过 @Bean 注册函数,与 Spring 深度整合 |
通过 @tool 装饰器,写起来更简练 |
| 记忆管理 | 提供独立的 ChatMemory 抽象 | 提供多种 Memory 实现 |
| 最适合的场景 | 企业内部工具、需要和现有 Java 服务集成的场景 | 快速原型、数据类研究、Python 生态应用 |
我实践下来的体会是:如果你本来就是 Java/Spring 背景,没必要为了做一个工具型聊天机器人去切换技术栈。Spring AI 的抽象做得挺清楚,接入 Ollama、OpenAI、通义等模型都只需要改配置,这正好符合“工具型机器人”的定位——核心逻辑在服务端,模型只是其中的一个能力提供者。
2.3 记忆方案:为什么需要向量数据库来管长期记忆
聊天机器人最容易被吐槽的一点就是“记性差”。如果你只在接口层面把用户消息发给模型,那模型天然不保存任何对话状态,每次请求都是“失忆”的。
短期记忆我用内存里的消息列表直接解决,每次请求把最近 N 轮对话拼进上下文。但“我的人生我做主”这个场景还有长期记忆:上周定下的目标、上周记录的情绪状态、喜欢的工作时间安排,等等。这些如果全部塞进上下文,既浪费 tokens,又容易让模型被无关信息干扰。
所以长期记忆我用向量数据库来存储。方案对比下来,我倾向于用 Chroma 或者 pg vector:
| 向量库 | 部署方式 | 优点 | 缺点 |
|---|---|---|---|
| Chroma | 轻量级,可以进程内启动 | 零运维,适合单机个人项目 | 大数据量性能一般 |
| Qdrant | Docker 独立部署 | 性能好,支持过滤 | 需要单独维护服务 |
| pgvector | 作为 PostgreSQL 插件 | 能复用关系数据,事务能力强 | 需要先有 PostgreSQL 环境 |
| Milvus | 独立部署,分布式 | 适合大规模场景 | 个人项目重度超配 |
我最终选了 Chroma,因为个人项目数据量不大,进程内启动最省事。后面如果你想把历史对话和日程安排全部做成可查询的知识库,迁移到 Qdrant 或 pgvector 也不难。
3. 架构与数据流:一条消息从发出到回复经历了什么
3.1 模块划分:不要把所有逻辑都塞进一个 Controller
整个机器人被我拆成了四个模块,分开部署在同一个 Java 进程中:
- 接入层:负责接收 Web 请求(HTTP/SSE),解析对话消息,把用户的自然语言转成内部指令。
- 记忆层:管理短期对话历史和长期向量记忆,给大模型提供对话背景。
- 推理层:调用大模型完成生成,包括意图判断、工具调用决策、回复内容合成。
- 执行层:把模型决定执行的工具动作落地,比如写日历、建待办、查时间等。
模块拆分的价值在项目中期非常明显:如果发现记忆有问题,只改记忆层;如果模型输出质量不好,只调推理层的 Prompt 和后处理逻辑;如果需要加一个“天气查询”工具,只在执行层加一个 Bean。这些改动彼此隔离,不会出现改一个功能牵连整个服务的情况。
3.2 核心数据流:一次"帮我安排演讲准备"的完整链路
当用户发来一条消息,服务端发生的事情是:
- 接入层收到原始文本,先把文本和用户ID绑定,准备进入处理流程。
- 记忆层把最近 N 轮对话历史取出来,同时去向量库检索跟“演讲”“准备”相关的备注或历史记录,拼装成上下文。
- 推理层把拼接好的上下文连同系统 Prompt 一起发给大模型。大模型判断:用户需要目标拆解,并且可能涉及日程创建。
- 大模型发出工具调用请求,比如
create_schedule(date="2025-06-12", task="补充演讲案例", priority="high")。 - 执行层对这个调用做校验,防止非法日期或重复创建,然后写入日历和待办。
- 执行层把工具执行结果返回给推理层,大模型生成最终的自然语言回复。
- 回复内容同时写入短期记忆和向量库,再返回给用户。
这个链路每一步都有可能出问题:比如模型生成了一个不存在的工具名,或者工具执行失败后模型没有感知到。所以我在执行层加了一个“执行结果回填”机制,确保模型知道自己调用的工具到底成功没有。
3.3 短期记忆和长期记忆的分工
我踩过一个很典型的坑:一开始把“所有历史对话”都塞给模型,结果模型只回复最近一句,前面的指令全被淹没了。后来我把记忆分成两层:
- 短期记忆:只保存最近10轮对话,放在进程内存里,每轮对话按用户ID隔离。它的作用是维持当前对话的连贯性,比如“刚才说的那个安排”能接得住。
- 长期记忆:把每次对话的关键结论、用户目标、状态变化,用向量化方式存进 Chroma。当新对话触发相关主题时,通过相似度检索召回最相关的几条记忆,拼进上下文。
这里有个重要的取舍:长期记忆不是越多越好。我一开始召回 top 10 条,结果模型经常被旧信息带偏,反而忽略了用户当前这句才是优先级最高的。后来我改成召回 top 3,并且把当前用户消息放到 Prompt 的最末尾,输出质量明显提升。
4. 动手实现:从环境准备到核心功能跑通
4.1 第一步:本地部署大模型,用 Ollama 跑起来
本地模型我用的是 Ollama + Qwen2.5 7B。Ollama 的安装非常简单,Mac 和 Linux 可以直接下载安装包或使用一行脚本,Windows 也有对应的安装包。
装好之后先确认版本:
bash复制ollama --version
然后下载模型。这里我强烈建议先拉一个 7B 级别的模型,比如 qwen2.5:7b。7B 模型在指令跟随、中文理解、工具调用三个维度上表现比较均衡,而且量化后大约 4.7GB 左右,多数 16GB 内存的电脑能跑得动:
bash复制ollama pull qwen2.5:7b
跑一个简单测试:
bash复制ollama run qwen2.5:7b "你好,请用一句话介绍一下你自己"
看到正常输出后,Ollama 就绪。这里提示一句:Ollama 默认监听 11434 端口,后面 Spring AI 默认会连这个端口,别改掉。
4.2 第二步:创建 Spring Boot 工程并接入 Spring AI
我用的是 Spring Boot 3.2 + Spring AI 1.0.0-M 系列版本。由于 Spring AI 版本迭代很快,后续 API 可能有微调,但整体接入思路没变。
在 pom.xml 里引入依赖:
xml复制<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-ollama-spring-boot-starter</artifactId>
<version>1.0.0-M6</version>
</dependency>
然后在 application.yml 里做基础配置:
yaml复制spring:
ai:
ollama:
base-url: http://localhost:11434
chat:
model: qwen2.5:7b
写一个最基础的聊天接口,先把对话链路跑通:
java复制@RestController
@RequestMapping("/api/chat")
public class ChatController {
private final ChatClient chatClient;
public ChatController(ChatClient.Builder builder) {
this.chatClient = builder
.defaultSystem("你叫小主,是我的私人生活助理。你的职责是帮我管理日程、跟踪目标、梳理情绪。你的风格是直接、温暖、逻辑清晰。")
.build();
}
@PostMapping
public String chat(@RequestBody String message) {
return chatClient.prompt()
.user(message)
.call()
.content();
}
}
启动应用后用 curl 测一下:
bash复制curl -X POST http://localhost:8080/api/chat \
-H "Content-Type: text/plain" \
-d "我今天状态不太好,应该怎么办"
能收到模型回复,说明从 Spring Boot 到 Ollama 的通路已经打通。
4.3 第三步:实现带记忆的聊天接口
基础接口能对话,但它是失忆的。我加了一个简单的 ConversationMemoryService,用内存 Map 存每个用户最近 10 轮对话:
java复制@Service
public class ConversationMemoryService {
private final Map<String, Deque<Message>> memoryStore = new ConcurrentHashMap<>();
public List<Message> getRecentMessages(String userId, int maxMessages) {
Deque<Message> deque = memoryStore.computeIfAbsent(userId, k -> new ArrayDeque<>());
return deque.stream()
.skip(Math.max(0, deque.size() - maxMessages))
.toList();
}
public void addMessage(String userId, Message message) {
memoryStore.computeIfAbsent(userId, k -> new ArrayDeque<>()).addLast(message);
if (memoryStore.get(userId).size() > 20) {
memoryStore.get(userId).removeFirst();
}
}
}
然后在 ChatClient 的调用里把历史消息带上:
java复制public String chatWithMemory(String userId, String userMessage) {
List<Message> history = memoryService.getRecentMessages(userId, 10);
// 把历史消息按角色拼成 Prompt
Prompt prompt = buildPromptWithHistory(history, userMessage);
return chatClient.prompt(prompt).call().content();
}
这里有个非常实用的经验:模型上下文里不要只放原始历史,要把最后一条用户消息在 Prompt 里再单独强调一次。 本地小模型特别吃这一套,注意力一下就能聚焦到当前问题上。
4.4 第四步:教机器人调用工具,把日程和待办落进真实存储
聊天机器人和“人生管理”结合的关键,在于它能把对话变成实际动作。我这里借用了大模型的 Function Calling 能力。
先定义一个创建日程的请求类型:
java复制public record CreateScheduleRequest(String date, String time, String task, String priority) {}
再用 Spring AI 的函数回调机制注册一个工具:
java复制@Component
public class ScheduleTools {
@Bean
public FunctionCallback createScheduleCallback() {
return FunctionCallback.builder()
.description("把用户说的任务创建到日程和待办清单,需要日期、时间、任务内容和优先级")
.function("create_schedule", (CreateScheduleRequest request) -> {
if (request.date() == null || request.task() == null) {
return "参数不完整:必须提供日期和任务内容";
}
// 写入自己的存储逻辑,比如 MySQL 或文件
scheduleRepository.save(request);
return "已创建日程:" + request.task();
})
.inputType(CreateScheduleRequest.class)
.build();
}
}
然后在构建 ChatClient 时把这个工具注册进去:
java复制this.chatClient = builder
.defaultSystem(...)
.defaultFunctions("create_schedule")
.build();
当用户说“安排周五晚上八点做演讲试讲”,模型会判断出需要调用 create_schedule,生成一个 JSON 参数,Spring AI 回调到我们的方法,存储完成后把结果返回给模型,模型再组织自然语言回复。
这一步是整个项目里最容易出问题的地方。我遇到过模型编造日期(比如 2025 年 6 月 31 日这种不存在的日期),也遇到过模型调用工具失败后直接硬编一个“已安排”的假回复。解决办法是在函数回调里做严格校验,并且在系统 Prompt 里明确写:只有工具执行成功后才能对用户说“已安排”。
4.5 第五步:Prompt 工程,让机器人有稳定的"人设"
聊到聊天机器人,不能只说模型,还得说 Prompt。同一个 Qwen2.5 模型,用不同的系统 Prompt,效果可以是天差地别。
我调试了很多版,最终沉淀出一个比较稳定的系统 Prompt 模板:
text复制你叫小主,是我的私人生活助理。
你的核心原则:
1. 目标导向:每当用户提到目标、计划、焦虑时,先把事情拆成可执行的小步骤。
2. 工具优先:如果用户的需求涉及日程、待办、提醒,必须调用对应工具,不能只是口头答应。
3. 不说废话:回复控制在100字左右,除非用户要求详细解释。
4. 情绪支持:用户表达负面情绪时,先具体化情绪来源,再给一到两个可行的行动建议,不要空洞安慰。
5. 边界清楚:如果不知道,就直接说不知道,不编造事实。
个人背景:
- 用户经常在下午3点到5点写文章,这个时间段精力最集中。
- 用户通常周三下午有团队会议。
那个“个人背景”部分是我从向量库里检索出来的长期记忆,每次请求动态拼进去。这个组合效果非常好:模型知道什么时候该动手(调工具),什么时候该动嘴(聊天)。
5. 部署与使用:让它 7x24 小时常驻
5.1 本地跑 vs 云服务器跑
项目开发阶段我一直跑在本地 Mac 上,内存 16GB,同时跑 Ollama 和 Spring Boot 应用没什么压力。但本地跑有几个问题:机器不能关机、外网访问不方便、占用开发机资源。
所以我后来把机器人部署到了一台云服务器。配置建议是:16GB 内存起步,如果是 7B 模型,纯 CPU 推理也能出结果,但速度偏慢;有 GPU 最好,显存 6GB 以上就跑得比较舒服。
如果不想为 GPU 买单,可以考虑两个思路:一是继续用纯 CPU 推理,把模型量化级别调整为 Q4_K_M,接受 10 秒左右的回复延迟;二是做混合路由,把更重的总结、规划任务交给云端大模型,日常闲聊走本地。
5.2 Docker Compose 一键编排
为了部署省心,我用 Docker Compose 把 Ollama 和应用服务编排在一起:
yaml复制version: '3.8'
services:
ollama:
image: ollama/ollama:latest
container_name: ollama
restart: always
ports:
- "11434:11434"
volumes:
- ollama_data:/root/.ollama
environment:
- OLLAMA_NUM_PARALLEL=1
- OLLAMA_MAX_LOADED_MODELS=1
chatbot:
build: .
container_name: chatbot
restart: always
ports:
- "8080:8080"
depends_on:
- ollama
volumes:
ollama_data:
首次部署时需要先拉模型:
bash复制docker compose up -d
docker exec -it ollama ollama pull qwen2.5:7b
我用 OLLAMA_NUM_PARALLEL=1 限制了并发数,避免多个请求同时进来把 CPU 打满。个人使用场景下并发本来就不高,反而稳定更重要。
5.3 性能调优:让普通机器也能流畅跑
如果你的机器内存只有 16GB,下面几条经验直接抄:
- 用 Q4_K_M 量化版本模型。Ollama 默认拉取的就是合适量化版本,不要刻意去换更大的量化,内存收益远大于那一点点精度损失。
- 设置上下文长度上限。在 Spring AI 的配置里限制
max-tokens和上下文窗口,避免模型无脑生成很长内容。 - 开启流式输出。把 HTTP 接口改成 SSE 流式返回,用户就能看到逐字输出,感知延迟能降低一个量级。
SSE 流式返回的接口大致是:
java复制@PostMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> streamChat(@RequestBody String message) {
return chatClient.prompt().user(message).stream().content();
}
前端用 EventSource 或 fetch + ReadableStream 就能接入,体验和官方聊天助手几乎一致。
5.4 手机随时访问的配置
机器人部署到云服务器后,我想在手机上随时打开用。最简单的方式是在服务器上用 Nginx 做反向代理,把域名或公网 IP 的 /api 路径代理到 8080 端口。同时把端口限制一下,只开放 443,不给其他人随便扫到 8080。
再往前进阶一点,可以套一层简单的 HTTP Basic Auth 或 Token 校验,保证只有自己能调用接口。个人项目用 Bearer Token 就够了,不用上完整的 OAuth 体系。
6. 两周实测与避坑记录:那些文档里不会写的事
6.1 实测一周的效果,和它的局限性
整个机器人跑了两周后,我最明显的感受是:“人生管理”真正有价值的地方不在聊天本身,而在“可追溯”。 我把一周的日程、待办、情绪记录全部沉淀下来之后,做周复盘时只需要问机器人“这周我完成了哪些目标”,它就能从向量库里检索历史,拼出一个还不错的周报。这个体验远好于我手动翻四个 App 做统计。
但它也有很明显的局限:
- 小模型的理解能力确实有限。 遇到绕口的、隐含多个前提的长句,Qwen2.5 7B 偶尔会答非所问。我的对策是让用户把复杂需求拆成几个短句,效果立刻改善。
- 工具调用的稳定性不是百分百。 即使做了校验,服务器上仍有约 3% 的调用会生成格式错误,需要做重试和兜底。
- 长期记忆的召回有时不够聪明。 向量相似度不等于语义相关性,偶尔会把不相关的旧目标拼进来。解决方法是只在“用户主动提到某件事”时才触发长期记忆检索,而不是每次请求都无条件召回。
6.2 踩过的坑汇总
| 坑 | 具体表现 | 解决办法 |
|---|---|---|
| 上下文被历史淹没 | 用户发“帮我安排”时,模型去回答历史里某句话 | 历史只保留最近10轮,并把用户当前消息在Prompt末尾单独强调 |
| 模型假报执行成功 | 工具调用失败后,模型仍回复“已安排” | 系统Prompt明令“只有工具返回成功才能说已安排”,并执行结果回填 |
| 日期格式混乱 | 模型生成"2025年6月32日"这类非法日期 | 在函数回调里做严格日期校验,非法则返回错误信息让模型重新生成 |
| Ollama 服务无故挂掉 | 应用跑着跑着模型没响应,11434端口不通 | 部署时加 restart: always,写一个健康检查脚本自动拉起 |
| Docker 资源不足 | 同时跑 Ollama 和 MySQL 时机器变卡 | 限制并发、降低模型量化级别、非必要服务不常驻 |
| 长期记忆干扰当前指令 | 检索到的旧目标影响当前回答 | 优先召回 top 3,并且把“当前用户消息”权重调高 |
6.3 还能怎么扩展:从聊天机器人到 Agent 工作流
项目目前的形态还只是一个“能够调用工具的聊天机器人”,离真正的 AI Agent 还有距离。我下一阶段的计划是:
- 给机器人增加主动提醒能力。每天早晨九点,让它根据日程和本周目标生成当天的三件要事,通过服务号或 Webhook 推送到手机。
- 增加目标复盘工作流。每周日晚自动扫描本周的日程完成情况,生成偏差报告,帮我调整下周计划。
- 增加更自然的语音入口。现在很多开源方案已经支持本地语音识别和语音合成,加上之后就跟真人助理更像了。
就我自己的实际体验来看,这个项目最有价值的地方不是“创造了一个 AI”,而是让我真正理解了一条完整的 AI 应用链路:本地模型部署、上下文管理、工具调用、记忆检索、部署运维。每一步的坑都在文档之外,需要亲手踩过才记得住。如果你也打算做类似的东西,建议不要一上来就追求功能复杂,先把“对话→理解→调用工具→回复”这一条链路跑透,再往里加目标管理、记忆检索这些进阶能力。项目不怕小,就怕链路不完整。
