乐于分享
好东西不私藏

别再只会调 API 了:从零手搓带工具调用的 AI Agent,Bash、读写文件、网页抓取一口气跑通

别再只会调 API 了:从零手搓带工具调用的 AI Agent,Bash、读写文件、网页抓取一口气跑通

很多人第一次写 AI Agent,最后都会写成一个“带记忆的聊天壳”:接模型、读用户输入、把上下文塞回去,然后等模型回话。

这当然能跑,但没什么用。因为它只能“说”,不能“做”。

真正的 Agent 起点,不是换更强模型,也不是把 Prompt 写得更玄,而是给它一组能被调用的工具:跑命令、读文件、列目录、写文件、改文件、抓网页。只要这层搭起来,一个玩具聊天壳就开始变成可以操作电脑的执行系统。

这篇原文是一个“从零搭 Agent Harness”系列里的工具调用篇,发布时间是 2026-05-31,但最近仍在 Hacker News 上保持较高讨论热度。它适合拿来拆成一套更直接的实战路线:不依赖现成 Agent 框架,先把工具调用这层最小骨架写明白。

别再把 Agent 想成聊天框:没有工具,它什么都干不了

最小 Agent Harness 通常只有四块:

模块
作用
模型连接
把用户请求发给 LLM
输入循环
接收用户命令
上下文记忆
保存多轮对话
Agent Loop
持续让模型响应

这套东西能回答问题,但它不能真正改变环境。

你问它“帮我读一下项目里有哪些文件”,如果没有工具,它只能猜。你让它“把结果写到 summary.md”,如果没有文件写入工具,它也只能嘴上说“已经完成”。

工具调用解决的就是这个断点:让模型从文本生成,进入可执行动作。

一个工具可以很简单,比如一个 Python 函数;也可以很复杂,比如一个 MCP Server、一个 HTTP API、一个数据库查询入口。关键不在形式,而在三件事:

要素
说明
工具名
模型要知道可以调用谁
参数 Schema
模型要知道怎么传参
执行函数
系统要能把调用落到真实动作上

这就是工具调用 Agent 的底层骨架。

先做 6 个基础工具,Agent 才算真正“有手”

如果你要写一个最小可用的本地 Agent,我建议先做这 6 个工具:

工具
用途
风险
run_bash
执行 Shell 命令
最高
read_file
读取文件内容
中等
list_files
查看目录结构
write_file
创建或覆盖文件
edit_file
局部替换文件内容
fetch_url
抓取网页内容
中等

这里面最危险的是 run_bash。它很强,因为模型会用终端做很多事;它也很吓人,因为一旦权限失控,模型同样能删文件、改配置、跑未知脚本。

所以第一版可以让它跑,但别裸跑。至少要先限制工作目录、记录日志,并在危险命令上加人工确认。

下面给一个简化版工具定义思路:

python
from pathlib import Path
import subprocess
import urllib.request
ROOT = Path.cwd()
def safe_path(path: str) -> Path:
target = (ROOT / path).resolve()
if not str(target).startswith(str(ROOT.resolve())):
raise ValueError("path escapes workspace")
return target
def list_files(path: str = ".") -> list[str]:
target = safe_path(path)
return [str(item.relative_to(ROOT)) for item in target.iterdir()]
def read_file(path: str) -> str:
return safe_path(path).read_text(encoding="utf-8")
def write_file(path: str, content: str) -> str:
target = safe_path(path)
target.parent.mkdir(parents=True, exist_ok=True)
target.write_text(content, encoding="utf-8")
return f"wrote {path}"
def edit_file(path: str, old: str, new: str) -> str:
target = safe_path(path)
text = target.read_text(encoding="utf-8")
if old not in text:
raise ValueError("old text not found")
target.write_text(text.replace(old, new, 1), encoding="utf-8")
return f"edited {path}"
def fetch_url(url: str) -> str:
req = urllib.request.Request(url, headers={"User-Agent": "agent/1.0"})
with urllib.request.urlopen(req, timeout=15) as resp:
return resp.read().decode("utf-8", errors="replace")[:12000]
def run_bash(command: str) -> str:
result = subprocess.run(
command,
shell=True,
cwd=ROOT,
text=True,
capture_output=True,
timeout=30,
)
return result.stdout + result.stderr

这段代码不是生产级安全方案,只是最小骨架。真正上线前,run_bash 必须继续收紧,比如禁用 rm -rf、限制网络、限制环境变量、限制最大输出,甚至放进沙箱。

工具函数不够,还要把 Schema 喂给模型

模型不会自动知道你写了哪些 Python 函数。你必须把工具描述成它能理解的 Schema。

结构大概长这样:

python
TOOL_SCHEMAS = [
{
"name": "read_file",
"description": "Read a UTF-8 text file inside the workspace.",
"input_schema": {
"type": "object",
"properties": {
"path": {"type": "string", "description": "Relative file path"}
},
"required": ["path"],
},
},
{
"name": "edit_file",
"description": "Replace the first matching text fragment in a file.",
"input_schema": {
"type": "object",
"properties": {
"path": {"type": "string"},
"old": {"type": "string"},
"new": {"type": "string"},
},
"required": ["path", "old", "new"],
},
},
]

写 Schema 时别省描述。很多工具调用失败,不是模型“笨”,而是 Schema 太含糊。

