1. 环境初始化(基础准备)
python
#!/usr/bin/env python3
"""
s02_tool_use.py-工具
s01的代理循环没有改变。我们只是将工具添加到数组中
和路由呼叫的调度地图。
+----------+ +-------+ +------------------+
| User | ---> | LLM | ---> | Tool Dispatch |
| prompt | | | | { |
+----------+ +---+---+ | bash: run_bash |
^ | read: run_read |
| | write: run_wr |
+----------+ edit: run_edit |
tool_result| } |
+------------------+
关键见解:"循环根本没有改变。我只是添加了工具。"
"""
import os
import subprocess
from pathlib import Path # 新增:更安全的路径处理
from anthropic import Anthropic
from dotenv import load_dotenv
# 加载 .env 文件中的 API 密钥、模型ID 等环境变量
load_dotenv(override=True)
# 如果用了自定义 base_url(比如代理),移除默认认证
if os.getenv("ANTHROPIC_BASE_URL"):
os.environ.pop("ANTHROPIC_AUTH_TOKEN", None)
# 定义工作目录(智能体只能操作这个目录下的文件)
WORKDIR = Path.cwd()
# 初始化 Anthropic 客户端
client = Anthropic(base_url=os.getenv("ANTHROPIC_BASE_URL"))
# 从环境变量获取模型名称(比如 claude-3-5-sonnet)
MODEL = os.environ["MODEL_ID"]
# 系统提示词:告诉模型是工作在 WORKDIR 下的代码智能体,用工具做事,少解释多行动
SYSTEM = f"You are a coding agent at {WORKDIR}. Use tools to solve tasks. Act, don't explain."
这部分是「基础配置」,核心新增了 Path 模块和 WORKDIR,为后续文件操作的安全性做准备。
2. 安全路径校验函数(核心安全机制)
python
def safe_path(p: str) -> Path:
# 把用户输入的路径和工作目录拼接,解析为绝对路径
path = (WORKDIR / p).resolve()
# 检查路径是否在工作目录内(防止 AI 操作 /etc/passwd 等系统文件)
if not path.is_relative_to(WORKDIR):
raise ValueError(f"Path escapes workspace: {p}")
return path
这个函数是文件操作的安全网关:无论 AI 想操作哪个文件,都会先经过这个函数校验,确保不会越权访问工作目录外的文件(比如用户恶意诱导 AI 修改系统文件,会被直接拦截)。
3. 工具实现函数(智能体的「动手能力」)
这部分是智能体的核心能力,每个函数对应一个工具:
(1) run_bash:执行 shell 命令(和上一版一致)
python
def run_bash(command: str) -> str:
# 危险命令过滤(防止删除系统文件、执行 sudo 等)
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()
# 限制输出长度,避免上下文过长
return out[:50000] if out else "(no output)"
except subprocess.TimeoutExpired:
return "Error: Timeout (120s)"
(2) run_read:读取文件内容
python
def run_read(path: str, limit: int = None) -> str:
try:
# 先校验路径是否安全,再读取文件内容
text = safe_path(path).read_text()
lines = text.splitlines()
# 如果指定了行数限制,只返回前 N 行(避免大文件撑爆上下文)
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}"
核心逻辑:安全读取文件,支持「行数限制」(比如用户说「读 xxx.txt 的前10行」,模型会传 limit=10)。
(3) run_write:写入文件内容
python
def run_write(path: str, content: str) -> str:
try:
# 安全路径校验
fp = safe_path(path)
# 自动创建父目录(比如写入 a/b/c.txt,若 a/b 不存在则创建)
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) run_edit:编辑文件内容(替换指定文本)
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}"
核心逻辑:精准替换文件中的指定文本(比如把「print('hello')」改成「print('world')」),避免全量覆盖。
4. 工具分发映射 + 工具定义(智能体的「工具清单」)
这部分是「工具调用的核心桥梁」,让模型知道有哪些工具可用,以及调用工具时该执行哪个函数:
(1) TOOL_HANDLERS:工具名称 → 函数的映射(分发器)
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"]),
}
作用:当模型调用 read_file 工具时,代码会通过这个映射找到 run_read 函数并执行;调用 bash 则找 run_bash。用 lambda 是为了灵活接收模型传过来的参数(比如 limit 是可选参数)。
(2) TOOLS:工具的 Schema 定义(告诉模型怎么用工具)
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"]}},
]
作用:把这个列表传给模型后,模型会知道:
- 有
bash/read_file/write_file/edit_file四个工具; - 每个工具需要传什么参数(比如
edit_file必须传path、old_text、new_text); - 每个工具的用途(比如
read_file是「读取文件内容」)。
5. 智能体循环(核心执行逻辑,和上一版几乎一致)
python
def agent_loop(messages: list):
while True:
# 调用 Anthropic API,传入对话历史和工具列表
response = client.messages.create(
model=MODEL, system=SYSTEM, messages=messages,
tools=TOOLS, max_tokens=8000,
)
# 把模型的回复加入对话历史(保持上下文)
messages.append({"role": "assistant", "content": response.content})
# 如果模型不需要调用工具(stop_reason != tool_use),说明任务完成,退出循环
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})
核心逻辑没变,只是把「固定执行 bash」改成了「根据模型调用的工具名称,从 TOOL_HANDLERS 找对应函数执行」,实现了多工具的灵活调用。
工具派发的完整逻辑是:
- 你把
TOOLS列表(工具定义)传给模型 → 模型知道有哪些工具可用、怎么用; - 模型根据用户需求,在
response中返回结构化的工具调用指令(比如「调用 write_file,参数是 path=test.txt, content=hello」); - 代码解析这个结构化指令,通过
TOOL_HANDLERS映射找到对应的执行函数 → 执行工具 → 把结果返回给模型。
实际的 Response 结构:
json
{
"stop_reason": "tool_use", // 告诉代码:我要调用工具,暂停生成
"content": [ // 内容是一个列表,包含1个或多个「块」
{
"type": "tool_use", // 块类型:工具调用
"id": "toolu_0123456789", // 工具调用ID(用于关联结果)
"name": "write_file", // 要调用的工具名称(和 TOOLS 里的 name 对应)
"input": { // 工具的入参(和 TOOLS 里的 input_schema 对应)
"path": "test.txt",
"content": "hello world"
}
},
{
"type": "text", // 可选:模型的辅助文本说明
"text": "我将创建 test.txt 文件并写入内容"
}
]
}
6. 主程序(用户交互入口)
python
if __name__ == "__main__":
history = [] # 保存对话历史
while True:
try:
# 获取用户输入(带彩色提示符)
query = input("\033[36ms02 >> \033[0m")
except (EOFError, KeyboardInterrupt):
# 处理 Ctrl+C / Ctrl+D 退出
break
# 输入 q/exit/空行 退出
if query.strip().lower() in ("q", "exit", ""):
break
# 把用户输入加入对话历史
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()
这部分是用户交互的入口,逻辑和上一版一致:接收用户输入→调用智能体→打印最终结果。
三、执行流程示例(直观理解)
假设用户输入:"在当前目录创建 test.txt,写入 hello world,然后读取它的内容"
- 用户输入加入历史:
history = [{"role": "user", "content": "在当前目录创建 test.txt,写入 hello world,然后读取它的内容"}]; - 进入
agent_loop,调用 Claude API,传入历史+工具列表; - 模型返回
stop_reason="tool_use",决定调用write_file工具,参数是path="test.txt", content="hello world"; - 代码通过
TOOL_HANDLERS找到run_write函数,执行后返回"Wrote 11 bytes to test.txt"; - 工具结果加入历史,再次调用 API;
- 模型返回
stop_reason="tool_use",决定调用read_file工具,参数是path="test.txt"; - 代码执行
run_read,返回"hello world"; - 工具结果加入历史,第三次调用 API;
- 模型返回
stop_reason="end_turn",输出文本:"已创建 test.txt 并写入 hello world,文件内容为:hello world"; - 主程序打印这个文本,等待用户下一次输入。
总结
这段代码的核心升级和关键要点:
- 功能扩展 :在 bash 基础上新增了
read_file/write_file/edit_file三个文件操作工具,覆盖了日常文件处理场景; - 安全控制 :通过
safe_path函数限制文件操作范围,过滤危险 bash 命令,避免越权和破坏; - 架构优化 :用
TOOL_HANDLERS实现工具名称和执行函数的解耦,新增工具只需加函数+更新映射,无需改核心循环; - 核心不变:智能体的「思考→执行→反馈→再思考」循环逻辑完全复用,体现了「扩展工具不改动核心流程」的设计思想。
简单来说,这段代码把 AI 智能体从「只会执行命令的终端」升级成了「能读写文件、编辑内容的全功能助手」,且保持了极高的安全性和可扩展性。