Appearance
Q78 · 如何设计 Agent 的完成判断与终止条件,避免过早结束?
顾客对耳机售后助手说:“请帮我为订单 B456 申请退款,并告诉我是否已受理。”助手查到政策后回复“已帮您办好”,但退款系统实际上超时了,根本没有返回申请编号。顾客看到一句“办好”会认为申请已受理;系统却没有能证明这件事的记录。反过来,如果助手一直等退款接口,接口迟迟没有确定结果,它也不能无限查询、无限重试。
设计完成判断时,先把用户的自然语言目标写成可由业务系统验证的验收条件,再把“成功完成”“仍在等待”“明确失败”“需要人工”和“达到上限”分别处理。模型负责理解问题与组织回答;能否宣告“已受理”,要由有权限的订单事实、现行政策和退款服务回执共同决定。LangGraph 官方文档将条件边用于按状态选择后续节点或结束图执行,也提供递归步数上限;这些运行机制需要应用自己定义业务上的“何为完成”。LangGraph Graph API、GRAPH_RECURSION_LIMIT
下文全是教学假设,不是任何商家的真实退款规则。假设评测日是 2026 年 9 月 25 日,现行政策 v3 规定“签收后 7 天内且未拆封,可申请自动退款”;B456 于 9 月 22 日签收,订单系统记录未拆封,当前登录用户经服务端核验有权申请这笔订单。本文的目标限定为:提交一笔退款申请,并由退款服务确认已受理,向用户告知申请编号。“钱已退回银行卡”是后续不同的目标,不能因为“申请已受理”就宣告“到账”。
术语与符号
| 术语或符号 | 零基础解释 | B456 中的具体含义 |
|---|---|---|
| Agent | 可围绕目标选择下一步并调用工具的程序;模型只是其中一部分 | 售后助手查单、查政策、提交退款申请 |
| 目标 / 验收条件 | 用户真正要达到的结果 / 判断结果是否达成的检查表 | 退款申请被服务端受理,有申请编号 |
| 状态(state) | 流程当前已知的结构化事实,不等于模型说的一段话 | 授权结果、政策版本、提交状态、回执编号 |
| 证据 | 可回到可信来源核验的事实或记录 | 订单归属、政策 v3、退款服务回执 |
| 工具 | Agent 调用的外部程序或服务 | 查订单、查政策、提交申请、查申请状态 |
| 副作用 | 调用会改变外部业务状态,而非仅查询 | 创建一笔退款申请 |
| 回执 | 业务服务确认操作结果的记录 | request_id=R789,状态为已受理 |
| 幂等键 | 同一逻辑操作在重试时沿用的唯一标识,帮助服务避免重复创建 | B456 本次申请固定用同一个 operation_key |
| 终态 | 本次运行停下来时写明是哪一种结果 | COMPLETED、WAITING、DECLINED 等 |
| 暂停 / 恢复 | 本次先停止并保存状态,等工具回调或人处理后从保存处继续 | 退款服务还在处理中,稍后用同一申请追踪号核实 |
| 业务次数上限 | 自己规定某操作最多尝试几次,防止空转 | 状态查询最多 3 次,超过后待核实或转人工 |
recursion_limit | LangGraph 一次执行允许的最大图步数;是框架保险丝 | 防止节点循环一直运行,不代表业务已完成 |
order_id | 订单号字段 | B456 |
policy_version | 本次判断所用政策版本 | 当前有效的 v3 |
authorized / eligible | 是否有权操作 / 是否满足政策条件 | 两者都须为 true 才能提交申请 |
operation_key | 本次逻辑申请的幂等键 | 提交重试与状态核对沿用同一值 |
request_id | 退款服务受理后给出的申请编号 | 示例为 R789 |
submit_state | 服务端可核验的申请状态 | accepted、pending、rejected 或 unknown |
check_count / MAX_CHECKS | 已查询状态次数 / 允许的上限 | 初始为 0 / 本例设为 3 |
完成闸门:三个证据缺一不可

