
前言
阅读 Agent 相关内容时,经常会遇到两个概念:PlanAct 和 ReAct。
一开始看这两个概念,很容易觉得它们只是 Agent 执行过程中的两种写法。但如果把它们放到真实业务系统中,例如客服工单、订单售后、会员补发、退款审批等场景,就会发现它们背后其实代表了两种不同的 Agent 设计思想。
简单来说:
PlanAct 解决的是:任务流程如何稳定执行。
ReAct 解决的是:未知问题如何动态调查。
在实际工程中,如果只使用 PlanAct,系统虽然稳定,但遇到异常时容易僵硬;如果只使用 ReAct,系统虽然灵活,但容易不可控、成本高、路径不稳定。
因此,在企业级 Agent 应用中,更推荐的做法是:
外层使用 PlanAct 控制主流程,内层使用 ReAct 处理异常分支。
一、为什么需要 PlanAct + ReAct?
在传统客服系统中,大多数问题都是通过固定流程处理的。
例如用户提交工单:
我昨天买了会员,但是今天账号还是普通用户,请帮我处理。
一个成熟客服系统通常不会直接让人工客服自由发挥,而是会有一套 SOP:
1. 查询用户订单
2. 查询支付状态
3. 查询会员状态
4. 判断是否满足补发条件
5. 补发会员或转人工
6. 回复用户
7. 关闭工单
这就是典型的流程驱动。
但是在真实业务中,问题往往没有这么简单。
例如可能出现:
订单不存在,但用户说已经付款
支付成功,但会员系统没有发放记录
用户用 A 邮箱付款,却登录了 B 账号
第三方支付回调失败
用户之前已经提交过相同工单
订单被风控系统拦截
这些异常情况很难完全用固定流程覆盖。
因此,我们需要两类能力:
|-----------------|-------------------|
| 能力 | 解决的问题 |
| PlanAct | 标准流程如何稳定、可控地执行 |
| ReAct | 异常问题如何动态调查、逐步推理 |
| PlanAct + ReAct | 标准问题自动处理,复杂问题智能调查 |
这也是本文讨论 PlanAct + ReAct 的核心原因。
二、PlanAct 是什么?
PlanAct 可以拆成两个部分:
Plan + Act
也就是:
先计划,再执行
它的核心思想是:
在任务开始前,先生成或加载一个明确的执行计划,然后按照计划一步步执行。
PlanAct 的执行过程可以理解为:

在客服系统中,PlanAct 通常不是让模型自由生成计划,而是加载业务侧预先定义好的 SOP。
例如"会员未到账"工单,可以定义如下 SOP:
1. 确认用户身份
2. 查询最近 7 天会员订单
3. 判断订单是否存在
4. 查询支付状态
5. 查询会员权益状态
6. 判断是否满足补发条件
7. 执行补发或转人工
8. 生成客服回复
9. 关闭工单
这就是一个典型的 PlanAct 过程。
2.1 PlanAct 的本质
PlanAct 的本质是:
流程控制权在外部系统,而不是完全交给模型。
模型可以参与其中,例如:
识别工单类型
生成客服回复
总结工单原因
判断用户情绪
提取关键信息
但整体流程由工作流或 SOP 控制。
如果用 Dify 来理解,Dify 工作流天然就是一种 PlanAct 思想:
开始节点
↓
LLM 分类节点
↓
HTTP 查询节点
↓
条件分支节点
↓
LLM 回复节点
↓
结束节点
这里的工作流就是 Plan,节点执行就是 Act。
三、ReAct 是什么?
ReAct 是 Reasoning + Acting 的组合。
它的核心流程是:
Reason → Act → Observe → Reason → Act → Observe
中文可以理解为:
推理 → 行动 → 观察 → 再推理 → 再行动 → 再观察
ReAct 的执行过程如下:

