1. 项目概述:从“代码生成”到“代码执行”的范式跃迁
如果你用过 Claude Code 或者 GitHub Copilot,肯定对它们“写代码”的能力印象深刻。你写个注释,它就能给你生成一段看起来不错的函数。但不知道你有没有遇到过这种情况:生成的代码逻辑上没问题,但一运行就报错,要么是导入了不存在的包,要么是调用了错误的 API。这背后的根本原因在于,传统的代码生成模型本质上是一个“文本预测”模型——它根据你给的上下文,预测下一个最可能出现的“词元”。它并不真正理解代码的“运行环境”,更无法验证代码的“执行结果”。
而 Claude Code 源码中揭示的ReAct 主循环,正是为了解决这个问题而生的核心架构。它不是一个简单的代码补全工具,而是一个具备“思考-行动-观察”能力的智能体。简单来说,它让 AI 不再只是“写”代码,而是开始“执行”代码,并根据执行结果进行“修正”和“迭代”。这就像是从一个只会纸上谈兵的参谋,变成了一个能亲自上阵、根据战场反馈调整战术的指挥官。
这个主循环机制,是当前 AI 编程助手从“辅助生成”迈向“自主执行”的关键一步。它不仅仅是 Claude Code 的技术内核,更代表了下一代开发工具的发展方向。无论是想深入理解 AI 如何与真实开发环境交互,还是希望在自己的项目中引入类似的智能体能力,拆解 ReAct 主循环都是绝佳的切入点。接下来,我将带你深入源码,看看这个循环是如何一步步运转起来的。
2. 核心架构与设计哲学:为什么是 ReAct?
在深入代码之前,我们必须先理解 Claude Code 选择 ReAct 框架的底层逻辑。ReAct 是 “Reasoning + Acting” 的缩写,其核心思想是让智能体交替进行“推理”和“行动”。在代码生成的场景下,这被具体化为:
- 推理:分析当前任务、已有代码、错误信息,规划下一步要做什么。
- 行动:执行一个具体操作,比如运行一段代码、读取一个文件、安装一个依赖。
- 观察:获取行动的结果(成功输出、错误信息、文件内容变化)。
这个循环会一直持续,直到任务被完成或达到终止条件。那么,为什么这种模式特别适合代码生成呢?
2.1 传统代码生成的局限性
传统的端到端代码生成模型,如基于 Transformer 的大模型,存在几个固有缺陷:
- 环境盲区:模型不知道你项目的
package.json里具体有哪些依赖和版本,也不知道当前工作目录的文件结构。它只能基于训练数据中的“常见模式”进行猜测。 - 静态幻觉:模型可能会“幻想”出一些不存在的 API 或函数签名,因为它学到的知识可能过时,或者与你的特定项目环境不符。
- 缺乏验证:生成的代码没有经过“运行”这一终极测试。语法正确不代表逻辑正确,更不代表能在你的环境中运行。
2.2 ReAct 带来的范式优势
Claude Code 的 ReAct 主循环直接针对了上述痛点:
- 动态环境感知:通过“行动”(如
ls,cat),智能体可以实时读取项目文件、查看日志、检查依赖,从而获得最新的、准确的上下文信息。这解决了“环境盲区”问题。 - 执行反馈驱动:通过“观察”代码运行的结果(成功或报错),智能体获得了最直接的反馈。如果代码报错,错误堆栈信息会成为下一轮“推理”的关键输入,指导它进行修正。这实现了闭环验证。
- 任务分解与规划:复杂的编程任务(如“实现一个用户登录API”)可以被拆解成一系列子步骤(检查现有路由、创建模型、编写控制器、测试端点)。ReAct 的推理步骤天然适合这种逐步推进的任务解决方式。
从源码的设计中可以看出,Claude Code 并不是简单套用 ReAct 论文中的模板,而是对其进行了深度定制,使其完全服务于“在真实开发环境中解决真实编程问题”这一核心目标。其架构设计紧密围绕工具调用、状态管理和循环控制这三个支柱展开。
3. 主循环核心模块深度解析
让我们进入正题,拆解 Claude Code 源码中 ReAct 主循环的核心模块。整个循环可以看作一个状态机,其核心类通常被命名为Agent或ReActLoop。以下是其关键组成部分的解析。
3.1 状态管理:AgentState
一切始于状态。主循环需要一个数据结构来保存当前任务执行的所有上下文。在 Claude Code 中,这个结构通常被定义为AgentState或类似的类。
# 示例性代码,展示状态管理的核心字段 class AgentState: def __init__(self): # 核心输入 self.task: str = "" # 用户提出的原始任务,如“修复这个bug” self.working_dir: Path = Path(".") # 智能体操作的工作目录 # 会话历史与上下文 self.conversation_history: List[Message] = [] # 包含用户、助手、工具消息的完整对话 self.code_context: Dict[str, str] = {} # 当前关注的相关代码文件片段,key为文件路径 # 执行环境 self.interpreter: CodeInterpreter = None # 代码解释器实例,用于执行代码 self.file_system: FileSystemTool = None # 文件系统工具实例 # 循环控制状态 self.max_steps: int = 20 # 最大循环步数,防止无限循环 self.current_step: int = 0 self.is_task_complete: bool = False # 任务完成标志 self.last_error: Optional[str] = None # 上一步行动产生的错误信息设计要点解析:
- 分离会话与代码上下文:
conversation_history保存了自然语言对话,供模型理解任务脉络;code_context则专门存储提取的代码,供模型分析引用。这种分离提高了信息检索的效率。 - 工具实例化:状态中持有
interpreter和file_system等工具实例,意味着这些工具是有状态的。例如,代码解释器会维护一个持久的运行时会话(如一个 Jupyter Kernel),使得前后执行的代码可以共享变量,这对于调试和迭代至关重要。 - 轻量级与可序列化:
AgentState的设计需要考虑可能的中断与恢复。虽然持有工具实例,但核心数据(如任务、历史、当前步骤)应易于序列化保存。
实操心得:在实际模仿实现时,
code_context的管理策略是性能关键。不要每次都把整个文件内容塞进去。更佳实践是使用一个ContextManager,它根据模型当前关注的焦点(如错误行号、提到的函数名),动态地从工作目录中读取相关的代码块(比如函数定义及其上下几行),这样可以有效控制输入给模型的令牌数量,节省成本并提升推理速度。
3.2 推理引擎:ReasoningModule
这是智能体的“大脑”,负责根据当前状态,推理出下一步该做什么。在 Claude Code 的实现中,这通常不是一个独立的复杂模块,而是通过精心设计的提示词,引导大模型(如 Claude 3)输出结构化的“思考”和“行动计划”。
核心流程如下:
- 组装提示词:将
AgentState中的任务、对话历史、相关代码、最近错误等信息,按照预定模板格式化成一段给模型的提示。 - 调用模型:请求大模型生成回复。
- 解析输出:模型的回复需要被解析成结构化的数据。Claude Code 极大可能利用了 Claude 模型对结构化输出(如 JSON)的良好支持,或者使用正则表达式进行抽取。
# 提示词模板示例(简化) REASONING_PROMPT_TEMPLATE = """ 你是一个在{working_dir}目录下工作的资深软件工程师。 你的最终任务是:{task} ## 当前上下文 {conversation_history_snippet} {code_context_snippet} {error_context_if_any} ## 可用工具 1. 执行代码:可以运行Python/Shell代码片段,并返回结果或错误。 2. 读写文件:可以读取、创建、编辑、删除文件。 3. 搜索代码:可以在项目中搜索特定模式。 ## 要求 请你逐步思考: 1. 首先,分析当前状况和任务目标。 2. 然后,决定下一步是使用工具,还是认为任务已完成。 3. 如果使用工具,请严格按照以下JSON格式输出: ```json {{ "thought": "你的详细推理过程,解释为什么这么做。", "action": "工具名称,如 'run_code' 或 'read_file'", "action_input": {{ // 工具参数 "code": "要执行的代码", "path": "文件路径" // ... 其他参数 }} }}- 如果任务完成,请输出:
{{ "thought": "解释任务如何完成。", "action": "finalize", "action_input": {{"summary": "任务完成总结"}} }}现在,开始你的思考: """
**关键设计解析**: * **思维链强制**:提示词中明确要求模型输出“thought”,这不仅仅是给用户看的,更重要的是让模型将自己的推理过程“外化”,这通常能显著提升行动决策的质量和一致性。 * **结构化输出**:要求模型输出严格的 JSON 格式,这是实现程序自动化解析的关键。Claude 模型在这方面非常可靠,大大降低了后处理的复杂度。 * **上下文精选**:提示词中插入的 `{code_context_snippet}` 等,需要由上游模块从 `AgentState` 中提取最相关的部分,而不是全部倒入,这是控制成本的核心。 > **注意事项**:模型有时会“不听话”,输出格式不正确的 JSON 或忘记输出“thought”。在源码中,你一定会看到健壮的**输出解析和错误处理逻辑**。例如,使用 `json.loads()` 配合 `try-catch`,并在解析失败时,将模型的错误输出连同解析失败信息一起,作为新的上下文喂回给模型,要求它纠正。这是一个典型的自我修正循环。 ### 3.3 工具执行器:`ToolExecutor` 推理模块决定了“做什么”,工具执行器则负责“怎么做”。它是连接智能体“思考”和真实世界“影响”的桥梁。Claude Code 的工具集是其强大能力的直接体现。 ```python class ToolExecutor: def __init__(self, state: AgentState): self.state = state self._tools = { 'run_code': self._execute_code, 'read_file': self._read_file, 'write_file': self._write_file, 'search_files': self._grep, 'run_shell': self._run_shell_command, } def execute(self, action: str, action_input: Dict) -> Dict: """执行指定工具,并返回标准化结果""" if action not in self._tools: return {"error": f"未知工具: {action}"} try: # 调用对应的工具函数 result = self._tools[action](**action_input) # 标准化成功返回 return { "status": "success", "output": result, "observation": str(result)[:500] # 截断过长的输出 } except Exception as e: # 标准化错误返回,错误信息对后续推理至关重要 return { "status": "error", "output": None, "observation": f"工具执行失败: {type(e).__name__}: {e}" } def _execute_code(self, code: str, language: str = "python") -> str: """核心:在持久化解释器中执行代码""" # 使用 state 中维护的解释器实例 return self.state.interpreter.run(code, language) def _read_file(self, path: str) -> str: """读取文件内容,路径相对于 working_dir""" full_path = self.state.working_dir / path return full_path.read_text() def _write_file(self, path: str, content: str, append: bool = False) -> str: """写入文件。注意:这是改变真实环境的操作!""" full_path = self.state.working_dir / path if append: full_path.write_text(full_path.read_text() + content) else: full_path.write_text(content) return f"文件 '{path}' 写入成功。"安全与设计解析:
- 沙箱化执行:
_execute_code是重中之重。Claude Code 的源码中,interpreter.run()的背后一定是一个严格的沙箱环境。它可能基于 Docker 容器、安全的进程隔离或专门的代码沙箱库(如pysandbox)。这是防止恶意或错误代码破坏宿主机的生命线。 - 路径隔离:所有文件操作都基于
self.state.working_dir。这意味着智能体被“禁锢”在指定的项目目录内,无法访问或修改系统其他文件,这是最基本的安全边界。 - 标准化返回:无论工具执行成功与否,
execute方法都返回结构一致的字典。observation字段的内容会直接进入下一轮推理的提示词。因此,将错误信息清晰、友好地格式化在这里非常重要,要便于模型理解。 - 工具设计的原子性:工具应该足够“原子”,每个工具只做一件事。比如,不设计一个“运行测试并修复”的复杂工具,而是拆成“运行测试”、“读取测试输出”、“编辑文件”等多个工具,由模型通过多次调用来组合完成复杂任务。这提高了系统的灵活性和可解释性。
3.4 循环控制器:ReActLoop.run()
这是将所有模块串联起来的“总指挥”。它的逻辑清晰而严谨,体现了 ReAct 模式的精髓。
class ReActLoop: def __init__(self, llm_client, initial_state: AgentState): self.llm = llm_client self.state = initial_state self.tool_executor = ToolExecutor(initial_state) def run(self) -> AgentState: """主循环入口""" while not self.state.is_task_complete and self.state.current_step < self.state.max_steps: self.state.current_step += 1 print(f"\n=== 步骤 {self.state.current_step} ===") # 1. 推理阶段:决定下一步行动 reasoning_result = self._reasoning_step() if reasoning_result.get("action") == "finalize": self.state.is_task_complete = True print(f"任务完成: {reasoning_result.get('summary')}") break # 2. 行动阶段:执行工具 action = reasoning_result["action"] action_input = reasoning_result["action_input"] print(f"执行动作: {action}, 输入: {action_input}") tool_result = self.tool_executor.execute(action, action_input) # 3. 观察阶段:记录结果到状态 self._update_state_with_observation(tool_result, reasoning_result) # 4. 检查终止条件(如连续失败) if self._should_early_stop(): print("提前终止:连续失败次数过多。") break return self.state def _reasoning_step(self) -> Dict: """封装提示词构建、调用LLM、解析输出的全过程""" prompt = self._construct_prompt() # 使用状态信息构建提示词 llm_response = self.llm.complete(prompt) return self._parse_llm_response(llm_response) # 解析出 thought, action, action_input def _update_state_with_observation(self, tool_result: Dict, reasoning_result: Dict): """将本轮结果记录到对话历史中,供下一轮推理使用""" # 添加助手的“思考”消息 self.state.conversation_history.append({ "role": "assistant", "content": reasoning_result["thought"] }) # 添加“工具调用”消息(可选,取决于如何设计消息角色) # 添加“工具结果”观察消息 self.state.conversation_history.append({ "role": "user", # 通常将工具返回视为“环境”的输入,用 user 角色模拟 "content": f"工具执行结果: {tool_result['observation']}" }) # 如果执行出错,更新错误状态 if tool_result["status"] == "error": self.state.last_error = tool_result["observation"] else: self.state.last_error = None # 根据工具结果,可能更新 code_context # 例如,如果刚读取了一个新文件,将其内容提取关键部分加入上下文 self._refresh_code_context()循环控制逻辑解析:
- 显式状态更新:每一轮循环后,都必须显式地将“思考”、“行动结果”更新到
state.conversation_history中。这是模型拥有“记忆”的关键。 - 错误处理与早期终止:
_should_early_stop()函数是一个安全阀。常见的策略是:如果连续 N 步(比如5步)工具执行都失败了,或者模型陷入了重复的动作循环,就主动终止任务,避免浪费资源。这需要设计一些启发式规则来检测循环。 - 上下文刷新策略:
_refresh_code_context()是一个智能函数。它不能简单地把所有文件内容都加进去。它的策略可能是:关注最近被读取或修改的文件;关注错误堆栈中提到的文件和行号;根据模型“思考”中提到的函数名或类名,去项目中进行轻量级检索(如使用ripgrep)并加载相关片段。
4. 实战推演:一个完整的调试循环
让我们通过一个虚构但非常典型的例子,看看主循环是如何实际运作的。假设任务是在一个 Flask 项目中“修复/login端点的 500 错误”。
步骤 0 (初始状态):
state.task: “修复/login端点的 500 错误”state.conversation_history: [用户消息]state.code_context: {} (初始为空)state.last_error: None
步骤 1 (推理):
- 模型思考: “我需要先复现这个错误。应该检查一下
app.py或相关路由文件,找到/login路由的定义,然后尝试运行一个简单的测试请求来获取具体的错误信息。” - 模型输出:
{ "thought": "首先,我需要定位到 /login 路由的处理函数。让我先查看项目的主要应用文件。", "action": "read_file", "action_input": {"path": "app.py"} }
步骤 1 (行动与观察):
ToolExecutor读取app.py。- 观察结果: “文件内容显示,
/login路由在auth.py文件中。app.py导入了auth蓝图。”
步骤 2 (推理):
- 新的提示词包含了上一步的思考和观察结果。
- 模型思考: “好的,路由在
auth.py。现在读取这个文件,查看login函数的具体实现。” - 模型输出:
{ "thought": "现在读取 auth.py 文件以检查 login 函数。", "action": "read_file", "action_input": {"path": "routes/auth.py"} }
步骤 2 (行动与观察):
- 读取
auth.py。 - 观察结果: “文件内容显示
login函数尝试从request.form获取username和password,然后调用User.query.filter_by(...).first()。”
步骤 3 (推理):
- 模型思考: “我需要运行这个 Flask 应用,并模拟一个对
/login的 POST 请求,来触发 500 错误并查看堆栈跟踪。首先确保依赖已安装,然后启动一个测试服务器。” - 模型输出:
{ "thought": "我将启动一个简单的 Flask 测试服务器,并向 /login 发送一个请求来捕获错误。", "action": "run_code", "action_input": { "code": "import sys\nsys.path.insert(0, '.')\nfrom app import app\napp.testing = True\nwith app.test_client() as client:\n resp = client.post('/login', data={'username':'test','password':'test'})\n print(f'Status: {resp.status_code}')\n print(f'Response: {resp.data.decode()}')\n", "language": "python" } }
步骤 3 (行动与观察):
- 在沙箱中执行上述代码。
- 观察结果 (错误): “工具执行失败:
AttributeError: 'NoneType' object has no attribute 'query'。错误发生在auth.py第 15 行User.query.filter_by(...)。”
步骤 4 (推理):
- 模型思考: “
User对象是None?这意味着 SQLAlchemy 的User模型可能没有正确导入或定义。让我检查一下models.py文件,并确认User类是否存在以及是否导入了db对象。” - 模型输出:
{ "thought": "错误表明 User 模型未定义或未与数据库绑定。检查 models.py 和数据库初始化代码。", "action": "read_file", "action_input": {"path": "models.py"} }
... 循环继续。
最终,模型可能会发现是models.py中缺少db = SQLAlchemy(app)的初始化,或者User类没有继承db.Model。它会通过“写文件”工具进行修复,然后再次运行测试进行验证,直到错误消失,最终输出finalize动作。
这个推演清晰地展示了 ReAct 主循环如何将复杂的调试任务,分解为一系列可执行的“读文件”、“运行代码”等原子操作,并通过执行反馈逐步逼近问题根源。
5. 性能优化与工程实践要点
阅读源码时,你会发现 Claude Code 的工程实现远不止上述基础循环。以下是一些关键的优化点和工程实践,它们决定了系统的可用性和效率。
5.1 上下文长度管理与优化
这是成本与效果平衡的核心。大模型的上下文窗口是宝贵资源(如 128K tokens),必须高效利用。
- 对话历史摘要:不可能把所有历史对话都塞进提示词。Claude Code 很可能实现了“滑动窗口”或“摘要”策略。例如,只保留最近 10 轮交互的完整消息,对于更早的消息,则使用另一个 LLM 调用将其总结成一段简短的背景描述。
- 代码上下文的动态加载:
code_context不是静态的。一个高效的ContextManager会做以下工作:- 监听焦点:从模型的“思考”和错误信息中,提取出它正在关注的文件名、函数名、类名、行号。
- 智能检索:使用轻量级代码分析(如 AST 解析)或文本搜索,只加载与焦点相关的代码片段。例如,当模型提到
handle_login函数时,不仅加载这个函数,还加载它的调用者、它调用的函数、以及同文件中的相关类和导入语句。 - 去除冗余:避免在不同轮次中重复加载相同的长代码块。
5.2 工具执行的稳定性保障
工具调用是故障高发区,必须有鲁棒的设计。
- 超时与资源限制:
run_code工具必须设置严格的执行超时(如30秒)和资源限制(内存、CPU)。防止一段死循环代码拖垮整个服务。 - 副作用管理与回滚:
write_file是危险操作。高级的实现会考虑:- 版本备份:在修改文件前,自动备份原文件。如果后续步骤导致更严重错误,可以提示用户或智能体回滚。
- 沙箱文件系统:更彻底的方案是让智能体在一个虚拟文件系统(如内存文件系统或 Docker 容器内的独立目录)中操作,所有修改在任务最终确认成功后才一次性提交到真实文件系统。
- 工具结果的后处理:代码执行的输出可能非常冗长(如
pip install的日志)。直接塞给模型会浪费 tokens 且干扰注意力。需要有一个OutputProcessor来:- 截断:过长的输出只保留头部和尾部关键行。
- 提取关键信息:例如,从
pytest的输出中,提取出失败测试的名称和错误断言;从ImportError中提取缺失的模块名。
5.3 提示词工程与少样本学习
Claude Code 的强大,很大程度上源于其精心设计的提示词。在源码中,你可能会发现一个“提示词模板库”。
- 角色扮演与约束:开头的系统提示词(如“你是一个资深软件工程师...”)设定了角色的行为边界和专业性。
- 少样本示例:提示词中很可能包含了几个精心构造的“示例对话”(Few-shot Examples)。例如:
这些示例教会了模型 ReAct 的格式和思考方式。用户:帮我写一个计算斐波那契数列的函数。 助手思考:我需要创建一个 Python 函数。首先检查当前目录是否有相关文件...(思考过程) 助手行动:{"action": "write_file", "action_input": {"path": "fib.py", "content": "def fib(n): ..."}} 工具结果:文件写入成功。 用户:现在测试一下 fib(5)。 助手思考:我需要运行刚创建的文件中的函数... 助手行动:{"action": "run_code", "action_input": {"code": "from fib import fib; print(fib(5))", "language": "python"}} 工具结果:5 - 动态提示词调整:根据任务类型(调试、重构、新功能开发)或当前状态(是否连续出错),系统可能会切换不同的提示词模板,以更好地引导模型。
5.4 循环终止与异常处理策略
一个健壮的循环必须知道何时停止。
- 成功终止:模型主动输出
finalize动作。 - 失败终止:
- 步数限制:
max_steps是硬限制。 - 循环检测:如果最近几步的“行动”序列出现重复模式(如反复读写同一个文件但状态不变),则触发终止。
- 用户中断:实现一个信号监听,允许用户手动停止。
- 步数限制:
- 优雅降级:当循环异常终止时,系统应能保存当前
AgentState(至少是对话历史和代码修改),并生成一份报告,说明已完成的步骤和遇到的关键问题,方便用户接手。
6. 从源码到实践:构建你自己的简易 ReAct 智能体
理解了 Claude Code 的设计精髓后,我们可以尝试用最简化的方式,构建一个自己的 ReAct 代码智能体。这里提供一个高度浓缩但可运行的骨架。
import json import subprocess from pathlib import Path from typing import Dict, List # 假设使用 OpenAI API,你需要安装 openai 库并设置 API_KEY import openai class SimpleCodeAgent: def __init__(self, task: str, work_dir: str = ".", model: str = "gpt-4"): self.task = task self.work_dir = Path(work_dir) self.model = model self.client = openai.OpenAI(api_key="your-api-key") self.conversation = [{"role": "user", "content": task}] self.max_steps = 15 def run(self): for step in range(self.max_steps): print(f"\n--- 步骤 {step+1} ---") # 1. 推理 response = self._call_llm() try: action_data = self._extract_action(response) except ValueError as e: print(f"解析模型输出失败: {e}") self.conversation.append({"role": "user", "content": f"你的输出格式不正确。请严格按照JSON格式输出。错误:{e}"}) continue if action_data.get("action") == "finalize": print(f"任务完成: {action_data.get('summary', '')}") break # 2. 行动 result = self._execute_tool(action_data["action"], action_data.get("action_input", {})) observation = result["observation"] # 3. 观察与记录 self.conversation.append({"role": "assistant", "content": action_data.get("thought", "")}) self.conversation.append({"role": "user", "content": f"结果: {observation}"}) print(f"观察: {observation[:200]}...") def _call_llm(self) -> str: prompt = self._build_prompt() response = self.client.chat.completions.create( model=self.model, messages=[{"role": "system", "content": "你是一个Python编程助手。请用JSON格式输出你的思考和行动。"}] + self.conversation[-10:], # 滑动窗口 temperature=0.2, ) return response.choices[0].message.content def _build_prompt(self): # 简化的提示词 return self.conversation def _extract_action(self, response: str) -> Dict: # 尝试从响应中提取JSON代码块 import re json_match = re.search(r'```json\n(.*?)\n```', response, re.DOTALL) if json_match: json_str = json_match.group(1) else: json_str = response.strip() return json.loads(json_str) def _execute_tool(self, action: str, inputs: Dict) -> Dict: if action == "run_python": code = inputs.get("code", "") return self._run_python_code(code) elif action == "read_file": path = inputs.get("path", "") return self._read_file(path) elif action == "write_file": path = inputs.get("path", "") content = inputs.get("content", "") return self._write_file(path, content) else: return {"observation": f"错误:未知动作 '{action}'"} def _run_python_code(self, code: str) -> Dict: try: # 警告:简易实现,无沙箱!仅用于演示,生产环境必须使用Docker等隔离。 result = subprocess.run( ["python3", "-c", code], cwd=self.work_dir, capture_output=True, text=True, timeout=10 ) if result.returncode == 0: return {"observation": f"执行成功:\n{result.stdout}"} else: return {"observation": f"执行失败:\n{result.stderr}"} except subprocess.TimeoutExpired: return {"observation": "错误:代码执行超时(10秒)。"} except Exception as e: return {"observation": f"错误:{e}"} def _read_file(self, path: str) -> Dict: try: full_path = self.work_dir / path content = full_path.read_text() return {"observation": f"文件内容:\n{content}"} except Exception as e: return {"observation": f"读取文件失败: {e}"} def _write_file(self, path: str, content: str) -> Dict: try: full_path = self.work_dir / path full_path.write_text(content) return {"observation": f"文件 '{path}' 已写入。"} except Exception as e: return {"observation": f"写入文件失败: {e}"} # 使用示例 if __name__ == "__main__": agent = SimpleCodeAgent( task="请检查当前目录下是否有 requirements.txt 文件,如果有,请列出其中的前三个包。", work_dir="./sample_project" ) agent.run()这个简易实现的重点与警告:
- 核心流程完整:它包含了 ReAct 的推理、行动、观察循环,以及状态(对话历史)管理。
- 极度简化:没有代码上下文管理,没有复杂的提示词工程,工具很少。
- 安全警告:
_run_python_code直接使用subprocess,存在严重安全风险,绝对不可用于生产环境或处理不可信代码。真实系统必须使用 Docker 或专用沙箱。 - 健壮性不足:错误处理、循环检测、上下文长度控制都非常基础。
这个例子旨在帮助你将前面解析的理论,映射到具体的代码结构上。要构建一个可用的系统,你需要在此基础上,逐一实现我们前面讨论的各个优化模块。
7. 常见陷阱与排查指南
在实际运行 ReAct 智能体时,你会遇到各种各样的问题。以下是一些典型陷阱及其排查思路。
7.1 模型不按格式输出
- 现象:模型回复是自然语言,而不是你要求的 JSON 格式。
- 原因:提示词不够清晰;少样本示例不足;模型温度(temperature)参数过高。
- 解决:
- 强化格式指令:在系统提示词和用户提示词中,用非常明确、加重的语言描述格式要求。例如:“你必须,且只能,输出一个 JSON 对象,包含 thought, action, action_input 字段。”
- 提供更清晰的示例:在少样本示例中,展示格式正确的完整交互。
- 降低温度:将
temperature设为 0.1 或 0.2,降低随机性,使输出更确定。 - 使用模型的结构化输出功能:如果使用的 API 支持(如 OpenAI 的
response_format或 Anthropic 的tool_use),优先使用这些功能,它们比文本指令更可靠。
7.2 智能体陷入死循环
- 现象:智能体反复执行相似操作(如反复读取同一个文件),无法推进任务。
- 原因:模型未能从观察结果中提取有效信息来更新计划;任务本身模糊或不可实现。
- 解决:
- 增强观察结果的信息量:确保工具返回的
observation不仅包含“成功/失败”,还包含对下一步推理有用的关键数据。例如,运行测试失败时,不要只返回“测试失败”,而要提取具体的失败断言和行号。 - 在提示词中加入“防呆”引导:例如,“如果你连续三次尝试相同或类似的操作都未能取得进展,请重新评估你的计划,或者尝试一个完全不同的方法。”
- 实现循环检测器:在
AgentState中记录最近 N 步的行动和观察摘要。如果检测到重复模式,则在提示词中明确告知模型:“注意:你似乎陷入了循环。你最近三步都在做 X。请尝试一个不同的策略。” - 设置硬性步数限制:这是最后的安全网。
- 增强观察结果的信息量:确保工具返回的
7.3 工具执行结果太长,干扰模型
- 现象:运行
pip install或npm start产生了数百行日志,模型被淹没在无关信息中。 - 原因:未对工具输出进行后处理。
- 解决:
- 为每个工具设计输出过滤器:
run_code:捕获最后 20 行输出,如果遇到错误(非零退出码),则优先捕获错误信息。search_files:只返回匹配的行和前后几行上下文,而不是整个文件。
- 总结冗长输出:对于特别长的输出,可以调用另一个轻量级模型(如 GPT-3.5-turbo)进行摘要,再将摘要放入观察中。例如:“安装过程成功,生成了大量日志。摘要:成功安装了 package-a (v1.2), package-b (v3.4),无错误。”
- 提供“查看更多”选项:在观察结果末尾注明“(输出已截断,完整日志共XXX行)”。如果模型在后续思考中明确要求查看完整日志,再通过一个新工具调用提供。
- 为每个工具设计输出过滤器:
7.4 代码修改引入新错误
- 现象:智能体为了修复一个错误而修改了代码,但修改后产生了新的、更隐蔽的错误。
- 原因:模型缺乏对代码库的全局理解,修改是局部的、基于试探的。
- 解决:
- 强制运行测试:在智能体完成一组修改后,在提示词中要求它必须运行相关的单元测试或集成测试,并将测试结果作为下一轮观察的核心依据。
- 实施“小步快跑”策略:鼓励模型进行更小、更增量的修改。每修改一个文件,就运行一次相关的检查(如语法检查
python -m py_compile,或导入检查)。 - 引入代码分析工具:将
lint(如flake8)、type check(如mypy)作为工具集成进来。在模型提交修改前,自动运行这些检查,并将警告和错误作为观察的一部分反馈给模型。
调试一个 ReAct 智能体本身就是一个递归过程:观察它的失败模式,然后修改你的提示词、工具设计或状态管理逻辑,再观察效果。这个过程本身,就充满了“智能体”的味道。