图只表达完成闸门:有权限、符合政策、回执经核实,才能向用户说“申请已受理”。第三张卡下方的“待核实”表示回执缺失或状态不明,不能走绿色成功出口。图没有画“政策不符”“用户取消”“人工接管”等其他分支,下面逐一展开。图片里的“已受理”特指申请被受理,不代表退款已经到账。
先把用户目标拆为四个可检查条件:
- 身份与订单:当前登录用户经服务端授权,有权为 B456 申请退款;不能用模型从聊天里猜出的“这是我的订单”作授权证明。
- 政策与事实:订单系统返回 B456 于 9 月 22 日签收、未拆封;检索或政策服务给出在 9 月 25 日有效的
v3;确定其符合本例自动申请条件。 - 业务副作用:退款申请工具实际执行,且后端返回与本次
operation_key、B456 对得上的结果;不能把“模型已发出工具调用请求”当成“工具执行成功”。OpenAI 的函数调用流程明确分开模型提出调用、应用执行工具、返回工具结果与模型继续回答。 - 完成证明:权威退款服务确认
submit_state=accepted且给出request_id=R789;对外答复“B456 的退款申请已受理,编号 R789,到账时间以退款系统后续通知为准”。若只有pending或超时,就不能说“已受理”。
这四项可写成一个业务谓词(即返回真或假的检查):
text
is_complete(state) =
state.authorized == true
AND state.eligible == true
AND state.policy_version == "v3" # 此处 v3 是本例评测日的有效版本
AND state.submit_state == "accepted"
AND state.request_id 不为空
AND 回执确实对应 B456 与本次 operation_keystate 是上表已定义的结构化状态,AND 表示所有条件必须同时成立。真实系统不应把 "v3" 永久写死,而应由政策服务核对处理时点的有效版本;本例写 v3 是为了便于复述。is_complete 不检查“模型有没有说完成”,因为那不是可信的业务事实。它也不把“无错误日志”当成功:工具可能请求已发送但结果未知。
一次运行停下,可能有六种不同结果
停止执行和用户目标完成不是同义词。以下名字是本文建议的业务状态,不是某框架的固定枚举:
| 业务状态 | 何时进入 | 应告诉用户什么 | 后续怎样办 |
|---|---|---|---|
COMPLETED(已完成) | 授权、资格和权威回执都核实,拿到申请号 | “申请已受理,编号 R789”;不要说已到账 | 结束本次申请任务,另行跟踪到账 |
WAITING(待核实) | 申请可能已提交,但接口超时或返回 pending,尚无确定回执 | “正在核实受理状态”,给可追踪编号或查询途径;不承诺成功 | 保存状态,等待回调、定时查询或用户后续查看 |
DECLINED(不符合或无权) | 权限不通过、政策条件不符,或业务服务明确拒绝 | 说明可披露的原因和下一步;不泄露其他人的订单 | 终止该自动申请,可转人工咨询 |
NEEDS_HUMAN(需人工) | 政策冲突、特殊故障、工具结果矛盾,自动规则无法安全决定 | 明确“转人工核实”,不编造受理编号 | 连同已知证据交给有权限人员 |
LIMIT_REACHED(达到上限) | 业务查询次数、总步骤、费用或时限到达上限 | 说明尚未确认结果,避免让用户以为成功 | 保留 operation_key,转待核实或人工;不盲重试 |
CANCELLED(用户取消) | 用户在副作用发生前撤回请求,或系统受控取消 | 准确说明未提交;若请求可能已送达则说“取消结果待核实” | 核实外部实际状态,不能只停对话 |
这里 WAITING 和 NEEDS_HUMAN 可以是本次运行的停止点,但业务事项仍未关闭,后续需要恢复或由人处理。LIMIT_REACHED 是资源保护结果,不能伪装成任务成功;如果有未确认的写入操作,还必须保留足够信息核对副作用。DECLINED 也不是“系统没用”:它可能是对不符合政策请求的正确处理,只是用户提出的“申请被受理”目标未达成。
B456 的正常路径如何一步步走完
设用户已通过登录;operation_key 为本次逻辑申请生成一次,后续网络重试不能换键。第 1 步,授权服务确认此用户对 B456 有申请权限,状态 authorized=true。第 2 步,订单服务返回 9 月 22 日签收、未拆封,政策服务确认 9 月 25 日有效版本为 v3,规则检查得 eligible=true。第 3 步,程序用 B456 和 operation_key 调用“提交退款申请”;服务返回 submit_state=accepted、request_id=R789。第 4 步,程序核对回执的订单号与本次操作键,is_complete 为真,状态进 COMPLETED。最后模型只负责把已核实事实写成易读答复,不能改写状态。
若申请服务只返回 pending 和跟踪号,本次运行应停在 WAITING,告诉用户“已收到申请请求,受理状态尚待核实”,不能提前说“已受理”。若工具响应完全丢失,连是否收到请求都未知,应记录 submit_state=unknown,先凭同一 operation_key 向退款服务查状态;查不到明确结果时保持待核实或转人工。这里假设业务服务支持按操作键核对申请;若不支持,接入前应补这种核对能力或设计人工对账。重复提交副作用操作可能创建两笔申请。Stripe 的幂等请求文档说明了相同幂等键在网络重试中避免重复创建的原理;本例不是 Stripe API,需由自己的退款服务提供并测试同等保证。
决策顺序:先看证据,再看错误与预算
下面是语言无关的伪代码,用来说明决策,不是可复制运行的退款系统。load_order_and_policy 读取订单和评测时有效政策;authorize 检查用户对订单的权限;submit_once 用固定 operation_key 提交一次逻辑申请;query_by_key 查该申请的权威状态。四个函数的输入、返回和异常处理都要由业务服务实现,模型不能自行假造结果。MAX_CHECKS=3 是本教学例子的业务选择,不是框架默认值。
text
处理申请(user, order_id="B456", operation_key):
state = 读取已保存的状态或创建新状态
if 用户已请求取消:
if state.提交可能已经送达:
return WAITING("先核对申请是否已产生,再处理取消")
return CANCELLED("尚未提交")
if authorize(user, order_id) != true:
return DECLINED("无权操作该订单")
订单事实, 当前政策 = load_order_and_policy(order_id)
if 订单事实缺失 or 当前政策缺失:
return NEEDS_HUMAN("关键依据缺失,不能猜")
if 按当前政策判断资格(订单事实) != true:
return DECLINED("不满足自动申请条件")
if state.尚未尝试提交:
state.标记可能提交 = true # 先持久化,防进程崩溃后误以为没调用过
try:
回执 = submit_once(order_id, operation_key)
state.保存(回执)
except 网络超时:
state.submit_state = "unknown"
if 回执或后续查询显示明确拒绝:
return DECLINED("退款服务拒绝申请")
if is_complete(state):
return COMPLETED(state.request_id)
while state.check_count < MAX_CHECKS and 总时限未到:
state.check_count += 1
查询结果 = query_by_key(operation_key)
state.保存(查询结果)
if 查询结果明确拒绝: return DECLINED("退款服务拒绝申请")
if is_complete(state): return COMPLETED(state.request_id)
if 查询结果互相矛盾: return NEEDS_HUMAN("回执冲突")
if state.check_count >= MAX_CHECKS:
return LIMIT_REACHED("查询次数已用完;保留操作键,安排后续核对")
return WAITING("总时限已到但受理结果未明;保留操作键,稍后核对")伪代码中的“先持久化再提交”表示写前意图记录:即使程序在调用后崩溃,恢复时也知道“可能已经发出”,不会无条件再创建一笔。生产系统还需处理持久化与外部服务间的竞态、幂等键保留期限、超时与回调安全、并发请求、订单状态变化和退款金额核验;不要把这段教学伪代码当完整交易系统。query_by_key 的返回必须能区分 accepted、pending、rejected 和暂时查不到,不能把空响应当“尚未提交”就重试创建。LIMIT_REACHED 表示本次自动核对停止,仍须安排后续对账或人工处理,不能把用户的退款事项丢在这个状态。
这个顺序也解释了为什么上限检查不应比已获得的完成证据优先:第三次查询正好拿到 accepted 与编号,应判 COMPLETED;第三次仍只有 pending 才停在 WAITING。同样,“模型觉得答案已经够了”不能越过证据门,而“还可继续想一轮”不能越过业务次数上限。达到上限要把已取得的事实交给安全的退出分支。LangGraph Graph API 的条件路由与步数说明
两种相反的错误:过早结束与一直不结束
过早结束常见于把“模型输出了完整句子”当成成功信号。比如模型说“已提交”,但工具调用还没执行;工具返回 HTTP 成功,却只是“请求排队”;工具报超时,模型凭之前的计划补出“R789”;政策检索找到旧版 v2,模型依旧说 B456 符合当前规则。这些情况都不满足验收谓词。要看权威业务状态、证据版本和回执关联,而不是看答复流畅程度。OpenAI 的Agent 轨迹评估文档建议检查工具调用、交接和策略违规;Tracing 文档展示了调用参数、结果、状态和错误如何进入一次执行轨迹。
一直不结束则常见于没有“足够”与“停止”的明确定义。Agent 对相同 B456 连续查五次订单,却没有新信息;退款服务连续给 pending,模型继续要求“再查一次”;两个 Agent 互相转交;检索每次返回相同旧政策。可按任务设总时限、模型调用与工具调用预算、单工具重试次数、相同参数重复检测、无新证据次数,并让预算耗尽走 WAITING 或 NEEDS_HUMAN。不应让 Agent 靠自己的语气判断“已经努力够了”。
框架保险丝与业务上限需要同时存在。以 LangGraph 为例,recursion_limit 限制一次图执行的 super-step(图执行步);触限会抛 GraphRecursionError。这能阻止无界运行,但不会自动判断退款申请是否受理,也不等于“最多三次查状态”。check_count < MAX_CHECKS 是业务级限制;recursion_limit 是最后的执行保护。即使捕获了框架错误,也要保存当前 operation_key 和工具状态,并向用户报告“尚未确认”,而不是在异常处理里输出“已完成”。LangGraph 递归上限说明、GRAPH_RECURSION_LIMIT 错误文档
若需要用户补资料或人审核,可以暂停并保存状态,待输入到来后恢复。LangGraph 的Interrupts 文档提供暂停、保存与恢复机制;但恢复时仍要重新核对会变化的权限、政策和订单事实,不能把几天前的条件永久当真。对会产生副作用的节点还要考虑恢复后重执行,确保不会重复创建申请。LangGraph Graph API 的重执行与幂等性说明
用测试证明它会停、也不会停得太早
测试需要检查业务终态与真实工具状态,不只断言最终文字。固定当前政策为 v3、订单事实和退款工具的模拟返回,记录每个工具是否被调用以及调用参数。对每个案例至少断言:返回的状态、给用户的说法、request_id 是否来自权威回执、申请工具调用次数、同一逻辑申请是否复用 operation_key。关键样例如下:
| 测试输入与工具返回 | 应有终态 | 不能发生什么 |
|---|---|---|
B456 有权、符合 v3,提交返回 accepted/R789 | COMPLETED,告知“申请已受理,编号 R789” | 说“退款已到账”;重复提交 |
提交返回 pending,查询三次仍为 pending | LIMIT_REACHED,保留操作键并安排后续对账;对用户仍说“待核实” | 把 pending 写成“已受理”;第四次无界查询 |
| 提交超时,但服务端其实已创建 R789;按同一键查到受理 | COMPLETED,回执 R789 | 换新键再创建第二笔 |
| 提交超时,按键查不到确定结果 | WAITING 或转人工;明确结果未知 | 把“查不到”当确定失败再自动重提 |
| A123 于 9 月 20 日签收、已拆封,不符合示例自动退款条件 | DECLINED,说明可申请质检 | 调用提交退款工具 |
| B456 属于别的用户 | DECLINED,不披露订单详情 | 先查敏感订单再补权限校验 |
政策 v2、v3 冲突或缺关键条款 | NEEDS_HUMAN 或先补证据 | 用旧政策直接放行 |
| 模型第一轮声称“已完成”,工具尚未返回 | 保持未完成状态 | 仅凭自然语言进入 COMPLETED |
| 连续相同工具结果、调用预算耗尽 | LIMIT_REACHED 或安全映射为 WAITING | 无限循环,或以预算耗尽冒充成功 |
还要测试并发与恢复:同一用户快速点两次申请,是否只生成一个逻辑操作;进程在提交后、保存回执前崩溃,恢复是否先对账;人工审核期间政策变更,恢复是否重验资格。把这些样本放入回归集,每次改提示词、模型、工具或状态转移逻辑都重跑。线上则记录终态分布、过早成功投诉、重复申请、WAITING 停留时间、达到上限的比例和人工接管结果。不要只追求“完成率”高:把未知都标成成功会让数字变漂亮,却让用户和财务承担风险。OpenAI Agent 工作流评估
面试时怎样回答
我会先把用户目标写成可验收的业务条件,而不是让模型自己决定“做完没有”。以退款申请为例,必须核对用户权限、当前政策和订单事实,提交后还要拿到对应这次操作的权威受理回执,才能答复“申请已受理”;这不代表款项已到账。若工具超时或只返回处理中,就保存操作键与状态,进入待核实,不盲重试写入;政策冲突或特殊情况转人工。流程用明确的完成、待核实、拒绝、人工、取消和上限等状态,再加查询次数、时限和框架步数限制,避免无限循环。测试既看最终回答,也看工具轨迹、回执、重复副作用和各个终态。
如果追问“模型说已经完成可不可以直接结束”,回答是:只可以结束一次文本生成,不能替代业务完成证明。如果追问“达到 recursion_limit 怎么办”,回答是:这是框架的循环保护;保存当前事实,核对可能发生的副作用,返回未确认或人工处理状态,不能把触限当成功,也不能一味调大步数上限。
资料依据
- LangGraph:Graph API——条件边、结束节点、重执行与递归步数保护。
- LangGraph:GRAPH_RECURSION_LIMIT——触限错误与排查。
- LangGraph:Interrupts——暂停、保存和恢复。
- OpenAI:Function calling——模型提出调用与应用执行工具的分工。
- OpenAI:Evaluate agent workflows、Tracing——按实际工具轨迹验证执行。
- Stripe:Idempotent requests——网络重试时用幂等键避免重复写入的真实 API 例子;本文未假设售后工具就是 Stripe。
继续阅读:Agent 为什么会无限循环,如何避免?、工具调用失败如何处理?、LangGraph 如何实现条件分支和循环?。