☰
Agent自我反思机制如何设计才能省Token?三个原则避免成本失控
2026/10/20 4:05:21 网站建设 项目流程

做Agent应用有一阵子了,最让我头疼的不是模型不听话,而是加了“自我反思”之后,Token账单变得非常感人。反思这个机制,本来是让Agent在执行链路中自己发现错误、修正计划、避免一条道走到黑,但很多人用下来发现,成功率没涨多少,调用成本却翻了好几倍。问题不在反思本身,而在于大多数实现里,反思被当成了一个“每次必执行的仪式”,而不是一个有触发条件、有预算上限的工程模块。这篇内容想从我自己调链路时踩过的坑出发,聊聊如何设计Agent执行链路里的自我反思机制,让每一个花出去的Token都真正用在纠错上。

1. 先想清楚:反思在Agent执行链路里到底是什么角色

1.1 反思不是“多想想”,而是执行链路上的一个控制节点

反思在业界有不少学术变体:ReAct里的Thought、Self-Refine的生成-修正循环、Reflexion的自我评估记忆,名字看着不一样,本质都是一回事——在执行过程中发现预期外的情况,暂停当前动作,对已有结果做一次重新评估,然后调整下一步计划。但很多工程落地时把它理解偏了,以为反思就是告诉模型“你再认真想想”,然后每调用一次工具就让模型写一段内心独白。这本质上不是反思,而是无效的自我重复。

我习惯把它类比成代码审查。写代码时你不会每写一行就做一次全量评审,而是等一个模块完成、测试挂了、或者有人提了CR才去仔细过一遍。Agent的反思节点也应该这样设计:它不是一个每次循环都要走一遍的固定流程,而是一个由异常信号触发的控制节点。把反思放在正确的位置上,它才是纠错器;放在每个动作后面,它就是Token粉碎机。

这里还要区分“反思”和“规划”。规划是带着目标看路,反思是发现偏航后重新定位。如果Agent连规划都做不好,反思救不回来;如果链路本身是对的,频繁反思又只会干扰原本的节奏。所以,设计反思机制的第一步,就是承认它有边界:只在链路出现“不确定性”或“明确失败”时启动。

1.2 为什么反思会成为Token消耗的重灾区

先把账单算清楚,你就能理解为什么那么多人被反思机制坑了。大模型API的计费基本是“输入Token + 输出Token”,而输入Token和上下文长度强相关。假设一个Agent执行8个步骤,每一步平均消耗2000输入Token,如果不反思,总共是16000输入Token。但如果每步都反思,第5步反思时需要把前5步累积的10000 Token重新发给模型,再算上反思输出500 Token;第8步反思时可能要把前8步的16000 Token重新发一遍。所有步骤加起来,输入Token总量会从16000膨胀到六七万,如果反思时还把完整历史带着,甚至会接近十万。这还没算反思输出本身也是要花钱的Token。

另一个隐蔽的浪费点在于反思输出的内容。模型一旦被要求“反思”,很容易进入道歉模式,写出一大段“作为AI,我意识到之前的回答可能不够完善……”这类正确的废话。这些内容既没有新增事实,也不会改变计划,纯粹是为了凑字数而消耗Token。更麻烦的是,反思结果经常被原样塞回后续上下文,导致后续每一次调用都要重复支付这段反思文本的成本。你以为是多了一次思考,实际上是让所有后续步骤都背着这段废话跑完全程。

所以,省Token的反思设计,核心就一个字:少。少触发、少上下文、少输出。这个“少”不是让Agent变笨,而是让它把每一次反思都用在刀刃上。

2. 三个设计原则:从源头把反思成本压下来

2.1 原则一:反思必须由失败信号触发,而不是由固定步数触发

我见过最暴力的反思实现是:循环体里固定每两步就调用一次“reflect”,还美其名曰“持续自我改进”。结果就是Agent在正确路径上反复自我怀疑,把原本稳定的输出改得一塌糊涂。正确的做法是给反思设计一个“信号门”,只有检测到以下情况之一才允许进入反思流程:

  • 工具调用返回了错误码或异常信息;
  • 返回结果的JSON解析失败、字段缺失、类型不对;
  • 自定义校验规则不通过(比如金额不匹配、查无此人);
  • 结果为空且该场景下不应当为空;
  • Agent自身的置信度低于阈值(需要模型输出置信度,或者用打分器判断);
  • 用户对当前中间结果表达了不满意。

