ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

让LLM学会推理止损:从徒劳推理到Agent的放弃机制

让LLM学会推理止损:从徒劳推理到Agent的放弃机制 做推理增强的 LLM 项目时我经常遇到这样一类让人头疼的输出模型在一道并不复杂的数学题上反复“自我修正”每修正一次都信心满满最终却在错误的答案上收尾。更麻烦的是它没有在任何一个环节表现出“我可能走偏了”的迹象。举个最简单的例子。让模型解方程3(x2)15。模型第一步展开成3x615没问题第二步写成3x9也没问题可如果它在第三步错写成3x6那么后续无论它怎么演算都不可能得到x3这个正确答案。它还是会继续写第四步、第五步甚至用代数法、代入法、图形法再验证一遍最后信心满满地给出x2。这个过程持续得越久错误被包装得越严密反而越难被用户一眼看穿。这类问题有个共同特征推理表现得非常努力但努力的方向没有价值。我们把这个过程称为徒劳推理futile reasoning。在 CoT、ReAct、Agent 等推理范式大面积落地之前推理长度还短“想多了”最多浪费几十个 token。但当长思维链成为常态、Agent 可以自主调用工具之后模型的“死磕”会直接转化为 token 成本、接口超时、用户流失甚至在自动化链路里放大为错误的业务决策。所以问题就变成我们能不能让 LLM 学会在时机合适的时候说“我该停下来了”更进一步如何在训练阶段就让模型具备这种能力而不是只在工程层外加一个max_attempts参数硬卡这篇文章围绕Knowing When to Quit这个主题从四个层面展开先讲徒劳推理为什么会发生再讲如何诊断它然后讨论训练层面的止损机制最后给出 Agent 与提示词层面的工程兜底方案。整篇文章适合做 LLM 推理、Agent 应用、模型微调和算法评估的同学阅读读完你可以建立一套完整的“推理止损”评估框架。1. 徒劳推理的本质它在解决什么问题如果你只是偶尔用 ChatGPT 做做问答可能很少注意到徒劳推理。但在下面的场景里它几乎是致命的一个数学推理模型在解线性方程时从中间步骤开始就把符号写错然后基于错误前提演算出 2000 字的推理过程最终给出一个看起来很有条理、实际完全错误的答案一个代码生成 Agent 在调用某个 API 失败后不检查错误信息而是连续五次用几乎相同的参数重试把任务日志刷满一个信息抽取系统在判断某个字段不存在时仍然会生成一段“合理推测”并当作事实返回。这些现象背后有一个共同点模型把“继续推理”当成了“推理质量”的一部分。它没有学会区分“有效探索”和“无效死磕”。这篇文章真正触及的是 LLM 推理系统的三个底层问题而不仅仅是“怎么让模型少说几句话”。第一训练目标里缺少“止损”维度。多数监督微调SFT数据以完整正确答案为目标模型学到的是“只要继续推导迟早能找到答案”。但在数学或逻辑题里错误路径上的持续推导只会让误差累积而不是给模型带来新的信息。第二评估指标会掩盖浪费。只看最终正确率时一个用 5000 token 答对题目的模型和一个用 300 token 答对题目的模型得分相同。于是模型没有动力早点结束反而可能因为训练信号而偏向更长推理。第三工程框架常常把停止逻辑放在最外层。比如设置最大轮数、最长 token 数。这种硬性截断的问题是它不理解“为什么停”它只是强制切断了输出。更理想的方式是模型本身能产生“需要终止当前计划”的内部信号工程层再配合兜底。所以这篇文章面向的读者有四类正在做 LLM 推理优化的算法工程师需要识别推理轨迹中的无效成本正在开发 Agent 应用的开发者需要避免工具调用循环和自动化决策放大准备做领域微调的数据工程师需要构造包含“正确放弃”的样本做模型评测与质量保障的同学需要设计能暴露“假性努力”的评测集。读完这篇文章你会得到一套从诊断到训练、再到工程兜底的完整方法。它不依赖某个特定模型也不绑定某个平台而是以当前主流 LLM 推理范式为基础讲清楚通用思路和关键抓手。2. 理解徒劳推理现象、代价与根本原因在进入技术细节之前我们先把概念对齐。2.1 什么是徒劳推理徒劳推理指的是模型在没有有效信息增益的情况下持续进行推理动作最终产出与正确结果偏离甚至背离的输出。它不一定意味着模型全程都在乱写。更常见的情况是推理前半段是合理的某个中间步骤出现微小偏差此后所有推理都建立在这个偏差之上形成一种“局部流畅、全局错误”的状态。这种状态特别难以发现因为模型生成的每一步看起来都有逻辑。它往往不像“胡言乱语”那样显眼而是表现为“解释得头头是道但就是不对”。2.2 代价分三层第一层是资源成本。长思维链会消耗大量 token尤其是在商用 API 计费或自建集群推理的场景下推理长度直接决定成本。徒劳推理会让单次请求的 token 消耗变成常规值的数倍。如果每天有十万次请求这就是一笔不可忽略的开支。第二层是延迟与交互体验。用户在等待一个最终答案时可能已经看到转圈 30 秒甚至更久。对于在线服务来说延迟直接影响了转化和留存。更棘手的是用户无法判断模型是在“认真思考”还是在“原地打转”。第三层是自动化场景的错误放大。Agent 在无人值守时连续重试失败的 API或者在财务、运维等场景把一个错误状态反复确认成“正常”这种问题的危害远大于对话里的歪楼。一次错误的自动决策可能引发一串连锁反应。2.3 为什么会发生原因可以从数据和训练目标两个层面理解。从数据层面看大多数 CoT 数据集是“从头推到尾”的完整轨迹很少包含中途放弃、回退、重新规划的过程。模型只能从这些样本里学到“推理就应该一路走下去”而没有学会“在某些状态下继续推导的预期收益已经低于停止重来”。从训练目标层面看SFT 阶段通常只对齐最终输出。强化学习阶段如果使用稀疏结果奖励模型不会自动学会节省步骤。只有当奖励函数显式惩罚冗余推理路径或者鼓励提前正确终止时模型才有动力改变行为。此外还有模型本身的不确定性评估问题。LLM 虽然可以输出 token 级概率但它们在自然语言中很少直接表达“我不确定”。我们通常只能从推理内容、停顿信号、置信度分支等间接推断。这里需要强调一个容易误判的点不要以为“推理长度越短越好”。正确目标不是削减推理而是在必要的地方保留探索在收益接近零的地方果断终止。所以“何时放弃”比“少推理”更准确。3. 现有推理范式中的放弃机制回顾为了说明“让模型学会放弃”为什么是一个缺口我们快速回顾几种主流推理/行动范式看看它们分别如何处理错误路径。3.1 Chain-of-ThoughtCoTCoT 通过让模型生成中间推理步骤来提高复杂任务正确率。它没有显式的放弃机制。如果某一步错了后续步骤大概率跟着错。它的“止损”方式只能是模型在下一次采样时好运地走了另一条分支。所以在纯 CoT 场景下我们经常看到模型在错误路径上越走越远。因为输出空间是自回归的一旦进入一条“局部合理但全局错误”的路径模型很难自己跳出来。3.2 ReActReAct 的核心是让模型交替进行 Reasoning推理和 Acting行动例如生成一段思考再调用一个工具然后观察结果继续推理。它比纯 CoT 多了一个“观察结果”的环节这为模型提供了外部反馈。观察结果如果明显异常理论上模型可以终止当前计划重新开始。但在实际应用中ReAct 类 Agent 最常见的失败恰恰是“工具调用循环”。模型反复调用同一个工具拿到相同或类似的错误结果仍不停止。原因在于基座模型没有把“重复失败”识别为“需要退出”的信号。它的训练数据更多来自任务正常完成的轨迹缺少失败-放弃-重规划的样本。3.3 Reflexion 与 Self-CorrectionReflexion 的思路是让模型在失败后根据反馈生成反思用于下一次尝试。这种机制确实包含“知道自己错了”的成分但它的代价是会产生多轮尝试在真实交互中成本和延迟都会成倍上升。如果每一次失败都触发反思和重试本质上又变成了另一种形式的死磕。Self-Correction 也有类似问题。模型被显式要求检查自己的答案但大多数训练和评测只关注“最后结果是否正确”没有关注“检查了几次”“第一次正确后是否还继续修改”。于是一些模型会形成“改来改去”的坏习惯原本正确的答案被改错。这类问题在长文本生成和数学推理里尤其明显。3.4 为什么这些范式都没有真正解决“停止”问题这些范式都把“继续”当作默认动作把“停止”当作异常或后备逻辑。无论是 ReAct 的max_attempts、CoT 的max_tokens还是 Reflexion 的max_reflections都是运行时的硬性边界而不是模型自身的判断能力。真正需要的是一种“退出协议”让模型在面对某个状态时能够产生“当前计划不值得继续需要放弃或重来”的内部表征并用自然语言或结构化动作表达出来。这需要从训练数据、奖励函数和评测指标三个方向同时下手。4. 诊断徒劳推理离线评估与在线监控在谈训练之前先讲诊断。如果无法量化“模型什么时候在死磕”我们就无法评估改进效果。诊断分为两个层面离线分析和在线监控。4.1 离线诊断思路离线诊断的基本方法是对一批推理轨迹做结构化分析。我们需要拿到每个步骤的中间信息推理文本、是否调用工具、工具返回值、最终答案、真实正确性。建议分析下面几个信号步骤数与最终正确性的关系如果一个模型答对题目的平均步骤远高于正常水平说明它存在冗余推理连续无效动作检测在 Agent 轨迹中连续 N 次相同的工具调用且返回错误可能构成徒劳循环不确定性表达回溯检查模型在错误答案前的最后若干步是否出现“可能”“或者”“我不确定”等弱化词这能辅助判断模型在走向错误时是否已有感知自我修正方向变化从第一次答案到最终答案答案正确性是否出现过“对→错”的变化。下面给一个轻量轨迹分析脚本示例。它假设你的推理轨迹已经保存为 JSONL 格式每条记录包含steps列表。# 文件路径diagnose_futile_reasoning.py import json from collections import Counter def analyze_trajectory(record): steps record[steps] correct record[label_correct] total_tokens sum(step.get(tokens, 0) for step in steps) # 检测重复工具调用 tool_calls [s for s in steps if s.get(type) tool_call] repeated_failures 0 last_call None for call in tool_calls: signature (call.get(tool_name), call.get(arguments)) if signature last_call and not call.get(success, True): repeated_failures 1 last_call signature # 检测不确定性表达 weak_markers [可能, 或许, 不确定, 我不太确定, 也许, might, could be] weak_count 0 for step in steps: text (step.get(text) or ).lower() for marker in weak_markers: if marker.lower() in text: weak_count 1 break return { sample_id: record.get(id), correct: correct, total_tokens: total_tokens, num_steps: len(steps), repeated_failures: repeated_failures, weak_marker_steps: weak_count, } def run_diagnosis(path): results [] with open(path, r, encodingutf-8) as f: for line in f: line line.strip() if not line: continue record json.loads(line) results.append(analyze_trajectory(record)) # 按答案正确性分组对比 correct_group [r for r in results if r[correct]] wrong_group [r for r in results if not r[correct]] print( 正确组 vs 错误组 平均统计 ) for name, group in [(correct, correct_group), (wrong, wrong_group)]: if not group: continue avg_tokens sum(r[total_tokens] for r in group) / len(group) avg_steps sum(r[num_steps] for r in group) / len(group) avg_repeated sum(r[repeated_failures] for r in group) / len(group) print(f{name}: avg_tokens{avg_tokens:.1f}, avg_steps{avg_steps:.1f}, favg_repeated_failures{avg_repeated:.2f}) if __name__ __main__: run_diagnosis(trajectories.jsonl)这个脚本不依赖外部库只读取 JSONL 并统计。它输出的核心价值是如果你发现错误组的平均 token 消耗明显高于正确组说明模型在错误路径上投入了大量无效推理。这些样本就是训练止损数据的重点来源。4.2 在线监控离线分析解决的是“回顾性评估”但是在生产环境里我们更需要在每次请求中进行实时判断。在线监控不需要修改模型可以在 Agent 框架层做信号采集记录每一步的 token 消耗、工具名、返回状态统计单轮请求内连续失败次数当累计成本超过阈值时自动降级或转入人工确认流程记录模型输出中的置信度词汇作为后端干预的辅助信号。在线监控的数据结构可以参考下面这个 JSON 示例方便后续做统计分析和训练数据回流{ request_id: req_123, task: 查询订单状态, steps: [ {step: 1, type: tool_call, tool: get_order, error: null, tokens: 120}, {step: 2, type: tool_call, tool: get_order, error: order_not_found, tokens: 130}, {step: 3, type: abort, error: null, tokens: 80} ], abort: { triggered: true, trigger: repeated_error, reason: order_not_found 连续两次切换为人工确认 } }在线监控的意义不只是限流更重要的是积累现场数据。这些失败轨迹会被存储下来成为下一轮训练数据的重要来源。这也是我反复强调“诊断一定要在训练之前设计好”的原因没有数据采集后续的止损训练就无从谈起。5. 训练模型学会放弃数据构造与训练思路诊断之后下一个问题是能不能通过训练让模型本身就具备“放弃”能力5.1 构造止损样本数据最直接的思路是扩展 SFT 数据集加入“正确放弃”的样本。也就是说不仅收集那些“最终完成”的轨迹还要收集“判定当前路径无望决定放弃或重新规划”的轨迹。构造样本时可以按下面流程从失败轨迹中提取筛选出最终答案错误的样本对轨迹做自动标注找到第一个产生偏差的步骤标记为“分叉点”从分叉点出发人工或借助更强模型生成一个“停止并说明原因”的参照结果人工审阅确认放弃理由与分叉点有因果关系避免模型把“放弃”学成“随便不干了”。这类样本需要标注清楚几个关键点放弃时机在哪个推理步骤后模型决定不再继续当前路径放弃理由模型需要生成一段简洁的原因说明例如“工具连续返回错误码继续重试没有意义”后续动作放弃之后是直接给出“无法完成”还是重新规划另一条路径。数据格式可以自定义。下面是一个适合微调和训练数据构造的 JSON 示例{ instruction: 查询用户订单状态, trajectory: [ {role: assistant, content: 我需要先调用订单查询接口参数是 order_id20240801}, {role: assistant, content: 调用结果{ \error\: \Order not found\, \error_code\: \404\ }}, {role: assistant, content: 连续两次返回相同的 404 错误当前订单 ID 可能已失效继续重试没有收益。决定放弃该路径并提示用户核实订单号。} ], stop_point: 3, stop_reason: repeated_error_404, expected_action: ask_user_to_verify_order_id }注意这里的关键不是把“放弃”写成一句“我不行了”而是让模型学会基于证据做出判断。在 SFT 数据中如果只写“我放弃了”模型学不到泛化能力。要让放弃理由包含充分条件比如“相同错误码连续出现 3 次”“目标工具返回结果与预期类型不符”“集合为空且无补充信息源”。5.2 过程奖励与结果奖励配合纯 SFT 无法解决所有问题因为模型可能记住了“放弃句式”但没有真正理解什么时候该放弃。更好的做法是在强化学习阶段引入过程奖励。常见设计是对每一步给一个即时奖励奖励不仅取决于最终答案是否正确还取决于这一步是否带来了新的有效信息、是否重复了无效动作、是否在确定路径上及时停止。可以用奖励项叠加的方式完成任务并正确1.0提前在明确的死胡同中止并给出可解释原因0.5每一步无信息增益的推理-0.05重复相同工具调用且结果不变-0.1最终答案错误-0.5这些数字需要根据任务调节不构成通用标准但它表达了一个重要原则模型不再只对“最终答案正确”负责还对一个更节约、更诚实的过程负责。5.3 让“停止”成为一个可被采样到的动作在更细的层面我们可以把“停止”想象成模型决策中的一个动作。当前的生成本质上是自回归每个 token 都有概率被采样。如果我们在提示词和训练中都显式给模型一个“退出/重来”的动作空间让它在这个动作与“继续推理”之间做权衡模型就有机会学习到更合理的边界。实际操作中不需要真的改动模型架构只要在 Agent 的提示词中定义一种结构化退出动作并在训练数据里让模型频繁使用它。例如让模型在需要停止时输出一个 JSON 动作{action: abort, reason: 连续三次相同异常需用户确认输入}这样工程层可以解析这个动作来决定是否停止而不是依赖自然语言里的“我想停止”去匹配。6. 提示词与 Agent 层面的工程兜底并不是每个团队都有资源和数据去做自定义微调。对大多数团队来说更现实的是通过提示词和 Agent 框架实现“止损”。这部分可以直接给出可落地的配置和代码。6.1 给模型显式的“预算”与“退出条件”先给一个易用的系统提示词模板适用于有工具调用能力的 Agent你是一个任务执行助手。在执行任务时请遵守以下退出协议 1. 开始任务前先规划步骤并为任务设置一个最大尝试预算。 2. 每次工具调用后检查结果是否为目标类型。如果结果包含错误码或异常字段仔细阅读错误码不要重复相同参数。 3. 如果同一个工具以相同参数返回相同错误超过 2 次必须停止该路径。 4. 如果你发现当前计划的前提条件已经不成立例如查询对象不存在、权限明确不足、数据为空且无后备数据源应当立即停止计划并给出清晰的原因说明。 5. 停止时先用一句话总结证据再输出结论。禁止在结论中假装任务完成。这里的核心不是让模型“少做”而是给它一个明确的证据判断标准。模型不会因为你说“不要死磕”就真的理解但如果你给出“相同错误超过 2 次”“前提条件不成立”这样的可观察条件它能更好地遵守。6.2 Agent 控制循环中的硬兜底提示词能力有限工程层才是最后防线。下面是一个简化版 Agent 控制循环它能统计连续失败次数并在达到阈值时自动终止。# 文件路径agent_with_abort.py import json from typing import Optional class ToolResult: def __init__(self, ok: bool, dataNone, error: Optional[str] None): self.ok ok self.data data self.error error class AgentWithAbort: def __init__(self, llm_call, max_same_error2, max_total_steps10): self.llm_call llm_call self.max_same_error max_same_error self.max_total_steps max_total_steps def run(self, task: str): messages [{role: system, content: SYSTEM_PROMPT}] messages.append({role: user, content: task}) last_error_key None same_error_count 0 for step in range(self.max_total_steps): response self.llm_call(messages) action self._parse_action(response) if action[type] abort: return {status: aborted, reason: action[reason], steps: step 1} if action[type] finish: return {status: finished, answer: action.get(answer), steps: step 1} if action[type] tool_call: result self._call_tool(action[tool], action[arguments]) error_key result.error or OK if error_key last_error_key and not result.ok: same_error_count 1 else: same_error_count 1 last_error_key error_key if same_error_count self.max_same_error: return {status: aborted, reason: f重复错误超过 {self.max_same_error} 次} messages.append({role: assistant, content: response}) messages.append({ role: tool, content: json.dumps(result.__dict__, ensure_asciiFalse) }) return {status: timeout, reason: 超过最大步数} def _parse_action(self, response): # 实际项目建议用 JSON 解析或结构化输出这里做简化 text response if abort in text: return {type: abort, reason: text} if finish in text: return {type: finish, answer: text} return {type: tool_call, tool: , arguments: } def _call_tool(self, tool, arguments): # 接入真实工具这里返回一个模拟结果 return ToolResult(okFalse, error404)这个示例展示了三个关键点统计“相同错误”而不是“连续失败”因为不同错误可能意味着问题在变化模型可以通过结构化动作主动退出而不是只能靠外层超时外层仍有max_total_steps作为硬兜底。在生产环境中这段代码需要接入真正的 LLM 调用和工具执行器并加上日志记录、监控上报和人工审批通道。这里也要特别提醒涉及生产操作、删除、资金变动等场景时Agent 的“放弃”动作应当升级为“转人工确认”不能让它自由终止任务后直接返回。6.3 针对不同任务的退出策略不同任务类型退出策略不同。数学题如果模型检测到中间步骤产生矛盾例如01、符号方向反转应当回到上一次可靠状态重推而非继续推导。这类任务更依赖训练数据中的退让行为。工具调用核心是错误码和重试策略。对可重试错误如限流可以等待后重试对不可重试错误如 400 参数错误应当立即停止并修正参数。代码生成模型可以把“测试失败”作为关键信号。如果连续多次 test case 失败且失败点相同更好的策略是重新审视需求而不是继续打补丁。提示词和 Agent 框架能兜住一部分问题但如果你想彻底改进模型训练仍然是必选项。所以不要把工程兜底当成终点。7. 验证效果如何衡量“放弃能力”训练和工程层都做完以后用什么指标判断效果如果只看最终正确率会漏掉“早停”带来的成本收益。我建议你建立一套“止损评估”指标体系至少包含以下维度。7.1 有效成功率有效成功率定义为任务最终完成的请求数 / 总请求数。这里的“完成”不仅是模型说完成还要经过下游规则或人工校验。这个指标用来防止模型学会了“放弃”之后把所有困难问题都丢给你。7.2 平均推理成本平均推理成本可以分解为 token 数、步骤数、工具调用次数。建议按结果正确性分组统计。一个健康的变化趋势是正确组的成本基本持平错误组的成本显著下降。因为错误组往往是死磕的重灾区。7.3 止损准确率与假放弃率止损准确率在模型主动中止的样本中中止决策正确的比例。假放弃率模型在明明还有可行路径时选择放弃的比例。这两个指标要放在一起看。如果模型为了避免长错误推理而过度放弃它会变得“保守但无用”。理想状态是止损准确率高、假放弃率低。计算方法可以用下面这个简单脚本def compute_abort_metrics(records): total len
返回列表