跳到主要内容

L6.2 Agent 与多步推理

三维坐标 layer: L6(应用架构)level: Seniorpillar: 训推框架

上一篇 RAG 解决了「让模型读到正确的知识」,但它本质仍是单轮的「检索 → 拼上下文 → 生成」。本文进入 Agent 领域——当一个任务需要多步推理、调用外部工具、并根据中间结果动态决策时,我们如何把一个「只会续写文本」的 LLM 组装成一个「能规划、能行动、能纠错」的智能体,以及为此付出的可控性代价

学习目标

  • 前置知识:读过 L6.1 RAG(知道「检索 → 拼上下文 → 生成」的单轮范式与其局限);对 L4 推理层有基本认知(知道一次 LLM 调用就是一次 token 生成、有延迟与成本);会写基础 Python(函数、字典、循环)。无需任何 Agent 框架经验。
  • 学完产出:① 能画出一个最小 Agent 的 ReAct 循环(Thought → Action → Observation → Reflection),并说清「反思边」为什么是 Agent 区别于一次性流水线的关键;② 能用「短期记忆 vs 长期记忆」「先规划后执行 vs 边走边规划」两组对照,讲清 Agent 跨轮状态管理的设计空间;③ 能设计一条带明确退出条件的 Fallback 降级链,并指出它在哪里会引入无限循环/雪崩;④ 能在「可控性 / 灵活性 / 成本 / 失败模式」四个维度上对比 DAG 与 Agentic 两种编排,并解释「能用 DAG 钉死的步骤绝不交给 Agent」这条黄金法则;⑤ 亲手跑一个最小 ReAct Agent,量化「单步 DAG 硬套多步任务」与「Agentic 自适应」的成功率差异。
  • 阅读姿势:盯住一条主线——「Agent 工程的全部努力,都是在『让模型自主』与『让系统可控』之间找平衡点」。从 ReAct 的反思边、到 Fallback 的退出条件、再到 DAG 与 Agentic 的混合编排,本质都是同一件事:用结构约束自主性,让开放探索的自由度只发生在它真正必要的局部。

背景与现状

Agent(智能体) 的工程定义并不玄学:它是一个以 LLM 为决策核心、以「感知 → 决策 → 行动」循环驱动、能调用外部工具并消化反馈的程序。与单次问答相比,Agent 的三个本质区别是:

  • 多步性:一个任务被拆成若干轮 thought → action → observation,而非一次生成完成。
  • 工具性:模型不再「凭参数记忆瞎编」,而是通过 tool calling(工具调用) 把计算、检索、写文件等动作外包给确定性的程序。
  • 状态性:跨轮维护 memory(记忆)planning(规划),让后续决策能看到前面的进展。

从产业演进看,可以概括为三个阶段:

  • 2022 前:思维链(Chain-of-Thought)让模型「把推理过程写出来」,但仍是纯文本、不接触外部世界。
  • 2022–2023ReActReason + Act)范式确立——把「推理」和「调用工具行动」交织进同一个循环,Agent 第一次能查维基、做计算、查实时数据。开源端 LangChainAutoGPT 引爆「让模型自己跑」的浪潮。
  • 2023 至今:行业从「能跑」回落到「可控」。大家发现纯 Agentic(全自主)在生产环境成功率低、成本高、难调试,于是 DAG(静态工作流)与 Agentic(动态自主)的混合编排 成为主流——确定的步骤用 DAG 钉死,开放的探索才交给 Agent。

业界信号:ReAct 论文成为 Agent 领域引用量最高的奠基工作之一;而 LangGraphTemporalDify 等框架不约而同回归「图(Graph)」抽象——这说明工程界的共识已是「用结构约束自主性」,而非放任模型自由发挥。

原理与架构

2.1 ReAct:推理与行动交织的反思循环

一个最小 Agent 的核心是一个循环,每一轮 LLM 输出三类内容之一:Thought(思考下一步)Action(决定调哪个工具、传什么参数)、或 Final Answer(任务完成)。工具执行后的结果作为 Observation 喂回上下文,进入下一轮。当某一轮行动失败或观测异常时,Reflection(反思) 让模型审视自己的轨迹并修正策略——这正是 Agent 区别于「一次性流水线」的关键。

读这张图:循环的「智能」来自 LLM(蓝色:Thought/Action/Observation 消化),「确定性」来自工具(绿色),而「鲁棒性」来自反思边(橙色:当观测异常时不是硬着头皮往下走,而是回退重新规划)。没有反思边的 Agent,一旦中间一步出错就会一路错到底