对应到代码里,这个信号门通常长这样:

def should_reflect(result, signal_thresholds): if result.return_code != 0: return True if not validate_schema(result.data, expected_schema): return True if result.data is None and not result.allowed_empty: return True if result.confidence and result.confidence < signal_thresholds.confidence: return True return False

没有信号就说明当前步骤正常,直接进入下一步,不产生任何额外调用。这样做还有一个好处:后续排查性能问题时,可以按信号类型统计哪些工具最容易失败,而不是面对一堆“因为反思才成功”的玄学记录。

注意,失败信号的定义一定要够窄。范围太宽会误伤正常路径,比如有些工具在“查无数据”时也会返回空列表,这看起来像失败,其实是合法响应。你需要针对每个工具写清楚“什么情况算失败”,而不是搞一个全局的“只要result为空就反思”。

2.2 原则二:反思时只给模型“局部上下文”,而不是重放全部历史

这是整个机制里最省钱、也最容易被忽略的一步。很多人反思时直接把messages数组全部丢给模型,包括最开始的任务描述、中间五轮成功工具调用的大段返回、上一步的规划文本——这些信息里,绝大多数和当前失败点无关。让模型从十万个Token里找一条线索,不仅贵,而且效果差。

我的做法是维护一个轻量级workspace,也就是“工作区”,只放三类东西:当前子任务的目标、已经确认的关键事实、最近一次失败信号。反思时,上下文组装成以下结构:

系统指令:只许判断当前步骤为什么失败,不许复述历史。 工作区事实:{workspace_facts} 当前目标:{current_goal} 失败信号:{failure_signal} 失败的原始输出片段:{last_output_snippet}

其中last_output_snippet只截取失败结果的附近几行,不要贴完整输出。举个例子,如果工具返回了一个包含500行数据的JSON里某个字段缺失,你只需要把缺失字段附近的20行给模型,而不是把500行全部丢进去。这样一次反思的输入Token通常可以控制在500以内,而不是动辄一两万。

这里的关键是想明白一个道理:反思需要的是“线索”,不是“档案”。完整执行历史是审计用的,不是推理用的。如果你觉得某些历史以后还可能用到,可以把它压缩成摘要存起来,而不是让模型每次重新读一遍原始文本。

2.3 原则三:反思输出必须是结构化JSON,并由代码来限制长度

不限制输出格式的反思注定会浪费Token。你让模型自由反思,它就会给你写小作文,而且写得越长越自信。为了让反思既有效又可计量,我要求所有反思结果强制输出JSON,字段越简单越好:

{ "failure_cause": "编号E32:目标接口返回的地址字段为空", "evidence": "第6行附近address为null", "next_action": "改用备用地字段coordinates,重新调用查询接口" }

不要加“reasoning”这种容易水字数的字段,也不要允许模型输出“我学到了……”之类的废话。后处理时用json.loads解析,解析失败就放弃本次反思,而不是让模型重新生成一次——重试本身又是一笔Token开销。同时要给输出长度设硬性上限,比如max_tokens=300,你以为模型会不够用,实际上大部分有效反思100个Token以内就能说完。

如果你担心模型不听话,可以在提示词里写“只允许输出3个字段,每个字段不超过50字”,并在解析后校验字段内容。更重要的是,代码要强迫反思结果“落地”:解析出next_action后,把它作为下一轮规划的一部分写入workspace,而不是把整段反思文本回贴进上下文。这样反思的输出Token是一次性的,不会被后续调用反复支付。

3. 一套不浪费Token的反思循环具体怎么落地

3.1 执行循环的骨架设计:先执行,再校验,按需反思

我把上面三条原则落到一个标准循环里。整体流程是:执行动作、校验结果、成功则更新工作区继续走、失败则检查反思次数是否用完、没用完就带着局部上下文反思一次、把反思结果转成新计划再继续。下面是一段可以在本地跑通的伪代码结构:

