乐于分享
好东西不私藏

一行代码让 Java 应用拥有 AI 记忆:自动向量检索,单机就能跑

一行代码让 Java 应用拥有 AI 记忆:自动向量检索,单机就能跑

AI Agent 的记忆问题,你遇到过吗

最近我在搭一个多轮对话的 Agent,有个需求是让它记住每个用户的历史偏好,下次对话时能直接调用。听起来很简单,真做起来发现事情不小。

第一个冲动是上 Chroma 或者 Milvus。但问题来了:本地开发要单独跑一个向量库进程,测试环境要多维护一个服务,上生产还要考虑高可用。对于一个中型应用来说,这个开销有点不成比例——业务核心不在这里,但为了存几万条记忆,我得多养一个数据库?

退而求其次,有人说用 Redis 存 JSON + 全文检索。勉强能用,但语义检索就别想了,用户说「上次聊的那个界面风格问题」,关键词搜不到任何东西。

这个问题肯定不止我一个人遇到过。


RogueMap 是什么

RogueMap 是一个 Java 嵌入式存储引擎,作者 bryan31,就是 LiteFlow 那个人。

LiteFlow 在国内 Java 规则引擎这块有点名气,这个背景我觉得是加分项,至少不是野路子项目。

它的核心思路是:数据不放 JVM 堆,走 mmap(内存映射文件)。mmap 是操作系统级别的文件映射机制,读写数据直接操作内存地址,OS 负责和磁盘同步,绕过了 JVM 的 GC 管辖范围。

这个设计带来几个直接结果:

  • • JVM 堆占用几乎不涨,Full GC 压力消失
  • • 进程重启后数据还在,因为底层是文件
  • • 可以存比物理内存大得多的数据集(只要磁盘够)

它暴露的 API 是四种数据结构:RogueMap(键值)、RogueList(双向链表)、RogueSet(集合)、RogueQueue(队列),写法和 Java 标准集合很像,迁移成本低。

1.0 版本发出来之后,社区里问 AI 相关需求的声音挺多,Agent 记忆和 RAG 这两块尤其集中。1.1.0 这次更新,基本上就是冲着这块来的。


1.1.0 版本一共更新了哪些东西

这次不是小打小闹。除了新增 AI 记忆层,基础设施层面也有几个改动,而且连依赖结构都拆重新组了。列一下:

特性
说明
RogueMemory AI 记忆层
新增,HNSW 向量检索 + BM25 关键词混合召回
TTL 过期
新增,四种数据结构全支持,惰性清理
自动检查点
新增,可按时间或写操作次数触发 checkpoint
自动扩容
新增,mmap 文件空间不够时自动按倍数扩
超低堆内存索引
新增,100 万条 key 的索引从 100MB 降到 0.02MB
复杂泛型支持
修复,List<User> 这类带泛型的类型序列化不再出错

我自己用下来感受是:TTL 这个功能其实等了挺久,之前 RogueMap 没有 TTL,做缓存场景必须搭 Redis,这下可以省掉了。自动扩容对 AI 记忆来说也很实用,记忆条数很难提前估,老版本写满直接抛异常,那体验实在不好。


RogueMemory 到底怎么工作

这块值得多说几句。

RogueMemory 是 1.1.0 新增的模块,Maven 坐标是 roguemap-memory,引入之后会自动把 roguemap-embedding 也带进来。它做的事情就一件:把向量检索能力直接嵌进你的 Java 进程,不需要任何外部服务。

检索架构

内部走的是混合检索策略:

  • • 向量路:用 HNSW 算法做近似最近邻搜索(ANN),对语义相似度敏感
  • • 关键词路:用 BM25 算法,对精确词汇匹配更准
  • • 融合:两路结果通过 RRF(Reciprocal Rank Fusion,倒数排名融合)合并排序

这个组合不算新鲜——Elasticsearch 8.x 的混合检索也是类似思路——但能在一个嵌入式 Java 库里开箱即用,我觉得还是挺有价值的。

三种模式可以按需选:

  • • HYBRID:向量 + BM25 双路,RRF 融合,默认推荐
  • • VECTOR_ONLY:纯语义向量检索
  • • KEYWORD_ONLY:纯 BM25,这个模式下甚至不需要接 Embedding 服务

基本用法

