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.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 成事件(追加原子—— * 对话流与按钮端点并发写互不覆盖),{@link #messages()} 读取 * {@link EventProjectionService} 的四段式 token 记账投影。账本即唯一事实源,本类不持有任何会话状态。 * *
写者分工:用户事件由控制器在收到请求时先落账(前端立即可见),本类对 UserMessage 做 * 去重跳过;{@code internalUserTurn=true} 时(反编造护栏重试的注入话术)例外——落一条 * {@code internal} 标记的用户事件,LLM 可见、前端历史不显示。模型消息(tool_call/tool_result/ai) * 只由本类落账。 * *
tool_call 事件存**中立格式** {@code calls=[{id,name,args}]}(脱 LangChain4j 序列化耦合),
* 旧版 {@code payload} 载荷投影层仍可读。
*/
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 AgentIdentity identity;
private final boolean internalUserTurn;
/** system prompt 每次由 provider 提供,不落账(保持账本纯业务事件)。 */
private volatile String systemText;
/**
* 本轮实际喂给模型的用户文本(原话 + 编排层附加的后缀)。账本只存原话;
* 投影时把当前轮用户消息替换为它。P2 编排层不再加后缀后,此字段恒空。
*/
private volatile String currentTurnUserText;
public EventLogChatMemory(String convId, LedgerService log, EventProjectionService projection,
AgentIdentity identity, boolean internalUserTurn) {
this.convId = convId;
this.log = log;
this.projection = projection;
this.identity = identity;
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