2.2 Memory 与 Planning:跨轮状态的两条线

维度短期记忆(Short-term)长期记忆(Long-term)
载体当前对话的 context window(轨迹历史)外部向量库 / KV 存储
生命周期单次任务内有效跨任务、跨会话持久
典型内容本轮已做的 thought/action/observation用户画像、历史结论、经验沉淀
瓶颈受上下文长度限制,轮次多了会溢出检索召回质量决定可用性

Planning(规划) 则分两派:先规划后执行(Plan-and-Execute:先让模型列出完整步骤计划,再逐条执行,可控但不灵活)与边走边规划(ReAct 式:每轮临场决策,灵活但容易跑偏)。生产系统常取折中——先出粗粒度计划骨架,骨架内的每一步再用 ReAct 小循环执行

2.3 Agent 路由与 Fallback:把「自主」关进笼子

真实系统里 Agent 极少是单一模型、单一工具。路由(Routing) 负责把请求分发到合适的处理者,Fallback(降级) 负责在首选失败时退到备选,二者共同决定系统的成功率与成本

  • 工具路由:根据意图选择计算器 / SQL / 检索 / 代码执行等不同工具。
  • 模型路由:简单子任务走小模型(便宜快),复杂推理才升级到大模型(贵但强)。
  • Fallback 链:工具调用超时/报错 → 重试 → 换备用工具 → 降级到「直接让大模型作答」→ 兜底返回「无法完成」。关键是每一级都要有明确的失败判定和退出条件,否则会陷入无限重试或无限循环。

2.4 DAG vs Agentic:编排范式的根本权衡

维度DAG(静态工作流)Agentic(动态自主)
路径编译期固定,节点与边预先画好运行期由 LLM 每轮临场决定
可控性 / 可调试高——可单步重放、可断言中间态低——同一输入轨迹可能不同
灵活性低——遇到计划外情况无法应变高——能处理开放式、未预见的任务
成本 / 延迟可预估、稳定不可控,轮次发散时成本飙升
失败模式节点失败可精确定位容易陷入循环、跑偏、幻觉行动

需要强调的是:DAG 与 Agentic 不是二选一,而是一根可控性光谱。工程上的黄金法则是——「能用 DAG 钉死的步骤绝不交给 Agent」。把开放探索的自由度严格限制在必要的局部,是 Agent 系统从 Demo 走向生产的分水岭。

动手实践:极简代码实操

实验目标:用纯 Python(CPU 即可,不依赖 GPU)实现一个带工具调用 + 反思循环的最小 ReAct Agent,配一个计算器工具与一个 mock LLM(用规则模拟模型决策,无需联网/显卡;文末给出切换到 ollama 的方法)。然后在一组多步算术任务上,对比「固定 DAG 流程」与「Agentic 动态流程」的成功率,亲手看到两种范式的权衡。

3.1 环境准备

# 仅需标准库即可运行;推荐 Python 3.11
python3 -m venv .venv && source .venv/bin/activate
# 本实验零三方依赖。若想换真实模型,可选装 ollama 客户端:
# pip install ollama # 并先 `ollama run qwen2.5:3b` 拉起本地小模型

3.2 代码:最小 ReAct Agent + DAG vs Agentic 对比

import re
import operator

# ---------- 工具层(确定性,可被 Agent 调用)----------
def calculator(expr: str) -> str:
"""安全的四则运算计算器工具。"""
if not re.fullmatch(r"[0-9+\-*/(). ]+", expr or ""):
return "ERROR: 非法表达式"
try:
# 受限 eval:仅允许数字与四则运算字符
return str(eval(expr, {"__builtins__": {}}, {}))
except Exception as e:
return f"ERROR: {e}"

TOOLS = {"calc": calculator}

# ---------- Mock LLM:用规则模拟「模型决策」----------
# 真实场景这里换成 ollama / OpenAI 调用,返回同样格式的决策。
def mock_llm(goal: str, history: list) -> dict:
"""
根据任务与历史轨迹,返回下一步决策:
{"type": "action", "tool": "calc", "input": "..."} 或
{"type": "final", "answer": "..."}
模拟「先拆解、逐步计算、最后汇总」的多步推理。
"""
nums = re.findall(r"-?\d+", goal)
steps_done = [h for h in history if h["role"] == "observation"]

# 任务1:求和 a..b 区间内整数和 -> 用公式分步算
if "区间和" in goal and len(nums) >= 2:
a, b = int(nums[0]), int(nums[1])
if len(steps_done) == 0:
return {"type": "action", "tool": "calc", "input": f"({a}+{b})*({b}-{a}+1)/2"}
last = steps_done[-1]["content"]
if last.startswith("ERROR"):
# 反思:换一种不带除法精度问题的表达式
return {"type": "action", "tool": "calc", "input": f"({a}+{b})*({b}-{a}+1)//2"}
return {"type": "final", "answer": str(int(float(last)))}

