
如果你最近在关注大模型的测试时推理大概率会看到 GradCuit 这样一类关键词信用分配、梯度流、潜在推理。这几个词拼在一起并不只是为了包装一个新概念而是在回答一个非常实际的问题——当模型在测试时没有标准答案它该如何“想”得更稳、更可解释我先说一个自己的观察。很多人在用大模型做多步数学题时都会有类似经历模型给了很长的推导格式工整步骤完整但最后结果错了。你把它拿来当结论会被错题带偏你把它当草稿又觉得至少比没有过程好。这个状态很尴尬我们既不能完全相信它又不知道它错在哪一步更不知道如何只修正那一个错误步骤而不影响其他正确推理。主流方案是让模型多生成几段推理然后多数投票或让模型自评。这种思路有效但也有隐藏成本样本多了计算量上去了离散的文本路径之间没有平滑关系错误路径再多也未必能组合出一个正确答案。更进一步的问题是多数投票只能告诉我们“哪条链出现的次数更多”没有办法告诉我们“为什么这条链更可信”。GradCuit 的方向看起来更底层不在文本层面做推理而是在模型的连续隐空间里做更新不靠随机采样而是靠梯度信号指出方向不只看最终结果而是把每一步贡献分配回各个维度。它的目标是把测试时推理从一个“猜答案”的过程变成一个“像训练一样有归因、有日志、可调试”的过程。1. 测试时推理为什么从“多写几步”变成“隐空间优化”1.1 CoT 的瓶颈离散、不可微、只能靠采样CoT 最大的贡献是让模型的中间推理过程显式化。它把“我该如何思考”这个隐藏判断拆解成若干行文本用户能看到模型按步骤推进。但它的瓶颈在被反复使用后逐渐显现。第一它是离散的。每一步生成一个 token一旦这个 token 落定后续生成就在它的影响下继续。如果某一步产生了错误概念模型通常只能沿着错误概念写下去很难通过“微调”把它拉回来。第二它不可微。训练时我们可以通过下一 token 预测损失来算梯度但测试时我们通常没有标准输出也没有办法给中间步骤一个连续分数。这会直接限制我们使用优化方法去引导推理。第三它的搜索空间呈指数膨胀。想要覆盖多条推理路径只能靠增大采样数量。采样多固然能找到更多可能性但也会引入大量重复路径浪费计算。这三个问题不是 CoT 独有的而是所有离散文本生成方案的共同矛盾。文本是离散的但人类推理过程中的“方向感”是连续的。比如你在估算一道物理题时可能会先感觉“这个量应该变大另一个量应该变小”再通过具体计算验证。那种“感觉”很难被直接写成一个 token却在隐空间中可能只是一个方向向量。1.2 潜在推理在隐空间中走一步而不是在 token 里走一步潜在推理尝试在离散文本层和抽象语义层之间找到另一个操作平面。它不再要求“每一步推理都要变成一句人话”而是允许模型在隐空间中执行若干次更新最后才把结果映射回文本。打个比方你在写一段论证文章时不可能每写一个字都停下来思考逻辑是否严谨更多时候是先让思路在脑子里转几圈形成一个大纲再落笔成文。CoT 类似“边说边想”潜在推理类似“先想后说”。隐空间中的每一次更新并不对应一个具体的词而是对应一种语义方向的偏移。这种偏移可能代表“更关注上一步的计算结果”也可能代表“尝试换一种公式”甚至代表“把问题重新形式化”。GradCuit 要做的就是在这样的潜在推理过程中加入两个此前不太容易同时获得的东西方向和解释。方向来自梯度流。如果我们能构造一个连续评分函数告诉模型当前推理状态离理想答案还有多远那么梯度就能指出“往哪个方向调整隐状态会让分数上升”。这比随机尝试几个方向要高效得多。解释来自信用分配。每次更新不是整体向前推进就完了它要把这个更新量分摊到不同语义维度上标记出哪些维度对结果产生了正面或负面的影响。这样看潜在推理就不只是“换一种生成方式”而是把推理从“编写文本序列”变成“在语义地图上做一次可寻路的搜索”。1.3 GradCuit 的核心判断用梯度流代替随机搜索用信用分配代替黑盒归零GradCuit 的设计可以概括成两句话用梯度流来决定“往哪里走”用信用分配来知道“是谁在起作用”。如果只把测试时推理当成“在隐空间里随机找更好的点”那本质上还是近似搜索只不过换了个空间。随机搜索在低维空间里可行但大模型的隐空间动辄上千维度纯随机采样的效率极低。梯度流把“找更好的点”变成“沿着最陡坡走”减少了无效探索。信用分配则是为了让这个过程可以被人类审查。梯度更新是一个整体但从调试视角看我们需要知道这个整体更新中包含了哪些成分哪些维度对“走向正确答案”贡献最大哪些维度反而在带偏方向。这就是 GradCuit 和普通测试时自适应方法的差别。普通的测试时自适应比如测试时训练一个适配器更多是调整模型去适应某个分布GradCuit 关注的是当前样本自身的推理轨迹更像是一个针对单条样本进行的小步优化。它真正改变的不是“模型变得更聪明”而是“我们多了一份机会在测试阶段看到模型为什么这样推理并且把错误推理当作可修正的路径而不是不可控的结果”。2. 拆解 GradCuit三个关键词就是一条方法论2.1 Credit-Assigned给谁记功为什么不能只看最终损失从常见训练说起。在训练分类模型时交叉熵损失通过反向传播把梯度分布到每一层。每一层都收到一个“该往哪里调”的信号权重更新后整体性能上升。这种梯度回传本身就是一种信用分配。但测试时推理面临的困难和训练不同。训练时你有大量样本和标签损失函数方向稳定。测试时你只有一个样本通常没有标签评分函数可能很粗糙。如果再把这个粗糙分数直接当梯度传回去它会把各种无关因素混在一起。比如隐状态里有维度 A 控制“是否回忆起某个公式”维度 B 控制“是否保持句法流畅”维度 C 控制“是否在答案中使用等号”。如果评分函数只关心最终答案是否正确那么只有触碰正确答案的维度会获得高梯度其他维度的贡献会被忽略或扭曲。单独一个数值的梯度做不到精细归因需要专门做一步把最终分数变化拆解到各个维度。信用分配的意义就在这它把一个总分数“为什么上升了”拆成若干份每一份对应隐空间中的一个方向或一个历史步骤。这样才能回答“模型被更新后究竟是因为更注重某个公式还是仅仅因为调整了表达方式才得到更高分”。我见过很多团队在尝试测试时推理时只记录了最终分数没有记录中间隐状态最后失败时无从下手。GradCuit 强调信用分配本质上也是提醒如果你的优化过程没有留下可归因的中间量那这个优化就只是一次碰运气。2.2 Gradient Flow怎么让迭代更新成为一个可控过程梯度流这个名字看起来高级但底层思想很朴素沿着分数上升最快的方向连续地移动当前参数。你在训练神经网络时做的反向传播就是一种离散化后的梯度流。区别在于GradCuit 里的更新对象不是模型权重而是某个和当前输入相关的隐状态向量。用梯度流的好处很大程度体现在可控性上。因为梯度方向由当前隐状态计算而来每一步都会根据当前推理方向重新调整。如果模型走偏了梯度方向会试图把它拉回分数更高的区域。这个“试图”的过程是连续的而不是在多个离散候选之间跳转因此步长设置得当的话可以避免大幅度震荡。当然梯度流也有自己的前提局部方向可信。在凸函数或局部平滑的曲面上梯度流能快速收敛在崎岖不平的高维曲面上梯度流可能陷入局部最优甚至被噪声梯度带着跑。这也是为什么 GradCuit 这样的方法通常需要配合良好的评分函数和隐状态初始化而不是简单地对几百维向量做几十次梯度更新就能得到稳定答案。实际复现时你不需要真的解微分方程用普通优化器即可。常见选择是 Adam 或带动量的 SGD控制步长在 0.01 到 0.1 左右迭代步数从 5 到 50 不等。每步记录分数如果分数已经进入平台期就可以提前停止。这样就把一次推理变成了一个可控的迭代过程。2.3 Test-Time Latent Reasoning在什么阶段、对什么变量做优化测试时潜在推理有一个很容易混淆的点它到底改了什么它不是改模型权重。模型权重在测试阶段是冻结的。它也不是改 prompt 文本。prompt 本身可能保持不变。它改的是模型内部用于生成当前样本输出的一小段隐状态。可以把它理解为在模型给出最终答案之前先在隐空间中进行若干次“思考”然后把思考后的隐状态作为生成起点。为什么这样可行因为大模型的生成过程并不是一次过。模型内部对输入进行多层编码后得到的 hidden state 已经携带了任务相关信息。如果我们能在这个 hidden state 上做一些有方向的调整那么后续解码出的文本也会随之改变。这不等于模型学会了新知识但相当于模型在推理时“多转了几个念头”。这个阶段很关键。如果我们把它放在模型的第一层输入 embedding 上那近似于修改 prompt如果放在最后一层的 hidden state 上那近似于直接干预输出前的语义。中间层则更像是在改变“思维过程”。GradCuit 在“哪个层做优化”这个问题上应该会和任务高度相关需要实验对比不同层的效果。但要注意潜在推理并不保证每一步隐状态更新都对应一个可解释的文本步骤。有些更新可能只是让模型更集中注意力有些可能代表一个中间量被记住。这没关系信用分配会让我们看到哪些维度在起作用如果没有信用分配这堆高维向量就是一堆无法解读的数字。2.4 为什么这三个词缺一不可如果只用 latent reasoning 而不用 gradient flow你会陷于盲目搜索也许可以用进化策略或贝叶斯优化去找更好的隐状态但很难规模化。如果只用 gradient flow 而不用 credit assignment你能得到一个优化过程但不知道每一步修改背后的语义出了问题很难追溯。如果只用 credit assignment 而不用 gradient flow你能分析模型当前哪里有问题但缺少一个自动修正的手段分析完还得靠人工改 prompt。GradCuit 的价值正在于把三件事连成一个闭环在潜在空间里做迭代用梯度决定迭代方向用信用分配解释每一步迭代。这样你获得的不是一个黑盒优化结果而是一个带轨迹、带归因的推理过程。这是它和大量“测试时采样”方法最本质的区别。3. 如果我想复现这套思路该怎么落地3.1 前置条件需要白盒模型而不是 API 黑盒落地 GradCuit 类思路第一个门槛是你能不能拿到隐状态并反传梯度如果你只是调用云端 API通常只能拿到文本输出或者有限的对数概率。这不够。你至少需要模型在指定层的 hidden state并且能对该状态求梯度。多数开源大模型都支持这个操作但具体启用方式不同。第二个门槛是计算资源。测试时推理需要在每个样本上多做 T 次前向和反向。T 通常大于等于 5。如果模型规模很大比如 70B那就不是普通开发机可以承受的。建议从 7B 或者 13B 模型开始先把流程跑通再考虑规模化。第三个门槛是评分函数。这是最难设计的部分。你需要一个能反映“当前隐状态推理质量”的可微分数。这里没有万能模板只能根据任务定制。下面给一些例子任务类型评分函数思路是否可微数学选择题计算正确选项对应的 softmax 概率可微代码题用单测结果的平滑惩罚或编译错误类型编码需要构造代理分数逻辑推理让模型自己对当前推理状态的置信度打分取 logit可微规划任务用环境奖励的连续近似或目标条件的概率需要设计硬核提醒如果评分函数不可微梯度是零GradCuit 也就无法运行。所以评分函数的设计是整个落地环节中最需要花时间的部分。3.2 最小闭环目标函数、潜变量、梯度、更新把流程拆成四个环节后很多团队会发现最难的是第二步也就是“定义一个可微的目标函数”。但不要因为难就跳过直接在最终分数上求梯度很容易踩坑。更合理的做法是先选择一个小型子问题比如一道确定答案的数学题先把评分函数设计成“正确答案的 token 概率”。不要一开始就做开放任务。在能重建 GradCuit 的最小闭环后再逐步替换成更复杂的任务评分器。落地时我建议记录以下字段当前步数分数值梯度范数更新前后隐向量距离关键维度列表这些字段会构成调试日志的关键部分。没有这些日志你无法判断迭代到底是在优化还是在一遍遍重复同样的错误。注意每次更新时模型权重必须处于冻结状态。你只对隐变量设置requires_gradTrue而不是对所有模型参数更新。如果你不小心解冻了全部模型参数测试时推理就变成了有监督微调会过拟合单条样本失去泛化意义。3.3 一个通用示例用隐向量做多步数学推断为了让你更好理解我写一个概念性的伪代码。它不是论文实现但代表了整个流程的写法。# 概念性伪代码展示测试时潜在推理的基本结构 # 依赖常见深度学习框架模型需支持返回中间层隐状态并允许反向传播 import torch model load_white_box_model() # 例如开源 LLM tokenizer load_tokenizer() def hidden_state_of(input_ids, layer_idx-1): with torch.enable_grad(): output model(input_ids, output_hidden_statesTrue, use_cacheFalse) return output.hidden_states[layer_idx] # shape [batch, seq_len, hidden] def score_function(input_ids, hidden_state): # 把隐状态接入模型计算答案标签的 soft 概率 logits model.compute_logits_with_hidden(input_ids, hidden_state) # 例如用正确选项的 logits 作为分数 return logits[0, correct_option_id].sum() def credit_assignment(trajectory, gradients): # 将每步梯度按维度累计得到每个隐维度的信用分 credit torch.zeros_like(trajectory[0]) for grad in gradients: credit grad.square() # 一种简化做法 return credit batch_size 1 input_ids tokenizer(题目和学生部分推理, return_tensorspt).input_ids h hidden_state_of(input_ids).detach().requires_grad_(True) trajectory [] gradient_log [] for step in range(20): score score_function(input_ids, h) grad torch.autograd.grad(score, h)[0] grad torch.nan_to_num(grad) trajectory.append(h.detach()) gradient_log.append(grad.detach()) h h.clone() 0.05 * grad # 沿“分数上升”方向更新 credit credit_assignment(trajectory, gradient_log) interpretable_credit interpret_credit(credit, tokenizer) answer model.generate_from_hidden(input_ids, h) print(answer) print(interpretable_credit)这段代码的重点不是能不能直接跑通而是让你看到你每步记录什么后面就能解释什么。没有记录的中间状态就没有信用分配的素材。代码里的score_function只是一个“怎么把隐状态变成分数”的占位符实际落地时你可能需要用模型当前的 logits 做一层变换而不是直接