这个专题从一个很实际的问题开始。
我们看着一个代码智能体连续执行了一百多步 read。它不断读取文件、继续读取、再继续读取,看起来像一个停不下来的循环。
于是问题来了:
Agent 是不是只要发现还有 Tool,就会继续 Call?
不是。
至少在经典的 ReAct 范式里,真正驱动循环的从来不是“还有工具可以调用”,而是:工具返回的新观察改变了上下文状态,模型基于新的状态重新做了一次决策。
这一点看起来简单,却几乎决定了我们怎样理解今天所有基于大模型的 Agent。
本文是「一起学智能体」专题第一期。学习主线参考 Datawhale 的开源项目 Hello-Agents,但不会照着教材逐章复述。我们会把概念不断拉回真实工程,尤其是 UIChat Mira 正在经历的 Planner、Tool、Evidence、Context 与完成判断问题。
最小 ReAct 到底是什么#
Hello-Agents 在第四章把 ReAct 概括为一个不断循环的过程:
Thought → Action → Observation → Thought → ...其中:
Thought:模型根据当前任务和已有信息判断下一步;Action:决定执行一个动作,通常是调用工具;Observation:工具执行后返回的新信息;- 然后再次进入新的
Thought。
所以真正的控制流更像这样:
用户任务
↓
LLM 决策
↓
调用工具
↓
工具返回 Observation
↓
Observation 进入上下文
↓
再次调用 LLM
↓
重新判断
├── 还缺信息 → 再调用工具
└── 已经完成 → 输出答案关键在最后那次“重新判断”。
Tool Call 本身不会自动生成下一次 Tool Call。
是新的 Observation 改变了当前状态,模型重新看了一遍“我要做什么、我已经知道什么、下一步还能做什么”,然后才决定继续还是停止。
“回看证据”不一定是一个独立节点#
我们很容易带着工程框架的视角去想:一个成熟 Agent 应该有 Planner、Evidence Review、Completion Checker,甚至还应该有专门的 Reflection Node。
但最原始的 ReAct 并不需要这些名字。
它可能只是不断增长的一段对话历史:
user: 查清楚 A、B、C
assistant: call read(A)
tool: A 的结果
assistant: call read(B)
tool: B 的结果
assistant: ...然后再次调用:
LLM(messages, tools)这次新的 LLM 调用,本身就是一次“回看”。
模型重新读到了原始目标,也读到了前面所有 Action 与 Observation,于是它可以从自然语言上下文中推断:A 已经完成了,B 有结果了,C 还没做。
因此,ReAct 可以没有显式的状态字段,却仍然表现得像一个状态机。
状态可以是隐式的#
如果我们用工程化的数据结构表达,一个 Agent 的状态可能长这样:
State(t) = {
globalGoal,
history,
observations,
evidence,
goalCoverage,
failures,
availableTools
}但最小 ReAct 并不一定真的维护这些字段。
它可能只有上下文:
用户要 A、B、C。
A 已经查到了。
B 已经查到了。
C 还没有查。模型自己从文本里“读出”当前状态。
于是每一轮实际上都在做:
Decision = LLM(Current Context + Tools)工具列表甚至可以几乎不变。
比如一个 CLI Agent 每轮都只有:
read
write
search
terminal真正不断变化的是上下文状态,而不是工具数量。
这就解释了为什么今天很多看起来能力惊人的 Coding Agent,底层暴露给模型的工具其实非常少。
少量稳定工具 + 动态上下文 + 每轮重新决策,就足以形成复杂行为。
为什么它又会陷入一百多次 read#
问题也恰恰出在这里。
如果所有控制责任都交给模型隐式完成,那么模型每轮不只是在“选择工具”。它实际上还同时承担:
理解刚才得到了什么
判断当前进度
判断证据是否充分
决定下一步动作
判断整个任务是否完成这些责任全部藏在一次 Thought 里。
于是一个 Agent 可以非常灵活,也可以非常失控。
它可能不断觉得:
再读一个文件,我就更确定了。
然后:
read A
→ 得到一些信息
→ read B
→ 得到一些信息
→ read C
→ 得到一些信息
→ 还是不确定
→ read D
→ ...只要模型每轮都判断“还有潜在信息增益”,循环就会继续。
所以看到一个 Agent 连续执行一百多次工具调用,不能立刻得出“它没有回看证据”的结论。
它可能每轮都在回看。
真正缺失的,往往是更可靠的完成判断与停止条件。
有证据,不等于任务完成#
假设用户要求:
查清楚 A、B、C,并给出结论。Agent 已经获得:
A ✅
B ✅
C ❌这时它可能已经拥有很多高质量证据,甚至足以写出一段看起来不错的回答。
于是会出现一个非常危险的判断:
canAnswer = true然后直接结束。
但正确的状态其实可能是:
canAnswer = true
taskComplete = false这两个状态完全可以同时成立。
“已经有东西可以回答”和“用户交给我的全部目标已经完成”,不是一回事。
更完整的 Agent 需要知道:
Goal:
- A
- B
- C
Coverage:
- A ✅
- B ✅
- C ❌这就是 Goal 与 Goal Coverage 的区别。
只记住原始目标还不够。系统还需要动态知道:目标的哪些部分已经被覆盖,哪些部分仍然悬空。
停止,也不等于完成#
再往前一步。
假设 C 连续调用工具失败,恢复次数已经耗尽,或者上下文已经接近容量上限。
此时:
A ✅
B ✅
C ❌
canAnswer = true
canContinue = falseAgent 应该停止继续执行。
但这仍然不意味着:
taskComplete = true它可以诚实地结束本轮运行:
A、B 已完成;C 因为工具失败或当前资源限制尚未完成。本轮无法继续可靠执行。
所以至少要区分三个概念:
Goal Complete
用户目标是否真的全部完成?
Loop Terminated
当前循环是否应该停止?
Run Completed
这次运行是否正常收尾?完全可能出现:
GoalComplete = false
LoopTerminated = true
RunCompleted = true事情没办完,但这一轮已经没有条件可靠继续,而且系统正常、诚实地完成了收尾。
这比一个粗暴的 status = completed 精确得多。
回到 Mira:显式化 ReAct 的隐式控制#
UIChat Mira 当前的 Agent Loop 大致可以抽象为:
Planner
→ Normalize
→ Policy
→ ToolNode
→ Evidence
→ Planner如果把名字拿掉,它和 ReAct 有非常明显的同构关系:
ReAct:
Reason → Action → Observation → Reason
Mira:
Planner → Tool → Evidence → Planner区别不在于 Mira “不是 ReAct”。
更准确地说,Mira 正在尝试把 ReAct 里原本藏在模型 Thought 中的一部分责任显式化。
比如:
answerReadiness
recoverable
terminal
latestSummary
goalCoverage
completion原始 ReAct 可能只需要模型说一句:
我觉得已经够了。
工程化 Agent 则希望知道:
globalGoal = A + B + C
coverage = A✅ B✅ C❌
canAnswer = true
taskComplete = false
retryCount = 3
evidenceGain = low然后再做下一步判断。
所以一个很有用的理解是:
Mira 不是在抛弃 ReAct,而是在给 ReAct 补仪表盘。
这一期真正要记住的四句话#
第一,Tool Call 不会自动产生下一次 Tool Call。
真正产生循环的是:Observation 更新状态以后,控制权重新回到决策。
第二,ReAct 的“回看证据”通常就是下一轮 LLM 调用本身。
不一定需要一个名字叫 Evidence Review 的节点。
第三,最小 ReAct 没有独立 Planner。
规划、进度判断、工具选择和完成判断,都可以隐式藏在每一轮 Thought 里。
第四,显式 Planner / Evidence / Goal Coverage 的价值,是把隐式控制逻辑工程化。
这样它才更容易被观察、测试、约束和修复。
下一期,我们继续一个和 Mira 更直接的问题:
Plan-and-Solve 和 ReAct 到底差在哪?Planner 应该先规划整条路,还是每一步根据新证据重新规划?
这会把“静态计划”和“动态规划式 Agent Loop”真正拆开。
