#AI#AI Agent

Agent 循环设计:停止、反思、分支

Agent 的核心是个循环,但"循环"设计得好不好决定它是智能还是乱跑。本文讲清 Agent 循环的几个关键设计:停止条件(何时结束)、反思(self-reflection,做完一步自评)、分支与回溯、最大步数与预算、以及在循环里加"人工检查点"把不可逆操作拦住。

本文要点

  • 主题聚焦:AI、AI Agent
  • 适合人群:独立开发者、全栈工程师、AI 应用开发者
  • 核心价值:快速理解背景、实践路径、常见误区和工程建议
作者 @admin·发布于 2025年11月11日·更新于 2026年7月23日

很多人第一次写 Agent,写完都觉得"就这?":套个 while True,把 prompt 塞进去,加几个 tool 调用,跑起来看着它自己查资料、改代码、调 API,确实有点魔法。但你很快会遇到三类崩盘:它停不下来(烧钱)、它跑偏了还自我感觉良好(智障)、它把生产库 DROP 了(事故)。

这些问题的根都不在模型,在循环。Agent 的"智能"有一半住在那个循环里——什么时候停、什么时候反思、什么时候换路、什么时候叫人——这些工程决策,决定了同样一个模型跑出来是"能用"还是"灾难"。

这篇文章拆解 Agent 循环的几个关键设计点,都是我在生产里踩过的坑。

一、循环的本质:思考 → 行动 → 观察

最朴素的 Agent 循环叫 ReAct(Reason + Act),三个阶段反复轮转:

text
┌─────────────────────────────────────────────┐
│   Thought(思考):我现在该干嘛?              │
│        ↓                                     │
│   Action(行动):调用某个 tool              │
│        ↓                                     │
│   Observation(观察):拿到 tool 的返回值    │
│        ↓                                     │
│   回到 Thought,直到任务完成                 │
└─────────────────────────────────────────────┘

伪代码长这样:

python
state = {"messages": [system, user]}
while True:
    msg = llm(state["messages"])
    state["messages"].append(msg)
    if msg.done:              # 停止条件
        break
    for call in msg.tool_calls:
        result = run_tool(call)
        state["messages"].append(result)

看起来就十行,但每一个分支判断都是设计决策。一个能上生产的循环,至少要回答清楚:什么时候停?做错了怎么办?走死胡同怎么办?花超了怎么办?危险动作谁拦? 这五个问题答不好,Agent 就是个不稳定的炸药包。

二、停止条件:什么时候该喊停

停止条件是循环设计里最容易被忽视、却最烧钱的一环。我见过太多 Agent 因为"不知道自己完成了"而无限绕圈:要么反复确认、要么反复"优化"、要么在两个状态间反复横跳。

一个稳健的 Agent 至少要有 五道停止信号,且是 OR 关系(任意一个触发就停):

  1. 任务完成信号:模型显式输出 done=true / 调用 finish tool / 输出约定的终止 token。这是"主动停"。
  2. 最大步数上限:max_steps=30,硬性兜底。这是"被动停"的第一道。
  3. 预算上限:时间(如 10 分钟)或 token(如 200k)超了就停。比步数更精确,因为有的 step 烧 token 不烧步。
  4. 用户中断:UI 上一个"停止"按钮,把 cancel 信号打进循环。
  5. 死循环检测:连续 N 步重复同样的 action、或状态长时间不变,主动打断。

死循环检测很多人不做,我强烈建议加。一个简单有效的实现:

python
recent_actions = state["recent_actions"][-5:]
if len(set(hash(a) for a in recent_actions)) == 1:
    # 连续 5 步做一模一样的事,打断
    raise LoopDetected("agent stuck")

更狠一点,看"状态相似度":如果连续 10 步的 observation 内容向量化后余弦相似度都 > 0.95,基本就是它在原地打转。

一个观点:停止条件不是"加一个就够了",而是层层兜底。完成信号是优雅退出,其余都是保险栓。没有保险栓的 Agent,迟早会在某个边角 case 上烧掉一整夜的预算。

三、反思(self-reflection):让 Agent 自评"我做得对吗"

ReAct 的原始论文里,Agent 只往前走,不回头看。但人解决复杂问题时有个关键动作:做完一步,停下来问一句"刚才这步对吗?够吗?要不要改?" 这就是 self-reflection。