def agent_run(goal, max_steps=8): workspace = init_workspace(goal) reflect_count = 0 for step in range(max_steps): action = plan_next(workspace, goal) result, ok = execute_action(action) if ok: workspace = update_workspace(workspace, action, result) continue if reflect_count >= MAX_REFLECT: return fallback_with_best_effort(workspace, goal) # 只取最近三条事件作为反思上下文 context = build_reflect_context( workspace=workspace, action=action, result=result, recent_events=3 ) reflection = reflect(context) workspace = apply_reflection(workspace, reflection) reflect_count += 1 return finalize(workspace)

我来说说几个容易写错的地方。build_reflect_context不是把历史全传进去,而是从workspace里拼出“当前目标+最近三条工具事件+当前失败信号”。reflect函数内部先调用一次模型,拿到JSON后立刻解析,解析成功才返回结构化对象。apply_reflection的核心逻辑是把next_action覆盖到当前节点,同时把failure_cause和evidence压缩成一行摘要追加到workspace,原始反思文本直接丢弃。

这个循环最大的特点是:成功路径上完全没有反思调用,失败路径上每次反思的上下文也很小,而且反思次数有上限。即便一个任务连续失败两次,额外消耗也就两次局部反思的成本,不会出现十几轮的反思循环。

3.2 反思次数和触发条件的默认参数参考

具体参数要根据业务调,但我可以给一套经过压测的默认值,先跑起来再改。

参数默认值设计理由
MAX_REFLECT2超过两次还失败,大概率是当前问题无法单纯靠反思解决,继续重试只是烧钱
REFLECT_TRIGGER校验失败或置信度低于阈值没有失败信号绝不反思,保证零无效调用
CONTEXT_WINDOW最近3条事件足够覆盖当前子任务的因果链,再往前的信息与本次失败相关性骤降
OUTPUT_LIMIT300 token逼模型提炼核心信息,防止反思小作文
FALLBACK返回当前最佳结果反思耗尽后必须有个止损出口,不能无限循环

这里最容易被低估的是MAX_REFLECT。很多同学会觉得“多反思几次总能成功”,实测下来,同一个节点连续反思3次以上,成功率提升不到5%,Token成本却翻两三倍。因为模型对同一段失败信号反复思考,输出开始趋同,甚至互相否定。2次是性价比比较高的上限,如果你跑在复杂的动态任务里,最多放宽到3次,再多就得靠外部规则而不是模型自省了。

另一个容易被忽略的是CONTEXT_WINDOW。反思上下文不是越多越好,而是越准越好。你给它5条无关的工具返回,它反而会编造一些“线索”把失败归因到无关事件上。窗口越小,模型越难跑偏。

3.3 用“事实卡片”替代冗长历史,让上下文长度保持稳定

真正让反思消耗不失控的底层机制,是我在workspace里维护的“事实卡片”。每次工具调用成功,我不把完整返回塞进历史,而是先对返回结果做一次抽取,只保留与目标相关的关键事实。比如说,Agent调用了一个订单查询接口,返回了20个字段,但当前目标只是判断“支付状态是否完成”,那事实卡片里只写“订单号X状态=paid,支付时间=2025-06-01 10:23”,其他字段一概不管。

事实抽取的提示词,我建议这样写:

请从下面的工具返回结果中,提取与当前任务相关的关键事实。 要求: 1. 最多3条; 2. 每条不超过20个字; 3. 忽略日志、错误码、无关元数据; 4. 只保留事实,不保留推理。 结果:...

这个抽取过程本身会消耗一些Token,但它是值得的。因为抽取出来的事实卡片会被后续所有步骤复用,直接把长期上下文压到几百Token以内。如果某一步需要更详细的原始信息,可以再针对性地调用一次“按需回看”,而不是把整段历史挂在上下文中。

这样做的代价是丢失了一部分细节,但对Agent执行链路来说,细节丢失恰恰是控制成本和防止信息过载的手段。模型记不住也不该记住所有中间结果,它只需要记住影响下一步决策的那些关键事实。

