AI Agent 开发学习与面试指南:04-Agent 架构与 LangGraph 状态编排

详解 ReAct 循环与 Plan-and-Execute 模式,手把手使用 LangGraph 实现显式状态图 (StateGraph)、Checkpointer 断点持久化与 HITL 人工审批机制。

本文目录29 个章节

第 4 章:Agent 架构与 LangGraph 状态编排

Agent 的价值不是“让模型自由发挥”,而是在不确定任务中允许模型选择下一步,同时把状态、权限、终止条件和错误恢复留在可控的工程结构里。一个可靠 Agent 通常是“确定性工作流 + 有限模型决策”的组合。

1. 先判断:到底需不需要 Agent

使用普通函数或工作流的条件:

  • 步骤固定;
  • 输入字段明确;
  • 规则可以穷举;
  • 错误代价高且没有人工审批;
  • 对延迟和可复现性要求很高。

使用 Agent 的条件:

  • 用户目标开放,路径无法预先完全确定;
  • 可用工具较多,需要根据上下文选择;
  • 中间结果会改变后续计划;
  • 需要搜索、尝试、反思或追问;
  • 能定义最大步数、权限和停止条件。

🔥 P0 高频必会:Agent 和 Workflow 有什么区别?
Workflow 的控制流主要由开发者预定义;Agent 让模型在运行时决定部分步骤或工具。生产系统通常不会二选一,而是在显式工作流中嵌入有限 Agent 决策,把高风险与关键规则保持确定性。

2. ReAct、Plan-and-Execute 与 Router

2.1 Router

一次模型判断把请求路由到固定处理链。它最简单、延迟低、可测试,适合“知识问答 / 订单查询 / 人工客服”分流。

2.2 ReAct 循环

模型根据当前状态选择动作,执行工具,观察结果,再决定下一步:

TEXT
输入 -> 决定动作 -> 调用工具 -> 观察 -> 再决定 -> 完成

优点是灵活;缺点是容易循环、重复调用和成本失控。必须设置最大步骤、工具权限和错误策略。

2.3 Plan-and-Execute

先生成计划,再逐步执行;执行结果可触发重规划。适合长任务,但计划可能过时,且多一次或多次模型调用。

2.4 选择建议

任务优先结构
单次分类/路由Router
2–5 步工具探索有上限的 ReAct
长期研究或复杂交付Plan-and-Execute + Checkpoint
高风险写操作Workflow + 审批节点
批量稳定处理确定性 Pipeline

3. 状态是 Agent 的事实来源

不要让状态只存在于一大段聊天文本中。用明确 Schema 保存:

PYTHON
from typing import Literal, TypedDict


class AgentState(TypedDict, total=False):
    run_id: str
    user_id: str
    goal: str
    route: Literal["knowledge", "ticket", "human"]
    retrieved_evidence: list[dict]
    tool_calls: list[dict]
    tool_results: list[dict]
    step_count: int
    max_steps: int
    final_answer: str
    error_code: str | None

状态设计原则:

  1. 保存原始结构化数据,不要只保存格式化字符串;
  2. 区分用户输入、模型建议、已验证事实和工具结果;
  3. 标出敏感字段,不把密钥和完整凭据写进状态;
  4. 状态更新应尽量追加或显式覆盖;
  5. 为长任务考虑序列化、版本迁移和过期清理。

LangGraph 官方把节点描述为“接收当前状态并返回更新的 Python 函数”,并建议把错误作为流程的一部分处理。参见 Thinking in LangGraph。

🔥 P0 高频必会:为什么需要显式状态?
显式状态让控制流可追踪、可持久化、可恢复和可测试;可以区分已验证事实与模型文本,也能在人工审批和失败重试时精确恢复。只依赖聊天历史很难保证一致性。

4. 一个最小状态图

下面示例不绑定具体模型 SDK,重点看控制流。

PYTHON
from typing import Literal, TypedDict

from langgraph.checkpoint.memory import InMemorySaver
from langgraph.graph import END, START, StateGraph


class SupportState(TypedDict, total=False):
    query: str
    route: Literal["knowledge", "ticket", "human"]
    evidence: list[str]
    ticket_id: str
    answer: str


def classify(state: SupportState) -> dict:
    query = state["query"]
    if "退款" in query or "故障" in query:
        return {"route": "ticket"}
    if "人工" in query:
        return {"route": "human"}
    return {"route": "knowledge"}


