☰
agent-core-java 长期记忆提取器 LongTermMemoryExtractor 源码解析:从对话消息到分类片段记忆
2026/10/14 15:05:15 网站建设 项目流程
  • 人工智能
  • AI Agent
  • Agent 框架
  • 工具调用
  • Agent 工作流
  • RAG
  • MCP Clients
  • 强化学习

【免费下载链接】agent-core-java

openJiuwen agent core Java是 openJiuwen Core的 Java版本,提供AI Agent开发、运行、调优与演进相关的全套SDK能力。

项目地址:https://gitcode.com/openJiuwen/agent-core-java
点击查看免费下载

LongTermMemoryExtractor是 agent-core-java 记忆引擎中负责长期片段记忆提取的核心组件:它通过提示词工程与模型调用,把"历史消息 + 当前消息"转换为结构化的 JSON 分类结果(用户画像、语义记忆、情景记忆),并内置重试与容错机制。本篇以该类为纲,结合仓库中的提示词模板、下游消费方Generator与单元测试,完整讲解它的输入组装、提示词变量、时间上下文推导、JSON 解析与重试语义,帮助你在开发记忆能力时准确调用并定制该组件。

一、类定位:长期记忆提取的"提取专家"

LongTermMemoryExtractor位于 memory 模块的com.openjiuwen.memory.process.extract包中,对应源码文件为 LongTermMemoryExtractor.java。它是一个final工具类,构造器私有,所有能力通过静态方法暴露:

