当前大模型推理的热点已经从“模型更大”转向“同样的模型怎么把推理做得更深”。这次我们来看一个值得关注的方向:Chained Recursive Language Models for Multi-Iteration Reasoning。
简单说,这类方法不是换更大的基座模型,而是让同一个语言模型在推理过程中反复“自我提问、自我回答、自我修正”,通过链式递归结构把一次性的弱推理扩展成多轮迭代的强推理。它解决的核心问题是:单次前向推理容易漏细节、跳步骤、产生幻觉,而多轮迭代可以让模型在上下文中看到自己的中间结果,再基于这些中间结果继续推理。
这个方向最值得关注的特点有三个。第一,不要求额外训练模型,多数实现可以直接套用现有 LLM 的接口完成递归调用;第二,显存门槛取决于底层模型,而不是“递归层数”,因为递归过程本质上是多次前向推理,不增加单次推理的显存峰值;第三,它天然适合拆成批量任务,每一轮递归都可以设计成独立请求,方便做并行、队列和失败重试。
这篇文章会介绍链式递归语言模型的核心思路,拆解多轮迭代推理的实现框架,给出一个可以本地运行的实验 Demo,然后覆盖环境准备、功能测试、接口集成、资源占用和排查方法。如果你正在做 Agent、复杂问答、数学推理、代码生成这类需要深度思考的任务,这篇文章可以直接参考。
1. 核心能力速览
| 能力项 | 说明 |
|---|---|
| 项目方向 | 链式递归语言模型,面向多轮迭代推理 |
| 核心思路 | 将 LLM 推理过程组织成链式递归结构,前一轮输出作为后一轮输入 |
| 主要功能 | 自纠正推理、多轮反思、复杂任务分解、推理结果验证 |
| 底层依赖 | 任意可调用的 LLM(本地模型或 API 模型) |
| 显存需求 | 取决于底层 LLM,递归本身不额外占用显存 |
| 支持平台 | 支持 Python 3.8+ 的 Linux / Windows / macOS |
| 启动方式 | Python 脚本 / FastAPI 服务 / 命令行批处理 |
| 是否支持 API | 可以封装为 HTTP 接口 |
| 是否支持批量任务 | 支持,天然适合队列化处理 |
| 适合场景 | 数学推理、逻辑问答、代码调试、Agent 规划、长链路任务 |
需要说明的是,链式递归是一个方法框架,不是某一个固定模型。不同论文、不同开源实现会有各自的递归策略和终止条件,核心差别在于“递归的触发时机”和“终止条件设计”。
2. 方法原理拆解:什么是链式递归多轮推理
理解这个方向,先拆三个关键词:链式、递归、多轮迭代。
2.1 链式
链式指的是推理过程不是一次完成,而是把最终回答拆成多个连续节点。每个节点只负责一次局部推理,节点之间以文本上下文串联。这个结构类似于思维链(Chain-of-Thought),但区别在于:思维链通常是一次性生成全部中间的思考过程,而链式递归是分多次调用模型,每一步都可能带着上一轮的完整结果继续。
链式结构的好处是每一轮模型的注意力窗口只需要关注当前上下文和上一轮结果,不需要在单次输出中规划过长的推理链路,降低了长程推理的出错概率。
2.2 递归
递归是这个方向的核心。它的意思是模型在推理过程中会调用“自己”来处理子问题,或者处理自己上一轮生成的结果。
常见的递归形式有两种:
- 自递归:模型对同一个问题反复执行“生成答案 - 评估答案 - 生成修正”的循环。
- 任务分解递归:模型把一个复杂问题拆成子问题,对每个子问题递归调用自身求解,再汇总结果。
在工程实现上,递归调用不一定要在代码层面真正调用同一个函数,而是通过循环调用 LLM 接口来模拟。每次调用 LLM 时,把上一轮的输出作为本轮对话的上下文传入。
2.3 多轮迭代
多轮迭代指的是模型有多次机会改进自己的答案。实验表明,在代码生成、数学推理任务中,允许模型看到自己上一轮的失败输出并给出反思,通常比一次生成要稳定。
多轮迭代过程中的两个关键设计:
- 迭代轮数:固定轮数还是动态终止。
- 反馈信号:模型自己评估,还是引入外部工具(如代码执行器、计算器)作为反馈。
动态终止条件通常更实用。当模型判定当前答案“已经满足要求”或“不再改进”时,就提前终止推理,节省 token 消耗。
3. 与主流推理增强方法的差异
| 方法 | 是否需要训练 | 推理方式 | token 开销 | 主要优点 | 主要限制 |
|---|---|---|---|---|---|
| 标准 Prompt(少样本) | 否 | 单次生成 | 低 | 简单直接 | 复杂推理容易出错 |
| 思维链 CoT | 否 | 单次生成中间步骤 | 中 | 引导模型逐步推理 | 无法自我纠正 |
| Self-Consistency | 否 | 多次采样取投票 | 高 | 提升稳定性 | 没有利用反馈信息 |
| Self-Refine | 否 | 生成 - 反馈 - 修正 | 较高 | 可通过反馈改进 | 反馈依赖模型自评质量 |
| Chained Recursive LM | 否 | 递归调用 + 多轮迭代 | 取决于轮数 | 可动态终止,可结合外部工具 | 递归策略设计有门槛 |
从表格可以看出,链式递归语言模型的优势不在“理论基础多新颖”,而在工程上更灵活:不需要训练,不增加显卡负担,只需要设计好递归策略和终止条件。
4. 环境准备与前置条件
链式递归语言模型的环境要求主要由底层 LLM 决定。如果使用本地模型,需要准备 GPU 推理环境;如果使用 API 模型,只需要 Python 环境和网络连接。
4.1 基础环境清单
| 项目 | 要求 |
|---|---|
| 操作系统 | Linux 优先,Windows/macOS 也可以 |
| Python 版本 | 3.8 及以上 |
| 模型接口 | OpenAI 兼容接口或本地推理服务 |
| 依赖库 | requests / openai / fastapi / uvicorn |
| GPU(可选) | 显存取决于模型大小,以本地模型量化版本为准 |
4.2 环境检查
建议先确认 Python 环境:
python --version pip --version然后安装依赖:
pip install requests openai fastapi uvicorn如果使用本地模型,参考以下两种常见方案:
- 使用 vLLM 或 LMDeploy 启动 OpenAI 兼容接口,显存需求以模型量化配置为准。
- 使用 llama.cpp 系列的 GGUF 量化模型,可以在较低显存下运行。
更稳妥的判断:先跑通底层模型,确认可以正常返回结果,再叠加递归框架。递归框架本身不负责模型推理,只负责组织多次调用。
5. 链式递归推理框架实现
这里给出一套通用的链式递归推理框架,不依赖特定模型接口。你只需要替换base_url和api_key,就可以接入本地模型或 API 模型。
5.1 递归调用核心函数
import json import time from typing import Callable, Optional def call_llm(messages, base_url, api_key, model_name, max_tokens=1024): """ 调用 OpenAI 兼容的 LLM 接口。 实际使用时需要按你的服务地址和模型名调整。 """ import requests url = f"{base_url}/chat/completions" headers = { "Authorization": f"Bearer {api_key}", "Content-Type": "application/json" } payload = { "model": model_name, "messages": messages, "max_tokens": max_tokens, "temperature": 0.3 } response = requests.post(url, headers=headers, json=payload, timeout=120) response.raise_for_status() data = response.json() return data["choices"][0]["message"]["content"]这个函数是最底层的基础设施。链式递归的所有后续逻辑都建立在call_llm之上。
5.2 递归推理主循环
主循环的设计思路是:模型生成答案,然后生成一个评估结果,根据评估结果决定继续修正还是终止。
def recursive_reasoning( question: str, base_url: str, api_key: str, model_name: str, max_iterations: int = 5, call_llm_func: Callable = call_llm, verbose: bool = True, ): """ 链式递归推理主循环。 - question: 待解决的问题 - max_iterations: 最大迭代轮数 - call_llm_func: LLM 调用函数 """ system_prompt = ( "你是一个严谨的推理引擎。你需要逐步分析问题," "并在每一步判断当前答案是否完整、是否正确。" "如果发现问题,请给出修正后的答案。" ) # 初始化对话上下文 messages = [ {"role": "system", "content": system_prompt}, {"role": "user", "content": question} ] current_answer = None for iteration in range(1, max_iterations + 1): if verbose: print(f"\n===== Iteration {iteration} =====") # 第一步:生成当前答案 prompt_with_context = messages + [ {"role": "user", "content": "请基于当前所有信息,给出最新答案。"} ] current_answer = call_llm_func( prompt_with_context, base_url=base_url, api_key=api_key, model_name=model_name, ) if verbose: print(f"[Answer {iteration}] {current_answer}") # 第二步:让模型自评估 evaluation_prompt = ( "请检查上面生成的答案。" "如果答案已经正确、完整且无需修改,请只输出:@@DONE@@。" "如果答案存在问题,请描述问题,并要求模型重新推理。" ) eval_messages = prompt_with_context + [ {"role": "assistant", "content": current_answer}, {"role": "user", "content": evaluation_prompt} ] evaluation = call_llm_func( eval_messages, base_url=base_url, api_key=api_key, model_name=model_name, max_tokens=512, ) if verbose: print(f"[Evaluation {iteration}] {evaluation}") # 第三步:如果自评估认为完成,则终止递归 if "@@DONE@@" in evaluation: if verbose: print("=== 模型判定完成,递归终止 ===") break # 否则将评估结果加入上下文,进行下一轮修正 messages = eval_messages + [ {"role": "assistant", "content": evaluation} ] time.sleep(0.5) return current_answer这个实现体现了链式递归的核心:每一轮评估结果都会作为下一轮的上下文输入,模型在后续推理中可以参考自己之前生成的答案和评估意见,从而逐步修正。
5.3 终止条件设计
上述代码中使用的是“模型自评终止”,即让模型输出@@DONE@@表示已经完成。更稳妥的做法是同时结合两个条件:
- 达到最大轮数上限,防止死循环。
- 模型输出包含显式的完成标记。
如果模型自评质量不稳定,可以引入更硬性的终止条件。例如,在代码生成任务中,实际执行生成的代码,根据运行结果判断是否终止。在数学推理任务中,可以引入计算器验证中间步骤。
判断终止条件是否合理,可以看两个指标:平均迭代轮数是否远低于上限、最终答案的正确率是否随轮数收敛。
6. 效果验证评测维度
链式递归的效果验证,需要区分“方法本身的增益”和“底层模型的基线能力”。建议先跑一组对比实验,再分析递归带来的增量。
6.1 测试数据集设计
可以从这几个维度准备测试数据:
| 维度 | 示例任务 | 测试目的 |
|---|---|---|
| 数学推理 | 鸡兔同笼、概率计算、代数方程 | 验证多步计算正确性 |
| 逻辑推理 | 真假命题、条件推理 | 验证逻辑链完整性 |
| 代码生成 | 写一个排序函数、修复一个有 bug 的函数 | 验证可执行性和自纠正能力 |
| 事实问答 | 多跳问答 | 验证信息整合能力 |
| 长链路规划 | 制定一个多步骤任务计划 | 验证长文本组织能力 |
6.2 对比实验配置
推荐至少对比三组配置:
- 基线:单次生成,不进行递归。
- 固定轮数递归:统一递归 3 轮。
- 动态终止递归:使用自评估终止条件。
每组配置需要记录:
- 正确率或任务成功率。
- 平均生成的 token 总数。
- 平均迭代轮数。
- 总耗时。
6.3 判断标准
一个合理的判断标准是:动态终止递归在正确率不低于固定轮数递归的前提下,平均迭代轮数更少,token 消耗更低。如果动态终止频繁提前结束且正确率明显下降,说明终止条件过于宽松,需要调整评估 prompt。
注意:链式递归不是所有任务都有效。如果基线任务本身很简单,单次生成正确率已经达到 90% 以上,递归带来的提升有限,反而增加延迟。建议先用中高难度任务验证效果。
7. 接口 API 与批量任务
链式递归推理在实际落地时,通常要封装成 API 服务,并支持批量任务处理。
7.1 使用 FastAPI 封装推理接口
from fastapi import FastAPI from pydantic import BaseModel, Field app = FastAPI() class ReasonRequest(BaseModel): question: str = Field(..., description="待推理的问题") max_iterations: int = Field(5, description="最大递归轮数") base_url: str = Field(..., description="LLM 服务地址") api_key: str = Field("EMPTY", description="API Key") model_name: str = Field(..., description="模型名称") class ReasonResponse(BaseModel): answer: str iterations: int finished_properly: bool @app.post("/reason", response_model=ReasonResponse) def reason_endpoint(request: ReasonRequest): # 在实际实现中,可将 call_llm_func 替换为对应客户端 answer = recursive_reasoning( question=request.question, base_url=request.base_url, api_key=request.api_key, model_name=request.model_name, max_iterations=request.max_iterations, verbose=False, ) return ReasonResponse( answer=answer, iterations=request.max_iterations, finished_properly=True, )启动服务:
uvicorn api_server:app --host 0.0.0.0 --port 80007.2 批量任务设计
批量任务的通用思路是:准备一个任务文件,每行一个待处理问题,逐条调用推理接口,收集结果到输出文件。
python batch_reasoning.py \ --input tasks.jsonl \ --output results.jsonl \ --base_url http://127.0.0.1:8000这里给一个批量调用 Python 示例框架:
import json import requests import time def process_batch(input_path: str, output_path: str, endpoint: str): with open(input_path, "r", encoding="utf-8") as f: tasks = [json.loads(line) for line in f if line.strip()] results = [] for idx, task in enumerate(tasks): print(f"Processing {idx + 1}/{len(tasks)}: {task['id']}") payload = { "question": task["question"], "max_iterations": task.get("max_iterations", 5), "base_url": task["base_url"], "api_key": task.get("api_key", "EMPTY"), "model_name": task["model_name"], } try: response = requests.post(endpoint, json=payload, timeout=300) response.raise_for_status() data = response.json() results.append({ "id": task["id"], "answer": data["answer"], "iterations": data["iterations"], }) except Exception as e: results.append({ "id": task["id"], "error": str(e), }) time.sleep(0.2) with open(output_path, "w", encoding="utf-8") as f: for result in results: f.write(json.dumps(result, ensure_ascii=False) + "\n") if __name__ == "__main__": process_batch( input_path="tasks.jsonl", output_path="results.jsonl", endpoint="http://127.0.0.1:8000/reason" )批量任务建议做好三件事:任务 ID 去重、失败重试、断点续跑。最简单的断点续跑方式就是最终写入时只追加不覆盖,或者定期把已完成的 task ID 写入单独的进度文件。
7.3 调用示例数据
输入文件tasks.jsonl示例:
{"id": "001", "question": "一个笼子里有鸡和兔共35个头,94只脚,问鸡和兔各多少只?", "max_iterations": 5, "base_url": "http://127.0.0.1:8000", "model_name": "your-model-name"} {"id": "002", "question": "写一个Python函数,判断一个字符串是否为回文。", "max_iterations": 3, "base_url": "http://127.0.0.1:8000", "model_name": "your-model-name"}注意:base_url、model_name、api_key这些参数需要根据实际运行的推理服务替换。代码中的EMPTY是本地推理服务常见默认值,如果使用云端 API,需要换成真实有效值。
8. 资源占用与性能观察
链式递归推理的显存占用和普通 LLM 推理保持一致,不会因为递归轮数增加而提高显存峰值。真正影响资源的是请求频率和并发数量。
8.1 显存观察方法
启动本地模型后,使用nvidia-smi观察显存占用:
watch -n 1 nvidia-smi在递归推理过程中,显存占用主要发生在模型前向计算阶段。如果使用的是 OpenAI 兼容 API,显存占用会发生在服务端,客户端只消耗网络和 CPU 资源。
8.2 CPU 推理与 GPU 推理差异
- GPU 推理:单轮生成速度快,递归轮数增加带来的延迟主要在语义上累积,而不是硬件瓶颈。
- CPU 推理:单轮生成速度慢,多轮迭代的延迟叠加会非常明显,建议先在小规模测试集上评估耗时,再决定是否适合生产。
8.3 降低 token 消耗的方法
链式递归的 token 消耗是单次生成的数倍,如果不做控制容易失控。三种常用策略:
- 设置合理的
max_iterations,避免无效循环。 - 在评估 prompt 中要求模型“只输出评估结论,不重复答案”,减少上下文膨胀。
- 使用动态终止条件,让模型在不再改进时提前结束。
8.4 性能优化清单
| 优化项 | 操作 | 预期效果 |
|---|---|---|
| 减少上下文冗余 | 每轮只保留必要的评估结论 | 降低 token 消耗 |
| 限制最大轮数 | 根据任务难度设置 2-5 轮 | 防止死循环 |
| 使用流式输出 | 如果接口支持,可以边生成边返回 | 改善用户体验 |
| 并发请求 | 对彼此独立的子任务使用线程池 | 提升吞吐 |
| 结果缓存 | 相同问题直接返回历史结果 | 减少重复计算 |
9. 常见问题与排查方法
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 递归一直不终止 | 终止条件设计过严,模型不输出完成标记 | 打印每一轮评估结果,检查模型输出模式 | 降低终止标记要求,或加入最大轮数硬限制 |
| 显存不足 | 底层模型过大或并发请求过多 | 观察 nvidia-smi 显存占用 | 换更小的量化版本模型,降低并发数 |
| API 返回超时 | 单次推理时间过长或网络不稳定 | 查看接口日志和调用耗时 | 增加 timeout,或拆分子任务 |
| 上下文长度超过限制 | 递归轮数过多导致上下文膨胀 | 检查报错信息中的 token 数量 | 限制轮数,压缩上下文,只保留关键信息 |
| 自评估质量差 | 评估 prompt 不明确,模型无法准确判断 | 对比单次生成结果和多轮结果 | 改进评估 prompt,或引入外部验证工具 |
| 批量任务中途失败 | 网络抖动或服务重启 | 查看输出文件,定位失败任务 ID | 增加重试机制和断点续跑 |
| 端口冲突 | 多个服务占用同一端口 | 检查端口监听 | 更换端口启动 |
9.1 模型自评不可靠时的替代方案
如果模型自评结果经常与真实质量不一致,可以引入外部器验证。比如:
- 代码生成:执行生成的代码,根据运行结果判断正确性。
- 数学计算:把数值表达式交给计算器模块计算,判断结果是否一致。
- 事实问答:结合检索结果判断答案是否与上下文匹配。
外部验证比模型自评更硬,但只适用于可以自动评估的任务类型。
10. 最佳实践与使用建议
综合来看,链式递归语言模型的多轮迭代推理是一个“门槛低、上限看策略”的方法。以下几条是实践中比较容易踩坑也最容易提升效果的经验。
10.1 第一个验证的应该是什么
先不要在复杂任务上贪多,用一个中间难度的数学题或逻辑题做对比实验,分别跑一次单次生成和一次递归推理,观察递归是否真的带来改进。如果两步跑下来没有明显差异,说明问题过于简单,换更难的任务。
10.2 模型选择策略
链式递归效果好坏的瓶颈在于模型的“自我反思能力”。如果底层模型本身逻辑性较弱,递归轮数再多也只是重复错误。建议:
- 轻量模型:控制轮数,最多 2-3 轮。
- 中等及以上能力模型:可以尝试动态终止和工具验证。
10.3 工程化落地建议
把链式递归当作一个独立的推理服务使用,而不是和业务代码强耦合。这样模型调整、轮数调整、prompt 调整都不会影响上游逻辑。
从安全边界角度,还要注意:如果处理的问题涉及真实人脸、个人数据或受版权保护的素材,需要先在授权范围内使用,并明确推理结果不用于规避平台规则或侵犯他人权益。递归调用模型对长文档进行多轮分析时,确保文档来源合法,不擅自传播未经授权的敏感信息。
10.4 项目目录规范
建议为实验项目建立清晰的目录:
reasoning-lab/ ├── data/ │ ├── input/ # 推理任务输入 │ └── output/ # 推理结果输出 ├── logs/ # 运行日志 ├── prompts/ # 各阶段的 prompt 配置 ├── src/ │ ├── llm_client.py # LLM 调用封装 │ ├── recursive.py # 递归推理逻辑 │ └── batch.py # 批量任务逻辑 └── config.yaml # 模型地址、轮数、路径等配置11. 总结
链式递归语言模型的价值在于把“一次生成”变成“多轮迭代”,并且不需要额外训练模型,不需要升级显卡,只需要在推理架构层面加入递归循环和终止策略。
值得先试点用的场景是中等以上难度的逻辑推理和代码生成。最容易踩的坑是自评估不可靠导致递归要么不终止,要么重复劳动。后续值得扩展的方向包括:结合外部工具做硬性验证、针对不同任务自动调节递归轮数、把多轮推理的中间过程缓存下来做训练数据。
如果你已经在部署本地模型或者接入 API 模型,建议直接拿上面的递归框架跑一组对比实验。能明显提升正确率,就值得继续优化终止条件;如果提升不明显,优先检查底层模型能力和 prompt 设计,而不是增加递归轮数。