def route_after_classify(state: SupportState) -> str:
    return state["route"]


def retrieve(state: SupportState) -> dict:
#    # 实际项目中调用 Retriever,并返回带来源的结构化 Evidence。
    return {"evidence": ["[S1] 示例制度:普通问题先查知识库。"]}


def answer(state: SupportState) -> dict:
    evidence = "\n".join(state.get("evidence", []))
    return {"answer": f"根据以下资料回答:\n{evidence}"}


def create_ticket(state: SupportState) -> dict:
#    # 真正实现必须有权限、幂等键和审计。
    return {"ticket_id": "T-001", "answer": "已创建工单 T-001"}


def handoff(state: SupportState) -> dict:
    return {"answer": "已转接人工客服"}


builder = StateGraph(SupportState)
builder.add_node("classify", classify)
builder.add_node("retrieve", retrieve)
builder.add_node("answer", answer)
builder.add_node("create_ticket", create_ticket)
builder.add_node("handoff", handoff)

builder.add_edge(START, "classify")
builder.add_conditional_edges(
    "classify",
    route_after_classify,
    {
        "knowledge": "retrieve",
        "ticket": "create_ticket",
        "human": "handoff",
    },
)
builder.add_edge("retrieve", "answer")
builder.add_edge("answer", END)
builder.add_edge("create_ticket", END)
builder.add_edge("handoff", END)

graph = builder.compile(checkpointer=InMemorySaver())

config = {"configurable": {"thread_id": "demo-thread-1"}}
result = graph.invoke({"query": "年假有多少天?"}, config=config)
print(result["answer"])

教学示例使用内存 Checkpointer;生产应使用持久化存储。LangGraph 的 Persistence 文档说明,Checkpoint 可支持人工介入、会话记忆、时间回放和故障恢复,参见 Persistence。

5. 模型决策节点应该返回什么

不要让路由节点返回自由文本。定义结构:

PYTHON
from typing import Literal
from pydantic import BaseModel, Field


class NextAction(BaseModel):
    action: Literal["search", "create_ticket", "ask_user", "finish"]
    tool_name: str | None = None
    tool_arguments: dict = Field(default_factory=dict)
    user_question: str | None = None
    final_answer: str | None = None

模型只建议下一动作,应用层仍要验证:

  • tool_name 是否在允许列表;
  • 参数是否通过 Schema;
  • 用户是否有权限;
  • 是否超过最大步数和成本预算;
  • 写操作是否需要审批;
  • 当前状态是否允许该动作。

6. 终止条件与循环控制

每个 Agent Run 至少需要:

  • 最大步骤;
  • 最大模型调用数;
  • 最大工具调用数;
  • 最大 Token/成本;
  • 全链路截止时间;
  • 相同动作重复检测;
  • 用户取消;
  • 明确成功与失败状态。
PYTHON
def guard_limits(state: AgentState) -> Literal["continue", "stop"]:
    if state["step_count"] >= state["max_steps"]:
        return "stop"

    calls = state.get("tool_calls", [])
    if len(calls) >= 2 and calls[-1] == calls[-2]:
        return "stop"

    return "continue"

相同工具名不一定代表重复,需比较规范化参数和业务意图;但简单检测能捕获大量死循环。

🔥 P0 高频必会:如何防止 Agent 无限循环?
限制步数、时间、Token 和成本;检测重复状态/动作;工具返回结构化错误;为无进展定义终止条件;高风险或不确定情况转人工;Trace 中记录每步原因。不能只在 Prompt 里写“不要循环”。

7. 错误是流程的一部分

LangGraph 官方示例把错误分为:瞬时错误、模型可恢复错误、用户可修复错误和未知错误。对应策略:

类型谁处理策略
429、网络抖动系统Retry Policy
工具参数非法模型把结构化错误写回状态,允许有限纠正
缺少订单号用户Interrupt / Ask User
越权系统拒绝并审计,不让模型绕过
未知异常工程师失败、报警、保留 Trace

不要让模型对所有异常自行决定。权限、资金、删除、外发等风险必须由确定性代码控制。

8. Checkpoint 与恢复

Checkpoint 保存每步状态快照,使长任务可以:

  • 进程重启后恢复;
  • 人工审批后继续;
  • 从失败步骤重试;
  • 回放错误轨迹;
  • 从历史状态 fork 新实验。

