Agent 面试题

Agent Loop 与 ReAct:循环怎么转、怎么停

Agent Loop 与 ReAct:循环怎么转、怎么停

Agent Loop 是一个 while 循环:组装上下文 → 模型决策 → 执行工具 → 观察回灌 → 判停。听起来是「谁都会写」,但能不能写严谨,是「会调框架」和「会做 Agent」的分界线——因为循环写得不严谨,模型再强也救不回来。

前六期铺垫到这一期,主线已经完整:模型只提出决策,运行时负责执行。这一期把「运行时」的心脏——那个 while 循环——拆到最细。

它与相邻篇的分工要说清:

  • 本篇只讲「循环本身」:怎么转、怎么停、怎么防绕圈;
  • 循环外面的容器(Harness 六件套:Loop / Context Manager / Tool Runtime / State Store / Memory / Guardrails)见第 09 期;
  • 循环里的计划与状态(Planning、Checkpoint、Resume)见第 08 期。

本篇回答两个问题:

#面试问题指向哪个工程面
Q1什么是 ReAct?它和 CoT 有什么区别?循环的认知范式
Q2Agent Loop 由哪几步组成?怎么决定什么时候停止?无限循环怎么防?循环的工程实现

Q1 说明「为什么要有循环」,Q2 说明「循环怎么写成能上线的样子」。Q1 答不出「外部观察注入」,Q2 的六步就只是机械背诵。


一、ReAct 与 Chain-of-Thought 是什么关系

1.1 推荐回答

CoT 是「只推理」,ReAct 是「推理与行动交错」。 差别只有一个字,但决定了整个范式:

CoT(Chain-of-Thought):问题 → 想 → 想 → 想 → 答案
                              全程封闭,没有外部信号

ReAct(Reasoning + Acting):想 → 做(调工具)→ 看(观察)→ 再想 → 再做 → …
                              每一步都被真实世界锚定

核心差异:ReAct 把外部观察(Observation)重新注入推理链,让真实世界的反馈可以推翻上一步的推理。CoT 没有这个回路——它一旦推错,误差就一路累积到最后。

1.2 逐项对比

维度Chain-of-ThoughtReAct
输出结构纯推理链推理 + 动作 + 观察 交替
信息源只有模型内部知识内部知识 + 外部工具返回
纠错时机结束后(错了就是错了)每步都能被观察纠正
调用成本1 次N 次(每步一次)
适用任务纯推理(数学、逻辑)需要外部信息/副作用的任务
失败模式误差累积、幻觉事实绕圈、贪心短视、成本高

一句话:CoT 让模型把推理写出来,ReAct 让模型把推理交给环境校验。

1.3 一个具体例子(代码实证)

同一个任务「订单能不能退款」,两种范式的写法:

CoT   :模型凭记忆推断「一般 7 天可退」,直接答「可以退」     ← 政策可能是别的天数,错
ReAct :先 get_order 拿到签收日期 → 再 check_refund_policy 判定 → 再决定写操作
        每一步都有事实依据,观察结果可以推翻上一步

这段差别不是「哪个更聪明」,而是有没有把不确定性交给环境。退款政策是业务事实,不该靠模型「记得」。


二、Agent Loop 的六个动作

这是本篇的骨架。一个能上线的循环,每一步都有它的坑:

① 组装上下文  →  ② 调用模型决策  →  ③ 解析输出
                                      ↓
⑥ 判停  ←  ⑤ 观察回灌  ←  ④ 执行工具

2.1 ① 组装上下文

把 System、用户目标、召回的记忆、任务状态、近期工具结果、最近对话按预算拼起来——细节全在第 05 期。这里只强调一句:这一步是每轮都要重做的,也是成本增长的主因(见 5.1)。

2.2 ② 调用模型决策

把组装好的上下文发给模型,拿回一个决策。这一步本身没坑,坑在于**「决策」的形态有三种**,第 ③ 步要处理。

2.3 ③ 解析输出

模型的输出可能是三种形态,解析层必须都接得住:

形态说明处理
原生 tool_callsAPI 结构化返回直接用
文本 JSON模型在文本里写了 JSON抽 {...} 再解析
传统 ReAct 文本Thought/Action/Observation 格式正则解析(脆弱)

为什么现在不流行文本 ReAct 格式? 因为它解析脆弱(正则一错整轮失败)、无法并行调用、格式错误就是硬失败。原生 function calling 把这层交给模型和 API 约束,运行时省心很多。但即便如此,解析层仍要处理「格式坏掉」——见 4.3 的三级处理。

2.4 ④ 执行工具

真正调用工具。这一步的工程约束:

  • 超时:每个工具必须有超时,不能挂死循环;
  • 权限:写操作要过权限校验(第 04 期);
  • 错误结构化:返回 {ok, error:{code, message, retryable}},而不是抛异常——因为下一步要把它回灌给模型(见 4.3)。

