Harness Engineering-第13章 多轮对话与会话状态机

《Harness Engineering --- AI Agent 工程方法论》完整目录

第13章 多轮对话与会话状态机

"State is the root of all complexity --- and the source of all capability."

:::tip 本章要点

  • Agent 交互本质上是多轮状态机:每一轮改变状态,状态决定下一步行为
  • 隐式状态(对话历史)vs 显式状态(LangGraph 的 Channel/Reducer 模型)
  • 会话中的关键挑战:上下文切换、并发隔离、断点恢复
  • 实践模式:任务追踪、分支对话、会话持久化 :::

13.1 单轮 vs 多轮

简单的 LLM 调用是单轮的------输入一条消息,得到一个回复。但真实的 Agent 交互几乎都是多轮的:

makefile 复制代码
用户: 帮我重构 auth 模块        ← 第 1 轮:确定任务
Agent: 让我先看看代码结构...     ← 第 2 轮:调研
Agent: [读取 5 个文件]           ← 第 3-7 轮:工具调用
Agent: 我建议分三步重构...       ← 第 8 轮:提出方案
用户: 第二步不太对,应该...      ← 第 9 轮:用户修正
Agent: 明白,我调整方案...       ← 第 10 轮:适应
Agent: [修改 3 个文件,运行测试] ← 第 11-18 轮:执行
Agent: 重构完成,所有测试通过    ← 第 19 轮:完成

19 轮交互中,Agent 需要持续追踪:当前在做什么、做到哪一步了、用户修改了什么要求、哪些文件已经改了。这就是会话状态

stateDiagram-v2 [*] --> 理解需求: 用户输入任务 理解需求 --> 调研: 需要更多信息 调研 --> 制定方案: 信息充足 制定方案 --> 等待确认: 方案就绪 等待确认 --> 执行: 用户确认 等待确认 --> 制定方案: 用户修正 执行 --> 验证: 执行完成 验证 --> 执行: 测试失败 验证 --> 完成: 测试通过 完成 --> [*] 执行 --> 中断: 用户切换任务 中断 --> 理解需求: 恢复之前的任务

这个状态转移图揭示了多轮对话的本质:它不是简单的请求-响应循环,而是一个有分支、有回退、有中断恢复的状态机。

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 对话中的上下文切换

用户经常在任务执行中途切换方向:

makefile 复制代码
用户: 帮我修复登录 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 工具来显式追踪多步骤任务的进度:

less 复制代码
Task #1: 修复登录 bug          [in_progress]
  - 调查错误日志               [completed]
  - 定位根因                   [completed]
  - 编写修复代码               [in_progress]
  - 运行测试                   [pending]

这不是底层状态机------而是模型自己管理的"备忘录"。优雅之处在于它同时服务于两个目的:帮助模型追踪进度,帮助用户了解当前状态。

13.5 并发会话隔离

同一个用户可能同时运行多个 Agent 实例:

bash 复制代码
终端 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 决定是否合并
graph TD Main["主 Agent\n(main branch)"] -->|"spawn"| Sub1["子 Agent A\n(worktree-abc)"] Main -->|"spawn"| Sub2["子 Agent B\n(worktree-def)"] Sub1 -->|"完成\n返回 branch"| Main Sub2 -->|"完成\n返回 branch"| Main Sub1 -.->|"独立文件系统\n互不干扰"| Sub2 style Main fill:#dbeafe,stroke:#3b82f6,stroke-width:2px style Sub1 fill:#fef3c7,stroke:#f59e0b style Sub2 fill:#fef3c7,stroke:#f59e0b

优势: 完全的文件系统隔离,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 既有能力又受控。

相关推荐
杨艺韬4 小时前
Harness Engineering-第16章 多 Agent 协调模式
agent
杨艺韬4 小时前
Harness Engineering-第1章 Agent 不等于大模型:Harness 的价值
agent
杨艺韬4 小时前
Harness Engineering-第11章 短期记忆:上下文窗口管理
agent
杨艺韬4 小时前
Harness Engineering-第18章 评估与测试方法论
agent
杨艺韬4 小时前
Harness Engineering-第19章 可观测性与调试
agent
杨艺韬4 小时前
Harness Engineering-第17章 Human-in-the-Loop:人机协作设计
agent
杨艺韬4 小时前
Harness Engineering-第6章 工具编排与并发执行
agent
杨艺韬4 小时前
Harness Engineering-第21章 设计模式与架构决策
agent
杨艺韬4 小时前
Harness Engineering-第5章 Tool Design:给 Agent 造趁手的兵器
agent