public final class LongTermMemoryExtractor { private static final LoggerProtocol MEMORY_LOGGER = Loggers.MEMORY; private static final int DEFAULT_RETRIES = 3; private static final DateTimeFormatter MONTH_DAY_FORMAT = DateTimeFormatter.ofPattern("MM.dd"); private static final java.util.concurrent.Executor IO_EXECUTOR = OpenJiuwenExecutors.newBoundedModulePool("long-term-memory-extractor-io", true); private LongTermMemoryExtractor() { } // ... }

源码注释说明它"Mirrors Python'sLongTermMemoryExtractorinopenjiuwen/core/memory/process/extract/long_term_memory_extractor.py",即 Java 版与 Python 版行为对齐。它所在的包extract承担"把对话消息转换为长期记忆相关结果"的职责,同包还有Generator(流程编排者)、MemoryAnalyzer(消息分析器)与参数类ExtractMemoryParams,包级说明见 extract 包 README。

文档中提到的两个关键字段与源码一致:

字段类型说明
MEMORY_LOGGERLoggerProtocol长期记忆提取流程使用的日志记录器,取自Loggers.MEMORY。
MAPPERObjectMapperJacksonObjectMapper实例。当前源码中定义但未直接参与公开流程(实际 JSON 解析由JsonOutputParser内部的OBJECT_MAPPER完成)。

除文档已列字段外,源码中还可见两个私有常量:DEFAULT_RETRIES = 3(默认重试次数)和IO_EXECUTOR(名为long-term-memory-extractor-io的有界模块线程池),后者用于把模型调用与解析放到 IO 线程上异步执行。

二、公开方法:两个重载与异步返回

文档给出的公开方法签名为:

签名说明
extractLongTermMemory(ExtractMemoryParams params, String timestamp, int retries)组装历史与当前消息,调用模型并解析 JSON 结果;失败时按retries次数重试。
extractLongTermMemory(ExtractMemoryParams params, String timestamp)使用默认重试次数3执行长期记忆提取。

对照当前源码,方法族以MemoryScopeConfig(作用域记忆配置)作为关键参数,返回类型为CompletionStage<Map<String, Object>>:

public static CompletionStage<Map<String, Object>> extractLongTermMemory( ExtractMemoryParams extractMemoryParas, String timestamp, MemoryScopeConfig scopeConfig ) { return extractLongTermMemory(extractMemoryParas, timestamp, scopeConfig, DEFAULT_RETRIES); } public static CompletionStage<Map<String, Object>> extractLongTermMemory( ExtractMemoryParams extractMemoryParas, String timestamp, MemoryScopeConfig scopeConfig, int retries ) { // 组装参考消息与目标消息 -> 填充提示词变量 -> 异步调用模型并解析 ... return CompletableFuture.supplyAsync( () -> invokeAndParse(extractMemoryParas, modelInput, retries), IO_EXECUTOR); }

要点:

  • 三参重载委托给四参重载,并固定使用DEFAULT_RETRIES = 3;
  • 四参重载允许显式指定重试次数,invokeAndParse内部会执行int maxRetries = Math.max(1, retries),即使传入0或负数也会至少尝试一次;
  • 返回CompletionStage,调用方用.toCompletableFuture().join()等待结果(下游Generator与测试用例均如此调用);
  • 模型调用通过CompletableFuture.supplyAsync(..., IO_EXECUTOR)提交到专用 IO 线程池,避免阻塞调用线程。

2.1 输入参数:ExtractMemoryParams

ExtractMemoryParams.java 是提取流程的参数 DTO,字段如下:

字段类型说明
userIdString用户 ID,用于记忆隔离与记忆 ID 生成。
scopeIdString作用域 ID(如会话/应用范围)。
messagesList<BaseMessage>当前轮待提取的消息列表。
historyMessagesList<BaseMessage>历史消息,仅作为指代消解的参考,严禁从中提取记忆。
baseChatModelModel执行提取的聊天模型实例。

构造函数为五参全参构造,并提供全套 getter/setter,字段可后续修改。从Generator.genAllMemorySync中可以看到它的真实构造方式:

ExtractMemoryParams extractMemoryParams = new ExtractMemoryParams( userId, scopeId, messages, historyMessages, model );

(见 Generator.java。)

三、提示词组装:fragment_memory_prompt 与七个变量

文档"行为说明"指出:提示词通过PromptApplier的apply("fragment_memory_prompt", variables)生成,输入模型的消息固定为单条UserMessage。源码实现:

Map<String, String> promptVariables = new LinkedHashMap<>(); promptVariables.put("conversation_time", timestamp); promptVariables.put("input_messages", inputMsgStr.toString()); promptVariables.put("reference_messages", referenceStr.toString()); promptVariables.put("user_profile_definition", nullToEmpty(effectiveScopeConfig.getUserProfileDefinition())); promptVariables.put("semantic_memory_definition", nullToEmpty(effectiveScopeConfig.getSemanticMemoryDefinition())); promptVariables.put("episodic_memory_definition", nullToEmpty(effectiveScopeConfig.getEpisodicMemoryDefinition())); promptVariables.put("current_week", buildTimeContext(timestamp)); String promptContent = new PromptApplier().apply("fragment_memory_prompt", promptVariables); List<BaseMessage> modelInput = List.of(new UserMessage(promptContent));

即提示词变量共 7 个:

变量来源含义
conversation_time入参timestamp对话发生时间,供模型做时间推理。
input_messages当前消息中role == "user"的行【目标消息】,唯一允许提取内容的来源。
reference_messages历史消息 + 当前消息的全部行【参考消息】,仅供指代消解,严禁从中提取。
user_profile_definitionMemoryScopeConfig用户画像记忆的自定义定义,可插拔。
semantic_memory_definitionMemoryScopeConfig语义记忆的自定义定义,可插拔。
episodic_memory_definitionMemoryScopeConfig情景记忆的自定义定义,可插拔。
current_weekbuildTimeContext(timestamp)对话时间所在自然周(周一~周日)的中文表述。

3.1 消息如何切分为"参考消息"与"目标消息"

私有方法appendReference决定两类消息的组装方式:

private static void appendReference( StringBuilder referenceStr, List<BaseMessage> messages, boolean collectUserMessages, StringBuilder inputMsgStr ) { if (messages == null) { return; } for (BaseMessage message : messages) { String name = message.getName() == null ? message.getRole() : message.getName(); String line = name + ": " + message.getContentAsString() + "\n"; referenceStr.append(line); if (collectUserMessages && "user".equals(message.getRole())) { inputMsgStr.append(line); } } }

调用侧先以collectUserMessages = false处理historyMessages,再以true处理messages:

  • 每条消息渲染为name: content一行,name优先取getName(),为空则回退getRole();
  • 历史消息与当前消息都进入"参考消息",保证代词回溯有足够的上下文;
  • 只有当前消息中user角色的消息进入"目标消息"——这实现了提示词中 R1 规则"来源唯一性"的底层保障:模型即使"看到"历史内容,提取也只会面向目标消息。

3.2 时间上下文推导:buildTimeContext

current_week变量不是简单透传时间戳,而是把 ISO 时间换算为所在自然周:

static String buildTimeContext(String timestamp) { try { LocalDate date = parseIsoDate(timestamp); LocalDate monday = date.with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY)); LocalDate sunday = monday.plusDays(6); return monday.getYear() + "年" + monday.getMonthValue() + "月" + monday.getDayOfMonth() + "日(周一)~" + sunday.getYear() + "年" + sunday.getMonthValue() + "月" + sunday.getDayOfMonth() + "日(周日)" + "(即" + MONTH_DAY_FORMAT.format(monday) + "~" + MONTH_DAY_FORMAT.format(sunday) + ")"; } catch (DateTimeParseException | NullPointerException exception) { return timestamp; } }
  • 时间解析支持三种格式:OffsetDateTime(带时区,如2026-01-01T00:00:00Z)、LocalDateTime、LocalDate(见parseIsoDate的三级回退);
  • 解析成功时输出形如2026年1月5日(周一)~2026年1月11日(周日)(即01.05~01.11);
  • 解析失败时优雅降级,直接返回原始timestamp,不抛异常。