ReAct 和 PlanAct 最大的区别在于:
ReAct 不要求一开始就有完整计划,而是让模型根据当前观察结果动态决定下一步。
例如用户提交工单:
我付款了,但是会员没到账。
ReAct Agent 可能会这样处理:
推理:用户说付款了但没到账,先查询订单。
行动:调用订单查询接口。
观察:发现订单支付成功。
推理:订单支付成功但会员没到账,继续查询会员状态。
行动:调用会员状态接口。
观察:用户仍然是普通用户。
推理:可能是权益发放失败,查询发放日志。
行动:调用权益日志接口。
观察:权益发放失败,原因是回调超时。
推理:符合补发条件,可以补发会员。
行动:调用会员补发接口。
观察:补发成功。
推理:问题已解决,回复用户。
这里每一步都依赖上一步的观察结果。
3.1 ReAct 的本质
ReAct 的本质是:
模型根据当前上下文动态决定下一步行动。
这意味着 ReAct 更像一个会调查问题的智能体,而不是一个固定流程执行器。
它特别适合:
问题原因未知
输入信息不完整
需要查询多个系统
每一步结果会影响下一步
异常情况较多
无法提前枚举所有路径
例如:
复杂投诉处理
支付异常排查
账号异常申诉
订单状态不一致
物流多节点异常
系统故障诊断
四、PlanAct 和 ReAct 的原理区别
PlanAct 和 ReAct 表面上都是 Agent 执行模式,但它们背后的控制逻辑完全不同。
4.1 控制权不同
PlanAct 的控制权在流程系统:
流程决定下一步做什么。
ReAct 的控制权在模型:
模型根据观察结果决定下一步做什么。
可以用下面这张图理解:

4.2 上下文形态不同
PlanAct 的上下文更像一个状态机。
例如:
{
"ticket_id": "T1001",
"user_id": "U1001",
"ticket_type": "membership_not_received",
"current_step": "query_payment",
"order_status": "success",
"membership_status": "normal",
"solution": "grant_membership"
}
ReAct 的上下文更像一个行动观察日志。
例如:
{
"goal": "调查会员未到账原因",
"actions": [
{
"step": 1,
"action": "query_order",
"observation": "订单存在,支付成功"
},
{
"step": 2,
"action": "query_membership",
"observation": "用户仍为普通用户"
},
{
"step": 3,
"action": "query_grant_log",
"observation": "权益发放失败,原因是回调超时"
}
],
"current_thought": "支付成功但权益发放失败,可以补发会员"
}
简单来说:
|-----------------|----------------|
| 模式 | 上下文形态 |
| PlanAct | 结构化状态 |
| ReAct | 行动观察日志 |
| PlanAct + ReAct | 主流程状态 + 异常调查日志 |
五、PlanAct、ReAct、PlanAct + ReAct 对比
5.1 核心对比表
|--------------|------------------------|-------------------|-------------------------|
| 对比维度 | PlanAct | ReAct | PlanAct + ReAct |
| 核心思想 | 先计划,再执行 | 边推理,边行动,边观察 | 主流程计划执行,异常分支动态调查 |
| 控制权 | 流程 / SOP | 模型 | 流程为主,模型为辅 |
| 执行路径 | 相对固定 | 动态变化 | 主路径固定,异常路径动态 |
| 上下文形态 | 结构化状态 | 行动观察日志 | 状态 + 调查日志 |
| 优点 | 稳定、可控、可审计 | 灵活、适应复杂问题 | 兼顾稳定性和灵活性 |
| 缺点 | 遇到异常容易僵硬 | 容易绕路、成本高、不可控 | 架构复杂度更高 |
| 适合场景 | 标准业务流程 | 异常排查、复杂问题 | 企业级 Agent 系统 |
| 工程实现 | Dify 工作流、LangGraph 流程图 | Agent Loop、工具调用循环 | 工作流 + Agent 子流程 |
| 风险控制 | 容易做 | 较难做 | 可在主流程中做边界控制 |
5.2 适用场景对比表
|--------------|-----------------|-------------------|
| 业务场景 | 推荐模式 | 原因 |
| 会员未到账标准处理 | PlanAct | 有明确 SOP |
| 退款申请 | PlanAct | 规则清晰,需要审计 |
| 发票申请 | PlanAct | 流程稳定 |
| 物流查询 | PlanAct | 接口查询路径固定 |
| 支付成功但订单不存在 | ReAct | 需要动态排查支付流水、回调日志 |
| 用户多账号冲突 | ReAct | 需要根据观察结果不断查询 |
| 复杂投诉处理 | ReAct | 问题路径不确定 |
| 客服自动化系统 | PlanAct + ReAct | 标准问题自动处理,异常问题动态调查 |
| 企业内部审批 Agent | PlanAct + ReAct | 主流程固定,但异常材料需要分析 |
六、客服工单场景设计
下面以"会员未到账"工单为例,设计一个 PlanAct + ReAct 混合系统。
6.1 用户工单
我昨天买了会员,但是今天账号还是普通用户,请帮我处理。
6.2 可用工具
classify_ticket(content) 识别工单类型
query_order(user_id) 查询用户订单
query_payment(order_id) 查询支付状态
query_membership(user_id) 查询会员状态
grant_membership(user_id) 补发会员权益
query_grant_log(order_id) 查询权益发放日志
query_callback_log(order_id) 查询支付回调日志
query_user_by_email(email) 根据邮箱查询用户
query_history_ticket(user_id) 查询历史工单
escalate_to_human(ticket_id) 转人工
reply_user(ticket_id, content) 回复用户
6.3 整体设计思想
主流程使用 PlanAct:
识别工单类型
查询订单
查询支付状态
查询会员状态
判断是否补发
生成回复
关闭工单
异常分支使用 ReAct:
订单不存在
支付状态未知
订单和用户不匹配
会员状态异常
权益发放日志异常
工具调用失败
模型判断不确定
整体流程如下:

