Least-to-Most提示法由Denny Zhou等人提出,用于增强大语言模型在组合泛化类任务上的推理能力。标准思维链虽然能通过中间步骤改善推理,但当任务需要分阶段解决并且子问题彼此依赖时,单次生成的推理链容易在长距离依赖上出错。Least-to-Most把提示过程拆成两个阶段:先用一个提示让模型分解问题,再按由易到难的顺序逐级求解,并把已获得的答案放回上下文。这种做法降低了单次推理的复杂度,也能让模型复用前面得到的中间结果。

任务分解与上下文堆叠如何协同工作
Least-to-Most的关键在于任务分解。面对一个复杂问题P,模型先被要求生成一个子问题序列p1、p2直到pn,其中p1最简单,pn最接近原问题。子问题之间的依赖关系通常是从简单到复杂,后一个子问题可以引用前一个子问题的答案。例如在符号操作任务中,原始指令是“把红色球放到绿色箱子里”,模型可以先分解出“识别红色球”“识别绿色箱子”“执行移动操作”等子问题。这种分解方式与人类解决复杂问题的思路接近,也方便后续对每一小步进行验证。
上下文堆叠则负责把已经解决的子问题答案融入后续提示。在求解第i个子问题时,提示中不仅包含原始问题描述,还包含前i-1个子问题及其答案。这样模型不必在一次推理中同时处理所有细节,而是借助逐步丰富的工作记忆来推进。实现时常见的做法是维护一个答案列表,每完成一个子问题就把问题和答案拼接到上下文中,再继续下一个。上下文堆叠还能帮助模型保持状态一致性,因为每次都显式看到了历史推理结果。
两者协同后,模型从“一次性生成完整思路”转变为“多轮交互式求解”。这种变化对长链条、强依赖的任务尤其有用。一个有趣的实验来自SCAN任务,模型需要把自然语言指令映射为动作序列,比如“jump twice”对应“JUMP JUMP”。标准CoT在训练分布之外的组合上表现一般,而Least-to-Most通过先学习基础动作,再组合修饰语,大幅提升了泛化能力。任务分解质量直接决定后续求解的天花板,因此提示中往往需要给出分解示例,帮助模型稳定输出子问题列表。
与标准思维链的差异:动态分解带来的优势与代价
标准思维链通常在单个提示中要求模型“一步一步思考”,模型先生成推理文本,再给出最终答案。推理过程是单次生成的,中间步骤之间没有显式的任务边界,也不会在步骤之间插入外部求解结果。对于结构相对线性的推理任务,如简单算术、常识推理,这种方法是有效的。但当问题可以拆成多个独立或递进的子问题时,单次生成长推理链容易因为上下文窗口有限、注意力分散或中间步骤出错而失败。
Least-to-Most与CoT最本质的区别在于“分解”和“逐级求解”是显式分离的。它不是让模型在一条链里完成所有事情,而是把推理拆成多次调用。每次调用解决一个更小的子问题,前一次的结果作为条件输入。这样做的第一个好处是大幅降低单次调用的推理负担;第二个好处是如果某个子问题答错,可以单独重新求解该子问题,而不必重来整条链。代价则包括更多API调用、更高的延迟与成本,以及需要额外设计分解提示。
从错误模式看,CoT在组合泛化任务上常出现“中间步骤漂移”,例如前一步已经计算出A,但后一步却使用B的结果。Least-to-Most通过把历史答案显式放入提示,减少了这种漂移。它也更适合那些训练数据中没见过的组合情况,因为分解后的基础子任务往往是模型已经掌握的技能,重新组合起来比直接应对陌生复合问题更容易。当然,如果分解阶段本身就产生了错误或遗漏,后续步骤也会被带偏,因此需要配合验证机制或多次采样。
实现Least-to-Most提示的代码思路
在工程实现上,Least-to-Most可以看作一个提示编排流程。首先需要一个分解提示,让模型把原问题拆成子问题。其次需要一个循环,从第一个子问题开始逐个求解,并把问答对追加到上下文。下面给出一段Python伪代码,展示如何调用大模型接口实现该流程。为了突出重点,这里忽略异常处理和重试逻辑,实际项目中建议加入答案校验与采样。
import openai
def call_llm(messages):
resp = openai.ChatCompletion.create(
model="gpt-4",
messages=messages,
temperature=0
)
return resp["choices"][0]["message"]["content"].strip()
def least_to_most(original_question):
# 第一阶段:让模型分解原问题
decompose_prompt = f"请把下面的问题分解成若干个由易到难的子问题,按顺序列出:\n{original_question}"
decompose_messages = [{"role": "user", "content": decompose_prompt}]
sub_questions_text = call_llm(decompose_messages)
sub_questions = [line.strip("- ").strip() for line in sub_questions_text.splitlines() if line.strip()]
context = f"原始问题:{original_question}\n"
answers = []
# 第二阶段:从简单到复杂逐级求解
for i, sub_q in enumerate(sub_questions):
prompt = (
f"{context}\n"
f"已知前序结果:{answers}\n"
f"当前需要解决的子问题:{sub_q}\n"
f"请给出该子问题的答案,并简要说明推理。"
)
messages = [{"role": "user", "content": prompt}]
answer = call_llm(messages)
answers.append({"子问题": sub_q, "答案": answer})
context += f"子问题{i+1}:{sub_q}\n答案:{answer}\n"
# 最终基于所有子问题答案求解原始问题
final_prompt = (
f"现在综合以下子问题及其答案,回答原始问题。\n"
f"{context}\n"
f"原始问题:{original_question}\n"
f"最终答案:"
)
final_messages = [{"role": "user", "content": final_prompt}]
final_answer = call_llm(final_messages)
return final_answer, answers
除自动分解外,也可以手工定义子问题序列。手工方式适合任务结构固定、子问题明确的场景,比如数据库查询生成中先提取实体、再生成条件、最后组合SQL。自动方式更通用,但需要给模型提供分解示例,必要时训练一个专门的分解器。还可以在每轮求解前加入验证步骤,比如让模型判断子问题答案是否合理,不合理则重新生成。这些工程化设计能显著提升最终答案的可靠性。
典型应用场景与需要注意的局限
Least-to-Most适合那些可以被分解为递进子任务的场景。数学应用题是典型代表:先求解一个中间量,再用它计算最终结果。多跳问答也属于此类,比如“谁导演了获得2020年奥斯卡最佳影片的电影?”需要先找到电影,再找导演。代码生成中,可以把大型函数拆成多个子函数或步骤描述,逐段生成。任务规划、符号操作、逻辑推理等同样受益。
优势方面,Least-to-Most无需对模型进行微调,只需通过提示工程调整推理流程,即可在组合泛化任务上获得明显提升。由于每一个子问题都有独立的推理和答案,整个推理过程更透明,方便定位错误环节。与标准CoT相比,它对长链推理更友好,并且可以复用已经验证过的子问题答案,降低重复计算。
局限也不能忽视。第一,分解质量决定最终效果,如果子问题划分得太粗或太细,都可能降低性能;分解出错时,后续步骤会被连锁影响。第二,错误累积问题:前一个子问题的错误答案会作为条件进入后一个子问题,导致最终答案偏差。第三,调用次数增加带来更高的延迟、成本和复杂度,不适合对实时性要求极高的场景。最后,某些强隐式推理任务未必能自然拆成显式子问题,这时Least-to-Most的优势会打折扣。因此实践中可以将它与CoT结合起来:对容易分解的任务使用Least-to-Most,对线性推理任务保留标准CoT。
思维链Least-to-Most提示法大模型推理修改时间:2026-09-18 09:44:09