这个"当前所在周"是提示词中"下周/上周"类相对时间推理的查表基准,与提示词模板中的 Step 3 时间推理规则(XX.XX各 ±7 天)严格配合。

3.3 提示词模板:fragment_memory_prompt.md

PromptApplier(PromptApplier.java)从类路径资源openjiuwen/core/memory/prompts/下加载fragment_memory_prompt.md模板并做{{variable}}填充,模板带ConcurrentHashMap缓存。仓库中该模板资源文件为 fragment_memory_prompt.md。模板将模型设定为"零幻觉信息提取专家",核心执行流程为六步:

  1. Step 1 指代消解:回溯【参考消息】替换代词;
  2. Step 2 主体锁定:主语为"我/本人/我们"锁定为用户本人;无主语的整句直接丢弃;
  3. Step 3 逻辑推理:知识推理与时间推理,相对时间必须基于【当前所在周】与【对话发生时间】换算为绝对日期(如"下周"= 周内两个日期各 +7 天);
  4. Step 4 内容提取:逐句扫描【目标消息】,排除高敏感信息、寒暄、短时效提问;记忆单元原子化,并列多值拆分为多条;
  5. Step 5 内容整理:画像类主语统一改为"用户"并写成单值陈述句;相对时间全部替换为绝对日期,禁止保留相对时间词;
  6. Step 6 输出:仅输出包裹在```json代码块中的纯净 JSON,禁止任何解释性文本。

其中分类标准支持通过{{user_profile_definition}}、{{semantic_memory_definition}}、{{episodic_memory_definition}}三个变量注入自定义定义,这就是MemoryScopeConfig的定制入口(见下节)。模板末尾还固定四条核心规则(Core Rules):

  • [R1] 来源唯一性:提取内容必须源自【目标消息】,严禁从【参考消息】提取;
  • [R2] 缺失即忽略:未提及的信息绝不输出"用户的 xx 未提及"之类占位;
  • [R3] 语言一致:输出语言与【目标消息】一致,严禁翻译;
  • [R4] 全覆盖:逐句扫描,不遗漏符合标准的琐碎信息。

四、MemoryScopeConfig:三类记忆定义的定制开关

MemoryScopeConfig(MemoryScopeConfig.java)允许按作用域定制三类记忆的判定定义,并提供默认值常量:

常量默认定义
DEFAULT_USER_PROFILE_DEFINITION用户本人的肯定或否定表述(包含不限于基本身份、兴趣偏好、人际关系、资产状况)
DEFAULT_SEMANTIC_MEMORY_DEFINITION用户对话中涉及的和时间无明确关系的事实性内容或概念
DEFAULT_EPISODIC_MEMORY_DEFINITION用户对话中涉及的和时间有明确关系的事实性内容或概念

该类还支持modelCfg、modelClientCfg、embeddingCfg等模型与嵌入配置项,并提供builder()构建器;setter 传null时自动回退到默认定义。提取器内部对scopeConfig == null做了兜底(new MemoryScopeConfig()),即不传配置也能用默认定义运行。

定制用法示例(与单测一致):

MemoryScopeConfig scopeConfig = MemoryScopeConfig.builder() .userProfileDefinition("自定义用户画像定义") .semanticMemoryDefinition("自定义语义记忆定义") .episodicMemoryDefinition("自定义情景记忆定义") .build();

对应的行为验证见 CustomPromptDefinitionPythonParityTest.java:

  • longTermMemoryExtractorInjectsCustomScopeConfigIntoPrompt:构造含"我喜欢打篮球"的ExtractMemoryParams,用记录型Model.ModelInvoker捕获最终 prompt,断言自定义定义文本出现在提示词中;
  • longTermMemoryExtractorUsesDefaultScopeConfigDefinitions:传入new MemoryScopeConfig(),断言默认定义文本出现在提示词中;
  • partialCustomDefinitionKeepsDefaultValues:只定制一项时其余两项保持默认。

这组测试同时验证了文档所述"输入模型的消息固定为单条UserMessage"——RecordingInvoker.prompt()直接取messages.get(0).getContentAsString()即完整提示词。

五、模型调用与 JSON 解析:invokeAndParse 的重试与容错

提取结果的获取由私有方法invokeAndParse完成:

private static Map<String, Object> invokeAndParse( ExtractMemoryParams extractMemoryParas, List<BaseMessage> modelInput, int retries ) { JsonOutputParser parser = new JsonOutputParser(); int maxRetries = Math.max(1, retries); for (int attempt = 0; attempt < maxRetries; attempt++) { try { AssistantMessage response = extractMemoryParas.getBaseChatModel() .invoke(modelInput).toCompletableFuture().join(); Object parsed = parser.parse(response.getContentAsString()).toCompletableFuture().join(); if (parsed instanceof Map<?, ?> parsedMap) { Map<String, Object> result = new LinkedHashMap<>(); parsedMap.forEach((key, value) -> result.put(String.valueOf(key), value)); return result; } } catch (RuntimeException exception) { if (attempt >= maxRetries - 1) { MEMORY_LOGGER.error("[{}] Long term memory extractor model output format error: {}", LogEventType.MEMORY_PROCESS.getValue(), exception.toString()); } } } return Map.of(); }

对应文档"行为说明"的三条事实,逐一印证:

  1. 只有能被JsonOutputParser解析成Map时才返回结果:解析产物若不是Map(例如 JSON 数组或字符串),视为格式不符,进入下一次重试;成功时把 key 统一String.valueOf后装入LinkedHashMap返回,保持模型输出的字段顺序;
  2. 失败按retries次数重试:模型调用异常(RuntimeException)与解析失败都会被捕获并重试;只有最后一次失败才通过MEMORY_LOGGER以LogEventType.MEMORY_PROCESS事件打 error 日志Long term memory extractor model output format error;
  3. 连续失败后返回空映射:Map.of(),调用方得到空 Map 而非异常——这是"优雅降级"设计,记忆提取失败不会阻断上层流程。

值得注意的解析细节:JsonOutputParser.java 会用正则```json\n(.*?)```优先抽取代码块内的 JSON,抽不到则整体 trim 后交给 Jackson 反序列化——这正好匹配提示词 Step 6 要求模型把 JSON 包在```json代码块中输出的约定;解析失败时记录 error 日志并返回null,由提取器的重试循环兜底。

