package com.xly.agent; import com.xly.service.EventProjectionService; import com.xly.service.LedgerService; import dev.langchain4j.data.message.AiMessage; import dev.langchain4j.data.message.ChatMessage; import dev.langchain4j.agent.tool.ToolExecutionRequest; import dev.langchain4j.data.message.ChatMessageSerializer; import dev.langchain4j.data.message.SystemMessage; import dev.langchain4j.data.message.ToolExecutionResultMessage; import dev.langchain4j.data.message.UserMessage; import dev.langchain4j.memory.ChatMemory; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; /** * 事件日志上的对话记忆:{@link #add} 把模型环消息逐条 append 成事件(rightPush 原子—— * 对话流与确认端点并发写互不覆盖,替代旧 chat:mem 整包读改写),{@link #messages()} 读取 * {@link EventProjectionService} 的四段式投影。日志即唯一事实源,本类不持有任何会话状态。 * *
写者分工:用户事件由控制器在收到请求时先落账(前端立即可见),本类对 UserMessage 做
* 去重跳过;{@code internalUserTurn=true} 时(反编造护栏重试的注入话术)例外——落一条
* {@code internal} 标记的用户事件,LLM 可见、前端历史不显示。模型消息(tool_call/tool_result/ai)
* 只由本类落账。
*/
public class EventLogChatMemory implements ChatMemory {
private static final int LLM_EVENT_WINDOW = 400;
private final String convId;
private final LedgerService log;
private final EventProjectionService projection;
private final int charBudget;
private final boolean internalUserTurn;
/** system prompt 每次由 provider 提供,不落日志(保持日志纯业务事件)。 */
private volatile String systemText;
/**
* 本轮实际喂给模型的用户文本(原话 + 编排层附加的 grounding/状态后缀)。日志只存原话;
* 投影时把当前轮用户消息替换为它。P2 编排层不再加后缀后,此字段恒空。
*/
private volatile String currentTurnUserText;
public EventLogChatMemory(String convId, LedgerService log, EventProjectionService projection,
int charBudget, boolean internalUserTurn) {
this.convId = convId;
this.log = log;
this.projection = projection;
this.charBudget = charBudget;
this.internalUserTurn = internalUserTurn;
}
@Override
public Object id() {
return convId;
}
@Override
public void add(ChatMessage m) {
if (m instanceof SystemMessage sm) {
systemText = sm.text();
return;
}
if (m instanceof UserMessage um) {
String text = um.hasSingleText() ? um.singleText() : String.valueOf(um.contents());
if (!internalUserTurn && alreadyLoggedPrefixOf(text)) {
currentTurnUserText = text;
return;
}
Map