S02|工具使用:让 Agent 真正会干活,添加工具

在上一章 S01 里,我们搭好了 Agent 的最小循环,让模型能调用工具、把结果喂回自己继续推理。但当时只有一个bash工具,既不安全也不好扩展。

这一章 S02,我们要解决工具扩展、安全限制、统一分发 三大问题,实现一句核心原则:加一个工具,只加一个 handler,循环完全不用改。


本章核心信息

  • 核心闭环:把模型意图 → 路由成真实动作
  • 工具数量:4 个(bash /read_file/write_file /edit_file)
  • 核心思想:主循环不变,靠分发层扩展能力

先把本章所有名词讲明白

1. Handler(工具处理器)

每个工具真正执行的代码函数。比如读文件、写文件、执行命令,各自有独立的处理逻辑。

2. Dispatch Map / 工具路由表

一张 "对照表":工具名字 → 对应的处理函数

比如,模型说要用 read_file,程序就查表找到 run_read 执行。

3. Harness 层(工具调度层)

统一管理所有工具的中间层,负责:

接收模型调用 → 找到对应工具 → 执行 → 返回结果。

4. Path Sandbox 路径沙箱

安全限制:Agent 只能读写指定文件夹内的文件,不能越权访问系统文件,防止删库、偷文件。

5. Tool Schema(工具描述)

给模型看的 "工具说明书",告诉它:这个工具叫什么、需要传什么参数、用来干什么。

6. normalize_messages(消息规范化)

把内部消息整理成模型 API 能接受的格式,保证:

  • 工具调用和结果一一对应
  • 消息角色不连续重复
  • 不携带内部字段导致报错

7. turn(轮次)

一轮 = 模型思考 + 可能的工具调用 + 结果返回一轮结束进入下一轮,直到任务完成。

8. state(状态)

Agent 运行时的全部记忆:消息历史、轮次数、继续 / 停止原因。


为什么不能只用一个 bash 工具?

S01 我们只用了 bash(命令行),看起来万能,其实隐患巨大:

  1. cat 读文件可能被截断特殊字符、长文件会导致输出不完整,模型拿到错误信息。

  2. sed 编辑极易崩溃 遇到 $ & / \ 等符号,命令直接报错。

  3. 完全没有安全边界模型可以执行任何命令:删文件、改系统配置、泄露信息。

所以必须拆成专用工具read_file write_file edit_file,并加上安全沙箱。


本章架构:一句话看懂

复制代码
用户输入 → LLM 思考 → 工具分发(查表执行)→ 结果返回 → 写回消息 → 下一轮

工具分发是关键:LLM 不需要知道代码怎么实现,它只说 "工具名 + 参数",系统查表找到对应函数执行,循环完全不用改


核心实现:工具路由表 + 安全路径

1. 路径安全校验(沙箱)

保证 Agent 只能在工作目录内操作:

python 复制代码
def safe_path(p: str) -> Path:
    path = (WORKDIR / p).resolve()
    if not path.is_relative_to(WORKDIR):
        raise ValueError(f"路径超出工作区:{p}")
    return path

2. 读文件工具(带长度限制)

python 复制代码
def run_read(path: str, limit: int = None) -> str:
    text = safe_path(path).read_text()
    lines = text.splitlines()
    if limit and limit < len(lines):
        lines = lines[:limit]
    return "\n".join(lines)[:50000]

3. 工具路由表(核心设计)

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"]),
}

4. 循环中统一分发

和 S01 循环几乎一样,只是把硬编码改成查表:

python 复制代码
for block in response.content:
    if block.type == "tool_use":
        handler = TOOL_HANDLERS.get(block.name)
        output = handler(**block.input) if handler else f"未知工具:{block.name}"
        results.append({
            "type": "tool_result",
            "tool_use_id": block.id,
            "content": output
        })

关键洞察:加工具不用改循环

以后你想加新工具,比如:

  • search_web
  • run_python
  • get_weather

只需要两步:

  1. 写一个 handler 函数
  2. TOOL_HANDLERS 里加一行映射

agent_loop 代码一行不动这就是工业级 Agent 的标准设计。


为什么要消息规范化?

随着工具变多,消息会越来越乱:

  • 工具调用缺结果
  • 连续两条 user 消息
  • 带内部字段 API 不识别

所以必须在发给模型前规范化

  1. 清理内部字段

  2. 补齐缺失的 tool_result

  3. 合并连续同角色消息