七、PlanAct 实现:标准流程处理
PlanAct 适合处理标准工单。
例如:
订单存在
支付成功
会员状态仍为普通用户
这种情况可以直接补发会员。
7.1 PlanAct 流程图

7.2 PlanAct 伪代码
def planact_customer_service(ticket):
context = {
"ticket": ticket,
"state": {},
"result": None
}
# Step 1:识别工单类型
ticket_type = classify_ticket(ticket["content"])
context["state"]["ticket_type"] = ticket_type
if ticket_type != "membership_not_received":
escalate_to_human(ticket["ticket_id"])
return context
# Step 2:查询订单
order = query_order(ticket["user_id"])
context["state"]["order"] = order
if order is None:
context["state"]["need_react"] = True
context["state"]["react_reason"] = "未查询到会员订单"
return context
# Step 3:查询支付状态
payment = query_payment(order["order_id"])
context["state"]["payment"] = payment
# Step 4:查询会员状态
membership = query_membership(ticket["user_id"])
context["state"]["membership"] = membership
# Step 5:标准规则判断
if payment["status"] == "success" and membership["status"] == "normal":
grant_result = grant_membership(ticket["user_id"])
context["state"]["grant_result"] = grant_result
reply = generate_reply({
"type": "membership_granted",
"order": order,
"payment": payment,
"membership": membership,
"grant_result": grant_result
})
reply_user(ticket["ticket_id"], reply)
context["result"] = reply
return context
if membership["status"] == "vip":
reply = generate_reply({
"type": "already_active",
"membership": membership
})
reply_user(ticket["ticket_id"], reply)
context["result"] = reply
return context
if payment["status"] != "success":
reply = generate_reply({
"type": "payment_failed",
"payment": payment
})
reply_user(ticket["ticket_id"], reply)
context["result"] = reply
return context
# 标准规则无法覆盖,交给 ReAct
context["state"]["need_react"] = True
context["state"]["react_reason"] = "标准规则无法判断"
return context
八、ReAct 实现:异常问题调查
ReAct 适合处理标准流程无法覆盖的问题。
例如:
订单查不到,但用户提供了支付截图
支付成功,但订单系统没有记录
权益发放失败,但失败原因未知
订单 user_id 为空,只有 email
用户存在多个账号
8.1 ReAct 异常调查流程图

8.2 ReAct Prompt 示例
你是一个客服工单异常调查 Agent。
你的目标:
调查会员未到账的真实原因,并给出下一步处理建议。
你可以使用以下工具:
1. query_order(user_id):查询订单
2. query_payment(order_id):查询支付状态
3. query_membership(user_id):查询会员状态
4. query_grant_log(order_id):查询权益发放日志
5. query_callback_log(order_id):查询支付回调日志
6. query_user_by_email(email):根据邮箱查询用户
7. query_history_ticket(user_id):查询历史工单
8. final_answer:输出最终调查结果
9. need_human:转人工
当前工单:
{{ticket}}
已知状态:
{{known_state}}
历史行动与观察:
{{observations}}
请根据当前信息判断下一步动作。
要求:
1. 只输出 JSON
2. 不要输出无关解释
3. 如果信息不足,选择一个最有价值的工具继续查询
4. 如果已经能够判断原因,输出 final_answer
5. 如果风险较高或无法判断,输出 need_human
输出格式:
{
"thought": "当前推理",
"action": "工具名称",
"action_input": {},
"reason": "选择该动作的原因"
}
8.3 ReAct 伪代码
def react_investigate(ticket, parent_context):
react_context = {
"goal": "调查会员未到账异常原因",
"ticket": ticket,
"known_state": parent_context["state"],
"actions": [],
"observations": []
}
max_steps = 5
for step in range(max_steps):
prompt = build_react_prompt(react_context)
decision = llm(prompt)
action = decision["action"]
action_input = decision.get("action_input", {})
react_context["actions"].append({
"step": step,
"thought": decision.get("thought"),
"action": action,
"input": action_input,
"reason": decision.get("reason")
})
if action == "final_answer":
return {
"status": "resolved",
"reason": decision.get("reason"),
"suggestion": decision.get("suggestion"),
"reply": decision.get("reply"),
"context": react_context
}
if action == "need_human":
return {
"status": "need_human",
"reason": decision.get("reason"),
"context": react_context
}
observation = call_tool(action, action_input)
react_context["observations"].append({
"step": step,
"action": action,
"observation": observation
})
return {
"status": "need_human",
"reason": "ReAct 调查超过最大步数",
"context": react_context
}
九、PlanAct + ReAct 混合实现
在实际项目中,推荐采用混合模式。
整体思想是:
先走标准流程。
标准流程能解决,直接解决。
标准流程不能解决,进入 ReAct 异常调查。
ReAct 能解决,回到主流程处理。
ReAct 不能解决,转人工。
9.1 混合模式流程图

