Skip to content

第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 可靠性的基础:

  1. 隐式 vs 显式——对话历史是最简方案,结构化状态更可控
  2. 上下文切换——任务栈或 TodoList 模式追踪多任务
  3. 并发隔离——Git Worktree 或进程隔离避免冲突
  4. 断点恢复——Checkpoint 机制让长任务不怕中断
  5. 超时清理——不活跃会话需要持久化后释放资源
  6. 状态可观测——运维和调试都需要能查看会话状态

下一章进入安全领域——如何设计权限模型让 Agent 既有能力又受控。

基于 VitePress 构建