gogo-agent

非ReActAgent如何做Session持久化

无状态 Agent,比如我们的QueryRewritingAgent、IntentRecognitionAgent 这类"单次调用、不持有 Memory"的分析型 Agent。我们也需要针对他们做session的持久化,但是这类Ag…

TL;DR

无状态 Agent,比如我们的QueryRewritingAgent、IntentRecognitionAgent 这类"单次调用、不持有 Memory"的分析型 Agent。我们也需要针对他们做session的持久化,但是这类Ag…

无状态 Agent,比如我们的QueryRewritingAgent、IntentRecognitionAgent 这类"单次调用、不持有 Memory"的分析型 Agent。我们也需要针对他们做session的持久化,但是这类Agent他并不是StateModule,无法直接做session的读取和存储。 于是,我们在SessionPersistenceHook需要针对这种情况做特殊处理:

/**
     * 非 ReActAgent 的 AgentBase(单次调用型分析 Agent,如 QueryRewritingAgent、IntentRecognitionAgent)
     * 的 Session 持久化。
     *
     * <p>这类 Agent 每次 {@code doCall} 都是「系统提示词 + 本轮输入」的一次性调用,自身不维护 Memory,
     * 因此这里绕开 StateModule(默认 no-op),直接读写 {@link Session} 的原始消息列表:
     * <ul>
     *   <li>{@link PreCallEvent}:读取此前累计的历史消息,与本轮输入拼接后写回
     *       {@link PreCallEvent#setInputMessages},使 {@code doCall} 能感知历史上下文;</li>
     *   <li>{@link PostCallEvent}:取出 PreCall 阶段暂存的「历史 + 本轮输入」,追加本轮回复后
     *       整体写回 Session,供下一轮加载;超过 {@link #MAX_HISTORY_MESSAGES} 时从头部裁剪。</li>
     * </ul>
     */
    private void persistStatelessAgent(HookEvent event, String sessionKey, String sessionId,
                                       String agentName, boolean resuming) {
        SessionKey key = SimpleSessionKey.of(sessionKey);

        if (event instanceof PreCallEvent preCallEvent) {
            if (resuming) {
                logger.debug("[SessionPersistence] 恢复暂停 Agent,跳过历史记忆加载: sessionId={}, agent={}", sessionId, agentName);
                return;
            }
            List<Msg> history = session.exists(key)
                    ? session.getList(key, MEMORY_MESSAGES_KEY, Msg.class)
                    : List.<Msg>of();
            List<Msg> merged = new ArrayList<>(history);
            merged.addAll(preCallEvent.getInputMessages());
            if (!history.isEmpty()) {
                preCallEvent.setInputMessages(merged);
                logger.debug("[SessionPersistence] 已加载历史记忆(无状态 Agent): sessionId={}, agent={}, historySize={}",
                        sessionId, agentName, history.size());
            }
            // 暂存本轮「历史 + 输入」,供 PostCall 追加回复后一并写回 Session
            pendingStatelessInput.put(sessionKey, merged);
        } else {
            PostCallEvent postCallEvent = (PostCallEvent) event;
            List<Msg> merged = pendingStatelessInput.remove(sessionKey);
            List<Msg> newHistory = merged != null ? new ArrayList<>(merged) : new ArrayList<>();
            newHistory.add(postCallEvent.getFinalMessage());
            if (newHistory.size() > MAX_HISTORY_MESSAGES) {
                newHistory = new ArrayList<>(
                        newHistory.subList(newHistory.size() - MAX_HISTORY_MESSAGES, newHistory.size()));
            }
            session.save(key, MEMORY_MESSAGES_KEY, newHistory);
            logger.debug("[SessionPersistence] 已保存对话记忆(无状态 Agent): sessionId={}, agent={}, size={}",
                    sessionId, agentName, newHistory.size());
        }
    }

它还暴露了 appendStatelessHistory(sessionId, agentName, userMsg, assistantMsg):当快路径(如意图直接命中、跳过了某无状态 Agent 的真实 call())也想让该 Agent"以为自己被调用过",可合成一对 USER→ASSISTANT 消息写入其历史,存储契约与上面完全一致,下一轮 PreCall 能无缝加载。

/**
 * 对外暴露的「无状态 Agent 影子历史追加」接口。
 *
 * <p>适用于快路径(例如 L1/L2 意图命中)绕过了某个无状态 Agent 的实际 {@code call()},
 * 却又希望下一轮该 Agent 依然能读到本轮上下文的场景:调用方可以合成一对
 * {@code USER → ASSISTANT} 消息,通过本方法写入对应 Agent 的 Session 历史,
 * 效果等价于该 Agent 真正被调用过一次。
 *
 * <p>存储契约与 {@link #persistStatelessAgent} 完全一致(同 key、同 MEMORY_MESSAGES_KEY、
 * 同 MAX_HISTORY_MESSAGES 裁剪),因此下一轮 {@link PreCallEvent} 能被无缝加载。
 *
 * @param sessionId  对话会话 ID
 * @param agentName  目标无状态 Agent 名(如 {@code QueryRewritingAgent})
 * @param userMsg    合成的用户消息(不能为 null)
 * @param assistantMsg 合成的 Agent 回复消息(不能为 null)
 */
public void appendStatelessHistory(String sessionId, String agentName, Msg userMsg, Msg assistantMsg) {
    if (sessionId == null || agentName == null || userMsg == null || assistantMsg == null) {
        return;
    }
    String sessionKey = sessionId + ":" + agentName;
    SessionKey key = SimpleSessionKey.of(sessionKey);
    try {
        List<Msg> history = session.exists(key)
                ? session.getList(key, MEMORY_MESSAGES_KEY, Msg.class)
                : List.<Msg>of();
        List<Msg> newHistory = new ArrayList<>(history);
        newHistory.add(userMsg);
        newHistory.add(assistantMsg);
        if (newHistory.size() > MAX_HISTORY_MESSAGES) {
            newHistory = new ArrayList<>(
                    newHistory.subList(newHistory.size() - MAX_HISTORY_MESSAGES, newHistory.size()));
        }
        session.save(key, MEMORY_MESSAGES_KEY, newHistory);
        logger.debug("[SessionPersistence] 已追加影子历史(无状态 Agent): sessionId={}, agent={}, size={}",
                sessionId, agentName, newHistory.size());
    } catch (Exception e) {
        logger.warn("[SessionPersistence] 追加影子历史失败 sessionId={}, agent={}: {}",
                sessionId, agentName, e.getMessage());
    }
}

暂存用 ConcurrentHashMap> pendingStatelessInput:跨 PreCall→PostCall 传递本轮合并结果。若调用被中断导致 PostCall 未触发,残留条目会在该 key 下次成功 PreCall 时被覆盖,不会无限增长。

版本提示

模型、框架与接口会持续变化。涉及版本号、参数与生产配置时,请在实践前对照对应官方文档。

LLMentor系统化学习大模型应用工程

内容来自个人课程知识库备份,并经过结构化整理。技术版本持续演进,生产使用前请结合官方文档验证。