一、引言:当"一步到位"失效时
过去一年,我一直在做一件事:把行业里那些重复、繁琐、依赖经验判断的业务流程,交给大语言模型(LLM)智能体去执行。最初的想法很简单------写一个精心设计的Prompt,把任务描述清楚,让模型直接给出结果。
结果并不理想。面对"核对本月供应商账单与采购订单差异并生成调整建议"这类真实任务,模型要么在一个漫长的单次推理中逐渐"迷失",要么在第一步就选错了工具,要么在中间某一步失败后整条链路崩溃。问题的根源在于:复杂任务的解决路径不是线性的,而模型的一次性生成天然是线性的。
这让我转向了智能体领域两个经典且互补的框架------ReAct (Reasoning + Acting)与 Plan-and-Execute。在经历多轮重构之后,我沉淀出一套"行业Skill"的构建方法论。这篇文章是这段实践的完整记录,包含框架原理、结合策略、可复用的代码骨架,以及踩过的坑。
二、两个框架的原理与取舍
2.1 ReAct:让"想"与"做"交替进行
ReAct的核心思想是:让模型在每一步决策时先输出Thought(推理),再输出Action(行动),观察环境返回的Observation(观察),如此循环,直到任务完成。它把"推理过程"外化到对话上下文里,使得模型每一步都基于最新的真实反馈做决策,而不是闭门造车。
用代码来表达,一个最小化的ReAct循环如下:
python
def react_loop(task: str, tools: dict, max_steps: int = 10):
messages = [{"role": "user", "content": f"任务:{task}\n可用工具:{list(tools.keys())}"}]
for _ in range(max_steps):
resp = llm_chat(messages)
messages.append({"role": "assistant", "content": resp})
action = parse_action(resp) # 从响应中解析出 Action
if action.name == "Finish":
return action.argument
if action.name not in tools:
observation = f"工具 {action.name} 不存在"
else:
observation = tools[action.name](action.argument)
messages.append({"role": "user", "content": f"Observation:{observation}"})
raise TimeoutError("达到最大步数")
这个循环简单、透明、易于调试,是绝大多数Agent框架(如LangChain的AgentExecutor、ReAct Agent)的底层形态。它的优势在于动态性 ------模型可以根据中间结果随时调整下一步;劣势在于长链路效率低------每一步都消耗Token,步骤一多,上下文变长,模型反而更容易在历史里"迷路",而且没有全局视角,容易出现"局部最优、整体次优"。
2.2 Plan-and-Execute:先有蓝图,再动工
Plan-and-Execute则把流程切成两个角色:Planner (规划者)与 Executor(执行者)。Planner先对任务做整体拆解,产出一份带依赖关系的步骤清单;Executor按清单逐条执行,执行结果回填给Planner,Planner根据反馈修订剩余计划。
python
class PlanExecutor:
def __init__(self, planner: LLM, executor: LLM):
self.planner, self.executor = planner, executor
def run(self, task: str) -> str:
plan = self.planner.make_plan(task) # 生成结构化计划
results = {}
while plan.has_pending():
step = plan.next_step() # 取下一个可执行步骤
out = self.executor.execute(step, results) # 执行并引用前置结果
results[step.id] = out
plan = self.planner.replan(task, plan, results) # 必要时修订
return plan.final_answer(results)
它的好处是全局可控 :计划先行意味着任务被拆解为可验证的单元,每个步骤可以独立测试、独立重试、独立审计------这正是行业落地最看重的能力。代价是灵活性不足:真实世界里计划赶不上变化,步骤之间隐藏的依赖、执行中的意外,都需要额外机制兜底。
2.3 两者不是二选一,而是互补
我把两者的差异归纳为一张决策表:
| 维度 | ReAct | Plan-and-Execute |
|---|---|---|
| 推理粒度 | 逐行动 | 整任务规划 |
| 灵活性 | 高,随机应变 | 中,依赖修订机制 |
| 长任务稳定性 | 低,易漂移 | 高,蓝图约束 |
| Token成本 | 高 | 中 |
| 可观测性 | 过程日志 | 计划+执行日志 |
| 适用场景 | 探索性、工具选择类任务 | 流程固定、步骤可拆的任务 |
行业任务------对账、合规审查、客户工单处理、报告生成------绝大多数属于"流程相对固定但细节充满变化"的形态。它们既需要Plan-and-Execute的结构约束来保证不跑偏,又需要ReAct的即时反馈来处理数据层面的意外。最终方案是混合:用Plan-and-Execute做骨架,在每一个执行步骤内部,用ReAct循环做细粒度推理。
三、行业Skill的抽象设计
所谓"行业Skill",我把它定义为:面向特定业务场景、可复用的、封装了任务拆解与执行逻辑的能力单元。它像函数一样有输入输出契约,内部则可能是一整套Agent编排。
一个Skill的接口设计如下:
python
from pydantic import BaseModel, Field
from typing import Any, Callable
class SkillSpec(BaseModel):
name: str
description: str # 供Planner选Skill时使用
input_schema: dict # 入参契约,如 {"account_id": "int"}
output_schema: dict # 出参契约
executor: Callable[[dict], dict] # 执行入口
class AgentSkill:
"""行业Skill的统一包装"""
def __init__(self, spec: SkillSpec, plan_capable: bool = False):
self.spec = spec
self.plan_capable = plan_capable
def run(self, **kwargs) -> dict:
validate(kwargs, self.spec.input_schema) # 参数校验
result = self.spec.executor(kwargs)
validate(result, self.spec.output_schema) # 结果校验
return result
description字段至关重要------它是Planner判断"该调用哪个Skill"的依据;输入输出schema则让Skill可以像乐高积木一样被组合,同时为失败检测提供了契约基础。
四、实录一:用ReAct构建原子Skill
以"查询供应商应付账款余额"为例。这是一个原子操作,内部不需要复杂规划,但需要根据用户提供的模糊信息(供应商名称、简称、或编号)做判断。这正适合ReAct:
python
def make_balance_query_skill(db) -> AgentSkill:
def executor(args: dict) -> dict:
target = args["supplier"]
def lookup_supplier(name: str) -> str:
rows = db.query("SELECT id, name, code FROM supplier WHERE name LIKE ?", f"%{name}%")
return str(rows) if rows else "未找到匹配供应商"
def lookup_balance(supplier_id: int) -> str:
row = db.query_one(
"SELECT SUM(amount) AS bal FROM ap_balance WHERE supplier_id = ?",
supplier_id)
return f"应付余额:{row['bal']:.2f}元" if row else "无余额记录"
tools = {"lookup_supplier": lookup_supplier, "lookup_balance": lookup_balance}
return {"balance": react_loop(f"查询供应商[{target}]的应付余额", tools)}
...
ReAct在这里的价值是:模型不知道供应商编号,但可以自行选择"先按名称模糊查询、再按ID查余额"的工具链,甚至能处理"重名供应商需要用户确认"的边界。把这个能力封装进Skill后,上层完全不需要关心它的内部挣扎。
五、实录二:用Plan-and-Execute编排复合Skill
单点查询远不够,业务要的是"月度应付对账报告"。这个Skill内部需要拆解为:拉取供应商清单 → 逐家查询余额 → 与上月快照比对 → 生成差异分析 → 汇总成报告。步骤间有明确的数据依赖,这正是Planner的舞台:
python
def make_monthly_reconcile_skill(db, report_writer) -> AgentSkill:
def executor(args: dict) -> dict:
month = args["month"]
def plan_and_run(plan: list[dict], ctx: dict) -> dict:
for step in plan: # 顺序执行计划
fn = STEP_REGISTRY[step["op"]] # 从注册表取执行函数
step["result"] = fn(month=month, ctx=ctx, **step.get("params", {}))
ctx[step["id"]] = step["result"]
return ctx
# Planner:LLM根据任务与步骤注册表生成计划
plan = planner.generate(
task=f"生成{month}应付对账报告",
available_ops=list(STEP_REGISTRY.keys()),
)
ctx = plan_and_run(plan, {})
return {"report": report_writer.render(month, ctx)}
注册表模式是关键设计:Planner不直接写代码,而是从预定义的STEP_REGISTRY里挑选已实现、已验证的操作。这既限制了LLM的自由度防止幻觉性步骤,又保留了编排的灵活性:
python
STEP_REGISTRY = {
"list_suppliers": list_suppliers,
"query_balance": query_balance,
"diff_with_last": diff_with_last,
"risk_analysis": risk_analysis,
"render_report": render_report,
}
六、实录三:反馈驱动的执行修订(两框架的缝合点)
纯顺序执行在理想数据下没问题,但现实数据总有脏数据:某供应商上月余额为空、某笔调整单日期越界......此时Executor必须在步骤内用ReAct自救,自救失败则把异常回抛给Planner让其修订计划。我在两者之间加了一个"异常走廊":
python
class ReconcileExecutor:
def execute_step(self, step, ctx):
try:
return self.run_with_react(step, ctx) # 步骤内部:ReAct细粒度执行
except StepHardError as e:
repair = self.planner.repair_plan(e, step) # 步骤失败:Planner修订
if repair is None:
raise
return self.execute_step(repair, ctx) # 修订后重试
def run_with_react(self, step, ctx):
react_loop(
f"{step['op']}:{step['params']}",
tools=STEP_TOOLS[step["op"]], # 每个操作有自己的工具集
max_steps=5,
)
实际运行中,这个机制处理了相当比例的真实异常------例如"对账差异超阈值"这种数据异常,Planner会追加"人工复核"步骤而不是硬算;而"上游接口超时"这种环境异常,则直接标记该步骤为阻塞并降级输出。
七、工程化经验:让Skill真正能交付
框架选对了,决定成败的往往是工程细节。以下是四个月实战踩坑后的沉淀:
1. 提示词中的"工具契约"要显式化。 Planner和Executor共享一份统一的工具/操作定义,包含参数、返回值、错误语义。我用JSON Schema生成,避免模型从自由文本里猜测参数类型。
2. 失败要分级。 我把错误分成RecoverableError(重试可解)、StepHardError(需修订计划)、FatalError(终止并转人工)。分级让异常走廊的决策变成查表而不是另一个LLM调用,既省钱又稳定。
3. 全程可观测。 每个Thought/Action/Observation、每版计划、每次修订都写入结构化日志(JSON Lines),配有trace_id贯穿全链路。行业场景下,"为什么得出这个结论"和结论本身同等重要------审计要求它,调试需要它,用户信任也依赖它。
4. 成本与延迟要预设上限。 max_steps、每步Token预算、Planner重规划次数全部硬编码上限;能并行执行的步骤(如逐家供应商查余额)用并发池并行,实测报告类任务耗时下降约60%。
5. 用真实失败样例做回归。 我把历史上每个导致链路崩溃的案例固化为测试用例,每次修改Skill定义或提示词后全量跑一遍,确保"修好A不弄坏B"。这是这套体系从"demo"走向"可用"的临门一脚。
八、总结
回看这段实践,我的核心收获是:不要在"用哪个框架"上站队,而是让框架服务于任务的形状。ReAct提供动态纠错的能力,Plan-and-Execute提供全局可控的结构,Skill则是对两者的封装与沉淀------它把一次性的智能体编排,变成行业里可复用、可测试、可审计的工程资产。
当前这套体系已支撑起对账、合规初筛、报告生成三类业务,日均处理数千个任务,人工介入率从初版的90%下降到约15%。下一步,我计划引入记忆机制让Skill记住历史偏好,以及用离线评测集量化每次提示词改动的效果。
技术没有银弹,但把"任务拆解"从模型的隐式行为,变成显式、可编排、可观测的工程结构------这条路,已被证明是智能体走向行业生产环境的可行路径。