ACTS:用 MDP 建模推理过程,让 LLM 省 token 还不掉准确率
最近在做一个 RAG 项目时遇到一个痛点:用 o3-mini 跑复杂推理问题,token 消耗动辄上万,客户一看账单直接问我"能不能控制一下思考深度?"。当时我能做的就是粗暴截断 max_tokens,结果准确率断崖式下降------一道数学推理题,给 full budget 能做对,砍到一半 token 直接答非所问。
后来我试了几种方案:缩短 system prompt 里的推理指令、用小模型先跑一遍再决定是否升级、甚至写了个正则把 CoT 里看起来"废话"的部分砍掉。效果都不理想------要么省不了多少,要么准确率掉得离谱。最离谱的一次,正则把 "Let me verify this step" 这类验证性语句删了,结果模型在关键步骤上犯了低级错误没 catch 住。
直到上周我偶然看到一个叫 ACTS 的开源项目(arXiv: 2606.03965),才发现有人把这个问题建模得非常漂亮。花了一天时间读完论文、跑了一遍代码,这篇笔记算是把我的理解整理出来。
看完本文你能知道:
- LLM 推理效率问题的本质是什么,为什么简单截断行不通
- ACTS 如何用 MDP + 强化学习实现"边推理边调控"
- 实际效果如何、代码怎么跑起来、有哪些坑
问题的本质:你管不了模型"怎么想"
Chain-of-Thought 推理是大模型能力跃升的关键。模型"想得越多",答案越准------这已经被无数实验验证。但硬币的另一面是:token 消耗和延迟也在飞涨。
我之前那个 RAG 项目里,一个复杂问题的 CoT 动辄 8000-12000 token,其中真正对最终答案有贡献的可能只有 3000-4000,剩下的都是模型在"自言自语"------反复确认已经知道的东西、走弯路再回头、用不同方式重复同一个结论。
现有的效率优化方案大致分三类:
| 方法 | 思路 | 问题 |
|---|---|---|
| 长度压缩 | 训练模型生成更短的 CoT | 需要额外训练,泛化性差 |
| 早停机制 | 判断何时"想够了"就停 | 无法控制推理策略,只管长度 |
| 推理蒸馏 | 用大模型 CoT 训练小模型 | 小模型天花板有限 |
这些方法有一个共同的盲区:它们只管"想多久",不管"怎么想"。模型内部的推理策略是隐式的、不受控的。你想让它"先列要点再逐个分析"还是"直接给结论再反向验证"?做不到。
我之前那个"正则砍 CoT"的方案本质上就是长度压缩,当然不好使------你不知道砍掉的那部分是不是关键推理步骤。更糟的是,模型在 CoT 中的"废话"有时候恰恰是它在做自我验证,砍掉反而降低了准确率。这让我意识到,问题不在于 CoT 太长,而在于我们没法控制 CoT 的"质量"。
核心思路:推理即 MDP
ACTS 的做法是:把 LLM 的推理过程看作一个马尔可夫决策过程(MDP),然后用一个轻量的 controller agent 来"驾驶"一个冻结的 reasoner。
这个建模方式的精妙之处在于:它把"控制推理"这件事从"修改模型"变成了"在模型外部加一个决策层"。传统方法(比如训练模型生成更短的 CoT)需要改动模型本身,而 ACTS 的 reasoner 完全不动,所有控制逻辑都在外面。这就像给一辆自动驾驶的车加了一个人类教练------车本身不需要改装,教练坐在副驾上给指令就行。
拆开来说:
状态(State) = 当前推理轨迹 + 剩余 token 预算
这是整个设计的关键------controller 不仅能看到模型已经"想了什么",还能看到"还剩多少额度"。这让它能在资源充裕时让模型仔细想,资源紧张时快速收敛。
动作(Action) = (推理策略, 引导短语)
推理策略是一组预定义的方向,比如 "continue"(继续当前方向)、"summarize"(总结已有内容并推进)、"verify"(验证当前结论)、"pivot"(换一个思路)。引导短语是一段自然语言,注入到 reasoner 的下一步生成中,起"方向盘"的作用。
比如当 controller 判断模型在某条路上绕了太久,它可以输出 ("pivot", "Let me try a different approach:"),强制 reasoner 换个方向。
奖励(Reward) = 最终答案正确性 × token 效率系数
这里的设计很巧妙:奖励不是简单的"答对得 1 分",而是同时考虑了正确性和效率。一个用 5000 token 答对的方案,奖励比用 10000 token 答对的方案高一倍。这让 controller 自然地学会了"用更少的 token 达到同样的效果"。
整个流程我写了个简化版伪代码:
python
class ACTSController:
def __init__(self, reasoner, budget):
self.reasoner = reasoner # 冻结的推理模型
self.budget = budget # token 上限
self.trace = "" # 累积推理轨迹
def step(self):
# 1. 观测当前状态
remaining = self.budget - count_tokens(self.trace)
state = (self.trace, remaining)
# 2. Controller 决策
strategy, phrase = self.controller_policy(state)
# strategy ∈ {"continue", "summarize", "verify", "pivot"}
# phrase = "Let me verify the above step..."
# 3. 将引导短语注入 reasoner 的下一步
next_chunk = self.reasoner.generate(
prefix=self.trace + phrase,
max_new_tokens=512
)
self.trace += phrase + next_chunk
return is_done(self.trace)
def run(self):
while not self.step():
pass
return extract_answer(self.trace)
关键在于:reasoner 是冻结的,不需要任何微调。所有"智能"都在 controller 里。这意味着你可以用同一个 controller 去搭配不同的 reasoner------QwQ、DeepSeek-R1、甚至未来的模型都行。
这种解耦设计在工程上非常友好:reasoner 可以是任何黑盒模型(甚至是你调用的外部 API),controller 只需要能看到推理轨迹和剩余预算就行。你不需要访问模型的内部状态,也不需要修改模型的权重。
为什么这个设计有效
我觉得 ACTS 做对了四件事:
1. 合成引导轨迹构建
没有现成的"带引导的推理数据"怎么办?团队用多 budget 级别(100%、75%、50%、25%)让 reasoner 自由推理,然后在每个 budget 水平上标注最佳的策略切换点,构建出 synthetic steering trajectories。这个数据构造思路本身就很值得借鉴------你不需要人工标注,只需要在不同 budget 下跑一遍,然后用启发式规则标注"在哪里切换策略最好"。
2. Budget-conditioned Reward Shaping
奖励函数不是固定的,而是条件化于剩余预算的。当预算充裕时,奖励偏向准确率;当预算紧张时,奖励偏向效率。这让 controller 学会在"够用就好"和"仔细想想"之间动态切换。直觉上:如果一个问题你已经花了 80% 的预算还没得出结论,那应该赶紧总结一下现有思路给出答案,而不是继续深挖。
3. 策略 + 短语的双层动作空间
不是简单地"继续或停止",而是同时输出一个语义策略和一段自然语言引导。策略决定方向,短语决定执行。这种设计比纯离散动作空间灵活得多------同一个 "verify" 策略,搭配不同的短语可以引导模型做不同类型的验证。比如 "verify" + "Let me check the arithmetic" 和 "verify" + "Let me verify the logical consistency" 会引导模型做完全不同的检查。
4. 跨模型泛化
因为 controller 和 reasoner 解耦,同一个 controller 可以搭配不同的 reasoner,也可以迁移到不同类型的推理任务上。论文里展示的跨模型迁移效果让我印象深刻:在 QwQ 上训练的 controller,直接用在 DeepSeek-R1 上依然有效。
实验数据
论文在多个推理 benchmark 上做了验证(数学、逻辑、代码等),我挑几个关键数据点:
Token 效率 vs 准确率:这是最核心的对比。在 75% token 预算下,ACTS 的准确率和 full-thinking 几乎持平(损失 < 2%),而 naive truncation(直接截断)掉了 8-12 个百分点。差距在低预算下更明显------25% 预算时 ACTS 保持 85%+,truncation 直接跌到 60% 以下。
跨 reasoner 迁移:在 QwQ 上训练的 controller,直接用在 DeepSeek-R1 上依然有效,准确率只掉了 1-2 个百分点。这意味着你不需要为每个模型都训一套 controller。这在实际应用中非常重要,因为模型迭代太快了,如果每换一个模型都要重新训 controller,维护成本会很高。
Budget 灵敏度:同一个问题,给不同预算,controller 会自动调整推理深度。简单题在 50% 预算下就能做对,难题在 100% 预算下才会深入思考。这说明 controller 确实学会了"看菜下饭",而不是一刀切地省 token。
最后一条最让我兴奋------这意味着你不需要为每个模型都训一套 controller。这在实际应用中非常重要,因为模型迭代太快了,如果每换一个模型都要重新训 controller,维护成本会很高。
实际踩坑
说几个论文没明说但实践中大概率会遇到的问题:
-
Controller 本身也有推理开销。虽然它比 reasoner 轻量,但每一步都要做一次前向推理来决策。如果 reasoner 本身的 step 很短(比如简单数学题),controller 的 overhead 可能吃掉你省下的 token。论文里的实验都是比较复杂的问题,简单问题上 ACTS 的收益可能不明显。我在本地测试时发现,对于一步就能出答案的简单算术题,ACTS 反而比直接推理多了 20% 的 token------controller 的决策开销白花了。
-
策略空间的离散化损失。目前只有 4 种预定义策略(continue/summarize/verify/pivot),对于某些需要更细粒度控制的场景可能不够用。比如代码生成任务中,你可能想让模型"先写函数签名再填实现"或者"先写测试再写代码",这种领域特定的推理顺序没法用现有策略表达。后续如果支持自定义策略空间会更好。
-
延迟问题。token 节省 ≠ 延迟节省。每步多一次 controller 推理,在 API 调用场景下意味着更多的 round-trip,实际加速可能没有 paper 里写得那么好看。我在本地跑的时候测了一下,同样的问题,ACTS 节省了 40% 的 token,但 wall-clock time 只快了 15% 左右,因为 controller 的推理本身也有延迟。如果你的场景是 batch 处理(不在乎延迟),收益会更明显。
-
合成轨迹的质量瓶颈。整个训练数据来自"事后标注",如果 reasoner 在低 budget 下本身就表现不好,标注出来的轨迹质量也会有问题。这在小模型上可能是个限制。我在 7B 的模型上试了一下,controller 的效果就不如 32B 上那么明显------小模型的推理本身就不太稳定,controller 很难从中提取出有意义的策略切换点。
-
与现有优化手段的叠加。如果你已经在用 KV cache 压缩、推测解码(speculative decoding)之类的优化,ACTS 的收益需要重新评估。这些技术本身就在减少 token 或加速推理,和 ACTS 叠加后可能没有论文里单独测试时那么好的效果。
跑起来
代码开源在 GitHub(Andree-9/ACTS),基于 PyTorch + transformers:
python
# 安装
git clone https://github.com/Andree-9/ACTS.git
cd ACTS
pip install -r requirements.txt
# 推理示例(伪代码,具体参见 repo README)
from acts import ACTSController, load_reasoner
reasoner = load_reasoner("QwQ-32B")
controller = ACTSController(reasoner, budget=2048)
answer = controller.run("Solve: if f(x) = x^2 + 3x, find f'(2)")
print(answer) # 7
建议直接看 repo 里的 examples/ 目录,有完整的 benchmark 复现脚本。如果你想在自己的项目里试,我的建议是:
- 先从简单的数学推理开始,把 budget 设成你原来 full-thinking 的 50%,看看准确率掉多少
- 如果你的场景是 API 调用(在乎延迟),先测一下 wall-clock time 的变化,别只看 token 数
- 如果你已经在用其他推理优化手段(KV cache 压缩、推测解码等),记得做 ablation,看叠加效果
代码结构比较清晰,核心就两个文件:controller.py(controller 的策略网络)和 environment.py(MDP 环境封装)。如果你想改策略空间,从 environment.py 里的 action_space 入手就行。训练脚本在 train.py,用的是 PPO 算法,代码注释还算清楚,跑起来不难。
总结
ACTS 最大的价值不在于具体省了多少 token,而在于它提供了一个推理过程可控的范式。以前我们只能"给模型足够的 token 让它自由发挥",现在可以"告诉模型每一步该怎么想"。
这是一个很自然的思路转变:推理不应该是一个黑盒,而应该是一个可以被监控和调控的过程。ACTS 用 MDP + RL 把这个思路落了地,虽然还有不少局限,但方向是对的。
从更大的视角看,ACTS 代表了一种"agent 控制 agent"的架构模式------用一个轻量的决策 agent 去调控一个重量级的执行 agent。这种模式在其他领域也有应用(比如用小模型做 routing、用 RL 做 prompt 选择),ACTS 把它用在了推理效率上,效果很直观。随着推理模型越来越大、推理链越来越长,这种外部调控机制会变得越来越重要。
下一步我打算在自己的 RAG 项目里试一下 ACTS 的 controller,看看在实际业务场景下能省多少成本。我的计划是先用 50% budget 跑一轮回归测试,对比一下准确率和 token 消耗的变化。如果你也在推理效率上有踩坑经验,评论区聊聊。