2.5 ⑤ 观察回灌——最容易漏的一步

把工具结果写回 messages。这一步如果漏了,模型下一轮「不记得」自己调过工具,就会再调一次——这是初学者 Agent 死循环的头号成因。

配套代码里,只要把回灌那两行注释掉,收敛型模型立刻退化成绕圈型。这不是理论——是代码跑出来的。

2.6 ⑥ 判停

见第 4 节。这是本期的重头。

2.7 单步执行的四个不变式

一个 step 要做到四件事,缺一不可:

  1. 一个 step 只做一件事——一个工具调用,或一次最终答复;
  2. 副作用必须幂等或可补偿——否则重试/Resume 会做两遍(第 08 期展开);
  3. 观察必须结构化——不要让模型读自由文本工具结果;
  4. 每步必须可序列化——不能含连接、句柄、闭包等不可恢复引用(否则无法 checkpoint)。

三、ReAct 的四类缺陷

ReAct 不是银弹,它的缺陷必须在面试里主动说出来:

缺陷表现缓解
贪心短视每步只看眼前,缺全局规划,长任务走偏外层加 Planning(第 08 期)
误差放大前一步的小错在后续循环被反复放大每步校验 + Reflection
漂移 / 绕圈长任务里忘记目标,原地打转循环检测(4.6)+ 目标重述
成本线性涨每步一次调用,且重发全历史上下文压缩(第 05 期)+ 预算兜底

「贪心短视」是 Plan-and-Execute 存在的理由——这正是第 08 期的起点。


四、终止条件:三重保障

4.1 推荐回答

终止必须三层,少任何一层,循环都会以你不希望的方式结束:

显式终止   ← 模型输出终止动作(正常收尾)
隐式兜底   ← max_steps / max_tokens / timeout / cost_budget / tool_call_limit
循环检测   ← 同一 (tool, args) 重复 N 次 / 连续无新增信息
  • 少了显式终止:任务完成了也不知道停;
  • 少了隐式兜底:模型不给终止时就无限跑(烧钱);
  • 少了循环检测:兜底之前它已经白烧了几十步的钱,还可能执行了不该执行的写操作。

4.2 三类终止的对照(代码实证)

同一个循环跑三个场景,停止原因完全不同:

场景模型停止原因步数token成本副作用
正常收敛收敛型MODEL_FINISHED445100.027061
原地绕圈绕圈型LOOP_DETECTED335250.021150
格式坏掉格式坏掉型MODEL_FINISHED(修复后收敛)449150.029490

三个读法:

  1. 显式终止:收敛型在第 4 步主动给出最终答复——正常收尾靠模型自收手;
  2. 循环检测:绕圈型永远不给终止动作。如果没有 LoopDetector,它会一直转到 MAX_STEPS,而且中间还敢真的调 create_refund——那就出事了;
  3. 隐式兜底:格式坏掉型前两次输出解析失败(占了两步),走的是「回灌 → 让模型自己修」。这两次失败也占步骤,这就是 max_steps 必须留够余量的原因。

4.3 解析失败的三级处理

格式坏掉型演示的是这段:

def parse_decision(raw):
    try:
        return {"ok": True, "value": json.loads(raw)}          # 1. 直接解析
    except json.JSONDecodeError:
        pass
    m = re.search(r"\{.*\}", raw, re.S)
    if m:
        try:
            return {"ok": True, "value": json.loads(m.group(0)), "repaired": True}  # 2. 抽 {...}
        except json.JSONDecodeError:
            pass
    return {"ok": False, "error": {                              # 3. 结构化错误 → 回灌
        "code": "MALFORMED_OUTPUT", "message": "输出不是合法 JSON,请只返回一个 JSON 对象",
        "retryable": True, "raw_preview": raw[:60]}}

关键:解析失败要回灌让模型修,而不是抛异常终止。 但必须有重试上限——同一错误反复重试就是绕圈的另一种形态(受 max_steps 兜底)。

4.4 一次 step 的完整流程(含所有兜底)

配套代码的 run_agent 主干(省略细节):

for step in range(1, budget.max_steps + 1):
    d = model.decide(msgs)                              # ② 调用模型
    budget.tokens_used += d.prompt_tokens + d.completion_tokens

    if d.kind == "malformed":                           # ③ 解析失败 → 回灌
        parsed = parse_decision(d.raw)
        if not parsed["ok"]:
            msgs.append({"role": "user", "content": json.dumps(parsed["error"])})
            continue
    if d.kind == "final":                               # 显式终止
        stop_reason = "MODEL_FINISHED"; break

    looped, why = detector.observe_action(d.tool, d.args)   # 循环检测(工具调用前)
    if looped:
        stop_reason = "LOOP_DETECTED"; break

    result = execute_tool(d.tool, d.args, side_effects)     # ④ 执行工具
    msgs.append({"role": "assistant", "name": d.tool})      # ⑤ 观察回灌
    msgs.append({"role": "tool", "name": d.tool, "payload": result})

    dup, why2 = detector.observe_observation(result, prev_obs)  # 无进展检测
    if dup:
        stop_reason = "NO_PROGRESS"; break

    if budget.tokens_used >= budget.max_tokens:             # ⑥ 隐式兜底
        stop_reason = "TOKEN_BUDGET"; break