实证数据很扎实:在 SWE-bench、WebArena 这类复杂任务上,加了 self-reflection 的 Agent 成功率普遍提升 10–30%。原因是 LLM 一次性规划经常出错(尤其步骤多的任务),但让它"回头看"它往往能发现自己哪儿错了——评判比生成容易。

实现上,反思可以插在两个粒度:

  • 每步反思:每个 action 之后,加一轮轻量自评。
  • 阶段反思:每完成一个子目标(比如"修完一个文件""跑完一次测试"),做一次更深的复盘。

反思的 prompt 结构大致是:

text
你刚才的目标: <goal>
你这几步的动作: <actions>
观察到的结果: <observations>
请判断:
1. 目标是否已完成?给出置信度。
2. 中间有没有走错的路?有的话错在哪?
3. 如果还没完成,下一步最该做什么?是否需要回退?

反思的产物不是"一段感想",而是一个 结构化决策:继续 / 回退 / 终止。这个决策要写回 state,驱动循环。

text
Action → Observation → Reflect →
   ├─ continue: 进入下一 Thought
   ├─ backtrack: 回到某历史节点换路
   └─ stop: 任务完成或判定无解

一个观点:反思不是越多越好。每步反思会显著增加 token 和延迟,在简单任务上纯属浪费。我的实践是——任务步数预计 > 5、或工具结果不确定性高(比如浏览器自动化、外部 API)时才开反思;简单的一次查询类任务,反思反而是负优化。另外反思要换一个 prompt 角色(让它"扮演评审者"),否则模型容易自我肯定、看不出自己的错。

四、分支与回溯:树搜索式 Agent

ReAct 是一条路走到底,走错了只能靠反思硬掰回来。但有些任务(比如复杂调试、多解方案探索)天然需要"试一条路,不行换一条"——这就是树搜索式 Agent。

核心思想:把 Agent 的探索过程建模成一棵树,每个节点是一个状态,每个 action 是一条边。走某条边失败后,回退到分叉点,换一条边。

text
           root
          /    \
       search-A  search-B
        / \         |
     ok   fail    ok  ← 回退到 root, 选 search-B, 再走

实现要点有三个:

  1. 状态快照:每个节点保存一份可恢复的 state(不只是 messages,还有文件改动、环境变量等副作用点)。
  2. 评分函数:给每个分支打分(可以用一个小 LLM、或基于规则的启发式),决定优先扩展哪条。
  3. 回退机制:失败后能"撤销"副作用——这是最难的部分。改了文件要能回滚(git 是天然的状态快照),调了外部 API(发了邮件、提交了订单)就没法回退,这种操作根本不该放进可回溯的分支里。
python
def explore(node, budget):
    if node.is_goal() or budget <= 0:
        return node
    for action in node.possible_actions():
        child = node.step(action)
        result = explore(child, budget - 1)
        if result.solved:
            return result
        # 失败,记录原因,尝试下一个 action
    return node  # 所有分支都失败

一个观点:树搜索式 Agent 在公开 benchmark 上效果惊艳,但生产里慎用。原因有二:一是代价巨大(同一个问题可能探索多条路径,token 翻倍),二是副作用回退在真实系统里几乎不可能做干净。我的建议是只在纯信息空间的任务上用(比如"从多个文档里找答案""生成多个方案让用户挑"),涉及真实世界副作用的任务还是老老实实走线性 + 反思。

五、预算与上限:防失控的三道闸

一个没有上限的 Agent 循环,本质就是一个"按 token 计费的死循环"。LLM 调用比传统循环贵三个数量级,几小时不管它能烧掉你一整月的 API 配额。

防失控至少要有三道闸,从松到紧:

闸门粒度作用
步数上限粗防 Agent 在任务里无限绕,最易实现
时间上限中防卡在某个慢 tool(比如爬一个不响应的页面)
token 上限细最精确的成本控制,直接对应钱

预算的关键设计是:预算是全局共享的,不是每个 tool 一份。否则 Agent 会在一个 tool 上把预算花光。我习惯维护一个 Budget 对象,每次 LLM 调用和 tool 调用都从里面扣:

python
class Budget:
    def __init__(self, max_steps, max_seconds, max_tokens):
        self.steps = max_steps
        self.deadline = time.time() + max_seconds
        self.tokens = max_tokens

    def spend(self, steps=1, tokens=0):
        self.steps -= steps
        self.tokens -= tokens
        if self.steps <= 0 or self.tokens <= 0:
            raise BudgetExceeded()
        if time.time() > self.deadline:
            raise Timeout()