六、下游消费:Generator 如何把提取结果变成记忆单元

LongTermMemoryExtractor不是孤立组件。在 Generator.java 的categoriesToMemoryUnit中,提取结果 Map 被转换为FragmentMemoryUnit列表:

Map<String, Object> memoryDict = LongTermMemoryExtractor.extractLongTermMemory( extractMemoryParas, timestamp, scopeConfig ).toCompletableFuture().join();

消费逻辑分两支:

  • 指令记忆分支:若memoryDict.get("has_explict_instruct")为真,则取instruct_memories列表,交由handleMemoryWithInstruct处理UPDATE/DELETE指令(先按old_mem检索候选记忆,再用semantic_validation提示词做语义校验后落操作);
  • 片段记忆分支:getFragmentMemoryUnit遍历 Map,仅识别三类 key——user_profile、semantic_memory、episodic_memory(由CATEGORY_TO_CLASS映射到MemoryType枚举,见 MemoryType.java),未知 key 直接跳过;每个条目经DataIdManager.generateNextId(userId)生成记忆 ID,包装为FragmentMemoryUnit(memType, memId, content, messageMemId, timestamp, OperationType.ADD)。

这意味着模型输出 JSON 的顶层 key 与MemoryType的字符串值一一对应,是提取器与记忆库之间的契约。上层genAllMemory还会依据AgentMemoryConfig的enableLongTermMem、enableUserProfile、enableSemanticMemory、enableEpisodicMemory开关决定是否启用长期记忆提取以及按类型过滤结果;若分析阶段has_key_information为 false,则根本不会走到提取器。