9.2 混合模式伪代码
def hybrid_customer_service_agent(ticket):
context = {
"ticket": ticket,
"state": {},
"mode": "planact"
}
# 1. 先执行 PlanAct 主流程
plan_result = planact_customer_service(ticket)
context["state"].update(plan_result["state"])
# 2. 如果 PlanAct 已经解决,直接返回
if plan_result.get("result"):
context["result"] = plan_result["result"]
return context
# 3. 如果 PlanAct 无法解决,进入 ReAct
if context["state"].get("need_react"):
context["mode"] = "react"
investigation = react_investigate(ticket, context)
context["state"]["investigation"] = investigation
# 4. ReAct 得出可解决结论
if investigation["status"] == "resolved":
if is_low_risk(investigation):
execute_result = execute_suggestion(investigation)
context["state"]["execute_result"] = execute_result
reply = investigation.get("reply") or generate_reply({
"type": "react_resolved",
"investigation": investigation,
"execute_result": execute_result
})
reply_user(ticket["ticket_id"], reply)
context["result"] = reply
return context
# 高风险动作需要人工确认
escalate_to_human(ticket["ticket_id"])
context["state"]["escalate_reason"] = "ReAct 建议涉及高风险动作"
return context
# 5. ReAct 无法解决,转人工
if investigation["status"] == "need_human":
escalate_to_human(ticket["ticket_id"])
context["state"]["escalate_reason"] = investigation["reason"]
return context
# 6. 兜底转人工
escalate_to_human(ticket["ticket_id"])
context["state"]["escalate_reason"] = "未知异常"
return context
十、Dify 中如何落地 PlanAct + ReAct?
如果使用 Dify,可以把系统拆成两个部分:
Dify 工作流主干:PlanAct
Agent 节点 / 子流程:ReAct
10.1 Dify 主流程设计