超预算时,优雅降级比硬报错好——让 Agent 收到一句"预算还剩 5%,请尽快收尾",它往往能在一个 step 内把当前进度总结给用户,而不是留下一个半成品。

六、人工检查点:把不可逆操作拦住

Agent 最危险的时刻,是它要执行不可逆操作的那一步:rm -rf、DROP TABLE、发邮件、推 commit、付款、调用生产环境的写接口。这些动作一旦做了就回不来,而 LLM 又天生有"自信地犯错"的毛病。

解法是 human-in-the-loop:在这类动作前,强制插入一个检查点,暂停循环,等人确认。

python
DANGEROUS_TOOLS = {"delete_file", "run_migration", "send_email", "deploy"}

while True:
    msg = llm(state)
    for call in msg.tool_calls:
        if call.name in DANGEROUS_TOOLS:
            approval = await request_human_approval(call)
            if not approval:
                state.append({"role": "tool",
                              "content": "用户拒绝了此操作,请换方案"})
                continue
        result = run_tool(call)

工程上有几个细节决定它好不好用:

  • 危险分级:不是所有动作都要打断。read_file 不拦,delete_file 拦,deploy_to_prod 不仅拦还要二次确认。给每个 tool 打 risk_level 标签。
  • 上下文充足:审批时不能只甩一句"要执行 delete_file,同意吗?",要把 Agent 的完整推理链路(为什么要删、删的是哪个文件、删了会怎样)展示给审批人,否则人也是瞎点。
  • 超时策略:人 10 分钟没回应,默认拒绝还是默认通过?我强烈建议默认拒绝,宁可卡住也不要默认放行。
  • 可批量化:如果一个任务里要删 50 个文件,逐个确认会把人逼疯。提供"本次任务内同类操作统一授权"的选项,但要限定范围(同一次会话、同一类 tool、同一目录)。

一个观点:检查点不是"降低 Agent 自主性",而是"扩大 Agent 能被授权做的工作范围"。一个有检查点的 Agent,可以被放心地接入生产环境;一个没有的,只敢在沙盒里玩。检查点是 Agent 从 demo 走向生产的必经之路。

七、循环的工程:持久化、trace、并发

最后讲三个"不性感但决定能不能上线"的工程问题。

状态持久化:长任务(几十分钟、跨小时)循环里,进程随时可能挂——OOM、网络抖动、用户关浏览器。循环状态必须能落盘、能恢复。把 state 序列化成 JSON 存 Redis/DB,每步 checkpoint,重启时从最后一步接着跑。这是 Agent 从"玩具"到"产品"的分水岭。

python
state = load_or_init()
while not done(state):
    state = step(state)
    save(state)   # 每步落盘

Trace 与可观测:Agent 出错时,你需要能看到完整推理链——每一步的 Thought、调了什么 tool、传了什么参数、返回了什么。这是调试的命脉。把每步的结构化事件打到 trace 系统(LangSmith、Langfuse、Arize Phoenix 都行),按 run_id 串成树。没有 trace 的 Agent 是黑盒,黑盒没法运维。

并发与并行尝试:树搜索里我提到"多分支并行尝试",工程上靠 asyncio / 线程池让多个 branch 同时跑。但要注意:只有纯函数式的、无副作用的分支才能并行(比如同时让两个思路去搜文档);涉及共享环境(同一个文件系统、同一个 DB)的并行只会互相踩踏。并行带来的复杂度(结果合并、资源竞争、错误处理)往往超过收益,默认串行、确实有收益才上并行。

小结

  1. 循环的本质是 think → act → observe,但"转起来"只是及格线。决定 Agent 好不好用的是循环里的判断:何时停、何时反思、何时换路。
  2. 停止条件要层层兜底。完成信号 + 最大步数 + 预算上限 + 用户中断 + 死循环检测,五道闸 OR 起来,缺一不可。
  3. 反思能显著提升复杂任务成功率,但要换角色、看场景,简单任务开反思是负优化。
  4. 预算的三道闸(步数/时间/token)对应三种失控模式,且要全局共享、可优雅降级。
  5. 人工检查点是不可逆操作的标配,它不是降低自主性,而是扩大 Agent 能被授权的工作范围。

Agent 的智能,一半在模型,一半在循环。模型决定天花板,循环决定下限——而生产环境里,下限才是决定生死的那条线。

Related

相关推荐