else:
    stop_reason = "MAX_STEPS"

注意循环检测的两个位置:工具调用前(抓重复动作)和观察回灌后(抓无新增信息)。

4.5 模型给了不存在的工具 / 参数不合法怎么办

把结构化错误回灌让它改,而不是抛异常终止。 例如:

{"code": "UNKNOWN_TOOL", "message": "未知工具 get_oder,可用工具:get_order, check_refund_policy, create_refund", "retryable": true}

模型下一轮会自己纠正拼写。但要设重试上限——同一个错误反复重试,本质还是绕圈,由 max_steps / 循环检测兜底。

4.6 绕圈检测的工程做法(代码实证)

两条互补的信号:

信号一:动作重复计数。 对 (tool_name, 规范化 args) 做哈希,同一组合重复到阈值就判定绕圈。

key = sha1((tool + json.dumps(args, sort_keys=True)).encode()).hexdigest()[:10]
self.counts[key] += 1
if self.counts[key] >= self.max_repeat:      # 默认 3
    return True, f"动作 {tool} 已重复 {self.counts[key]} 次,判定为原地绕圈"

为什么必须是「动作」而不是「话术」? 看这段对比:

第 1 轮:「让我先查一下订单状态。」
第 2 轮:「好的,我再确认一下这个订单的信息。」
第 3 轮:「为了准确起见,我再查询一次订单详情。」

措辞三轮全不一样,动作完全一样。所以判据落在动作上。

信号二:观察无进展。 连续两轮观察结果完全相同 → 新增信息量为零 → 无进展。

两个信号互补:一个抓「重复做」,一个抓「做了也白做」。

4.7 怎么区分「合理的重试」和「绕圈」

这是面试里的好问题。判据不是「重复了几次」,而是新增信息量是否趋零:

  • 重试后拿到了新的错误码 / 新的状态 → 有信息增益,是合理重试;
  • 重试后观察结果与上一轮完全相同 → 零信息增益,是绕圈。

所以循环检测不能只看重复计数,还要看观察是否变化——这也是代码里两个信号都要有的原因。

4.8 终止条件设多少步合适

不背数字,给判据:

任务类型参考区间判据
单工具查询3–5 步一次查询 + 一次答复 + 余量
多步分析10–20 步每步一个工具,留 2–3 步格式修复余量
长任务(带 Planning)按计划步数 × 1.5留重规划余量

关键是留余量给格式修复和非预期步骤——代码里格式坏掉型就白占了 2 步。


五、Loop 的成本形状

5.1 为什么 Agent 比 Workflow 贵得多

每轮都要重发完整历史。第 N 轮的输入 token ≈ 前 N-1 轮的累积:

第 1 轮:system + user
第 2 轮:+ assistant + tool result
第 3 轮:+ assistant + tool result
...
第 N 轮:全部历史

输入 token 随轮次近似二次增长。这和第 02 期「Workflow vs Agent 4.54×」的实测是同一个根因——不是模型调用次数多,而是每一轮都在重发累积的上下文。

5.2 缓解手段

  • KV Cache / 前缀稳定:让重复前缀命中缓存(第 05 期 3.4);
  • 上下文压缩:压掉旧的工具结果(第 05 期第 4 节);
  • 预算兜底:max_tokens / cost_budget 硬性截断;
  • 子 Agent 隔离:把探索过程放到子循环,只回传结论(第 05 期 4.5)。

六、一轮里的并行工具调用

无依赖的工具可以同一轮并行发出;有依赖的必须串行。判据:

可以并行必须串行
多个独立查询(查 A 订单 + 查 B 订单)有数据依赖(先查 ID 再用 ID 查详情)
只读操作之间有顺序语义(先创建再更新)
无共享资源写写同一共享资源

并行能显著降低墙钟时间,但返回值顺序必须与发出顺序对应,否则模型会把结果对错工具。细节与第 12 期的并发章打通。