4. 提示词这样写,模型才肯“废话少说”

4.1 一套可以直接抄的反思提示词模板

结构化的反思流程需要配套结构化的提示词。下面这个模板我在多个模拟项目里改过几轮,效果比较稳,你可以直接拿来当起点。

你现在是一个执行诊断器。你的任务是根据失败信号判断当前动作为什么失败,并给出可执行的下一个动作。 可以使用的信息: 工作区事实:{workspace_facts} 当前目标:{current_goal} 失败信号:{failure_signal} 失败输出片段:{last_output_snippet} 规则: 1. 只诊断当前这一个失败点,不要回顾其他步骤; 2. 不要复述已有信息,不要道歉,不要写总结; 3. 只有在能找到具体证据时才允许指出失败原因,否则写“证据不足”; 4. 下一步动作必须具体到可以直接执行,禁止写“继续尝试”这类空话; 5. 只输出JSON,格式如下: {"failure_cause": "不超过50字", "evidence": "不超过30字", "next_action": "不超过50字"}

这个模板的精髓在于“三个禁止”:禁止复述、禁止道歉、禁止空泛的下一步。再加一个强制JSON,让代码能自动约束长度。实际使用时,模型偶尔还是可能输出一段解释文字再接JSON,我建议在后处理里用正则截取最后一个JSON对象,截不到就放弃本次反思。宁可错过一次修正机会,也不能放任输出失控。

4.2 大小模型搭配:先让便宜模型分类,再让大模型深度反思

很多反思之所以贵,是因为把所有问题都扔给了同一个强推理模型。但失败信号里有一大半是简单模式:JSON格式错误、字段缺失、超时。这些根本不需要“反思”,只需要“分类”。我在正式链路上加了一个轻量分类器——可以用一个小参数模型,甚至用规则判断——先把失败原因粗分成几类:

  • 可恢复且结构明确(格式错误、字段缺失、数据为空),这类直接套固定修复模板,不调反思;
  • 可恢复但原因不明(业务校验不通过、依赖项异常、多步数据联动出错),这类才进入反思流程;
  • 不可恢复(权限不足、目标不存在),这类直接终止并提示用户。

做了一个对比实验:在模拟项目X里,大约60%的失败信号属于“可恢复但结构明确”,直接被规则修复;真正需要调大模型反思的只有40%。那个项目的反思调用次数下降了五成左右,总Token开销则降了大概三成多。具体数字会因为业务差异有浮动,但方向是确定的:不要让大模型做选择题,让它只做难题。

这里也有个取舍:小模型分类器本身也是调用,如果分类器判断不准确,反而会兜底成大模型反思,造成双重消耗。所以分类器的目标不是“百分之百分对”,而是“宁可把模糊的判进可恢复且原因不明”,也不要自信地把它判成规则可修复,导致修复完发现根本没解决问题。

4.3 反思结果如何回写,才不会让上下文二次膨胀

前面提过一句,但这里值得展开:反思结果回写是很多实现没做好的环节。最糟糕的做法是把反思返回的文本追加到messages数组里,再继续跑下一轮。这样下一轮调用时,上下文里既有旧历史,又有长反思,还有新一轮动作,长度直接起飞。正确做法是只提取三个字段:failure_cause、evidence、next_action。然后:

  1. 把failure_cause和evidence合并成一条短摘要,覆盖掉workspace里原来的错误记录;
  2. 把next_action作为当前子任务的强制计划项,插到待执行队列头部;
  3. 原始反思文本在解析后立即删除,不进任何上下文。

如果你担心丢失模型背后的推理过程,可以在一个专用的“反思日志”里单独存一份完整文本,用于事后审计。但审计日志和在线上下文必须分开,前者进存储,后者进模型。这样在线调用的上下文长度始终保持在“工作区摘要 + 最近三步事件”的范围内,不会随着反思次数增加而线性膨胀。

5. 避坑实录与常见问题排查

5.1 为什么模型越反思越差?多半是反思污染

