
作者:逆境不可逃
技术永无止境
希望我的内容可以帮助到你!!!!!
大家吼 ! 我是 逆境不可逃 今天给大家带来文章《【与我学 ClaudeCode】工具与执行篇:从 0 到 1 拆解 Agent Loop 与 Tool Use 的极简设计哲学》.
Learn-Claude-Code 官方地址 : https://github.com/shareAI-lab/learn-claude-code
开篇:为什么 ClaudeCode 的 Agent 能跑起来?
很多人聊 AI Agent,张口就是 "规划框架""状态机""多智能体编排",仿佛不堆砌复杂组件就做不出能干活的 Agent。但当你扒开 ClaudeCode 的底层设计会发现:它的核心逻辑,用不到 30 行代码就能跑通;它的核心能力,只靠一个循环 + 几个工具就实现了。
这篇我们就从 ClaudeCode 的v0版本设计出发,拆解它最底层的两个核心模块 ------Agent Loop(智能体循环)和Tool Use(工具使用),搞懂为什么 "一个循环 + Bash = 一个可用的 Agent",以及它是如何一步步扩展到 4 个核心工具,支撑起完整的编程任务的。
一、Agent Loop:模型与真实世界的第一道桥梁
1. 问题:为什么 LLM 不能直接 "干活"?
语言模型天生擅长推理代码、理解需求,但它有个致命缺陷 ------它碰不到真实世界:
- 它没法直接读本地文件、修改代码;
- 没法运行测试、查看报错日志;
- 更没法根据执行结果调整后续动作。
没有循环之前,这些操作都得你手动完成:模型输出命令→你复制到终端执行→把结果粘回对话里→模型再根据结果继续推理。你,就是那个人肉循环。
2. 核心解法:极简 Agent 循环,30 行代码跑通端到端
ClaudeCode 的 Agent 循环,核心逻辑就是 "模型思考→工具执行→结果反馈→再思考" 的闭环,直到模型不再调用工具为止。
我们直接看它的完整实现(和官方设计完全对齐):
def agent_loop(query):
# 1. 初始化对话上下文,用户prompt作为第一条消息
messages = [{"role": "user", "content": query}]
while True:
# 2. 调用LLM,传入对话历史、系统提示和工具定义
response = client.messages.create(
model=MODEL, system=SYSTEM, messages=messages,
tools=TOOLS, max_tokens=8000,
)
# 3. 追加模型的响应到对话历史
messages.append({"role": "assistant", "content": response.content})
# 4. 关键退出条件:如果模型不再调用工具,直接结束循环
if response.stop_reason != "tool_use":
return
# 5. 执行所有工具调用,收集结果
results = []
for block in response.content:
if block.type == "tool_use":
# 执行Bash命令(后续会扩展为工具分发)
output = run_bash(block.input["command"])
results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": output,
})
# 6. 把工具执行结果作为用户消息,追加到对话历史,开启下一轮循环
messages.append({"role": "user", "content": results})
这个循环的设计,完美契合了 ClaudeCode 的核心哲学:最小可行 Agent,一个工具 + 一个循环。
3. 设计决策:为什么仅靠 Bash 就够了?
在v0版本中,ClaudeCode 只提供了 Bash 这一个工具,背后的设计思考非常值得学习:
- 能力全覆盖 :Bash 能读写文件、运行任意程序、在进程间传递数据、管理文件系统,任何额外的
read_file/write_file工具,都只是 Bash 能力的子集; - 降低模型负担:模型只需要学习一个工具的 Schema,不用在多个工具中做选择,出错概率更低;
- 聚焦核心逻辑:循环本身是 Agent 的灵魂,过早引入复杂工具集,会掩盖 "带 Shell 的 LLM 本身就是通用 Agent" 这个核心洞察。
4.Agent Loop 阶段完整代码
#!/usr/bin/env python3
# Harness: the loop -- the model's first connection to the real world.
"""
s01_agent_loop.py - The Agent Loop
The entire secret of an AI coding agent in one pattern:
while stop_reason == "tool_use":
response = LLM(messages, tools)
execute tools
append results
+----------+ +-------+ +---------+
| User | ---> | LLM | ---> | Tool |
| prompt | | | | execute |
+----------+ +---+---+ +----+----+
^ |
| tool_result |
+---------------+
(loop continues)
This is the core loop: feed tool results back to the model
until the model decides to stop. Production agents layer
policy, hooks, and lifecycle controls on top.
"""
# ==================== 导入依赖库 ====================
import os
import subprocess # 用于执行系统命令(bash)
try:
# 处理终端输入,修复macOS中文/退格问题
import readline
readline.parse_and_bind('set bind-tty-special-chars off')
readline.parse_and_bind('set input-meta on')
readline.parse_and_bind('set output-meta on')
readline.parse_and_bind('set convert-meta off')
readline.parse_and_bind('set enable-meta-keybindings on')
except ImportError:
pass
# 接入Anthropic Claude官方SDK
from anthropic import Anthropic
# 加载.env环境变量(API KEY、模型ID等)
from dotenv import load_dotenv
load_dotenv(override=True)
# ==================== 初始化Claude客户端 ====================
# 如果配置了自定义base_url,清除多余的认证变量(兼容代理/本地部署)
if os.getenv("ANTHROPIC_BASE_URL"):
os.environ.pop("ANTHROPIC_AUTH_TOKEN", None)
# 创建LLM客户端
client = Anthropic(base_url=os.getenv("ANTHROPIC_BASE_URL"))
# 从环境变量读取模型ID(如claude-3-5-sonnet-20241022)
MODEL = os.environ["MODEL_ID"]
# 系统提示词:告诉模型它是一个编码智能体,只做不说
SYSTEM = f"You are a coding agent at {os.getcwd()}. Use bash to solve tasks. Act, don't explain."
# ==================== 定义工具:仅Bash(s01版本核心) ====================
TOOLS = [{
"name": "bash",
"description": "Run a shell command.",
# 工具入参格式:必须传入command字符串
"input_schema": {
"type": "object",
"properties": {"command": {"type": "string"}},
"required": ["command"],
},
}]
# ==================== Bash执行函数:安全+封装 ====================
def run_bash(command: str) -> str:
"""
安全执行bash命令,防止危险操作,捕获输出和错误
"""
# 黑名单:禁止执行高危命令
dangerous = ["rm -rf /", "sudo", "shutdown", "reboot", "> /dev/"]
if any(d in command for d in dangerous):
return "Error: Dangerous command blocked"
try:
# 执行shell命令,捕获标准输出+标准错误
r = subprocess.run(
command,
shell=True,
cwd=os.getcwd(), # 在当前目录执行
capture_output=True, # 捕获输出
text=True, # 以字符串返回
timeout=120 # 超时2分钟
)
# 合并输出并裁剪长度,防止token爆炸
out = (r.stdout + r.stderr).strip()
return out[:50000] if out else "(no output)"
# 异常处理
except subprocess.TimeoutExpired:
return "Error: Timeout (120s)"
except (FileNotFoundError, OSError) as e:
return f"Error: {e}"
# ==================== 核心:Agent Loop(智能体循环) ====================
def agent_loop(messages: list):
"""
整个Agent的灵魂:循环调用LLM → 执行工具 → 返回结果 → 继续循环
直到模型停止调用工具
"""
while True:
# 1. 调用Claude模型,传入对话历史+工具
response = client.messages.create(
model=MODEL,
system=SYSTEM,
messages=messages,
tools=TOOLS,
max_tokens=8000,
)
# 2. 把模型的回复加入对话历史
messages.append({"role": "assistant", "content": response.content})
# 3. 退出条件:如果模型没有调用工具,直接结束循环
if response.stop_reason != "tool_use":
return
# 4. 模型调用了工具 → 逐条执行
results = []
for block in response.content:
if block.type == "tool_use":
# 黄色打印执行的命令(终端显示)
print(f"\033[33m$ {block.input['command']}\033[0m")
# 执行bash命令
output = run_bash(block.input["command"])
# 打印前200字符输出
print(output[:200])
# 组装工具执行结果(必须带tool_use_id)
results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": output
})
# 5. 把工具结果作为用户消息传回LLM,进入下一轮循环
messages.append({"role": "user", "content": results})
# ==================== 主程序:用户交互入口 ====================
if __name__ == "__main__":
# 全局对话历史
history = []
while True:
try:
# 蓝色提示符
query = input("\033[36ms01 >> \033[0m")
except (EOFError, KeyboardInterrupt):
break
# 输入q/exit退出程序
if query.strip().lower() in ("q", "exit", ""):
break
# 把用户问题加入历史
history.append({"role": "user", "content": query})
# 启动Agent循环
agent_loop(history)
# 打印模型最终回答
response_content = history[-1]["content"]
if isinstance(response_content, list):
for block in response_content:
if hasattr(block, "text"):
print(block.text)
print()
- Agent Loop 只有一个 while True,是整个智能体的大脑
- 退出条件只有一个 :
stop_reason != "tool_use" - 对话历史不断累积:用户提问 → 模型思考 → 工具结果 → 模型再思考
- 工具只有 bash ,完美体现
One loop & Bash is all you need - 安全机制:命令黑名单、超时限制、输出截断
- 完全可扩展:后面加工具只需要加 handler,循环完全不变
二、Tool Use:不修改循环,优雅扩展模型能力
1. 问题:为什么后来又从 1 个工具扩展到 4 个?
只用 Bash 虽然能跑,但实际用下来有很多痛点:
cat读取大文件时截断不可预测;sed处理特殊字符时极易出错;- 所有操作都走 Shell,安全边界不可控,容易出现路径逃逸问题。
这时候就需要引入专用工具,但 ClaudeCode 的设计原则是:加工具,不改循环。
2. 核心解法:工具分发器,用字典替代硬编码
ClaudeCode 用一个TOOL_HANDLERS字典实现了工具的优雅扩展,循环本身完全不用修改:
# 工具分发映射:工具名 → 处理函数
TOOL_HANDLERS = {
"bash": lambda **kw: run_bash(kw["command"]),
"read_file": lambda **kw: run_read(kw["path"], kw.get("limit")),
"write_file": lambda **kw: run_write(kw["path"], kw["content"]),
"edit_file": lambda **kw: run_edit(kw["path"], kw["old_text"], kw["new_text"]),
}
# 循环中执行工具的逻辑(和v0版本相比,只改了工具调用部分)
for block in response.content:
if block.type == "tool_use":
# 按工具名查找对应的处理函数
handler = TOOL_HANDLERS.get(block.name)
output = handler(**block.input) if handler else f"Unknown tool: {block.name}"
results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": output,
})
3. 四个核心工具:覆盖 95% 编程任务的极简组合
ClaudeCode 最终选择了 4 个工具,而非更多,背后的设计逻辑非常清晰:
| 工具 | 核心能力 | 解决的问题 |
|---|---|---|
bash |
执行任意命令、运行程序、管理进程 | 通用操作,处理复杂命令和程序运行 |
read_file |
带行号的精确文件读取 | 避免cat截断问题,精准读取文件内容 |
write_file |
创建 / 覆盖文件内容 | 避免 Bash 重定向的编码、权限问题 |
edit_file |
精确字符串替换 | 避免sed/awk处理特殊字符的错误 |
为什么是这四个?
- 覆盖了约 95% 的编程任务,没有冗余工具;
- 工具越少,模型的认知负担越轻,选错工具的概率越低;
- 减少了需要维护的 Schema 和边界情况,系统更稳定。
4. 关键安全设计:路径沙箱,防止工作区逃逸
在专用工具中,ClaudeCode 引入了safe_path机制,彻底解决了 Bash 的安全隐患:
def safe_path(p: str) -> Path:
path = (WORKDIR / p).resolve()
# 检查路径是否在工作区内,防止../等路径逃逸
if not path.is_relative_to(WORKDIR):
raise ValueError(f"Path escapes workspace: {p}")
return path
# read_file工具使用沙箱路径
def run_read(path: str, limit: int = None) -> str:
text = safe_path(path).read_text()
lines = text.splitlines()
if limit and limit < len(lines):
lines = lines[:limit]
return "\n".join(lines)[:50000]
5. 设计决策:为什么不用更复杂的编排框架?
ClaudeCode 的工具设计,和市面上很多 Agent 框架形成了鲜明对比:
- 其他框架 :依赖
ReAct循环、Thought/Action/Observation解析、LangChain 式链式编排,假设模型需要框架 "脚手架" 才能当 Agent; - ClaudeCode:认为模型本身已经知道如何当 Agent,它只需要工具来和世界交互,不需要框架定义它的行为。
这也是为什么 ClaudeCode 的工具系统没有引入规划器、任务队列、状态机 ------模型的思维链本身就是计划,循环只是不断询问模型下一步做什么。
6.Tool Use 阶段完整代码
#!/usr/bin/env python3
# Harness: tool dispatch -- expanding what the model can reach.
"""
s02_tool_use.py - Tools
The agent loop from s01 didn't change. We just added tools to the array
and a dispatch map to route calls.
+----------+ +-------+ +------------------+
| User | ---> | LLM | ---> | Tool Dispatch |
| prompt | | | | { |
+----------+ +---+---+ | bash: run_bash |
^ | read: run_read |
| | write: run_wr |
+----------+ edit: run_edit |
tool_result| } |
+------------------+
Key insight: "The loop didn't change at all. I just added tools."
"""
# ==================== 1. 导入依赖库 ====================
import os
import subprocess # 执行bash命令
from pathlib import Path # 处理文件路径(安全路径核心)
from anthropic import Anthropic # Claude官方SDK
from dotenv import load_dotenv # 加载环境变量
# 加载.env配置文件
load_dotenv(override=True)
# 如果配置了自定义接口地址,清除冲突的认证变量
if os.getenv("ANTHROPIC_BASE_URL"):
os.environ.pop("ANTHROPIC_AUTH_TOKEN", None)
# ==================== 2. 全局配置初始化 ====================
# 定义工作目录:所有文件操作都限制在这个目录内(安全沙箱)
WORKDIR = Path.cwd()
# 初始化Claude客户端
client = Anthropic(base_url=os.getenv("ANTHROPIC_BASE_URL"))
# 从环境变量获取模型ID
MODEL = os.environ["MODEL_ID"]
# 系统提示词:定义Agent身份,只执行不解释
SYSTEM = f"You are a coding agent at {WORKDIR}. Use tools to solve tasks. Act, don't explain."
# ==================== 3. 核心安全机制:路径沙箱 ====================
def safe_path(p: str) -> Path:
"""
安全路径校验:防止模型通过 ../ 等方式逃逸到工作目录外
这是Agent的核心安全防线
"""
# 拼接绝对路径并标准化
path = (WORKDIR / p).resolve()
# 校验路径是否在允许的工作目录内
if not path.is_relative_to(WORKDIR):
raise ValueError(f"Path escapes workspace: {p}")
return path
# ==================== 4. 工具执行函数(4大核心工具) ====================
def run_bash(command: str) -> str:
"""
工具1:执行bash命令
带安全黑名单、超时、输出限制
"""
# 高危命令黑名单
dangerous = ["rm -rf /", "sudo", "shutdown", "reboot", "> /dev/"]
if any(d in command for d in dangerous):
return "Error: Dangerous command blocked"
try:
# 执行命令,捕获输出和错误
r = subprocess.run(
command, shell=True, cwd=WORKDIR,
capture_output=True, text=True, timeout=120
)
out = (r.stdout + r.stderr).strip()
# 限制输出长度,防止token爆炸
return out[:50000] if out else "(no output)"
except subprocess.TimeoutExpired:
return "Error: Timeout (120s)"
def run_read(path: str, limit: int = None) -> str:
"""
工具2:读取文件内容
支持行数限制,自动截断超长文件,比cat更稳定
"""
try:
text = safe_path(path).read_text()
lines = text.splitlines()
# 如果设置了行数限制,进行截断
if limit and limit < len(lines):
lines = lines[:limit] + [f"... ({len(lines) - limit} more lines)"]
return "\n".join(lines)[:50000]
except Exception as e:
return f"Error: {e}"
def run_write(path: str, content: str) -> str:
"""
工具3:写入文件(新建/覆盖)
自动创建父目录,安全路径校验
"""
try:
fp = safe_path(path)
fp.parent.mkdir(parents=True, exist_ok=True) # 自动创建文件夹
fp.write_text(content)
return f"Wrote {len(content)} bytes to {path}"
except Exception as e:
return f"Error: {e}"
def run_edit(path: str, old_text: str, new_text: str) -> str:
"""
工具4:精确编辑文件(替换指定文本)
必须精准匹配原文,避免sed乱改文件
"""
try:
fp = safe_path(path)
content = fp.read_text()
# 必须找到原文才允许替换,防止误修改
if old_text not in content:
return f"Error: Text not found in {path}"
fp.write_text(content.replace(old_text, new_text, 1)) # 只替换第一次出现的内容
return f"Edited {path}"
except Exception as e:
return f"Error: {e}"
# ==================== 5. 核心设计:工具分发字典 ====================
# 工具名 -> 执行函数 的映射表
# 【关键】加新工具只需要在这里加一行,循环完全不用改!
TOOL_HANDLERS = {
"bash": lambda **kw: run_bash(kw["command"]),
"read_file": lambda **kw: run_read(kw["path"], kw.get("limit")),
"write_file": lambda **kw: run_write(kw["path"], kw["content"]),
"edit_file": lambda **kw: run_edit(kw["path"], kw["old_text"], kw["new_text"]),
}
# ==================== 6. 工具定义(给LLM看的Schema) ====================
# 告诉Claude模型:有哪些工具、参数是什么
TOOLS = [
{
"name": "bash",
"description": "Run a shell command.",
"input_schema": {
"type": "object",
"properties": {"command": {"type": "string"}},
"required": ["command"]
}
},
{
"name": "read_file",
"description": "Read file contents.",
"input_schema": {
"type": "object",
"properties": {"path": {"type": "string"}, "limit": {"type": "integer"}},
"required": ["path"]
}
},
{
"name": "write_file",
"description": "Write content to file.",
"input_schema": {
"type": "object",
"properties": {"path": {"type": "string"}, "content": {"type": "string"}},
"required": ["path", "content"]
}
},
{
"name": "edit_file",
"description": "Replace exact text in file.",
"input_schema": {
"type": "object",
"properties": {"path": {"type": "string"}, "old_text": {"type": "string"}, "new_text": {"type": "string"}},
"required": ["path", "old_text", "new_text"]
}
},
]
# ==================== 7. Agent Loop(和s01完全一样!) ====================
def agent_loop(messages: list):
"""
核心循环:和s01没有任何区别!
这就是ClaudeCode的设计精髓:循环不变,只扩展工具
"""
while True:
# 1. 调用LLM
response = client.messages.create(
model=MODEL, system=SYSTEM, messages=messages,
tools=TOOLS, max_tokens=8000,
)
# 2. 保存模型回复到对话历史
messages.append({"role": "assistant", "content": response.content})
# 3. 退出条件:模型不再调用工具
if response.stop_reason != "tool_use":
return
# 4. 执行工具:通过分发字典查找对应函数
results = []
for block in response.content:
if block.type == "tool_use":
# 从字典中获取工具处理函数(核心改动点)
handler = TOOL_HANDLERS.get(block.name)
# 执行函数并传参
output = handler(**block.input) if handler else f"Unknown tool: {block.name}"
# 打印执行日志
print(f"> {block.name}:")
print(output[:200])
# 组装结果返回给LLM
results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": output
})
# 5. 把工具结果加入对话历史,开启下一轮循环
messages.append({"role": "user", "content": results})
# ==================== 8. 主程序:用户交互入口 ====================
if __name__ == "__main__":
history = [] # 全局对话历史
while True:
try:
query = input("\033[36ms02 >> \033[0m") # 终端蓝色提示符
except (EOFError, KeyboardInterrupt):
break
# 退出指令
if query.strip().lower() in ("q", "exit", ""):
break
# 存入用户提问并启动Agent
history.append({"role": "user", "content": query})
agent_loop(history)
# 打印模型最终回答
response_content = history[-1]["content"]
if isinstance(response_content, list):
for block in response_content:
if hasattr(block, "text"):
print(block.text)
print()
-
循环完全没变
agent_loop函数和 Agent Loop 版 一模一样,证明循环是 Agent 的灵魂,永远不变。 -
工具分发字典(TOOL_HANDLERS) 用字典替代了硬编码
if-else,加新工具 = 加一行代码,极致优雅。 -
路径沙箱(safe_path) 所有文件操作都必须通过校验,彻底防止模型逃逸到系统目录,生产级安全设计。
三、Let's see 核心设计对比:从 v0 到 v1,我们到底改了什么?
| 组件 | v0(仅 Bash) | v1(四工具扩展) | 核心优势 |
|---|---|---|---|
| Agent Loop | while True + stop_reason |
完全不变 | 核心逻辑稳定,后续所有迭代都基于这个循环 |
| 工具数量 | 1 个(仅 Bash) | 4 个(bash/read/write/edit) | 覆盖更多场景,同时控制模型负担 |
| 工具调用 | 硬编码 Bash 调用 | TOOL_HANDLERS字典分发 |
新增工具无需修改循环,扩展性极强 |
| 路径安全 | 无 | safe_path()沙箱控制 |
防止工作区逃逸,提升安全性 |
| 模型交互 | 纯 Bash 命令 | 结构化 JSON Schema 调用 | 消除解析歧义,提升工具调用成功率 |
四、In the end:极简设计才是工程之美
ClaudeCode 的 Agent Loop 和 Tool Use 设计,给我们的最大启发,从来不是 "怎么写一个 Agent 循环",而是如何用极简的核心设计,支撑起复杂的能力扩展:
- 循环是灵魂,永远不变:后续的子代理、规划功能,都是在这个循环之上叠加,而不是修改它;
- 工具是手脚,可插拔扩展:新增工具只需要加一个处理函数和 Schema,循环完全不用动;
- 模型是大脑,不被框架绑架:让模型自己决定做什么、怎么做,框架只提供和世界交互的能力。