但是恢复会导致某些代码重新执行,所以副作用必须幂等。官方 Interrupt 文档特别提醒:恢复时节点可能从头执行,interrupt 之前的副作用需要幂等。参见 Interrupts。

9. Human-in-the-Loop

PYTHON
from langgraph.types import Command, interrupt


def approve_ticket(state: SupportState) -> dict:
    decision = interrupt({
        "question": "是否创建工单?",
        "proposed_title": state["query"][:80],
    })
    return {"approved": bool(decision)}


## 首次运行遇到 interrupt 后暂停;恢复时必须使用同一 thread_id。
## graph.invoke(Command(resume=True), config=config)

审批载荷必须让人能够判断风险:动作、对象、关键参数、预计影响,而不是只显示“是否继续”。

🔥 P0 高频必会:哪些动作需要 HITL?
不可逆、高金额、外部沟通、敏感数据访问、权限变更、代码执行和低置信度动作。阈值应由风险和业务政策决定,不是所有工具都弹窗,也不能把关键审批交给模型自己取消。

10. 确定性边界

把任务拆成两类:

适合模型判断

  • 用户意图;
  • 信息提取;
  • 候选工具选择;
  • 文本总结;
  • 开放式计划建议。

必须由代码决定

  • 身份与权限;
  • 金额上限;
  • 状态迁移是否合法;
  • 参数类型与业务约束;
  • 是否需要审批;
  • 重试次数、超时和成本上限;
  • 审计记录。

这条边界是生产 Agent 与 Demo 的关键差异。

11. 单 Agent 先于多 Agent

如果一个状态图加几个专用节点能解决,不要立即拆成多个“角色”。多 Agent 会带来:

  • 更多模型调用和延迟;
  • 上下文传递损失;
  • 责任归属不清;
  • 更复杂的死循环和评测;
  • 权限与身份管理难度。

只有当任务可清晰分区、需要并行或不同工具/权限隔离时才考虑多 Agent。第 6 章会详细展开。

12. 本章练习

练习 A:三路客服图

实现知识问答、创建工单、人工转接三条路径。路由器必须结构化输出;写操作需要审批;每条路径有测试。

练习 B:故障恢复

让“创建工单”在第一次调用后模拟网络超时。证明恢复执行不会重复创建工单,并保存 Checkpoint。

练习 C:循环红队

构造工具持续返回“请重试”的情况。系统必须在最大步数内停止,并返回可诊断错误,而不是耗尽预算。

13. 面试高频问答

🔥 P0:你如何设计 Agent 状态?

区分输入、计划、已验证事实、工具请求、工具结果、审批、错误和最终输出;使用类型化 Schema;保存原始数据;支持序列化和版本迁移;敏感凭据不进入状态;更新规则明确。

🔥 P0:Checkpoint 和 Memory 有什么区别?

Checkpoint 保存某次运行/线程的执行状态,用于恢复和回放;长期 Memory 保存跨线程需要复用的信息。两者生命周期、作用域和写入策略不同,不能把所有历史 Checkpoint 都当用户记忆。

🔥 P0:如何评估 Agent 的轨迹?

不仅看最终答案,还看工具选择、参数、步骤数、重复调用、无效动作、权限违规和是否在适当时机停止/转人工。用标注轨迹、允许动作集合和成本指标共同评测。

⭐ P1:ReAct 和 Plan-and-Execute 如何选?

短、探索性任务用有上限 ReAct;长任务可先计划、分步执行并允许重规划。固定流程不应强行 Agent 化。选择依据是任务长度、路径不确定性、工具数量、错误代价和延迟预算。

⭐ P1:图结构升级会遇到什么问题?

旧 Checkpoint 可能指向已删除或改名节点,状态字段类型也可能不兼容。需要状态版本、迁移策略、向后兼容、灰度发布和对进行中线程的处理方案。

14. 本章完成标准

  • 能说明何时不用 Agent;
  • 能实现带条件路由的状态图;
  • 有最大步数、错误分类、Checkpoint 和审批;
  • 写工具恢复后不会重复产生副作用;
  • 能从 Trace 解释每一步为什么发生;
  • 能清楚划分模型判断和确定性代码边界。

REFERENCES

参考链接

  1. 01Thinking in LangGraph
  2. 02Persistence
  3. 03Interrupts

所属系列

AI Agent 开发学习与面试指南

下一步

继续浏览相关主题

沿着同一主题继续阅读。

查看最新资讯