Appearance
第13章 多轮对话与会话状态机
"State is the root of all complexity — and the source of all capability."
本章要点
- Agent 交互本质上是多轮状态机:每一轮改变状态,状态决定下一步行为
- 隐式状态(对话历史)vs 显式状态(LangGraph 的 Channel/Reducer 模型)
- 会话中的关键挑战:上下文切换、并发隔离、断点恢复
- 实践模式:任务追踪、分支对话、会话持久化
13.1 单轮 vs 多轮
简单的 LLM 调用是单轮的——输入一条消息,得到一个回复。但真实的 Agent 交互几乎都是多轮的:
用户: 帮我重构 auth 模块 ← 第 1 轮:确定任务
Agent: 让我先看看代码结构... ← 第 2 轮:调研
Agent: [读取 5 个文件] ← 第 3-7 轮:工具调用
Agent: 我建议分三步重构... ← 第 8 轮:提出方案
用户: 第二步不太对,应该... ← 第 9 轮:用户修正
Agent: 明白,我调整方案... ← 第 10 轮:适应
Agent: [修改 3 个文件,运行测试] ← 第 11-18 轮:执行
Agent: 重构完成,所有测试通过 ← 第 19 轮:完成19 轮交互中,Agent 需要持续追踪:当前在做什么、做到哪一步了、用户修改了什么要求、哪些文件已经改了。这就是会话状态。
这个状态转移图揭示了多轮对话的本质:它不是简单的请求-响应循环,而是一个有分支、有回退、有中断恢复的状态机。
13.2 隐式状态:对话历史模型
最简单的状态管理:把全部对话历史发送给模型,让模型自己从中提取状态。
typescript
// Claude Code 的核心循环(简化版)
const messages: Message[] = []
while (true) {
const userInput = await getUserInput()
messages.push({ role: 'user', content: userInput })
const response = await llm.chat({
system: systemPrompt,
messages: messages, // 完整历史就是全部状态
})
messages.push({ role: 'assistant', content: response })
if (response.hasToolCalls) {
const results = await executeTools(response.toolCalls)
messages.push({ role: 'user', content: formatToolResults(results) })
// 继续循环,让模型处理工具结果
}
}优点: 实现极简,模型自动从历史中理解上下文 缺点: 状态是隐式的,无法程序化访问。你不能问"当前任务完成了百分之多少"——这个信息散落在几十条消息中。
13.3 显式状态:LangGraph 的 Channel 模型
LangGraph 将状态管理提升为一等公民:
python
from langgraph.graph import StateGraph
from typing import TypedDict, Annotated
from operator import add
class AgentState(TypedDict):
messages: Annotated[list, add] # 对话历史(追加)
current_task: str # 当前任务描述
files_modified: list[str] # 已修改的文件
plan: list[str] # 执行计划
plan_step: int # 当前步骤
user_approved: bool # 用户是否批准
graph = StateGraph(AgentState)每个节点读取和修改状态,Reducer 定义合并规则:
python
def planning_node(state: AgentState) -> dict:
plan = llm.generate_plan(state["current_task"])
return {"plan": plan, "plan_step": 0}
def execution_node(state: AgentState) -> dict:
step = state["plan"][state["plan_step"]]
result = execute_step(step)
return {
"plan_step": state["plan_step"] + 1,
"files_modified": [result.file_path],
"messages": [{"role": "assistant", "content": f"完成: {step}"}]
}优点: 状态可观测、可序列化、可恢复 缺点: 需要预定义状态结构,灵活性低于自由对话
13.4 对话中的上下文切换
用户经常在任务执行中途切换方向:
用户: 帮我修复登录 bug
Agent: [开始调查...]
用户: 等等,先帮我看另一个问题——部署脚本报错了
Agent: ??? ← 需要保存当前任务状态,切换到新任务处理策略:
隐式切换(Claude Code 的做法)
不做特殊处理——模型从对话历史中理解用户改变了方向。简单但可能"忘记"之前的任务。
显式任务栈
维护一个任务栈,支持中断和恢复:
typescript
interface TaskStack {
tasks: TaskState[]
pushTask(task: string): void {
// 保存当前任务的快照
if (this.current) {
this.current.status = 'suspended'
this.current.snapshot = captureCurrentState()
}
this.tasks.push({ task, status: 'active', snapshot: null })
}
popTask(): TaskState | null {
this.tasks.pop() // 移除已完成的任务
const previous = this.tasks[this.tasks.length - 1]
if (previous) {
previous.status = 'active'
restoreState(previous.snapshot)
}
return previous
}
}Claude Code 的 TodoList 模式
Claude Code 使用 TaskCreate/TaskUpdate 工具来显式追踪多步骤任务的进度:
Task #1: 修复登录 bug [in_progress]
- 调查错误日志 [completed]
- 定位根因 [completed]
- 编写修复代码 [in_progress]
- 运行测试 [pending]这不是底层状态机——而是模型自己管理的"备忘录"。优雅之处在于它同时服务于两个目的:帮助模型追踪进度,帮助用户了解当前状态。
13.5 并发会话隔离
同一个用户可能同时运行多个 Agent 实例:
终端 1: Agent 在修复 bug(修改 src/auth.ts)
终端 2: Agent 在添加新功能(也要修改 src/auth.ts)如果两个 Agent 操作同一文件,必然冲突。
Git Worktree 隔离
Claude Code 的解决方案:为子 Agent 创建独立的 Git Worktree:
typescript
// 在隔离的 worktree 中运行 Agent
const agent = spawnAgent({
prompt: "修复这个 bug",
isolation: "worktree" // 创建临时 worktree
})
// Agent 在 /tmp/worktree-abc123/ 中工作
// 完成后,变更以 branch 形式返回
// 主 Agent 决定是否合并优势: 完全的文件系统隔离,Agent 怎么折腾都不影响主分支 劣势: Worktree 创建有开销,需要清理机制
进程级隔离
每个 Agent 会话有独立的:
- 对话历史(上下文窗口)
- 工作目录(或 worktree)
- 权限上下文
- 环境变量
不共享的设计保证了并发安全。
13.6 会话持久化与恢复
长任务可能因为网络断开、进程崩溃而中断。能否恢复到断点继续?
LangGraph 的 Checkpoint 机制
LangGraph 在每个节点执行后自动保存状态快照:
python
from langgraph.checkpoint.sqlite import SqliteSaver
checkpointer = SqliteSaver.from_conn_string("checkpoints.db")
app = graph.compile(checkpointer=checkpointer)
# 执行——每个节点自动 checkpoint
config = {"configurable": {"thread_id": "session-123"}}
result = app.invoke(initial_state, config)
# 恢复——从最后一个 checkpoint 继续
state = app.get_state(config)
# state.values 包含完整的 AgentState这让 Agent 能在任意节点中断后恢复——用户关闭浏览器、服务器重启,都不影响任务连续性。
对话历史持久化
Claude Code 通过 --resume 标志恢复上一次对话:
bash
claude --resume # 加载上次的对话历史继续底层是将对话消息序列化到本地文件,启动时反序列化回来。简单但有效。
13.7 会话超时与清理
不活跃的会话需要超时机制:
typescript
class SessionManager {
private sessions = new Map<string, Session>()
private readonly TIMEOUT_MS = 30 * 60 * 1000 // 30 分钟
getOrCreate(sessionId: string): Session {
let session = this.sessions.get(sessionId)
if (session) {
session.lastActive = Date.now()
return session
}
session = new Session(sessionId)
this.sessions.set(sessionId, session)
return session
}
// 定期清理
cleanup(): void {
const now = Date.now()
for (const [id, session] of this.sessions) {
if (now - session.lastActive > this.TIMEOUT_MS) {
session.persist() // 持久化再清理
this.sessions.delete(id)
}
}
}
}13.8 状态可观测性
会话状态不能是黑盒。运维和调试都需要能查看当前状态:
typescript
// 状态查询 API
GET /api/sessions/:id/state
{
"session_id": "abc123",
"status": "active",
"current_task": "重构 auth 模块",
"messages_count": 24,
"tokens_used": 85000,
"tools_called": ["Read", "Edit", "Bash"],
"files_modified": ["src/auth.ts", "src/auth.test.ts"],
"started_at": "2026-04-15T10:30:00Z",
"last_active": "2026-04-15T10:45:23Z"
}LangGraph Studio 提供了图形化的状态检查工具——可以看到当前在哪个节点、状态值是什么、历史路径是怎样的。这种可视化对于调试复杂的多 Agent 工作流至关重要。
13.9 本章小结
多轮对话的状态管理是 Agent 可靠性的基础:
- 隐式 vs 显式——对话历史是最简方案,结构化状态更可控
- 上下文切换——任务栈或 TodoList 模式追踪多任务
- 并发隔离——Git Worktree 或进程隔离避免冲突
- 断点恢复——Checkpoint 机制让长任务不怕中断
- 超时清理——不活跃会话需要持久化后释放资源
- 状态可观测——运维和调试都需要能查看会话状态
下一章进入安全领域——如何设计权限模型让 Agent 既有能力又受控。