从零手写 AI Agent 工具系统:给 Agent 装上「双手」的七步教程
核心观点:Agent 再聪明,没有工具也只是「纸上谈兵」。本文从零实现 7 个核心工具——bash、文件读写、代码搜索、网页抓取——带你理解现代 AI Agent 工具系统的完整设计。
当 Agent 只能跟你聊天,什么实事都干不了时,它只是一个「会说话的 AI 壳子」。要让 Agent 真正干活,必须给它装上工具系统(Tool System)。
工具是什么?
在 AI Agent 的语境中,工具(Tool) 是指暴露给大语言模型调用的函数或程序。一个工具可以简单到一行 Python 函数,也可以复杂到一个 MCP 服务器背后的完整 API 服务。
早期 LLM 是通过文本约定来「调用工具」的——模型输出 Action: run_bash,然后代码解析这段文本并执行。这种方式不可靠:模型经常拼错函数名、忘记参数、格式跑偏。现代 LLM 已经内置了 原生工具调用(Native Tool Calling)——模型直接输出 JSON 格式的 function call,框架负责校验和路由。这大幅降低了工具调用的幻觉率。
第一步:Bash 执行工具 — Agent 的「万能遥控器」
Bash 工具是所有 Agent 工具中最强大也最危险的一个。有了它,Agent 可以运行脚本、安装依赖、操作 Git、启动服务——你需要的几乎所有操作都能通过 shell 完成,再也不用为每个程序单独写工具了。
import subprocess
def run_bash(command: str) -> str:
"""Run a bash command and return its output."""
result = subprocess.run(
command, shell=True, text=True, capture_output=True
)
output = result.stdout
if result.stderr:
output += f"\nSTDERR:\n{result.stderr}"
return output or "(no output)"
因为 Agent 可以执行任意 shell 命令,你再也不需要为每个程序单独写工具——curl、git、docker、pip 这些 LLM 本来就认识它们。但一把万能钥匙既能开门也能把自己锁死,bash 工具的安全性是未来需要重点处理的问题(后续系列会覆盖沙箱方案)。
第二步:文件读取工具 — Agent 的「阅读能力」
文件读取是编码 Agent 最核心的能力之一。没有它,Agent 无法理解你的代码库:
from pathlib import Path
def read_file(path: str, offset: int = 1, limit: int = 200) -> str:
"""Read lines from a file, with optional offset and limit."""
p = Path(path)
if not p.exists():
return f"Error: file not found: {path}"
lines = p.read_text(errors="replace").splitlines()
selected = lines[offset - 1 : offset - 1 + limit]
return "\n".join(
f"{offset + i}:{line}" for i, line in enumerate(selected)
)
注意 offset 和 limit 参数——没有分页读取时,Agent 可能一次性读入几十万行代码撑爆上下文。行号前缀也很重要,因为 Agent 后续要引用特定行进行修改。
第三步:文件搜索工具 — 让 Agent 找到代码在哪里
Agent 需要在上千个文件中快速定位目标。glob 和 grep 配合使用,构成了「先发现、再深挖」的文件探索能力:
import glob as glob_module
def glob_files(pattern: str, path: str = ".") -> str:
"""Find files matching a glob pattern inside a directory."""
matches = glob_module.glob(f"{path}/**/{pattern}", recursive=True)
matches += glob_module.glob(f"{path}/{pattern}")
unique = sorted(set(matches))
return "\n".join(unique) if unique else "(no matches)"
再配合 grep 工具——搜索文件内容中的特定模式:
import re
def grep(pattern: str, path: str = ".", include: str = "*") -> str:
"""Search file contents for a regex pattern."""
results = []
for filepath in glob_module.glob(f"{path}/**/{include}", recursive=True):
fp = Path(filepath)
if not fp.is_file():
continue
try:
for i, line in enumerate(fp.read_text(errors="replace").splitlines(), 1):
if re.search(pattern, line):
results.append(f"{filepath}:{i}: {line}")
except OSError:
pass
return "\n".join(results) if results else "(no matches)"
标准工作流:glob_files(".ts") 找到 TypeScript 文件 → grep("useEffect", include=".ts") 在这些文件中搜索特定函数。这种「先发现、再深挖」模式是 AI 编码工具快速上手陌生代码库的关键。
第四步:文件写入工具 — Agent 的「创作能力」
没有写文件的能力,Agent 只是一个「只会看不会做」的顾问:
def write_file(path: str, content: str) -> str:
"""Write content to a file, creating it if it doesn't exist."""
p = Path(path)
p.parent.mkdir(parents=True, exist_ok=True)
p.write_text(content)
return f"Wrote {len(content)} bytes to {path}"
注意 p.parent.mkdir——这确保了即使目标文件的上级目录不存在,Agent 也能自动创建。这看似小细节,但在真实的编码场景中至关重要:Agent 生成一个新模块时,往往需要同时创建 src/services/api.ts 等深层嵌套的目录结构。
第五步:编辑文件工具 — 精准修改而非整体覆盖
单纯地写文件还不够——Agent 还需要精准修改现有文件的部分内容。整体重写一个 500 行的文件既浪费 Token,又容易引入意外错误:
def edit_file(path: str, old_string: str, new_string: str) -> str:
"""Replace the first occurrence of old_string with new_string."""
p = Path(path)
if not p.exists():
return f"Error: file not found: {path}"
content = p.read_text(errors="replace")
if old_string not in content:
return "Error: old_string not found"
new_content = content.replace(old_string, new_string, 1)
p.write_text(new_content)
return f"Replaced one occurrence in {path}"
这个 replace(old, new, 1) 只替换第一次出现——如果要批量替换,可以用 replace_all 参数。这种「搜索-替换」模式是目前最流行的 AI 编码工具(如 Claude Code、Cursor)的标准编辑方式,因为它比「定位到第 N 行并修改」更鲁棒。
第六步:网页抓取工具 — Agent 的「网络访问」
Agent 需要阅读文档、查看 GitHub Issue、拉取 API 响应。一个干净的 web_fetch 工具可以让 Agent 直接获取网页内容:
import urllib.request, re
def web_fetch(url: str) -> str:
"""Fetch a public URL and return plain-text content (up to 2 MB)."""
if not url.startswith(("http://", "https://")):
return "Error: only http/https URLs are supported"
try:
resp = urllib.request.urlopen(url, timeout=15)
raw = resp.read(2 * 1024 * 1024) # 2 MB limit
charset = resp.headers.get_content_charset() or "utf-8"
text = raw.decode(charset, errors="replace")
# 简单 HTML 标签剥离
text = re.sub(r"<[^>]+>", "\n", text)
return re.sub(r"\n{3,}", "\n\n", text).strip()
except Exception as e:
return f"Error fetching {url}: {e}"
关键设计点:2MB 上限防止大型页面撑爆 Agent 上下文;仅支持 http/https 防止 SSRF 攻击;timeout=15 防止 Agent 因一个卡住的请求而长时间的等待。
第七步:把工具缝合到 Agent 循环中
有了工具函数,还需要两样东西:工具注册表和工具调度器。
把工具名到实现函数的映射字典称为工具注册表:
def get_tool_registry():
return {
"run_bash": run_bash,
"read_file": read_file,
"glob_files": glob_files,
"grep": grep,
"write_file": write_file,
"edit_file": edit_file,
"web_fetch": web_fetch,
}
同时还需要为每个工具编写 JSON Schema——这是 LLM 理解工具的描述方式:
def get_tool_schemas():
return [
{
"type": "function",
"function": {
"name": "run_bash",
"description": "Run a bash command on the user's machine.",
"parameters": {
"type": "object",
"properties": {
"command": {"type": "string", "description": "The bash command to execute."}
},
"required": ["command"],
},
},
},
# ... 其他工具的 schema(结构相同,参数不同)
]
最后,在 Agent 主循环中增加工具调用处理逻辑:
import json
def handle_tool_calls(tool_calls, messages):
"""Execute each tool the LLM requested and append results to messages."""
for tool_call in tool_calls:
name = tool_call.function.name
args = json.loads(tool_call.function.arguments)
print(f" [tool] {name}({args})")
if name not in TOOL_REGISTRY:
result = f"Error: unknown tool '{name}'"
else:
result = TOOL_REGISTRY[name](**args)
print(f" [tool result] {result[:200]}...")
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": result,
})
就是这样!当 Agent 说「我需要读取 config.json」,LLM 会输出一个 JSON 格式的 read_file(path="config.json") 调用,你的调度器捕获它、执行它、把结果塞回对话上下文,Agent 继续处理下一个任务。
总结
本文实现的 7 个核心工具——bash、read_file、glob_files、grep、write_file、edit_file、web_fetch——构成了现代 AI Agent 工具系统的基础。几乎所有主流 AI 编码工具(Claude Code、Codex、Cursor、OpenCode)都涵盖这些能力。
但它们也只是起点。真正的生产级工具系统还需要:MCP 协议集成外部服务、安全沙箱隔离危险操作、权限管理控制 Agent 的访问范围。这些我们将在后续系列逐一覆盖。
本文参考了 ruxudev 的系列教程(ruxu.dev),在原始内容基础上增加了中文注释、实践建议和框架无关的设计视角。