舰长Captain
交付可恢复的引用型事故 AgentShip a resumable, cited incident agent
任务简报Brief
服务重启后,用户发现对话“失忆”了。事故指挥官必须检索 runbook、写出带引用的简报、让独立评审通过,并在进程重启后恢复会话继续工作。这是前五关的合体。
After a service restart, users discover the conversation “lost its memory.” The incident commander must retrieve the runbook, write a cited brief, pass an independent review and resume the session after a process restart. This is the capstone.
毕业设计不再只考一个 API,而是考一条完整链路:检索证据 → 调用报告工具 → 结构化评审 → 持久化 AgentState → 恢复会话。你会看到 Python 与 Java 在状态持久化上的不同做法:Python 直接序列化 AgentState,Java 通过 JsonFileAgentStateStore 保存并在同一 session id 下恢复。最终产物包括 report.md、state.json、capstone_run.json。
The capstone tests a whole chain rather than one API: retrieve evidence → call the report tool → structured review → persist AgentState → resume the session. You will see how Python serializes AgentState directly while Java saves through JsonFileAgentStateStore and resumes under the same session id. Artifacts include report.md, state.json and capstone_run.json.
学习目标Learning goals
复用 M3 的检索,拿到
Session loss作为 top-1 证据。Reuse M3 retrieval and get
Session lossas the top-1 evidence.通过
write_report工具写出包含证据、行动计划和引用的报告。Write a report with evidence, action plan and citations through the
write_reporttool.用独立评审 Agent 返回结构化
Review。Get a structured
Reviewfrom an independent reviewer agent.持久化 AgentState,并在同一 session id 下恢复对话。
Persist AgentState and resume the conversation under the same session id.
核心概念Core concepts
RAG evidence
把检索结果作为报告的事实来源;每条行动都必须能映射到某个 runbook 段落。
Retrieved results are the factual basis of the report; every action must map to a runbook section.
write_report
唯一有副作用的工具;必须限制在工作目录内,并返回路径/字节数,便于验证。
The only side-effecting tool; confine it to the work directory and return path/bytes for verification.
Review
结构化评审结果:approved、reasons、missing;没有它,审批只是寒暄。
A structured review: approved, reasons, missing. Without it, approval is just pleasantries.
AgentState persistence
保存会话上下文与状态;同一 session id 恢复后才能继续之前的对话。
Persists conversation context and state; resuming under the same session id continues the earlier conversation.
JsonFileAgentStateStore
Java 的文件状态存储;按 session 保存,Agent 重新构建后可按 session id 加载。
Java's file-backed state store; saves per session and reloads when an agent is rebuilt with the same session id.
双轨实现Build it twice
先完整跑通一条轨道,再切换重来。两条轨道写出同一种产物,方便直接对照行为。Run one track end to end, then switch and repeat. Both tracks write the same artifact so you can compare behavior directly.
这一关复用 m3_librarian.mission.build_knowledge();先确保 python lab.py check m3 通过,再做 m6。
This mission reuses m3_librarian.mission.build_knowledge(); make sure python lab.py check m3 passes before m6.
检索证据并渲染报告Retrieve evidence and render the report
报告包含证据段、行动计划段和引用段;引用里带 runbook.md、标题、chunk 序号和分数。
The report has evidence, action plan and citations; each citation carries runbook.md, heading, chunk index and score.
question = "Why does a restarted service forget the conversation session?"
hits = await evidence(question)
top = (hits[0].chunk.metadata or {}).get("heading")
assert top == "Session loss"
report = render_report(question, hits)
assert report.count("runbook.md") >= 3
调用报告工具并流式观察Call the report tool and stream events
write_report 的权限是 ALLOW,但仍被限制在 lab/work/m6。流式事件里应出现 ToolCallStart、ToolResultEnd、ReplyEnd。
write_report is ALLOW but still confined to lab/work/m6. The stream should contain ToolCallStart, ToolResultEnd and ReplyEnd.
model = ScriptedModel([
tool_response("write_report", call_id="report-1",
path="report.md", content=report),
"Report written and ready for review.",
])
agent = build_agent(model)
async for event in agent.reply_stream(UserMsg("user", question)):
print(type(event).__name__)
评审、持久化与恢复Review, persist and resume
先让独立 reviewer 返回 Review(approved=True, reasons=[...]);然后序列化 AgentState,重新载入并继续对话,验证上下文变长且 session id 不变。
Get a structured Review(approved=True, reasons=[...]), serialize AgentState, reload it and continue; verify context grows and the session id is unchanged.
state_path.write_text(agent.state.model_dump_json(indent=2), encoding="utf-8")
reloaded = AgentState.model_validate_json(
state_path.read_text(encoding="utf-8"))
resumed = build_agent(resumed_model, state=reloaded)
await resumed.reply(UserMsg("user", "Continue after the restart."))
assert len(resumed.state.context) > len(agent.state.context)
assert resumed.state.session_id == agent.state.session_id
# python lab.py check m6
# PASS CODE: AS-M6-G5PF
python lab.py check m6
产物:report.md、state.json、capstone_run.json。capstone_run.json 记录问题、top 段落、事件类型、引用数、评审结果、session id 与 state 字节数。
Artifacts: report.md, state.json, capstone_run.json. The trace records question, top section, event types, citation count, review result, session id and state bytes.
lab/missions/m6_capstone/mission.py判题证明了什么What the check proves
- 检索到 3 条可引用证据Retrieved three cited evidence chunks
- 报告包含证据、行动计划与引用The report contains evidence, action plan and citations
- 评审通过并给出两条理由The reviewer approved with two reasons
- AgentState 重新载入并在同一 session 下恢复AgentState reloaded and resumed under the same session
Java 通过 JsonFileAgentStateStore 持久化;状态目录在 lab/work/m6/state-store,最终还会写出人类可读的 state.json。
Java persists through JsonFileAgentStateStore under lab/work/m6/state-store, and also writes a human-readable state.json.
复用检索并写报告Reuse retrieval and write the report
M3Librarian.buildKnowledge() 返回 Java 知识库;write_report 用 @Tool 注册,路径限制在 lab/work/m6。
M3Librarian.buildKnowledge() returns the Java knowledge base; write_report is registered with @Tool and confined to lab/work/m6.
SimpleKnowledge knowledge = M3Librarian.buildKnowledge();
List<Document> hits = knowledge.retrieve(QUESTION,
RetrieveConfig.builder().limit(3).scoreThreshold(0.0).build()).block();
String report = renderReport(QUESTION, hits);
FakeChatModel model = new FakeChatModel(List.of());
model.queue(LabSupport.toolCall("report-1", "write_report",
Map.of("path", "report.md", "content", report)));
model.queue(LabSupport.text("Report written."));
评审与状态存储Review and state store
评审使用 record + call(..., Review.class, ...);状态存储需要 stateStore(store) 和固定的 defaultSessionId,否则恢复时找不到同一个 session。
Review uses a record + call(..., Review.class, ...); state storage needs stateStore(store) and a fixed defaultSessionId, or resume cannot find the same session.
Review review = review(written); // structured generate_response
AgentStateStore store = new JsonFileAgentStateStore(
LabSupport.missionWork("m6").resolve("state-store"));
ReActAgent agent = ReActAgent.builder()
.name("captain").model(model).toolkit(toolkit())
.stateStore(store).defaultSessionId("m6-capstone")
.build();
恢复会话并判题Resume the session and check
保存后重新构建 Agent,使用同一 store 与 session id 再调用一次;检查会验证上下文变长、session id 不变、报告工具无错误,以及三个产物。
Rebuild the agent with the same store and session id and call again; the check validates context growth, unchanged session id, no tool error and all three artifacts.
AgentState state = agent.getAgentState(context);
agent.saveAgentState(context);
Files.writeString(statePath, state.toJson(), UTF_8);
try (ReActAgent resumed = agent(resumedModel, store)) {
resumed.call(List.of(LabSupport.user("Continue after the restart.")), context).block();
AgentState resumedState = resumed.getAgentState(context);
assert resumedState.getContext().size() > state.getContext().size();
}
// cd lab/java && mvn -q -o compile exec:java -Dexec.args="check m6"
cd lab/java && mvn -q -o compile exec:java -Dexec.args="check m6"
Java 没有 SOPEngine,也没有 Python 的 model_dump_json() 便捷方法;toJson() 用于人类可读产物,真正的恢复依赖状态存储。
Java has neither SOPEngine nor Python's model_dump_json() convenience; toJson() is for the human-readable artifact, while real resume depends on the state store.
lab/java/src/main/java/lab/m6/M6Capstone.java判题证明了什么What the check proves
- 检索到 3 条可引用证据,top-1 为 Session lossRetrieved three cited chunks with Session loss at top-1
- 报告包含证据、行动计划与引用The report contains evidence, action plan and citations
- 流式事件包含 TOOL_CALL_START、TOOL_RESULT_END、AGENT_ENDStream events include TOOL_CALL_START, TOOL_RESULT_END and AGENT_END
- 评审通过,AgentState 在同一 session 下恢复Review approved and AgentState resumed under the same session
Python ↔ Java 差异Python ↔ Java differences
Python 直接序列化
AgentState并作为构造参数传回;Java 通过AgentStateStore按 session 存取,Agent 重建后按 session id 加载。Python serializes
AgentStateand passes it back into the constructor; Java usesAgentStateStorekeyed by session, loaded after rebuilding the agent.Python 有
SOPEngine,m6 仍是单 Agent + 工具;Java 则把检索、报告、评审和恢复显式串起来。两边的“编排感”不同。Python has SOPEngine but m6 is still a single agent plus tools; Java explicitly wires retrieval, report, review and resume. The orchestration feel differs.
Python 用
reply_stream收集事件名;Java 用streamEvents+AgentEventType。产物格式保持一致,便于对照。Python collects event names from
reply_stream; Java usesstreamEvents+AgentEventType. Artifact shapes stay aligned for comparison.
常见坑Pitfalls
恢复时换了 session id:状态存储找不到旧会话,表现就像“重启后失忆”。session id 必须稳定并可配置。
Changing session id on resume: the store cannot find the old session, causing exactly the “amnesia” symptom. Keep the id stable and configurable.
只保存摘要、不保存上下文:模型看不到之前的消息,恢复后答复会自相矛盾。持久化要覆盖真正影响推理的状态。
Saving only a summary, not context: the model cannot see earlier messages and contradicts itself. Persist what actually affects reasoning.
报告引用与检索结果不一致:模型“记得”某个段落但没引用它,或引用了未检索到的段落。生成报告时应以 hits 为唯一事实源。
Report citations diverging from retrieval: the model “remembers” a section it did not retrieve or cites an absent one. Generate reports from hits as the only source of truth.
评审 Agent 与执行 Agent 共用状态/提示词:容易自审自过。评审应是独立会话、独立提示词,至少独立上下文。
Reviewer and executor sharing state/prompts invites self-approval. The reviewer should use a separate session, prompt and at least separate context.
状态文件无限增长:每轮都追加完整上下文,长期运行会撑爆磁盘和上下文窗口。生产需要截断、摘要或分层记忆。
Unbounded state files: appending full context every turn eventually blows disk and context windows. Production needs truncation, summaries or tiered memory.
领取通行码Claim your pass code
先运行当前轨道的 check。CLI 会打印 PASS CODE、XP 与徽章。粘贴到这里;同一关在每条轨道可各领取一次。Run the check for the current track. The CLI prints PASS CODE plus the exact XP and badge. Paste it here; the same mission can be claimed once on each track.
费曼复述Feynman recall
合上教程,用自己的话讲清机制。关键点覆盖 65%、篇幅 20%、结构 15%;60 分通过并一次性获得 20 XP。Explain the mechanism without looking. Coverage 65%, length 20%, structure 15%; 60 passes and earns +20 XP once.
一个可恢复的 Agent 服务需要持久化什么?为什么 session id 不能变?请结合检索、报告、评审和恢复讲一遍完整链路。
What must a resumable agent service persist? Why must the session id stay stable? Walk through retrieval, report, review and resume.
查看参考答案Show the model answer
可恢复的 Agent 至少要持久化会话上下文、session id、已执行的关键工具结果和必要的业务状态;如果涉及审批,还要保存评审结论。session id 是状态存储的键,换了 id 就找不到旧会话,表现就是“重启后失忆”。完整链路是:检索 runbook 得到带引用的证据,调用报告工具写出事实一致的报告,独立评审返回结构化结论,最后把 AgentState 落盘;重启后用同一 session id 重建 Agent,加载上下文继续对话。
A resumable agent must persist at least the conversation context, session id, key tool results and necessary business state; if approval matters, persist the review verdict too. The session id is the key into the state store, so changing it makes the old session invisible—exactly the “amnesia after restart” symptom. The chain is: retrieve runbook evidence with citations, call the report tool to write a fact-aligned report, get a structured verdict from an independent reviewer, then persist AgentState; after restart, rebuild the agent with the same session id, load context and continue.
间隔复习Spaced review
首次领取后会在明天安排复习。按真实感受评分,下一次间隔会自适应。After the first claim a review is scheduled for tomorrow. Rate how it felt; the next interval adapts.