跳转到内容

JeecgBoot 记忆库即插件架构

JeecgBoot 记忆库即插件架构

学习目标

通过本章学习,你将能够:

  • 理解记忆库与知识库的区别
  • 掌握记忆库转换为 MCP 插件的实现机制
  • 了解 add_memory / query_memory 工具的定义与自动触发策略
  • 了解可插拔记忆在多 AI 应用中的管理方案

前置知识

本章涉及记忆系统的通用原理,建议先阅读:

下文假设你已理解上述概念,直接聚焦 JeecgBoot 的具体实现。


项目实践

记忆库 vs 知识库

在 JeecgBoot 中,记忆库和知识库共享同一套底层存储(AiragKnowledge 表),通过 type 字段区分:

维度知识库(knowledge)记忆库(memory)
用途领域知识检索(RAG)用户个人信息持久化
检索方式向量相似度检索工具调用(add/query)
更新频率低(管理员上传文档)高(AI 自动检测并追加)
内容类型文档、网页、FAQ用户偏好、历史交互记录

记忆库转插件

记忆库通过 getPluginMemory() 方法转换为标准的 MCP 插件格式:

// 伪代码:getPluginMemory
Map<String, Object> getPluginMemory(String memoryId) {
AiragKnowledge knowledge = knowledgeMapper.selectById(memoryId);
if (knowledge == null) return null;
// 构建 AiragMcp(插件格式)
AiragMcp tool = new AiragMcp();
tool.setId(memoryId);
tool.setName("memory");
tool.setDescr("用户长期记忆管理");
tool.setCategory("plugin");
// 定义两个工具:add_memory 和 query_memory
JSONArray toolsArray = new JSONArray();
toolsArray.add(buildAddMemoryTool(memoryId, knowledge.getDescr()));
toolsArray.add(buildQueryMemoryTool(memoryId, knowledge.getDescr()));
tool.setTools(toolsArray.toJSONString());
// 元数据(认证 + 租户)
tool.setMetadata(buildMetadata(tenantId));
// 转换为 ToolSpecification + ToolExecutor
Map<ToolSpecification, ToolExecutor> tools = PluginToolBuilder.buildTools(tool, request);
return Map.of("pluginTool", tools, "pluginId", memoryId);
}

关键设计:转换后的记忆库在 AI 眼中就是一个普通的 MCP 插件。LLM 通过工具调用的方式读写记忆,而非通过内置的记忆模块。

add_memory 工具定义

{
"name": "add_memory",
"description": "【自动触发】向记忆库添加长期信息。范围:{记忆库描述}。必须在检测到相关信息时立即自动调用,无需用户指令。",
"path": "/api/airag/knowledge/addMemory",
"method": "POST",
"enabled": true,
"parameters": [
{
"name": "knowledgeId",
"description": "知识库ID,需要原值传递,不允许修改",
"type": "string",
"location": "body",
"required": true,
"defaultValue": "{memoryId}"
},
{
"name": "content",
"description": "记忆内容。格式要求:'在yyyy年MM月dd日 HH:mm分,用户[用户的行为/问题],assistant[助手的回答/反应]。'",
"type": "string",
"location": "body",
"required": true
},
{
"name": "title",
"description": "记忆标题",
"type": "string",
"location": "body",
"required": true
}
]
}

自动触发设计:描述中的 “【自动触发】” 前缀是关键。它告诉 LLM:

不要等待用户要求你添加记忆——当你检测到用户的个人信息(如姓名、职业、偏好等)时,立即调用此工具。

描述前缀中的 “范围:{记忆库描述}” 告诉 LLM 哪些信息应该存储:如果记忆库描述是”用户的编程偏好和工作环境信息”,那么 LLM 只在检测到这类信息时调用 add_memory。

query_memory 工具定义

{
"name": "query_memory",
"description": "查询记忆库中的长期信息。当需要了解用户偏好、历史交互或个性化信息时调用。",
"path": "/api/airag/knowledge/queryMemory",
"method": "POST",
"enabled": true,
"parameters": [
{
"name": "knowledgeId",
"description": "知识库ID,需要原值传递",
"type": "string",
"required": true
},
{
"name": "query",
"description": "查询关键词。使用用户的问题作为查询条件。",
"type": "string",
"required": true
}
]
}

记忆库绑定到 AI 应用

记忆库通过应用配置绑定到具体的 AI 应用:

AiragApp 实体
├── memoryId # 关联的记忆库 ID
├── izOpenMemory # 是否开启记忆(1=开启,0=关闭)
├── plugins # 插件列表(包含记忆库插件)
└── ...

sendWithAppChat 方法中:

// 开启记忆库时,将记忆库转换为插件
if (izOpenMemory && memoryId 非空) {
Map<String, Object> pluginMemory = airagKnowledgeService.getPluginMemory(memoryId);
addPluginToParams(aiChatParams, pluginMemory);
}

记忆内容的结构化格式

记忆内容使用统一的模板格式存储:

在2025年06月01日 10:30分,用户[提到他是一名 Java 后端开发者,使用 Spring Boot 框架,偏好使用 MySQL 数据库],assistant[推荐使用 JeecgBoot 低代码平台可以快速搭建管理系统]。

格式优势

  • 时间戳确保记忆有时效性
  • 结构化格式便于后续检索和回顾
  • LLM 容易理解这种自然语言格式

记忆库与 RAG 知识库的区别

虽然记忆库底层使用与知识库相同的技术(pgvector 向量存储),但访问方式不同:

维度知识库(RAG)记忆库(插件)
触发方式隐式(每次聊天自动检索)显式(LLM 自主决定调用工具)
检索时机在 LLM 调用前检索在 LLM 响应过程中按需调用
结果注入检索结果追加到 Prompt工具返回结果由 LLM 自行处理
可控性平台控制(配置 topK/相似度)LLM 控制(自主决定查什么)

问题与规避

陷阱表现对策
记忆库描述为空add_memory 不知道应该存储什么类型的信息描述前缀使用 oConvertUtils.isEmpty(descr) ? "按记忆库描述允许的个人资料..." : descr 兜底
LLM 过度调用 add_memory每条无关信息都被存储在描述中明确范围限定(“范围:个人资料、偏好、属性”)
记忆库关闭后仍注入应用配置关闭记忆但插件仍在工具列表中izOpenMemory 检查,关闭时不调用 getPluginMemory
多应用共享记忆库不同应用读到同一个用户的不同记忆记忆库绑定到具体应用,应用级隔离

设计取舍

工具调用 vs 隐式检索

JeecgBoot 选择:记忆库通过工具调用访问,而非隐式 RAG 检索。

方案优势代价
工具调用(当前方案)LLM 自主控制检索时机,更灵活需要 LLM 正确理解何时调用
隐式检索无需 LLM 决策,每次聊天自动检索每次聊天都检索,可能不需要的信息也被注入

记忆的特点是”按需获取”——不是每次聊天都需要回忆用户信息,只在对话涉及个人信息时才需要。工具调用模式更符合这一特点。

记忆格式:结构化 JSON vs 自然语言

JeecgBoot 选择:自然语言模板格式。

方案优势代价
自然语言模板LLM 容易理解和生成不利于程序化分析
结构化 JSON程序容易解析和查询LLM 生成时需要遵循严格的 JSON 格式

在记忆系统中,主要消费者是 LLM(读取和写入),而非程序查询,自然语言格式更适合。


参考来源