Agent 开发入门(二):加工具不改循环,靠的是这个设计

4 阅读4分钟

上一篇文章我们讲了 Agent Loop,今天聊一个很实用的设计:工具分发(Tool Dispatch)

上一章留下的问题

上一章的最小 Agent 只有一把瑞士军刀:bash

但用 bash 干活有很多问题:

# cat 截断不可预测
cat large_file.py  # 可能输出几十屏

# sed 遇到特殊字符就崩
sed -i 's/old/new/' file.txt  # 中文、特殊符号全挂

# 每次都是不受约束的安全面
rm -rf /  # 危险命令也能执行

怎么办?

解决方案:专用工具 + 分发地图

这一章的核心思路很简单:

不要用 bash 做所有事,而是给 Agent 添加专用工具。

专用工具的好处:

  • 语义清晰,模型知道什么时候该用
  • 可以加安全校验
  • 行为更可预测

但问题是:每加一个工具就要改一次循环吗?

答案:不用。

Tool Dispatch Map:工具分发地图

用一个字典,把「工具名」映射到「处理函数」:

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

调用工具时,只需按名字查找:

for block in response.content:
    if block.type == "tool_use":
        handler = TOOL_HANDLERS.get(block.name)
        output = handler(**block.input) if handler else "Unknown tool"
        # ...

关键结论

  • 加新工具 = 加 schema + 加 handler
  • 循环代码完全不用动

三个核心概念

1. Tool Schema(工具描述)

给模型看的「工具说明书」:

{
    "name": "read_file",
    "description": "读取文件内容",
    "input_schema": {
        "type": "object",
        "properties": {
            "path": {"type": "string"},
            "limit": {"type": "integer"},  # 可选参数
        },
        "required": ["path"],
    },
}

2. Handler(处理函数)

工具的「实际执行逻辑」:

def run_read(path: str, limit: int = None) -> str:
    content = safe_read(path)
    if limit:
        lines = content.splitlines()[:limit]
        content = "\n".join(lines)
    return content

3. tool_result(工具结果)

结果的「回流机制」:

results.append({
    "type": "tool_result",
    "tool_use_id": block.id,  # 关联到哪次调用
    "content": output,
})

路径安全:防止工具「逃逸」

如果 Agent 能读写任意文件,系统就危险了。

所以要给工具加「沙箱」:

def safe_path(path_str: str) -> Path:
    workdir = Path.cwd()
    target = (workdir / path_str).resolve()

    # 检查是否在工作目录内
    if not target.is_relative_to(workdir):
        raise ValueError(f"禁止访问工作目录外的文件: {path_str}")

    return target

这样 Agent 只能操作当前项目下的文件,安全性大大提升。

消息规范化:API 的硬性要求

当系统变复杂后,内部 messages 列表可能会出现 API 不接受的格式。

API 有三条硬性约束:

  1. 每个 tool_use 必须有匹配的 tool_result
  2. user/assistant 消息必须严格交替
  3. 只接受协议定义的字段

所以每次调用 API 前,需要规范化:

def normalize_messages(messages: list) -> list:
    # 1. 剥离内部字段
    # 2. 补齐缺失的 tool_result
    # 3. 合并连续同角色消息
    return normalized

关键洞察messages 是内部状态,API 看到的是规范化后的副本,两者不是同一个东西。

对比:s01 vs s02

组件s01s02
工具数1 (bash)4 (bash, read, write, edit)
分发方式硬编码TOOL_HANDLERS 字典
路径安全safe_path() 沙箱
循环代码-完全不变

怎么给 Agent 加新工具

以「搜索文件」工具为例:

第一步:写处理函数

def run_grep(path: str, pattern: str) -> str:
    matches = subprocess.run(
        ["grep", "-n", pattern, path],
        capture_output=True, text=True
    )
    return matches.stdout or "(no matches)"

第二步:注册到分发地图

TOOL_HANDLERS = {
    # ... 现有工具 ...
    "grep": lambda **kw: run_grep(kw["path"], kw["pattern"]),
}

第三步:添加 Schema

{
    "name": "grep",
    "description": "在文件中搜索匹配的行",
    "input_schema": {
        "type": "object",
        "properties": {
            "path": {"type": "string"},
            "pattern": {"type": "string"},
        },
        "required": ["path", "pattern"],
    },
}

完成!循环代码零改动。

进阶:工具层不只是「分发表」

学到这里,工具对你来说已经是一张「handler map」。

但实际上,当系统继续变大,工具层还会长出更多能力:

  • 权限环境(什么情况下禁止调用)
  • 上下文注入(工具能访问当前状态)
  • 调用日志和追踪
  • 缓存机制

也就是说,完整的工具层会更像一条「工具控制平面」。

不过那是后面的内容,先把今天的基础打牢。

一句话总结

Tool Dispatch Map 让「加工具」变成「注册到字典」,循环代码永远不用改。


下一篇文章我们会聊:如何让 Agent 记住「当前在做什么」,不再走一步忘一步。


往期回顾