面试-Dispatch Tools

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 必须传 pathold_textnew_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,然后读取它的内容"

  1. 用户输入加入历史:history = [{"role": "user", "content": "在当前目录创建 test.txt,写入 hello world,然后读取它的内容"}]
  2. 进入 agent_loop,调用 Claude API,传入历史+工具列表;
  3. 模型返回 stop_reason="tool_use",决定调用 write_file 工具,参数是 path="test.txt", content="hello world"
  4. 代码通过 TOOL_HANDLERS 找到 run_write 函数,执行后返回 "Wrote 11 bytes to test.txt"
  5. 工具结果加入历史,再次调用 API;
  6. 模型返回 stop_reason="tool_use",决定调用 read_file 工具,参数是 path="test.txt"
  7. 代码执行 run_read,返回 "hello world"
  8. 工具结果加入历史,第三次调用 API;
  9. 模型返回 stop_reason="end_turn",输出文本:"已创建 test.txt 并写入 hello world,文件内容为:hello world"
  10. 主程序打印这个文本,等待用户下一次输入。

总结

这段代码的核心升级和关键要点:

  1. 功能扩展 :在 bash 基础上新增了 read_file/write_file/edit_file 三个文件操作工具,覆盖了日常文件处理场景;
  2. 安全控制 :通过 safe_path 函数限制文件操作范围,过滤危险 bash 命令,避免越权和破坏;
  3. 架构优化 :用 TOOL_HANDLERS 实现工具名称和执行函数的解耦,新增工具只需加函数+更新映射,无需改核心循环;
  4. 核心不变:智能体的「思考→执行→反馈→再思考」循环逻辑完全复用,体现了「扩展工具不改动核心流程」的设计思想。

简单来说,这段代码把 AI 智能体从「只会执行命令的终端」升级成了「能读写文件、编辑内容的全功能助手」,且保持了极高的安全性和可扩展性。

相关推荐
IT_陈寒2 小时前
JavaScript开发者必看:3个让代码效率翻倍的隐藏技巧
前端·人工智能·后端
嘉琪0012 小时前
Day8 完整学习包(Vue 基础 & 响应式)——2026 0320
前端·vue.js·学习
FlyWIHTSKY2 小时前
Vue3 单文件中不同的组件
前端·javascript·vue.js
一字白首2 小时前
小程序组件化进阶:从复用到通信的完整指南DAY04
前端·小程序·apache
读忆2 小时前
你是否用过Tailwind CSS?你是在什么情况下使用的呢?
前端·css·经验分享·笔记·taiiwindcss
阿珊和她的猫2 小时前
探秘小程序:为何拿不到 DOM 相关 API
前端·小程序
FlyWIHTSKY2 小时前
Vue 3 onMounted 中控制同步与异步执行策略
前端·javascript·vue.js
蜗牛攻城狮2 小时前
【Vue3实战】El-Table实现“超过3行省略,悬停显示全文”的完美方案(附性能优化)
前端·vue.js·性能优化·element-plus
孙12~2 小时前
前端vue3+vite,后端SpringBoot+MySQL
前端·html·学习方法