How to Build an AI Agent
How to Build an AI Agent
s02

ツール

ツールと実行

One Handler Per Tool

120 LOC4 ツールTool dispatch map
The loop stays the same; new tools register into the dispatch map

s01 > [ s02 ] s03 > s04 > s05 > s06 | s07 > s08 > s09 > s10 > s11 > s12

"ツールを足すなら、ハンドラーを1つ足すだけ" -- ループは変わらない。新ツールは dispatch map に登録するだけ。

問題

bashだけでは、エージェントは何でもシェル経由で行う。catは予測不能に切り詰め、sedは特殊文字で壊れ、すべてのbash呼び出しが制約のないセキュリティ面になる。read_filewrite_fileのような専用ツールなら、ツールレベルでパスのサンドボックス化を強制できる。

重要な点: ツールを追加してもループの変更は不要。

解決策

+--------+      +-------+      +------------------+
|  User  | ---> |  LLM  | ---> | Tool Dispatch    |
| prompt |      |       |      | {                |
+--------+      +---+---+      |   bash: run_bash |
                    ^           |   read: run_read |
                    |           |   write: run_wr  |
                    +-----------+   edit: run_edit |
                    tool_result | }                |
                                +------------------+

The dispatch map is a dict: {tool_name: handler_function}.
One lookup replaces any if/elif chain.

仕組み

  1. 各ツールにハンドラ関数を定義する。パスのサンドボックス化でワークスペース外への脱出を防ぐ。
def safe_path(p: str) -> Path:
    path = (WORKDIR / p).resolve()
    if not path.is_relative_to(WORKDIR):
        raise ValueError(f"Path escapes workspace: {p}")
    return path

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]
  1. ディスパッチマップがツール名とハンドラを結びつける。
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"]),
}
  1. ループ内で名前によりハンドラをルックアップする。ループ本体はs01から不変。
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}"
        results.append({
            "type": "tool_result",
            "tool_use_id": block.id,
            "content": output,
        })

ツール追加 = ハンドラ追加 + スキーマ追加。ループは決して変わらない。

s01からの変更点

ComponentBefore (s01)After (s02)
Tools1 (bash only)4 (bash, read, write, edit)
DispatchHardcoded bash callTOOL_HANDLERS dict
Path safetyNonesafe_path() sandbox
Agent loopUnchangedUnchanged

試してみる

python agents/s02_tool_use.py
  1. Read the file requirements.txt
  2. Create a file called greet.py with a greet(name) function
  3. Edit greet.py to add a docstring to the function
  4. Read greet.py to verify the edit worked