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

Outils

Outils et exécution

One Handler Per Tool

120 LOC4 outilsTool 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

"Ajouter un outil signifie ajouter un gestionnaire" -- le boucle reste le même ; les nouveaux outils s'enregistrent dans la carte de distribution.

Problème

Avec seulement bash, l'agent utilise le shell pour tout. cat tronque de manière imprévisible, sed échoue sur les caractères spéciaux, et chaque appel bash est une surface de sécurité non contrainte. Des outils dédiés comme read_file et write_file permettent d'appliquer le sandboxing de chemin au niveau de l'outil.

L'idée clé : ajouter des outils ne nécessite pas de modifier le boucle.

Solution

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

La carte de distribution est un dict : {nom_outil: fonction_handler}.
Un seul lookup remplace n'importe quelle chaîne if/elif.

Comment Ça Marche

  1. Chaque outil obtient une fonction handler. Le sandboxing de chemin empêche l'échappement de l'espace de travail.
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. La carte de distribution lie les noms d'outils aux gestionnaires.
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. Dans la boucle, recherchez le handler par nom. Le corps de la boucle lui-même est inchangé depuis 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,
        })

Ajouter un outil = ajouter un handler + ajouter une entrée de schéma. Le boucle ne change jamais.

Qu'est-ce qui a Changé par Rapport à s01

ComposantAvant (s01)Après (s02)
Outils1 (bash seulement)4 (bash, read, write, edit)
DistributionAppel bash codé en durTOOL_HANDLERS dict
Sécurité cheminNonesafe_path() sandbox
Boucle agentInchangéeInchangée

Essayer

python agents/s02_tool_use.py
  1. Lire le fichier requirements.txt
  2. Créer un fichier appelé greet.py avec une fonction greet(name)
  3. Modifier greet.py pour ajouter un docstring à la fonction
  4. Lire greet.py pour vérifier que la modification a fonctionné