# 任务2:复利 本金*(1+r)^n -> 需要分两步(DAG 只算一步会失败)
if "复利" in goal and len(nums) >= 3:
p, r_pct, n = int(nums[0]), int(nums[1]), int(nums[2])
factor = f"(1+{r_pct}/100)"
if len(steps_done) == 0:
return {"type": "action", "tool": "calc", "input": f"{factor}**{n}"}
if len(steps_done) == 1:
growth = steps_done[-1]["content"]
return {"type": "action", "tool": "calc", "input": f"{p}*{growth}"}
return {"type": "final", "answer": f"{float(steps_done[-1]['content']):.2f}"}

return {"type": "final", "answer": "无法处理的任务"}

# ---------- Agentic 流程:ReAct 循环 + 反思 ----------
def run_agentic(goal: str, max_steps: int = 6) -> str:
history = []
for _ in range(max_steps):
decision = mock_llm(goal, history)
if decision["type"] == "final":
return decision["answer"]
history.append({"role": "thought", "content": f"调用 {decision['tool']}"})
obs = TOOLS[decision["tool"]](decision["input"])
history.append({"role": "observation", "content": obs})
# 反思边:观测出错则记录,下一轮 mock_llm 会读到并改策略
return "FAIL: 超出最大步数"

# ---------- DAG 流程:固定单步「检索->计算->输出」,无反思无多轮 ----------
def run_dag(goal: str) -> str:
# 静态流程假设「一步计算即得答案」,无法应对需要多步的任务
decision = mock_llm(goal, history=[])
if decision["type"] == "final":
return decision["answer"]
obs = TOOLS[decision["tool"]](decision["input"])
# DAG 到此结束:直接把第一次工具结果当答案(不再回流、不再多步)
try:
return str(int(float(obs)))
except Exception:
return f"FAIL: {obs}"

# ---------- 评测:一组任务上对比成功率 ----------
TASKS = [
("求 1 到 100 的区间和", "5050"), # 单步可解,DAG/Agentic 都行
("求 5 到 50 的区间和", "1265"), # 单步可解
("本金 1000 利率 5 复利 3 年的复利终值", "1157.63"), # 需两步,DAG 会失败
("本金 2000 利率 10 复利 2 年的复利终值", "2420.00"), # 需两步,DAG 会失败
]

def grade(name, fn):
ok = 0
for goal, gold in TASKS:
got = fn(goal)
passed = got == gold
ok += passed
print(f" [{name}] {goal[:20]:<22} 期望={gold:<9} 得到={got:<9} {'✅' if passed else '❌'}")
print(f" >>> {name} 成功率: {ok}/{len(TASKS)} = {ok/len(TASKS):.0%}\n")

if __name__ == "__main__":
print("== DAG 静态流程 ==")
grade("DAG", run_dag)
print("== Agentic 动态流程(ReAct+反思)==")
grade("Agentic", run_agentic)

3.3 运行与观察

python react_agent_lab.py

预期输出(关键对照):

== DAG 静态流程 ==
[DAG] 求 1 到 100 的区间和 期望=5050 得到=5050 ✅
[DAG] 求 5 到 50 的区间和 期望=1265 得到=1265 ✅
[DAG] 本金 1000 利率 5 复利 3 年 期望=1157.63 得到=1 ❌(int() 截断了浮点结果)
[DAG] 本金 2000 利率 10 复利 2 年 期望=2420.00 得到=2 ❌(int() 截断了浮点结果)
>>> DAG 成功率: 2/4 = 50%

== Agentic 动态流程(ReAct+反思)==
... 四题全部 ✅
>>> Agentic 成功率: 4/4 = 100%

核心结论:对单步任务,DAG 与 Agentic 都能解(且 DAG 更省 token、更可控);但对需要中间结果回流的多步任务(复利 = 先算增长因子,再乘本金),固定 DAG 只走一步就交卷,必然失败,而带循环与反思的 Agentic 流程能逐步推进直到收敛。这正是「该用 Agent 的地方」的精确画像——当且仅当任务路径无法在编译期确定时。