python 复制代码
def normalize_messages(messages: list) -> list:
    """将内部消息列表规范化为 API 可接受的格式。"""
    normalized = []

    for msg in messages:
        # Step 1: 剥离内部字段
        clean = {"role": msg["role"]}
        if isinstance(msg.get("content"), str):
            clean["content"] = msg["content"]
        elif isinstance(msg.get("content"), list):
            clean["content"] = [
                {k: v for k, v in block.items()
                 if k not in ("_internal", "_source", "_timestamp")}
                for block in msg["content"]
            ]
        normalized.append(clean)

    # Step 2: tool_result 配对补齐
    # 收集所有已有的 tool_result ID
    existing_results = set()
    for msg in normalized:
        if isinstance(msg.get("content"), list):
            for block in msg["content"]:
                if block.get("type") == "tool_result":
                    existing_results.add(block.get("tool_use_id"))

    # 找出缺失配对的 tool_use, 插入占位 result
    for msg in normalized:
        if msg["role"] == "assistant" and isinstance(msg.get("content"), list):
            for block in msg["content"]:
                if (block.get("type") == "tool_use"
                        and block.get("id") not in existing_results):
                    # 在下一条 user 消息中补齐
                    normalized.append({"role": "user", "content": [{
                        "type": "tool_result",
                        "tool_use_id": block["id"],
                        "content": "(cancelled)",
                    }]})

    # Step 3: 合并连续同角色消息
    merged = [normalized[0]] if normalized else []
    for msg in normalized[1:]:
        if msg["role"] == merged[-1]["role"]:
            # 合并内容
            prev = merged[-1]
            prev_content = prev["content"] if isinstance(prev["content"], list) \
                else [{"type": "text", "text": prev["content"]}]
            curr_content = msg["content"] if isinstance(msg["content"], list) \
                else [{"type": "text", "text": msg["content"]}]
            prev["content"] = prev_content + curr_content
        else:
            merged.append(msg)

    return merged

调用模型时这样用:

python 复制代码
response = client.messages.create(
    model=MODEL,
    system=SYSTEM,
    messages=normalize_messages(messages),  # 先规范化
    tools=TOOLS,
    max_tokens=8000
)

重点区分:messages 列表是系统的内部表示, API 看到的是规范化后的副本,二者不等价。


S01 → S02 到底升级了什么?

模块 S01 S02
工具数量 1 个(bash) 4 个
工具调用 硬编码 路由表分发
安全 路径沙箱
消息 直接发送 规范化后发送
扩展性 极强
主循环 不变 不变

初学者可以直接试的 Prompt

复制代码
Read the file requirements.txt
Create a file called greet.py with a greet(name) function
Edit greet.py to add a docstring to the function
Read greet.py to check the result

你会看到:Agent 会自主选择工具,自动读写编辑,完全不用你干预。


本章教学边界(不搞复杂,只抓核心)

本章不讲权限、不讲缓存、不讲流式、不讲异常恢复。只牢牢抓住三件事:

  1. Tool Schema 给模型看
  2. Handler Map 给代码分发
  3. Tool Result 回流到循环

只要懂这三点,你就掌握了所有 Agent 工具系统的底层结构


一句话总结本章

Agent 的能力增长,不靠把循环写复杂, 而靠一层清晰的工具分发层。 加工具只加 handler,循环永远不动。

相关推荐
Dwzun2 小时前
基于Java+SpringBoot+Vue的校园二手物品置换系统设计与实现【附源码+文档+部署视频+讲解】
java·开发语言·spring boot
charlie1145141912 小时前
嵌入式Linux驱动开发(3)——内核模块机制 - Linux 的插件系统
linux·运维·开发语言·驱动开发·嵌入式硬件·学习
凌奕2 小时前
Mac 从零部署 Hermes Agent 并接入飞书:一篇就够的保姆级教程
agent
C、空白格2 小时前
Java集成Vosk实现离线语音识别
java·开发语言·语音识别
编码浪子2 小时前
基于 Rust + Axum 的企业级权限管理系统设计与实现
开发语言·后端·rust
历程里程碑2 小时前
MySQL事务深度解析:ACID到MVCC实战+万字长文解析
开发语言·数据结构·数据库·c++·sql·mysql·排序算法
jerrywus2 小时前
把 Obsidian 知识库接进 Claude Code:400 行代码实现 AI 长期记忆
前端·agent·claude
Lyyaoo.2 小时前
【JAVA基础面经】native方法
java·开发语言
牛十二2 小时前
nacos2.4连接出错源码分析
java·linux·开发语言