Learn Claude Code Agent 开发 | 2、插拔式工具系统:扩展功能不修改核心循环
整体概述
多工具分发核心实现是基础智能体循环的直接扩展,核心思想就是:
"加一个工具, 只加一个 handler" -- 循环不用动, 新工具注册进 dispatch map 就行。
这是 智能体工具分发机制 的核心实现,体现了 Harness 层最重要的设计原则:扩展工具不修改核心循环。
解决的核心问题
单bash工具存在很多缺陷:
cat截断不可预测,sed遇到特殊字符容易崩溃- 每次 bash 调用都是不受约束的安全面,容易产生越权操作
- 扩展工具需要修改核心循环代码,违反开闭原则
核心设计思想
+--------+ +-------+ +------------------+
| User | ---> | LLM | ---> | Tool Dispatch |
| prompt | | | | { |
+--------+ +---+---+ | bash: run_bash |
^ | read: run_read |
| | write: run_wr |
+-----------+ edit: run_edit |
tool_result | } |
+------------------+
核心创新:用**分发字典(dispatch map)**替代硬编码的工具调用,实现工具的插拔式扩展。
逐段解析
1. 环境初始化
python
WORKDIR = Path.cwd()
client = Anthropic(base_url=os.getenv("ANTHROPIC_BASE_URL"))
MODEL = os.environ["MODEL_ID"]
SYSTEM = f"You are a coding agent at {WORKDIR}. Use tools to solve tasks. Act, don't explain."
- 新增
WORKDIR常量作为工作区根目录,是路径沙箱的基础 - 系统提示从 "Use bash" 改成 "Use tools",明确支持多工具
2. 路径安全沙箱 ⭐ 核心安全机制
python
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
功能:防止智能体访问工作目录之外的文件
- 将输入路径解析为绝对路径
- 校验路径是否在
WORKDIR之下 - 如果路径逃逸(比如
../etc/passwd)直接抛出错误 - 所有文件操作工具都会先调用这个函数做安全校验
3. 工具实现
提供了4个工具,每个工具都是独立的处理函数:
(1)bash 工具
和基础实现基本一致,只是将 cwd 固定为 WORKDIR,提升安全性。
(2)read_file 工具
python
def run_read(path: str, limit: int = None) -> str:
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}"
功能:安全读取文件内容
- 支持
limit参数限制读取行数,避免大文件撑爆上下文 - 超过行数限制时会提示剩余行数,更友好
- 统一异常处理,错误信息直接返回给模型
(3)write_file 工具
python
def run_write(path: str, content: str) -> str:
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}"
功能:写入文件
- 自动创建不存在的父目录
- 返回写入结果给模型确认
(4)edit_file 工具
python
def run_edit(path: str, old_text: str, new_text: str) -> str:
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}"
功能:精确替换文件内容
- 只替换第一次出现的匹配文本,避免误修改
- 匹配不到内容时明确返回错误,模型可以调整匹配文本重试
4. 工具分发字典 ⭐ 核心扩展机制
python
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"]),
}
- 键是工具名,值是对应的处理函数
- 用 lambda 统一参数传递,适配不同工具的参数差异
- 加新工具只需要在这里加一行映射,不需要修改其他代码
5. 工具定义数组
python
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"]}},
]
- 给 LLM 提供的工具描述,遵循 Anthropic 函数调用格式
- 每个工具包含名称、功能描述、参数结构(JSON Schema)
- LLM 根据这些描述决定调用什么工具、传递什么参数
6. 核心智能体循环
和基础智能体循环几乎完全一致,只有工具调用部分做了修改:
python
def agent_loop(messages: list):
while True:
response = client.messages.create(
model=MODEL, system=SYSTEM, messages=messages,
tools=TOOLS, max_tokens=8000,
)
messages.append({"role": "assistant", "content": response.content})
if response.stop_reason != "tool_use":
return
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}: {output[:200]}")
results.append({"type": "tool_result", "tool_use_id": block.id, "content": output})
messages.append({"role": "user", "content": results})
关键变化:
- 去掉了硬编码的
run_bash调用 - 改为从
TOOL_HANDLERS字典中根据工具名查找处理函数 - 支持未知工具的错误提示,鲁棒性更强
- 核心循环逻辑完全不变,实现了对扩展开放、对修改关闭的开闭原则
7. 交互式主循环
和基础实现完全一致,仅提示符做了修改。
基础版对比
| 组件 | 基础版(s01) | 多工具版(s02) |
|---|---|---|
| 工具数量 | 1个(仅 bash) | 4个(bash, read, write, edit) |
| 工具调用方式 | 硬编码 bash 调用 | TOOL_HANDLERS 分发字典 |
| 路径安全 | 无 | safe_path() 沙箱防护 |
| Agent 循环 | 固定 | 完全不变 |
设计优势
-
扩展性极强:加新工具只需要两步:
- 写工具处理函数
- 在
TOOL_HANDLERS和TOOLS中注册 - 核心循环不需要动一行代码
-
安全性提升:
- 路径沙箱防止文件越权访问
- 专用工具比 bash 更可控,减少安全风险
-
稳定性更高:
- 核心循环经过验证不需要修改,减少出问题的概率
- 工具之间独立,修改一个工具不影响其他功能
使用示例
启动后可以测试这些指令:
bash
python agents/s02_tool_use.py
s02 >> Read the file requirements.txt
> read_file: [文件内容]
s02 >> Create a file called greet.py with a greet(name) function
> write_file: Wrote 50 bytes to greet.py
s02 >> Edit greet.py to add a docstring to the function
> edit_file: Edited greet.py
s02 >> Read greet.py to verify the edit worked
> read_file: [修改后的文件内容]
核心收获
这个版本建立了 Harness 层最核心的设计范式:
循环永远不变,变化的只有工具和分发规则
后续的所有功能扩展都是在这个基础上扩展更多工具和机制,核心循环始终保持稳定。这种设计思想也贯穿了整个 Claude Code 的架构。
本内容参考开源项目 learn-claude-code