踩坑预警 (Gotchas)

  • eval 是炸弹:示例用正则白名单 + 空 __builtins__eval 关进笼子。真实工具调用绝不能直接 eval 模型输出,应走 ast.literal_eval、沙箱或专用解析器,否则模型一句话就能执行任意代码。
  • 没有 max_steps = 烧钱无底洞:Agentic 循环若不设步数上限,模型一旦跑偏会无限调用工具,token 成本和延迟瞬间爆炸。任何生产 Agent 都必须有硬性步数/超时熔断。
  • 反思要「能读到失败」才有效:反思不是魔法咒语——本例中 mock_llm 必须真正读到 historyERROR 开头的 observation 才会改策略。若上下文里丢了失败信息,反思边形同虚设。
  • mock LLM 的「成功」是规则保证的:本实验用规则模拟决策,成功率是确定的;换成真实 LLM 后,成功率会因模型能力、prompt 质量、工具描述清晰度而大幅波动,这恰恰是 Agent 工程的难点所在。
  • DAG「失败」不代表 DAG 差:若把复利任务的 DAG 显式设计成两节点,它同样能 100% 成功且更可控。本实验对比的是「用单步 DAG 硬套多步任务」与「用 Agentic 自适应」——揭示的是「范式与任务匹配度」,而非范式优劣。

深入思考

下面三题每题先给题干,再用 <details> 折叠一份图文并茂的参考答案。建议先合上答案自己想 3 分钟,再展开对照。

思考题 1:可控性与成功率的权衡

你的团队有一个客服 Agent,80% 的请求是「查订单状态」这类固定流程,20% 是「帮我对比并退掉最划算的那笔订单」这类开放任务。结合 2.4 节 DAG vs Agentic 的可控性光谱,你会如何混合 DAG 与 Agentic?请画出路由决策点,并说明「在哪一层判定一个请求该走静态流程还是动态 Agent」。

展开参考答案(含分流路由决策图 + 成本算一遍)

结论:在请求入口处先用一个轻量「意图分流器」判定任务是否落在已枚举的固定流程清单里,命中就走 DAG(可控、便宜、可断言),未命中才升级到 Agentic(自主、贵、需熔断)——把 Agent 的自由度严格关进那 20% 真正开放的请求里。

判定发生在哪一层:分流必须在请求入口的第一跳完成,而不是让 Agent 自己「跑着跑着发现这是固定流程」——后者已经付出了规划与工具调用的代价。分流器本身要尽量轻(规则匹配、关键词、或一个小模型 / 嵌入分类器),它只回答一个布尔问题:「这个请求是否落在我能枚举的固定流程清单里?」

用成本算一遍(数量级估算,假设日均 10 万次请求):

  1. 全 Agentic 方案:每次请求平均 4 轮 LLM 调用,若每轮约 0.002 美元,则单次约 0.008 美元,日成本 ≈ 100000 × 0.008 = 800 美元
  2. 分流方案:80% 走 DAG(单次仅 1 次小模型分流 + 模板响应,约 0.0005 美元),20% 走 Agentic(约 0.008 美元)。日成本 ≈ 80000 × 0.0005 + 20000 × 0.008 = 40 + 160 = 200 美元
  3. 结论:仅靠入口分流就把日成本从 800 美元压到 200 美元(降约 75%),同时 80% 的请求获得了 DAG 的可单步重放、可断言中间态——这正是「能用 DAG 钉死的就别交给 Agent」的真金白银体现。

思考题 2:Fallback 链设计

一个 Agent 的工具调用链是 检索 → 计算 → 生成。当「计算」工具偶发超时(5% 概率),请结合 2.3 节的路由与 Fallback 思想,设计一条带退出条件的 fallback 链——重试几次?何时换备用工具?何时降级到「让大模型直接估算并标注不确定」?如何防止 fallback 本身引入无限循环或雪崩?

展开参考答案(含 Fallback 降级链状态图 + 重试次数算一遍)

结论:Fallback 不是「失败就无脑重试」,而是一条每一级都带明确退出条件的有限状态机——重试有上限、换工具有上限、最终必有一个不会再失败的兜底出口,且全链路受一个总预算(总耗时 / 总成本)约束,任何一级耗尽预算都直接跳到兜底,从根上杜绝无限循环。

各级退出条件

  • 重试:同一工具最多重试 2 次,且必须指数退避(如 100ms → 300ms),避免在下游抖动时把请求一股脑砸过去引发雪崩。
  • 换备用工具:重试耗尽后切到备用计算服务,最多 1 次;备用工具同样有自己的超时。
  • 降级兜底:备用也失败,则降级到「让大模型直接估算并显式标注『结果不确定』」——这一级保证不会再失败(不依赖任何外部工具),是整条链的最终出口。
  • 总预算守卫:在所有级之上挂一个总耗时 / 总成本预算,任意时刻超限直接跳到降级兜底——这是防无限循环的「保险丝」。

