1. 从“思考-行动”到“代码生成”:ReAct主循环的范式革命
如果你最近在关注AI编程助手,尤其是像Claude Code这样的工具,你可能会好奇它背后是如何“思考”的。它不像一个简单的代码补全工具,输入一个函数名就给你补全参数。当你提出一个复杂需求,比如“帮我写一个爬虫,爬取某网站的数据并存入数据库,同时处理分页和反爬”,Claude Code会先“想一想”,然后告诉你它打算怎么做,再一步步生成代码。这个“想一想”的过程,其核心引擎就是ReAct主循环。
ReAct,即“Reasoning + Acting”(推理+行动),它不是一个新概念,但在代码生成领域,它代表了一种根本性的范式转变。传统的代码生成模型,无论是基于Transformer的代码补全,还是早期的代码生成工具,本质上是一种“模式匹配”或“序列预测”。它们根据你输入的上下文(注释、函数签名、已有代码),预测下一个最可能的token(词元)。这种方式在局部、语法正确的代码片段生成上表现不错,但面对需要多步骤规划、依赖外部知识(如API文档)或需要与环境(如文件系统、测试结果)交互的复杂任务时,就显得力不从心。
ReAct主循环将代码生成从一个“静态预测”问题,转变为一个“动态规划与执行”问题。它模拟了一个资深开发者的工作流: 先理解问题,再拆解步骤,然后执行每一步,并根据执行结果(如错误、测试失败)调整后续计划 。这个循环的核心在于,模型不仅生成代码,还生成“思考过程”(Reasoning)和“下一步行动”(Acting),并根据行动的“观察结果”(Observation)来修正自己的思考和后续行动。
理解ReAct主循环,是理解Claude Code这类现代AI编程助手强大能力的关键。它解释了为什么这些工具能处理更开放、更复杂的任务,而不仅仅是补全一行代码。本章,我们将深入Claude Code源码中ReAct主循环的实现,拆解其每一个组件,看看这个“思考-行动”的引擎是如何精密运转的。
2. ReAct主循环的架构拆解:一个状态机的视角
要理解ReAct主循环在Claude Code中的实现,我们可以将其抽象为一个 状态机 。这个状态机管理着整个代码生成任务的执行流,其核心状态包括:任务解析、计划制定、工具调用、代码执行、结果评估与循环控制。
2.1 核心状态与数据流
首先,我们来看主循环的入口。在Claude Code的代码库中,你可能会找到一个名为
ReActAgent
或
CodeGenerationAgent
的核心类。它的
run
或
execute_task
方法是循环的起点。
class ReActAgent:
def __init__(self, llm, tools, max_iterations=10):
self.llm = llm # 大语言模型,如Claude的API客户端
self.tools = tools # 可用的工具集(如文件读写、命令执行、网络搜索)
self.max_iterations = max_iterations # 防止无限循环
self.memory = [] # 存储交互历史(Thought, Action, Observation)
async def execute_task(self, task_description: str):
"""ReAct主循环的入口"""
# 初始化:将用户任务转化为初始状态
state = self._initialize_state(task_description)
for iteration in range(self.max_iterations):
# 1. 思考:基于当前状态,生成下一步的“思考”和“行动”
thought, action = await self._reason(state)
# 2. 行动:执行生成的行动(如调用工具、运行代码)
observation = await self._act(action)
# 3. 观察:将行动结果记录到状态中
state = self._update_state(state, thought, action, observation)
self.memory.append((thought, action, observation))
# 4. 终止判断:检查任务是否完成或是否应终止
if self._should_terminate(state, observation):
break
# 5. 最终输出:整理最终结果(通常是生成的代码文件或解决方案描述)
return self._format_final_output(state)
这个简化的框架揭示了主循环的五个核心阶段,它们在一个循环中不断迭代。
state
是这个状态机的核心,它通常包含以下信息:
- 任务描述 :用户的原始需求。
- 当前工作区上下文 :已生成或修改的文件列表、当前目录结构。
- 交互历史 :历次的(思考,行动,观察)三元组。
- 临时变量 :如当前正在编辑的文件路径、上一条命令的执行结果。
注意 :这里的
_reason和_act方法是高度简化的。在实际的Claude Code实现中,_reason过程涉及复杂的提示工程(Prompt Engineering),引导模型生成结构化的输出;_act则是一个工具分发器,需要安全地解析和执行模型指定的操作。
2.2 思考(Reasoning)阶段:提示工程的艺术
_reason
方法是ReAct循环的“大脑”。它的输入是当前的
state
,输出是一个结构化的“思考”文本和一个“行动”指令。这个过程完全由大语言模型驱动,但模型的行为被精心设计的
系统提示词(System Prompt)
所塑造。
在Claude Code的源码中,你会找到一个非常长的、多段落的系统提示词。它至少包含以下几个部分:
- 角色与能力定义 :告诉模型“你是一个专业的软件工程师,擅长编写清晰、高效、可维护的代码”。
-
工作流程指令
:明确要求模型遵循ReAct格式:“你必须按以下格式输出:
Thought: <你的思考过程>,然后Action: <工具名> <JSON格式参数>”。 -
可用工具清单
:详细列出所有可调用的工具及其用法、参数和返回值。例如:
-
read_file(path): 读取文件内容。 -
write_file(path, content): 写入或覆盖文件。 -
run_command(cmd): 在安全沙箱中执行Shell命令。 -
search_web(query): 搜索网络获取最新信息(如API用法)。 -
run_tests(): 运行项目测试。
-
-
约束与安全规则
:例如,“禁止执行破坏性命令(如
rm -rf /)”,“生成代码前必须先理解现有代码结构”。 -
任务目标重申
:将当前
state中的任务描述和上下文信息注入。
当
_reason
被调用时,它会将上述系统提示词与
state
中积累的完整历史对话(即之前的Thought-Action-Observation序列)组合,形成最终的提示,发送给LLM。模型基于这个庞大的上下文,生成下一步的
Thought
和
Action
。
为什么需要如此复杂的提示?
因为我们需要模型不仅生成代码,还要学会“使用工具”、“规划步骤”和“从错误中学习”。
Thought
字段让模型将其内部推理过程外化,这既有助于调试,也能让后续的思考基于更丰富的上下文。
Action
的严格格式化(工具名+JSON参数)则使得程序可以可靠地解析和执行。
2.3 行动(Acting)与观察(Observation)阶段:工具执行与安全沙箱
_act
方法接收解析后的
Action
对象,其核心是一个工具分发器(Tool Dispatcher)。它会根据
action.tool_name
找到对应的工具函数,传入
action.parameters
,并执行。
工具执行的挑战与实现细节:
-
安全性 :这是重中之重。
run_command工具必须在严格的 沙箱环境 中执行。Claude Code的实现可能使用如docker run、nsjail或gVisor等技术,将命令限制在一个隔离的、资源受限的容器中运行,防止其对主机系统造成任何损害。同时,会有一个 允许列表(Allowlist) 机制,只允许执行一部分安全的命令(如python,pip install,npm test,git clone等),并过滤掉所有危险命令和参数。 -
状态管理 :工具执行会改变环境状态。
write_file会创建或修改工作区文件;run_command可能会安装依赖或改变当前目录。这些状态变化必须被准确地反映到主循环的state中。例如,执行pip install requests后,后续生成代码时,模型就应该“知道”现在可以import requests了。这通常通过将关键的工具输出(如标准输出、标准错误、返回值)作为observation捕获并存入state来实现。 -
错误处理 :工具执行可能失败(如文件不存在、命令执行错误、网络超时)。
_act方法必须能捕获这些异常,并将有意义的错误信息作为observation返回,而不是让整个循环崩溃。例如,observation可能是:“Error: File./src/main.pynot found.” 这个错误信息会被反馈给下一轮的_reason,模型从而可以调整计划(比如先创建这个文件)。
_update_state
方法相对简单,它将本次循环的
(thought, action, observation)
三元组追加到历史记录中,并可能更新一些派生状态(如“最近修改了哪个文件”)。
2.4 终止判断与最终输出
_should_terminate
函数决定循环何时结束。终止条件通常包括:
-
成功条件
:模型生成了一个明确的终止动作,如
Action: FinalAnswer,并附上最终代码或总结。 -
自然完成
:从
observation中可以推断任务已完成(例如,测试全部通过,且模型没有提出下一步行动)。 -
失败或循环
:达到最大迭代次数(
max_iterations),或检测到模型在重复无意义的操作(陷入死循环)。 - 用户中断 :在交互式场景中,用户手动停止。
_format_final_output
则负责整理最终成果。它通常会扫描工作区,将生成或修改的所有代码文件打包,并附上一个由模型生成的最终说明(README),解释代码结构和如何使用。
3. 从理论到实践:一个完整的代码生成案例推演
让我们通过一个具体的例子,将上述架构串联起来,看看ReAct主循环是如何一步步解决一个实际编程问题的。
用户任务
:“在
/project
目录下,创建一个Python Flask Web API,提供一个
/weather
端点,接收城市名参数,返回该城市的模拟天气数据。”
初始状态
:
state
包含任务描述,工作区
/project
为空。
循环迭代1:
-
思考 (Thought)
:“用户需要创建一个Flask API。首先,我需要检查当前目录结构,然后创建必要的文件。我应该先创建一个
requirements.txt来管理依赖,再创建主应用文件。” -
行动 (Action)
:
read_file{“path”: “.”}(尝试读取当前目录,但可能只返回文件列表,或需要list_directory工具)。 -
观察 (Observation)
:
Directory is empty. - 状态更新 :历史记录增加,模型知道目录为空。
循环迭代2:
-
思考
:“目录是空的。我需要创建
requirements.txt和主应用文件app.py。先创建requirements.txt,写入Flask依赖。” -
行动
:
write_file{“path”: “requirements.txt”, “content”: “Flask==2.3.3\nrequests==2.31.0”}(模型可能知道需要requests来模拟外部API调用,尽管任务说“模拟”)。 -
观察
:
File “requirements.txt” created successfully. - 状态更新 :记录文件已创建。
循环迭代3:
-
思考
:“
requirements.txt已创建。现在创建app.py。我需要导入Flask,创建应用实例,定义/weather路由,解析查询参数,并返回一个模拟的JSON响应。” -
行动
:
write_file{“path”: “app.py”, “content”: “from flask import Flask, request, jsonify\n\napp = Flask(__name__)\n\n@app.route(‘/weather’)\ndef get_weather():\n city = request.args.get(‘city’, ‘Beijing’)\n # 模拟天气数据\n weather_data = {\n ‘city’: city,\n ‘temperature’: 22,\n ‘condition’: ‘Sunny’\n }\n return jsonify(weather_data)\n\nif __name__ == ‘__main__’:\n app.run(debug=True)”} -
观察
:
File “app.py” created successfully. - 状态更新 :主应用文件已创建。
循环迭代4:
- 思考 :“核心文件已创建。我应该安装依赖并测试API是否运行。先使用pip安装依赖。”
-
行动
:
run_command{“cmd”: “pip install -r requirements.txt”}(在沙箱中执行)。 -
观察
:
Successfully installed Flask-2.3.3 requests-2.31.0 ... - 状态更新 :依赖安装成功。
循环迭代5:
- 思考 :“依赖已安装。现在启动Flask开发服务器,并在后台测试端点。我可以写一个简单的测试脚本,或者用curl命令。”
-
行动
:
run_command{“cmd”: “python app.py &”}(后台启动,注意实际实现会更复杂,需要管理进程)。 -
观察
:
* Serving Flask app ‘app’ ... * Running on http://127.0.0.1:5000 - 状态更新 :服务器已启动。
循环迭代6:
-
思考
:“服务器正在运行。现在测试
/weather端点。使用curl命令发送请求。” -
行动
:
run_command{“cmd”: “sleep 2 && curl -s ‘http://127.0.0.1:5000/weather?city=Shanghai’”}(等待服务器启动,然后测试)。 -
观察
:
{“city”:”Shanghai”,”temperature”:22,”condition”:”Sunny”} - 状态更新 :API测试成功,返回了预期的JSON。
循环迭代7:
- 思考 :“API创建成功并通过测试。任务完成。我可以提供最终答案了。”
-
行动
:
FinalAnswer{“summary”: “已成功创建Flask API。项目包含 requirements.txt 和 app.py。启动应用后,访问 /weather?city=<城市名> 即可获取模拟天气数据。”} - 观察 :(循环终止)
-
终止判断
:检测到
FinalAnswer动作,循环结束。
这个推演展示了ReAct主循环如何将一个大任务分解为一系列可执行的小步骤(检查、创建、安装、测试),并根据每一步的结果动态调整后续计划。模型的“思考”让它表现得像一个有计划的开发者,而不仅仅是代码片段的生成器。
4. 源码中的关键实现细节与工程挑战
阅读Claude Code中ReAct主循环的源码,你会发现许多为了稳定性和实用性而引入的工程细节。这些细节往往是决定一个AI编程助手是否好用的关键。
4.1 提示词模板与输出解析的鲁棒性
系统提示词虽然强大,但模型并不总是完美遵循指令。源码中会有一个专门的
OutputParser
模块,负责从模型返回的文本中提取结构化的
Thought
和
Action
。
class ReActOutputParser:
def parse(self, llm_output: str) -> Tuple[str, Optional[Action]]:
# 使用正则表达式或更复杂的状态机来匹配 “Thought:” 和 “Action:” 部分
thought_match = re.search(r'Thought:\s*(.*?)(?=\nAction:|\nFinalAnswer:|\Z)', llm_output, re.DOTALL)
action_match = re.search(r'Action:\s*(\w+)\s*(.*)', llm_output, re.DOTALL)
thought = thought_match.group(1).strip() if thought_match else ""
action = None
if action_match:
tool_name = action_match.group(1)
try:
# 尝试解析JSON参数,需要处理各种边界情况
params_json = action_match.group(2).strip()
# 模型可能输出不标准的JSON(如缺少引号,尾随逗号)
params = self._safe_json_parse(params_json)
action = Action(tool=tool_name, parameters=params)
except json.JSONDecodeError:
# 解析失败,将错误信息作为Observation,让模型在下轮修正
observation = f"Error: Could not parse action parameters as JSON: {params_json}"
# 这里可能触发一个特殊的处理流程,而不是直接返回action
...
return thought, action
这个解析器必须非常鲁棒,能处理模型输出的各种变体(如多余的空格、换行、甚至轻微的格式错误)。当解析失败时,一个良好的设计不是让循环崩溃,而是将错误信息作为
observation
反馈给模型,让它在下一次输出时纠正。
4.2 工具的设计与扩展性
工具集是ReAct智能体的“手脚”。Claude Code的工具设计遵循几个原则:
-
原子性
:每个工具只做一件事。
read_file只读文件,不修改。这降低了工具的复杂度,也使得模型的决策更清晰。 -
丰富的上下文
:工具的
observation应尽可能信息丰富。例如,run_command不仅返回退出码,还返回标准输出和标准错误,这为模型诊断问题提供了关键信息。 - 安全性前置 :所有工具,尤其是执行类工具,在实现时就将安全作为第一考量。参数验证、输入清洗、权限检查都在工具内部完成。
工具的注册机制也值得关注。通常有一个
ToolRegistry
类,允许动态注册新工具。这使得Claude Code的能力可以模块化扩展。例如,未来可以轻松加入一个
query_database
工具或
deploy_to_cloud
工具。
4.3 循环控制与“死循环”检测
无限循环是ReAct代理的常见故障模式。模型可能因为逻辑错误或信息不足,在两个状态间来回切换。除了设置
max_iterations
硬性限制外,源码中可能还实现了更智能的检测机制:
-
状态重复检测
:记录最近N次循环的
(thought, action)对或状态的哈希值。如果检测到完全相同的模式重复出现,则提前终止并报错。 -
无进展检测
:监控
observation的内容。如果连续多次观察结果都表明“文件未找到”、“命令执行失败”且模型没有采取有效的纠正措施,可以判断为陷入僵局。 -
复杂度剪枝
:对于特别复杂的任务,模型可能会生成过于庞大的计划。可以在
_reason阶段对生成的thought进行轻量级分析,如果发现其描述的行动链异常长且复杂,可以提前干预,要求模型简化或分步进行。
4.4 记忆(Memory)与上下文窗口管理
ReAct循环的历史记录(memory)会随着迭代不断增长。大语言模型有上下文窗口限制(例如,Claude 3的200K token)。当交互历史超过这个限制时,必须进行压缩或摘要。
一种策略是
选择性记忆
:只保留最近几次完整的交互和历史上所有关键的
observation
(如错误信息、重要文件内容)。另一种策略是
增量摘要
:在每轮循环后,用一个单独的LLM调用对当前历史进行摘要,用摘要替代部分旧历史,保留核心信息。在Claude Code的源码中,你可能会看到一个
MemoryManager
类来处理这些逻辑,确保传递给
_reason
的提示既包含足够的历史信息,又不会超出上下文窗口。
5. 超越基础循环:高级模式与优化策略
基础的ReAct循环已经很强大了,但在生产级应用中,还有更多高级模式和优化策略被引入到Claude Code这样的系统中。
5.1 分层规划与子任务分解
对于极其复杂的任务(如“重构一个单体应用为微服务”),让模型在一个循环内规划所有步骤是不现实的。高级的实现会引入
分层ReAct
。顶层代理负责将大任务分解为几个子任务(如“1. 拆分用户服务,2. 拆分订单服务,3. 配置服务间通信”)。然后,为每个子任务启动一个独立的ReAct子循环去执行。顶层代理监督子任务的完成情况,并协调它们之间的依赖。这在源码中可能体现为
HierarchicalReActAgent
或
Planner-Executor
架构。
5.2 集成外部知识与检索增强
模型的内置知识可能过时或不足。当任务涉及特定库的新版本、公司内部API或冷门技术时,需要从外部获取知识。这就是
检索增强生成(RAG)
与ReAct的结合。在
_reason
阶段之前,系统可以根据当前任务和状态,自动从文档库、代码仓库或网络搜索中检索相关信息,并将其作为上下文提供给模型。例如,当模型需要知道“如何使用FastAPI的Pydantic模型进行请求验证”时,它可以先调用
search_docs
工具获取最新官方文档片段,然后再生成代码。
5.3 验证与自修复循环
生成代码只是第一步,确保代码正确运行更重要。一个成熟的ReAct代理会集成 自动化验证 。这不仅仅是运行用户提到的测试,还包括:
-
静态检查
:在
write_file后,自动运行pyright、ruff或eslint进行代码风格和类型检查,并将错误作为observation反馈。 -
动态测试
:如果项目有测试套件,在关键节点自动运行
run_tests。如果没有,代理甚至可以尝试自己为刚生成的代码编写简单的单元测试。 - 执行验证 :对于生成的是一个可运行脚本或服务,代理会尝试执行它,并验证其输出是否符合预期(例如,启动的Web服务器是否能响应HTTP请求)。
这形成了一个“生成-验证-修复”的 内循环 。模型根据验证结果(测试失败、lint错误)不断调整代码,直到通过所有检查。这极大地提高了最终代码的质量和可靠性。
5.4 人机协同与交互式调试
Claude Code并非完全自主运行。在IDE插件或聊天界面中,它支持 人机协同 。ReAct循环可以被暂停,将模型的“思考”和计划展示给用户,并允许用户:
- 批准/否决行动 :在模型执行一个潜在的危险操作(如删除文件)前,请求用户确认。
- 提供额外指导 :用户可以在循环中途插入评论,如“这个函数需要更详细的错误处理”,模型会将此作为新的输入,调整后续计划。
- 纠正错误 :如果模型误解了需求或走向错误方向,用户可以及时纠正,循环将从新的正确上下文中继续。
这种交互模式将ReAct从全自动代理转变为强大的“副驾驶”,结合了机器的执行力和人类的判断力。
剖析Claude Code中ReAct主循环的实现,我们看到的是一个将大语言模型的推理能力与程序化工具执行紧密结合的复杂系统。它远不止是调用一个API生成文本那么简单,而是一个涉及状态管理、提示工程、安全沙箱、循环控制和错误处理的完整软件架构。理解这个循环,不仅能让你更高效地使用AI编程助手,更能启发你思考如何将类似的“思考-行动”范式应用到其他需要自动规划和执行的复杂任务中去。随着模型能力的提升和工具生态的丰富,ReAct主循环的潜力还将不断被挖掘,成为连接AI智能与真实世界操作的关键桥梁。

316

被折叠的 条评论
为什么被折叠?