七、端到端调用示例与可验证依据

综合源码与单测,一次最小化调用形态如下(与 CustomPromptDefinitionPythonParityTest.java 中一致的写法):

ExtractMemoryParams params = new ExtractMemoryParams( "u1", // userId "s1", // scopeId List.of(new UserMessage("我喜欢打篮球")), // 当前消息 List.of(new AssistantMessage("很好")), // 历史消息(仅参考) model // 执行提取的模型 ); Map<String, Object> result = LongTermMemoryExtractor.extractLongTermMemory( params, "2026-01-01T00:00:00", // 对话发生时间(支持 ISO 格式) new MemoryScopeConfig() // 传 null 亦可,内部回退默认配置 ).toCompletableFuture().join(); // 成功时形如 {user_profile=[...], semantic_memory=[...], episodic_memory=[...]} // 连续失败时为空 Map,不抛异常

需要注意的前提与限制:

  • 方法为异步语义,必须通过CompletionStage取结果;阻塞获取会占用当前线程等待 IO 线程池中的模型调用;
  • timestamp建议传 ISO 8601 格式以启用"当前所在周"推导,格式不可解析时提示词中会退化为原始字符串,模型的时间推理精度会下降;
  • 结果 Map 的 key 契约(三类记忆 key + 可选指令字段)由提示词模板与Generator共同决定,替换模型或自定义提示词时需保证输出结构不被破坏,否则会被解析层判定为失败并触发重试。

八、小结

  • LongTermMemoryExtractor以"单条 UserMessage 提示词 + JSON 输出解析 + 有限重试"的轻量形态,把对话消息提炼为user_profile/semantic_memory/episodic_memory三类片段记忆;
  • 参考消息与目标消息的拆分在 Java 侧硬编码实现(当前消息中的 user 行才是提取目标),配合提示词 R1 规则共同保证来源唯一性;
  • MemoryScopeConfig三类定义的注入是官方定制点,测试用例已验证自定义与默认定义的完整行为;
  • 失败路径始终收敛为空 Map,保证记忆提取作为异步旁路能力不影响主对话流程。

关键源码与文档索引:LongTermMemoryExtractor.java、ExtractMemoryParams.java、MemoryScopeConfig.java、Generator.java、fragment_memory_prompt.md、LongTermMemoryExtractor API 文档。

  • 人工智能
  • AI Agent
  • Agent 框架
  • 工具调用
  • Agent 工作流
  • RAG
  • MCP Clients
  • 强化学习

【免费下载链接】agent-core-java

openJiuwen agent core Java是 openJiuwen Core的 Java版本,提供AI Agent开发、运行、调优与演进相关的全套SDK能力。

项目地址:https://gitcode.com/openJiuwen/agent-core-java
点击查看免费下载

相关推荐

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询