面试-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 智能体从「只会执行命令的终端」升级成了「能读写文件、编辑内容的全功能助手」,且保持了极高的安全性和可扩展性。

相关推荐
weixin_3823952314 分钟前
为小工厂量身打造:本地部署的物料管理系统带缺料计算
前端·制造
__zRainy__20 分钟前
解决pnpm v10+不自动构建
前端·pnpm·工程化
猫猫不是喵喵.1 小时前
Vue3 Props 属性
前端·javascript·vue.js
醉城夜风~2 小时前
CSS元素显示模式(display)
前端·css
AI大模型-小华3 小时前
Codex 三方充值快速入门指南
java·前端·数据库·chatgpt·ai编程·codex·chatgpt pro
做前端的娜娜子5 小时前
同一链接实现 PC Web 与移动 H5 自适应
前端·掘金·金石计划
小帅不太帅5 小时前
架构没变、规模没变,DeepSeek V4 Flash 正式版凭什么暴涨 47 分?
前端·aigc·deepseek
jarvisuni5 小时前
DeepSeekFlash前端依旧拉垮,而且变慢了很多!
前端·javascript·算法
卷福同学7 小时前
AI编程出海第二步:验证关键词能否做站
前端·人工智能·后端