2026年6月7日 4 分钟阅读

从零手写 AI Agent 工具系统:给 Agent 装上「双手」的七步教程

tinyash 0 条评论

核心观点: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 命令,你再也不需要为每个程序单独写工具——curlgitdockerpip 这些 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)
    )

注意 offsetlimit 参数——没有分页读取时,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),在原始内容基础上增加了中文注释、实践建议和框架无关的设计视角。

发表评论

你的邮箱地址不会被公开,带 * 的为必填项。