Solon v3.8.0

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

</> markdown
2026年1月4日 下午10:43:52

模拟场景: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.agent.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.team.TeamAgent;
import org.noear.solon.ai.agent.team.TeamTrace;
import org.noear.solon.ai.agent.react.ReActAgent;
import org.noear.solon.ai.chat.ChatModel;
import org.noear.solon.ai.chat.prompt.Prompt;
import org.noear.solon.flow.FlowContext;

/**
 * 状态持久化与断点续跑测试
 * 验证:系统崩溃或主动挂起后,能够完整恢复上下文记忆并继续后续决策。
 */
public class TeamAgentPersistenceAndResumeTest {

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

        TeamAgent tripAgent = TeamAgent.of(chatModel)
                .name(teamId)
                .addAgent(ReActAgent.of(chatModel)
                        .name("planner")
                        .title("行程规划")
                        .description("资深行程规划专家")
                        .build())
                .graphAdjuster(spec -> {
                    spec.addStart(Agent.ID_START).linkAdd("searcher");
                    spec.addActivity(ReActAgent.of(chatModel)
                                    .name("searcher")
                                    .title("天气搜索")
                                    .description("天气搜索员")
                                    .build())
                            .linkAdd(Agent.ID_SUPERVISOR);
                }).build();

        String yaml = tripAgent.getGraph().toYaml();

        System.out.println("------------------\n\n");
        System.out.println(yaml);
        System.out.println("\n\n------------------");


        // 1. 【模拟第一阶段:挂起】执行了搜索,状态存入 DB
        FlowContext contextStep1 = FlowContext.of("order_sn_998");
        contextStep1.trace().recordNodeId(tripAgent.getGraph(), Agent.ID_SUPERVISOR);

        TeamTrace snapshot = new TeamTrace(null, Prompt.of("帮我规划上海行程并给穿衣建议"));
        snapshot.addStep("searcher", "上海明日天气:大雨转雷阵雨,气温 12 度。", 800L);

        contextStep1.put("__" + teamId, snapshot);

        String jsonState = contextStep1.toJson(); // 模拟落库序列化
        System.out.println(">>> 阶段1完成:业务快照已持久化至数据库。");

        // 2. 【模拟第二阶段:恢复】从序列化数据中重建上下文
        FlowContext contextStep2 = FlowContext.fromJson(jsonState);
        System.out.println(">>> 阶段2启动:正在从断点 [" + contextStep2.lastNodeId() + "] 恢复任务...");

        String finalResult = tripAgent.call(contextStep2); // 传入 null 触发自动恢复

        // 3. 改进的测试断言
        TeamTrace finalTrace = contextStep2.getAs("__" + teamId);

        // 核心验证点1:状态恢复是否成功
        Assertions.assertNotNull(finalTrace, "应该能恢复轨迹");
        Assertions.assertTrue(finalTrace.getStepCount() >= 2,
                "轨迹应包含至少2步(searcher + planner)");

        // 核心验证点2:历史信息是否被保留
        boolean hasSearcherStep = finalTrace.getSteps().stream()
                .anyMatch(step -> "searcher".equals(step.getAgentName()) &&
                        step.getContent().contains("上海明日天气"));
        Assertions.assertTrue(hasSearcherStep, "快照中的searcher步骤应该被保留");

        // 核心验证点3:任务是否完成
        Assertions.assertNotNull(finalResult, "任务应该有结果");
        Assertions.assertFalse(finalResult.trim().isEmpty(), "结果不应该为空");

        // 核心验证点4:最终答案是否合理
        // 不再检查具体内容,因为Mediator可能只输出总结
        System.out.println(">>> 测试通过:状态恢复和任务完成验证成功");

        // 输出详细调试信息
        System.out.println("=== 恢复后轨迹详情 ===");
        System.out.println("总步数: " + finalTrace.getStepCount());
        System.out.println("轨迹内容: " + finalTrace.getFormattedHistory());
        System.out.println("最终结果: " + finalResult);
    }

    @Test
    public void testResetOnNewPrompt() throws Throwable {
        // 测试:resetOnNewPrompt 参数的效果
        ChatModel chatModel = LlmUtil.getChatModel();

        TeamAgent team = TeamAgent.of(chatModel)
                .name("reset_test_team")
                .addAgent(ReActAgent.of(chatModel)
                        .name("agent")
                        .description("测试Agent")
                        .build())
                .build();

        FlowContext context = FlowContext.of("test_reset");

        // 第一次调用
        String result1 = team.call(context, "第一个问题");
        System.out.println("第一次结果: " + result1);

        // 获取轨迹
        Object trace1 = context.get("__reset_test_team");
        Assertions.assertNotNull(trace1);

        // 第二次调用(新提示词,应该重置)
        String result2 = team.call(context, "第二个问题");
        System.out.println("第二次结果: " + result2);

        // 应该开始新的轨迹
        // 这里可以添加更详细的检查逻辑
    }

    @Test
    public void testContextStateIsolation() throws Throwable {
        // 测试:不同 FlowContext 之间的状态隔离
        ChatModel chatModel = LlmUtil.getChatModel();

        TeamAgent team = TeamAgent.of(chatModel)
                .name("isolation_team")
                .addAgent(ReActAgent.of(chatModel)
                        .name("agent")
                        .description("测试Agent")
                        .build())
                .build();

        // 两个独立的上下文
        FlowContext context1 = FlowContext.of("session_1");
        FlowContext context2 = FlowContext.of("session_2");

        // 在 context1 中设置状态
        context1.put("custom_state", "value1");
        String result1 = team.call(context1, "会话1的问题");

        // context2 不应该看到 context1 的状态
        context2.put("custom_state", "value2");
        String result2 = team.call(context2, "会话2的问题");

        Assertions.assertNotEquals(
                context1.get("custom_state"),
                context2.get("custom_state"),
                "不同会话的状态应该隔离"
        );
    }
}