feat(memory): create prompts for submodules of memory selector

This commit is contained in:
2026-04-18 19:19:05 +08:00
parent e0543a8966
commit 92c8e01000
6 changed files with 285 additions and 126 deletions

View File

@@ -14,11 +14,10 @@ import work.slhaf.partner.framework.agent.interaction.flow.RunningFlowContext;
import work.slhaf.partner.framework.agent.model.pojo.Message;
import work.slhaf.partner.module.memory.runtime.MemoryRuntime;
import work.slhaf.partner.module.memory.runtime.exception.MemoryLookupException;
import work.slhaf.partner.module.memory.selector.evaluator.SliceSelectEvaluator;
import work.slhaf.partner.module.memory.selector.evaluator.MemoryRecallEvaluator;
import work.slhaf.partner.module.memory.selector.evaluator.entity.EvaluatorInput;
import work.slhaf.partner.module.memory.selector.extractor.MemorySelectExtractor;
import work.slhaf.partner.module.memory.selector.extractor.MemoryRecallCueExtractor;
import work.slhaf.partner.module.memory.selector.extractor.entity.ExtractorInput;
import work.slhaf.partner.module.memory.selector.extractor.entity.ExtractorMatchData;
import work.slhaf.partner.module.memory.selector.extractor.entity.ExtractorResult;
import work.slhaf.partner.runtime.PartnerRunningFlowContext;
@@ -36,9 +35,9 @@ public class MemorySelector extends AbstractAgentModule.Running<PartnerRunningFl
@InjectModule
private MemoryRuntime memoryRuntime;
@InjectModule
private SliceSelectEvaluator sliceSelectEvaluator;
private MemoryRecallEvaluator memoryRecallEvaluator;
@InjectModule
private MemorySelectExtractor memorySelectExtractor;
private MemoryRecallCueExtractor memoryRecallCueExtractor;
@Override
protected void doExecute(@NotNull PartnerRunningFlowContext runningFlowContext) {
@@ -49,7 +48,7 @@ public class MemorySelector extends AbstractAgentModule.Running<PartnerRunningFl
runningFlowContext.getFirstInputDateTime().toLocalDate()
);
ExtractorResult extractorResult = memorySelectExtractor.execute(input);
ExtractorResult extractorResult = memoryRecallCueExtractor.execute(input);
if (extractorResult.getMatches().isEmpty()) {
return;
}
@@ -149,17 +148,16 @@ public class MemorySelector extends AbstractAgentModule.Running<PartnerRunningFl
.inputs(snapshotInputs)
.memorySlices(new ArrayList<>(candidates.values()))
.build();
return sliceSelectEvaluator.execute(evaluatorInput);
return memoryRecallEvaluator.execute(evaluatorInput);
}
private void setMemoryCandidates(LinkedHashMap<String, ActivatedMemorySlice> candidates,
List<ExtractorMatchData> matches) {
for (ExtractorMatchData match : matches) {
private void setMemoryCandidates(LinkedHashMap<String, ActivatedMemorySlice> candidates, List<ExtractorResult.ExtractorMatchData> matches) {
for (ExtractorResult.ExtractorMatchData match : matches) {
try {
List<ActivatedMemorySlice> recalledSlices = switch (match.getType()) {
case ExtractorMatchData.Constant.TOPIC ->
case ExtractorResult.ExtractorMatchData.Constant.TOPIC ->
memoryRuntime.queryActivatedMemoryByTopicPath(match.getText());
case ExtractorMatchData.Constant.DATE ->
case ExtractorResult.ExtractorMatchData.Constant.DATE ->
memoryRuntime.queryActivatedMemoryByDate(LocalDate.parse(match.getText()));
default -> List.of();
};

View File

@@ -25,7 +25,74 @@ import java.util.Locale;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
public class SliceSelectEvaluator extends AbstractAgentModule.Sub<EvaluatorInput, List<ActivatedMemorySlice>> implements ActivateModel {
public class MemoryRecallEvaluator extends AbstractAgentModule.Sub<EvaluatorInput, List<ActivatedMemorySlice>> implements ActivateModel {
private static final String MODULE_PROMPT = """
你负责在记忆切片召回流程中对单个候选 memory slice 做保留评估你的任务不是重新提取检索线索也不是在多个切片之间排序而是基于当前新输入近期 communication 语境以及当前这个 memory slice 本身判断这段切片是否值得保留进入后续记忆上下文
你会收到
- 一条结构化上下文消息其中包含当前活跃的 communication 域内容
- 一条任务消息其中包含
- new_inputs一组按时间顺序累积的新输入每条输入附带 interval-to-first
- memory_slice当前正在评估的单个候选切片其中包括
- slice_summary该切片的摘要
- source_messages该切片对应的部分原始消息若消息较多只会展示前两条中间四条末尾两条并在中间插入省略说明
你的任务
- 判断当前这个 memory slice 是否与本轮输入明显相关并且值得保留
- 如果值得保留则返回 passed=true
- 如果不值得保留则返回 passed=false
评估目标
- 这里的值得保留不是指勉强有点关系而是指这段切片很可能对理解当前输入承接当前话题补充当前回指对象或支持接下来的回应有实际帮助
- 你评估的是当前这个切片自身是否值得保留而不是是否存在某个相关记忆主题
- 不要把切片所属主题近期很活跃误当成它当前就应被保留
核心判断原则
- new_inputs 应整体理解不要只看最后一句如果多条输入共同收敛到某个具体关注点应按整体意图判断
- communication 域用于辅助理解当前输入是否在承接回指或延续近期对话中的某个对象某段讨论某个比较目标
- memory_slice 是当前唯一需要评估的对象判断重点在于它是否真正贴合当前输入而不是它是否看起来总体上像是相关的
- slice_summary source_messages 应结合起来看如果 summary 看似相关 source_messages 显示其实际讨论重点并不一致则不应仅凭 summary 通过
- source_messages 可能被截断展示因此你可以基于已展示内容做审慎判断若现有内容已足以看出明显不贴合则应直接拒绝不要自行脑补缺失内容
何时应通过
- 当前输入明显在追问延续回指比较复盘这段切片所对应的那段讨论
- 当前输入中的代词简称模糊表达结合 communication 与该切片内容后可以较明确地对应到这段切片
- 当前输入虽然没有复述切片内容但它正在继续推进这段切片中的核心议题
- 当前输入需要补足的背景上下文或前情正是该切片能够提供的
- 该切片与当前输入在关注点上是同一条线而不只是共享一些表层关键词
何时不应通过
- 该切片只是在宽泛主题上相关但与当前输入的具体关注点并不一致
- 当前输入谈的是某条主题中的一个更具体方向而该切片实际对应的是同主题下的另一支内容
- 该切片只是最近讨论过当前活跃过但当前输入并没有明显指向它
- 该切片与当前输入只有表层关键词重合缺乏稳定的语义承接
- 只能看出可能有一点关系但不足以认为它对当前轮次真的有帮助
- 当前输入是在排除收窄或转离该切片所对应的方向
- 仅凭切片摘要中的高层概括词抽象标签热门主题就武断通过
- 仅因为 communication 中存在宽泛相关话题就把当前切片也一起保留
关于评估尺度
- 通过标准应偏保守宁可少保留也不要把低质量弱相关误方向的切片混入后续上下文
- 属于同一个大主题不等于值得保留
- 曾经讨论过类似内容不等于当前就在指向这段切片
- 能勉强联想到不等于应通过
- 只有当你能够比较稳定地判断这段切片对当前输入确实有帮助时才应通过
你不应做的事
- 不要重新选择别的切片
- 不要把别的可能更相关的记忆当成当前切片通过的理由
- 不要扩展用户话题
- 不要回答用户问题
- 不要总结整个对话
- 不要输出解释理由附加字段或额外文本
- 不要因为不确定就倾向通过
输出要求
- 严格按照 EvaluatorBatchResult 对应结构输出
- 结果中只表达当前这个 memory slice 是否通过
- 不要输出除结构要求之外的任何内容
""";
@InjectCapability
private ActionCapability actionCapability;
@@ -45,16 +112,12 @@ public class SliceSelectEvaluator extends AbstractAgentModule.Sub<EvaluatorInput
List<ActivatedMemorySlice> result = new ArrayList<>();
CountDownLatch latch = new CountDownLatch(preparedSlices.size());
Message contextMessage = cognitionCapability.contextWorkspace().resolve(List.of(
ContextBlock.FocusedDomain.MEMORY
)).encodeToMessage();
for (ActivatedMemorySlice slice : preparedSlices) {
executor.execute(() -> {
try {
EvaluatorBatchInput batchInput = new EvaluatorBatchInput(evaluatorInput.getInputs(), slice);
List<Message> messages = List.of(
contextMessage,
resolveContextMessage(),
resolveTaskMessage(batchInput)
);
formattedChat(messages, EvaluatorBatchResult.class)
@@ -74,11 +137,18 @@ public class SliceSelectEvaluator extends AbstractAgentModule.Sub<EvaluatorInput
try {
latch.await();
} catch (InterruptedException ignored) {
Thread.currentThread().interrupt();
}
return result;
}
private Message resolveContextMessage() {
return cognitionCapability.contextWorkspace().resolve(List.of(
ContextBlock.FocusedDomain.COMMUNICATION
)).encodeToMessage();
}
private Message resolveTaskMessage(EvaluatorBatchInput batchInput) {
return new TaskBlock() {
@Override
@@ -137,6 +207,12 @@ public class SliceSelectEvaluator extends AbstractAgentModule.Sub<EvaluatorInput
}.encodeToMessage();
}
@Override
@NotNull
public List<Message> modulePrompt() {
return List.of(new Message(Message.Character.SYSTEM, MODULE_PROMPT));
}
@Override
@NotNull
public String modelKey() {

View File

@@ -0,0 +1,182 @@
package work.slhaf.partner.module.memory.selector.extractor;
import kotlin.Unit;
import org.jetbrains.annotations.NotNull;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import work.slhaf.partner.core.cognition.CognitionCapability;
import work.slhaf.partner.core.cognition.ContextBlock;
import work.slhaf.partner.framework.agent.factory.capability.annotation.InjectCapability;
import work.slhaf.partner.framework.agent.factory.component.abstracts.AbstractAgentModule;
import work.slhaf.partner.framework.agent.factory.component.annotation.InjectModule;
import work.slhaf.partner.framework.agent.model.ActivateModel;
import work.slhaf.partner.framework.agent.model.pojo.Message;
import work.slhaf.partner.framework.agent.support.Result;
import work.slhaf.partner.module.TaskBlock;
import work.slhaf.partner.module.memory.runtime.MemoryRuntime;
import work.slhaf.partner.module.memory.selector.extractor.entity.ExtractorInput;
import work.slhaf.partner.module.memory.selector.extractor.entity.ExtractorResult;
import java.time.format.DateTimeFormatter;
import java.util.List;
public class MemoryRecallCueExtractor extends AbstractAgentModule.Sub<ExtractorInput, ExtractorResult> implements ActivateModel {
private static final String MODULE_PROMPT = """
你负责在记忆召回前,根据当前新输入与现有语境,提取本次值得检索的记忆线索。你的任务不是直接召回记忆内容,也不是总结对话,而是从当前输入中识别出:接下来应优先尝试检索哪些记忆主题路径,或是否应按具体日期检索记忆。
你会收到:
- 一条结构化上下文消息,其中包含当前活跃的 communication 域与 memory 域内容;
- 一条任务消息,其中包含:
- new_inputs一组按时间顺序累积的新输入每条输入附带 interval-to-first
- current_date当前日期
- memory_topic_tree当前可用的记忆主题树结构。
你的任务:
- 基于 new_inputs、当前语境与已有记忆主题树提取本次记忆召回最值得尝试的匹配项
- 匹配项只允许有两类topic 或 date
- topic 用于表示应优先检索的记忆主题路径;
- date 用于表示应优先检索的具体日期;
- 若当前输入不足以支持稳定的记忆检索线索,则返回空列表。
提取原则:
- 你的目标是提取“可用于后续召回”的线索,而不是复述输入内容本身。
- new_inputs 应整体理解,不要只抓最后一句;如果多条输入共同收敛到同一记忆方向,应提取更稳定的主题线索。
- communication 域用于判断当前输入是否在承接近期某段对话、某个旧话题或某个已出现过的指代对象。
- memory 域用于辅助判断当前输入与哪些已激活记忆方向明显相关;只有在这种相关性明确时才使用,不要机械复述 memory 域内容。
- memory_topic_tree 是 topic 提取的主要参照topic 应尽量贴近主题树中已有的层级与命名,不要随意发明与主题树无关的新路径。
- 如果输入只能支持较上层的主题方向,则输出较短路径;不要为了显得具体而伪造下层节点。
- 如果输入同时指向多个可能的记忆方向,只保留最有召回价值、最稳定的少量结果,不要泛化扩散。
关于 topic
- topic 表示一个记忆主题路径。
- topic 的 text 必须是纯路径文本,使用 `->` 连接层级,例如 A->B->C。
- topic 应尽量对齐 memory_topic_tree 中已有的节点表达、层级关系与命名习惯。
- topic 应体现“当前输入最可能在回指、延续或需要补充回忆的主题”。
- 不要输出过于空泛、没有检索价值的 topic例如“聊天”“问题”“内容”“事情”这类抽象词。
- 不要在 topic 中附加解释、括号说明、标签注释或额外文字。
关于 date
- date 表示一个明确的记忆日期。
- date 的 text 必须是可被 Java LocalDate.parse 正常解析的日期文本,即 yyyy-MM-dd。
- 只有在输入中存在明确日期,或结合 current_date 后可以稳定推断出具体某一天时,才输出 date。
- 像“今天”“昨天”“前天”“上周六”这类表达,只有在能够稳定落到某个具体日期时才可输出。
- 对于“最近”“前几天”“那段时间”“之前”“上次”这类无法稳定定位到某一天的表达,不要输出 date。
何时应提取 topic
- 当前输入明显在延续某个已出现过的话题;
- 当前输入在追问、回指、比较、复盘某个过去讨论过的方向;
- 当前输入虽然表面简短,但结合 communication 或 memory 上下文后,可以明确看出其所指主题;
- 当前输入本身就在讨论一个具有长期记忆意义、适合按主题检索的内容。
何时应提取 date
- 当前输入明确提到了某个具体日期;
- 当前输入使用相对日期表达,但结合 current_date 可以稳定还原到具体某一天;
- 当前输入中的回忆目标明显依赖某个特定日期,且该日期能够被明确确定。
何时不应轻易输出:
- 当前输入只是即时情绪、临时感叹或泛泛回应,没有形成稳定的记忆检索方向;
- 只能看出模糊相关性,无法判断应检索哪个主题;
- 只能看出模糊时间范围,无法稳定确定到某一天;
- 仅凭 memory 域里恰好出现过某个内容,但当前输入并没有明显指向它;
- 仅凭表层关键词联想出某条路径,但缺乏足够语境支持。
关于 matches 列表:
- 每个 match 只能是 topic 或 date 两种类型之一。
- 可以同时输出 topic 与 date如果两者都对当前记忆召回明显有帮助则都可以保留。
- 不要输出语义重复或明显冗余的 match。
- 如果已经能够稳定定位到较具体的 topic通常不要再同时输出它的宽泛父路径除非父路径本身也具有独立检索价值。
- 若没有足够明确、足够稳定的匹配项,返回空的 matches 列表。
其他约束:
- 你不是在生成记忆内容,只是在提取检索线索。
- 不要回答用户问题,不要总结输入,不要解释推理过程。
- 不要输出除 topic / date 之外的类型。
- 不要编造上下文中不存在的主题、事实或日期。
- 不要为了凑结果而输出低质量匹配项。
输出要求:
- 严格按照 ExtractorResult 对应结构输出。
- matches 中每一项都必须只包含 type 与 text。
- type 只能是 "topic""date"
- topic 的 text 必须是 `A->B->C` 形式的纯路径文本。
- date 的 text 必须是 yyyy-MM-dd 形式的日期文本。
""";
@InjectCapability
private CognitionCapability cognitionCapability;
@InjectModule
private MemoryRuntime memoryRuntime;
@Override
protected ExtractorResult doExecute(ExtractorInput input) {
ExtractorResult extractorResult;
List<Message> messages = List.of(
resolveContextMessage(),
resolveTaskMessage(input)
);
Result<ExtractorResult> result = formattedChat(
messages,
ExtractorResult.class
);
extractorResult = result.fold(
value -> value,
exception -> {
ExtractorResult fallback = new ExtractorResult();
fallback.setMatches(List.of());
return fallback;
}
);
return fix(extractorResult);
}
private Message resolveTaskMessage(ExtractorInput input) {
return new TaskBlock() {
@Override
protected void fillXml(@NotNull Document document, @NotNull Element root) {
appendChildElement(document, root, "new_inputs", (inputsElement) -> {
appendListElement(document, inputsElement, "inputs", "input", input.getInputs(), (inputElement, entry) -> {
inputElement.setAttribute("interval-to-first", String.valueOf(entry.getOffsetMillis()));
inputElement.setTextContent(entry.getContent());
return Unit.INSTANCE;
});
return Unit.INSTANCE;
});
appendTextElement(document, root, "current_date", input.getDate().format(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
appendTextElement(document, root, "memory_topic_tree", input.getTopic_tree());
}
}.encodeToMessage();
}
private Message resolveContextMessage() {
return cognitionCapability.contextWorkspace().resolve(List.of(
ContextBlock.FocusedDomain.COMMUNICATION, ContextBlock.FocusedDomain.MEMORY
)).encodeToMessage();
}
private ExtractorResult fix(ExtractorResult extractorResult) {
extractorResult.getMatches().forEach(m -> {
if (m.getType().equals(ExtractorResult.ExtractorMatchData.Constant.DATE)) {
return;
}
m.setText(memoryRuntime.fixTopicPath(m.getText()));
});
if (extractorResult.getMatches().isEmpty()) {
return extractorResult;
}
extractorResult.getMatches().removeIf(m -> m.getText().split("->")[0].isEmpty());
return extractorResult;
}
@Override
@NotNull
public List<Message> modulePrompt() {
return List.of(new Message(Message.Character.SYSTEM, MODULE_PROMPT));
}
@NotNull
@Override
public String modelKey() {
return "topic_extractor";
}
}

View File

@@ -1,94 +0,0 @@
package work.slhaf.partner.module.memory.selector.extractor;
import kotlin.Unit;
import org.jetbrains.annotations.NotNull;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import work.slhaf.partner.core.cognition.CognitionCapability;
import work.slhaf.partner.core.cognition.ContextBlock;
import work.slhaf.partner.framework.agent.factory.capability.annotation.InjectCapability;
import work.slhaf.partner.framework.agent.factory.component.abstracts.AbstractAgentModule;
import work.slhaf.partner.framework.agent.factory.component.annotation.InjectModule;
import work.slhaf.partner.framework.agent.model.ActivateModel;
import work.slhaf.partner.framework.agent.model.pojo.Message;
import work.slhaf.partner.framework.agent.support.Result;
import work.slhaf.partner.module.TaskBlock;
import work.slhaf.partner.module.memory.runtime.MemoryRuntime;
import work.slhaf.partner.module.memory.selector.extractor.entity.ExtractorInput;
import work.slhaf.partner.module.memory.selector.extractor.entity.ExtractorMatchData;
import work.slhaf.partner.module.memory.selector.extractor.entity.ExtractorResult;
import java.time.format.DateTimeFormatter;
import java.util.List;
public class MemorySelectExtractor extends AbstractAgentModule.Sub<ExtractorInput, ExtractorResult> implements ActivateModel {
@InjectCapability
private CognitionCapability cognitionCapability;
@InjectModule
private MemoryRuntime memoryRuntime;
@Override
protected ExtractorResult doExecute(ExtractorInput input) {
ExtractorResult extractorResult;
List<Message> messages = List.of(
resolveContextMessage(),
resolveTaskMessage(input)
);
Result<ExtractorResult> result = formattedChat(
messages,
ExtractorResult.class
);
extractorResult = result.fold(
value -> value,
exception -> {
ExtractorResult fallback = new ExtractorResult();
fallback.setMatches(List.of());
return fallback;
}
);
return fix(extractorResult);
}
private Message resolveTaskMessage(ExtractorInput input) {
return new TaskBlock() {
@Override
protected void fillXml(@NotNull Document document, @NotNull Element root) {
appendChildElement(document, root, "new_inputs", (inputsElement) -> {
appendListElement(document, inputsElement, "inputs", "input", input.getInputs(), (inputElement, entry) -> {
inputElement.setAttribute("interval-to-first", String.valueOf(entry.getOffsetMillis()));
inputElement.setTextContent(entry.getContent());
return Unit.INSTANCE;
});
return Unit.INSTANCE;
});
appendTextElement(document, root, "current_date", input.getDate().format(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
appendTextElement(document, root, "memory_topic_tree", input.getTopic_tree());
}
}.encodeToMessage();
}
private Message resolveContextMessage() {
return cognitionCapability.contextWorkspace().resolve(List.of(
ContextBlock.FocusedDomain.COMMUNICATION, ContextBlock.FocusedDomain.MEMORY
)).encodeToMessage();
}
private ExtractorResult fix(ExtractorResult extractorResult) {
extractorResult.getMatches().forEach(m -> {
if (m.getType().equals(ExtractorMatchData.Constant.DATE)) {
return;
}
m.setText(memoryRuntime.fixTopicPath(m.getText()));
});
if (extractorResult.getMatches().isEmpty()) {
return extractorResult;
}
extractorResult.getMatches().removeIf(m -> m.getText().split("->")[0].isEmpty());
return extractorResult;
}
@Override
public String modelKey() {
return "topic_extractor";
}
}

View File

@@ -1,14 +0,0 @@
package work.slhaf.partner.module.memory.selector.extractor.entity;
import lombok.Data;
@Data
public class ExtractorMatchData {
private String type;
private String text;
public static class Constant {
public static final String DATE = "date";
public static final String TOPIC = "topic";
}
}

View File

@@ -7,4 +7,15 @@ import java.util.List;
@Data
public class ExtractorResult {
private List<ExtractorMatchData> matches;
@Data
public static class ExtractorMatchData {
private String type;
private String text;
public static class Constant {
public static final String DATE = "date";
public static final String TOPIC = "topic";
}
}
}