比如 edit_file 只写“edit a file”,模型很容易直接传一大段新文件内容。你要明确告诉它:这是局部替换,只替换第一个匹配片段;如果没找到旧文本,就会失败。

工具描述越具体,Agent 越少瞎猜。

用注册表把“模型想调用”变成“系统真执行”

有了工具函数和 Schema,还差一层注册表。

模型返回的不是 Python 函数调用,而是一个结构化请求:

json
{"name": "read_file", "input": {"path": "README.md"}}

你要把这个请求路由到真实函数:

python
TOOL_REGISTRY = {
"list_files": list_files,
"read_file": read_file,
"write_file": write_file,
"edit_file": edit_file,
"fetch_url": fetch_url,
"run_bash": run_bash,
}
def execute_tool(name: str, tool_input: dict) -> str:
if name not in TOOL_REGISTRY:
raise ValueError(f"unknown tool: {name}")
result = TOOL_REGISTRY[name](**tool_input)
return str(result)

这层看起来很普通,但它是工具调用 Agent 的安全边界。后面你要加日志、权限、确认、重试、超时、脱敏,基本都会加在这里。

我不建议把工具函数直接暴露给模型。中间一定要有一个可控执行层。

把工具塞进 Agent Loop,真正的执行闭环才开始

Agent Loop 的核心逻辑可以拆成四步:

1.把用户输入加入消息历史。
2.把历史、系统提示词、工具 Schema 发给模型。
3.如果模型请求工具调用,就执行工具。
4.把工具结果追加进上下文,再让模型继续推理。

伪代码大概是这样:

python
def agent_loop(client):
messages = []
while True:
user_input = input("> ").strip()
if user_input in {"exit", "quit"}:
break
messages.append({"role": "user", "content": user_input})
response = client.messages.create(
model="your-model",
system=(
"You are a helpful assistant. You can use tools to read files, "
"write files, edit files, fetch URLs, and run safe shell commands."
),
messages=messages,
tools=TOOL_SCHEMAS,
)
for item in response.content:
if item.type == "text":
print(item.text)
messages.append({"role": "assistant", "content": item.text})
if item.type == "tool_use":
tool_result = execute_tool(item.name, item.input)
messages.append({
"role": "user",
"content": [{
"type": "tool_result",
"tool_use_id": item.id,
"content": tool_result,
}],
})

不同模型 SDK 的字段名会不一样,但思路一样:模型提出工具请求,程序执行工具,再把执行结果喂回去。

这一步跑通后,你的 Agent 就不再只是“回答”。它可以开始做事。

第一条测试任务:抓网页,再写成文件

别一上来就让 Agent 改项目代码。第一条测试任务最好可控一点:

text
请抓取 https://www.ruxu.dev 的文章列表,总结成 Markdown,并保存到 ruxu.md。

一个正常工具调用链应该长这样:

text
user request
-> fetch_url("https://www.ruxu.dev")
-> write_file("ruxu.md", "...markdown summary...")
-> final answer

如果它没先抓网页就直接写总结,说明模型在猜。

如果它抓了网页但写文件失败,说明文件工具或路径权限有问题。

如果它把内容写到了奇怪位置,说明 safe_path() 和工具描述还不够硬。

这类测试的价值在于,它能同时验证三件事:

验证点
你要看什么
工具选择
模型有没有选对 fetch_url 和 write_file
参数生成
URL、路径、内容是否正确
上下文回填
写文件后模型是否知道任务完成

别急着做复杂任务。工具调用层没稳,后面加规划、记忆、多 Agent 都是在堆风险。

真正的坑:工具越强,越要先设计刹车

从零手搓 Agent 很容易让人上头。跑通 run_bash 的那一刻,你会觉得它突然“活了”。

但这里千万别省安全设计。

至少补上这几类护栏:

护栏
最低要求
工作目录限制
所有文件操作不得逃出项目目录
命令黑名单
拦截明显危险命令
超时控制
每个工具都要有 timeout
输出截断
防止一次命令塞爆上下文
操作日志
每次工具调用都记录 name、input、duration、error
人工确认
写文件、改文件、跑命令前可要求确认

尤其是 write_file 和 run_bash。这两个工具一旦给出去,Agent 就有了真实副作用。你可以让它用,但不能让它悄悄用。

我的建议是:第一版先让它“能做”,第二版立刻让它“可控”,不要等出事后再补。

总结:从聊天壳到 Agent,中间只差一个可控工具层

这篇教程最有价值的地方,不是某段代码多复杂,而是把 Agent 的核心拆得很干净:

text
工具函数
-> 工具 Schema
-> 工具注册表
-> Agent Loop
-> 工具结果回填

只要这条链路跑通,你就能理解大多数 Agent 框架在底层做什么。

Claude Code、Codex、Cursor、Hermes Agent 当然更完整,但它们绕不开同一件事:模型必须知道有哪些工具,系统必须安全执行工具,执行结果必须回到上下文。

所以,别只停在“调 API”。自己手搓一遍工具调用 Agent,你会更清楚 Agent 为什么强,也会更清楚它为什么危险。

参考资料:

1.原文:https://www.ruxu.dev/articles/ai/build-an-ai-agent-with-tools/
   创作来源:个人观点仅供参考