我踩过最大的坑,是加了反思之后,Agent在处理原本正确的答案时越改越错。后来发现,反思提示词里默认给模型传递了一个隐含信息:你失败了,你需要找出哪里错了。于是模型开始过度否定,把一个本来已经稳定的计划拆开重看,甚至编造出一些不存在的失败原因。我把这个现象叫“反思污染”。

要避免它,有几个原则可以立成铁律。第一,校验通过的步骤绝对不触发反思,成功路径上不让模型有“找茬”的机会。第二,反思提示词里明确写“只有存在具体证据时才允许指出失败原因,否则输出证据不足”,从根上遏制模型为了满足指令而强行编错因。第三,如果一次反思给出的evidence是空的,或者与失败信号明显无关,直接丢弃这次反思结果,不要让它进入下一轮。

这个现象在复杂任务里特别常见。模型本身是生成模型,它不一定真的“知道”自己的错误,它只是在语言上把错误合理化。所以反思不该变成对模型自我认知的测试,而应该变成对执行结果的客观诊断。记住这句话:反思的对象是结果,不是模型本身。

5.2 反思循环卡死的三个典型原因和对应排查

循环卡死是反思机制最常见的故障,通常是下面三种原因造成的。

第一个原因:反思后的next_action和原来的action完全一样。模型嘴上说“换个方案”,实际给出的下一步还是重跑同一个工具,于是陷入“失败-反思-再失败”的死循环。排查方法很简单:在apply_reflection之前,拿新计划动作和旧动作做个比对,如果文本相似度超过一定阈值,就终止反思,直接走fallback,而不是让它再烧一轮Token。

第二个原因:失败信号定义得太宽。比如你把“返回数据为空”都当成失败,但有些查询在正常业务下就是会返回空列表,结果Agent在一个合法结果上白白反思了一次又一次。排查方法是逐个工具审查失败判定条件,把“正常空结果”和“异常空结果”区分开,比如加上“没有返回是否是因为参数非法”这种前置规则。

第三个原因:反思上下文里重复出现同一段错误信息。由于workspace没有清理已处理过的错误,模型每次反思都看到同一段红色警报,于是每次都在同一处打转。排查方法是在每次反思完成后,把对应错误片段从待观察事件中移除,或者打上error_handled标记,确保下一轮反思看到的是新现象,而不是旧伤口。

5.3 更进一步:把反思结果沉淀成经验库,让Agent越用越稳

当反思机制稳定跑起来之后,你会发现很多失败原因其实是重复出现的。同一个接口偶尔超时、同一个字段偶尔缺失、同一种请求偶尔格式不对,这类问题反反复复出现。每次都用大模型反思一遍,浪费不说,结果还不一定比上次好。所以我在反思流程后面加了一个“经验库”,把每次反思的结果压缩成一条规则存下来。

规则可以是一行结构化记录:

{ "trigger": "order_query.address is null", "repair": "use fallback field coordinates", "reflection_saved_cost": 850 }

当新的失败信号到达时,先去经验库里做一次检索,看有没有能直接匹配的旧规则。命中就直接采用旧规则里的修复动作,完全跳过模型反思。我在一个比较规范的模拟项目X里试过,大概跑了百来次任务之后,有30%左右的失败可以靠经验库直接命中,省下来的Token很可观。当然,经验库不能无限膨胀,要做容量上限和过期策略,避免旧规则在业务变化后变成“错误经验”。

这个方案执行起来不复杂,但回报很高。它相当于给Agent装了一个“记性”:同一个坑第一次踩要花大模型反思,第二次踩只需要一张索引卡片。这才是反思机制应该有的终点:不是每次都从头思考,而是把思考过的结论变成可复用的资产。

我在实际项目里习惯把整套逻辑拆成三个独立模块:失败信号检测器、反思执行器、经验缓存。每次调链路,先在日志里看三个模块各自的调用次数。只要发现“反思执行器”调用次数明显高于“失败信号检测器”触发的次数,说明有反思被无谓地重复调用了,这时候回看最近三条上下文,十有八九能找到是我哪个参数没配紧。反思机制的设计没有银弹,但只要你坚持“少触发、短上下文、结构化输出、有上限、有缓存”这几条,Token基本不会被白白烧掉。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询