RogueMemory mem = RogueMemory.mmap()    .persistent("data/mem")    .searchMode(SearchMode.HYBRID)    .embeddingProvider(new UniversalEmbeddingProvider(apiKey))    .build();// 存一条记忆mem.add("用户XXX是一名Java开发者,生成代码的语言要采用Java而不是其他");// 语义检索,取最相关的 5 条List<MemoryResult> results = mem.search("用户偏好", 5);// 支持 namespace 做隔离,metadata 附加业务标签mem.add("上次对话读取了一个抢票业务逻辑",        Map.of("session", "abc123"),        "chat_history");mem.close();

几个细节需要注意一下:

  • • 持久化路径 persistent("data/mem") 指向本地文件夹,里面会生成 mmap 文件,进程重启后记忆完整保留
  • • namespace 参数可以做多租户或多会话的隔离,不同 namespace 的数据互不干扰
  • • mem.close() 要记得调,负责把内存中的索引刷到磁盘

自动检查点 + 自动扩容,AI 记忆场景都支持

这两个特性对 RogueMemory 来说特别有用。记忆条数很难预估,开启自动扩容之后空间不够会自动翻倍,业务代码完全感知不到:

RogueMemory mem = RogueMemory.mmap()    .persistent("data/mem")    .autoExpand(true)            // 开启自动扩容    .autoCheckpoint(10, TimeUnit.MINUTES)  // 每 10 分钟自动 checkpoint    .autoCheckpoint(5_000)       // 或每 5000 次写操作触发一次    .embeddingProvider(new UniversalEmbeddingProvider(apiKey))    .build();

两个 autoCheckpoint 条件可以同时配,哪个先满足哪个先触发,后台守护线程跑,不阻塞业务。


Embedding 服务怎么接

RogueMemory 做向量化的模块叫 UniversalEmbeddingProvider,对接标准是 OpenAI 的 /v1/embeddings 接口。

这个选择我觉得很务实。OpenAI 这套接口已经成了事实标准,国内外大多数 Embedding 服务都实现了兼容,直接复用意味着开发者几乎零额外学习成本。而且实现上没有引任何第三方 HTTP 库,用的是原生 HttpURLConnection,零额外依赖。

下面是目前支持的服务全景:

服务
类型
接入方式
OpenAI
海外云服务
new UniversalEmbeddingProvider(apiKey)
阿里云百炼
国内云服务
改 baseUrl + apiKey + model
智谱 GLM
国内云服务
同上
Moonshot / Kimi
国内云服务
同上
Mistral
海外云服务
同上
Jina AI
海外云服务
同上
Ollama
本地部署
baseUrl
 改为 http://localhost:11434/v1
vLLM / LocalAI
本地/私有化
改 baseUrl 即可

切换服务只需要改三个参数:baseUrlapiKeymodel,向量维度不需要手动写,第一次调用时自动探测。

我个人倾向于在生产环境用国内服务(延迟更低、不用考虑网络问题),本地开发就跑 Ollama,这样开发环境完全离线,不消耗 token。两套配置来回切换的成本几乎可以忽略。


实战

接入阿里云百炼,从拿到 API Key 到跑通检索

我选百炼主要有两个原因:text-embedding-v3 这个模型的效果在中文语料上比 OpenAI 的 text-embedding-ada-002 好一些,而且价格便宜很多(0.0007 元 / 1k Token),新用户还有 100 万 Token 的免费额度可以用。

Step 1:获取 API Key

登录阿里云百炼控制台,在 API-KEY 管理页面创建一个 Key。

Step 2:Maven 依赖

<!-- 核心存储引擎 --><dependency>    <groupId>com.yomahub</groupId>    <artifactId>roguemap-core</artifactId>    <version>1.1.0</version></dependency><!-- AI 记忆模块(会自动依赖 roguemap-embedding) --><dependency>    <groupId>com.yomahub</groupId>    <artifactId>roguemap-memory</artifactId>    <version>1.1.0</version></dependency>

Step 3:初始化 RogueMemory,指向百炼

