《Harness Engineering --- AI Agent 工程方法论》完整目录
- 前言
- 第1章 Agent 不等于大模型:Harness 的价值
- 第2章 Agent 架构模式全景
- 第3章 Agent Loop:心跳与决策循环
- 第4章 上下文工程:比 Prompt Engineering 更重要的事
- 第5章 Tool Design:给 Agent 造趁手的兵器
- 第6章 工具编排与并发执行
- 第7章 工具结果处理与错误恢复
- 第8章 System Prompt 分层设计
- 第9章 指令优先级与冲突消解
- 第10章 Few-shot、CoT 与动态提示策略
- 第11章 短期记忆:上下文窗口管理
- 第12章 长期记忆:持久化与检索
- 第13章 多轮对话与会话状态机(当前)
- 第14章 Agent 权限模型设计
- 第15章 沙箱、隔离与防御性编程
- 第16章 多 Agent 协调模式
- 第17章 Human-in-the-Loop:人机协作设计
- 第18章 评估与测试方法论
- 第19章 可观测性与调试
- 第20章 成本控制与性能优化
- 第21章 设计模式与架构决策
第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 需要持续追踪:当前在做什么、做到哪一步了、用户修改了什么要求、哪些文件已经改了。这就是会话状态。
这个状态转移图揭示了多轮对话的本质:它不是简单的请求-响应循环,而是一个有分支、有回退、有中断恢复的状态机。
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 决定是否合并
优势: 完全的文件系统隔离,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 既有能力又受控。