multi - 示例3 - 持久化与恢复
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"),
"不同会话的状态应该隔离"
);
}
}