先看一个售后测试场景。
2026 年初,某跨境电商测试一款基于 GPT-4o 的售后 Agent。用户确认退货地址后突然改口:“等等,我刚才填的邮编好像错了,能改一下吗?”
Agent 先调用“修改地址”,随后调用“核对地址”。问题出在上下文里同时留着新旧两个邮编。模型把新邮编判断成“与之前的信息不匹配”,再次要求用户确认。用户回复“对,就是用新的”,流程又回到修改和核对。
3 分钟内,两个工具来回调用了 24 次,直到该单的 Token 额度耗尽,并触发 API Rate Limit。
这个循环暴露的缺口很具体:系统没有独立记录当前有效邮编、已经重试多少次,以及何时必须退出。所有判断都压在模型的一次次生成上。
Prompt 里的“最多两次”不算上限
开发时最常见的补丁,是在 System Prompt 里加一句:如果用户修改地址,请先核对;如果核对失败,不要重复调用修改接口超过 2 次。
这句话只是一段等待模型理解的文本。它没有真正的计数器,也不会在第三次调用前自动拦截。
随着历史对话、工具返回的 JSON 和报错信息不断累积,模型可能在某一轮重新解释规则。继续往 Prompt 里追加例外,只会让输入变长,仍然没有程序级的出口。
此前关于反思闭环的讨论提出过一个实用做法:把生成、评估和修正拆开。这个思路能减少一次生成包办所有判断的问题,但只要修正节点可以无限回流,死循环依然存在。
退出条件需要写进代码。
把跳转权从模型手里拿出来
有限状态机(Finite State Machine,FSM)放进 Agent 工作流后,主要管理三类东西:
- 状态(State):保存对话、当前业务数据、错误信息、重试次数和审批结果。
- 节点(Node):完成一个明确动作,例如生成代码、执行编译、修改地址或等待人工审核。
- 条件边(Conditional Edge):检查状态,再决定流程进入哪个节点。
模型可以在修复节点里决定代码怎么改,但它不能决定自己是否还有第四次修复机会。后者交给一行明确的 Python 判断,例如 compile_retry_count >= 3。
这一区分在带有副作用的流程中尤其重要。Agent 一旦可以调用工具、使用权限、发起支付或写回系统,节点跳转就不只是对话选择。地址可能被重复修改,支付也可能被重复发起。状态机负责守住流程边界,重要操作则可以停在人工审核节点,等待 Human-in-the-loop 确认。
用 LangGraph 给循环装一个出口
下面沿用从 PRD 到测试脚本的场景:Agent 先生成 API 测试脚本,再交给编译器执行。编译失败时允许模型修复,但最多重试 3 次。
先定义全局状态:
from typing import TypedDict, List, Annotated
import operator
class AgentState(TypedDict):
messages: Annotated[List[dict], operator.add]
code_snippet: str
error_message: str
compile_retry_count: int
is_approved: bool
compile_retry_count 要在任务开始时初始化为 0。它是流程的硬计数器,不需要模型从对话里猜测已经重试了几次。
编译结束后,由路由函数决定下一步:
def route_after_compile(state: AgentState) -> str:
if not state['error_message']:
return 'human_approval'
if state['compile_retry_count'] >= 3:
return 'fallback_error'
return 'fix_code'
修复节点必须同步增加计数。漏掉这一步,状态图看起来有上限,运行时仍可能一直循环。
def fix_code_fn(state: AgentState) -> dict:
fixed_code = ... # 调用模型,根据 error_message 修复代码
return {
'code_snippet': fixed_code,
'compile_retry_count': state['compile_retry_count'] + 1,
}
随后把节点和条件边接入 LangGraph:
from langgraph.graph import StateGraph, END
workflow = StateGraph(AgentState)
workflow.add_node('generate_code', generate_code_fn)
workflow.add_node('compile_code', compile_code_fn)
workflow.add_node('fix_code', fix_code_fn)
workflow.add_node('fallback_error', fallback_error_fn)
workflow.add_node('human_approval', human_approval_fn)
workflow.set_entry_point('generate_code')
workflow.add_edge('generate_code', 'compile_code')
workflow.add_conditional_edges(
'compile_code',
route_after_compile,
{
'human_approval': 'human_approval',
'fix_code': 'fix_code',
'fallback_error': 'fallback_error',
},
)
workflow.add_edge('fix_code', 'compile_code')
workflow.add_edge('fallback_error', END)
workflow.add_edge('human_approval', END)
app = workflow.compile()
fix_code -> compile_code 依然是一个循环,但它已经有了明确上限。编译成功,流程进入人工审核;连续修复 3 次仍然失败,流程进入 fallback_error,保留错误信息并停止继续调用模型。
这里把 human_approval 视为完成审批后结束的节点。如果业务还要区分通过与驳回,可以再为它增加一条条件边。
状态里要放能决定去向的数据
很多 Agent 只保存 messages,真正影响流程的数据仍然埋在对话里。这样一来,每次路由都要让模型重新阅读历史并做判断,旧值和新值很容易混在一起。
售后地址场景可以单独保存 confirmed_postcode。用户确认新邮编后,程序直接更新这个字段;旧邮编可以留在消息历史中,但后续工具调用只读取已经确认的结构化值。
编译修复场景至少要保留这些字段:
error_message:判断编译是否成功,也为下一轮修复提供输入。compile_retry_count:控制循环上限。code_snippet:确保编译器拿到的是当前版本。is_approved:控制需要人工确认的节点能否继续执行。
排查故障时,日志也应记录当前节点、路由结果、重试次数和错误信息。看到 compile_code -> fix_code -> compile_code 的状态快照,通常比先翻完整 Prompt 更容易找到问题。
状态机能管住什么,管不住什么
状态机能限制循环次数,让执行路径可追踪,也能在付款、退款审核和重要数据修改前挂起流程。它不会让每次模型调用消耗完全相同的 Token,也不能保证模型一定修好代码。
工具接口本身仍要处理幂等和权限校验。状态机可以阻止流程无限回转,但如果“发起支付”接口允许同一请求重复生效,风险仍然存在。
更稳妥的分工是:模型负责理解用户、生成方案和修复内容;代码负责重试次数、审批条件、权限边界和终止状态。这样做没有消除模型的随机性,只是把它限制在一条能退出、能追踪的路径里。