10.2 Dify 变量设计
建议在 Dify 中维护以下变量:
|----------------------|--------------|
| 变量名 | 含义 |
| ticket_id | 工单 ID |
| user_id | 用户 ID |
| ticket_content | 工单内容 |
| ticket_type | 工单类型 |
| order_result | 订单查询结果 |
| payment_result | 支付查询结果 |
| membership_result | 会员状态 |
| planact_status | 主流程执行状态 |
| need_react | 是否进入 ReAct |
| react_reason | 进入 ReAct 的原因 |
| investigation_result | ReAct 调查结果 |
| final_reply | 最终客服回复 |
10.3 ReAct 节点输出格式
ReAct 节点建议输出结构化 JSON:
{
"status": "resolved",
"reason": "支付成功但权益发放回调失败",
"suggestion": {
"action": "grant_membership",
"risk_level": "low",
"params": {
"user_id": "U1001",
"product": "monthly_vip"
}
},
"reply": "您好,已为您核实,您的会员订单支付成功,但权益发放出现延迟。我们已为您补发会员,请刷新后查看。"
}
如果无法解决,则输出:
{
"status": "need_human",
"reason": "用户存在多个账号,无法确认订单归属,需要人工核验"
}
十一、上下文工程设计
PlanAct + ReAct 的关键不是简单调用模型,而是上下文如何组织。
11.1 PlanAct 上下文
PlanAct 上下文强调结构化状态:
{
"goal": "处理会员未到账工单",
"current_step": "query_membership",
"ticket": {
"ticket_id": "T1001",
"user_id": "U1001",
"content": "我昨天买了会员,但是今天账号还是普通用户"
},
"state": {
"ticket_type": "membership_not_received",
"order_exists": true,
"payment_status": "success",
"membership_status": "normal"
}
}
它关心的是:
当前流程走到哪一步?
每一步结果是什么?
是否满足分支条件?
下一步应该进入哪个节点?
11.2 ReAct 上下文
ReAct 上下文强调历史行动和观察:
{
"goal": "调查会员未到账异常原因",
"known_state": {
"order_exists": false,
"user_id": "U1001"
},
"actions": [
{
"step": 1,
"thought": "订单不存在,但用户声称已付款,应查询支付流水",
"action": "query_payment_log",
"observation": "存在一笔成功支付记录"
},
{
"step": 2,
"thought": "支付存在但订单不存在,应查询支付回调日志",
"action": "query_callback_log",
"observation": "回调失败,错误码 TIMEOUT"
}
]
}
它关心的是:
已经查过什么?
观察到了什么?
当前推理是什么?
下一步最有价值的动作是什么?
是否已经可以得出结论?
11.3 混合上下文
PlanAct + ReAct 最推荐的上下文结构是:
{
"ticket": {},
"planact_state": {},
"react_investigation": {},
"final_decision": {}
}
也就是:
主流程状态和异常调查日志分开存储。
这样做的好处是:
方便调试
方便审计
方便回放
方便优化 prompt
方便人工接管
十二、工程实践注意点
12.1 ReAct 必须限制最大步数
ReAct 不能无限循环。
建议设置:
max_steps = 3 # 简单异常
max_steps = 5 # 中等复杂异常
max_steps = 8 # 复杂调查,不建议更高
超过最大步数后,直接转人工。
12.2 工具权限必须分级
不要把所有工具都开放给模型。
建议分成三类:
|--------------|-----------------|-----------------|
| 工具类型 | 示例 | 是否可自动执行 |
| 只读工具 | 查询订单、查询支付、查询会员 | 可以 |
| 低风险写工具 | 写备注、生成回复、关闭普通工单 | 可以 |
| 高风险写工具 | 退款、赔付、补偿、账号解封 | 需要规则校验或人工确认 |
12.3 高风险动作要有规则兜底
例如补发会员可以设置规则:
订单必须存在
支付必须成功
当前会员状态必须未开通
最近 30 天没有重复补发
补发金额或权益等级不能超过订单商品
如果不满足规则,则不能让模型直接执行。
12.4 输出必须结构化
ReAct 的输出不要只给自然语言。
推荐统一 JSON:
{
"status": "resolved",
"reason": "支付成功但权益未发放",
"suggestion": {
"action": "grant_membership",
"risk_level": "low"
},
"reply": "您好,已为您补发会员。"
}
结构化输出可以让主流程继续判断,而不是依赖人工读模型回答。
12.5 必须保留人工兜底
Agent 不是为了替代所有人工,而是优先处理高频、低风险、规则明确的问题。
以下情况建议转人工:
用户身份不一致
订单归属不明确
支付状态异常
涉及退款、赔付、投诉升级
模型置信度不足
工具调用失败
超过最大调查步数
十三、从 Dify 视角重新理解 PlanAct + ReAct
如果你正在使用 Dify + 工作流 + 模型,可以这样理解:
|-----------------|---------------------|
| Dify 组件 | 对应 Agent 设计 |
| 工作流节点 | PlanAct 的步骤 |
| 条件分支 | PlanAct 的流程控制 |
| HTTP 请求节点 | 工具调用 |
| LLM 节点 | 局部推理或文本生成 |
| 变量 | 上下文状态 |
| Agent 节点 | ReAct 动态调查 |
| 知识库检索 | 上下文补充 |
| 人工转接 | 安全兜底 |
所以,Dify 工作流天然偏 PlanAct。
如果想让它具备更强的 Agent 能力,可以在关键节点引入 ReAct:
标准路径:Dify 工作流稳定执行
异常路径:Agent 节点动态调查
高风险路径:转人工确认
这是一种比较务实的企业级 Agent 落地方式。
十四、总结
PlanAct 和 ReAct 不是谁替代谁的关系,而是互补关系。
PlanAct 的价值在于:
稳定
可控
可审计
适合标准业务流程
ReAct 的价值在于:
灵活
动态
适合异常调查
适合信息不完整的问题
在客服工单场景中,最推荐的设计是:
PlanAct 做主流程
ReAct 做异常调查
人工做最终兜底
最终系统可以形成这样的能力:
标准问题自动处理
异常问题智能调查
高风险问题人工确认
完整链路可追踪
一句话总结:
PlanAct 让 Agent 按流程稳定做事,ReAct 让 Agent 在遇到未知问题时会调查。二者结合,才更接近真实可用的企业级 Agent。