面试表达
面试时怎么讲
一句话:我围绕「Agent 状态持久化与人审(LangGraph Checkpoint / HITL / Time-Travel)」做过从问题拆解、方案设计、工程落地到效果验证的闭环。
追问重点:为什么这么设计、指标怎么验证、失败案例如何定位、上线后怎么观测和回滚。
证据建议:准备 README、架构图、关键代码片段、评测表和 2 个坏例复盘。
把 Agent 从"一次性脚本"升级到"可中断、可恢复、可人审、可回放"的生产级系统;覆盖 Checkpointer、短/长记忆、interrupt/resume、审批、Time-Travel。
本地学习进度
只保存在当前浏览器,不上传服务器。
学习产出
实战交付,P0。阅读时尽量把知识点转成可演示项目、面试回答和简历证据。
能回答
课程目标(14 天) / 为什么需要(5 个真实灾难) / 核心概念图
能交付
完成练习与自测,留下可复盘的代码、截图或 README
下一步
先读手把手版,再回到正文补架构和取舍
面试表达
一句话:我围绕「Agent 状态持久化与人审(LangGraph Checkpoint / HITL / Time-Travel)」做过从问题拆解、方案设计、工程落地到效果验证的闭环。
追问重点:为什么这么设计、指标怎么验证、失败案例如何定位、上线后怎么观测和回滚。
证据建议:准备 README、架构图、关键代码片段、评测表和 2 个坏例复盘。
简历表达
围绕 Agent 状态持久化与人审(LangGraph Checkpoint / HITL / Time-Travel) 场景,完成「把 Agent 从"一次性脚本"升级到"可中断、可恢复、可人审、可回放"的生产级系统;覆盖 Checkpointer、短/长记忆、interrupt/resume、审批、Time-Travel。」相关能力建设,沉淀可复用工程方案、测试样本和面试表达材料。
读完本课,你要能在白板上完整回答:
Thread (会话/任务)
├── Checkpoint 序列 (step0, step1, step2, ...) ← Checkpointer 存储
│ └── 每个 Checkpoint = {state, next_nodes, metadata, ts}
├── 短期记忆:state.messages / state.scratchpad
└── 长期记忆:Store(跨 thread,按 user/namespace 查询)
interrupt 节点 → 暂停并写 checkpoint → 外部审批 → resume(thread_id, decision) → 继续| Checkpointer | 适用 | 说明 |
|---|---|---|
InMemorySaver | 开发/单测 | 重启即失 |
SqliteSaver | 单机 / Demo | 文件持久化,单写者 |
PostgresSaver | 生产推荐 | 支持并发、事务、TTL |
| 自研(Redis/Mongo) | 特殊场景 | 实现 BaseCheckpointSaver 协议 |
| 长期记忆 Store | 说明 |
|---|---|
InMemoryStore | 开发 |
PostgresStore | 带 pgvector 做语义记忆 |
| 自研(向量库 + KV) | 用你现有 Chroma / Redis |
from langgraph.checkpoint.sqlite import SqliteSaver
from langgraph.graph import StateGraph
graph = StateGraph(MyState)
# ... 加节点 ...
app = graph.compile(checkpointer=SqliteSaver.from_conn_string("state.db"))
cfg = {"configurable": {"thread_id": "u1-task-42"}}
for chunk in app.stream({"input": "..."}, cfg):
print(chunk)thread_id 继续跑,状态仍在。Annotated[list, add_messages]、operator.add、自定义 reducer。return {"messages": [...]} 不会覆盖现有 messages。thread_id。get_state(config) 查当前;get_state_history(config) 拿全部 checkpoint。trim_messages(max_tokens=4000, strategy="last")。SystemMessage("[历史摘要] ...")。from langgraph.store.memory import InMemoryStore
store = InMemoryStore(index={"embed": embeddings, "dims": 1536})
# 写
store.put(("user_42", "prefs"), "diet", {"value": "vegetarian"})
# 读(按 namespace + 语义)
hits = store.search(("user_42", "prefs"), query="dietary restriction", limit=3)interrupt 实现人审from langgraph.types import interrupt, Command
def risky_node(state):
decision = interrupt({
"action": "refund",
"amount": state["amount"],
"reason": state["reason"],
})
if decision["approved"]:
return {"status": "refunded"}
return {"status": "rejected"}
# 运行到 interrupt 会暂停
app.invoke({"amount": 500}, cfg)
# 审批后 resume
app.invoke(Command(resume={"approved": True}), cfg)resume(thread_id, decision)。/approvals/{thread_id} + 前端审批卡片。history = list(app.get_state_history(cfg))
# 回到第 5 步,换一个决策重跑
past = history[5]
new_cfg = {"configurable": {
"thread_id": cfg["configurable"]["thread_id"],
"checkpoint_id": past.config["configurable"]["checkpoint_id"],
}}
app.update_state(new_cfg, {"user_choice": "alternative"})
app.invoke(None, new_cfg)idempotency_key = hash(thread, step, args),服务端去重。SELECT FOR UPDATE。trace_id, thread_id, step, node, latency, token, cost。app.astream_events(..., version="v2") 同时产出 token chunk 与 state delta。SqliteSaver 直接上生产,多实例写冲突。thread_id 用用户 ID 而不是"用户+任务",导致任务互串。graphs/research_agent.py(含 HITL + checkpoint)api/approvals.py(审批回调)ui/approval-card.tsxdocs/agent-recovery-playbook.mdeval/replay-test.py(Time-Travel 回归)| 级别 | 资源 |
|---|---|
| A | LangGraph Persistence |
| A | LangGraph Human-in-the-loop |
| A | LangChain Short-term Memory |
| A | LangGraph Time Travel |
| A | PostgresSaver 源码 |
| B | Saga / Outbox Pattern 经典文章(微服务事务) |
与 agent-architecture、langchain-langgraph、ai-application-security、ai-frontend-streaming-ux 联动。