Solon v3.9.3

team - 示例3 - 持久化与恢复

</> markdown
2026年2月14日 下午9:07:58

模拟场景:Agent 团队在执行中途状态被存入数据库,随后重启并从断点恢复执行。

示例代码

示例也可以改造成这样的编排:

id: persistence_team
layout:
  - {id: 'start', type: 'start', link: 'searcher'}
  - {id: 'searcher', type: 'activity', task: '@searcher', link: 'router'}
  - {id: 'router', type: 'exclusive', task: '@router',
     meta: {agentNames: ['planner']},
     link: [{nextId: 'planner', when: '"planner".equals(next_agent)'},
            {nextId: 'end'}
     ]
  }
  - {id: 'planner', type: 'activity', task: '@planner', link: 'end'}
  - {id: 'end',  type: 'end'}

示例代码:

import demo.ai.llm.LlmUtil;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.noear.solon.ai.agent.Agent;
import org.noear.solon.ai.agent.AgentSession;
import org.noear.solon.ai.agent.react.ReActAgent;
import org.noear.solon.ai.agent.session.InMemoryAgentSession;
import org.noear.solon.ai.agent.team.TeamAgent;
import org.noear.solon.ai.agent.team.TeamResponse;
import org.noear.solon.ai.agent.team.TeamTrace;
import org.noear.solon.ai.chat.ChatModel;
import org.noear.solon.ai.chat.ChatRole;
import org.noear.solon.ai.chat.prompt.Prompt;

/**
 * 状态持久化与断点续跑测试
 * <p>验证:当 Agent 系统发生崩溃或主动挂起后,能够通过序列化快照重建上下文记忆并继续后续决策。</p>
 */
public class TeamAgentPersistenceAndResumeTest {

    @Test
    public void testPersistenceAndResume() throws Throwable {
        ChatModel chatModel = LlmUtil.getChatModel();
        String teamName = "persistent_trip_manager";

        // 1. 构建一个带有自定义流程的团队
        TeamAgent tripAgent = TeamAgent.of(chatModel)
                .name(teamName)
                .graphAdjuster(spec -> {
                    // 自定义流程:Start -> searcher -> Supervisor (决策后续)
                    spec.addStart(Agent.ID_START).linkAdd("searcher");
                    spec.addActivity(ReActAgent.of(chatModel)
                                    .name("searcher")
                                    .role("天气搜索员")
                                    .instruction("负责提供实时气候数据,并为后续行程规划提供依据")
                                    .build())
                            .linkAdd(TeamAgent.ID_SUPERVISOR);
                }).build();

        // --- 阶段 A:模拟第一阶段执行并手动构建持久化快照 ---
        // 假设我们在另一台机器上运行,执行完 searcher 后,我们将状态序列化到 DB
        InMemoryAgentSession session = InMemoryAgentSession.of("order_sn_998");

        // 手动模拟 Trace 状态:已经完成了天气搜索
        TeamTrace trace = new TeamTrace(Prompt.of("帮我规划上海行程并给穿衣建议"));
        trace.addRecord(ChatRole.ASSISTANT, "searcher", "上海明日天气:大雨转雷阵雨,气温 12 度。", 800L);
        // 设置当前路由断点为 Supervisor,准备让它恢复后进行决策
        trace.setRoute(TeamAgent.ID_SUPERVISOR);

        // 将轨迹存入上下文,key 遵循框架规范 "__" + teamName
        session.getSnapshot().put("__" + teamName, trace);

        // 模拟落库序列化(JSON)
        String jsonState = session.getSnapshot().toJson();
        System.out.println(">>> 阶段 A:初始状态已持久化至数据库。当前断点:" + trace.getRoute());

        // --- 阶段 B:从持久化数据恢复并续跑 ---
        System.out.println("\n>>> 阶段 B:正在从 JSON 快照恢复任务...");

        // 从 JSON 重建 FlowContext,并包装成新的 AgentSession

        // 验证恢复:调用时不传 Prompt,触发“断点续跑”模式
        TeamResponse resp = tripAgent.prompt()
                .session(session)
                .call();

        // --- 阶段 C:核心验证 ---

        // 验证 1:状态恢复完整性
        Assertions.assertNotNull(resp.getTrace(), "恢复后的轨迹不应为空");
        Assertions.assertTrue(resp.getTrace().getRecordCount() >= 2, "轨迹应包含预设的 searcher 步及后续生成步");

        // 验证 2:历史记忆持久性(Agent 是否还记得 searcher 提供的数据)
        boolean remembersWeather = resp.getTrace().getFormattedHistory().contains("上海明日天气");
        Assertions.assertTrue(remembersWeather, "恢复后的 Agent 应该记得快照中的天气信息");

        // 验证 3:最终决策结果
        Assertions.assertNotNull(resp.getContent());
        System.out.println("恢复执行后的最终答复: " + resp.getContent());
    }

    @Test
    public void testResetOnNewPrompt() throws Throwable {
        // 测试:在新提示词驱动下,Session 是否会自动开启新轨迹
        ChatModel chatModel = LlmUtil.getChatModel();
        TeamAgent team = TeamAgent.of(chatModel)
                .name("reset_test_team")
                .agentAdd(ReActAgent.of(chatModel).name("agent")
                        .role("智能助手")
                        .instruction("根据用户提示词提供帮助")
                        .build())
                .build();

        AgentSession session = InMemoryAgentSession.of("test_reset_id");

        // 第一次调用:建立初始上下文
        TeamResponse resp = team.prompt("你好").session(session).call();
        Assertions.assertNotNull(resp.getTrace());

        // 第二次调用:传入完全不同的 Prompt
        String result2 = team.prompt("再见").session(session).call().getContent();

        Assertions.assertNotNull(result2);
        System.out.println("第二次调用成功完成");
    }

    @Test
    public void testContextStateIsolation() throws Throwable {
        // 测试:不同 Session 实例之间的完全状态隔离
        ChatModel chatModel = LlmUtil.getChatModel();
        TeamAgent team = TeamAgent.of(chatModel)
                .name("isolation_team")
                .agentAdd(ReActAgent.of(chatModel).name("agent")
                        .role("隔离测试助手")
                        .instruction("识别并引用上下文中的变量")
                        .build())
                .build();

        // 创建两个独立的 Session
        AgentSession session1 = InMemoryAgentSession.of("session_1");
        AgentSession session2 = InMemoryAgentSession.of("session_2");

        // 分别注入私有状态
        session1.getSnapshot().put("user_name", "张三");
        session2.getSnapshot().put("user_name", "李四");

        // 执行调用
        team.prompt("谁在和你说话?").session(session1).call();
        team.prompt("谁在和你说话?").session(session2).call();

        Assertions.assertNotEquals(
                session1.getSnapshot().get("user_name"),
                session2.getSnapshot().get("user_name"),
                "不同会话的私有变量必须物理隔离"
        );
    }
}