用具体数字算一遍最坏路径:原始调用 1 次 + 重试 2 次 + 换备用工具 1 次 = 最多 4 次工具尝试,之后必落降级兜底。配合「单工具超时 0.5s + 总预算 3s」,任何请求都会在有限时间内拿到一个确定的出口——要么真结果,要么带不确定标注的估算,绝不会卡死或无限烧钱

防雪崩的关键:除了重试上限与指数退避,生产中还应对「计算工具」加熔断器(circuit breaker)——当其错误率在短窗口内飙升(如超过 50%)时,直接短路跳过重试与换工具、立即降级,避免一个抖动的下游把整个 Agent 的尾延迟拖垮。

思考题 3:反思的边界

反思循环能纠正「行动出错」,但也可能让模型陷入「自我怀疑—反复改写—永不收敛」。结合本文 2.1 节的 ReAct 图,谈谈你如何用工程手段约束反思的次数与触发条件,让反思「只在该反思时反思」,而不是每轮都质疑自己?

展开参考答案(含反思触发门控判定图 + 对比表)

结论:反思必须由「客观失败信号」触发、而非模型主观情绪,且要受一个独立的反思计数器约束——只有当观测确实异常(报错 / 校验不通过 / 重复打转)时才进入反思边,且全任务的反思次数有硬上限,超限即停止并交人工,从而把反思关进「该反思才反思、最多反思 N 次」的笼子里。

「客观信号触发」vs「主观情绪触发」对比:

维度❌ 主观情绪触发(每轮自问「我对吗」)✅ 客观信号触发(命中失败信号才反思)
触发条件模型每轮主观判断「要不要再想想」工具报错 / 校验不通过 / 检测到重复动作
收敛性易陷入自我怀疑—反复改写—不收敛正常轮次直接推进,反思只发生在确有问题时
可观测性难定位为何反复改写每次反思都对应一条具体失败信号,可追溯
成本每轮多一次「元推理」调用,token 翻倍仅异常时多花一次反思,成本可控

工程手段清单

  1. 触发门控:反思边只在观测命中客观失败信号时打开——工具返回 ERROR、结果未通过断言/校验、或检测到「连续两轮动作与参数完全相同」(打转)。正常观测直接进下一轮,不给模型「无端自我怀疑」的机会。
  2. 独立反思计数器:与总步数上限分开,单独限制反思次数(如最多 3 次)。每触发一次反思 +1,耗尽即停止反思、返回当前最优解或转人工——对应图中 STOP 出口。
  3. 反思要能读到失败:呼应 Gotchas——反思有效的前提是 history 里真的留有那条 ERROR 观测;若上下文被裁剪丢掉了失败信息,反思边形同虚设。
  4. 打转检测:维护最近若干轮的 (action, input) 指纹,一旦出现重复就强制升级策略或退出,防止「换汤不换药」的伪反思空转。

延伸阅读

1. 核心 Paper

  • ReAct: Synergizing Reasoning and Acting in Language Models(Yao et al., 2022)— 本篇基石必读,确立「推理与行动交织循环」范式。
  • Reflexion: Language Agents with Verbal Reinforcement Learning(Shinn et al., 2023)— 把「反思」形式化为可迭代的语言强化信号。
  • Chain-of-Thought Prompting Elicits Reasoning in Large Language Models(Wei et al., 2022)— 多步推理的思想源头,理解「为什么要让模型把推理写出来」。

2. 相关高 Star 仓库与源码必读路径

  • langchain-ai/langgraph2026 生产 Agent 首选编排;看其 Graph 抽象如何把 Agentic 自主性约束进可控的节点/边结构(DAG vs Agentic 的工程折中)。
  • langchain-ai/langchain — 入口看 agents/tools/,理解 tool calling 与 ReAct executor 的标准实现(新项目更推荐 LangGraph 而非 legacy AgentExecutor)。
  • microsoft/autogen — 多 Agent 协作与对话式编排的参考实现。

3. 优质博客 / 视频

  • Anthropic 工程博客「Building Effective Agents」— 业界对「何时用 Workflow、何时用 Agent」的权威实践指南。
  • Lilian Weng「LLM Powered Autonomous Agents」— 系统梳理 Planning / Memory / Tool Use 三大模块的经典长文。

下一篇L6.3 平台治理与模式:把单个 Agent 放大到平台尺度,讲清多 Agent / 多模型服务的治理范式、权限边界与可观测性模式。