Cursor、Claude Code、Manus 都是 Agent。那 Agent 到底是什么?最好的理解方式是:自己写一个。这篇文章用 ReAct 模式,从零构建一个能写代码、读文件、执行命令的最小 Agent。
Agent 的定义
Agent 的本质可以用一个公式表达:
Agent = LLM + 工具 + 循环执行
- LLM:提供推理和决策能力
- 工具:突破 LLM 的纯文本边界(读文件、写文件、执行命令)
- 循环:不是一次性的问答,而是多步骤迭代直到任务完成
Claude Code 能帮你写一个完整的项目,是因为它在一次任务中可能循环执行几十次:读文件 → 分析 → 写代码 → 运行测试 → 修 Bug → 再运行...
ReAct 模式:Agent 的基本运行框架
ReAct(Reasoning + Acting)是目前最主流的 Agent 运行模式:
用户输入
↓
┌─────────────────────────────┐
│ Thought: 我需要先读一下文件 │ ← 模型先思考
│ Action: read_file("main.py") │ ← 决定行动
└─────────────────────────────┘
↓
执行 read_file
↓
Observation: 文件内容... ← 观察结果
↓
┌─────────────────────────────┐
│ Thought: 我看到了问题所在... │ ← 继续思考
│ Action: write_to_file(...) │ ← 下一步行动
└─────────────────────────────┘
↓
... 循环直到任务完成 ...
↓
Final Answer: 任务已完成 ← 输出最终结果
开始实现:工具定义
首先定义 Agent 能使用的工具。我们实现三个最基础的工具:
# tools.py
import subprocess
from pathlib import Path
def read_file(path: str) -> str:
"""
读取文件内容
Args:
path: 文件路径(相对于工作目录)
Returns:
文件内容字符串,如果文件不存在返回错误信息
"""
try:
file_path = Path(path)
if not file_path.exists():
return f"错误:文件 '{path}' 不存在"
return file_path.read_text(encoding="utf-8")
except Exception as e:
return f"读取文件失败:{e}"
def write_to_file(path: str, content: str) -> str:
"""
写入内容到文件(如果目录不存在会自动创建)
Args:
path: 文件路径
content: 要写入的内容
Returns:
成功或失败的信息
"""
try:
file_path = Path(path)
file_path.parent.mkdir(parents=True, exist_ok=True)
file_path.write_text(content, encoding="utf-8")
return f"成功写入文件:{path}({len(content)} 字符)"
except Exception as e:
return f"写入文件失败:{e}"
def run_terminal_command(command: str, working_dir: str = ".") -> str:
"""
在终端执行命令
Args:
command: 要执行的 shell 命令
working_dir: 命令执行的工作目录
Returns:
命令的标准输出和标准错误输出
"""
try:
result = subprocess.run(
command,
shell=True,
capture_output=True,
text=True,
timeout=60,
cwd=working_dir
)
output = ""
if result.stdout:
output += f"[stdout]\n{result.stdout}"
if result.stderr:
output += f"[stderr]\n{result.stderr}"
if not output:
output = f"命令执行完成,退出码:{result.returncode}"
return output
except subprocess.TimeoutExpired:
return "命令执行超时(60秒)"
except Exception as e:
return f"命令执行失败:{e}"
# 工具注册表(名称 → 函数映射)
AVAILABLE_TOOLS = {
"read_file": read_file,
"write_to_file": write_to_file,
"run_terminal_command": run_terminal_command,
}
核心实现:ReAct Agent
# agent.py
import re
import anthropic
from tools import AVAILABLE_TOOLS
# System Prompt:告诉 LLM 它是什么、能用什么工具、怎么用
SYSTEM_PROMPT = """你是一个强大的 AI 编程助手,能够通过使用工具来帮助完成各种编程任务。
## 可用工具
### read_file
读取指定文件的内容
用法:
<tool_call>
<tool_name>read_file</tool_name>
<path>文件路径</path>
</tool_call>
### write_to_file
将内容写入文件
用法:
<tool_call>
<tool_name>write_to_file</tool_name>
<path>文件路径</path>
<content>文件内容</content>
</tool_call>
### run_terminal_command
在终端执行命令
用法:
<tool_call>
<tool_name>run_terminal_command</tool_name>
<command>要执行的命令</command>
</tool_call>
## 工作方式
1. 收到任务后,先在 <thinking> 标签中分析任务,制定计划
2. 按计划使用工具,每次只调用一个工具
3. 根据工具返回的结果继续推理
4. 任务完成后,输出 <final_answer> 标签
## 注意事项
- 写代码时先规划文件结构,再逐一写入
- 每次工具调用后等待结果,再决定下一步
- 遇到错误时分析原因,尝试修复
- 不要假设工具执行的结果,必须等实际返回
"""
MAX_ITERATIONS = 20 # 防止无限循环
class ReActAgent:
def __init__(self, working_dir: str = "."):
self.client = anthropic.Anthropic()
self.working_dir = working_dir
self.messages = []
def run(self, task: str) -> str:
"""
执行任务,返回最终结果
"""
print(f"\n{'='*60}")
print(f"任务:{task}")
print('='*60)
self.messages = [{"role": "user", "content": f"<task>\n{task}\n</task>"}]
for iteration in range(MAX_ITERATIONS):
print(f"\n--- 第 {iteration + 1} 轮推理 ---")
# 调用 LLM
response = self.client.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=4096,
system=SYSTEM_PROMPT,
messages=self.messages
)
assistant_message = response.content[0].text
print(f"\n[模型输出]\n{assistant_message[:500]}{'...' if len(assistant_message) > 500 else ''}")
self.messages.append({
"role": "assistant",
"content": assistant_message
})
# 检查是否完成
if "<final_answer>" in assistant_message:
final_answer = self._extract_tag(assistant_message, "final_answer")
print(f"\n{'='*60}")
print(f"[任务完成]\n{final_answer}")
return final_answer
# 解析工具调用
tool_call = self._parse_tool_call(assistant_message)
if tool_call is None:
# 没有工具调用也没有最终答案,让模型继续
self.messages.append({
"role": "user",
"content": "请继续执行任务,或者输出 <final_answer> 表示完成。"
})
continue
# 执行工具
tool_result = self._execute_tool(tool_call)
print(f"\n[工具执行结果]\n{tool_result[:300]}{'...' if len(tool_result) > 300 else ''}")
# 把工具结果作为用户消息返回
self.messages.append({
"role": "user",
"content": f"<tool_result>\n{tool_result}\n</tool_result>"
})
return "任务执行超过最大轮次限制,未能完成"
def _parse_tool_call(self, text: str) -> dict | None:
"""解析 XML 格式的工具调用"""
if "<tool_call>" not in text:
return None
tool_name = self._extract_tag(text, "tool_name")
if not tool_name:
return None
tool_call = {"name": tool_name.strip()}
# 根据工具名提取参数
if tool_name == "read_file":
tool_call["path"] = self._extract_tag(text, "path", "")
elif tool_name == "write_to_file":
tool_call["path"] = self._extract_tag(text, "path", "")
tool_call["content"] = self._extract_tag(text, "content", "")
elif tool_name == "run_terminal_command":
tool_call["command"] = self._extract_tag(text, "command", "")
return tool_call
def _execute_tool(self, tool_call: dict) -> str:
"""执行工具调用"""
tool_name = tool_call["name"]
if tool_name not in AVAILABLE_TOOLS:
return f"错误:未知工具 '{tool_name}'"
print(f"\n[执行工具] {tool_name}")
try:
if tool_name == "read_file":
return AVAILABLE_TOOLS[tool_name](tool_call.get("path", ""))
elif tool_name == "write_to_file":
return AVAILABLE_TOOLS[tool_name](
tool_call.get("path", ""),
tool_call.get("content", "")
)
elif tool_name == "run_terminal_command":
return AVAILABLE_TOOLS[tool_name](
tool_call.get("command", ""),
self.working_dir
)
except Exception as e:
return f"工具执行出错:{e}"
def _extract_tag(self, text: str, tag: str, default: str = "") -> str:
"""从 XML 格式文本中提取标签内容"""
pattern = f"<{tag}>(.*?)</{tag}>"
match = re.search(pattern, text, re.DOTALL)
return match.group(1).strip() if match else default
# 入口
if __name__ == "__main__":
import sys
if len(sys.argv) < 3:
print("用法:python agent.py <工作目录> <任务描述>")
sys.exit(1)
working_dir = sys.argv[1]
task = sys.argv[2]
agent = ReActAgent(working_dir=working_dir)
result = agent.run(task)
实际运行效果
# 让 Agent 写一个贪吃蛇游戏
python agent.py ./snake "写一个贪吃蛇游戏,用 HTML、CSS 和 JavaScript 实现,
代码分别放在 index.html、style.css、game.js 三个文件中"
Agent 会自动执行:
- 分析任务,制定文件结构
- 写
index.html(骨架) - 写
style.css(样式) - 写
game.js(游戏逻辑) - 用
run_terminal_command检查文件是否创建成功 - 输出完成报告
ReAct vs Plan-and-Execute
除了 ReAct,还有一种 Plan-and-Execute 模式:
ReAct(适合单步任务):
每一步根据上一步的结果动态决定下一步
Plan-and-Execute(适合复杂任务):
1. Planner:生成完整执行计划 ["步骤1", "步骤2", ...]
2. Executor:执行每一步(可以是 ReAct Agent)
3. Re-planner:根据执行结果更新计划
Manus 这类复杂 Agent 用的是 Plan-and-Execute 变体——先做整体规划,再动态调整。
总结
200 行代码,实现了一个能工作的最小 Agent。它和 Claude Code 的差距在于:
- 没有完善的错误恢复机制
- 没有 Context 管理(长任务会超出 Context Window)
- 工具集非常有限
- 没有用户交互(权限确认、进度展示)
但核心逻辑是完全一样的:LLM + 工具 + ReAct 循环。
理解了这个最小实现,再去用 Claude Code,你会发现它的每个设计决策都有迹可循。