七、常见的错误认识

  1. 「Agent 就是 while True 调模型」 —— 六个动作,每个都有坑;
  2. 「有 max_steps 就不会死循环」 —— 会,只是死得慢;兜底之前可能已经烧了几十步;
  3. 「模型承诺会停就会停」 —— 模型说「我马上收尾」和它真的收尾是两件事,兜底必须存在;
  4. 「观察回灌可以省」 —— 漏了它,收敛型立刻变绕圈型;
  5. 「解析失败就抛异常」 —— 应该回灌让模型修,但有上限;
  6. 「绕圈看模型有没有说重复话」 —— 要看动作,不是话术;
  7. 「重试次数超过 N 就是绕圈」 —— 看信息增益,不看次数;
  8. 「循环检测可以省,反正有 max_steps」 —— max_steps 是最后一道墙,不是第一道;
  9. 「Loop 写得不严谨没关系,模型强就行」 —— 循环是骨架,骨架歪了肌肉救不回来;
  10. 「并行调用随便并行」 —— 有依赖/有写冲突的并行会让状态错乱。

八、概念速查

概念一句话
ReAct推理与行动交错,把观察注入推理链
CoT只推理,全程封闭,无外部信号
六个动作组装上下文 / 调模型 / 解析输出 / 执行工具 / 观察回灌 / 判停
显式终止模型输出终止动作
隐式兜底max_steps / max_tokens / timeout / cost / tool_call_limit
循环检测(tool,args) 哈希计数 + 观察无进展
单步四不变式一事 / 幂等 / 结构化 / 可序列化
三级解析直接解析 → 抽 {...} → 结构化错误回灌
Loop 成本形状每轮重发全历史 → 输入 token 近似二次增长

九、动手实验

配套代码:

agent-developer-interview/agent-07-agent-loop

实验用一个零依赖的循环实现,把六个动作、三类终止、绕圈检测全部参数化,三个脚本化模型分别演示正常收敛、原地绕圈、格式坏掉三种情形。

9.1 核心代码节选(一):循环主干

见 4.4 的 run_agent——每一步都有编号注释,重点在「循环检测出现在两个位置」。

9.2 核心代码节选(二):绕圈检测

见 4.6 的 LoopDetector.observe_action。真实输出:

第 1 次 get_order({"order_id": "A1001"}) → 继续
第 2 次 get_order({"order_id": "A1001"}) → 继续
第 3 次 get_order({"order_id": "A1001"}) → 🛑 已重复 3 次,判定为原地绕圈

9.3 核心代码节选(三):ReAct vs CoT

def show_react_vs_cot():
    # CoT:全程封闭,中间错了也没有外部信号纠正
    # ReAct:想 → 做 → 看 → 再想,每一步都被真实世界锚定
    ...

9.4 脚本与结论对应表

段落验证的结论
三场景 trace显式终止 / 循环检测 / 隐式兜底各自的触发形态
终止对照表三类终止条件「少一层会怎样」
绕圈哈希演示判据是动作,不是话术
ReAct/CoT 对比「把推理交给环境校验」

9.5 三个「改一改再跑」的练习

  1. 把 Budget.max_steps 从 8 降到 3,观察「格式坏掉型」在第 3 步还没修完就被 MAX_STEPS 掐掉;
  2. 把「⑤ 观察回灌」那两行 msgs.append 注释掉,观察收敛型模型如何退化成绕圈型;
  3. 把 LoopDetector(max_repeat=2),观察多早就会误伤「合法的重复查询」(如分页翻页)。

十、延伸阅读

站内

  • LLM 基础与模型边界:Attention 的 O(n²) 与 KV Cache,是 Loop 成本形状的机制来源。
  • 第 02 期:什么是 AI Agent——标准链路、Workflow vs Agent 的 4.54× 实测。
  • 第 05 期:上下文工程与压缩——每轮组装上下文的预算管理与压缩手段。
  • 第 06 期:记忆系统——循环里召回的「关于用户的事实」从哪来。
  • 第 08 期:Planning 与状态管理——循环里的计划、Checkpoint 与断点续跑。
  • 第 09 期:Harness——循环外面的六件套容器。
  • AI 笔记:异步流水线:工具执行的异步化与调度。

站外

  • Yao et al.:ReAct: Synergizing Reasoning and Acting in Language Models——ReAct 原始论文。
  • Wei et al.:Chain-of-Thought Prompting Elicits Reasoning in Large Language Models——CoT 原始论文。
  • Anthropic:Building effective agents——为什么「简单循环 + 好工具」常胜过复杂框架。
  • OpenAI:Function calling 文档——原生 tool_calls 的返回结构。

十一、一句话总结

Agent Loop 的六个动作里,最容易漏的是「观察回灌」;终止条件的三个层次里,最容易被省的是「循环检测」。这两个恰恰是循环能不能上线的分水岭。

而一切终止机制背后有一条统一的判据:循环该继续还是该停,看的是「有没有新生信息」,不是「模型怎么说」。 有信息增益就继续,零增益就是绕圈——动作重复计数只是这条判据的一个可实现的代理。

AI Agent面试ReActAgent Loop

← 返回面试题