- 人工智能
- AI Agent
- Agent 框架
- 工具调用
- Agent 工作流
- RAG
- MCP Clients
- 强化学习
【免费下载链接】agent-core-java
openJiuwen agent core Java是 openJiuwen Core的 Java版本,提供AI Agent开发、运行、调优与演进相关的全套SDK能力。
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_LOGGER | LoggerProtocol | 长期记忆提取流程使用的日志记录器,取自Loggers.MEMORY。 |
MAPPER | ObjectMapper | JacksonObjectMapper实例。当前源码中定义但未直接参与公开流程(实际 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,字段如下:
| 字段 | 类型 | 说明 |
|---|---|---|
userId | String | 用户 ID,用于记忆隔离与记忆 ID 生成。 |
scopeId | String | 作用域 ID(如会话/应用范围)。 |
messages | List<BaseMessage> | 当前轮待提取的消息列表。 |
historyMessages | List<BaseMessage> | 历史消息,仅作为指代消解的参考,严禁从中提取记忆。 |
baseChatModel | Model | 执行提取的聊天模型实例。 |
构造函数为五参全参构造,并提供全套 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_definition | MemoryScopeConfig | 用户画像记忆的自定义定义,可插拔。 |
semantic_memory_definition | MemoryScopeConfig | 语义记忆的自定义定义,可插拔。 |
episodic_memory_definition | MemoryScopeConfig | 情景记忆的自定义定义,可插拔。 |
current_week | buildTimeContext(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。模板将模型设定为"零幻觉信息提取专家",核心执行流程为六步:
- Step 1 指代消解:回溯【参考消息】替换代词;
- Step 2 主体锁定:主语为"我/本人/我们"锁定为用户本人;无主语的整句直接丢弃;
- Step 3 逻辑推理:知识推理与时间推理,相对时间必须基于【当前所在周】与【对话发生时间】换算为绝对日期(如"下周"= 周内两个日期各 +7 天);
- Step 4 内容提取:逐句扫描【目标消息】,排除高敏感信息、寒暄、短时效提问;记忆单元原子化,并列多值拆分为多条;
- Step 5 内容整理:画像类主语统一改为"用户"并写成单值陈述句;相对时间全部替换为绝对日期,禁止保留相对时间词;
- 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(); }对应文档"行为说明"的三条事实,逐一印证:
- 只有能被
JsonOutputParser解析成Map时才返回结果:解析产物若不是Map(例如 JSON 数组或字符串),视为格式不符,进入下一次重试;成功时把 key 统一String.valueOf后装入LinkedHashMap返回,保持模型输出的字段顺序; - 失败按
retries次数重试:模型调用异常(RuntimeException)与解析失败都会被捕获并重试;只有最后一次失败才通过MEMORY_LOGGER以LogEventType.MEMORY_PROCESS事件打 error 日志Long term memory extractor model output format error; - 连续失败后返回空映射:
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能力。
相关推荐
agent-core-java 记忆引擎 Generator 类详解:对话消息分析、摘要与长期片段提取的协调中枢
agent core java 记忆引擎 Generator 类详解:对话消息分析、摘要与长期片段提取的协调中枢 Generator 是 openJiuwen
人工智能AI AgentAgent 框架工具调用Agent 工作流RAGMCP Clients强化学习DLSS Swapper:9 种 DLL、8 个游戏库,不更新游戏也能切换缩放版本
DLSS Swapper:9 种 DLL、8 个游戏库,不更新游戏也能切换缩放版本 如果你的游戏还停在上一代 DLSS,而新版早就发布,你又不想等厂商推送补丁、
桌面应用告别失忆对话:LangChain记忆系统实现长期对话记忆管理
告别失忆对话:LangChain记忆系统实现长期对话记忆管理 你是否遇到过这样的尴尬?与AI助手聊了半天后,它突然忘记了你之前说过的话?或者在构建对话机器人时,
人工智能大模型AI AgentAgent 框架RAG
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考