复杂任务里单次调用语言模型往往只适合完成一步推理。真正需要连续判断、自我修正和逐步收敛的场景比如多步数学计算、信息抽取、代码走查和规划拆解只靠一次生成很难稳定得到最终结论。Chained Recursive Language Models for Multi-Iteration Reasoning 这类思路就是把语言模型放进一个可重复执行的循环中让模型基于上一轮输出继续推理而不是一次性给出最终结果。这篇文章不纠结于某个版本或某次实验结果而是把这个标题背后的策略当作一个可落地的推理模式来拆解并给出一个最小可运行的 Python 示例。如果你想在项目里加入“多轮自纠正”“链式推理”“递归迭代”能力可以先从本文的框架入手。它会覆盖核心概念、调用链路、输出格式、停止条件、校验逻辑、常见问题和生产化建议。读完以后你可以把一个简单的多迭代推理循环接到任意 OpenAI 兼容接口上也可以继续扩展成工具调用或多智能体协作。1. 先理解 Chained Recursive Language Models 是什么1.1 从单次推理到链式递归推理可以把单次调用理解成“一锤子买卖”。模型收到问题后只能根据上下文窗口里的信息生成一段回答。问题足够简单的时候这样做没有问题但问题需要多步处理时模型容易在第一步出错而且没有机会在后续步骤里修正。链式递归推理则不同。它不再是“一次生成完就结束”而是把同一个任务反复喂给模型模型先输出一个中间结果系统检查这个结果是否满足约束如果不满足就把检查信息作为反馈拼回提示词再让模型继续生成。这个循环可以重复多次直到结果通过校验或者达到最大迭代次数。从技术实现上看链式递归语言模型具备三个特征同一个模型实例在循环中被多次调用。后一次调用的输入中包含前一次调用的输出、检查结果或中间状态。循环必须有一个显式的终止条件否则任务会一直执行下去。第一点是“递归/链式”的体现。第二点是“多迭代”的核心价值每一步推理都能看到当前进展和错误反馈。第三点是工程稳定性的前提也是很多人最容易忽略的部分。1.2 多迭代推理要解决的本质问题多迭代推理要解决的本质问题是“单次生成无法完成需要逐步收敛的任务”。举个例子给定一段包含多个数字的文本让模型一次性输出所有整数的和。模型可能漏掉某个隐藏的负数可能在求和时算错也可能把小数当作整数。单次调用没有机会重新检查一旦出错就得重新生成一整段回答。使用多迭代推理后系统可以这样做模型先提取数字并计算和。一个确定性的校验器用正则表达式重新扫描文本对比模型提取的数字列表。如果两边不一致校验器告诉模型“你漏了数字 -2请重新提取。”模型在下一轮修正输出。校验通过后循环终止。这里的校验器不依赖语言模型所以它能提供稳定、可复现的反馈。语言模型负责理解语义和生成推理过程校验器负责事实性校验这种分工比让模型自己判断“对不对”更可靠。1.3 与 CoT、ReAct、Self-Consistency 的边界和 Chain-of-ThoughtCoT相比链式递归更强调“多次调用”而不是“一次性写出推理链”。CoT 是在一次生成中让模型逐步思考链式递归则是把多轮生成结果拼接成一条不断修正的推理链。和 ReAct 相比链式递归不一定依赖外部工具。ReAct 的核心是“思考 - 行动 - 观察”链式递归可以只做内部自我修正模型的输出就是行动确定性校验器的结果就是观察。当然你也可以在链式递归中加入工具调用让它更接近 ReAct。和 Self-Consistency 相比Self-Consistency 是多次独立采样再用投票选出答案链式递归是多次相关采样每一步都依赖上一步的结果。两者可以结合但不应该混为一谈。注意递归不是无限循环的代名词。在实现时最大迭代次数、超时时间和上下文长度上限必须写死在系统里否则一次错误的模型输出可能让请求一直重试直到资源耗尽。2. 设计一个可复现的多迭代推理循环2.1 核心模块与整体调用链路实现一个链式递归推理系统至少需要四个模块推理引擎、输出解析器、校验器和循环控制器。推理引擎负责调用语言模型输出解析器负责把模型返回的文本转成结构化对象校验器负责判断当前结果是否正确循环控制器负责拼接历史消息、判断是否继续迭代并执行递归调用。整体的调用链路可以用下面这个有序步骤表示接收用户任务文本。初始化消息列表放入系统提示词和用户任务。调用语言模型接口得到原始输出。解析输出为 JSON 或其他结构化格式。使用校验器检查结果。如果校验通过返回最终答案。如果校验不通过把校验反馈追加到消息列表回到第 3 步。如果达到最大迭代次数返回最后一次结果并附带 warning。这个链路的核心是“消息列表不断增长”。每一轮模型输出都被保存为 assistant 消息每一轮校验反馈都被保存为 user 消息。这样模型才能在下一轮看到上一轮自己说了什么、系统为什么判定它错了。2.2 输出格式约定为什么用 JSON递归推理最难处理的问题之一是“模型输出的结果无法稳定解析”。如果模型自由输出一大段话系统很难判断它是否完成、中间结果是什么。所以第一版实现最稳妥的做法是约定一个 JSON 输出格式。下面是一个示例{ thinking: 模型当前认为的推理过程, extracted_numbers: [-2, 3, 5, 12], sum: 18, finished: true }使用 JSON 有两个好处一是系统能稳定提取关键字段二是模型天然适合按固定格式生成内容。缺点是部分模型对 JSON 格式的跟随能力较弱这时候需要在提示词里反复强调“只输出 JSON不要输出其他文本”并在代码里做解析兜底。如果底层接口支持response_format可以在请求里加上response_format{type: json_object}能显著降低解析失败率。如果模型不支持也不要慌张可以靠后处理从文本中用正则提取 JSON 块。2.3 停止条件与最大迭代次数递归推理必须设计停止条件。推荐按优先级组合使用以下几种停止条件说明推荐设置校验通过结果满足确定性规则强烈建议使用模型声明完成模型返回finishedtrue可作为辅助条件最大迭代次数防止无限循环学习环境 3~5生产环境按成本设计上下文窗口上限防止 Token 超限预留 token 余量通常不超过窗口的 70%请求超时或异常防止调用卡死单次请求 30 秒整轮 120 秒这里特别要注意“模型声明完成”不可单独作为终止条件。模型可能认为它算完了但结果其实是错的。更好的做法是把“模型声明完成”和“校验通过”同时作为必要条件。3. 用 Python 实现一个最小示例3.1 环境准备与依赖下面代码基于 Python 3.9 以上版本使用 openai 客户端的 OpenAI 兼容接口。这个接口既能连接 OpenAI 官方服务也能连接本地部署或其他兼容服务方便你把示例迁移到自己的环境。先准备依赖pip install openai python-dotenv项目目录可以按以下结构组织recursive_reason/ ├── requirements.txt ├── .env.example └── recursive_reason.pyrequirements.txt内容openai1.0.0 python-dotenv1.0.0.env.example内容OPENAI_API_KEYsk-your-key OPENAI_BASE_URLhttps://api.openai.com/v1 MODEL_NAMEgpt-4o-mini如果你的模型来自本地部署服务只要把OPENAI_BASE_URL改成对应的兼容地址即可。不同模型的版本差异较大落地前要先确认模型是否支持 JSON 输出和系统提示词。3.2 实现递归推理函数下面是完整的recursive_reason.py最小示例。这个示例的任务是从一段文本中提取所有整数并计算它们的和。校验器使用正则表达式重新扫描文本如果模型提取的数字列表与正则结果不一致就生成反馈信息并继续迭代。import json import os import re import time from openai import OpenAI client OpenAI( api_keyos.getenv(OPENAI_API_KEY, sk-example), base_urlos.getenv(OPENAI_BASE_URL, https://api.openai.com/v1), ) SYSTEM_PROMPT 你是一个多迭代推理引擎。用户会给你一段文本。 你必须只返回一个 JSON 对象包含以下字段 - thinking: string说明当前推理过程 - extracted_numbers: int[]从文本中提取的所有整数不要重复遗漏 - sum: intextracted_numbers 中所有数字的和 - finished: boolean是否确认最终结果已完成 规则 1. 只输出 JSON不要输出其他任何文字。 2. 当 feedback 中指出了错误时必须根据反馈修正结果。 3. 如果无法确认提取完整finished 必须为 false。 4. 所有数字必须是整数不要提取小数部分。 def call_llm(messages, max_tokens800, temperature0): resp client.chat.completions.create( modelos.getenv(MODEL_NAME, gpt-4o-mini), messagesmessages, temperaturetemperature, max_tokensmax_tokens, response_format{type: json_object}, timeout30, ) return resp.choices[0].message.content def safe_parse_json(raw): if not raw: return None try: return json.loads(raw) except json.JSONDecodeError: pass match re.search(r\{.*\}, raw, re.S) if match: try: return json.loads(match.group(0)) except json.JSONDecodeError: return None return None def extract_expected_numbers(raw_text): # 只做演示用匹配所有可选负号的整数 return [int(x) for x in re.findall(r-?\d, raw_text)] def validate_response(response, raw_text): if not isinstance(response, dict): return False, 响应必须是 JSON 对象 expected_numbers sorted(extract_expected_numbers(raw_text)) extracted_numbers response.get(extracted_numbers) s response.get(sum) finished response.get(finished) if not isinstance(extracted_numbers, list) or any( not isinstance(n, int) for n in extracted_numbers ): return False, extracted_numbers 必须是整数数组 if sorted(extracted_numbers) ! expected_numbers: return ( False, fextracted_numbers 应为 {expected_numbers}当前是 {extracted_numbers}请按文本重新提取, ) real_sum sum(extracted_numbers) if s ! real_sum: return False, fsum 应为 {real_sum}当前是 {s}请重新计算 if not isinstance(finished, bool): return False, finished 必须是布尔值 return True, def recursive_reason(raw_text, max_iterations5): messages [ {role: system, content: SYSTEM_PROMPT}, {role: user, content: f任务文本\n{raw_text}}, ] response None for iteration in range(1, max_iterations 1): print(f--- iteration {iteration} ---) raw call_llm(messages) print(raw output:, raw) response safe_parse_json(raw) if response is None: messages.append({role: assistant, content: raw or }) messages.append( {role: user, content: 上轮输出不是合法 JSON请只输出 JSON 对象。} ) continue ok, feedback validate_response(response, raw_text) if ok: return {answer: response, iterations: iteration} messages.append({role: assistant, content: raw}) messages.append({role: user, content: f校验不通过请修正。\n{feedback}}) return { answer: response, iterations: max_iterations, warning: 达到最大迭代次数结果未通过校验, } def main(): text 昨天买了3个苹果和5个香蕉今天又买了-2个退款和12个橙子一共处理了几笔请计算这些整数之和。 result recursive_reason(text, max_iterations5) print(json.dumps(result, ensure_asciiFalse, indent2)) if __name__ __main__: main()这段代码有三个关键设计。第一recursive_reason把消息列表一直往下传。模型输出被追加为assistant消息校验反馈被追加为user消息所以每一轮模型都能看到完整的推理历史。第二safe_parse_json做了兜底解析。即使模型在输出外面包了 Markdown 代码块也能通过正则提取 JSON 对象。第三validate_response不依赖模型自我评估。它使用正则表达式扫描原文本得到标准答案后再与模型结果比较。这样可以避免模型自我感觉良好却输出错误结果。3.3 引入通用 Python 工具调用可选扩展上面的示例只依赖文本校验还没有接入工具调用。实际业务中多迭代推理经常需要查询数据库、读取文件、调用外部服务或执行计算因此可以在校验器之外增加一个工具注册表。下面是一个安全的最小工具调用示例只查询当前时间和环境变量不执行任意表达式def get_current_time(): import datetime return datetime.datetime.now().isoformat() TOOLS { get_current_time: get_current_time, } def tool_call(tool_name): if tool_name in TOOLS: return TOOLS[tool_name]() return f未知工具: {tool_name}扩展工具时要注意不要让模型直接执行任意系统命令或任意 Python 表达式。生产环境尤其需要做白名单、参数校验和操作审计。3.4 运行验证与预期输出运行方式很简单cd recursive_reason python recursive_reason.py第一次运行时模型可能第一轮就正确输出也可能漏掉-2导致校验失败。当模型漏掉-2时你会看到类似下面的日志--- iteration 1 --- raw output: {thinking:我先提取文本中的整数,extracted_numbers:[3,5,12],sum:20,finished:true} 校验不通过请修正。 extracted_numbers 应为 [-2, 3, 5, 12]当前是 [3, 5, 12]请按文本重新提取 --- iteration 2 --- raw output: {thinking:上一轮漏掉了退款 -2需要补上,extracted_numbers:[3,5,-2,12],sum:18,finished:true} 校验通过最终输出类似{ answer: { thinking: 上一轮漏掉了退款 -2需要补上, extracted_numbers: [3, 5, -2, 12], sum: 18, finished: true }, iterations: 2 }这里的检查点是iterations是否大于 1、校验反馈是否被模型理解、最终sum是否正确。你的模型可能第一轮就通过也可能迭代三轮这取决于模型对整数提取的敏感程度。4. 关键设计点与参数取舍4.1 上下文窗口的利用方式链式递归最容易踩的坑是“消息列表无限增长”。每一轮模型输出和校验反馈都会增加 Token迭代到第 10 轮时前面的历史可能已经占据了大量上下文窗口。常见做法有三种策略做法适用场景保留全量历史所有消息都传给模型对话轮次少、任务链路短滑动窗口只保留最近 N 轮消息迭代次数可能较多的场景摘要压缩把前面轮次压缩为摘要长任务、多智能体协作对于最小示例建议先保留全量历史。进入生产环境后再引入max_history_rounds截断逻辑。下面是一种简单的截断方式def trim_messages(messages, max_rounds2): # 前两条是 system 和初始用户任务必须保留 head messages[:2] tail messages[2:] if len(tail) max_rounds * 2: tail tail[-max_rounds * 2:] return head tail要注意的是截断历史可能会让模型丢失早期信息。在截断之前最好把原始任务和已经确认的中间结论单独放在当前提示词里避免模型“失忆”。4.2 递归深度、Token 消耗与成本递归深度直接决定延迟和成本。每一次调用都会新增输入 Token 和输出 Token所以迭代次数越多单任务成本越高。一般建议学习环境设置max_iterations3先用小任务验证循环正确性生产环境根据任务复杂度和预算动态调整并在日志中记录每次迭代的 Token 使用量。如果发现大量任务都达到迭代上限说明校验器或提示词设计有问题不应该简单调大迭代次数。4.3 错误处理与结果校验多迭代系统对错误处理的要求比普通单次调用高得多。下面列举几类必须处理的错误错误类型现象处理方式JSON 解析失败json.JSONDecodeError使用safe_parse_json兜底并追加修正提示接口超时请求超过 30 秒设置timeout必要时重试一次限流错误返回429指数退避重试上下文超限报 token 超限错误截断历史、压缩摘要、减少迭代次数模型不可用返回500或连接失败记录错误返回降级结果实现时不要使用裸的except: pass吞掉异常。至少要把异常类型、请求参数片段和响应状态记录到日志里方便排查。4.4 关键参数速查表下面是第一版实现最常用的参数速查表参数含义推荐值调大影响调小影响temperature随机性0 或 0.1输出更随机不利于稳定推理更稳定但可能缺乏探索max_tokens单次输出上限500~1000允许更长推理可能截断最终结果max_iterations最大循环轮数3~5更多修正机会成本和延迟上升更快终止但可能失败timeout单次请求超时30 秒容忍慢接口太短容易误判超时response_format强制 JSON接口支持时开启解析更稳定不支持则依赖后处理参数之间会互相影响。比如max_tokens太小模型可能来不及生成finished字段导致循环继续最终表现成“迭代次数异常”。出现这种情况时不要只调大迭代次数要先检查单次输出是否被截断。5. 常见问题与排查链路5.1 模型反复输出“需要继续”任务不收敛现象迭代次数一直运行到max_iterations模型要么说“还需要观察”要么不断修改但仍无法通过校验。可能原因任务本身没有明确结束标准。校验器过于严格模型没有能力满足。提示词没有告诉模型“校验通过后必须结束”。上下文被截断模型丢失了原始任务。排查方式打印每一轮的raw output和feedback看模型是否理解反馈。检查校验器是不是要求了模型做不到的字段。检查trim_messages是否把原始任务丢掉了。尝试把“如果sum与extracted_numbers一致则必须返回finishedtrue”写进系统提示词。解决方案先用简单的示例任务跑通再逐步增加复杂度。不要一次引入太多限制。5.2 上下文被撑爆出现 Token 超限错误现象任务运行到几轮后请求返回类似“maximum context length exceeded”的错误。可能原因消息列表不断累积输入 Token 超过模型窗口上限。排查方式在每一轮打印sum(消息中的 token 估算)。检查模型输出是否每轮都携带上一次大段内容。检查是否把全量历史都传给了模型。解决方案引入滑动窗口或摘要压缩。比如只保留最近两轮消息同时把上一轮的最终中间结果单独作为当前 user 消息拼接。这样模型仍然能看到结果又不会让历史无限膨胀。5.3 模型输出不是合法 JSON现象safe_parse_json返回None代码进入“上轮输出不是合法 JSON”分支。可能原因模型不支持或没有正确响应response_format。提示词不够强制模型输出了解释文字。max_tokens太小JSON 被截断。排查方式打印raw output看是哪一部分不合法。检查请求中是否传了response_format{type: json_object}。在模型支持时把 JSON 示例直接放在系统提示词里。解决方案在解析失败时增加一次重试并把“只输出 JSON”加入反馈消息。同时不要把safe_parse_json写得太复杂否则它会掩盖模型输出问题。5.4 请求超时、限流或 OpenAI 兼容接口报错现象调用模型接口时出现超时、限流、连接错误或 5xx。可能原因网络不稳定、并发过高、密钥无效、模型名错误、接口类型不兼容。排查方式先单独测试一个最小 chat completion 请求。检查OPENAI_API_KEY、OPENAI_BASE_URL、MODEL_NAME是否配置正确。检查接口返回的状态码和错误信息。解决方案在call_llm外层加一个带指数退避的重试函数。第一次失败等 1 秒第二次等 2 秒最多重试三次。重试仍失败时不要继续递归直接返回错误结果。def call_llm_with_retry(messages, retries3): for attempt in range(retries): try: return call_llm(messages) except Exception as exc: print(fretry {attempt 1}: {exc}) time.sleep(2 ** attempt) raise RuntimeError(LLM call failed after retries)这里要特别注意重试只适合幂等的网络请求。如果请求中已经携带了会改变服务端状态的工具调用重试前必须确认操作是否已经执行避免重复扣费或重复写入数据。6. 最佳实践与扩展方向6.1 学习环境与生产环境的差异最小示例只能在学习环境里帮助你理解递归推理的机制。进入生产环境后至少还要补上以下几层能力关注点学习环境生产环境密钥管理写在.env使用密钥管理服务禁止写入日志日志print 输出结构化日志记录每次迭代的 token、耗时、错误限制固定max_iterations5按任务类型配置限额增加熔断监控无统计迭代次数、失败率、平均耗时数据安全随意测试文本对敏感信息脱敏不在日志中保留原文回滚直接改代码模型名、提示词、校验规则都做版本管理生产环境还需要考虑并发控制。不要把每个用户请求都无限制地递归调用模型要设置全局 QPS 限制避免某个异常任务消耗大量算力。6.2 可复用检查清单在写完一版链式递归推理系统后建议按下面的清单检查是否设置了max_iterations是否设置了单次请求超时和整轮总超时是否使用确定性校验器而不是只靠模型自我判断是否处理了 JSON 解析失败是否在日志中记录每一轮的模型输出、校验结果和 Token 消耗是否限制上下文长度避免无限增长是否对接口错误做了重试和熔断是否对敏感数据做了脱敏生产环境是否对模型名称、提示词和校验规则做了版本管理是否评估过最坏情况下的成本和延迟这份清单可以用在任何多迭代推理项目里不局限于本文的数字求和示例。6.3 下一步可以怎么扩展第一把确定性校验器替换成更丰富的工具调用。比如接入代码解释器、数据库查询、搜索接口等让模型在每一轮“思考”之后获得真实的外部观察结果。第二引入多候选结果。同一轮可以并行采样多个模型输出用校验器选出最可能通过的那个再进入下一轮迭代。这会提高成功率但会增加成本。第三使用更结构化的“记忆”机制。不要只让模型读历史消息而是把已经验证过的中间结论放在一个固定字段里每次迭代开始时重新注入。这能减少上下文截断带来的遗忘问题。第四针对更复杂的任务可以把递归调用从“一个模型”扩展成“多个模型协作”。比如一个模型负责规划一个模型负责执行校验一个模型负责最终判断。链式递归语言模型的思路仍然适用只是每个节点变成了独立步骤。如果只记住一点建议先把输出格式、停止条件和上下文截断写死在第一版里再考虑更复杂的路由和工具。大多数递归推理失控都发生在没有这些工程约束的时候。