import com.yomahub.roguemap.memory.RogueMemory;import com.yomahub.roguemap.memory.SearchMode;import com.yomahub.roguemap.embedding.UniversalEmbeddingProvider;public class AgentMemoryDemo {    public static void main(String[] args) throws Exception {        String apiKey = "sk-xxxxxxxxxxxxxxxxxxxxxxxx"; // 替换成你的百炼 API Key        String baseUrl = "https://dashscope.aliyuncs.com/compatible-mode/v1";        String model = "text-embedding-v3";        RogueMemory mem = RogueMemory.mmap()            .persistent("data/agent_memory")   // 持久化目录,首次运行自动创建            .searchMode(SearchMode.HYBRID)       // 混合检索模式            .autoExpand(true)                    // 记忆增长时自动扩容            .autoCheckpoint(5, TimeUnit.MINUTES) // 每 5 分钟刷一次磁盘            .embeddingProvider(                new UniversalEmbeddingProvider(baseUrl, apiKey, model)            )            .build();        // 写入几条记忆        mem.add("用户倾向于使用深色主题,讨厌自动播放的视频广告");        mem.add("用户上次会话询问了 Spring Boot 3 迁移的问题",                Map.of("topic", "spring"), "tech_pref");        mem.add("用户反馈说目前的表单交互太繁琐,希望减少步骤");        // 语义检索:查跟「界面体验」相关的历史偏好        List<MemoryResult> results = mem.search("界面和交互体验", 3);        for (MemoryResult r : results) {            System.out.println("[score=" + r.getScore() + "] " + r.getContent());        }        mem.close();    }}

几个实际踩坑点

1. 模型维度

百炼的 text-embedding-v3 支持多档维度(1024 / 768 / 512 等),不填默认是 1024。UniversalEmbeddingProvider 会在第一次调用时自动探测维度,后续写入的向量全部对齐这个维度,中途不能换。如果要切换维度,需要清空并重建 data/agent_memory 目录。

2. 本地开发时的网络

百炼在国内访问没问题,但如果你本地有某些代理设置,注意 HttpURLConnection 会走系统代理,有时候会报超时。本地测试可以临时换成 Ollama,逻辑完全一样,只是 baseUrl 改成 http://localhost:11434/v1,model 改成 nomic-embed-text(Ollama 不校验 apiKey,填什么都行)。

3. namespace 的用法

如果你的应用要同时服务多个用户,强烈建议用 namespace 做隔离,按 userId 或 sessionId 来。不然不同用户的记忆会混在一起,检索结果会出现串数据。

// 按用户 ID 隔离mem.add("喜欢科幻类内容", Map.of(), "user_" + userId);List<MemoryResult> userResults = mem.search("内容偏好", 5, "user_" + userId);

4. 关于费用

每次 mem.add() 都会调用一次 Embedding API,百万 Token 的免费额度大概够存 50~100 万条短记忆。生产环境要注意控制写入频率,避免每轮对话都写一堆冗余数据进去。


什么情况下别用它

这个我觉得还是得说清楚,不然就成了广告稿了。

如果你已经有独立的向量数据库:Milvus 或者 Qdrant 已经在跑了,RogueMemory 不会比它们更好,集群化、分布式查询、元数据过滤等高级特性是嵌入式方案做不到的。

如果你的记忆需要跨多实例共享:RogueMemory 是单进程嵌入式,数据文件绑定在某台机器上。多个 Java 实例要共享同一份记忆,不行。

如果你用 lowHeapIndex 开了超低内存索引:这个模式下事务操作不支持,对一致性要求高的场景要注意。

数据量极大的场景:HNSW 的构建和查询在亿级向量规模下性能会明显下降,这个体量还是应该上专业向量库。

我自己的判断是:中小型 AI 应用、原型项目、单机部署的 Agent 服务,这是 RogueMemory 最舒服的使用区间


快速上手和资源汇总

最后汇一下依赖坐标和项目资源。

基础依赖(只用存储结构,不用 AI 记忆):

<dependency>    <groupId>com.yomahub</groupId>    <artifactId>roguemap-core</artifactId>    <version>1.1.0</version></dependency>

AI 记忆模块(会自动带上 roguemap-embedding):

<dependency>    <groupId>com.yomahub</groupId>    <artifactId>roguemap-memory</artifactId>    <version>1.1.0</version></dependency>

注意:1.1.0 的依赖结构和之前版本相比有变化,升级时先把旧依赖清掉再加新的,不然可能出现冲突。

开源地址:

https://github.com/bryan31/RogueMap

我有时候觉得,国内开源生态里其实不缺想法,缺的是把一个具体痛点做到够用的工程意志。RogueMap 这次更新的方向对了——不去和 Milvus 抢生态,只是把「Java 进程里就能跑的记忆层」这件事做扎实。对我这种不想为几万条记忆多维护一套服务的人来说,它刚好够用。