refactor(communication): create prompts for summarizer, and optimize message structure

This commit is contained in:
2026-04-17 23:16:20 +08:00
parent 0c079c127e
commit e0543a8966
12 changed files with 423 additions and 135 deletions

View File

@@ -81,6 +81,7 @@ public class ActionCore implements StateSerializable {
log.warn("{} tasks still running", count); log.warn("{} tasks still running", count);
} }
} catch (InterruptedException ignored) { } catch (InterruptedException ignored) {
Thread.currentThread().interrupt();
} }
} }

View File

@@ -1,5 +1,6 @@
package work.slhaf.partner.core.cognition; package work.slhaf.partner.core.cognition;
import org.w3c.dom.Element;
import work.slhaf.partner.framework.agent.factory.capability.annotation.Capability; import work.slhaf.partner.framework.agent.factory.capability.annotation.Capability;
import work.slhaf.partner.framework.agent.model.pojo.Message; import work.slhaf.partner.framework.agent.model.pojo.Message;
@@ -21,6 +22,8 @@ public interface CognitionCapability {
void refreshRecentChatMessagesContext(); void refreshRecentChatMessagesContext();
Element messageNotesElement();
Lock getMessageLock(); Lock getMessageLock();
} }

View File

@@ -7,6 +7,7 @@ import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.w3c.dom.Document; import org.w3c.dom.Document;
import org.w3c.dom.Element; import org.w3c.dom.Element;
import work.slhaf.partner.common.base.Block;
import work.slhaf.partner.framework.agent.factory.capability.annotation.CapabilityCore; import work.slhaf.partner.framework.agent.factory.capability.annotation.CapabilityCore;
import work.slhaf.partner.framework.agent.factory.capability.annotation.CapabilityMethod; import work.slhaf.partner.framework.agent.factory.capability.annotation.CapabilityMethod;
import work.slhaf.partner.framework.agent.interaction.AgentRuntime; import work.slhaf.partner.framework.agent.interaction.AgentRuntime;
@@ -30,13 +31,22 @@ public class CognitionCore implements StateSerializable {
private static final String RECENT_CHAT_MESSAGE_NOTES = """ private static final String RECENT_CHAT_MESSAGE_NOTES = """
消息格式: 消息格式:
- user 消息当前写入格式: [[USER]: <userId>]: <正文> 或 [[AGENT]: self]: <正文> - 所有消息统一写为“标记行 + 空行 + 正文”,比如:
- assistant 消息直接记录回复正文;若以 [NOT_REPLIED]: <正文> 形式出现,表示该结果未直接发送给用户
[[AGENT]: self]: [NOT_REPLIED][COMPRESSED]:
正文内容
- 标记行一定包含身份标签,通常格式为 [[USER]: <userName>] 或 [[AGENT]: self]
- 若身份标签提取失败,可能回退为 [[Unknown]: Unknown]
- 若存在其他标签,则写为“身份标签: 状态标签串:”
- 正文永远从空行后开始
标记含义: 标记含义:
- [USER]: 外部用户来源 - [USER]: 外部用户来源
- [AGENT]: 系统内部来源 - [AGENT]: 系统内部来源
- [NOT_REPLIED]: 仅保留在历史中的未直接回复结果 - [NOT_REPLIED]: 仅保留在历史中的未直接回复结果
- [COMPRESSED]: 该消息正文经过压缩
"""; """;
private final ReentrantLock messageLock = new ReentrantLock(); private final ReentrantLock messageLock = new ReentrantLock();
@@ -117,7 +127,7 @@ public class CognitionCore implements StateSerializable {
new BlockContent("recent_chat_messages", "communication_producer") { new BlockContent("recent_chat_messages", "communication_producer") {
@Override @Override
protected void fillXml(@NotNull Document document, @NotNull Element root) { protected void fillXml(@NotNull Document document, @NotNull Element root) {
appendTextElement(document, root, "message_tag_notes", RECENT_CHAT_MESSAGE_NOTES); document.appendChild(document.importNode(messageNotesElement(), true));
Element chatMessagesElement = document.createElement("chat_messages"); Element chatMessagesElement = document.createElement("chat_messages");
root.appendChild(chatMessagesElement); root.appendChild(chatMessagesElement);
appendRepeatedElements(document, chatMessagesElement, "chat_message", resolveRecentChatMessages(), (messageElement, message) -> { appendRepeatedElements(document, chatMessagesElement, "chat_message", resolveRecentChatMessages(), (messageElement, message) -> {
@@ -135,6 +145,16 @@ public class CognitionCore implements StateSerializable {
contextWorkspace.register(block); contextWorkspace.register(block);
} }
@CapabilityMethod
public Element messageNotesElement() {
return new Block("message_tag_notes") {
@Override
protected void fillXml(@NotNull Document document, @NotNull Element root) {
root.setTextContent(RECENT_CHAT_MESSAGE_NOTES);
}
}.encodeToXml();
}
private List<Message> resolveRecentChatMessages() { private List<Message> resolveRecentChatMessages() {
int exclusiveEnd = Math.max(chatMessages.size() - 1, 0); int exclusiveEnd = Math.max(chatMessages.size() - 1, 0);
if (exclusiveEnd == 0) { if (exclusiveEnd == 0) {

View File

@@ -29,8 +29,9 @@ public class CommunicationProducer extends AbstractAgentModule.Running<PartnerRu
private static final String INTERRUPTED_MARKER = " [response interrupted due to internal exception]"; private static final String INTERRUPTED_MARKER = " [response interrupted due to internal exception]";
private static final String NO_REPLY_MARKER = "NO_REPLY"; private static final String NO_REPLY_MARKER = "NO_REPLY";
private static final String NOT_REPLIED_MARKER = "NOT_REPLIED"; private static final String AGENT_MARKER = "[[AGENT]: self]";
private static final String NOT_REPLIED_PREFIX = "[" + NOT_REPLIED_MARKER + "]: "; private static final String NOT_REPLIED_PREFIX = "[NOT_REPLIED]";
private static final String MARKER_BODY_SEPARATOR = ":\n\n";
private static final String MODULE_PROMPT = """ private static final String MODULE_PROMPT = """
你当前正在承担 Partner 的对外交流职责。你需要基于系统此刻的上下文状态、保留的对话轨迹以及最新输入,生成自然、贴合当前情境、并与系统整体状态一致的交流结果。 你当前正在承担 Partner 的对外交流职责。你需要基于系统此刻的上下文状态、保留的对话轨迹以及最新输入,生成自然、贴合当前情境、并与系统整体状态一致的交流结果。
@@ -86,9 +87,7 @@ public class CommunicationProducer extends AbstractAgentModule.Running<PartnerRu
private void executeChat(PartnerRunningFlowContext runningFlowContext) { private void executeChat(PartnerRunningFlowContext runningFlowContext) {
StreamChatMessageConsumer consumer = ReplyDispatcher.INSTANCE.createConsumer(runningFlowContext.getTarget()); StreamChatMessageConsumer consumer = ReplyDispatcher.INSTANCE.createConsumer(runningFlowContext.getTarget());
this.streamChat(buildChatMessages(runningFlowContext), consumer) this.streamChat(buildChatMessages(runningFlowContext), consumer)
.onFailure(exception -> { .onFailure(exception -> consumer.onDelta(INTERRUPTED_MARKER));
consumer.onDelta(INTERRUPTED_MARKER);
});
updateChatMessages(runningFlowContext, consumer.collectResponse()); updateChatMessages(runningFlowContext, consumer.collectResponse());
cognitionCapability.refreshRecentChatMessagesContext(); cognitionCapability.refreshRecentChatMessagesContext();
} }
@@ -115,7 +114,10 @@ public class CommunicationProducer extends AbstractAgentModule.Running<PartnerRu
formatConversationUserMessage(runningFlowContext) formatConversationUserMessage(runningFlowContext)
); );
chatMessages.add(primaryUserMessage); chatMessages.add(primaryUserMessage);
Message assistantMessage = new Message(Message.Character.ASSISTANT, normalizeAssistantHistoryMessage(response)); Message assistantMessage = new Message(
Message.Character.ASSISTANT,
normalizeAssistantHistoryMessage(response)
);
chatMessages.add(assistantMessage); chatMessages.add(assistantMessage);
} finally { } finally {
cognitionCapability.getMessageLock().unlock(); cognitionCapability.getMessageLock().unlock();
@@ -125,15 +127,23 @@ public class CommunicationProducer extends AbstractAgentModule.Running<PartnerRu
private String normalizeAssistantHistoryMessage(String response) { private String normalizeAssistantHistoryMessage(String response) {
String trimmed = response == null ? "" : response.trim(); String trimmed = response == null ? "" : response.trim();
if (trimmed.equals(NO_REPLY_MARKER)) { if (trimmed.equals(NO_REPLY_MARKER)) {
return NOT_REPLIED_PREFIX.trim(); return formatMarkedHistoryMessage(AGENT_MARKER, NOT_REPLIED_PREFIX, "");
} }
if (trimmed.startsWith(NO_REPLY_MARKER + "\n")) { if (trimmed.startsWith(NO_REPLY_MARKER + "\n")) {
return NOT_REPLIED_PREFIX + trimmed.substring((NO_REPLY_MARKER + "\n").length()).trim(); return formatMarkedHistoryMessage(
AGENT_MARKER,
NOT_REPLIED_PREFIX,
trimmed.substring((NO_REPLY_MARKER + "\n").length()).trim()
);
} }
if (trimmed.startsWith(NO_REPLY_MARKER + "\r\n")) { if (trimmed.startsWith(NO_REPLY_MARKER + "\r\n")) {
return NOT_REPLIED_PREFIX + trimmed.substring((NO_REPLY_MARKER + "\r\n").length()).trim(); return formatMarkedHistoryMessage(
AGENT_MARKER,
NOT_REPLIED_PREFIX,
trimmed.substring((NO_REPLY_MARKER + "\r\n").length()).trim()
);
} }
return response; return formatMarkedHistoryMessage(AGENT_MARKER, "", trimmed);
} }
private List<Message> snapshotConversationMessages() { private List<Message> snapshotConversationMessages() {
@@ -189,7 +199,17 @@ public class CommunicationProducer extends AbstractAgentModule.Running<PartnerRu
} }
private String formatConversationUserMessage(PartnerRunningFlowContext runningFlowContext) { private String formatConversationUserMessage(PartnerRunningFlowContext runningFlowContext) {
return "[" + runningFlowContext.getSource() + "]" + ": " + runningFlowContext.formatInputsForHistory(); return formatMarkedHistoryMessage("[" + runningFlowContext.getSource() + "]", "", runningFlowContext.formatInputsForHistory());
}
private String formatMarkedHistoryMessage(String identityMarker, String statusMarkers, String body) {
String markerLine = statusMarkers == null || statusMarkers.isBlank()
? identityMarker
: identityMarker + ": " + statusMarkers;
if (body == null || body.isBlank()) {
return markerLine + ":";
}
return markerLine + MARKER_BODY_SEPARATOR + body.trim();
} }
private Document newDocument() throws Exception { private Document newDocument() throws Exception {

View File

@@ -24,8 +24,8 @@ import work.slhaf.partner.framework.agent.factory.component.annotation.InjectMod
import work.slhaf.partner.framework.agent.model.pojo.Message; import work.slhaf.partner.framework.agent.model.pojo.Message;
import work.slhaf.partner.framework.agent.support.Result; import work.slhaf.partner.framework.agent.support.Result;
import work.slhaf.partner.module.action.scheduler.ActionScheduler; import work.slhaf.partner.module.action.scheduler.ActionScheduler;
import work.slhaf.partner.module.communication.summarizer.MultiSummarizer; import work.slhaf.partner.module.communication.summarizer.MessageCompressor;
import work.slhaf.partner.module.communication.summarizer.SingleSummarizer; import work.slhaf.partner.module.communication.summarizer.MessageSummarizer;
import work.slhaf.partner.runtime.PartnerRunningFlowContext; import work.slhaf.partner.runtime.PartnerRunningFlowContext;
import java.time.ZonedDateTime; import java.time.ZonedDateTime;
@@ -53,9 +53,9 @@ public class DialogRolling extends AbstractAgentModule.Running<PartnerRunningFlo
private ActionCapability actionCapability; private ActionCapability actionCapability;
@InjectModule @InjectModule
private MultiSummarizer multiSummarizer; private MessageSummarizer messageSummarizer;
@InjectModule @InjectModule
private SingleSummarizer singleSummarizer; private MessageCompressor messageCompressor;
@InjectModule @InjectModule
private ActionScheduler actionScheduler; private ActionScheduler actionScheduler;
@InjectModule @InjectModule
@@ -154,8 +154,8 @@ public class DialogRolling extends AbstractAgentModule.Running<PartnerRunningFlo
@NotNull @NotNull
RollingResult buildRollingResult(List<Message> chatSnapshot, int rollingSize, int retainDivisor) { RollingResult buildRollingResult(List<Message> chatSnapshot, int rollingSize, int retainDivisor) {
singleSummarizer.execute(chatSnapshot); messageCompressor.execute(chatSnapshot);
Result<String> summaryResult = multiSummarizer.execute(chatSnapshot); Result<String> summaryResult = messageSummarizer.execute(chatSnapshot);
String summary = summaryResult.fold( String summary = summaryResult.fold(
value -> value, value -> value,
exp -> "no summary, due to exception" exp -> "no summary, due to exception"

View File

@@ -0,0 +1,238 @@
package work.slhaf.partner.module.communication.summarizer;
import org.jetbrains.annotations.NotNull;
import work.slhaf.partner.core.action.ActionCapability;
import work.slhaf.partner.core.action.ActionCore;
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.Init;
import work.slhaf.partner.framework.agent.model.ActivateModel;
import work.slhaf.partner.framework.agent.model.pojo.Message;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.IntStream;
public class MessageCompressor extends AbstractAgentModule.Sub<List<Message>, Void> implements ActivateModel {
private static final String MODULE_PROMPT = """
你负责对单条消息进行压缩改写。
目标:
- 在不改变原意的前提下,压缩冗余表达,减少长度;
- 保留消息中真正有价值的信息;
- 让压缩结果仍然像原消息,而不是另一种文体的摘要。
核心要求:
- 尽量保留原消息的视角、语气、态度、情绪与表达倾向,不要无故改写成中性、客观、旁白式总结。
- 不要把第一人称改成第三人称;不要把直接表达改成“用户表示……”“其意思是……”这类转述,除非原消息本身就是这种口吻。
- 若原消息包含明显的情绪、评价、犹豫、强调、否定、推进意图、反问、吐槽等内容,压缩后应尽量保留这些信息。
- 压缩的重点是删除冗余、合并重复、收紧表达,不是改写说话风格。
格式要求:
- 允许保留原消息中已有的 markdown、标题、项目符号、编号列表、引用、代码块、代码片段等结构。
- 不要为了压缩而强行去除这些结构;若这些结构本身承载了信息层级或语义边界,应尽量保留。
- 也不要为了“更整齐”主动新增原消息没有的标题、列表或代码块。
- 原消息有结构时,优先继承其组织方式;原消息没有结构时,保持自然文本即可。
压缩策略:
- 删除明显重复、空转、口头垫话、对主旨无帮助的展开。
- 合并语义接近、重复推进的句子。
- 保留真正影响理解的事实、判断、条件、限制、结论、态度和情绪。
- 若原消息包含技术内容、代码、配置、接口、规则、步骤等,优先保留这些实质信息,不要只保留泛泛结论。
- 若原消息本身已经很短或进一步压缩会损失重要语义,则可基本保持原样。
关于日志、代码及长文本片段:
- 若原消息中包含日志、代码、配置、报错堆栈、命令输出等长片段,且内容较长、重复性强或并非全部都对理解当前消息同等重要,则可以进行截断。
- 截断时应优先保留:
- 与当前问题、判断、结论直接相关的部分;
- 首尾中能体现上下文和结果的关键部分;
- 报错、异常、返回值、状态变化、关键参数、关键命令、关键代码段。
- 不要无说明地直接删去中间内容;若发生截断,必须显式标注。
- 截断标注统一使用以下格式之一,并与原文风格保持尽量一致:
- `...[中间内容已截断]...`
- 代码或日志块内可使用:`// ...[中间内容已截断]...` 或 `# ...[中间内容已截断]...`
- 截断后的内容仍应保持可读,且不能歪曲原始含义。
- 若长片段本身就是当前消息的核心,且截断会损失关键语义,则不要截断。
禁止事项:
- 不要补充原消息没有的新信息。
- 不要替原消息做解释、分析、总结或评价。
- 不要把技术表达改写得过于口语化,也不要把口语表达改写得过于书面化。
- 不要输出“压缩后:”之类前缀,只直接输出压缩结果。
输出要求:
- 只输出压缩后的消息正文。
""";
private static final int COMPRESS_TRIGGER_LENGTH = 1200;
private static final int FALLBACK_MAX_LENGTH = 900;
private static final String FALLBACK_OMITTED_MARKER = "\n...[中间内容已裁剪]...\n";
private static final String COMPRESSED_MARKER = "[COMPRESSED]";
private static final String UNKNOWN_ROLE_MARKER = "[[Unknown]: Unknown]";
private static final String MARKER_BODY_SEPARATOR = ":\n\n";
private static final Pattern ROLE_PREFIX_PATTERN = Pattern.compile("(\\[\\[(?:USER|AGENT)]:\\s*[^]]+])");
@InjectCapability
private ActionCapability actionCapability;
private ExecutorService executor;
@Init
public void init() {
executor = actionCapability.getExecutor(ActionCore.ExecutorType.VIRTUAL);
}
@Override
protected Void doExecute(List<Message> chatMessages) {
List<Integer> targetIndexes = IntStream.range(0, chatMessages.size())
.filter(index -> shouldCompress(chatMessages.get(index)))
.boxed()
.toList();
CountDownLatch latch = new CountDownLatch(targetIndexes.size());
for (Integer index : targetIndexes) {
Message chatMessage = chatMessages.get(index);
ParsedMessage parsedMessage = parseMessage(chatMessage.getContent());
executor.execute(() -> {
try {
String summarized = summarizeOrFallback(parsedMessage.body());
chatMessages.set(index, new Message(chatMessage.getRole(), rebuildMessage(parsedMessage, summarized)));
} finally {
latch.countDown();
}
});
}
try {
latch.await();
} catch (InterruptedException ignored) {
Thread.currentThread().interrupt();
}
return null;
}
private boolean shouldCompress(Message chatMessage) {
return parseMessage(chatMessage.getContent()).body().length() > COMPRESS_TRIGGER_LENGTH;
}
private String summarizeOrFallback(String content) {
String summarized = chat(List.of(new Message(Message.Character.USER, content))).fold(
res -> res,
exp -> null
);
if (isAcceptableSummary(summarized, content)) {
return summarized.trim();
}
return truncateForFallback(content);
}
private boolean isAcceptableSummary(String summarized, String originalContent) {
if (summarized == null) {
return false;
}
String normalized = summarized.trim();
return !normalized.isEmpty() && normalized.length() < originalContent.length();
}
private String truncateForFallback(String content) {
if (content == null || content.length() <= FALLBACK_MAX_LENGTH) {
return content;
}
int available = FALLBACK_MAX_LENGTH - FALLBACK_OMITTED_MARKER.length();
int headBudget = available / 2;
int tailBudget = available - headBudget;
int headEnd = adjustHeadEnd(content, headBudget);
int tailStart = adjustTailStart(content, content.length() - tailBudget);
if (tailStart <= headEnd) {
return content.substring(0, FALLBACK_MAX_LENGTH).stripTrailing();
}
return content.substring(0, headEnd).stripTrailing()
+ FALLBACK_OMITTED_MARKER
+ content.substring(tailStart).stripLeading();
}
private int adjustHeadEnd(String content, int preferredEnd) {
int safePreferredEnd = Math.clamp(preferredEnd, 0, content.length());
int windowEnd = Math.min(content.length(), safePreferredEnd + 80);
for (int i = safePreferredEnd; i < windowEnd; i++) {
if (isBoundary(content.charAt(i))) {
return i + 1;
}
}
return safePreferredEnd;
}
private int adjustTailStart(String content, int preferredStart) {
int safePreferredStart = Math.clamp(preferredStart, 0, content.length());
int windowStart = Math.max(0, safePreferredStart - 80);
for (int i = safePreferredStart; i > windowStart; i--) {
if (isBoundary(content.charAt(i - 1))) {
return i;
}
}
return safePreferredStart;
}
private boolean isBoundary(char ch) {
return ch == '\n'
|| ch == '。'
|| ch == ''
|| ch == ''
|| ch == ';'
|| ch == ''
|| ch == '.';
}
private ParsedMessage parseMessage(String content) {
String source = content == null ? "" : content;
int separatorIndex = source.indexOf(MARKER_BODY_SEPARATOR);
String markerLine = separatorIndex >= 0 ? source.substring(0, separatorIndex).trim() : "";
String remaining = separatorIndex >= 0 ? source.substring(separatorIndex + MARKER_BODY_SEPARATOR.length()).trim() : source.trim();
String rolePrefix = null;
String statusMarkers = "";
Matcher roleMatcher = ROLE_PREFIX_PATTERN.matcher(markerLine);
if (roleMatcher.find()) {
rolePrefix = roleMatcher.group(1);
statusMarkers = markerLine.substring(roleMatcher.end()).trim();
if (statusMarkers.startsWith(":")) {
statusMarkers = statusMarkers.substring(1).trim();
}
if (statusMarkers.endsWith(":")) {
statusMarkers = statusMarkers.substring(0, statusMarkers.length() - 1).trim();
}
}
return new ParsedMessage(rolePrefix, statusMarkers, remaining);
}
private String rebuildMessage(ParsedMessage parsedMessage, String compressedBody) {
return buildMarkerHeader(parsedMessage.rolePrefix(), parsedMessage.statusMarkers())
+ MARKER_BODY_SEPARATOR
+ compressedBody;
}
private String buildMarkerHeader(String rolePrefix, String statusMarkers) {
String identityMarker = rolePrefix == null || rolePrefix.isBlank() ? UNKNOWN_ROLE_MARKER : rolePrefix;
String normalizedStatusMarkers = statusMarkers == null ? "" : statusMarkers.trim();
normalizedStatusMarkers = normalizedStatusMarkers.replace(COMPRESSED_MARKER, "").trim();
normalizedStatusMarkers += COMPRESSED_MARKER;
return identityMarker + ": " + normalizedStatusMarkers;
}
@Override
@NotNull
public List<Message> modulePrompt() {
return List.of(new Message(Message.Character.SYSTEM, MODULE_PROMPT));
}
@NotNull
@Override
public String modelKey() {
return "single_summarizer";
}
private record ParsedMessage(String rolePrefix, String statusMarkers, String body) {
}
}

View File

@@ -0,0 +1,89 @@
package work.slhaf.partner.module.communication.summarizer;
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.framework.agent.factory.capability.annotation.InjectCapability;
import work.slhaf.partner.framework.agent.factory.component.abstracts.AbstractAgentModule;
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 java.util.List;
public class MessageSummarizer extends AbstractAgentModule.Sub<List<Message>, Result<String>> implements ActivateModel {
private static final String MODULE_PROMPT = """
你负责对一组已经发生过的聊天消息进行总结整理,生成一段可供后续系统使用的摘要结果。
你会收到一条结构化任务消息,其中:
- <message_tag_notes> 说明聊天消息中可能出现的标签及其含义;
- <chat_messages> 承载本次需要总结的消息列表,每条消息都带有 role 与正文内容。
你的任务:
- 基于整组消息,提炼出一段紧凑、连贯、信息完整的摘要;
- 摘要应尽量覆盖这组消息中的主要事实、结论、约束、推进情况、未决点、明显态度与情绪变化;
- 若消息中包含技术讨论、配置、代码、报错、规则、方案比较、设计判断等内容,应优先保留这些对后续理解真正有帮助的信息。
摘要视角要求:
- 摘要默认采用 AGENT 视角书写,即以“我”的立场整理这组对话,而不是使用外部旁观者口吻。
- 对于来自 [AGENT] 或 assistant 的消息,可将其理解为我的表达、我的判断、我的推进、我的反思或我的内部反馈,并以“我”来概括。
- 对于来自 [USER] 的消息,应明确保留其“用户”身份,不要模糊为无来源的陈述,也不要误写成“我”的观点。
- 不要默认把整组消息改写成“用户近期……”“系统如何……”这类第三人称阶段报告,除非原消息本身就是这种汇报视角。
- 若消息中出现 [NOT_REPLIED],表示这是一条我未直接发给用户、但保留在交流轨迹中的内部交流结果;必要时可在摘要中说明这是我内部保留的判断或反馈。
总结原则:
- 重点提炼这组消息中真正影响后续理解和推进的信息,不要平均分配篇幅。
- 合并重复表达、重复确认和多轮来回拉扯后的同类结论。
- 若消息中形成了明确结论、决定、偏好、限制条件、行动推进或阶段性判断,应优先写出。
- 若消息中仍存在未解决问题、待确认事项、分歧点或风险点,也应明确保留。
- 若消息整体只是闲聊、感叹或状态表达,也应如实概括其主要情绪和交流走向,不要硬总结出不存在的任务结论。
关于技术内容:
- 若消息中包含代码、日志、命令输出、配置片段等长内容,不要原样大段复写;
- 应概括其中真正关键的信息,例如:关键报错、关键配置、关键判断、关键修改点、关键结果。
- 只有在少量原文片段对后续理解不可替代时,才可保留必要短句。
输出要求:
- 只输出一段摘要正文,不要添加标题、前缀、说明或额外标签。
- 不要输出项目符号列表,除非原始内容极度结构化且不用列表会明显损失可读性。
- 不要输出结构之外的解释、注释或额外文本。
""";
@InjectCapability
private CognitionCapability cognitionCapability;
@Override
protected @NotNull Result<String> doExecute(List<Message> messages) {
return chat(List.of(buildChatMessagesBlock(messages).encodeToMessage()));
}
private @NotNull TaskBlock buildChatMessagesBlock(List<Message> messages) {
return new TaskBlock() {
@Override
protected void fillXml(@NotNull Document document, @NotNull Element root) {
document.appendChild(document.importNode(cognitionCapability.messageNotesElement(), true));
appendListElement(document, root, "chat_messages", "message", messages, (element, message) -> {
element.setAttribute("role", message.roleValue());
element.setTextContent(message.getContent());
return Unit.INSTANCE;
});
}
};
}
@Override
@NotNull
public List<Message> modulePrompt() {
return List.of(new Message(Message.Character.SYSTEM, MODULE_PROMPT));
}
@NotNull
@Override
public String modelKey() {
return "multi_summarizer";
}
}

View File

@@ -1,32 +0,0 @@
package work.slhaf.partner.module.communication.summarizer;
import cn.hutool.json.JSONUtil;
import org.jetbrains.annotations.NotNull;
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.memory.runtime.MemoryRuntime;
import java.util.List;
public class MultiSummarizer extends AbstractAgentModule.Sub<List<Message>, Result<String>> implements ActivateModel {
@InjectModule
private MemoryRuntime memoryRuntime;
@Override
protected @NotNull Result<String> doExecute(List<Message> messages) {
return chat(List.of(new Message(
Message.Character.USER,
JSONUtil.toJsonPrettyStr(messages)))
);
}
@NotNull
@Override
public String modelKey() {
return "multi_summarizer";
}
}

View File

@@ -1,63 +0,0 @@
package work.slhaf.partner.module.communication.summarizer;
import org.jetbrains.annotations.NotNull;
import work.slhaf.partner.core.action.ActionCapability;
import work.slhaf.partner.core.action.ActionCore;
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.Init;
import work.slhaf.partner.framework.agent.model.ActivateModel;
import work.slhaf.partner.framework.agent.model.pojo.Message;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
public class SingleSummarizer extends AbstractAgentModule.Sub<List<Message>, Void> implements ActivateModel {
@InjectCapability
private ActionCapability actionCapability;
private ExecutorService executor;
@Init
public void init() {
executor = actionCapability.getExecutor(ActionCore.ExecutorType.VIRTUAL);
}
@Override
protected Void doExecute(List<Message> chatMessages) {
CountDownLatch latch = new CountDownLatch(chatMessages.size());
for (int i = 0; i < chatMessages.size(); i++) {
Message chatMessage = chatMessages.get(i);
if (chatMessage.getRole() == Message.Character.ASSISTANT) {
String content = chatMessage.getContent();
if (chatMessage.getContent().length() > 500) {
int index = i;
executor.execute(() -> {
try {
String summarized = chat(List.of(new Message(Message.Character.USER, content))).fold(
res -> res,
exp -> content
);
chatMessages.set(index, new Message(Message.Character.ASSISTANT, summarized));
} finally {
latch.countDown();
}
});
}
}
}
try {
latch.await();
} catch (InterruptedException ignored) {
}
return null;
}
@NotNull
@Override
public String modelKey() {
return "single_summarizer";
}
}

View File

@@ -1,6 +1,7 @@
package work.slhaf.partner.module.communication; package work.slhaf.partner.module.communication;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.w3c.dom.Element;
import work.slhaf.partner.core.cognition.CognitionCapability; import work.slhaf.partner.core.cognition.CognitionCapability;
import work.slhaf.partner.core.cognition.ContextWorkspace; import work.slhaf.partner.core.cognition.ContextWorkspace;
import work.slhaf.partner.framework.agent.model.pojo.Message; import work.slhaf.partner.framework.agent.model.pojo.Message;
@@ -51,8 +52,8 @@ class CommunicationProducerTest {
List<Message> chatMessages = cognitionCapability.getChatMessages(); List<Message> chatMessages = cognitionCapability.getChatMessages();
assertEquals(2, chatMessages.size()); assertEquals(2, chatMessages.size());
assertEquals("[[USER]: user-1]: hello", chatMessages.get(0).getContent()); assertEquals("[[USER]: user-1]:\n\nhello", chatMessages.get(0).getContent());
assertEquals("[NOT_REPLIED]: not now", chatMessages.get(1).getContent()); assertEquals("[[AGENT]: self]: [NOT_REPLIED]:\n\nnot now", chatMessages.get(1).getContent());
} }
@Test @Test
@@ -68,7 +69,7 @@ class CommunicationProducerTest {
); );
List<Message> chatMessages = cognitionCapability.getChatMessages(); List<Message> chatMessages = cognitionCapability.getChatMessages();
assertEquals("normal reply", chatMessages.get(1).getContent()); assertEquals("[[AGENT]: self]:\n\nnormal reply", chatMessages.get(1).getContent());
} }
private static final class StubCognitionCapability implements CognitionCapability { private static final class StubCognitionCapability implements CognitionCapability {
@@ -103,6 +104,11 @@ class CommunicationProducerTest {
public void refreshRecentChatMessagesContext() { public void refreshRecentChatMessagesContext() {
} }
@Override
public Element messageNotesElement() {
return null;
}
@Override @Override
public Lock getMessageLock() { public Lock getMessageLock() {
return lock; return lock;

View File

@@ -9,8 +9,8 @@ import work.slhaf.partner.core.memory.pojo.MemorySlice;
import work.slhaf.partner.core.memory.pojo.MemoryUnit; import work.slhaf.partner.core.memory.pojo.MemoryUnit;
import work.slhaf.partner.framework.agent.model.pojo.Message; import work.slhaf.partner.framework.agent.model.pojo.Message;
import work.slhaf.partner.framework.agent.support.Result; import work.slhaf.partner.framework.agent.support.Result;
import work.slhaf.partner.module.communication.summarizer.MultiSummarizer; import work.slhaf.partner.module.communication.summarizer.MessageCompressor;
import work.slhaf.partner.module.communication.summarizer.SingleSummarizer; import work.slhaf.partner.module.communication.summarizer.MessageSummarizer;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.nio.file.Path; import java.nio.file.Path;
@@ -42,13 +42,13 @@ class DialogRollingTest {
String sessionId = "dialog-rolling-" + UUID.randomUUID(); String sessionId = "dialog-rolling-" + UUID.randomUUID();
StubMemoryCapability memoryCapability = new StubMemoryCapability(sessionId); StubMemoryCapability memoryCapability = new StubMemoryCapability(sessionId);
DialogRolling dialogRolling = new DialogRolling(); DialogRolling dialogRolling = new DialogRolling();
MultiSummarizer multiSummarizer = Mockito.mock(MultiSummarizer.class); MessageSummarizer messageSummarizer = Mockito.mock(MessageSummarizer.class);
SingleSummarizer singleSummarizer = Mockito.mock(SingleSummarizer.class); MessageCompressor messageCompressor = Mockito.mock(MessageCompressor.class);
setField(dialogRolling, "memoryCapability", memoryCapability); setField(dialogRolling, "memoryCapability", memoryCapability);
setField(dialogRolling, "multiSummarizer", multiSummarizer); setField(dialogRolling, "messageSummarizer", messageSummarizer);
setField(dialogRolling, "singleSummarizer", singleSummarizer); setField(dialogRolling, "messageCompressor", messageCompressor);
when(multiSummarizer.execute(Mockito.any())).thenReturn(Result.success("new-summary")); when(messageSummarizer.execute(Mockito.any())).thenReturn(Result.success("new-summary"));
MemoryUnit existingUnit = new MemoryUnit(sessionId); MemoryUnit existingUnit = new MemoryUnit(sessionId);
existingUnit.getConversationMessages().addAll(List.of( existingUnit.getConversationMessages().addAll(List.of(
@@ -83,13 +83,13 @@ class DialogRollingTest {
String sessionId = "dialog-rolling-" + UUID.randomUUID(); String sessionId = "dialog-rolling-" + UUID.randomUUID();
StubMemoryCapability memoryCapability = new StubMemoryCapability(sessionId); StubMemoryCapability memoryCapability = new StubMemoryCapability(sessionId);
DialogRolling dialogRolling = new DialogRolling(); DialogRolling dialogRolling = new DialogRolling();
MultiSummarizer multiSummarizer = Mockito.mock(MultiSummarizer.class); MessageSummarizer messageSummarizer = Mockito.mock(MessageSummarizer.class);
SingleSummarizer singleSummarizer = Mockito.mock(SingleSummarizer.class); MessageCompressor messageCompressor = Mockito.mock(MessageCompressor.class);
setField(dialogRolling, "memoryCapability", memoryCapability); setField(dialogRolling, "memoryCapability", memoryCapability);
setField(dialogRolling, "multiSummarizer", multiSummarizer); setField(dialogRolling, "messageSummarizer", messageSummarizer);
setField(dialogRolling, "singleSummarizer", singleSummarizer); setField(dialogRolling, "messageCompressor", messageCompressor);
when(multiSummarizer.execute(Mockito.any())).thenReturn(Result.success("fresh-summary")); when(messageSummarizer.execute(Mockito.any())).thenReturn(Result.success("fresh-summary"));
RollingResult rollingResult = dialogRolling.buildRollingResult(List.of( RollingResult rollingResult = dialogRolling.buildRollingResult(List.of(
message(Message.Character.USER, "first"), message(Message.Character.USER, "first"),
@@ -138,13 +138,13 @@ class DialogRollingTest {
String sessionId = "dialog-rolling-" + UUID.randomUUID(); String sessionId = "dialog-rolling-" + UUID.randomUUID();
StubMemoryCapability memoryCapability = new StubMemoryCapability(sessionId); StubMemoryCapability memoryCapability = new StubMemoryCapability(sessionId);
DialogRolling dialogRolling = new DialogRolling(); DialogRolling dialogRolling = new DialogRolling();
MultiSummarizer multiSummarizer = Mockito.mock(MultiSummarizer.class); MessageSummarizer messageSummarizer = Mockito.mock(MessageSummarizer.class);
SingleSummarizer singleSummarizer = Mockito.mock(SingleSummarizer.class); MessageCompressor messageCompressor = Mockito.mock(MessageCompressor.class);
setField(dialogRolling, "memoryCapability", memoryCapability); setField(dialogRolling, "memoryCapability", memoryCapability);
setField(dialogRolling, "multiSummarizer", multiSummarizer); setField(dialogRolling, "messageSummarizer", messageSummarizer);
setField(dialogRolling, "singleSummarizer", singleSummarizer); setField(dialogRolling, "messageCompressor", messageCompressor);
when(multiSummarizer.execute(Mockito.any())).thenReturn(Result.success(" ")); when(messageSummarizer.execute(Mockito.any())).thenReturn(Result.success(" "));
RollingResult rollingResult = dialogRolling.buildRollingResult(List.of( RollingResult rollingResult = dialogRolling.buildRollingResult(List.of(
message(Message.Character.USER, "u1"), message(Message.Character.USER, "u1"),

View File

@@ -6,6 +6,7 @@ import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir; import org.junit.jupiter.api.io.TempDir;
import org.w3c.dom.Element;
import work.slhaf.partner.core.cognition.CognitionCapability; import work.slhaf.partner.core.cognition.CognitionCapability;
import work.slhaf.partner.core.memory.MemoryCapability; import work.slhaf.partner.core.memory.MemoryCapability;
import work.slhaf.partner.core.memory.pojo.MemorySlice; import work.slhaf.partner.core.memory.pojo.MemorySlice;
@@ -93,6 +94,11 @@ class MemoryRuntimeTest {
} }
@Override
public Element messageNotesElement() {
return null;
}
@Override @Override
public Lock getMessageLock() { public Lock getMessageLock() {
return lock; return lock;