algonote

There's More Than One Way To Do It

ゆっくりsmolagents

ゆっくりしていってね!

本書リポジトリ: https://github.com/hiromichinomata/yukkuri-smolagents


第0章 プロローグ — エージェントってなに?

本章のゴール: エージェントsmolagents の位置づけを掴み、環境を用意して 最初の CodeAgent を動かす


0.1 最近よく聞く「エージェント」

霊夢: 最近、ニュースでも SNS でも「AI エージェント」って言葉、よく聞くのよね。ChatGPT に何かやらせる、みたいな話?

魔理沙: だいたい合ってるぜ。ただ、本書で扱う「エージェント」はもう一段踏み込んだものだ。

霊夢: 一段?

魔理沙: LLM に ツール を持たせて、考える → 行動する → 結果を見る を繰り返しながら、タスクを最後まで運ぶ仕組みだ。いわば「頭脳(LLM)」に「手足(ツール)」をつけたロボットだな。

霊夢: 手足……検索とか計算とか?

魔理沙: その通り。Web 検索、Python 実行、DB 問い合わせ、Hub 上の画像生成 Space 呼び出し……なんでもツールにできる。エージェントはその中から選んで使うんだ。


0.2 3 つの部品 — LLM・ツール・ループ

霊夢: 構成要素、整理して教えて。

魔理沙: 最小構成は次の 3 つだ。

部品 役割
LLM(モデル) 次に何をするか考える
ツール 外界とやり取りする(検索・実行・API)
エージェントループ タスクが終わるまで「思考 → 実行 → 観察」を回す
flowchart LR
  User[ユーザー: タスク] --> Agent[エージェント]
  Agent --> LLM[LLM]
  LLM -->|方針・コード・ツール呼び出し| Agent
  Agent --> Tools[ツール群]
  Tools -->|結果| Agent
  Agent -->|完了| Answer[最終回答]

霊夢: ユーザーが「パリの天気は?」って聞いたら?

魔理沙: 例えばこうなる。

  1. LLM が「検索ツールを使おう」と判断
  2. ツールが Web を検索
  3. 結果を LLM に渡す
  4. LLM が要約して final_answer で返す

霊夢: 1 回のチャットじゃなくて、ループなのね。

魔理沙: 当たり。だから本書では agent.run("...")中で何ステップ起きたか をログで追う練習もするぜ。


0.3 普通の LLM 呼び出しとの違い

霊夢: chat.completions で聞くだけと、何が違うの?

魔理沙: コードで比べるのが早い。まず「ただ聞く」パターン。

# パターン A: 単発の LLM 呼び出し(エージェントではない)
question = "1 から 10 までの合計は?"

# 疑似コード — 実際は OpenAI / HF などの API を 1 回叩く
# response = model.generate(question)
# print(response.text)

霊夢: 答えを 言うだけ だわ。

魔理沙: 次がエージェント。中で Python を実行 して確認できる。

# パターン B: CodeAgent(本書の主役)
from smolagents import CodeAgent, InferenceClientModel

model = InferenceClientModel()
agent = CodeAgent(tools=[], model=model)

result = agent.run("1 から 10 までの合計を計算して")
print(result)

霊夢: tools=[] なのに動くの?

魔理沙: CodeAgent自分で Python コードを書いて実行 できるから、ツールが空でも計算はできる。ツールは「外の世界」に出るための追加装備だと思えばいい。


0.4 smolagents って何?

霊夢: じゃあ smolagents は、そのエージェントを作るライブラリ?

魔理沙: その通り。Hugging Face が公開している 軽量な Python ライブラリ だ。名前の "smol" は "small" の意、コアがコンパクトなのが売りだぜ。

霊夢: 他にもエージェント用フレームワークあるんでしょ?

魔理沙: ある。smolagents の特徴をざっくり表にするとこうだ。

特徴 内容
シンプル エージェントのコアが ~1000 行級。抽象化が薄い
CodeAgent 第一級 行動を Python コード で書く(JSON ツール呼び出しだけじゃない)
ToolCallingAgent も可 従来型の JSON ツール呼び出しにも対応
モデル非依存 HF Inference、LiteLLM、Transformers、Ollama など
Hub 連携 ツール・エージェントの共有
安全な実行 ローカル制限付き実行、E2B / Docker 等のサンドボックス

霊夢: CodeAgent がメインなのね。

魔理沙: 本書も CodeAgent 中心 で進める。複雑な処理はコードの方が書きやすいし、ベンチマークでも有利なことが多いからな。

公式の入口はこちら。


0.5 transformers.agents との関係

霊夢: transformers パッケージにもエージェントっぽいの、なかった?

魔理沙: あった。transformers.agentssmolagents に置き換わっていく 方向だ。新規プロジェクトは smolagents を使うのがおすすめだぜ。

霊夢: 移行する人向けの話は?

魔理沙: 概念は同じ(model + tools + run)。API 名が変わっているだけのことが多い。例えばモデルは今は InferenceClientModel が主流だ(旧 HfApiModel など)。

# 旧イメージ(参考・バージョンにより異なる)
# from smolagents import HfApiModel
# model = HfApiModel()

# 現在の推奨(2025 年時点のドキュメント準拠)
from smolagents import InferenceClientModel

model = InferenceClientModel()

霊夢: 第 0 章では深追いしなくていいわね。

魔理沙: ああ。とにかく 「これからは smolagents」 と覚えておけば十分だ。


🖥️ ハンズオン 0-1 — 環境を用意する

霊夢: 話は分かったわ。動かしてみたい!

魔理沙: ここから手を動かすぜ。Python 3.10 以上 を推奨する。

リポジトリを手元に置く

本書のサンプルは yukkuri-smolagents リポジトリに載せていく想定だ。

git clone https://github.com/hiromichinomata/yukkuri-smolagents.git
cd yukkuri-smolagents

霊夢: すでにクローン済みなら cd だけでいいのね。

仮想環境を作る

python3 -m venv .venv
source .venv/bin/activate   # Windows: .venv\Scripts\activate
python -V
pip install -U pip

smolagents を入れる

本書ではまず 標準ツール付き のインストールを使う。

pip install 'smolagents[toolkit]'

霊夢: [toolkit] って何?

魔理沙: extras ってやつだ。[toolkit] を付けると Web 検索などの追加依存が入る。第 2 章で他の extras もまとめて説明するぜ。

インストール確認

python -c "import smolagents; print(smolagents.__version__)"

バージョンが表示されれば OK。

python -c "from smolagents import CodeAgent, InferenceClientModel; print('import OK')"

Hugging Face トークン(推論 API 用)⚠️

クラウド上のモデルを使うハンズオンでは HF トークン が必要になる。

  1. Hugging Face → Settings → Access Tokens でトークンを作成
  2. 環境変数に設定
export HF_TOKEN="hf_xxxxxxxxxxxxxxxxxxxxxxxx"

霊夢: .env に書いてもいい?

魔理沙: いいぜ。ただし Git にコミットするな よ。

# .env(リポジトリには含めない)
HF_TOKEN=hf_xxxxxxxxxxxxxxxxxxxxxxxx
# Python から読む例(python-dotenv を使う場合)
# pip install python-dotenv
from dotenv import load_dotenv

load_dotenv()

.gitignore.env があるか確認しておく。

.env
.venv/
__pycache__/
*.pyc

🖥️ ハンズオン 0-2 — 10 行で最初の CodeAgent

魔理沙: いよいよ本番だ。次のファイルを作るぜ。

examples/ch00/first_agent.py:

"""第0章: 最初の CodeAgent"""
from smolagents import CodeAgent, InferenceClientModel

def main() -> None:
    model = InferenceClientModel()
    agent = CodeAgent(tools=[], model=model)

    task = "1 から 10 までの整数の合計を計算し、答えだけを返して"
    result = agent.run(task)
    print("=== 最終回答 ===")
    print(result)

if __name__ == "__main__":
    main()

ディレクトリを作って保存する。

mkdir -p examples/ch00
# 上記を examples/ch00/first_agent.py に保存
python examples/ch00/first_agent.py
# examples/ch00/first_agent.py(リポジトリ同梱・全文)
"""第0章: 最初の CodeAgent"""
from smolagents import CodeAgent, InferenceClientModel

def main() -> None:
    model = InferenceClientModel()
    agent = CodeAgent(tools=[], model=model)

    task = "1 から 10 までの整数の合計を計算し、答えだけを返して"
    result = agent.run(task)
    print("=== 最終回答 ===")
    print(result)

if __name__ == "__main__":
    main()

霊夢: 実行したら、ログがいっぱい出てきたわ……

魔理沙: 正常だ。ざっくり読み方はこうだ。

╭──────────────── New run ────────────────╮
│  1 から 10 までの整数の合計を……          │
╰─────────────────────────────────────────╯
━━━━━━━━━━━━━━━━━━━━ Step 0 ━━━━━━━━━━━━━━━
╭─ Executing this code: ─────────────────╮
│ total = sum(range(1, 11))              │
│ print(total)                           │
╰────────────────────────────────────────╯
...
╭─ Executing this code: ─────────────────╮
│ final_answer(55)                       │
╰────────────────────────────────────────╯
Out - Final answer: 55
ログの部分 意味
New run 今回のタスク
Step N N 回目の思考・実行サイクル
Executing this code LLM が書いた Python
final_answer(...) エージェントが完了を宣言
Input tokens / Output tokens コストの目安

霊夢: 答えは 55 ね。合ってるわ!


0.6 もっと短い一発スクリプト

魔理沙: ファイルを作らず REPL で試すなら、これだけでもいい。

from smolagents import CodeAgent, InferenceClientModel

agent = CodeAgent(tools=[], model=InferenceClientModel())
print(agent.run("フィボナッチ数列の第 10 項を求めて"))

霊夢: 本当に数行ね……

魔理沙: これが smolagents の「smol」だぜ。あとは ツールを足すモデルを替えるマルチエージェントにする と広がっていく。


0.7 よくあるエラーと対処 ⚠️

霊夢: うち、エラー出たんだけど……

魔理沙: 第 0 章で多いのはこのあたりだ。

認証エラー(401 / 403)

Unauthorized ... HF_TOKEN ...

対処:

echo $HF_TOKEN   # 空なら設定し直す
export HF_TOKEN="hf_..."

モデル・レート制限

Rate limit exceeded ...

対処: しばらく待つ、HF Pro の検討、または第 4 章で触る 別モデル(Ollama ローカルなど)に切り替える。

ModuleNotFoundError: smolagents

pip install 'smolagents[toolkit]'
# 仮想環境を activate し忘れていないか確認
which python

コード実行エラー(Step 内の Python)

Code execution failed ...

対処: タスクを具体化する(「答えだけ」「整数で」など)。第 9 章以降で additional_authorized_imports も学ぶ。


0.8 本章のまとめ

霊夢: 整理するわ。

  1. エージェント = LLM + ツール + ループでタスクを完遂する仕組み
  2. smolagents = それを少ないコードで書ける HF 製ライブラリ
  3. CodeAgent = 行動を Python コードで書く(本書の主役)
  4. ハンズオン = pip install 'smolagents[toolkit]'CodeAgent を動かした

魔理沙: 次の第 1 章では、本書の読み方・examples/ の構成・ログの詳しい見方を説明するぜ。


✅ 章末チェックリスト

手を動かしたら、次を確認しよう。

  • [ ] Python 3.10+ と仮想環境が使える
  • [ ] pip install 'smolagents[toolkit]' が成功した
  • [ ] from smolagents import CodeAgent, InferenceClientModel ができる
  • [ ] HF_TOKEN を設定した(クラウド推論を使う場合)
  • [ ] examples/ch00/first_agent.py または REPL で agent.run(...) が動いた
  • [ ] ログに Step 0final_answer が出た
  • [ ] .env を Git にコミットしていない

次章へ

霊夢: 第 0 章、クリアよ!

魔理沙: 次は本格的に環境とモデルを整えるぜ。ゆっくりしていこうな。


第1章 本書の舞台裏 — ゆっくり解説のルールと道具

本章のゴール: 本書の読み方examples/ の対応HF_TOKEN とログの見方を押さえ、第 0 章以降のハンズオンを迷わず進められる。


1.1 霊夢と魔理沙の役割

霊夢: 第 0 章でエージェントは動かしたわ。でもこの本、ずっと私と魔理沙が喋ってるのよね。読者の私たち、どっちの声を聞けばいいの?

魔理沙: 役割分担は固定だ。

担当
霊夢 素朴な疑問・要約・「やってみる」 「ログの Step って何?」
魔理沙 実装・コマンド・突っ込み 「だから agent.logs を見ろだぜ」

霊夢: つまり私が 読者の代弁、魔理沙が 先生兼デモ係 ね。

魔理沙: その通り。会話のあとには 必ずコードか表 が来る。会話だけで終わる章はないと思え。


1.2 章の流れ — 導入 → 概念 → ハンズオン → 振り返り

霊夢: 毎章、同じリズムなの?

魔理沙: 目次 のとおり、だいたい次の順だ。

1. ゴール(引用ブロック 1 文)
2. 概念(会話 + 表 / mermaid + コード)
3. 🖥️ ハンズオン(手を動かす)
4. ⚠️ よくあるエラー
5. まとめ + ✅ チェックリスト + 次章へ
記号 意味
🖥️ ハンズオン(実行する)
⚠️ API キー・コスト・セキュリティ
🔗 公式ドキュメント
📦 リポジトリ同梱ファイル

1.3 examples/ と章番号の対応

霊夢: サンプルコード、どこに置いてあるの?

魔理沙: 章ごとに examples/chNN/ だ。NN は 2 桁の章番号。

ディレクトリ 主なスクリプト
0 examples/ch00/ first_agent.py
1 examples/ch01/ check_env.py, inspect_run_logs.py
2 examples/ch02/ import_check.py, hf_connection.py
3 examples/ch03/ fibonacci_no_tools.py, inspect_agent_logs.py
# リポジトリルートで
ls examples/
ls examples/ch01/

霊夢: 本文に examples/ch03/foo.py と書いてあったら、そのファイルが本当にあるってことね。

魔理沙: 当たり。📦 と書いてあるパスは そのまま実行できる 前提だぜ。


1.4 HF_TOKEN.env ⚠️

霊夢: 第 0 章で HF_TOKEN って言われたけど、もう一度整理して。

魔理沙: Hugging Face の アクセストークン だ。クラウド上の Inference Providers 経由でモデルを叩くときに使う。

  1. Settings → Access Tokens で作成(Read 権限で足りることが多い)
  2. シェルまたは .env に設定
export HF_TOKEN="hf_xxxxxxxxxxxxxxxxxxxxxxxx"
# リポジトリルートにテンプレがある
cp .env.example .env
# .env を編集(Git にコミットするな)
# .env の中身の例
HF_TOKEN=hf_xxxxxxxxxxxxxxxxxxxxxxxx
# python-dotenv を使う場合(requirements.txt に含まれる)
from dotenv import load_dotenv

load_dotenv()

霊夢: 本書、実際のキーを書いてないわね。

魔理沙: 絶対にコミットするな。ドキュメントでは hf_... のプレースホルダだけ使う。HUGGINGFACEHUB_API_TOKEN という名前でも読まれることがある。

# 設定確認(値そのものは表示しない方が安全)
test -n "$HF_TOKEN" && echo "HF_TOKEN is set" || echo "HF_TOKEN is empty"

.gitignore の例:

.env
.venv/
__pycache__/

1.5 ログの読み方(おさらい)

霊夢: agent.run() すると画面いっぱいログが出るの。何を見ればいい?

魔理沙: 最低限、次の 4 つを追えばいい。

表示 意味
New run 今回のタスク開始
Step N N 回目の思考・実行サイクル
Executing this code CodeAgent が実行した Python
final_answer(...) タスク完了の宣言
╭──────────────── New run ────────────────╮
│  1 から 5 までの合計を……                │
╰─────────────────────────────────────────╯
━━━━━━━━━━━━━━━━━━━━ Step 0 ━━━━━━━━━━━━━━━
╭─ Executing this code: ─────────────────╮
│ total = sum(range(1, 6))               │
│ final_answer(total)                    │
╰────────────────────────────────────────╯
Out - Final answer: 15

霊夢: トークン数も出るわ。

魔理沙: Input tokens / Output tokensコストの目安 だ。無料枠やレート制限に当たったら、第 4 章で別モデルに切り替える話をするぜ。

プログラムからログを見るには agent.logswrite_memory_to_messages() がある(第 3 章で深掘り)。

# run のあと
len(agent.logs)
messages = agent.write_memory_to_messages()

1.6 推奨モデル・無料枠・コスト ⚠️

霊夢: お金かかるの? 無料でやれる?

魔理沙: 状況次第 だ。Inference Providers はアカウント・モデル・混雑で変わる。

注意点 内容
無料枠 あるが上限・レート制限あり
モデル ID 重いモデルほど遅く・高くなりがち
ループ agent.run 1 回で 複数 Step → 複数回 LLM 呼び出し
本番 トークン数 × 単価を監視(第 21 章)

霊夢: じゃあ本書のデフォルトは?

魔理沙: CodeAgent + InferenceClientModel()(モデル ID 省略時はライブラリのデフォルト)。ローカルで無料に寄せたいなら第 4 章の OllamaTransformersModel だ。

from smolagents import CodeAgent, InferenceClientModel

# 本書のデフォルト構成
model = InferenceClientModel()
agent = CodeAgent(tools=[], model=model)

🔗 Installation Guide


🖥️ ハンズオン 1-1 — 環境とトークンを確認する

霊夢: 舞台裏の話は分かった。自分の PC が準備できてるか試したい!

魔理沙: 📦 examples/ch01/check_env.py を実行するぜ。

git clone https://github.com/hiromichinomata/yukkuri-smolagents.git
cd yukkuri-smolagents
source .venv/bin/activate
python examples/ch01/check_env.py
# examples/ch01/check_env.py(リポジトリ同梱・全文)
"""第1章: HF_TOKEN と import の事前確認"""
from __future__ import annotations

import os
import sys

def mask_token(token: str) -> str:
    if len(token) <= 8:
        return "hf_***"
    return f"{token[:4]}...{token[-4:]}"

def main() -> None:
    print("=== 第1章: 環境チェック ===")
    print(f"Python: {sys.version.split()[0]}")

    try:
        import smolagents

        print(f"smolagents: {smolagents.__version__}")
    except ImportError:
        print("smolagents が未インストールです。")
        print("  pip install 'smolagents[toolkit]'")
        sys.exit(1)

    from smolagents import CodeAgent, InferenceClientModel

    print("import OK: CodeAgent, InferenceClientModel")

    token = os.environ.get("HF_TOKEN") or os.environ.get("HUGGINGFACEHUB_API_TOKEN")
    if token:
        print(f"HF_TOKEN: 設定済み ({mask_token(token)})")
    else:
        print("HF_TOKEN: 未設定(クラウド推論のハンズオンには必要)")
        print("  export HF_TOKEN='hf_...'")
        print("  または .env に HF_TOKEN=... を書いて python-dotenv を使う")

    # モデルは作るだけ(API は叩かない)
    model = InferenceClientModel()
    agent = CodeAgent(tools=[], model=model)
    del agent
    print("CodeAgent / InferenceClientModel の初期化: OK")

if __name__ == "__main__":
    main()

期待される出力の例:

=== 第1章: 環境チェック ===
Python: 3.12.x
smolagents: 1.x.x
import OK: CodeAgent, InferenceClientModel
HF_TOKEN: 設定済み (hf_...xxxx)
CodeAgent / InferenceClientModel の初期化: OK

霊夢: HF_TOKEN: 未設定 だったら?

魔理沙: クラウド推論のハンズオンはスキップか失敗する。先に export するか .env を用意しろ。


🖥️ ハンズオン 1-2 — ログをプログラムから要約する

魔理沙: 第 0 章と同じく API を叩くが、今度は ログの中身 を数える。

python examples/ch01/inspect_run_logs.py
# examples/ch01/inspect_run_logs.py(リポジトリ同梱・全文)
"""第1章: agent.run 後のログを要約表示"""
from __future__ import annotations

import os
import sys

from smolagents import CodeAgent, InferenceClientModel

def summarize_logs(logs: list) -> None:
    print(f"\n=== agent.logs: {len(logs)} エントリ ===")
    for i, entry in enumerate(logs):
        keys = ", ".join(sorted(entry.keys())) if isinstance(entry, dict) else type(entry).__name__
        print(f"  [{i}] keys: {keys}")

def main() -> None:
    if not os.environ.get("HF_TOKEN") and not os.environ.get("HUGGINGFACEHUB_API_TOKEN"):
        print("HF_TOKEN が未設定のためスキップします。")
        print("  export HF_TOKEN='hf_...' のあと再実行してください。")
        sys.exit(0)

    model = InferenceClientModel()
    agent = CodeAgent(tools=[], model=model, verbosity_level=1)

    task = "1 から 5 までの合計を計算し、答えだけを返して"
    print(f"タスク: {task}\n")
    result = agent.run(task)

    print("\n=== 最終回答 ===")
    print(result)

    summarize_logs(agent.logs)

    messages = agent.write_memory_to_messages()
    print(f"\n=== write_memory_to_messages(): {len(messages)} 件 ===")
    for j, msg in enumerate(messages[:6]):
        role = getattr(msg, "role", type(msg).__name__)
        content = getattr(msg, "content", str(msg))
        preview = (content[:80] + "…") if len(str(content)) > 80 else content
        print(f"  [{j}] {role}: {preview}")
    if len(messages) > 6:
        print(f"  ... 他 {len(messages) - 6} 件")

if __name__ == "__main__":
    main()

スクリプトの要点:

"""第1章: agent.run 後のログを要約表示"""
from smolagents import CodeAgent, InferenceClientModel

agent = CodeAgent(tools=[], model=InferenceClientModel(), verbosity_level=1)
result = agent.run("1 から 5 までの合計を計算し、答えだけを返して")
print(result)
print(len(agent.logs))
agent.write_memory_to_messages()

霊夢: logs の各要素に keys: って出たわ。中身は章によって違うのね。

魔理沙: バージョンでキー名は多少変わる。件数と Step の流れ が分かれば十分だ。細部は第 3 章でまた触る。


1.7 よくあるエラーと対処 ⚠️

トークンを Git に載せてしまった

# 1. Hub でトークンを revoke
# 2. 新トークンを発行
# 3. git history から消す必要があるなら filter-repo 等(上級)

HF_TOKEN はあるのに 401

Unauthorized ...
echo "${HF_TOKEN:+set}"   # set なら変数は存在
# 先頭・末尾の空白、引用符の付け間違いを確認

別の Python を見ている

which python
pip show smolagents | grep Location

1.8 本章のまとめ

霊夢: 整理するわ。

  1. 霊夢=疑問、魔理沙=実装 の掛け合いで読む
  2. サンプルは examples/chNN/ に章対応で置いてある
  3. HF_TOKEN はクラウド推論用。.env はコミットしない
  4. ログは Step / 実行コード / final_answer / トークン数 を見る
  5. コストは 1 run = 複数 Step になりうる

魔理沙: 次章はインストールと extras の大作戦だ。


✅ 章末チェックリスト

  • [ ] 第0章first_agent.py を実行した
  • [ ] examples/ch01/check_env.py が import OK を表示した
  • [ ] HF_TOKEN を設定した(またはローカルモデル方針を決めた)
  • [ ] .env.gitignore した
  • [ ] ログの Step 0final_answer の意味が説明できる
  • [ ] examples/ の章番号ルールが分かった

次章へ

霊夢: ルール、把握したわ!

魔理沙: 次は依存関係をきっちり入れるぜ。ゆっくりしていこうな。


第2章 インストール大作戦 — extras と仮想環境

本章のゴール: smolagents を extras 付きで正しく入れ、仮想環境で import 確認HF_TOKEN 経由の Inference API 接続まで完了する。


2.1 extras って何?

霊夢: 第 0 章で pip install 'smolagents[toolkit]' って言われたけど、[toolkit] って何なの?

魔理沙: extras(追加依存のまとまり)だ。コアだけ入れるか、検索ツール付きか、LiteLLM 付きか……用途で選ぶ。

extra 主な用途
(なし) 最小の smolagents 本体
toolkit Web 検索など標準ツール(本書の基本
litellm OpenAI / Anthropic / Ollama 等 100+ プロバイダ
transformers ローカル TransformersModel
mlx-lm Apple Silicon の MLXModel
openai AzureOpenAIModel など
bedrock AmazonBedrockModel
mcp MCP サーバー連携
all 全部(重い・開発用)
# 本書の推奨(第 0〜6 章)
pip install 'smolagents[toolkit]'

# 第 4 章で Ollama を試すとき
pip install 'smolagents[toolkit,litellm]'

霊夢: [toolkit][litellm] の違いは?

魔理沙: toolkit = smolagents 公式の検索・音声ツール群。litellm = 外部 API をまとめて叩くアダプタだ。役割が違うから、両方必要ならカンマで並べる。

pip install "smolagents[toolkit,litellm]"

🔗 Installation


2.2 仮想環境 — venv と uv

霊夢: システムの Python に直接入れちゃダメなの?

魔理沙: 他プロジェクトと バージョン衝突 するから、venv が鉄則だ。

python3 -m venv .venv
source .venv/bin/activate   # Windows: .venv\Scripts\activate
python -V
pip install -U pip

uv を使う場合:

uv venv .venv
source .venv/bin/activate
uv pip install 'smolagents[toolkit]'
確認コマンド 期待
which python プロジェクトの .venv 配下
pip list | grep smolagents パッケージが表示される

2.3 依存関係トラブルシュート

霊夢: pip install で赤い文字が……

魔理沙: よくあるのはこの3つだ。

ERROR: Could not find a version that satisfies the requirement ...

→ Python が 3.10 未満 の可能性。

python3.12 -m venv .venv
ModuleNotFoundError: No module named 'smolagents'

→ venv 未 activate。

ImportError: cannot import name 'WebSearchTool'

[toolkit] なしで入れた。

pip install 'smolagents[toolkit]' --upgrade

2.4 HF_TOKEN と Inference API

霊夢: インストールできたら、つながるか試したい!

魔理沙: InferenceClientModel は内部で huggingface_hub.InferenceClient を使う。トークンは環境変数 HF_TOKEN が読まれる。

from smolagents import InferenceClientModel

# token 引数を省略 → 環境変数 HF_TOKEN を使用
model = InferenceClientModel()

明示的に渡す例(本番は環境変数推奨):

import os
from smolagents import InferenceClientModel

model = InferenceClientModel(token=os.environ["HF_TOKEN"])

⚠️ コードに token="hf_..."直書きしない


🖥️ ハンズオン 2-1 — import 確認

霊夢: 入ったかどうか、コマンド一発で見たいわ。

魔理沙: 📦 examples/ch02/import_check.py だ。

pip install 'smolagents[toolkit]'
python examples/ch02/import_check.py
# examples/ch02/import_check.py(リポジトリ同梱・全文)
"""第2章: extras 導入後の import 確認"""
from __future__ import annotations

import sys

def main() -> None:
    print("=== 第2章: import チェック ===")
    print(f"Python: {sys.version.split()[0]}\n")

    try:
        import smolagents

        print(f"  OK  smolagents {smolagents.__version__}")
        from smolagents import CodeAgent, InferenceClientModel

        print("  OK  CodeAgent, InferenceClientModel")
    except ImportError as exc:
        print(f"  NG  smolagents: {exc}")
        print("\npip install 'smolagents[toolkit]' を実行してください")
        sys.exit(1)

    try:
        from smolagents import WebSearchTool

        print("  OK  WebSearchTool ([toolkit])")
    except ImportError:
        print("  NG  WebSearchTool — pip install 'smolagents[toolkit]'")

    try:
        import huggingface_hub

        print(f"  OK  huggingface_hub {huggingface_hub.__version__}")
    except ImportError:
        print("  NG  huggingface_hub(toolkit インストールで入ることが多い)")

if __name__ == "__main__":
    main()

期待出力:

=== 第2章: import チェック ===
Python: 3.12.x
  OK  smolagents 1.x.x
  OK  CodeAgent, InferenceClientModel
  OK  WebSearchTool ([toolkit])
  OK  huggingface_hub x.x.x

ワンライナーでも可:

python -c "from smolagents import CodeAgent, InferenceClientModel, WebSearchTool; print('OK')"

🖥️ ハンズオン 2-2 — Inference API に接続

魔理沙: 短いタスクで 実際に 1 run する。

export HF_TOKEN="hf_..."   # プレースホルダを実トークンに
python examples/ch02/hf_connection.py
# examples/ch02/hf_connection.py(リポジトリ同梱・全文)
"""第2章: HF Inference API への接続テスト(短いタスク)"""
from __future__ import annotations

import os
import sys

from smolagents import CodeAgent, InferenceClientModel

def main() -> None:
    token = os.environ.get("HF_TOKEN") or os.environ.get("HUGGINGFACEHUB_API_TOKEN")
    if not token:
        print("HF_TOKEN が未設定です。")
        print("  export HF_TOKEN='hf_...'")
        sys.exit(1)

    print("=== 第2章: Inference API 接続テスト ===")
    model = InferenceClientModel()
    agent = CodeAgent(tools=[], model=model, verbosity_level=1)

    result = agent.run("2 + 2 は? 答えの数字だけ返して。")
    print("\n=== 最終回答 ===")
    print(result)

if __name__ == "__main__":
    main()

霊夢: 4 って返ってきた! つながってるわね。

魔理沙: ここまでできれば第 3 章の本番ハンズオンに進める。


2.5 よくあるエラー集 ⚠️

401 Unauthorized

Unauthorized ... HF_TOKEN ...
export HF_TOKEN="hf_..."
python examples/ch02/hf_connection.py
# examples/ch02/hf_connection.py(リポジトリ同梱・全文)
"""第2章: HF Inference API への接続テスト(短いタスク)"""
from __future__ import annotations

import os
import sys

from smolagents import CodeAgent, InferenceClientModel

def main() -> None:
    token = os.environ.get("HF_TOKEN") or os.environ.get("HUGGINGFACEHUB_API_TOKEN")
    if not token:
        print("HF_TOKEN が未設定です。")
        print("  export HF_TOKEN='hf_...'")
        sys.exit(1)

    print("=== 第2章: Inference API 接続テスト ===")
    model = InferenceClientModel()
    agent = CodeAgent(tools=[], model=model, verbosity_level=1)

    result = agent.run("2 + 2 は? 答えの数字だけ返して。")
    print("\n=== 最終回答 ===")
    print(result)

if __name__ == "__main__":
    main()

Rate limit

Rate limit exceeded ...

対処: 待つ / 別モデル / HF アカウントの枠を確認(第 4 章)。

モデル ID が無効

Model ... not supported ...
# 別 ID を試す(例)
InferenceClientModel(model_id="Qwen/Qwen2.5-Coder-32B-Instruct")

ネットワーク

curl -I https://huggingface.co

2.6 本章のまとめ

霊夢: まとめるわ。

  1. [toolkit] で標準ツール付きインストールが本書の基本
  2. venv でプロジェクトごとに隔離
  3. InferenceClientModelHF_TOKEN で認証
  4. ハンズオンで import短い run を確認した

✅ 章末チェックリスト

  • [ ] Python 3.10+ の venv を使っている
  • [ ] pip install 'smolagents[toolkit]' 成功
  • [ ] examples/ch02/import_check.py が OK
  • [ ] HF_TOKEN を設定した
  • [ ] examples/ch02/hf_connection.pyagent.run が完走した

次章へ

霊夢: インストール編、クリアよ!

魔理沙: 次はエージェントの最小構成だぜ。


第3章 エージェントの最小構成 — model と tools

本章のゴール: model + tools の 2 要素CodeAgent を組み立て、run() の流れと agent.logs / write_memory_to_messages() で実行の中身を読める。


3.1 動かす 2 要素

霊夢: エージェントって、結局何があれば動くの?

魔理沙: 最小は モデルツールリスト だ。

from smolagents import CodeAgent, InferenceClientModel

model = InferenceClientModel()          # 頭脳
tools = []                              # 手足(空でも可)
agent = CodeAgent(tools=tools, model=model)
引数 役割
model LLM を呼び出すアダプタ
tools エージェントが使える Tool のリスト
add_base_tools True で標準ツールを追加(第 6 章)

霊夢: tools=[] なのに第 0 章は動いたわ。

魔理沙: CodeAgent は内蔵の Python 実行 があるから、計算だけならツール不要だ。


3.2 CodeAgent.run() の流れ

霊夢: run("...") の中で何が起きてるの?

魔理沙: ざっくり ループ だ。

flowchart TD
  A[タスク受付] --> B[LLM: 次の行動を決める]
  B --> C[Python コード生成・実行]
  C --> D{final_answer?}
  D -->|No| B
  D -->|Yes| E[最終回答を返す]
  1. タスクをシステムプロンプトに載せる
  2. LLM がコード(やツール呼び出し)を出す
  3. 実行結果を LLM に返す
  4. final_answer(...) が出るまで繰り返す
result = agent.run("フィボナッチ数列の第 10 項を求めて")
# result には最終回答の値が入る

ログ上の目印:

━━━━━━━━━━━━━━━━━━━━ Step 0 ━━━━━━━━━━━━━━━
╭─ Executing this code: ─────────────────╮
│ ...                                    │
╰────────────────────────────────────────╯
...
╭─ Executing this code: ─────────────────╮
│ final_answer(55)                       │
╰────────────────────────────────────────╯

🔗 Quickstart


3.3 ログの読み方(Step・コード・トークン)

霊夢: Step が増えるたびにお金っぽいのよね……

魔理沙: その認識でいい。1 Step ≒ 1 回の LLM 往復 に近いと思え。

ログ要素 見るポイント
Step N 何回考え直したか
Executing this code 実際に走った Python
Observation / Out 実行結果
Input tokens プロンプト長の目安
Output tokens 生成量の目安

verbosity を下げる例:

agent = CodeAgent(tools=[], model=model, verbosity_level=0)

3.4 agent.logswrite_memory_to_messages()

魔理沙: 画面ログの裏側には 構造化された履歴 がある。

  • agent.logs: 各 Step の詳細が dict などで リストに追加 される
  • write_memory_to_messages(): モデルに再投入する形式の チャットメッセージ列 に変換(全ログは含まない)
agent.run("3 の階乗を計算して")

print(len(agent.logs))
for i, entry in enumerate(agent.logs):
    print(i, entry.keys() if isinstance(entry, dict) else type(entry))

messages = agent.write_memory_to_messages()
print(len(messages))

霊夢: 全部は messages に入らないのね。

魔理沙: デバッグの 高レベル要約 用だと思え。細部は logs を見ろ。


🖥️ ハンズオン 3-1 — ツールなしでフィボナッチ

霊夢: 自分で数列、出してもらいたい!

魔理沙: 📦 examples/ch03/fibonacci_no_tools.py

export HF_TOKEN="hf_..."
python examples/ch03/fibonacci_no_tools.py
# examples/ch03/fibonacci_no_tools.py(リポジトリ同梱・全文)
"""第3章: ツールなし CodeAgent でフィボナッチ"""
from __future__ import annotations

import os
import sys

from smolagents import CodeAgent, InferenceClientModel

def main() -> None:
    if not os.environ.get("HF_TOKEN") and not os.environ.get("HUGGINGFACEHUB_API_TOKEN"):
        print("HF_TOKEN が未設定のためスキップします。")
        sys.exit(0)

    model = InferenceClientModel()
    agent = CodeAgent(tools=[], model=model)

    task = (
        "フィボナッチ数列の第 10 項を Python で計算し、"
        "整数の答えだけを final_answer で返して。"
    )
    print(f"タスク: {task}\n")
    result = agent.run(task)
    print("=== 最終回答 ===")
    print(result)

if __name__ == "__main__":
    main()

期待: 第 10 項は 55(0-indexed で F(10)=55 の定義に注意。モデルが 1-indexed で答える場合はタスク文で指定を厳密に)。

Out - Final answer: 55

霊夢: ログに def fib みたいなコードが出てたわ。

魔理沙: CodeAgent の典型だ。ツールなしでもアルゴリズムを書いて実行 する。


🖥️ ハンズオン 3-2 — logs と messages を覗く

python examples/ch03/inspect_agent_logs.py
# examples/ch03/inspect_agent_logs.py(リポジトリ同梱・全文)
"""第3章: agent.logs と write_memory_to_messages を覗く"""
from __future__ import annotations

import json
import os
import sys

from smolagents import CodeAgent, InferenceClientModel

def main() -> None:
    if not os.environ.get("HF_TOKEN") and not os.environ.get("HUGGINGFACEHUB_API_TOKEN"):
        print("HF_TOKEN が未設定のためスキップします。")
        sys.exit(0)

    model = InferenceClientModel()
    agent = CodeAgent(tools=[], model=model, verbosity_level=1)

    agent.run("3 の階乗を計算し、答えだけ返して。")

    print(f"\n=== logs 件数: {len(agent.logs)} ===")
    for i, step in enumerate(agent.logs):
        if not isinstance(step, dict):
            print(f"  [{i}] {step!r}")
            continue
        step_type = step.get("step_type") or step.get("type") or "?"
        print(f"  [{i}] step_type={step_type!r}, keys={list(step.keys())}")

    print("\n=== 最後の log エントリ(整形) ===")
    if agent.logs:
        last = agent.logs[-1]
        if isinstance(last, dict):
            print(json.dumps(last, ensure_ascii=False, indent=2, default=str)[:2000])
        else:
            print(last)

    messages = agent.write_memory_to_messages()
    print(f"\n=== messages: {len(messages)} 件 ===")
    for j, msg in enumerate(messages):
        role = getattr(msg, "role", "?")
        content = str(getattr(msg, "content", msg))
        print(f"--- [{j}] {role} ({len(content)} chars) ---")

if __name__ == "__main__":
    main()

霊夢: messages の方が短いわ。会話用に間引いてるのね。

魔理沙: 当たり。UI や再プロンプト用の 別ビュー だ。


3.5 よくあるエラー ⚠️

final_answer しないまま max steps

Max steps reached ...
agent = CodeAgent(tools=[], model=model, max_steps=10)
# タスクを具体化: 「必ず final_answer で整数のみ返す」

Step 内の Python 失敗

Code execution failed ...

対処: 入力を小さくする・「標準ライブラリのみ」など制約を足す。

トークン不足っぽい途中切れ

対処: タスクを短くする / 第 4 章で max_tokens を調整。


3.6 本章のまとめ

霊夢: 整理!

  1. model + tools がエージェントの最小構成
  2. run() は Step ループで final_answer まで進む
  3. CodeAgent はツールが空でも Python 実行できる
  4. agent.logs が詳細、write_memory_to_messages() が要約ビュー

✅ 章末チェックリスト

  • [ ] CodeAgent(tools=[], model=InferenceClientModel()) を説明できる
  • [ ] examples/ch03/fibonacci_no_tools.py が完走した
  • [ ] ログで Stepfinal_answer を指せる
  • [ ] examples/ch03/inspect_agent_logs.pylogs 件数を確認した
  • [ ] write_memory_to_messages() の役割を一言で言える

次章へ

霊夢: 最小構成、マスターしたわ!

魔理沙: 次は頭脳を差し替えるぜ。


第4章 モデルを選ぶ — 頭脳の付け替え

本章のゴール: 複数の *Model クラスの違いを把握し、Inference API または Ollama のどちらか一方に接続して同じタスクを実行できる。


4.1 ローカル? クラウド? 無料?

霊夢: モデルって選択肢が多すぎて困るのよね。

魔理沙: smolagents は 同じ CodeAgent に、違う model= を刺すだけだ。

方式 クラス 向き
HF Inference Providers InferenceClientModel 本書デフォルト・無料枠あり
100+ API LiteLLMModel OpenAI / Anthropic / Ollama 等
ローカル transformers TransformersModel GPU/CPU で完結
Apple Silicon MLXModel Mac 向け高速
Azure AzureOpenAIModel 企業 Azure OpenAI
AWS AmazonBedrockModel Bedrock ネイティブ
flowchart LR
  Agent[CodeAgent]
  Agent --> ICM[InferenceClientModel]
  Agent --> LLM[LiteLLMModel]
  Agent --> TR[TransformersModel]
  Agent --> MLX[MLXModel]

4.2 InferenceClientModel(推奨)

魔理沙: huggingface_hub.InferenceClient 経由で Hub 上の多数プロバイダを叩く。

from smolagents import CodeAgent, InferenceClientModel

model = InferenceClientModel()
# または model_id を明示
model = InferenceClientModel(model_id="Qwen/Qwen2.5-Coder-32B-Instruct")

agent = CodeAgent(tools=[], model=model)

認証:

export HF_TOKEN="hf_..."
import os
from smolagents import InferenceClientModel

model = InferenceClientModel(token=os.environ.get("HF_TOKEN"))

4.3 LiteLLMModel — OpenAI / Anthropic / Ollama

pip install 'smolagents[litellm]'
from smolagents import CodeAgent, LiteLLMModel

model = LiteLLMModel(
    model_id="anthropic/claude-3-5-sonnet-latest",
    api_key="YOUR_ANTHROPIC_API_KEY",  # 本番は環境変数推奨
)
agent = CodeAgent(tools=[], model=model)

Ollama(ローカル)の例:

from smolagents import CodeAgent, LiteLLMModel

model = LiteLLMModel(
    model_id="ollama_chat/llama3.2",
    api_base="http://localhost:11434",
    api_key="ollama",
    num_ctx=8192,
)
agent = CodeAgent(tools=[], model=model)

⚠️ Ollama は別途 ollama serve とモデルの ollama pull が必要。


4.4 TransformersModel と MLXModel

pip install 'smolagents[transformers]'
from smolagents import CodeAgent, TransformersModel

model = TransformersModel(model_id="meta-llama/Llama-3.2-3B-Instruct")
agent = CodeAgent(tools=[], model=model)

Apple Silicon:

pip install 'smolagents[mlx-lm]'
from smolagents import CodeAgent, MLXModel

model = MLXModel(model_id="mlx-community/Llama-3.2-3B-Instruct-4bit")
agent = CodeAgent(tools=[], model=model)

霊夢: GPU メモリと相談ね……

魔理沙: ああ。VRAM 計算機などでモデルサイズを選べ。


4.5 Azure OpenAI と Amazon Bedrock

pip install 'smolagents[openai]'
from smolagents import CodeAgent, AzureOpenAIModel

model = AzureOpenAIModel(model_id="gpt-4o-mini")
agent = CodeAgent(tools=[], model=model)

環境変数の例:

export AZURE_OPENAI_ENDPOINT="https://xxxx.openai.azure.com"
export AZURE_OPENAI_API_KEY="..."
export OPENAI_API_VERSION="2024-10-01-preview"

Bedrock:

pip install 'smolagents[bedrock]'
from smolagents import AmazonBedrockModel, CodeAgent

model = AmazonBedrockModel(model_id="anthropic.claude-3-sonnet-20240229-v1:0")
agent = CodeAgent(tools=[], model=model)

LiteLLM 経由の Bedrock:

from smolagents import LiteLLMModel

model = LiteLLMModel(model_id="bedrock/anthropic.claude-3-sonnet-20240229-v1:0")

4.6 モデルパラメータ — temperature / max_tokens / REMOVE_PARAMETER

魔理沙: 各 Model は生成パラメータを受け取れる。サポート外のキーは REMOVE_PARAMETER で送らないようにできる。

from smolagents import InferenceClientModel, REMOVE_PARAMETER

model = InferenceClientModel(
    temperature=0.2,
    max_tokens=512,
    # プロバイダが未対応の引数を落とす例
    # some_vendor_specific_arg=REMOVE_PARAMETER,
)
パラメータ 効果
temperature 低いほど決定的
max_tokens 出力上限
model_id チェックポイント / デプロイ名

4.7 モデル選びの指針表

状況 おすすめ
本書を最短で InferenceClientModel + HF_TOKEN
オフライン / 課金回避 TransformersModel / Ollama
既存 OpenAI 契約 LiteLLMModel / AzureOpenAIModel
AWS 統一 AmazonBedrockModel
Mac のローカル MLXModel
エージェント向きコード生成 Coder 系 model_id

🖥️ ハンズオン 4-1 — 同じタスクを 2 モデルで比較

霊夢: 違い、体感したい!

魔理沙: 📦 examples/ch04/compare_models.py

export HF_TOKEN="hf_..."
python examples/ch04/compare_models.py

# 2 つ目を比較する場合
export SMOLAGENTS_MODEL_ID_SECOND="Qwen/Qwen2.5-Coder-32B-Instruct"
python examples/ch04/compare_models.py
# examples/ch04/compare_models.py(リポジトリ同梱・全文)
"""第4章: 同じタスクを2つのモデル設定で比較(HF + 任意の2つ目)"""
from __future__ import annotations

import os
import sys
import time

from smolagents import CodeAgent, InferenceClientModel

TASK = "1 から 20 までの整数の合計を計算し、答えだけ返して。"

def run_once(label: str, model_id: str | None) -> None:
    print(f"\n{'=' * 50}")
    print(f"モデル: {label}")
    if model_id:
        model = InferenceClientModel(model_id=model_id)
    else:
        model = InferenceClientModel()
    agent = CodeAgent(tools=[], model=model, verbosity_level=0)
    started = time.perf_counter()
    result = agent.run(TASK)
    elapsed = time.perf_counter() - started
    print(f"回答: {result}")
    print(f"所要時間: {elapsed:.1f}s / logs: {len(agent.logs)} 件")

def main() -> None:
    if not os.environ.get("HF_TOKEN") and not os.environ.get("HUGGINGFACEHUB_API_TOKEN"):
        print("HF_TOKEN が未設定のためスキップします。")
        sys.exit(0)

    # デフォルトモデル
    run_once("InferenceClientModel(デフォルト)", None)

    # 2 つ目は環境変数で上書き可能
    second = os.environ.get("SMOLAGENTS_MODEL_ID_SECOND")
    if second:
        run_once(f"InferenceClientModel({second})", second)
    else:
        print("\n2 つ目の比較をするには:")
        print("  export SMOLAGENTS_MODEL_ID_SECOND='Qwen/Qwen2.5-Coder-32B-Instruct'")
        print("  python examples/ch04/compare_models.py")

if __name__ == "__main__":
    main()
==================================================
モデル: InferenceClientModel(デフォルト)
回答: 210
所要時間: 12.3s / logs: 3 件

霊夢: 所要時間と logs 件数、メモったわ。


🖥️ ハンズオン 4-2 — Inference か Ollama か一方に接続

📦 examples/ch04/connect_backend.py

# A: Hugging Face(デフォルト)
export HF_TOKEN="hf_..."
python examples/ch04/connect_backend.py

# B: Ollama
export SMOLAGENTS_BACKEND=ollama
export OLLAMA_MODEL_ID="ollama_chat/llama3.2"
python examples/ch04/connect_backend.py
# examples/ch04/connect_backend.py(リポジトリ同梱・全文)
"""第4章: Inference API または Ollama のどちらか一方に接続"""
from __future__ import annotations

import os
import sys

from smolagents import CodeAgent, InferenceClientModel

TASK = "7 の平方根を概算し、小数第2位まで返して。"

def run_hf() -> None:
    print("=== バックエンド: Hugging Face InferenceClientModel ===")
    model = InferenceClientModel()
    agent = CodeAgent(tools=[], model=model, verbosity_level=1)
    print(agent.run(TASK))

def run_ollama() -> None:
    print("=== バックエンド: Ollama (LiteLLMModel) ===")
    try:
        from smolagents import LiteLLMModel
    except ImportError:
        print("pip install 'smolagents[litellm]' が必要です")
        sys.exit(1)

    model = LiteLLMModel(
        model_id=os.environ.get("OLLAMA_MODEL_ID", "ollama_chat/llama3.2"),
        api_base=os.environ.get("OLLAMA_API_BASE", "http://localhost:11434"),
        api_key=os.environ.get("OLLAMA_API_KEY", "ollama"),
        num_ctx=int(os.environ.get("OLLAMA_NUM_CTX", "8192")),
    )
    agent = CodeAgent(tools=[], model=model, verbosity_level=1)
    print(agent.run(TASK))

def main() -> None:
    backend = os.environ.get("SMOLAGENTS_BACKEND", "hf").lower()
    if backend == "ollama":
        run_ollama()
    elif backend == "hf":
        if not os.environ.get("HF_TOKEN") and not os.environ.get("HUGGINGFACEHUB_API_TOKEN"):
            print("HF_TOKEN が未設定です。Ollama を使う場合:")
            print("  export SMOLAGENTS_BACKEND=ollama")
            sys.exit(1)
        run_hf()
    else:
        print("SMOLAGENTS_BACKEND は 'hf' または 'ollama' を指定してください")
        sys.exit(1)

if __name__ == "__main__":
    main()
# Ollama 側の準備例
ollama pull llama3.2
ollama serve

4.8 よくあるエラー ⚠️

Ollama connection refused

Connection refused ... 11434

ollama serve が起動しているか確認。

Azure / Bedrock の認証

→ 環境変数とリージョン・デプロイ名を再確認。キーはコードに直書きしない。

モデルが tool use に弱い

→ Step が増える・final_answer しない。Coder 系 model_id に変更。


4.9 本章のまとめ

霊夢: 頭脳は差し替え可能、ってことね。

  1. InferenceClientModel が本書のデフォルト
  2. LiteLLMModel で Ollama / OpenAI 等
  3. ローカルは Transformers / MLX
  4. 企業向けは Azure / Bedrock
  5. パラメータで 温度・長さ を調整

✅ 章末チェックリスト

  • [ ] 5 種類以上の Model クラス名を挙げられる
  • [ ] examples/ch04/compare_models.py を実行した
  • [ ] HF または Ollama のどちらかで connect_backend.py が動いた
  • [ ] temperature / max_tokens の意味を説明できる

次章へ

魔理沙: 次はエージェントの「手」だぜ。


第5章 ツールの基本 — エージェントの「手」

本章のゴール: @toolTool サブクラスで自作ツールを定義し、Hub ダウンロード数トップのモデル名を返すエージェントを動かし、agent.tools で差し替えできる。


5.1 ツールとは何か

霊夢: CodeAgent は Python を書けるのに、わざわざツールが要るの?

魔理沙: 外の世界 に出るときはツールだ。Hub API、検索、社内 DB……は 名前・説明・入出力スキーマ が要る。

要素 役割
name LLM が呼び分ける識別子
description システムプロンプトに載る説明
inputs 引数の型と説明
output_type 戻り値の型
forward 実際の処理
flowchart LR
  LLM[LLM] -->|ツール呼び出し| Tool[Tool.forward]
  Tool -->|結果| LLM

初期化時に、全ツールの説明が エージェントのシステムプロンプト に焼き込まれる。

🔗 Tools tutorial


5.2 @tool デコレータ

魔理沙: 関数を書いて、docstring で説明すれば最短だ。

from smolagents import tool

@tool
def model_download_tool(task: str) -> str:
    """
    指定タスクで Hugging Face Hub のダウンロード数が最も多いモデル ID を返す。

    Args:
        task: タスク名(例: text-classification)
    """
    from huggingface_hub import list_models

    model = next(iter(list_models(filter=task, sort="downloads", direction=-1)))
    return model.id

要件:

  • 型ヒント(引数・戻り値)
  • docstring に Args: で各引数の説明
  • 関数名がそのままツール名になる
from smolagents import CodeAgent, InferenceClientModel

agent = CodeAgent(tools=[model_download_tool], model=InferenceClientModel())

5.3 Tool サブクラス

霊夢: もっと複雑な道具は?

魔理沙: Tool を継承する。Hub 公式例と同じ形だ。

from smolagents import Tool

class ModelDownloadTool(Tool):
    name = "model_download_tool"
    description = (
        "指定タスクで Hugging Face Hub のダウンロード数が最も多いモデル ID を返す。"
    )
    inputs = {
        "task": {
            "type": "string",
            "description": "タスク名(例: summarization)",
        }
    }
    output_type = "string"

    def forward(self, task: str) -> str:
        from huggingface_hub import list_models

        model = next(iter(list_models(filter=task, sort="downloads", direction=-1)))
        return model.id
方式 向き
@tool 短い関数 1 本
Tool サブクラス 状態を持つ・メソッド複数

5.4 システムプロンプトへの自動埋め込み

霊夢: エージェントはツールの存在、どう知るの?

魔理沙: 初期化時に ツール一覧がプロンプトに注入 される。だから descriptionArgs は丁寧に書け、とさっき言ったんだ。

agent = CodeAgent(tools=[model_download_tool], model=InferenceClientModel())
# agent.tools は dict: name -> Tool インスタンス
print(list(agent.tools.keys()))

手動でツールだけ試す(エージェントなし):

from huggingface_hub import list_models

task = "text-classification"
print(next(iter(list_models(filter=task, sort="downloads", direction=-1))).id)

🖥️ ハンズオン 5-1 — Hub ダウンロード数トップのモデル名

霊夢: エージェントに Hub を調べさせたい!

魔理沙: 📦 examples/ch05/hub_top_model_tool.py

export HF_TOKEN="hf_..."
python examples/ch05/hub_top_model_tool.py
# examples/ch05/hub_top_model_tool.py(リポジトリ同梱・全文)
"""第5章: Hub ダウンロード数トップのモデル名を返す自作ツール"""
from __future__ import annotations

import os
import sys

from huggingface_hub import list_models
from smolagents import CodeAgent, InferenceClientModel, tool

@tool
def model_download_tool(task: str) -> str:
    """
    指定タスクで Hugging Face Hub のダウンロード数が最も多いモデル ID を返す。

    Args:
        task: タスク名(例: text-classification, text-to-video)
    """
    model = next(iter(list_models(filter=task, sort="downloads", direction=-1)))
    return model.id

def main() -> None:
    if not os.environ.get("HF_TOKEN") and not os.environ.get("HUGGINGFACEHUB_API_TOKEN"):
        print("HF_TOKEN が未設定のためスキップします。")
        sys.exit(0)

    model = InferenceClientModel()
    agent = CodeAgent(tools=[model_download_tool], model=model)

    task = (
        "text-classification タスクで Hub のダウンロード数が最も多い "
        "モデル名を model_download_tool で調べ、名前だけ返して。"
    )
    print(agent.run(task))

if __name__ == "__main__":
    main()

霊夢: ログに model_download_tool の呼び出しが出てた!

魔理沙: CodeAgent は Python からツールを呼ぶコード を書く。JSON 専用の ToolCallingAgent とは違う見え方だな。


🖥️ ハンズオン 5-2 — agent.tools で差し替え

📦 examples/ch05/swap_tools.py

python examples/ch05/swap_tools.py
# examples/ch05/swap_tools.py(リポジトリ同梱・全文)
"""第5章: agent.tools 辞書で実行中にツールを差し替え"""
from __future__ import annotations

import os
import sys

from huggingface_hub import list_models
from smolagents import CodeAgent, InferenceClientModel, Tool

class ModelDownloadTool(Tool):
    name = "model_download_tool"
    description = (
        "指定タスクで Hugging Face Hub のダウンロード数が最も多いモデル ID を返す。"
    )
    inputs = {
        "task": {
            "type": "string",
            "description": "タスク名(例: summarization)",
        }
    }
    output_type = "string"

    def forward(self, task: str) -> str:
        model = next(iter(list_models(filter=task, sort="downloads", direction=-1)))
        return model.id

def main() -> None:
    if not os.environ.get("HF_TOKEN") and not os.environ.get("HUGGINGFACEHUB_API_TOKEN"):
        print("HF_TOKEN が未設定のためスキップします。")
        sys.exit(0)

    model = InferenceClientModel()
    agent = CodeAgent(tools=[], model=model, add_base_tools=False)

    print("=== 初期 tools ===")
    print(list(agent.tools.keys()))

    hub_tool = ModelDownloadTool()
    agent.tools[hub_tool.name] = hub_tool

    print("=== 差し替え後 tools ===")
    print(list(agent.tools.keys()))

    result = agent.run(
        "summarization タスクで最もダウンロードされている Hub モデル名を返して。"
    )
    print("\n=== 最終回答 ===")
    print(result)

if __name__ == "__main__":
    main()

霊夢: 空で作ってから辞書に足すのね。

魔理沙: agent.tools は普通の dict だ。実行中の差し替えも可能だが、走ってる run の途中 は基本いじらない方がいい。


5.5 ツールを増やしすぎない ⚠️

魔理沙: 弱いモデルほど、ツールが多いと 迷子 になる。

対策 内容
ツール数 タスクに必要な分だけ
説明 1 ツール 1 責務
統合 似た API は 1 ツールにまとめる(第 14 章)

5.6 よくあるエラー ⚠️

Hub API / ネットワーク

ConnectionError ... huggingface.co

→ ネットワークと HF_TOKEN(プライベートモデルでない限り Read は不要なことも多いが、レート制限あり)

docstring に Args がない

@tool の説明が貧弱になり、LLM が引数を間違える。

forward の import

Hub に push する場合は forward 内で import するルールあり(第 7 章)。


5.7 本章のまとめ

霊夢: 手の作り方、分かったわ。

  1. ツール = LLM 向け API + forward 実装
  2. @tool が手軽、Tool 継承 が本格
  3. 説明はシステムプロンプトに載る
  4. agent.tools[name] = tool で追加・差し替え
  5. 多すぎるツールは弱モデルを壊す

✅ 章末チェックリスト

  • [ ] @tool で関数をツール化した
  • [ ] Tool サブクラスの 4 属性を説明できる
  • [ ] examples/ch05/hub_top_model_tool.py が完走した
  • [ ] examples/ch05/swap_tools.pyagent.tools を更新した
  • [ ] ツール説明が LLM 向けである理由を説明できる

次章へ

霊夢: 自作ツール、楽しいわね!

魔理沙: 次は公式が用意した道具箱だぜ。


第6章 標準ツールボックス — すぐ使える道具

本章のゴール: add_base_toolsWebSearchTool を理解し、Web 検索タスクをエージェントに実行させ、ツール単体の手動テストもできる。


6.1 add_base_tools=True の意味

霊夢: 毎回ツールを自作しなくても、最初から入ってるの?

魔理沙: [toolkit] extra を入れていれば、標準ツールボックス を足せる。

from smolagents import CodeAgent, InferenceClientModel

agent = CodeAgent(
    tools=[],
    model=InferenceClientModel(),
    add_base_tools=True,
)
print(list(agent.tools.keys()))
標準ツール(概要) 用途
DuckDuckGo 検索系 Web 検索
Python interpreter ToolCallingAgent 向け(CodeAgent は自前実行)
Transcriber 音声→テキスト(Whisper 系)

霊夢: CodeAgent なのに Python インタプリタツール?

魔理沙: CodeAgent は もともとコード実行できる から、重複を避ける設計だ。検索だけ欲しいときは WebSearchTool を明示 する方が分かりやすい。


6.2 DuckDuckGoSearchToolWebSearchTool

魔理沙: ドキュメントでは WebSearchTool が推奨されることが多い。

from smolagents import DuckDuckGoSearchTool, WebSearchTool

# 低レベル(DuckDuckGo 直)
ddg = DuckDuckGoSearchTool()

# 本書のハンズオンで使うラッパ(推奨)
search_tool = WebSearchTool()

add_base_tools 内部では DuckDuckGo 系が使われるが、ハンズオンでは WebSearchTool を直接渡す 形に統一する。

from smolagents import CodeAgent, InferenceClientModel, WebSearchTool

agent = CodeAgent(
    tools=[WebSearchTool()],
    model=InferenceClientModel(),
    add_base_tools=False,
)

6.3 PythonInterpreterTool(ToolCallingAgent 向け)

霊夢: CodeAgent じゃないエージェント用?

魔理沙: ああ。ToolCallingAgent は JSON でツールを呼ぶから、Python 実行用のツール が別途要る。CodeAgent には不要(第 10 章)。

from smolagents import ToolCallingAgent, InferenceClientModel

agent = ToolCallingAgent(
    tools=[],
    model=InferenceClientModel(),
    add_base_tools=True,
)

6.4 音声 — Transcriber(概要)

pip install 'smolagents[toolkit,audio]'

音声ファイル URL を additional_args で渡すパターンは第 19 章。本章では 存在を知っている 程度で十分だ。

(第19章で詳述)Transcriber + マルチモーダル model

🖥️ ハンズオン 6-1 — Web 検索でニュースを調べる

霊夢: 今日のニュース、エージェントに調べさせたい!

魔理沙: 📦 examples/ch06/web_search_agent.py

pip install 'smolagents[toolkit]'
export HF_TOKEN="hf_..."
python examples/ch06/web_search_agent.py
# examples/ch06/web_search_agent.py(リポジトリ同梱・全文)
"""第6章: Web 検索でニュースを調べる CodeAgent"""
from __future__ import annotations

import os
import sys

from smolagents import CodeAgent, InferenceClientModel, WebSearchTool

def main() -> None:
    if not os.environ.get("HF_TOKEN") and not os.environ.get("HUGGINGFACEHUB_API_TOKEN"):
        print("HF_TOKEN が未設定のためスキップします。")
        sys.exit(0)

    model = InferenceClientModel()
    agent = CodeAgent(
        tools=[WebSearchTool()],
        model=model,
        add_base_tools=False,
    )

    task = (
        "Web 検索ツールを使い、日本の主要ニュースの見出しを 3 つ挙げ、"
        "出典 URL も含めて要約して返して。"
    )
    print(agent.run(task))

if __name__ == "__main__":
    main()

霊夢: Step が増えたわ。検索 → 読む → まとめ、って感じね。

魔理沙: 外部 API + LLM の 往復が増える から、トークンと時間に注意だ。


🖥️ ハンズオン 6-2 — ツールを手動で試す

霊夢: エージェント抜きで検索だけ試せない?

魔理沙: できる。デバッグの定石だ。

python examples/ch06/manual_search_tool.py
# examples/ch06/manual_search_tool.py(リポジトリ同梱・全文)
"""第6章: WebSearchTool をエージェントなしで手動実行"""
from __future__ import annotations

import sys

from smolagents import WebSearchTool

def main() -> None:
    search_tool = WebSearchTool()
    query = "smolagents Hugging Face"
    print(f"クエリ: {query}\n")

    try:
        result = search_tool(query)
    except Exception as exc:  # noqa: BLE001 — ネットワーク・依存の差を表示
        print(f"検索に失敗しました: {exc}")
        print("pip install 'smolagents[toolkit]' とネット接続を確認してください。")
        sys.exit(1)

    print("=== 検索結果(先頭 1500 文字) ===")
    text = str(result)
    print(text[:1500])
    if len(text) > 1500:
        print(f"\n... (全 {len(text)} 文字)")

if __name__ == "__main__":
    main()

期待: 検索結果のテキスト(長いので先頭だけ表示)。

=== 検索結果(先頭 1500 文字) ===
...

霊夢: エージェントが変なとき、ここで切り分けられるのね。

魔理沙: ツールが壊れてるのか LLM が壊れてるのか を分ける。


6.5 ツールを増やしすぎない理由 ⚠️

問題 原因
間違ったツール選択 説明が似ているツールが多い
遅い 検索 × 多 Step
高い 長い検索結果がプロンプトに入る

対策:

# 検索だけ渡す(余計な base を足さない)
CodeAgent(tools=[WebSearchTool()], model=model, add_base_tools=False)

タスク文で 出典・件数・言語 を指定する(第 14・17 章)。


6.6 よくあるエラー ⚠️

ImportError: WebSearchTool

pip install 'smolagents[toolkit]'

検索が空 / タイムアウト

→ ネットワーク、DuckDuckGo のレート制限。クエリを短くする。

エージェントが検索せず hallucination

→ タスクに「必ず WebSearchTool を使え」と明記。

agent.run("必ず WebSearchTool で検索してから答えて: ...")

6.7 本章のまとめ

霊夢: 公式の道具箱、使えるようになったわ。

  1. add_base_tools=True で標準セットを追加できる
  2. 本書の CodeAgent 例は WebSearchTool() を明示
  3. PythonInterpreterTool は主に ToolCallingAgent 用
  4. 手動 search_tool(query) で切り分けデバッグ
  5. 検索ツールは コストと Step が増えやすい

✅ 章末チェックリスト

  • [ ] add_base_tools の意味を説明できる
  • [ ] WebSearchTool と CodeAgent を組み合わせた
  • [ ] examples/ch06/web_search_agent.py を実行した
  • [ ] examples/ch06/manual_search_tool.py で単体テストした
  • [ ] ツール過多のリスクを一言で言える

次章へ

霊夢: 第 1 部の土台、だいぶ固まったわね!

魔理沙: 次は Hub にツールを載せる話だ。ゆっくりしていこうな。


第7章 ツールを共有する — Hugging Face Hub

本章のゴール: 自作ツールを Hub に公開する流れと、load_tool / Tool.from_hub / ToolCollection.from_hub で他人のツールを安全に取り込む手順を理解する。


7.1 なぜ Hub に載せるの?

霊夢: 第 5 章で作ったツール、自分の PC にしかないのよね。チームで使いたいときは?

魔理沙: そのときは Hugging Face Hub に載せる。ツールは Space リポジトリとして公開でき、Gradio UI 付きで試せる。load_tool("username/my-tool") 一行でエージェントに足せるんだ。

霊夢: エージェント本体も Hub に出せるの?

魔理沙: できる(第 20 章)。今章は ツール に絞るぜ。

API 用途
tool.push_to_hub(repo_id) 自作ツールを Space にアップロード
load_tool(repo_id) Hub からツールを復元
Tool.from_hub(repo_id) 同上(クラスメソッド)
ToolCollection.from_hub(slug) コレクション内のツールをまとめて取得

🔗 Tools tutorial — Share your tool


7.2 push できるツールのルール

霊夢: なんでも push できるの?

魔理沙: いいえ。Hub 上で tool.py として再現できる形が必要だ。

  1. import は forward の中 — モジュール先頭の import os は push 時に壊れることがある
  2. __init__ に余計な引数を足さない — インスタンス固有の状態は Hub 共有と相性が悪い
  3. メソッドは自己完結 — グローバル変数に依存しない
# ❌ push 向きでない例
import requests  # トップレベル import

class BadTool(Tool):
    def __init__(self, api_key: str):
        self.api_key = api_key  # Hub 共有で追跡しづらい
# ✅ push 向きの例(import は forward 内)
class GoodTool(Tool):
    name = "my_tool"
    description = "..."
    inputs = {"q": {"type": "string", "description": "query"}}
    output_type = "string"

    def forward(self, q: str) -> str:
        import requests

        return requests.get(f"https://httpbin.org/get?q={q}").status_code

📦 本書の実装: examples/ch07/custom_downloads_tool.py

# examples/ch07/custom_downloads_tool.py(リポジトリ同梱・全文)
"""第7章: Hub に push する自作ツール(Tool サブクラス)"""
from smolagents import Tool

class HubDownloadsTool(Tool):
    name = "model_download_counter"
    description = (
        "Returns the Hugging Face Hub model id with the most downloads "
        "for a given task category (e.g. text-classification)."
    )
    inputs = {
        "task": {
            "type": "string",
            "description": "Task category on the Hub, such as text-classification",
        }
    }
    output_type = "string"

    def forward(self, task: str) -> str:
        from huggingface_hub import list_models

        model = next(iter(list_models(filter=task, sort="downloads", direction=-1)))
        return model.id

def main() -> None:
    tool = HubDownloadsTool()
    print(tool.forward("text-classification"))

if __name__ == "__main__":
    main()

7.3 push_to_hub の流れ

霊夢: 手順、順番に教えて。

魔理沙: こうだ。

flowchart LR
  A[Tool クラスを書く] --> B[Hub で空の Space 作成]
  B --> C[push_to_hub]
  C --> D[Gradio で試す]
  D --> E[load_tool で再利用]
  1. Hugging FaceSDK: Gradio の Space を作成
  2. リポジトリ名を決める(例: yourname/hub-downloads-tool
  3. HF_TOKEN を write 権限付きで設定 ⚠️
  4. Python から push_to_hub
"""push の最小イメージ(トークンは環境変数から)"""
import os
from smolagents import Tool

# ... HubDownloadsTool を定義 ...

tool = HubDownloadsTool()
tool.push_to_hub("yourname/hub-downloads-tool", token=os.environ["HF_TOKEN"])

📦 任意ハンズオン用: examples/ch07/push_to_hub_optional.py

export HF_TOKEN="hf_..."   # 実値を Git にコミットしない
export HUB_TOOL_REPO="yourname/hub-downloads-tool"
python examples/ch07/push_to_hub_optional.py
# examples/ch07/push_to_hub_optional.py(リポジトリ同梱・全文)
"""第7章: 自作ツールを Hub に push(任意・HF_TOKEN 必須)"""
import os
import sys
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parent))
from custom_downloads_tool import HubDownloadsTool

def main() -> None:
    token = os.environ.get("HF_TOKEN")
    if not token:
        print("⚠️  HF_TOKEN が未設定のため push はスキップします。")
        print("    export HF_TOKEN='hf_...'")
        print("    Hub で YOUR_USER/hub-downloads-tool リポジトリを先に作成してください。")
        return

    repo_id = os.environ.get("HUB_TOOL_REPO", "YOUR_USER/hub-downloads-tool")
    if "YOUR_USER" in repo_id:
        print("⚠️  HUB_TOOL_REPO を自分の Hugging Face ユーザー名に置き換えてください。")
        print("    export HUB_TOOL_REPO='yourname/hub-downloads-tool'")
        return

    tool = HubDownloadsTool()
    tool.push_to_hub(repo_id, token=token)
    print(f"✅ pushed: https://huggingface.co/spaces/{repo_id}")

if __name__ == "__main__":
    main()

霊夢: トークンないと?

魔理沙: スクリプトは スキップしてメッセージだけ出す ようにしてある。load のハンズオンは公開済みツールで試せるぜ。


7.4 trust_remote_code とは ⚠️

霊夢: load_tool の例で trust_remote_code=True って出てきたわ。怖いの?

魔理沙: Hub から取得した Python をそのまま実行する から、悪意あるリポジトリならマシン上で何でもされうる。だから:

  • 信頼できる作者・組織のツールだけ 使う
  • 中身は Space の tool.py必ず目視 する
  • 本番ではサンドボックス(第 12 章)を検討
from smolagents import load_tool

# 明示的に信頼を宣言しないと load できない
tool = load_tool("m-ric/hf-model-downloads", trust_remote_code=True)
選択 意味
trust_remote_code=False(既定) 未検証コードは実行しない
trust_remote_code=True 「この repo のコードを実行していい」とユーザーが同意

霊夢: MCP でも同じ話?

魔理沙: 第 8 章で ToolCollection.from_mcp(..., trust_remote_code=True) も出てくる。思想は同じ 信頼境界 だ。


7.5 load_toolTool.from_hub

魔理沙: どちらも同じツールを返す。好みで選べ。

from smolagents import load_tool, Tool

t1 = load_tool("m-ric/hf-model-downloads", trust_remote_code=True)
t2 = Tool.from_hub("m-ric/hf-model-downloads", trust_remote_code=True)

エージェントに載せる例:

from smolagents import CodeAgent, InferenceClientModel, load_tool

hub_tool = load_tool("m-ric/hf-model-downloads", trust_remote_code=True)
model = InferenceClientModel()
agent = CodeAgent(tools=[hub_tool], model=model)

print(agent.run("depth-estimation で最も DL 数の多いモデル id は?"))

Space には Gradio UI も付く。ブラウザで入出力を試してからエージェントに渡すと安全だ。

🔗 例: m-ric/hf-model-downloads


7.6 ToolCollection.from_hub

霊夢: ツールがたくさんあるコレクションは?

魔理沙: Tool Collection として Hub にまとめられているものを、slug で一括ロードできる。

from smolagents import ToolCollection, CodeAgent, InferenceClientModel
import os

collection = ToolCollection.from_hub(
    collection_slug="huggingface-tools/diffusion-tools-6630bb19a942c2306a2cdb6f",
    token=os.environ["HF_TOKEN"],
)

agent = CodeAgent(
    tools=[*collection.tools],
    model=InferenceClientModel(),
    add_base_tools=False,
)

霊夢: 全部いっぺんにエージェントに?

魔理沙: できるが、遅延ロード され、エージェントが呼んだツールだけ初期化される。それでもツール過多は LLM を混乱させる(第 6 章)ので注意だ。


🖥️ ハンズオン 7-1 — 自作ツールを Hub に push(任意)

霊夢: 自分のツール、載せてみたい!

魔理沙: トークンと空 Space があればいける。無ければスキップで OK だ。

準備

pip install 'smolagents[toolkit]' huggingface_hub
export HF_TOKEN="hf_..."   # write 権限

ツール定義の確認

python examples/ch07/custom_downloads_tool.py
# examples/ch07/custom_downloads_tool.py(リポジトリ同梱・全文)
"""第7章: Hub に push する自作ツール(Tool サブクラス)"""
from smolagents import Tool

class HubDownloadsTool(Tool):
    name = "model_download_counter"
    description = (
        "Returns the Hugging Face Hub model id with the most downloads "
        "for a given task category (e.g. text-classification)."
    )
    inputs = {
        "task": {
            "type": "string",
            "description": "Task category on the Hub, such as text-classification",
        }
    }
    output_type = "string"

    def forward(self, task: str) -> str:
        from huggingface_hub import list_models

        model = next(iter(list_models(filter=task, sort="downloads", direction=-1)))
        return model.id

def main() -> None:
    tool = HubDownloadsTool()
    print(tool.forward("text-classification"))

if __name__ == "__main__":
    main()
(text-classification で DL 数トップの model id が表示される)

push(任意)

export HUB_TOOL_REPO="yourname/hub-downloads-tool"
python examples/ch07/push_to_hub_optional.py
# examples/ch07/push_to_hub_optional.py(リポジトリ同梱・全文)
"""第7章: 自作ツールを Hub に push(任意・HF_TOKEN 必須)"""
import os
import sys
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parent))
from custom_downloads_tool import HubDownloadsTool

def main() -> None:
    token = os.environ.get("HF_TOKEN")
    if not token:
        print("⚠️  HF_TOKEN が未設定のため push はスキップします。")
        print("    export HF_TOKEN='hf_...'")
        print("    Hub で YOUR_USER/hub-downloads-tool リポジトリを先に作成してください。")
        return

    repo_id = os.environ.get("HUB_TOOL_REPO", "YOUR_USER/hub-downloads-tool")
    if "YOUR_USER" in repo_id:
        print("⚠️  HUB_TOOL_REPO を自分の Hugging Face ユーザー名に置き換えてください。")
        print("    export HUB_TOOL_REPO='yourname/hub-downloads-tool'")
        return

    tool = HubDownloadsTool()
    tool.push_to_hub(repo_id, token=token)
    print(f"✅ pushed: https://huggingface.co/spaces/{repo_id}")

if __name__ == "__main__":
    main()

霊夢: YOUR_USER のままだと止まったわ。

魔理沙: 意図的だ。Hub の Space は 自分の Hugging Face ユーザー名 で作る。誤 push を防ぐため HUB_TOOL_REPO を差し替えろ。本書の Git リポジトリだけが hiromichinomata/yukkuri-smolagents だぜ。


🖥️ ハンズオン 7-2 — 公開ツールを load_tool で使う

霊夢: push しなくても、他人のツールは試せるのね。

魔理沙: こちらが本命だ。📦 examples/ch07/load_hub_tool.py

python examples/ch07/load_hub_tool.py
# examples/ch07/load_hub_tool.py(リポジトリ同梱・全文)
"""第7章: Hub 上の他人ツールを load_tool で読み込む"""
import os

from smolagents import CodeAgent, InferenceClientModel, load_tool

def main() -> None:
    # 公式チュートリアルで公開されているデモ Space
    repo_id = "m-ric/hf-model-downloads"

    print(f"Loading tool from {repo_id} (trust_remote_code=True) ...")
    hub_tool = load_tool(repo_id, trust_remote_code=True)

    if not os.environ.get("HF_TOKEN"):
        print("⚠️  エージェント実行には HF_TOKEN が必要です。load のみ確認して終了します。")
        print(f"    tool name: {hub_tool.name}")
        return

    model = InferenceClientModel()
    agent = CodeAgent(tools=[hub_tool], model=model)

    task = (
        "text-classification タスクで Hub 上もっともダウンロードされている "
        "モデル id を教えて。答えはモデル id だけ。"
    )
    result = agent.run(task)
    print("=== 最終回答 ===")
    print(result)

if __name__ == "__main__":
    main()

HF_TOKEN が無い場合は load だけ して終了する。

Loading tool from m-ric/hf-model-downloads (trust_remote_code=True) ...
⚠️  エージェント実行には HF_TOKEN が必要です。load のみ確認して終了します。
    tool name: model_download_counter

トークンありなら CodeAgent が Hub ツールを呼ぶ。

━━━━━━━━━━━━━━━━━━━━ Step 0 ━━━━━━━━━━━━━━━
...
final_answer("bert-base-uncased")   # 例・時期により異なる
ログ 意味
model_download_counter(...) Hub ツールの forward 実行
final_answer タスク完了

コレクション(任意)

export HF_TOKEN="hf_..."
python examples/ch07/tool_collection_from_hub.py
# エージェントまで: RUN_AGENT=1 python examples/ch07/tool_collection_from_hub.py
# examples/ch07/tool_collection_from_hub.py(リポジトリ同梱・全文)
"""第7章: ToolCollection.from_hub でコレクション一括読み込み(任意)"""
import os

from smolagents import CodeAgent, InferenceClientModel, ToolCollection

def main() -> None:
    token = os.environ.get("HF_TOKEN")
    if not token:
        print("⚠️  HF_TOKEN 未設定。from_hub はトークンが必要なことが多いです。")
        return

    # 公式ドキュメントの例(slug は Hub 上の Tool Collection)
    collection_slug = "huggingface-tools/diffusion-tools-6630bb19a942c2306a2cdb6f"

    print(f"Loading collection: {collection_slug}")
    collection = ToolCollection.from_hub(collection_slug=collection_slug, token=token)
    print(f"Loaded {len(collection.tools)} tool(s): {[t.name for t in collection.tools]}")

    if os.environ.get("RUN_AGENT") != "1":
        print("エージェントまで動かす場合: RUN_AGENT=1 python ... (画像生成で課金の可能性)")
        return

    model = InferenceClientModel()
    agent = CodeAgent(tools=[*collection.tools], model=model, add_base_tools=False)
    result = agent.run("Draw a simple icon of a book.")
    print(result)

if __name__ == "__main__":
    main()

7.7 よくあるエラー ⚠️

霊夢: RepositoryNotFound って……

魔理沙: よくあるのはこのへん。

401 / 403 on push

Unauthorized ...

対処: HF_TOKENwrite 権限があるか、repo 名が Hub 上の Space と一致するか確認。

trust_remote_code 未指定

Loading ... requires you to execute remote code ...

対処: trust_remote_code=True を付ける。中身を読んだうえで

push 時の import エラー

Error when saving tool: imports should be inside forward

対処: トップレベル import を forward 内へ移動。


7.8 本章のまとめ

霊夢: 整理するわ。

  1. push_to_hub — 自作 Tool を Space として共有(import 規則あり)
  2. load_tool / from_hub — 他人ツールを再利用、trust_remote_code=True は同意
  3. ToolCollection.from_hub — コレクション slug で複数ツール
  4. セキュリティ — 信頼できない repo は載せない・読まない

魔理沙: 次章は LangChain・Gradio Space・MCP で既存エコシステムとつなぐぜ。


✅ 章末チェックリスト

  • [ ] HubDownloadsTool をローカルで forward 実行した
  • [ ] (任意)push_to_hub で自分の Space に上げた
  • [ ] load_tool(..., trust_remote_code=True) の意味を説明できる
  • [ ] m-ric/hf-model-downloads をエージェントに載せた(または load のみ確認)
  • [ ] ToolCollection.from_hub の用途を理解した
  • [ ] 秘匿トークンを Git にコミットしていない

次章へ

霊夢: Hub、便利だけど信頼が大事ね。

魔理沙: その通り。次は MCP で社内 API ともつなぐぜ。ゆっくりしていこうな。


第8章 既存エコシステムとつなぐ — LangChain・Space・MCP

本章のゴール: LangChain ツール・Gradio Space・MCP サーバーを smolagents に橋渡しし、信頼境界と structured_output を理解する。


8.1 わざわざ作らなくていい?

霊夢: 第 5 章で @tool を書いたばかりなのに、また別の作り方?

魔理沙: 車輪の再発明を避ける 章だ。すでに LangChain 用ツールや Hub の Gradio Space、社内の MCP サーバーがあるなら、ラッパーで載せるだけでいい。

経路 典型用途
Tool.from_langchain() 既存 LC ツールの再利用
Tool.from_space() Hub 上の画像生成・ASR Space
MCPClient / ToolCollection.from_mcp() 標準化された外部ツールサーバー

🔗 Tools tutorial


8.2 Tool.from_langchain

霊夢: LangChain 入れてないプロジェクトでも使える?

魔理沙: 使うときだけ langchain-community などを入れる。ラッパーは LC ツールの名前・説明・実行 を smolagents の Tool API に写す。

pip install 'smolagents[toolkit]' langchain-community
from langchain_community.tools import DuckDuckGoSearchRun
from smolagents import Tool, CodeAgent, InferenceClientModel

lc_tool = DuckDuckGoSearchRun()
search_tool = Tool.from_langchain(lc_tool)

agent = CodeAgent(tools=[search_tool], model=InferenceClientModel())
agent.run("Attention is All You Need の著者数は?")

📦 examples/ch08/langchain_search_tool.py

# examples/ch08/langchain_search_tool.py(リポジトリ同梱・全文)
"""第8章: LangChain ツールを Tool.from_langchain で再利用"""
import os

from smolagents import CodeAgent, InferenceClientModel, Tool

def main() -> None:
    try:
        from langchain_community.tools import DuckDuckGoSearchRun
    except ImportError:
        print("⚠️  pip install 'langchain-community' が必要です。")
        return

    lc_tool = DuckDuckGoSearchRun()
    search_tool = Tool.from_langchain(lc_tool)

    if not os.environ.get("HF_TOKEN"):
        print("⚠️  HF_TOKEN 未設定。ツールの手動呼び出しのみ。")
        print(search_tool("smolagents huggingface"))
        return

    model = InferenceClientModel()
    agent = CodeAgent(tools=[search_tool], model=model)
    result = agent.run(
        "smolagents とは何か、1 文で説明して。出典 URL があれば末尾に。"
    )
    print("=== 最終回答 ===")
    print(result)

if __name__ == "__main__":
    main()

霊夢: SerpAPI みたいな有料キーは?

魔理沙: LangChain 側の設定のままだ。キーは .env で、Git には載せない ⚠️


8.3 Tool.from_space — Gradio Space をツール化

魔理沙: Space ID を渡すと、裏で gradio-client が API を叩く。

from smolagents import Tool

image_tool = Tool.from_space(
    "black-forest-labs/FLUX.1-schnell",
    name="image_generator",
    description="Generate an image from a prompt",
)

# 単体テスト
# result = image_tool("A sunny beach")

エージェントに additional_args でコンテキストを渡す例(公式パターン):

from smolagents import CodeAgent, InferenceClientModel

agent = CodeAgent(tools=[image_tool], model=InferenceClientModel())
agent.run(
    "Improve this prompt, then generate an image.",
    additional_args={"user_prompt": "A rabbit wearing a space suit"},
)

📦 スケッチ: examples/ch08/from_space_sketch.pyRUN_SPACE=1 で実行)

# examples/ch08/from_space_sketch.py(リポジトリ同梱・全文)
"""第8章: Gradio Space を Tool.from_space で使う(スケッチ・任意実行)"""
import os

from smolagents import CodeAgent, InferenceClientModel, Tool

def main() -> None:
    # 画像生成 Space — 実行は GPU/課金の可能性あり
    space_id = "black-forest-labs/FLUX.1-schnell"

    image_tool = Tool.from_space(
        space_id,
        name="image_generator",
        description="Generate an image from a text prompt",
    )

    print(f"Tool ready: {image_tool.name}")
    print("Space 直呼び出しは RUN_SPACE=1 のときのみ実行します。")

    if os.environ.get("RUN_SPACE") != "1":
        return

    if not os.environ.get("HF_TOKEN"):
        print("⚠️  HF_TOKEN が必要です。")
        return

    model = InferenceClientModel()
    agent = CodeAgent(tools=[image_tool], model=model)
    result = agent.run(
        "Improve the prompt then generate an image.",
        additional_args={"user_prompt": "A rabbit in a space suit on the moon"},
    )
    print(result)

if __name__ == "__main__":
    main()

霊夢: GPU と課金が怖いわ……

魔理沙: デフォルトは ツール定義だけ確認 で止めるようにしてある。


8.4 MCP とは

霊夢: MCP、略語ばっかり……

魔理沙: Model Context Protocol。ツールを サーバー として提供し、エージェントはクライアントで接続する。stdio(子プロセス)か Streamable HTTP が多い。

flowchart LR
  Agent[CodeAgent] --> MCPClient
  MCPClient -->|stdio or HTTP| Server[MCP Server]
  Server --> API[DB / API / ファイル]

smolagents では主に:

  • MCPClient — 接続を管理し、ツール一覧をエージェントに渡す
  • ToolCollection.from_mcp — 同様にコレクションとして取得

8.5 MCPClient — stdio と HTTP

stdio(ローカルプロセス)

from mcp import StdioServerParameters
from smolagents import MCPClient, CodeAgent, InferenceClientModel
import os

server_parameters = StdioServerParameters(
    command="uvx",
    args=["--quiet", "pubmedmcp@0.1.3"],
    env={"UV_PYTHON": "3.12", **os.environ},
)

with MCPClient(server_parameters) as tools:
    agent = CodeAgent(tools=tools, model=InferenceClientModel(), add_base_tools=True)
    agent.run("COVID-19 治療の最近の研究を要約して")

Streamable HTTP

from smolagents import MCPClient, CodeAgent, InferenceClientModel

with MCPClient({"url": "http://127.0.0.1:8000/mcp", "transport": "streamable-http"}) as tools:
    agent = CodeAgent(tools=tools, model=InferenceClientModel())
    agent.run("二日酔いの対処法は?")

手動で接続を閉じる

mcp_client = MCPClient(server_parameters)
try:
    tools = mcp_client.get_tools()
    agent = CodeAgent(tools=tools, model=model)
    print(agent.run("..."))
finally:
    mcp_client.disconnect()

8.6 複数 MCP サーバー

魔理沙: リストを渡せば 複数サーバーのツールをマージ する。

from smolagents import MCPClient

server_params1 = StdioServerParameters(command="uvx", args=["--quiet", "pubmedmcp@0.1.3"])
server_params2 = {"url": "http://127.0.0.1:8000/mcp", "transport": "streamable-http"}

with MCPClient([server_params1, server_params2]) as tools:
    agent = CodeAgent(tools=tools, model=model)
    agent.run("論文を調べ、頭痛の対処も教えて")

📦 スケッチ: examples/ch08/mcp_multi_sketch.py

# examples/ch08/mcp_multi_sketch.py(リポジトリ同梱・全文)
"""第8章: 複数 MCP サーバー接続のスケッチ(コメント中心)"""
import os

from mcp import StdioServerParameters
from smolagents import MCPClient

# 例: stdio MCP + Streamable HTTP MCP を同時に
# server_params1 = StdioServerParameters(command="uvx", args=["--quiet", "pubmedmcp@0.1.3"], ...)
# server_params2 = {"url": "http://127.0.0.1:8000/mcp", "transport": "streamable-http"}
#
# with MCPClient([server_params1, server_params2], structured_output=True) as tools:
#     agent = CodeAgent(tools=tools, model=model)
#     agent.run("...")

def main() -> None:
    print("複数 MCP は MCPClient([param1, param2]) で接続します。")
    print("本番では各サーバーの信頼性を個別に検証してください ⚠️")
    if not os.environ.get("HF_TOKEN"):
        print("(エージェント実行には HF_TOKEN が必要)")

if __name__ == "__main__":
    main()

霊夢: ツール名が被ったら?

魔理沙: 衝突に注意。本番ではプレフィックス付きサーバー設計が安全だ。


8.7 structured_output=True

霊夢: JSON スキーマって、MCP の新機能?

魔理沙: ツールが 構造化データ を返すとき、LLM がスキーマをシステムプロンプトで見られる。structured_output=True で有効化する(将来デフォルト True 予定のため、明示指定を推奨)。

with MCPClient(server_parameters, structured_output=True) as tools:
    agent = CodeAgent(tools=tools, model=model)
    agent.run("東京の気温を華氏でも教えて")

ローカルデモサーバー(Pydantic モデル付き):

📦 サーバー: examples/ch08/mcp_weather_server.py
📦 クライアント: examples/ch08/mcp_weather_agent.py

pip install 'smolagents[toolkit]' mcp
python examples/ch08/mcp_weather_agent.py
# examples/ch08/mcp_weather_agent.py(リポジトリ同梱・全文)
"""第8章: MCPClient でローカル天気 MCP に接続"""
import os

from mcp import StdioServerParameters
from smolagents import CodeAgent, InferenceClientModel, MCPClient

def main() -> None:
    if not os.environ.get("HF_TOKEN"):
        print("⚠️  HF_TOKEN が未設定です。エージェントは Inference API を使います。")

    server_parameters = StdioServerParameters(
        command="python",
        args=["examples/ch08/mcp_weather_server.py"],
    )

    model = InferenceClientModel()

    with MCPClient(server_parameters, structured_output=True) as tools:
        print(f"MCP tools: {[t.name for t in tools]}")
        agent = CodeAgent(tools=tools, model=model)
        result = agent.run(
            "東京の気温を摂氏で教えて。湿度も一言で。"
        )
        print("=== 最終回答 ===")
        print(result)

if __name__ == "__main__":
    main()
# examples/ch08/mcp_weather_server.py(リポジトリ同梱・全文)
"""第8章: 構造化出力付きのローカル MCP 天気デモサーバー"""
from mcp.server.fastmcp import FastMCP
from pydantic import BaseModel, Field

mcp = FastMCP("Weather Service")

class WeatherInfo(BaseModel):
    location: str = Field(description="Location name")
    temperature: float = Field(description="Temperature in Celsius")
    conditions: str = Field(description="Weather conditions")
    humidity: int = Field(description="Humidity percentage", ge=0, le=100)

@mcp.tool(
    name="get_weather_info",
    description="Get weather information for a location as structured data.",
)
def get_weather_info(city: str) -> WeatherInfo:
    """Demo weather — not real API data."""
    return WeatherInfo(
        location=city,
        temperature=22.5,
        conditions="partly cloudy",
        humidity=65,
    )

if __name__ == "__main__":
    mcp.run()

8.8 セキュリティ — MCP の信頼境界 ⚠️

霊夢: Hub の trust_remote_code と同じ?

魔理沙: 同族だ。

種類 リスク
stdio MCP サーバー起動 = ローカルでコード実行
HTTP MCP リモートだが、悪意ある指示でデータ流出の可能性
複数サーバー 攻撃面が増える

チェックリスト(本番):

  • [ ] サーバー作者・ソースを検証した
  • [ ] 最小権限の API キーだけ渡した
  • [ ] 社外サーバーに社内シークレットを渡していない
  • [ ] 可能なら第 12 章のサンドボックスと併用

🖥️ ハンズオン 8-1 — LangChain 検索ツール

霊夢: 手を動かすわ。

pip install langchain-community
export HF_TOKEN="hf_..."
python examples/ch08/langchain_search_tool.py
# examples/ch08/langchain_search_tool.py(リポジトリ同梱・全文)
"""第8章: LangChain ツールを Tool.from_langchain で再利用"""
import os

from smolagents import CodeAgent, InferenceClientModel, Tool

def main() -> None:
    try:
        from langchain_community.tools import DuckDuckGoSearchRun
    except ImportError:
        print("⚠️  pip install 'langchain-community' が必要です。")
        return

    lc_tool = DuckDuckGoSearchRun()
    search_tool = Tool.from_langchain(lc_tool)

    if not os.environ.get("HF_TOKEN"):
        print("⚠️  HF_TOKEN 未設定。ツールの手動呼び出しのみ。")
        print(search_tool("smolagents huggingface"))
        return

    model = InferenceClientModel()
    agent = CodeAgent(tools=[search_tool], model=model)
    result = agent.run(
        "smolagents とは何か、1 文で説明して。出典 URL があれば末尾に。"
    )
    print("=== 最終回答 ===")
    print(result)

if __name__ == "__main__":
    main()

トークンなし → DuckDuckGo を ツール単体 で 1 回実行。


🖥️ ハンズオン 8-2 — ローカル MCP 天気デモ

魔理沙: 別ターミナル不要。MCPClient が子プロセスでサーバーを起動する。

pip install mcp
python examples/ch08/mcp_weather_agent.py
# examples/ch08/mcp_weather_agent.py(リポジトリ同梱・全文)
"""第8章: MCPClient でローカル天気 MCP に接続"""
import os

from mcp import StdioServerParameters
from smolagents import CodeAgent, InferenceClientModel, MCPClient

def main() -> None:
    if not os.environ.get("HF_TOKEN"):
        print("⚠️  HF_TOKEN が未設定です。エージェントは Inference API を使います。")

    server_parameters = StdioServerParameters(
        command="python",
        args=["examples/ch08/mcp_weather_server.py"],
    )

    model = InferenceClientModel()

    with MCPClient(server_parameters, structured_output=True) as tools:
        print(f"MCP tools: {[t.name for t in tools]}")
        agent = CodeAgent(tools=tools, model=model)
        result = agent.run(
            "東京の気温を摂氏で教えて。湿度も一言で。"
        )
        print("=== 最終回答 ===")
        print(result)

if __name__ == "__main__":
    main()
MCP tools: ['get_weather_info']
...
(最終回答に気温・湿度)
ポイント 説明
structured_output=True WeatherInfo スキーマを LLM が参照
デモデータ 実 API ではなく固定値

8.9 ToolCollection.from_mcp

魔理沙: MCPClient と同様の接続だが、with ToolCollection.from_mcp(...) as col:col.tools を展開するスタイル。

from smolagents import ToolCollection, CodeAgent

with ToolCollection.from_mcp(server_parameters, trust_remote_code=True, structured_output=True) as col:
    agent = CodeAgent(tools=[*col.tools], model=model, add_base_tools=True)
    agent.run("...")

8.10 よくあるエラー ⚠️

ModuleNotFoundError: mcp

pip install mcp

MCP サーバー起動失敗

Connection closed / Failed to initialize MCP session

対処: command / args のパス、uvx が入っているか、ファイアウォール(HTTP 時)を確認。

LangChain ツールの import エラー

pip install langchain-community

8.11 本章のまとめ

霊夢: まとめるわ。

  1. from_langchain — 既存ツールをラップ
  2. from_space — Hub Gradio Space を 1 ツール化
  3. MCPClient — stdio / HTTP、複数サーバー可
  4. structured_output — 構造化ツール出力を LLM が理解しやすく
  5. 信頼 — stdio = ローカル実行と同等の危険度

✅ 章末チェックリスト

  • [ ] Tool.from_langchain で LC ツールを載せた(または import 確認)
  • [ ] Tool.from_space の引数(space_id, name, description)を説明できる
  • [ ] ローカル MCP 天気デモを実行した
  • [ ] structured_output=True の意味を理解した
  • [ ] MCP 利用時のセキュリティチェックリストを読んだ

次章へ

霊夢: エコシステム、広いわね……

魔理沙: 次はいよいよ CodeAgent の芯 を解剖するぜ。


第9章 CodeAgent — コードで動くエージェント

本章のゴール: CodeAgent の実行モデルを理解し、additional_authorized_importsfinal_answer_checksadditional_args を使ったハンズオンを完了する。


9.1 JSON じゃなくて Python?

霊夢: 第 0 章から CodeAgent ばかり見てきたけど、なんでコードなの?

魔理沙: ツール呼び出しを Python スニペット として書けるから、ループ・分岐・中間変数・複数ツールの合成が楽なんだ。研究ベンチマークでもコードエージェントが有利なことが多い。

# CodeAgent が書きやすいパターン
docs = search("smolagents secure execution")
summary = summarize(docs[0])
final_answer(summary)

霊夢: ToolCallingAgent は?

魔理沙: JSON で 1 ツールずつ。第 10 章で並べて比べるぜ。

🔗 Guided tour — CodeAgent


9.2 エージェントループの中身

魔理沙: 1 ステップはざっくり次の流れだ。

flowchart TD
  T[タスク + システムプロンプト] --> LLM[LLM: Python を生成]
  LLM --> EX[LocalPythonExecutor 等で実行]
  EX --> OBS[stdout / エラー / ツール結果]
  OBS --> LLM
  LLM --> FA{final_answer?}
  FA -->|Yes| Done[終了]
  FA -->|No| LLM
関数・概念 役割
生成コード ツール呼び出しや計算
print(...) 次ステップ用の観察ログ
final_answer(x) 正常終了の宣言
max_steps 無限ループ防止

9.3 LocalPythonExecutor の概要

霊夢: 普通の python じゃないの?

魔理沙: デフォルトは AST を解釈する専用エグゼキュータ。許可されていない import や危険な属性アクセスは弾かれる。詳細は第 12 章。

# エージェント内部イメージ(利用者は通常意識しない)
# from smolagents.local_python_executor import LocalPythonExecutor
# executor = LocalPythonExecutor(additional_authorized_imports=[...])

リモート実行に切り替えるときは executor_type="e2b" など(第 12 章)。


9.4 additional_authorized_imports

霊夢: Web のタイトル取りたいだけなのに、import requests が弾かれたわ……

魔理沙: 既定では 安全な標準ライブラリだけ。外のパッケージは 明示許可リスト に足す。

from smolagents import CodeAgent, InferenceClientModel

agent = CodeAgent(
    tools=[],
    model=InferenceClientModel(),
    additional_authorized_imports=["requests", "bs4"],
)

agent.run("https://huggingface.co/blog の <title> を取得して")

サブモジュール制限

魔理沙: numpy だけ許可しても numpy.random は別途 必要。またはワイルドカード:

additional_authorized_imports=["numpy", "numpy.random"]
# または
additional_authorized_imports=["numpy.*"]

⚠️ 危険な import を足すな — LLM が生成したコードがそのまま動く。

pip install requests beautifulsoup4

📦 examples/ch09/web_title_agent.py

# examples/ch09/web_title_agent.py(リポジトリ同梱・全文)
"""第9章: additional_authorized_imports で Web ページタイトル取得"""
from smolagents import CodeAgent, InferenceClientModel

def main() -> None:
    model = InferenceClientModel()
    agent = CodeAgent(
        tools=[],
        model=model,
        additional_authorized_imports=["requests", "bs4"],
    )

    url = "https://huggingface.co/docs/smolagents"
    task = (
        f"URL '{url}' の HTML から <title> テキストだけを取得して返して。"
        "requests と BeautifulSoup (bs4) を使ってよい。"
    )
    result = agent.run(task)
    print("=== 最終回答 ===")
    print(result)

if __name__ == "__main__":
    main()

9.5 final_answerfinal_answer_checks

霊夢: final_answer(42) って出たけど、文字列 "42" だったときは?

魔理沙: 検証関数で 弾いて続行 できる。

def is_integer(final_answer, agent_memory=None) -> bool:
    try:
        int(str(final_answer).strip())
        return True
    except ValueError:
        return False

agent = CodeAgent(
    tools=[],
    model=InferenceClientModel(),
    final_answer_checks=[is_integer],
)
戻り値 動作
True その final_answer で終了
False エラーをログに残し、エージェント継続

用途: 数値問題・JSON 形式・社内 ID 形式など。

📦 examples/ch09/final_answer_checks.py

# examples/ch09/final_answer_checks.py(リポジトリ同梱・全文)
"""第9章: final_answer_checks で整数回答を強制"""
from smolagents import CodeAgent, InferenceClientModel

def is_integer(final_answer, agent_memory=None) -> bool:
    try:
        int(str(final_answer).strip())
        return True
    except ValueError:
        return False

def main() -> None:
    model = InferenceClientModel()
    agent = CodeAgent(
        tools=[],
        model=model,
        final_answer_checks=[is_integer],
        max_steps=8,
    )

    result = agent.run("3 と 7 の最小公倍数を計算し、答えは整数だけ返して")
    print("=== 最終回答 ===")
    print(result)

if __name__ == "__main__":
    main()

9.6 additional_args

霊夢: タスク文に URL を書くの、長くて嫌なのよね。

魔理沙: agent.run(task, additional_args={...}) で、実行時の Python から見える変数を渡せる。画像 URL・DataFrame・設定 dict など。

agent.run(
    "user_prompt を改善してから image_generator を呼んで",
    additional_args={"user_prompt": "月面のうさぎ"},
)

本書のシンプル例:

agent.run(
    "base_url と path を連結した完全 URL を返して",
    additional_args={
        "base_url": "https://huggingface.co",
        "path": "/blog/smolagents",
    },
)

📦 examples/ch09/additional_args_demo.py

# examples/ch09/additional_args_demo.py(リポジトリ同梱・全文)
"""第9章: additional_args でタスク外の変数をエージェントに渡す"""
from smolagents import CodeAgent, InferenceClientModel

def main() -> None:
    model = InferenceClientModel()
    agent = CodeAgent(tools=[], model=model)

    base_url = "https://huggingface.co"
    path = "/blog/smolagents"

    result = agent.run(
        "base_url と path を連結した URL のパス部分(ドメイン除く)だけを返して。",
        additional_args={
            "base_url": base_url,
            "path": path,
        },
    )
    print("=== 最終回答 ===")
    print(result)

if __name__ == "__main__":
    main()

霊夢: プロンプトに埋め込まれるの?

魔理沙: state にマージされ、システム側から 利用可能な変数 として告知される。機密情報を渡すときはログ漏れに注意 ⚠️


🖥️ ハンズオン 9-1 — Web ページタイトル取得

霊夢: requests と BeautifulSoup、動かしてみる!

pip install requests beautifulsoup4
export HF_TOKEN="hf_..."
python examples/ch09/web_title_agent.py
# examples/ch09/web_title_agent.py(リポジトリ同梱・全文)
"""第9章: additional_authorized_imports で Web ページタイトル取得"""
from smolagents import CodeAgent, InferenceClientModel

def main() -> None:
    model = InferenceClientModel()
    agent = CodeAgent(
        tools=[],
        model=model,
        additional_authorized_imports=["requests", "bs4"],
    )

    url = "https://huggingface.co/docs/smolagents"
    task = (
        f"URL '{url}' の HTML から <title> テキストだけを取得して返して。"
        "requests と BeautifulSoup (bs4) を使ってよい。"
    )
    result = agent.run(task)
    print("=== 最終回答 ===")
    print(result)

if __name__ == "__main__":
    main()
━━━━━━━━━━━━━━━━━━━━ Step 0 ━━━━━━━━━━━━━━━
╭─ Executing this code: ─────────────────╮
│ import requests                        │
│ from bs4 import BeautifulSoup          │
│ ...                                    │
│ final_answer("...")                    │
╰────────────────────────────────────────╯
エラー 対処
Import of requests is not allowed additional_authorized_imports"requests" を追加
bs4 not found pip install beautifulsoup4

🖥️ ハンズオン 9-2 — final_answer_checks

python examples/ch09/final_answer_checks.py
# examples/ch09/final_answer_checks.py(リポジトリ同梱・全文)
"""第9章: final_answer_checks で整数回答を強制"""
from smolagents import CodeAgent, InferenceClientModel

def is_integer(final_answer, agent_memory=None) -> bool:
    try:
        int(str(final_answer).strip())
        return True
    except ValueError:
        return False

def main() -> None:
    model = InferenceClientModel()
    agent = CodeAgent(
        tools=[],
        model=model,
        final_answer_checks=[is_integer],
        max_steps=8,
    )

    result = agent.run("3 と 7 の最小公倍数を計算し、答えは整数だけ返して")
    print("=== 最終回答 ===")
    print(result)

if __name__ == "__main__":
    main()

最初に final_answer("21") のように文字列で返すと、チェックが失敗して 追加ステップ が走ることがある。

Final answer check failed: is_integer returned False
...
final_answer(21)

霊夢: 自分で検証してくれるの、地味に便利ね。


9.7 CodeAgent の強み(再掲)

# 複数ツール + 制御フロー(概念例)
results = []
for q in ["smolagents", "CodeAgent"]:
    results.append(web_search(q))
final_answer(max(results, key=len))
強み 弱み
表現力・合成 構文エラー・予測しづらさ
動的ロジック 実行環境のセキュリティ設計が必須

9.8 よくあるエラー ⚠️

InterpreterError: Import of X is not allowed

additional_authorized_imports=["X"]

Forbidden access to module: os(サブモジュール経由)

random._os など — 許可パッケージでも内部経路はブロックされる(第 12 章)。

final_answer しないまま max_steps

タスクを具体化するか max_steps を増やす。第 13 章でデバッグ設定も学ぶ。


9.9 本章のまとめ

霊夢: チェックリスト用に言うわ。

  1. CodeAgent — Python 生成 → 実行 → final_answer
  2. additional_authorized_imports — 外部パッケージの許可
  3. final_answer_checks — 回答形式の検証
  4. additional_args — タスク外コンテキストの注入
  5. 実行の実体は LocalPythonExecutor(第 12 章で深掘り)

✅ 章末チェックリスト

  • [ ] CodeAgent ループ(生成 → 実行 → 観察)を説明できる
  • [ ] requests / bs4 でタイトル取得ハンズオンを完了した
  • [ ] final_answer_checks で整数チェックを試した
  • [ ] additional_args を 1 回以上使った
  • [ ] 危険な import を安易に許可していない

次章へ

霊夢: CodeAgent、だいぶ腹落ちしたわ。

魔理沙: 次は 対になる ToolCallingAgent と比べるぜ。ゆっくりしていこうな。


第10章 ToolCallingAgent — 構造化ツール呼び出し

本章のゴール: ToolCallingAgentCodeAgent の違いを表で整理し、同じ Web タイトル取得タスクを両方で実行してログを比較する。


10.1 2 つのエージェント哲学

霊夢: ずっと CodeAgent だったけど、もう一種類あるんでしょ?

魔理沙: ToolCallingAgent だ。行動を JSON 形式のツール呼び出し で表す。OpenAI API の function calling に近い。

flowchart TB
  subgraph CodeAgent
    C1[LLM] --> C2[Python コード]
    C2 --> C3[Executor]
  end
  subgraph ToolCallingAgent
    T1[LLM] --> T2[JSON tool call]
    T2 --> T3[Tool.forward]
  end

10.2 比較表

観点 CodeAgent ToolCallingAgent
行動の表現 Python スニペット JSON(name + arguments)
コード実行 あり(ローカル or サンドボックス) 基本なし(ツール内のみ)
表現力 ループ・合成・変数 ツール定義内に限定
予測可能性 低め(構文・ロジックエラー) 高め(スキーマ検証)
安全性 import 制御が必要 任意コードを書かない
向き用途 計算・パイプライン・複合 API ラッパー・検索・定型操作

霊夢: どっちが「強い」の?

魔理沙: 難しいタスク・合成 は CodeAgent、信頼性・監査 は ToolCallingAgent、と割り切るとよい。

同じタスクの CodeAgent 側(対比用)

from smolagents import CodeAgent, InferenceClientModel

code_agent = CodeAgent(
    tools=[],
    model=InferenceClientModel(),
    additional_authorized_imports=["requests", "bs4"],
)
code_agent.run("https://example.com の <title> だけ返して")

霊夢: 片方は Python、片方は JSON ツール呼び出し、って違いね。


10.3 ToolCallingAgent の最小例

from smolagents import ToolCallingAgent, InferenceClientModel, WebSearchTool

model = InferenceClientModel()
agent = ToolCallingAgent(tools=[WebSearchTool()], model=model)

agent.run("フランスの首都は?")

ログではおおよそ次の形(モデルにより異なる):

{
  "tool_call": {
    "name": "web_search",
    "arguments": { "query": "capital of France" }
  }
}

VisitWebpageTool を手動で試す

魔理沙: エージェント抜きでツールだけ叩くと、ToolCalling の中身が分かりやすい。

from smolagents import VisitWebpageTool

page = VisitWebpageTool()("https://huggingface.co/docs/smolagents")
print(str(page)[:800])

10.4 PythonInterpreterTool との関係

魔理沙: add_base_tools=True のとき、ToolCallingAgent には PythonInterpreterTool が付くことがある。CodeAgent は もともとコード実行できる ので重複を避ける設計だ。

from smolagents import ToolCallingAgent, InferenceClientModel

agent = ToolCallingAgent(
    tools=[],
    model=InferenceClientModel(),
    add_base_tools=True,  # WebSearch + PythonInterpreter 等
)

10.5 いつ ToolCallingAgent を選ぶか

選ぶ 避ける
ツールが独立した API 呼び出し 複雑なデータ変換を毎回ツールに書く
OpenAI 互換エンドポイントに載せたい ループ・条件分岐がタスクの中心
監査ログでツール名を固定したい 1 ステップで大量の Python 合成が必要

10.6 OpenAI 互換 API 向け設定

魔理沙: LiteLLMModel + ToolCallingAgent が定番だ。

# pip install 'smolagents[litellm]'
import os
from smolagents import LiteLLMModel, ToolCallingAgent, VisitWebpageTool

model = LiteLLMModel(
    model_id="gpt-4o-mini",
    api_key=os.environ["OPENAI_API_KEY"],
)
agent = ToolCallingAgent(tools=[VisitWebpageTool()], model=model)
agent.run("https://example.com の title は?")

📦 examples/ch10/tool_calling_litellm_sketch.py

# examples/ch10/tool_calling_litellm_sketch.py(リポジトリ同梱・全文)
"""第10章: OpenAI 互換 API 向け ToolCallingAgent(スケッチ)"""
import os

from smolagents import ToolCallingAgent, VisitWebpageTool

def main() -> None:
    api_key = os.environ.get("OPENAI_API_KEY")
    if not api_key:
        print("⚠️  OPENAI_API_KEY 未設定。以下は設定後の例です。")
        print("""
# pip install 'smolagents[litellm]'
from smolagents import LiteLLMModel, ToolCallingAgent, VisitWebpageTool

model = LiteLLMModel(model_id="gpt-4o-mini", api_key=os.environ["OPENAI_API_KEY"])
agent = ToolCallingAgent(tools=[VisitWebpageTool()], model=model)
print(agent.run("https://example.com の title テキストは?"))
""")
        return

    from smolagents import LiteLLMModel

    model = LiteLLMModel(model_id="gpt-4o-mini")
    agent = ToolCallingAgent(tools=[VisitWebpageTool()], model=model)
    result = agent.run("https://example.com のページタイトルだけ教えて")
    print(result)

if __name__ == "__main__":
    main()

🖥️ ハンズオン 10-1 — Web タイトル取得の比較

霊夢: 同じ質問、両方に投げてみるわ!

魔理沙: 📦 examples/ch10/compare_web_title.py

pip install requests beautifulsoup4
export HF_TOKEN="hf_..."
python examples/ch10/compare_web_title.py
# examples/ch10/compare_web_title.py(リポジトリ同梱・全文)
"""第10章: 同じ URL タイトル取得を CodeAgent と ToolCallingAgent で比較"""
import time

from smolagents import CodeAgent, InferenceClientModel, ToolCallingAgent, VisitWebpageTool

URL = "https://huggingface.co/docs/smolagents"
TASK = f"次の URL のページタイトル(<title>)だけを返して: {URL}"

def run_code_agent(model) -> tuple[str, float]:
    agent = CodeAgent(
        tools=[],
        model=model,
        additional_authorized_imports=["requests", "bs4"],
        verbosity_level=1,
    )
    start = time.perf_counter()
    result = agent.run(TASK)
    return str(result), time.perf_counter() - start

def run_tool_calling_agent(model) -> tuple[str, float]:
    agent = ToolCallingAgent(
        tools=[VisitWebpageTool()],
        model=model,
        verbosity_level=1,
    )
    start = time.perf_counter()
    result = agent.run(TASK)
    return str(result), time.perf_counter() - start

def main() -> None:
    model = InferenceClientModel()

    print("=== CodeAgent ===")
    code_answer, code_sec = run_code_agent(model)
    print(code_answer)
    print(f"({code_sec:.1f}s)")

    print("\n=== ToolCallingAgent ===")
    tc_answer, tc_sec = run_tool_calling_agent(model)
    print(tc_answer)
    print(f"({tc_sec:.1f}s)")

    print("\n--- メモ ---")
    print("CodeAgent: Python 生成 + ローカル実行")
    print("ToolCallingAgent: VisitWebpageTool 等の構造化ツール呼び出し")

if __name__ == "__main__":
    main()

スクリプトの骨格:

from smolagents import CodeAgent, ToolCallingAgent, InferenceClientModel, VisitWebpageTool

model = InferenceClientModel()
url = "https://huggingface.co/docs/smolagents"
task = f"次の URL のページタイトル(<title>)だけを返して: {url}"

code_agent = CodeAgent(
    tools=[],
    model=model,
    additional_authorized_imports=["requests", "bs4"],
)
tool_agent = ToolCallingAgent(tools=[VisitWebpageTool()], model=model)

print(code_agent.run(task))
print(tool_agent.run(task))
エージェント 典型ステップ
CodeAgent requests + BeautifulSoup を生成
ToolCallingAgent visit_webpageVisitWebpageTool)を JSON 呼び出し
=== CodeAgent ===
Agents - Hugging Face smolagents documentation
(12.3s)

=== ToolCallingAgent ===
Agents - Hugging Face smolagents documentation
(8.1s)

(時間・文言はモデルとネットワークで変動)

霊夢: ToolCalling の方がステップ、読みやすかったわ。

魔理沙: ツール名がログにそのまま出るから、本番監査には向くな。


🖥️ ハンズオン 10-2 — OpenAI 互換 ToolCallingAgent

export OPENAI_API_KEY="sk-..."
pip install 'smolagents[litellm]'
python examples/ch10/tool_calling_litellm_sketch.py
# examples/ch10/tool_calling_litellm_sketch.py(リポジトリ同梱・全文)
"""第10章: OpenAI 互換 API 向け ToolCallingAgent(スケッチ)"""
import os

from smolagents import ToolCallingAgent, VisitWebpageTool

def main() -> None:
    api_key = os.environ.get("OPENAI_API_KEY")
    if not api_key:
        print("⚠️  OPENAI_API_KEY 未設定。以下は設定後の例です。")
        print("""
# pip install 'smolagents[litellm]'
from smolagents import LiteLLMModel, ToolCallingAgent, VisitWebpageTool

model = LiteLLMModel(model_id="gpt-4o-mini", api_key=os.environ["OPENAI_API_KEY"])
agent = ToolCallingAgent(tools=[VisitWebpageTool()], model=model)
print(agent.run("https://example.com の title テキストは?"))
""")
        return

    from smolagents import LiteLLMModel

    model = LiteLLMModel(model_id="gpt-4o-mini")
    agent = ToolCallingAgent(tools=[VisitWebpageTool()], model=model)
    result = agent.run("https://example.com のページタイトルだけ教えて")
    print(result)

if __name__ == "__main__":
    main()

キーなし → スクリプトが 貼り付け用サンプル を表示して終了。


10.7 ベンチマーク上の CodeAgent(概要)

魔理沙: 論文・ブログでは、コード行動 がツール合成タスクで有利な報告が多い。一方、単純 API 呼び出しだけなら ToolCalling の方がステップ数・失敗率で勝つこともある。

霊夢: 深追いは?

魔理沙: 公式 Examples と GAIA 系の第 16 章へ。本章は 選び方 までだ。


10.8 よくあるエラー ⚠️

ToolCallingAgent で Web 取得できない

VisitWebpageTooltools= に含める。または add_base_tools=True

CodeAgent 側だけ import エラー

additional_authorized_importsCodeAgent 専用。ToolCalling には効かない。

JSON パースエラー

モデルが tool call 形式に弱い → モデル変更、または CodeAgent へ。


10.9 本章のまとめ

霊夢: まとめ。

  1. CodeAgent — Python 行動、高い表現力
  2. ToolCallingAgent — JSON ツール呼び出し、高い構造化
  3. 同タスク比較 — ログの読みやすさ・ステップ数が違う
  4. OpenAI 系LiteLLMModel + ToolCallingAgent

✅ 章末チェックリスト

  • [ ] 比較表を自分の言葉で説明できる
  • [ ] compare_web_title.py を実行し、両方のログを見た
  • [ ] ToolCallingAgent に VisitWebpageTool を渡した
  • [ ] (任意)OpenAI API スケッチを読んだ

次章へ

霊夢: 使い分け、だいぶ見えてきたわ。

魔理沙: 次は CLI でさっと試す 章だ。ゆっくりしていこうな。


第11章 CLI でさっと試す — smolagent と webagent

本章のゴール: smolagent / webagent CLI の使い方と、Python API との使い分けを理解する。


11.1 なぜ CLI?

霊夢: 毎回 .py 書くの、面倒なときあるのよね。

魔理沙: インストール時に smolagentwebagent コマンドが付く。プロンプト・モデル・ツール・import を フラグで渡して即実行 できる。

pip install 'smolagents[toolkit]'
smolagent --help

11.2 smolagent — ワンショット実行

魔理沙: 引数にタスク文字列、その後オプションだ。

smolagent "Plan a trip to Tokyo, Kyoto and Osaka between Mar 28 and Apr 7." \
  --model-type InferenceClientModel \
  --model-id Qwen/Qwen2.5-Coder-32B-Instruct \
  --imports pandas numpy \
  --tools web_search
フラグ 意味
--model-type InferenceClientModel, LiteLLMModel など
--model-id モデル ID
--imports additional_authorized_imports(空白区切り)
--tools web_search 等、または user/space-id
--provider Inference Providers 指定(任意)
--api-base / --api-key LiteLLM 系(任意)

Space をツールとして指定

smolagent "Generate a cat icon." \
  --tools black-forest-labs/FLUX.1-schnell \
  --model-type InferenceClientModel

/ を含む名前は Tool.from_space として読み込まれる(CLI 実装)。


11.3 インタラクティブモード

霊夢: 引数なしだと?

魔理沙: ウィザード が起動する。

smolagent

案内される項目(バージョンにより多少異なる):

  1. エージェント種別 — CodeAgent / ToolCallingAgent
  2. ツール選択 — 利用可能ツールボックスから
  3. モデル — タイプ・ID・API 設定
  4. 追加 import
  5. タスクプロンプト

霊夢: 試行錯誤にちょうどいいわね。


11.4 action_type — code vs tool_calling

魔理沙: 内部では --action-type で切り替え(対話モードでも選択可)。

smolagent "What is 2+2?" \
  --model-type InferenceClientModel \
  --action-type tool_calling \
  --tools web_search
エージェント
code(既定) CodeAgent
tool_calling ToolCallingAgent

11.5 Python API との対応

📦 examples/ch11/cli_equivalent_agent.py

# examples/ch11/cli_equivalent_agent.py(リポジトリ同梱・全文)
"""第11章: smolagent CLI と同等の Python API 構成"""
from smolagents import CodeAgent, InferenceClientModel, WebSearchTool

def main() -> None:
    # CLI 例:
    # smolagent "Plan a trip to Tokyo..." \\
    #   --model-type InferenceClientModel \\
    #   --model-id Qwen/Qwen2.5-Coder-32B-Instruct \\
    #   --imports pandas numpy \\
    #   --tools web_search

    model = InferenceClientModel()
    agent = CodeAgent(
        tools=[WebSearchTool()],
        model=model,
        additional_authorized_imports=["pandas", "numpy"],
        stream_outputs=True,
    )

    prompt = (
        "3月28日から4月7日まで、東京・京都・大阪を巡る旅行の"
        "大まかな日別プランを箇条書きで。各日 1 行程度。"
    )
    result = agent.run(prompt)
    print("=== 最終回答 ===")
    print(result)

if __name__ == "__main__":
    main()
場面 おすすめ
ちょっと試す・デモ CLI
本番・テスト・CI Python API
Hub push / カスタムツール Python API

11.6 webagent — ブラウザ自動化の概要

霊夢: webagent は普通の smolagent と何が違うの?

魔理沙: Helium ベースで、実ブラウザを操作する Web 閲覧特化 エージェントだ。クリック・フォーム・商品ページ取得など。

# pip install 'smolagents[toolkit]'  # helium 等が入る構成を確認
webagent "go to example.com, open the docs page, return the main heading." \
  --model-type LiteLLMModel \
  --model-id gpt-4o-mini

⚠️ 注意:

  • ブラウザは ローカルで動く — ログイン状態・Cookie に注意
  • モデルは 視覚+DOM 理解が必要なことが多く、強いモデル推奨
  • 本番スクレイピングより 調査・デモ 向き

🔗 Web browser examples

flowchart LR
  User[プロンプト] --> webagent
  webagent --> Helium[Helium / ブラウザ]
  Helium --> Site[Web サイト]
  Site --> Answer[最終回答]

🖥️ ハンズオン 11-1 — CLI で旅行プラン

霊夢: ターミナルだけでやってみる!

A. CLI 直叩き(推奨)

export HF_TOKEN="hf_..."
smolagent "3月28日から4月7日、東京・京都・大阪の大まかな旅行プランを箇条書きで" \
  --model-type InferenceClientModel \
  --tools web_search

B. Python 同等スクリプト

python examples/ch11/cli_equivalent_agent.py
# examples/ch11/cli_equivalent_agent.py(リポジトリ同梱・全文)
"""第11章: smolagent CLI と同等の Python API 構成"""
from smolagents import CodeAgent, InferenceClientModel, WebSearchTool

def main() -> None:
    # CLI 例:
    # smolagent "Plan a trip to Tokyo..." \\
    #   --model-type InferenceClientModel \\
    #   --model-id Qwen/Qwen2.5-Coder-32B-Instruct \\
    #   --imports pandas numpy \\
    #   --tools web_search

    model = InferenceClientModel()
    agent = CodeAgent(
        tools=[WebSearchTool()],
        model=model,
        additional_authorized_imports=["pandas", "numpy"],
        stream_outputs=True,
    )

    prompt = (
        "3月28日から4月7日まで、東京・京都・大阪を巡る旅行の"
        "大まかな日別プランを箇条書きで。各日 1 行程度。"
    )
    result = agent.run(prompt)
    print("=== 最終回答 ===")
    print(result)

if __name__ == "__main__":
    main()

魔理沙: web_search が Inference + 検索 API に依存する。エラー時は第 2 章のトークン・ネットワークを確認だ。

╭──────────────── New run ────────────────╮
...
╭─ Executing tool 'web_search' ───────────╮
...
final_answer("Day 1: Tokyo ...")

11.7 環境変数と .env

魔理沙: CLI は python-dotenv.env を読む実装になっていることが多い。

# .env(Git にコミットしない)
HF_TOKEN=hf_...
OPENAI_API_KEY=sk-...
smolagent "Hello" --model-type InferenceClientModel

11.8 よくあるエラー ⚠️

smolagent: command not found

pip install 'smolagents[toolkit]'
which smolagent   # venv activate 確認

Tool X is not recognized

--tools登録済み名web_search)か Space IDorg/space)のみ。

webagent でブラウザが起動しない

Helium / ChromeDriver の依存。OS ごとの Helium ドキュメント を参照。


11.9 本章のまとめ

霊夢: 整理。

  1. smolagent — 汎用 CodeAgent / ToolCallingAgent を CLI から
  2. インタラクティブ — 引数なしでウィザード
  3. webagent — ブラウザ操作特化(Helium)
  4. 本番は Python API — 再現性・テストのため

✅ 章末チェックリスト

  • [ ] smolagent --help を実行した
  • [ ] ワンショットまたはインタラクティブで 1 タスク動かした
  • [ ] --tools web_search--imports の意味を説明できる
  • [ ] cli_equivalent_agent.py と CLI の対応を理解した
  • [ ] webagent がブラウザ連携であることを説明できる

次章へ

霊夢: CLI、試験用に便利ね。

魔理沙: 次は いよいよ安全 の本番編だ。ゆっくりしていこうな。


第12章 コード実行のセキュリティ — サンドボックスと executor

本章のゴール: LocalPythonExecutor の制限を体験し、executor_type(e2b / docker 等) の位置づけと マルチエージェント制約 を理解する。


12.1 LLM が書いたコード、怖くない?

霊夢: CodeAgent、便利だけど……私の PC で import os されたら?

魔理沙: 正当な不安だ。脅威はざっくり 4 つ。

脅威
LLM のミス 意図しない rm -rf 風操作
プロンプトインジェクション 悪意ある Web ページが「このコードを実行せよ」
悪意ある Hub ツール trust_remote_code の乱用
公開エージェントへの攻撃 外部から有害タスクを投入

霊夢: 100% 安全な方法は?

魔理沙: ない。レイヤーを重ねる(許可 import → 専用インタプリタ → リモートサンドボックス)のが現実的だ。

🔗 Secure code execution


12.2 2 つのサンドボックス戦略

flowchart TB
  subgraph A[スニペットのみリモート]
    L1[ローカル: LLM + エージェント] --> R1[リモート: 生成コード実行]
  end
  subgraph B[システム全体リモート]
    R2[リモート: エージェント + モデル + ツール]
  end
方式 executor_type 特徴
A. スニペットのみ e2b, docker, modal, blaxel セットアップ比較的楽。マネージドエージェントは制約あり
B. 全体 E2B 内で agent 全体を起動など 隔離は強いが API キー運搬が難しい

12.3 LocalPythonExecutor の仕組み

魔理沙: 通常の CPython ではなく、AST を歩いて安全な操作だけ 実行する。

  • 許可リスト外 import 禁止
  • サブモジュール も個別許可(numpy.random など)
  • 演算回数上限 で無限ループ抑制
  • 未定義操作は 即エラー
from smolagents.local_python_executor import LocalPythonExecutor

executor = LocalPythonExecutor(additional_authorized_imports=["numpy"])

try:
    executor("import os; os.system('echo pwned')")
except Exception as e:
    print(e)

📦 examples/ch12/local_executor_sandbox.py

python examples/ch12/local_executor_sandbox.py
# examples/ch12/local_executor_sandbox.py(リポジトリ同梱・全文)
"""第12章: LocalPythonExecutor のガードレールを直接試す"""
from smolagents.local_python_executor import LocalPythonExecutor

def run_capture(executor: LocalPythonExecutor, code: str) -> None:
    print(f"\n>>> {code.strip()[:60]}...")
    try:
        print(executor(code))
    except Exception as exc:
        print("ERROR:", exc)

def main() -> None:
    executor = LocalPythonExecutor(additional_authorized_imports=["numpy"])

    run_capture(executor, "import os\nos.system('echo bad')")
    run_capture(executor, "import random\nrandom._os.system('echo bad')")
    run_capture(executor, "while True:\n    pass")

if __name__ == "__main__":
    main()
ERROR: Import of os is not allowed ...
ERROR: Forbidden access to module: os
ERROR: Maximum number of ... iterations in While loop exceeded

⚠️ ローカル完封は不可能Pillow で巨大画像を量産するなど、許可パッケージの悪用は理論上あり得る。


12.4 CodeAgent と許可 import

霊夢: 第 9 章の additional_authorized_imports は、この executor への渡し込み?

魔理沙: その通り。エージェントは内部で LocalPythonExecutor(またはリモート executor)を使う。

from smolagents import CodeAgent, InferenceClientModel

agent = CodeAgent(
    tools=[],
    model=InferenceClientModel(),
    additional_authorized_imports=["requests"],
)

許可 しないos は弾かれる — 次のハンズオン。


12.5 executor_type — E2B / Docker / Modal / Blaxel

魔理沙: エージェント初期化時に指定。生成コードだけがリモートで走る。

E2B

pip install 'smolagents[e2b]'
export E2B_API_KEY="..."
from smolagents import CodeAgent, InferenceClientModel

with CodeAgent(
    model=InferenceClientModel(),
    tools=[],
    executor_type="e2b",
) as agent:
    agent.run("フィボナッチ数列の第 100 項を求めて")

Docker

pip install 'smolagents[docker]'
# Docker デーモンが起動していること
with CodeAgent(
    model=InferenceClientModel(),
    tools=[],
    executor_type="docker",
) as agent:
    agent.run("100 番目のフィボナッチ数は?")

📦 任意: examples/ch12/docker_executor_optional.py

# examples/ch12/docker_executor_optional.py(リポジトリ同梱・全文)
"""第12章: executor_type=docker(任意・Docker 必須)"""
import os
import shutil

from smolagents import CodeAgent, InferenceClientModel

def main() -> None:
    if not shutil.which("docker"):
        print("⚠️  docker コマンドが見つかりません。スキップします。")
        return
    if not os.environ.get("HF_TOKEN"):
        print("⚠️  HF_TOKEN 未設定。Inference API が必要です。")
        return

    model = InferenceClientModel()
    with CodeAgent(model=model, tools=[], executor_type="docker") as agent:
        result = agent.run("フィボナッチ数列の第 15 項を計算して整数で返して")
        print("=== 最終回答 ===")
        print(result)

if __name__ == "__main__":
    main()
キー 用途
E2B_API_KEY E2B サンドボックス
BL_API_KEY / BL_WORKSPACE Blaxel
Modal Modal アカウント設定

霊夢: with で囲むのはなぜ?

魔理沙: コンテナ / VM を 確実に破棄 するため。agent.cleanup() でも可。


12.6 マルチエージェントと E2B の制約

魔理沙: スニペットのみリモート のとき、モデル呼び出しはローカルに残る。マネージドエージェントをリモートで再帰呼び出しすると シークレットを渡せない 問題があり、公式も 複雑なマルチエージェントは未対応 としている。

対処: E2B コンテナ内で エージェント全体 を起動する(第 12 章公式の長いサンプル参照)。


🖥️ ハンズオン 12-1 — 失敗する import

霊夢: わざと os を使わせてみる!

📦 examples/ch12/blocked_import_agent.py

export HF_TOKEN="hf_..."
python examples/ch12/blocked_import_agent.py
# examples/ch12/blocked_import_agent.py(リポジトリ同梱・全文)
"""第12章: 許可されていない import で CodeAgent が失敗する例"""
from smolagents import CodeAgent, InferenceClientModel

def main() -> None:
    model = InferenceClientModel()
    agent = CodeAgent(
        tools=[],
        model=model,
        additional_authorized_imports=[],  # os は許可しない
        max_steps=4,
        verbosity_level=1,
    )

    task = (
        "import os を使ってカレントディレクトリのファイル一覧を "
        "final_answer で返して。os 以外は使わないで。"
    )
    try:
        result = agent.run(task)
        print("=== 最終回答 ===")
        print(result)
    except Exception as exc:
        print("=== 例外 ===")
        print(exc)

if __name__ == "__main__":
    main()
Code execution failed at line 'import os' due to:
InterpreterError: Import of os is not allowed. Authorized imports are: [...]
観察 学び
ステップは継続しうる LLM が別手段を試す場合あり
os 未許可 ホワイトリストが効いている

🖥️ ハンズオン 12-2 — (任意)Docker executor

pip install 'smolagents[docker]'
export HF_TOKEN="hf_..."
python examples/ch12/docker_executor_optional.py
# examples/ch12/docker_executor_optional.py(リポジトリ同梱・全文)
"""第12章: executor_type=docker(任意・Docker 必須)"""
import os
import shutil

from smolagents import CodeAgent, InferenceClientModel

def main() -> None:
    if not shutil.which("docker"):
        print("⚠️  docker コマンドが見つかりません。スキップします。")
        return
    if not os.environ.get("HF_TOKEN"):
        print("⚠️  HF_TOKEN 未設定。Inference API が必要です。")
        return

    model = InferenceClientModel()
    with CodeAgent(model=model, tools=[], executor_type="docker") as agent:
        result = agent.run("フィボナッチ数列の第 15 項を計算して整数で返して")
        print("=== 最終回答 ===")
        print(result)

if __name__ == "__main__":
    main()

Docker 未インストール時は スキップメッセージ のみ。

E2B を試す場合:

pip install 'smolagents[e2b]'
export E2B_API_KEY="..."
with CodeAgent(model=InferenceClientModel(), tools=[], executor_type="e2b") as agent:
    print(agent.run("第 20 フィボナッチ数は?"))

12.7 セキュリティチェックリスト(本番)⚠️

  • [ ] additional_authorized_imports を最小限にした
  • [ ] Hub / MCP は信頼ソースのみ
  • [ ] 公開エージェントにレート制限・入力検証
  • [ ] 本番では executor_type リモートを検討
  • [ ] ログに API キーを出していない
  • [ ] マルチエージェント時は E2B の制約を読んだ

12.8 よくあるエラー ⚠️

Import of X is not allowed

許可リストに追加するか、本当に必要か 再検討。

E2B / Docker 接続失敗

E2B_API_KEY not set
Cannot connect to the Docker daemon

環境変数とデーモン起動を確認。

ローカルでは動くがサンドボックスで失敗

リモート環境に パッケージ未インストール。カスタム Docker イメージで pip install する。


12.9 本章のまとめ

霊夢: まとめるわ。

  1. LocalPythonExecutor — import・サブモジュール・ループ上限
  2. 完全安全は無理 — リモート executor で層を足す
  3. executor_typee2b / docker / modal / blaxel
  4. with / cleanup() — リソース解放
  5. マルチエージェント — リモートスニペット方式に制約

✅ 章末チェックリスト

  • [ ] local_executor_sandbox.py で 3 種のエラーを確認した
  • [ ] blocked_import_agent.pyos 拒否を見た
  • [ ] 2 つのサンドボックス戦略を説明できる
  • [ ] executor_type="docker" または e2b の手順を読んだ(任意実行)
  • [ ] 本番セキュリティチェックリストを眺めた

次章へ

霊夢: 怖かったけど、対策はあるのね。

魔理沙: 次は max_steps や Gradio でエージェントを育てる章だ。ゆっくりしていこうな。


第13章 エージェントを育てる — 設定とデバッグ

本章のゴール: max_stepsverbosity_levelinterruptreset=False・ストリーミング を使い分け、GradioUI で対話デバッグできる。


13.1 動くけど、中身が見えない

霊夢: 第 3 章でログは見たけど……本番に近づくと、ステップが多すぎたり、途中で止めたくなったりするのよね。

魔理沙: そのときは エージェントのノブ を回す。max_steps で暴走を防ぎ、verbosity_level でログ量を調整、interrupt() で人間が割り込む。会話アプリなら reset=False で記憶をつなぐ。

パラメータ / API 役割
max_steps 1 タスクあたりの最大ステップ数
verbosity_level コンソールログの詳しさ(0〜2 程度)
agent.interrupt() 現在ステップの終了後に停止
run(..., reset=False) メモリを消さず次タスクへ
run(..., stream=True) ステップをジェネレータで逐次取得
stream_outputs=True モデル出力のトークンストリーム(対応モデルのみ)

13.2 max_steps — 無限ループのブレーキ

魔理沙: final_answer しないまま回り続けると、コストも時間も溶ける。既定値はあるが、タスクに合わせて下げたり上げたりする。

from smolagents import CodeAgent, InferenceClientModel

agent = CodeAgent(
    tools=[],
    model=InferenceClientModel(),
    max_steps=10,
)

打ち切り時の例:

Max steps reached ...

霊夢: 増やせばいい?

魔理沙: 根本は タスク文を具体化 すること。「必ず final_answer で整数のみ」など。max_steps は保険だ。

📦 examples/ch13/verbosity_max_steps.py

export HF_TOKEN="hf_..."
python examples/ch13/verbosity_max_steps.py
# examples/ch13/verbosity_max_steps.py(リポジトリ同梱・全文)
"""第13章: verbosity_level と max_steps の違いを確認"""
from __future__ import annotations

import os
import sys

from smolagents import CodeAgent, InferenceClientModel

def main() -> None:
    if not os.environ.get("HF_TOKEN") and not os.environ.get("HUGGINGFACEHUB_API_TOKEN"):
        print("HF_TOKEN が未設定のためスキップします。")
        sys.exit(0)

    model = InferenceClientModel()
    task = "1 から 5 までの合計を計算し、整数だけを final_answer で返して。"

    print("=== verbosity_level=0(静か) ===")
    quiet = CodeAgent(tools=[], model=model, verbosity_level=0, max_steps=5)
    print(quiet.run(task))

    print("\n=== verbosity_level=2(詳細) ===")
    verbose = CodeAgent(tools=[], model=model, verbosity_level=2, max_steps=5)
    print(verbose.run(task))

    print("\n=== max_steps=1(打ち切りの例) ===")
    tight = CodeAgent(tools=[], model=model, verbosity_level=1, max_steps=1)
    try:
        print(tight.run("100 個の素数を列挙して final_answer で返して。"))
    except Exception as exc:
        print(f"例外または打ち切り: {exc}")

if __name__ == "__main__":
    main()

13.3 verbosity_level — ログの濃さ

レベル 目安
0 静か(最終回答中心)
1 既定。Step・コード・トークン数
2 より詳細なデバッグ向け
agent = CodeAgent(tools=[], model=InferenceClientModel(), verbosity_level=0)
result = agent.run("2+2 は?")

本番ログ集約では 0、開発中は 12 が多い。


13.4 agent.interrupt() — 人間の停止ボタン

霊夢: Gradio で長いタスクを走らせたら、止めたいわ。

魔理沙: interrupt()いまのステップが終わったあと に止まる。Gradio の停止ボタンから呼ぶ想定だ。

import threading
import time

from smolagents import CodeAgent, InferenceClientModel

agent = CodeAgent(tools=[], model=InferenceClientModel(), max_steps=20)

def stop_later():
    time.sleep(8)
    agent.interrupt()

threading.Thread(target=stop_later, daemon=True).start()

try:
    agent.run("非常に長い計算タスク ...")
except Exception as e:
    print("中断:", e)

📦 examples/ch13/interrupt_demo.py

# examples/ch13/interrupt_demo.py(リポジトリ同梱・全文)
"""第13章: agent.interrupt() で実行を止めるデモ"""
from __future__ import annotations

import os
import sys
import threading
import time

from smolagents import CodeAgent, InferenceClientModel

def main() -> None:
    if not os.environ.get("HF_TOKEN") and not os.environ.get("HUGGINGFACEHUB_API_TOKEN"):
        print("HF_TOKEN が未設定のためスキップします。")
        sys.exit(0)

    model = InferenceClientModel()
    agent = CodeAgent(tools=[], model=model, verbosity_level=1, max_steps=12)

    def interrupt_after_delay() -> None:
        time.sleep(8)
        print("\n>>> interrupt() を呼び出します <<<")
        agent.interrupt()

    watcher = threading.Thread(target=interrupt_after_delay, daemon=True)
    watcher.start()

    task = (
        "1 から 200 までの素数をすべて列挙し、"
        "見つかった個数も含めて final_answer で返して。"
        "時間がかかってもよい。"
    )
    try:
        result = agent.run(task)
        print("=== 最終回答 ===")
        print(result)
    except Exception as exc:
        print("=== 中断またはエラー ===")
        print(type(exc).__name__, exc)

if __name__ == "__main__":
    main()

13.5 reset=False — 会話をつなぐ

魔理沙: run(task, reset=True)(既定)だと、前のタスクのメモリが消える。チャット UI では reset=False で文脈を継ぐ。

agent.run("私の名前は霊夢。記憶したと返して。", reset=True)
agent.run("さっきの私の名前は?", reset=False)   # 「霊夢」と答えやすい
agent.run("さっきの私の名前は?", reset=True)    # 記憶クリア

GradioUI も内部で reset=False を使う(公式 Guided tour)。

📦 examples/ch13/reset_false_demo.py

python examples/ch13/reset_false_demo.py
# examples/ch13/reset_false_demo.py(リポジトリ同梱・全文)
"""第13章: reset=False で会話メモリを継続"""
from __future__ import annotations

import os
import sys

from smolagents import CodeAgent, InferenceClientModel

def main() -> None:
    if not os.environ.get("HF_TOKEN") and not os.environ.get("HUGGINGFACEHUB_API_TOKEN"):
        print("HF_TOKEN が未設定のためスキップします。")
        sys.exit(0)

    model = InferenceClientModel()
    agent = CodeAgent(tools=[], model=model, verbosity_level=1, max_steps=6)

    first = agent.run(
        "私の名前はゆっくり霊夢。これを覚えて、final_answer で「記憶した」とだけ返して。",
        reset=True,
    )
    print("--- 1 ターン目 ---")
    print(first)

    second = agent.run(
        "さっき教えた私の名前は? final_answer で名前だけ返して。",
        reset=False,
    )
    print("--- 2 ターン目(reset=False) ---")
    print(second)

    third = agent.run(
        "さっき教えた私の名前は?",
        reset=True,
    )
    print("--- 3 ターン目(reset=True で記憶クリア) ---")
    print(third)

if __name__ == "__main__":
    main()

13.6 ストリーミング出力の概要

霊夢: トークンが少しずつ出るやつ?

魔理沙: 2 層ある。

run(stream=True) — ステップ単位

for step in agent.run("12 * 13 を計算して", stream=True):
    print(type(step).__name__)

各イテレーションで ActionStep などが返る。UI で「Step ごとに更新」したいとき向き。

stream_outputs=True — LLM トークン単位

agent = CodeAgent(
    tools=[],
    model=InferenceClientModel(),
    stream_outputs=True,
)

モデルが generate_stream を実装している必要がある。未対応なら初期化時に警告される。

📦 examples/ch13/stream_run_demo.py

# examples/ch13/stream_run_demo.py(リポジトリ同梱・全文)
"""第13章: agent.run(stream=True) でステップを逐次取得"""
from __future__ import annotations

import os
import sys

from smolagents import CodeAgent, InferenceClientModel

def main() -> None:
    if not os.environ.get("HF_TOKEN") and not os.environ.get("HUGGINGFACEHUB_API_TOKEN"):
        print("HF_TOKEN が未設定のためスキップします。")
        sys.exit(0)

    model = InferenceClientModel()
    agent = CodeAgent(tools=[], model=model, verbosity_level=0, max_steps=5)

    task = "12 × 13 を計算し、整数を final_answer で返して。"
    print("=== stream=True: 各ステップを表示 ===")
    step_gen = agent.run(task, stream=True)
    for i, step in enumerate(step_gen):
        print(f"[step {i}] {type(step).__name__}")

    print("\n=== stream_outputs=True(モデルが generate_stream 対応時) ===")
    streaming_agent = CodeAgent(
        tools=[],
        model=model,
        verbosity_level=1,
        max_steps=4,
        stream_outputs=True,
    )
    print(streaming_agent.run("7 の階乗を計算して final_answer で返して。"))

if __name__ == "__main__":
    main()

🔗 API — MultiStepAgent.run


🖥️ ハンズオン 13-1 — GradioUI でチャット UI

霊夢: ブラウザで試したい!

魔理沙: pip install 'smolagents[gradio]' が要る。

pip install 'smolagents[gradio]'
export HF_TOKEN="hf_..."
python examples/ch13/gradio_ui_agent.py
# examples/ch13/gradio_ui_agent.py(リポジトリ同梱・全文)
"""第13章: GradioUI でチャット形式のエージェント UI"""
from __future__ import annotations

import os
import sys

from smolagents import CodeAgent, GradioUI, InferenceClientModel, WebSearchTool

def main() -> None:
    if not os.environ.get("HF_TOKEN") and not os.environ.get("HUGGINGFACEHUB_API_TOKEN"):
        print("HF_TOKEN が未設定のためスキップします。")
        sys.exit(0)

    try:
        import gradio  # noqa: F401
    except ImportError:
        print("Gradio が未インストールです: pip install 'smolagents[gradio]'")
        sys.exit(1)

    model = InferenceClientModel()
    agent = CodeAgent(
        tools=[WebSearchTool()],
        model=model,
        add_base_tools=False,
        verbosity_level=1,
        max_steps=10,
    )

    print("ブラウザで Gradio UI を開きます。終了は Ctrl+C。")
    print("内部では各送信ごとに agent.run(message, reset=False) が呼ばれます。")
    GradioUI(agent).launch(share=False)

if __name__ == "__main__":
    main()
  • 各メッセージ送信 → agent.run(ユーザー入力, reset=False)
  • 思考・コード・ツール結果がチャット UI に流れる
  • 停止ボタン → agent.interrupt() を接続できる(カスタム UI 時)

⚠️ share=True は一時的な公開 URL が作られる。デモ以外は False 推奨。

🔗 Guided tour — GradioUI


🖥️ ハンズオン 13-2 — reset=False を 2 ターンで体験

python examples/ch13/reset_false_demo.py
# examples/ch13/reset_false_demo.py(リポジトリ同梱・全文)
"""第13章: reset=False で会話メモリを継続"""
from __future__ import annotations

import os
import sys

from smolagents import CodeAgent, InferenceClientModel

def main() -> None:
    if not os.environ.get("HF_TOKEN") and not os.environ.get("HUGGINGFACEHUB_API_TOKEN"):
        print("HF_TOKEN が未設定のためスキップします。")
        sys.exit(0)

    model = InferenceClientModel()
    agent = CodeAgent(tools=[], model=model, verbosity_level=1, max_steps=6)

    first = agent.run(
        "私の名前はゆっくり霊夢。これを覚えて、final_answer で「記憶した」とだけ返して。",
        reset=True,
    )
    print("--- 1 ターン目 ---")
    print(first)

    second = agent.run(
        "さっき教えた私の名前は? final_answer で名前だけ返して。",
        reset=False,
    )
    print("--- 2 ターン目(reset=False) ---")
    print(second)

    third = agent.run(
        "さっき教えた私の名前は?",
        reset=True,
    )
    print("--- 3 ターン目(reset=True で記憶クリア) ---")
    print(third)

if __name__ == "__main__":
    main()

2 ターン目で名前を覚えていれば成功。3 ターン目 reset=True では忘れるのが正常。


13.7 よくあるエラー ⚠️

Max steps reached

タスクを短くする・final_answer を明示・max_steps を少し増やす。

stream_outputs の警告

別モデルにするか stream_outputs=False に戻す。

Gradio が import できない

pip install 'smolagents[gradio]'

13.8 本章のまとめ

霊夢: まとめるわ。

  1. max_steps — 暴走とコストの上限
  2. verbosity_level — 開発時は上げ、本番は下げる
  3. interrupt() — ステップ境界での安全な停止
  4. reset=False — チャット継続・Gradio 既定
  5. ストリームstream=True(ステップ)と stream_outputs(トークン)

✅ 章末チェックリスト

  • [ ] verbosity_max_steps.py でログの濃さの差を見た
  • [ ] reset_false_demo.py で 2 ターン目が文脈を引き継ぐことを確認した
  • [ ] (任意)interrupt_demo.py を実行した
  • [ ] (任意)gradio_ui_agent.py で UI を開いた
  • [ ] stream=Truestream_outputs の違いを説明できる

次章へ

霊夢: ノブが増えたわ。次は「しょぼいエージェント」の改善?

魔理沙: 第 14 章で ツール設計と API 統合 だ。ゆっくりしていこうな。


第14章 うまいエージェントの作り方 — シンプルさとツール設計

本章のゴール: LLM 呼び出しを減らす設計読みやすいツールを身につけ、天気 API を 悪い例→良い例→統合ツール でリファクタする。


14.1 動くけど、しょぼい……

霊夢: エージェント、動くんだけどステップばっかりで遅いのよね。

魔理沙: 多くは 設計の問題 だ。LLM は部屋の中に閉じ込められ、ツール結果だけが窓から渡される。情報が薄いと、何度も試行錯誤する。

🔗 Building good agents


14.2 最優先: ワークフローを単純に

魔理沙: ベストなマルチエージェントより、単純な単一エージェント + 良いツール の方が勝つことが多い。

原則 具体策
LLM 呼び出しを減らす 2 つの API を 1 ツール にまとめる
決定論に寄せる 計算・整形は Python / ツール内で完結
タスクを明確に 曖昧な「調べて」より入出力形式を指定
flowchart LR
  subgraph bad[悪い例]
    A1[LLM] --> T1[天気 API]
    A1 --> T2[距離 API]
  end
  subgraph good[良い例]
    A2[LLM] --> M[return_spot_information]
    M --> T1
    M --> T2
  end

14.3 ツール設計 — 天気 API の悪い例

霊夢: 何が「悪い」の?

魔理沙: 公式チュートリアルの Poor version ベースだ。

  • date_time の形式が不明
  • location の書き方が不明
  • エラー時の説明がない
  • 戻り値が str([28.0, 0.35, 0.85]) で読みにくい
  • forward 内に ログがない
@tool
def get_weather_api(location: str, date_time: str) -> str:
    """
    Returns the weather report.
    Args:
        location: the name of the place ...
        date_time: the date and time ...
    """
    lon, lat = convert_location_to_coordinates(location)
    parsed = datetime.datetime.strptime(date_time, "%m/%d/%y %H:%M:%S")
    return str(get_weather_report_at_coordinates((lon, lat), parsed))

📦 examples/ch14/bad_weather_tool.py

export HF_TOKEN="hf_..."
python examples/ch14/bad_weather_tool.py
# examples/ch14/bad_weather_tool.py(リポジトリ同梱・全文)
"""第14章: 悪い例 — ログ・形式・エラー説明が不足した天気ツール"""
from __future__ import annotations

import datetime
import os
import sys

from smolagents import CodeAgent, InferenceClientModel, tool

def _coords_from_location(location: str) -> tuple[float, float]:
    # デモ用ダミー座標
    return 3.3, -42.0

def _weather_at_coords(coords: tuple[float, float], date_time: datetime.datetime) -> list[float]:
    # [気温°C, 降水リスク 0-1, 波の高さ m]
    return [28.0, 0.35, 0.85]

@tool
def get_weather_api(location: str, date_time: str) -> str:
    """
    Returns the weather report.

    Args:
        location: the name of the place that you want the weather for.
        date_time: the date and time for which you want the report.
    """
    lon, lat = _coords_from_location(location)
    parsed = datetime.datetime.strptime(date_time, "%m/%d/%y %H:%M:%S")
    return str(_weather_at_coords((lon, lat), parsed))

def main() -> None:
    if not os.environ.get("HF_TOKEN") and not os.environ.get("HUGGINGFACEHUB_API_TOKEN"):
        print("HF_TOKEN が未設定のためスキップします。")
        sys.exit(0)

    agent = CodeAgent(
        tools=[get_weather_api],
        model=InferenceClientModel(),
        verbosity_level=1,
        max_steps=8,
    )
    task = (
        "2026/05/23 12:00:00 の時点で、"
        "モロッコのタガズートのサーフスポットの天気を教えて。"
    )
    print(agent.run(task))

if __name__ == "__main__":
    main()

日付形式を間違えると、LLM が何度もリトライしがちだ。


14.4 ツール設計 — 良い例へリファクタ

魔理沙: 自問:「初めてこのツールを使う自分が、エラーを直せるか?」

改善ポイント:

  1. Args に具体例(国名まで、日時フォーマット)
  2. print でログ(ツール名・引数・失敗理由)
  3. ValueError に修正ヒント を載せる
  4. 人間が読める 1 文 で返す
DATE_FMT = "%m/%d/%y %H:%M:%S"

@tool
def get_weather_api(location: str, date_time: str) -> str:
    """
    指定した場所・日時のサーフ向け天気レポートを返す。

    Args:
        location: 例: "Anchor Point, Taghazout, Morocco"
        date_time: 例: '05/23/26 12:00:00'(形式 '%m/%d/%y %H:%M:%S')
    """
    lon, lat = _coords_from_location(location)
    try:
        parsed = datetime.datetime.strptime(date_time, DATE_FMT)
    except ValueError as exc:
        raise ValueError(
            f"date_time は '{DATE_FMT}' 形式で渡してください。詳細: {exc}"
        ) from exc
    temp_c, rain_risk, wave_m = _weather_at_coords((lon, lat), parsed)
    return (
        f"Weather report for {location}, {parsed.strftime(DATE_FMT)}: "
        f"Temperature {temp_c}°C, rain risk {rain_risk * 100:.0f}%, "
        f"wave height {wave_m}m."
    )

📦 examples/ch14/good_weather_tool.py

python examples/ch14/good_weather_tool.py
# examples/ch14/good_weather_tool.py(リポジトリ同梱・全文)
"""第14章: 良い例 — 形式・ログ・読みやすい出力の天気ツール"""
from __future__ import annotations

import datetime
import os
import sys

from smolagents import CodeAgent, InferenceClientModel, tool

DATE_FMT = "%m/%d/%y %H:%M:%S"

def _coords_from_location(location: str) -> tuple[float, float]:
    print(f"[get_weather_api] 座標変換: location={location!r}")
    if not location.strip():
        raise ValueError("location が空です。例: 'Anchor Point, Taghazout, Morocco'")
    return 3.3, -42.0

def _weather_at_coords(coords: tuple[float, float], date_time: datetime.datetime) -> tuple[float, float, float]:
    print(f"[get_weather_api] 天気取得: coords={coords}, at={date_time.isoformat()}")
    return 28.0, 0.35, 0.85

@tool
def get_weather_api(location: str, date_time: str) -> str:
    """
    指定した場所・日時のサーフ向け天気レポートを返す。

    Args:
        location: 場所名。例: "Anchor Point, Taghazout, Morocco"(国名まで含めるとよい)。
        date_time: 日時。必ず '%m/%d/%y %H:%M:%S' 形式の文字列(例: '05/23/26 12:00:00')。
    """
    lon, lat = _coords_from_location(location)
    try:
        parsed = datetime.datetime.strptime(date_time, DATE_FMT)
    except ValueError as exc:
        raise ValueError(
            "date_time の変換に失敗しました。"
            f"形式は '{DATE_FMT}' です。例: '05/23/26 12:00:00'。"
            f"詳細: {exc}"
        ) from exc
    temp_c, rain_risk, wave_m = _weather_at_coords((lon, lat), parsed)
    return (
        f"Weather report for {location}, {parsed.strftime(DATE_FMT)}: "
        f"Temperature {temp_c}°C, rain risk {rain_risk * 100:.0f}%, wave height {wave_m}m."
    )

def main() -> None:
    if not os.environ.get("HF_TOKEN") and not os.environ.get("HUGGINGFACEHUB_API_TOKEN"):
        print("HF_TOKEN が未設定のためスキップします。")
        sys.exit(0)

    agent = CodeAgent(
        tools=[get_weather_api],
        model=InferenceClientModel(),
        verbosity_level=1,
        max_steps=6,
    )
    task = (
        "2026年5月23日12時(現地)の、モロッコ・タガズートの天気を "
        "get_weather_api で取得し、要約して final_answer で返して。"
        f"date_time は必ず {DATE_FMT!r} 形式で渡すこと。"
    )
    print(agent.run(task))

if __name__ == "__main__":
    main()

霊夢: ログに [get_weather_api] が出ると、デバッグしやすそうね。


14.5 タスク文の書き方

魔理沙: ツールだけじゃない。タスク もプロンプトの一部だ。

悪い例 良い例
天気を教えて date_time'%m/%d/%y %H:%M:%S'get_weather_api を 1 回使え
調べて 公式ドキュメント URL を ## 出典 に列挙せよ
レポートを書いて Markdown で 3 節、各節 3 行以内

エージェント全体への恒久指示は instructions=(システムプロンプトに追記)。

agent = CodeAgent(
    tools=[...],
    model=model,
    instructions="最終回答は日本語。出典 URL を必ず含める。",
)

14.6 2 つの API を 1 ツールに統合

霊夢: 天気と距離、別々に呼ばせるのが悪いの?

魔理沙: サーフ旅行の例では 1 回のツール呼び出し で両方返す方が、ステップ数・レイテンシ・失敗率が下がる。

@tool
def return_spot_information(
    origin_city: str,
    surf_spot: str,
    date_time: str,
) -> str:
    """
    移動時間とサーフスポットの天気をまとめて返す。
  ...
    """
    hours = _travel_hours(origin_city, surf_spot)
    weather = _weather_report(surf_spot, parsed)
    return f"Travel ...\nWeather: {weather}"

📦 examples/ch14/merged_spot_info_agent.py

python examples/ch14/merged_spot_info_agent.py
# examples/ch14/merged_spot_info_agent.py(リポジトリ同梱・全文)
"""第14章: 天気 API と距離 API を 1 ツールに統合して LLM 呼び出しを減らす"""
from __future__ import annotations

import datetime
import os
import sys

from smolagents import CodeAgent, InferenceClientModel, tool

DATE_FMT = "%m/%d/%y %H:%M:%S"

def _travel_hours(origin: str, destination: str) -> float:
    print(f"[return_spot_information] 距離 API: {origin!r} -> {destination!r}")
    return 2.5

def _weather_report(location: str, date_time: datetime.datetime) -> str:
    print(f"[return_spot_information] 天気 API: {location!r} @ {date_time.isoformat()}")
    return (
        f"Temperature 26°C, rain risk 20%, wave height 1.2m "
        f"at {location} on {date_time.strftime(DATE_FMT)}."
    )

@tool
def return_spot_information(
    origin_city: str,
    surf_spot: str,
    date_time: str,
) -> str:
    """
    サーフスポットへの移動時間と、そのスポットの天気をまとめて返す。

    Args:
        origin_city: 出発都市(例: 'Marrakech, Morocco')。
        surf_spot: サーフスポット名(例: 'Taghazout, Morocco')。
        date_time: 現地の日時。'%m/%d/%y %H:%M:%S' 形式(例: '05/23/26 12:00:00')。
    """
    try:
        parsed = datetime.datetime.strptime(date_time, DATE_FMT)
    except ValueError as exc:
        raise ValueError(
            f"date_time は '{DATE_FMT}' 形式で渡してください。詳細: {exc}"
        ) from exc
    hours = _travel_hours(origin_city, surf_spot)
    weather = _weather_report(surf_spot, parsed)
    return (
        f"Travel from {origin_city} to {surf_spot}: about {hours:.1f} hours.\n"
        f"Weather: {weather}"
    )

def main() -> None:
    if not os.environ.get("HF_TOKEN") and not os.environ.get("HUGGINGFACEHUB_API_TOKEN"):
        print("HF_TOKEN が未設定のためスキップします。")
        sys.exit(0)

    agent = CodeAgent(
        tools=[return_spot_information],
        model=InferenceClientModel(),
        verbosity_level=1,
        max_steps=5,
    )
    task = (
        "マラケシュからタガズートへ行くサーフ旅行について、"
        "移動時間と 2026/05/23 12:00:00 時点の天気をまとめて教えて。"
        "ツールは return_spot_information を 1 回だけ使うこと。"
    )
    print(agent.run(task))

if __name__ == "__main__":
    main()

タスク側で「ツールは 1 回だけ」と書くと、さらに安定する。


🖥️ ハンズオン 14-1 — 悪い天気ツール → 良いツール

霊夢: 比較したいわ。

魔理沙: 同じタスク文で badgood を実行し、ステップ数とログ を比べろ。

  1. python examples/ch14/bad_weather_tool.py
# examples/ch14/bad_weather_tool.py(リポジトリ同梱・全文)
"""第14章: 悪い例 — ログ・形式・エラー説明が不足した天気ツール"""
from __future__ import annotations

import datetime
import os
import sys

from smolagents import CodeAgent, InferenceClientModel, tool

def _coords_from_location(location: str) -> tuple[float, float]:
    # デモ用ダミー座標
    return 3.3, -42.0

def _weather_at_coords(coords: tuple[float, float], date_time: datetime.datetime) -> list[float]:
    # [気温°C, 降水リスク 0-1, 波の高さ m]
    return [28.0, 0.35, 0.85]

@tool
def get_weather_api(location: str, date_time: str) -> str:
    """
    Returns the weather report.

    Args:
        location: the name of the place that you want the weather for.
        date_time: the date and time for which you want the report.
    """
    lon, lat = _coords_from_location(location)
    parsed = datetime.datetime.strptime(date_time, "%m/%d/%y %H:%M:%S")
    return str(_weather_at_coords((lon, lat), parsed))

def main() -> None:
    if not os.environ.get("HF_TOKEN") and not os.environ.get("HUGGINGFACEHUB_API_TOKEN"):
        print("HF_TOKEN が未設定のためスキップします。")
        sys.exit(0)

    agent = CodeAgent(
        tools=[get_weather_api],
        model=InferenceClientModel(),
        verbosity_level=1,
        max_steps=8,
    )
    task = (
        "2026/05/23 12:00:00 の時点で、"
        "モロッコのタガズートのサーフスポットの天気を教えて。"
    )
    print(agent.run(task))

if __name__ == "__main__":
    main()
  1. python examples/ch14/good_weather_tool.py
# examples/ch14/good_weather_tool.py(リポジトリ同梱・全文)
"""第14章: 良い例 — 形式・ログ・読みやすい出力の天気ツール"""
from __future__ import annotations

import datetime
import os
import sys

from smolagents import CodeAgent, InferenceClientModel, tool

DATE_FMT = "%m/%d/%y %H:%M:%S"

def _coords_from_location(location: str) -> tuple[float, float]:
    print(f"[get_weather_api] 座標変換: location={location!r}")
    if not location.strip():
        raise ValueError("location が空です。例: 'Anchor Point, Taghazout, Morocco'")
    return 3.3, -42.0

def _weather_at_coords(coords: tuple[float, float], date_time: datetime.datetime) -> tuple[float, float, float]:
    print(f"[get_weather_api] 天気取得: coords={coords}, at={date_time.isoformat()}")
    return 28.0, 0.35, 0.85

@tool
def get_weather_api(location: str, date_time: str) -> str:
    """
    指定した場所・日時のサーフ向け天気レポートを返す。

    Args:
        location: 場所名。例: "Anchor Point, Taghazout, Morocco"(国名まで含めるとよい)。
        date_time: 日時。必ず '%m/%d/%y %H:%M:%S' 形式の文字列(例: '05/23/26 12:00:00')。
    """
    lon, lat = _coords_from_location(location)
    try:
        parsed = datetime.datetime.strptime(date_time, DATE_FMT)
    except ValueError as exc:
        raise ValueError(
            "date_time の変換に失敗しました。"
            f"形式は '{DATE_FMT}' です。例: '05/23/26 12:00:00'。"
            f"詳細: {exc}"
        ) from exc
    temp_c, rain_risk, wave_m = _weather_at_coords((lon, lat), parsed)
    return (
        f"Weather report for {location}, {parsed.strftime(DATE_FMT)}: "
        f"Temperature {temp_c}°C, rain risk {rain_risk * 100:.0f}%, wave height {wave_m}m."
    )

def main() -> None:
    if not os.environ.get("HF_TOKEN") and not os.environ.get("HUGGINGFACEHUB_API_TOKEN"):
        print("HF_TOKEN が未設定のためスキップします。")
        sys.exit(0)

    agent = CodeAgent(
        tools=[get_weather_api],
        model=InferenceClientModel(),
        verbosity_level=1,
        max_steps=6,
    )
    task = (
        "2026年5月23日12時(現地)の、モロッコ・タガズートの天気を "
        "get_weather_api で取得し、要約して final_answer で返して。"
        f"date_time は必ず {DATE_FMT!r} 形式で渡すこと。"
    )
    print(agent.run(task))

if __name__ == "__main__":
    main()

意図的に date_time を曖昧にしたタスクを試すと差が出やすい。


🖥️ ハンズオン 14-2 — API 統合でステップ削減

python examples/ch14/merged_spot_info_agent.py
# examples/ch14/merged_spot_info_agent.py(リポジトリ同梱・全文)
"""第14章: 天気 API と距離 API を 1 ツールに統合して LLM 呼び出しを減らす"""
from __future__ import annotations

import datetime
import os
import sys

from smolagents import CodeAgent, InferenceClientModel, tool

DATE_FMT = "%m/%d/%y %H:%M:%S"

def _travel_hours(origin: str, destination: str) -> float:
    print(f"[return_spot_information] 距離 API: {origin!r} -> {destination!r}")
    return 2.5

def _weather_report(location: str, date_time: datetime.datetime) -> str:
    print(f"[return_spot_information] 天気 API: {location!r} @ {date_time.isoformat()}")
    return (
        f"Temperature 26°C, rain risk 20%, wave height 1.2m "
        f"at {location} on {date_time.strftime(DATE_FMT)}."
    )

@tool
def return_spot_information(
    origin_city: str,
    surf_spot: str,
    date_time: str,
) -> str:
    """
    サーフスポットへの移動時間と、そのスポットの天気をまとめて返す。

    Args:
        origin_city: 出発都市(例: 'Marrakech, Morocco')。
        surf_spot: サーフスポット名(例: 'Taghazout, Morocco')。
        date_time: 現地の日時。'%m/%d/%y %H:%M:%S' 形式(例: '05/23/26 12:00:00')。
    """
    try:
        parsed = datetime.datetime.strptime(date_time, DATE_FMT)
    except ValueError as exc:
        raise ValueError(
            f"date_time は '{DATE_FMT}' 形式で渡してください。詳細: {exc}"
        ) from exc
    hours = _travel_hours(origin_city, surf_spot)
    weather = _weather_report(surf_spot, parsed)
    return (
        f"Travel from {origin_city} to {surf_spot}: about {hours:.1f} hours.\n"
        f"Weather: {weather}"
    )

def main() -> None:
    if not os.environ.get("HF_TOKEN") and not os.environ.get("HUGGINGFACEHUB_API_TOKEN"):
        print("HF_TOKEN が未設定のためスキップします。")
        sys.exit(0)

    agent = CodeAgent(
        tools=[return_spot_information],
        model=InferenceClientModel(),
        verbosity_level=1,
        max_steps=5,
    )
    task = (
        "マラケシュからタガズートへ行くサーフ旅行について、"
        "移動時間と 2026/05/23 12:00:00 時点の天気をまとめて教えて。"
        "ツールは return_spot_information を 1 回だけ使うこと。"
    )
    print(agent.run(task))

if __name__ == "__main__":
    main()

ログで return_spot_information1 回 呼ばれていれば成功に近い。


14.7 計画(planning)と stream=True(概要)

魔理沙: 複雑タスクではエージェントが 計画ステップ を挟むことがある。run(stream=True)PlanningStep も逐次見られる(第 13 章)。

本番では:

  • 計画を許すタスク → max_steps に余裕
  • 単純タスク → ツール統合で計画自体不要にする

14.8 デバッグの順序

  1. より強いモデル に切り替え(コストとトレードオフ)
  2. タスク・ツール説明 を具体化
  3. verbosity_level を上げてログ確認
  4. (最後の手段)プロンプトテンプレート変更 — 公式は非推奨気味

14.9 よくあるエラー ⚠️

ツールを何度も呼ぶ

→ タスクに「1 回だけ」と書く、または API を統合。

strptime 失敗のループ

→ ツールの docstring と ValueError メッセージに 正しい例 を書く。


14.10 本章のまとめ

霊夢: まとめ。

  1. シンプルなワークフロー が最強に近い
  2. ツール — 形式・ログ・読みやすい出力・エラーメッセージ
  3. タスク文 — 入出力と回数制限を明示
  4. API 統合 — LLM ステップとコストを削る

✅ 章末チェックリスト

  • [ ] 悪い例と良い例の差を 3 点説明できる
  • [ ] good_weather_tool.py を実行した
  • [ ] merged_spot_info_agent.py で統合ツールを試した
  • [ ] 公式 Building good agents を一読した

次章へ

霊夢: ツールの説明文、地味だけど大事ね。

魔理沙: 次は 複数エージェント で専門化するぜ。ゆっくりしていこうな。


第15章 マネージャーと専門家 — managed_agents

本章のゴール: name / description / managed_agents でマネージャーと検索専門エージェントを組み立て、ログから どのエージェントが呼ばれたか 追跡できる。


15.1 なぜマルチエージェントか

霊夢: ツールを増やすんじゃなくて、エージェントを増やすの?

魔理沙: 専門化メモリ分離 だ。Web 検索エージェントの履歴に、コード生成の失敗ログを詰め込まなくていい。ベンチマークでは、役割分担した方が成績が上がることも多い。

flowchart TB
  User[ユーザー] --> M[マネージャー CodeAgent]
  M -->|task 文字列| W[web_search_agent]
  W -->|要約結果| M
  M --> Answer[final_answer]

🔗 Multi-agents (guided tour)


15.2 namedescription — ツールと同じ重要性

魔理沙: マネージドエージェントも ツールと同様、初期化時にマネージャーのシステムプロンプトへ埋め込まれる。

属性 役割
name 呼び出し名(Python 関数名のように使う)
description いつ・何を渡すかの説明
web_agent = CodeAgent(
    tools=[WebSearchTool()],
    model=model,
    name="web_search_agent",
    description=(
        "Runs web searches for you. Give it your query as an argument."
    ),
)

霊夢: description が曖昧だと?

魔理沙: マネージャーが 間違った専門家 を呼ぶか、タスク文が薄くて失敗する。第 14 章のツール設計と同じ思想だ。


15.3 managed_agents — マネージャーの組み立て

魔理沙: マネージャー側は tools=[] でも、チームメンバー呼び出し がプロンプトに載る。

from smolagents import CodeAgent, InferenceClientModel, WebSearchTool

model = InferenceClientModel()

web_agent = CodeAgent(
    tools=[WebSearchTool()],
    model=model,
    name="web_search_agent",
    description="Runs web searches for you. Give it your query as an argument.",
    add_base_tools=False,
)

manager_agent = CodeAgent(
    tools=[],
    model=model,
    managed_agents=[web_agent],
)

manager_agent.run("Who is the CEO of Hugging Face?")

マネージャーのコード生成では、だいたい次のように見える:

result = web_search_agent(task="Hugging Face CEO 2026 ...")
print(result)

15.4 メモリとツールセットの切り分け

専門家 ツール例 メモリ
web_search_agent WebSearchTool 検索クエリ・URL
code_agent (空 + Python 実行) 計算・整形
マネージャー なし(委譲のみ) 最終統合

霊夢: 全部 1 人のエージェントにツールを載せるよりいい?

魔理沙: 単純タスクなら 1 人の方が安い(第 14 章)。複雑・長文・役割がぶつかるときにマルチを検討する。


🖥️ ハンズオン 15-1 — web_search 専門 + マネージャー

霊夢: 動かしてみるわ。

📦 examples/ch15/manager_web_search.py

export HF_TOKEN="hf_..."
python examples/ch15/manager_web_search.py
# examples/ch15/manager_web_search.py(リポジトリ同梱・全文)
"""第15章: マネージャー CodeAgent + Web 検索専門エージェント"""
from __future__ import annotations

import os
import sys

from smolagents import CodeAgent, InferenceClientModel, WebSearchTool

def build_agents(model: InferenceClientModel) -> CodeAgent:
    web_agent = CodeAgent(
        tools=[WebSearchTool()],
        model=model,
        name="web_search_agent",
        description=(
            "Runs web searches for you. Give it your query as a detailed task string. "
            "Returns summarized search results with URLs when possible."
        ),
        add_base_tools=False,
        verbosity_level=1,
        max_steps=8,
    )

    manager = CodeAgent(
        tools=[],
        model=model,
        managed_agents=[web_agent],
        verbosity_level=1,
        max_steps=10,
    )
    return manager

def main() -> None:
    if not os.environ.get("HF_TOKEN") and not os.environ.get("HUGGINGFACEHUB_API_TOKEN"):
        print("HF_TOKEN が未設定のためスキップします。")
        sys.exit(0)

    model = InferenceClientModel()
    manager = build_agents(model)

    task = "Hugging Face の CEO は誰?根拠 URL を含めて final_answer で答えて。"
    print(manager.run(task))

if __name__ == "__main__":
    main()
Step 0: web_search_agent(task=...)
...
Out - Final answer: ...

🖥️ ハンズオン 15-2 — ログで呼び出しを追跡

📦 examples/ch15/trace_managed_agent_logs.py

python examples/ch15/trace_managed_agent_logs.py
# examples/ch15/trace_managed_agent_logs.py(リポジトリ同梱・全文)
"""第15章: ログからマネージドエージェント呼び出しを追跡"""
from __future__ import annotations

import json
import os
import sys

from smolagents import CodeAgent, InferenceClientModel, WebSearchTool

def main() -> None:
    if not os.environ.get("HF_TOKEN") and not os.environ.get("HUGGINGFACEHUB_API_TOKEN"):
        print("HF_TOKEN が未設定のためスキップします。")
        sys.exit(0)

    model = InferenceClientModel()
    web_agent = CodeAgent(
        tools=[WebSearchTool()],
        model=model,
        name="web_search_agent",
        description="Runs web searches. Pass a clear search task.",
        add_base_tools=False,
        verbosity_level=1,
        max_steps=6,
    )
    manager = CodeAgent(
        tools=[],
        model=model,
        managed_agents=[web_agent],
        verbosity_level=1,
        max_steps=8,
    )

    manager.run("smolagents とは何? 2 文で要約して。")

    print("=== agent.logs の件数 ===", len(manager.logs))
    for i, entry in enumerate(manager.logs):
        text = json.dumps(entry, ensure_ascii=False, default=str)
        if "web_search_agent" in text or "managed" in text.lower():
            print(f"\n--- log[{i}] (managed 関連) ---")
            print(text[:1500])

if __name__ == "__main__":
    main()

霊夢: web_search_agent という文字列が log に出てた!

魔理沙: 本番ではこの文字列や Step の tool_calls 相当をメトリクスに載せると、どの専門家が高コストか 分かる。


15.5 additional_args で専門家にコンテキストを渡す

魔理沙: マネージドエージェント呼び出しも、ツールと同様 additional_args で画像や DataFrame を渡せる(公式プロンプトテンプレート参照)。

# マネージャーのタスク例
manager.run(
    "code_agent にこの CSV を集計させて",
    additional_args={"dataframe": df},
)

15.6 コストと E2B ⚠️

霊夢: 第 12 章のサンドボックスと両立する?

魔理沙: マネージドエージェント + リモート executor(E2B スニペット方式) には制約がある。本番構成では公式 Secure code execution を再確認しろ。


15.7 よくあるエラー ⚠️

マネージャーが自分で検索しようとする

description を「検索が必要なら 必ず web_search_agent を呼べ」と具体化。

専門家が final_answer しない

専門家タスクに「結果を テキストで返す 。final_answer はマネージャーが行う」と書くか、専門家にも max_steps を適切に設定。

名前の衝突

name は Python 識別子として有効な文字(スネークケース推奨)。


15.8 本章のまとめ

霊夢: まとめ。

  1. マルチエージェント — 専門化とメモリ分離
  2. name / description — ツールと同レベルで丁寧に
  3. managed_agents=[...] — マネージャーにチームを登録
  4. ログweb_search_agent 等で呼び出しを追跡

✅ 章末チェックリスト

  • [ ] manager_web_search.py を実行した
  • [ ] trace_managed_agent_logs.py で専門家名をログから見つけた
  • [ ] マネージャーと専門家の役割分担を図で説明できる
  • [ ] 単一エージェントの方がよいケースを 1 つ挙げられる

次章へ

霊夢: チーム編成、楽しいわね。

魔理沙: 次は GAIA 級の 3 役構成 だ。ゆっくりしていこうな。


第16章 実践マルチエージェント — GAIA スタイルの構成

本章のゴール: 検索・コード・マネージャー の 3 役にツールを切り分け、調査レポート タスクを GAIA ブログの要点に沿って実行する。


16.1 GAIA と「役割分担」

霊夢: GAIA って何?

魔理沙: 現実世界の質問に答える エージェント・ベンチマーク だ。HF チームは smolagents でマルチエージェント構成を組み、リーダーボード上位を狙った。

🔗 Beating GAIA blog

要点(本書用に圧縮):

要点 内容
専門化 検索・閲覧・計算でエージェントを分ける
ツールの最小化 各専門家に 必要なツールだけ
マネージャー 計画と最終統合。重い閲覧履歴を抱えない
検証 可能なら別ステップで事実確認(本章はマネージャー指示で簡略化)

16.2 3 役の設計パターン

flowchart TB
  User[調査レポート依頼] --> M[report_manager]
  M --> S[search_agent<br/>WebSearch + VisitWebpage]
  M --> C[code_agent<br/>Python 計算]
  S --> M
  C --> M
  M --> Report[Markdown レポート + 出典]
name ツール 責務
検索 search_agent WebSearchTool, VisitWebpageTool 検索・ページ読取・要点抽出
コード code_agent (なし) 数値検算・表整形
マネージャー report_manager なし 委譲・レポート形式・出典セクション

霊夢: 検証役は?

魔理沙: 本番 GAIA 構成では ファクトチェック 用のエージェントを足すこともある。本章は 3 役 + マネージャー指示で「出典必須」に留める(第 17 章で単一エージェントのリサーチも学ぶ)。


16.3 ツールセットの切り分け

魔理沙: code_agentWebSearchTool を渡すと、検索ログがコード専門家のメモリを汚す。渡さない

search_agent = CodeAgent(
    tools=[WebSearchTool(), VisitWebpageTool()],
    name="search_agent",
    description="Web 検索とページ閲覧の専門家。要点と URL を返す。",
    add_base_tools=False,
)

code_agent = CodeAgent(
    tools=[],
    name="code_agent",
    description="数値計算・データ整形の専門家。",
)

16.4 コストとレイテンシのトレードオフ

構成 LLM 呼び出し 向き
単一 + 全ツール 少なめ 簡単タスク
3 役マルチ 多め 長い調査・役割衝突を避けたい
統合ツール(第 14 章) 中程度 API 呼び出しの重複を削る

霊夢: 常にマルチが正解?

魔理沙: 違う。まず単一 + 良いツール。足りなければ専門家を増やす。


🖥️ ハンズオン 16-1 — 3 役で調査レポート

霊夢: レポート、書いてもらうのよ!

📦 examples/ch16/gaia_three_agent_report.py

export HF_TOKEN="hf_..."
python examples/ch16/gaia_three_agent_report.py
# examples/ch16/gaia_three_agent_report.py(リポジトリ同梱・全文)
"""第16章: 3 役(検索・コード・マネージャー)で調査レポートを生成"""
from __future__ import annotations

import os
import sys

from smolagents import CodeAgent, InferenceClientModel, VisitWebpageTool, WebSearchTool

def build_research_system(model: InferenceClientModel) -> CodeAgent:
    search_agent = CodeAgent(
        tools=[WebSearchTool(), VisitWebpageTool()],
        model=model,
        name="search_agent",
        description=(
            "Web 検索とページ閲覧の専門家。"
            "調査タスクを受け取り、要点と出典 URL を箇条書きで返す。"
        ),
        add_base_tools=False,
        verbosity_level=1,
        max_steps=10,
    )

    code_agent = CodeAgent(
        tools=[],
        model=model,
        name="code_agent",
        description=(
            "数値計算・データ整形の専門家。"
            "検索結果の数値を検算したり、表形式にまとめたりする。"
        ),
        verbosity_level=1,
        max_steps=8,
    )

    manager = CodeAgent(
        tools=[],
        model=model,
        name="report_manager",
        description="調査レポートを統合するマネージャー。",
        managed_agents=[search_agent, code_agent],
        instructions=(
            "最終回答は Markdown 形式の短い調査レポートにすること。"
            "必ず ## 出典 セクションに URL を列挙すること。"
            "search_agent に調査、必要なら code_agent に計算を任せる。"
        ),
        verbosity_level=1,
        max_steps=12,
    )
    return manager

def main() -> None:
    if not os.environ.get("HF_TOKEN") and not os.environ.get("HUGGINGFACEHUB_API_TOKEN"):
        print("HF_TOKEN が未設定のためスキップします。")
        sys.exit(0)

    model = InferenceClientModel()
    manager = build_research_system(model)

    topic = "smolagents"
    task = (
        f"トピック「{topic}」について調査レポートを書いて。"
        "概要、主な特徴 3 点、公式ドキュメント URL を含めること。"
        "数値の比較があれば code_agent で検算してよい。"
    )
    print(manager.run(task))

if __name__ == "__main__":
    main()

期待する流れ:

  1. マネージャーが search_agent(task=...) を計画
  2. 必要なら code_agent で数値整理
  3. マネージャーが ## 概要 ## 特徴 ## 出典 形式で final_answer
Step 0: search_agent(task=...)
Step 1: ...
Out - Final answer: ## 概要 ...

16.5 ブログ要点の整理

魔理沙: Beating GAIA から持ち帰るべきこと:

  1. エージェントは薄く、ツールは厚く(ただしツール数は増やしすぎない)
  2. マネージャーは委譲に徹する — 自分で Web を読み始めないよう description を書く
  3. 出典と検算 — ハルシネーション対策はプロセスで入れる
  4. モデル選定 — 難問ほど強いモデル(第 4 章)

16.6 よくあるエラー ⚠️

レポートに URL がない

マネージャーの instructions とタスク文の両方に「## 出典」を要求。

search_agentfinal_answer して終わる

専門家の description に「マネージャー向けに中間結果を返す」と明記。

ステップ過多

max_steps を役割ごとに絞る、またはトピックを狭める(「smolagents のインストール方法のみ」など)。


16.7 本章のまとめ

霊夢: まとめ。

  1. GAIA — 現実的な質問でエージェントを評価するベンチマーク
  2. 3 役 — 検索・コード・マネージャー
  3. ツール切り分け — 専門家に必要最小限
  4. トレードオフ — コスト増 ↔ 品質・安定性

✅ 章末チェックリスト

  • [ ] gaia_three_agent_report.py を実行した
  • [ ] ログで search_agent / code_agent の呼び出しを確認した
  • [ ] Beating GAIA blog の見出しを 3 つ言える
  • [ ] 単一エージェントの方がよいケースを説明できる

次章へ

霊夢: チーム戦、なんか本格的ね。

魔理沙: 次章は 1 体の Web リサーチャー に絞る。VisitWebpage と出典の付け方だ。ゆっくりしていこうな。


第17章 Web リサーチエージェント — 検索・取得・要約

本章のゴール: WebSearchTool + VisitWebpageTool で検索→ページ取得→要約のパイプラインを組み、「smolagents の最新バージョン」を 出典付き で調べさせる。


17.1 Web リサーチの基本パイプライン

霊夢: 検索結果のスニペットだけじゃ足りないことがあるのよね。

魔理沙: 本格的なリサーチは次の 3 段だ。

sequenceDiagram
  participant A as CodeAgent
  participant S as WebSearchTool
  participant V as VisitWebpageTool
  A->>S: web_search(query)
  S-->>A: URL 一覧
  A->>V: visit_webpage(url)
  V-->>A: Markdown 化本文
  A->>A: 要約 + final_answer
ツール 出力
発見 WebSearchTool タイトル・スニペット・URL
精読 VisitWebpageTool HTML → Markdown 近似
統合 (エージェント) 回答 + 出典 URL

17.2 VisitWebpageTool と markdown 化

魔理沙: 第 10 章では ToolCallingAgent 向けに登場した。CodeAgent でも tools=[VisitWebpageTool()] で使える。

from smolagents import VisitWebpageTool

tool = VisitWebpageTool()
# 手動テスト(エージェント外)
# print(tool("https://huggingface.co/docs/smolagents"))
  • 長いページは トークン制限 に注意 → タスクで「公式ドキュメントの Installation 節だけ読め」と絞る
  • JavaScript 多用サイトは本文が取れないことがある → 別 URL を試させる

17.3 出典を最終回答に含める

霊夢: URL、忘れがちでしょ?

魔理沙: instructions とタスク文の 両方 で縛るのが確実だ。

agent = CodeAgent(
    tools=[WebSearchTool(), VisitWebpageTool()],
    model=model,
    instructions=(
        "final_answer には必ず ## 回答 と ## 出典(URL の箇条書き)を含める。"
    ),
)

第 14 章の「タスク文の具体化」もセットで使え。


17.4 CodeAgent での典型的なコード

公式チュートリアルに近い流れ:

pages = web_search(query="smolagents pypi version")
print(pages)

for url in ["https://pypi.org/project/smolagents/", "..."]:
    text = visit_webpage(url)
    print(text[:500])

エージェントはこのパターンを 1 Step または複数 Step で書く。print 出力が次 Step の Observation になる(第 3 章)。


🖥️ ハンズオン 17-1 — 最新バージョンを調査

霊夢: smolagents、今何版なの?

魔理沙: 📦 完成例を動かせ。

export HF_TOKEN="hf_..."
python examples/ch17/web_research_agent.py
# examples/ch17/web_research_agent.py(リポジトリ同梱・全文)
"""第17章: 検索 + VisitWebpageTool で Web リサーチ"""
from __future__ import annotations

import os
import sys

from smolagents import CodeAgent, InferenceClientModel, VisitWebpageTool, WebSearchTool

def build_agent(model: InferenceClientModel) -> CodeAgent:
    return CodeAgent(
        tools=[WebSearchTool(), VisitWebpageTool()],
        model=model,
        add_base_tools=False,
        verbosity_level=1,
        max_steps=12,
        instructions=(
            "Web 調査では次の手順を守ること。"
            "1) web_search で候補 URL を得る。"
            "2) visit_webpage で本文を読む。"
            "3) final_answer には ## 回答 と ## 出典(URL 一覧)を含める。"
        ),
    )

def main() -> None:
    if not os.environ.get("HF_TOKEN") and not os.environ.get("HUGGINGFACEHUB_API_TOKEN"):
        print("HF_TOKEN が未設定のためスキップします。")
        sys.exit(0)

    model = InferenceClientModel()
    agent = build_agent(model)

    task = (
        "smolagents の最新の安定版バージョン番号は?"
        "PyPI または公式ドキュメントを visit_webpage で確認し、"
        "根拠 URL とともに final_answer で答えて。"
    )
    print(agent.run(task))

if __name__ == "__main__":
    main()

期待する最終回答の形(例):

## 回答
(バージョン番号と 1 行の説明)

## 出典
- https://pypi.org/project/smolagents/
- https://huggingface.co/docs/smolagents/en/installation

⚠️ バージョンは実行時点で変わる。手順が正しいか を評価すること。


17.5 マルチエージェント版との使い分け

構成 向き
本章(単一 + 2 ツール) 1 トピックの deep dive、実装が簡単
第 16 章(3 役) 長いレポート・役割分離・GAIA 級

17.6 よくあるエラー ⚠️

visit_webpage だけで検索しない

先に web_search で URL を得るタスク文にする。

ページが空 / 短すぎる

Bot 対策サイトの可能性。公式ミラー URL をタスクで指定。

トークン超過

visit_webpage の結果を print 全文しないよう、「先頭 2000 文字だけ処理」とタスクに書く。

ハルシネーション

出典 URL がログの visit_webpage 引数と一致しているか人間が確認。


17.7 本章のまとめ

霊夢: まとめ。

  1. WebSearchTool — 候補 URL の発見
  2. VisitWebpageTool — Markdown 化された本文
  3. 出典セクション — プロンプトで必須化
  4. 完成例examples/ch17/web_research_agent.py

✅ 章末チェックリスト

  • [ ] web_research_agent.py を実行した
  • [ ] ログに web_searchvisit_webpage の両方がある
  • [ ] 最終回答に ## 出典 がある(またはタスクを直して再実行した)
  • [ ] 単一エージェントと第 16 章 3 役の使い分けを説明できる

次章へ

霊夢: 出典付き、ちゃんと書けたわ。

魔理沙: 次は DB に話しかける Text-to-SQL だ。ゆっくりしていこうな。


第18章 Text-to-SQL エージェント — 自然言語で DB に聞く

本章のゴール: SQLite サンプル DB に対し、additional_argsスキーマを渡し読み取り専用の SQL ツール で Text-to-SQL エージェントを動かす。


18.1 なぜ「パイプライン」じゃなくエージェント?

霊夢: 自然言語を SQL に変換するライブラリ、昔からあるわよね。わざわざエージェントにする理由は?

魔理沙: 単発の text-to-SQL は 壊れた SQL をそのまま実行 しがちだ。エージェントなら 結果を見てやり直すJOIN が足りないと気づく0 件なら条件を変える、という ReAct ループが使える。

方式 長所 短所
パイプライン(NL→SQL→実行) 速い・安い 誤 SQL の自己修正が弱い
CodeAgent + SQL ツール 試行錯誤・説明付き回答 LLM コスト・ステップ数

霊夢: 本番 DB に繋ぐ前に、サンドボックスで練習した方がいいわね。

魔理沙: その通り。本章は SQLite のサンプルSELECT だけ に絞るぜ。

🔗 公式 Text-to-SQL 例


18.2 パイプラインとの対比コード

魔理沙: まず「1 発 SQL 生成」イメージ(エージェントではない)。

# パターン A: 単発生成(自己修正なし)
question = "チップ合計が最大のウェイターは?"
# sql = llm.generate(f"次の DB について SQL を1つ: {schema}\n{question}")
# rows = db.execute(sql)  # 誤りでもそのまま実行されうる

霊夢: SQL が間違っても止まらないの、怖いわ。

魔理沙: 次が本章の形。ツールで実行し、エージェントがログを読む。

from smolagents import CodeAgent, InferenceClientModel
from examples.ch18.readonly_sql_tool import readonly_sql_engine  # 第18章サンプル

model = InferenceClientModel()
agent = CodeAgent(tools=[readonly_sql_engine], model=model)
# agent.run("チップ合計が最大のウェイターは?", additional_args={...})

18.3 additional_args でスキーマを渡す

霊夢: 第 9 章で聞いた additional_args、DB でも使うの?

魔理沙: 使う。agent.run(task, additional_args={...}) に入れた dict は、エージェントが書く Python から変数として参照 できる。スキーマ文字列や DB パスを渡すと、プロンプトがスッキリする。

schema = """
Table 'receipts':
  - receipt_id: INTEGER
  - customer_name: TEXT
  ...
"""

agent.run(
    "質問に答えて。使った SQL も書いて。",
    additional_args={
        "db_schema": schema,
        "db_path": "/path/to/sample_receipts.db",
    },
)

霊夢: エージェントのコード内で db_schema って書けるのね。

魔理沙: ああ。ただし 機密の接続文字列を additional_args に平文で載せない こと。本番は環境変数やシークレットマネージャ経由にする(第 21 章)。

flowchart LR
  User[自然言語の質問] --> Run["agent.run(..., additional_args)"]
  Run --> Code[生成 Python]
  Code --> Tool[readonly_sql_engine]
  Tool --> DB[(SQLite readonly)]
  DB --> Code
  Code --> Answer[final_answer]

18.4 SQL インジェクションと読み取り専用 DB ⚠️

霊夢: LLM が DROP TABLE とか書いたら?

魔理沙: 多層防御 が鉄則だ。

対策
DB 接続 SQLite URI file:...?mode=ro で読み取り専用
ツール SELECT のみ許可、キーワード拒否
権限 本番は読み取り専用レプリカ・限定ユーザー
監査 実行 SQL をログに残す
import re
import sqlite3

_FORBIDDEN = re.compile(
    r"\b(INSERT|UPDATE|DELETE|DROP|ALTER|CREATE)\b",
    re.IGNORECASE,
)

def assert_select_only(query: str) -> None:
    if _FORBIDDEN.search(query):
        raise ValueError("書き込み系 SQL は拒否")
    if not re.match(r"^\s*SELECT\b", query, re.IGNORECASE):
        raise ValueError("SELECT のみ")
# 読み取り専用接続(URI mode=ro)
db_path = "examples/ch18/sample_receipts.db"
uri = f"file:{db_path}?mode=ro"
conn = sqlite3.connect(uri, uri=True)

霊夢: ユーザー入力を SQL に 連結 しないのも大事よね。

魔理沙: その通り。パラメータバインドはツール側でやる。LLM 生成 SQL は 監査対象 として扱え。


18.5 スキーマ説明をツール docstring にも載せる

魔理沙: 公式例では ツールの description / docstring にカラム一覧を書く。additional_args二重に 書いてもよいが、矛盾しないよう注意だ。

from smolagents import tool

@tool
def readonly_sql_engine(query: str) -> str:
    """
    読み取り専用 SELECT。テーブル receipts, waiters あり。

    Args:
        query: SQLite の SELECT 文。
    """
    ...

霊夢: JOIN が要る質問はステップが増えるわね。

魔理沙: だから max_steps を少し余裕を持たせるか、強いモデルに切り替える(公式例の Level 2 参照)。


🖥️ ハンズオン 18-1 — サンプル DB を作る

霊夢: 手を動かすわ!

魔理沙: まず DB ファイルを作る。📦 examples/ch18/setup_sample_db.py

pip install 'smolagents[toolkit]' sqlalchemy
mkdir -p examples/ch18
python examples/ch18/setup_sample_db.py
# examples/ch18/setup_sample_db.py(リポジトリ同梱・全文)
"""第18章: Text-to-SQL 用のサンプル SQLite DB を作成する"""
import sqlite3
from pathlib import Path

DB_PATH = Path(__file__).resolve().parent / "sample_receipts.db"

def create_schema(conn: sqlite3.Connection) -> None:
    conn.executescript(
        """
        DROP TABLE IF EXISTS waiters;
        DROP TABLE IF EXISTS receipts;

        CREATE TABLE receipts (
            receipt_id INTEGER PRIMARY KEY,
            customer_name TEXT NOT NULL,
            price REAL NOT NULL,
            tip REAL NOT NULL
        );

        CREATE TABLE waiters (
            receipt_id INTEGER PRIMARY KEY,
            waiter_name TEXT NOT NULL,
            FOREIGN KEY (receipt_id) REFERENCES receipts(receipt_id)
        );
        """
    )

def insert_sample_rows(conn: sqlite3.Connection) -> None:
    receipts = [
        (1, "Alan Payne", 12.06, 1.20),
        (2, "Alex Mason", 23.86, 0.24),
        (3, "Woodrow Wilson", 53.43, 5.43),
        (4, "Margaret James", 21.11, 1.00),
    ]
    waiters = [
        (1, "Corey Johnson"),
        (2, "Michael Watts"),
        (3, "Michael Watts"),
        (4, "Margaret James"),
    ]
    conn.executemany(
        "INSERT INTO receipts VALUES (?, ?, ?, ?)",
        receipts,
    )
    conn.executemany(
        "INSERT INTO waiters VALUES (?, ?)",
        waiters,
    )

def main() -> None:
    DB_PATH.parent.mkdir(parents=True, exist_ok=True)
    if DB_PATH.exists():
        DB_PATH.unlink()

    with sqlite3.connect(DB_PATH) as conn:
        create_schema(conn)
        insert_sample_rows(conn)
        conn.commit()

    print(f"Created sample DB: {DB_PATH}")
    with sqlite3.connect(DB_PATH) as conn:
        count = conn.execute("SELECT COUNT(*) FROM receipts").fetchone()[0]
    print(f"receipts rows: {count}")

if __name__ == "__main__":
    main()
Created sample DB: .../examples/ch18/sample_receipts.db
receipts rows: 4

中身を人間の目で確認するなら:

sqlite3 examples/ch18/sample_receipts.db "SELECT * FROM receipts;"
1|Alan Payne|12.06|1.2
2|Alex Mason|23.86|0.24
...

🖥️ ハンズオン 18-2 — Text-to-SQL エージェントを走らせる

魔理沙: 読み取り専用ツール + additional_args で本番だ。📦 examples/ch18/text_to_sql_agent.py

export HF_TOKEN="hf_..."   # Inference API 利用時
python examples/ch18/text_to_sql_agent.py
# examples/ch18/text_to_sql_agent.py(リポジトリ同梱・全文)
"""第18章: additional_args でスキーマを渡す Text-to-SQL エージェント"""
import sys
from pathlib import Path

from smolagents import CodeAgent, InferenceClientModel

_CH18 = Path(__file__).resolve().parent
sys.path.insert(0, str(_CH18))
from readonly_sql_tool import build_schema_description, readonly_sql_engine

DB_PATH = Path(__file__).resolve().parent / "sample_receipts.db"

def main() -> None:
    if not DB_PATH.exists():
        raise SystemExit(
            "先に python examples/ch18/setup_sample_db.py を実行してください"
        )

    schema = build_schema_description()
    model = InferenceClientModel()
    agent = CodeAgent(
        tools=[readonly_sql_engine],
        model=model,
        additional_authorized_imports=["sqlite3"],
    )

    task = """
    次の質問に答えてください。
    1. readonly_sql_engine で SQL を実行する
    2. 結果を日本語で要約する
    3. 使った SQL 文も最終回答に含める

    質問: チップの合計が最も多いウェイターの名前は?
    """

    result = agent.run(
        task,
        additional_args={
            "db_schema": schema,
            "db_path": str(DB_PATH),
        },
    )
    print("=== 最終回答 ===")
    print(result)

if __name__ == "__main__":
    main()

エージェント本体の要点:

"""第18章: additional_args でスキーマを渡す Text-to-SQL エージェント"""
import sys
from pathlib import Path

from smolagents import CodeAgent, InferenceClientModel

_CH18 = Path(__file__).resolve().parent
sys.path.insert(0, str(_CH18))
from readonly_sql_tool import build_schema_description, readonly_sql_engine

schema = build_schema_description()
agent = CodeAgent(
    tools=[readonly_sql_engine],
    model=InferenceClientModel(),
)

result = agent.run(
    "チップの合計が最も多いウェイターの名前は? SQL も示して。",
    additional_args={"db_schema": schema, "db_path": str(_CH18 / "sample_receipts.db")},
)
print(result)

霊夢: ログに readonly_sql_engine が出てきたわ。Michael Watts あたり?

魔理沙: データ次第だが、JOIN + SUM(tip) の SELECT を試行してから final_answer する流れになる。Step 内の SQL を必ず確認しろ。

━━━━━━━━━━━━━━━━━━━━ Step 0 ━━━━━━━━━━━━━━━
╭─ Executing this code: ─────────────────╮
│ result = readonly_sql_engine("SELECT …")│
╰────────────────────────────────────────╯
...
╭─ Executing this code: ─────────────────╮
│ final_answer("ウェイターは …")          │
╰────────────────────────────────────────╯

18.6 タスク文のテンプレート

魔理沙: 曖昧だと変な SQL になる。第 14 章のタスク設計と組み合わせろ。

TASK_TEMPLATE = """
あなたはデータアナリストです。
- db_schema を参照してテーブル構造を理解すること
- SQL は readonly_sql_engine のみで実行(SELECT のみ)
- 結果が空なら条件を見直すこと
- 最終回答に: 結論、根拠となった数値、実行した SQL を含めること

質問: {user_question}
"""

agent.run(
    TASK_TEMPLATE.format(user_question="最も高い会計の顧客名は?"),
    additional_args={"db_schema": schema},
)

18.7 よくあるエラーと対処 ⚠️

霊夢: database is locked って出たわ……

魔理沙: よくあるのはこれだ。

DB ファイルがない

先に python examples/ch18/setup_sample_db.py を実行してください
python examples/ch18/setup_sample_db.py
# examples/ch18/setup_sample_db.py(リポジトリ同梱・全文)
"""第18章: Text-to-SQL 用のサンプル SQLite DB を作成する"""
import sqlite3
from pathlib import Path

DB_PATH = Path(__file__).resolve().parent / "sample_receipts.db"

def create_schema(conn: sqlite3.Connection) -> None:
    conn.executescript(
        """
        DROP TABLE IF EXISTS waiters;
        DROP TABLE IF EXISTS receipts;

        CREATE TABLE receipts (
            receipt_id INTEGER PRIMARY KEY,
            customer_name TEXT NOT NULL,
            price REAL NOT NULL,
            tip REAL NOT NULL
        );

        CREATE TABLE waiters (
            receipt_id INTEGER PRIMARY KEY,
            waiter_name TEXT NOT NULL,
            FOREIGN KEY (receipt_id) REFERENCES receipts(receipt_id)
        );
        """
    )

def insert_sample_rows(conn: sqlite3.Connection) -> None:
    receipts = [
        (1, "Alan Payne", 12.06, 1.20),
        (2, "Alex Mason", 23.86, 0.24),
        (3, "Woodrow Wilson", 53.43, 5.43),
        (4, "Margaret James", 21.11, 1.00),
    ]
    waiters = [
        (1, "Corey Johnson"),
        (2, "Michael Watts"),
        (3, "Michael Watts"),
        (4, "Margaret James"),
    ]
    conn.executemany(
        "INSERT INTO receipts VALUES (?, ?, ?, ?)",
        receipts,
    )
    conn.executemany(
        "INSERT INTO waiters VALUES (?, ?)",
        waiters,
    )

def main() -> None:
    DB_PATH.parent.mkdir(parents=True, exist_ok=True)
    if DB_PATH.exists():
        DB_PATH.unlink()

    with sqlite3.connect(DB_PATH) as conn:
        create_schema(conn)
        insert_sample_rows(conn)
        conn.commit()

    print(f"Created sample DB: {DB_PATH}")
    with sqlite3.connect(DB_PATH) as conn:
        count = conn.execute("SELECT COUNT(*) FROM receipts").fetchone()[0]
    print(f"receipts rows: {count}")

if __name__ == "__main__":
    main()

書き込み SQL を拒否された

ValueError: 書き込み系 SQL は拒否しました(読み取り専用)

対処: 正常動作。タスクに「SELECT のみ」と明記する。

no such table

対処: db_schema と実 DB の不一致。setup_sample_db.py を再実行。


18.8 本章のまとめ

霊夢: 整理するわ。

  1. Text-to-SQL は エージェントの試行錯誤 が効く
  2. スキーマadditional_args とツール docstring で渡す
  3. 読み取り専用 DB + SELECT 制限 で被害を限定する
  4. 実行 SQL はログで監査する

魔理沙: 次章は画像・音声の マルチモーダル だ。additional_args の使い道がまた増えるぜ。


✅ 章末チェックリスト

  • [ ] setup_sample_db.pysample_receipts.db を作れた
  • [ ] readonly_sql_engineINSERT / DROP を拒否することを確認した
  • [ ] text_to_sql_agent.pyadditional_argsdb_schema を渡した
  • [ ] ログ内の SQL が SELECT のみであることを確認した
  • [ ] 最終回答に結論と SQL が含まれていた
  • [ ] 本番 DB 接続情報を Git にコミットしていない

次章へ

霊夢: DB に話しかけるエージェント、なんかカッコいいわね。

魔理沙: 本番は権限と監査が本番だぜ。ゆっくりしていこうな。


第19章 マルチモーダル — 画像・音声をエージェントに渡す

本章のゴール: ビジョン入力additional_args でのメディア渡しを理解し、Hub 画像生成 Space ツールでプロンプト改善ループを試す(音声は任意)。


19.1 マルチモーダルとは何が増える?

霊夢: ずっとテキストばかりだったけど、画像や音声も扱えるの?

魔理沙: 扱える。ただし 2 つの軸 に分けて考えろ。

内容 本章での例
入力 モデルが画像・音声を「見る/聞く」 ビジョンモデル + image_path
出力・加工 ツールで生成・転写する load_tool の画像 Space、Transcriber

霊夢: モデル選びが大事なのね。

魔理沙: 当たり。テキスト専用モデルに画像パスを渡しても意味がない。VL(Vision-Language)モデルを選ぶ。

🔗 Web browser / vision examples


19.2 ビジョン入力 — モデルと additional_args

魔理沙: 第 18 章と同様、additional_argsファイルパスや URL を渡し、エージェントの Python から参照する。

from smolagents import CodeAgent, InferenceClientModel

VISION_MODEL_ID = "Qwen/Qwen2.5-VL-7B-Instruct"  # 利用可能な VL モデルに差し替え

model = InferenceClientModel(model_id=VISION_MODEL_ID)
agent = CodeAgent(tools=[], model=model, additional_authorized_imports=["PIL"])

agent.run(
    "image_path の画像に写っている物体を3つ列挙して",
    additional_args={"image_path": "examples/ch19/sample.png"},
)

霊夢: Pillow で開くコードを LLM が書くのね。

魔理沙: ああ。依存が要るなら additional_authorized_imports を忘れるな。

pip install pillow

19.3 画像生成 Space ツール

霊夢: 第 8 章の Space ツール、画像でも使えるの?

魔理沙: 使える。Hub から load_tool だ。

from smolagents import CodeAgent, InferenceClientModel, load_tool

image_tool = load_tool("m-ric/text-to-image", trust_remote_code=True)
agent = CodeAgent(tools=[image_tool], model=InferenceClientModel())

⚠️ trust_remote_code=True信頼できるリポジトリだけ に使え(第 7・20 章)。

flowchart LR
  Prompt[user_prompt] --> Agent[CodeAgent]
  Agent --> Improve[プロンプト改善]
  Improve --> Space[text-to-image Space]
  Space --> Image[生成画像]
  Image --> Agent
  Agent --> Answer[final_answer]

19.4 additional_args でユーザープロンプトを渡す

魔理沙: 公式ツールチュートリアルでも、改善前のプロンプトadditional_args で渡している。

agent.run(
    "Improve this prompt, then generate an image of it.",
    additional_args={"user_prompt": "A rabbit wearing a space suit"},
)

日本語版タスク例:

task = """
additional_args の user_prompt を英語の画像プロンプトに改善し、
画像生成ツールで1枚生成して、改善後プロンプトを回答に含めてください。
"""
agent.run(task, additional_args={"user_prompt": "うさぎが宇宙服を着ている"})

19.5 音声 — URL を additional_args で渡す(任意)

霊夢: 音声ファイルはどう渡すの?

魔理沙: URL やローカルパスを additional_args に入れ、Transcriberadd_base_tools=True 時など)で文字起こしさせる。

from smolagents import CodeAgent, InferenceClientModel

agent = CodeAgent(
    tools=[],
    model=InferenceClientModel(),
    add_base_tools=True,
)

audio_url = "https://cdn-media.huggingface.co/speech_samples/sample1.flac"
agent.run(
    "audio_url を文字起こしし、3文で日本語要約して",
    additional_args={"audio_url": audio_url},
)

霊夢: 長い会議録音はコストが……

魔理沙: 事前にチャンク分割や専用 STT API を検討しろ。第 21 章のコスト試算ともセットだ。


19.6 マルチモーダル時の注意 ⚠️

項目 注意
著作権 生成画像・入力画像の利用範囲
PII 音声・画像に個人情報が含まれないか
サイズ 巨大ファイルは API 制限に抵触
モデル VL / 音声対応 ID を Inference で確認
# 悪い例: 巨大ファイルを丸ごと base64 でプロンプトに埋め込む
# 良い例: パス・URL を additional_args で渡しツール/コードで処理

🖥️ ハンズオン 19-1 — 画像プロンプト改善 + 生成

霊夢: かわいいうさぎ、出してみたいわ!

魔理沙: 📦 examples/ch19/image_prompt_loop.py だ。HF_TOKEN 必須。

export HF_TOKEN="hf_..."
pip install 'smolagents[toolkit]'
python examples/ch19/image_prompt_loop.py
# examples/ch19/image_prompt_loop.py(リポジトリ同梱・全文)
"""第19章: 画像生成 Space ツール + プロンプト改善ループ(HF_TOKEN 要)"""
import os

from smolagents import CodeAgent, InferenceClientModel, load_tool

def main() -> None:
    if not os.environ.get("HF_TOKEN"):
        raise SystemExit("HF_TOKEN を設定してください(Hub ツール読み込み用)")

    image_tool = load_tool("m-ric/text-to-image", trust_remote_code=True)
    model = InferenceClientModel()
    agent = CodeAgent(tools=[image_tool], model=model)

    user_prompt = "うさぎが宇宙服を着ているイラスト"
    task = """
    additional_args の user_prompt を読み、英語の画像生成プロンプトに改善してから
    画像生成ツールで1枚生成し、改善したプロンプトを最終回答に含めてください。
    """

    result = agent.run(task, additional_args={"user_prompt": user_prompt})
    print("=== 最終回答 ===")
    print(result)

if __name__ == "__main__":
    main()

霊夢: Space ツールのログ、時間かかるわね……

魔理沙: 正常だ。タイムアウトは第 21 章で扱う。


🖥️ ハンズオン 19-2 — (任意)ビジョン additional_args

📦 examples/ch19/vision_additional_args.py

# 任意: 手元の画像を sample.png として配置
cp /path/to/photo.jpg examples/ch19/sample.png
python examples/ch19/vision_additional_args.py
# examples/ch19/vision_additional_args.py(リポジトリ同梱・全文)
"""第19章: ビジョン対応モデル + additional_args で画像パスを渡すデモ"""
from pathlib import Path

from smolagents import CodeAgent, InferenceClientModel

# ビジョン対応モデル ID は利用可能なものに差し替えてください
VISION_MODEL_ID = "Qwen/Qwen2.5-VL-7B-Instruct"

def main() -> None:
    image_path = Path(__file__).resolve().parent / "sample.png"
    if not image_path.exists():
        print(
            "任意: examples/ch19/sample.png を置くとビジョンデモが動きます。"
            " 今回はパスの渡し方だけ表示します。"
        )
        print(f"additional_args 例: image_path={image_path}")
        return

    model = InferenceClientModel(model_id=VISION_MODEL_ID)
    agent = CodeAgent(
        tools=[],
        model=model,
        additional_authorized_imports=["PIL"],
    )

    task = """
    additional_args の image_path にある画像を開き、
    写っている物体を3つ日本語で列挙して final_answer してください。
    Pillow が必要なら import してよい。
    """

    result = agent.run(
        task,
        additional_args={"image_path": str(image_path)},
    )
    print("=== 最終回答 ===")
    print(result)

if __name__ == "__main__":
    main()

画像が無い場合は パスの渡し方だけ 表示される。


🖥️ ハンズオン 19-3 — (任意)音声要約

📦 examples/ch19/audio_summary_optional.py

python examples/ch19/audio_summary_optional.py
# examples/ch19/audio_summary_optional.py(リポジトリ同梱・全文)
"""第19章(任意): 音声 URL を additional_args で渡して要約するデモ"""
import os

from smolagents import CodeAgent, InferenceClientModel, load_tool

def main() -> None:
    if not os.environ.get("HF_TOKEN"):
        raise SystemExit("HF_TOKEN を設定してください")

    agent = CodeAgent(
        tools=[],  # 実運用では add_base_tools=True や Transcriber を明示
        model=InferenceClientModel(),
        add_base_tools=True,
    )

    # 公開サンプル音声 URL(差し替え可)
    audio_url = "https://cdn-media.huggingface.co/speech_samples/sample1.flac"

    task = """
    additional_args の audio_url の音声を文字起こしし、
    3文で日本語要約して final_answer してください。
    利用可能なら音声転写ツールを使うこと。
    """

    result = agent.run(task, additional_args={"audio_url": audio_url})
    print("=== 最終回答 ===")
    print(result)

if __name__ == "__main__":
    main()

19.7 Gradio でマルチモーダル UI(概要)

魔理沙: 第 13 章の GradioUI(agent).launch() にファイルアップロードを足す構成もある。本章では API レベルに集中する。

from smolagents import GradioUI

# GradioUI(agent).launch()  # ブラウザで対話

19.8 よくあるエラー ⚠️

モデルが画像非対応

... does not support image inputs ...

対処: InferenceClientModel(model_id=...) を VL モデルに変更。

Hub ツール読み込み失敗

trust_remote_code ...
load_tool("org/repo", trust_remote_code=True)

ModuleNotFoundError: PIL

pip install pillow

19.9 本章のまとめ

霊夢: まとめるわ。

  1. 入力は VL モデル + additional_args のパス/URL
  2. 生成は Hub Space ツール + プロンプト改善ループ
  3. 音声は Transcriber + audio_url(任意)
  4. trust_remote_code とコストに注意

✅ 章末チェックリスト

  • [ ] VL 対応モデル ID を確認した
  • [ ] additional_argsuser_prompt または image_path を渡した
  • [ ] image_prompt_loop.py を実行した(または Hub 制限でスキップ理由を記録)
  • [ ] trust_remote_code の意味を説明できる
  • [ ] 入力メディアに PII が無いことを確認した

次章へ

霊夢: うさぎ、宇宙……テンション上がるわ!

魔理沙: 次は自分のエージェントを世界に公開する章だぜ。


第20章 Hub にエージェントを公開する — push と from_hub

本章のゴール: agent.push_to_hub() で Space にエージェントを公開する流れを理解し、CodeAgent.from_hub()公開前セキュリティチェックリスト を身につける。


20.1 なぜ Hub に載せる?

霊夢: 手元で動けばいいんじゃないの?

魔理沙: チーム共有・デモ・再現性のために Gradio Space として載せられる。ツールは第 7 章、エージェント全体 は本章だ。

共有単位 API
ツール Tool.push_to_hub() / load_tool()
エージェント agent.push_to_hub() / CodeAgent.from_hub()
flowchart LR
  Local[ローカル CodeAgent] -->|push_to_hub| Space[HF Space Gradio]
  Space -->|from_hub| Other[他環境で再利用]

20.2 push_to_hub の流れ

魔理沙: 内部ではエージェントを 一時ディレクトリに save して Space リポジトリ に upload する。

from smolagents import CodeAgent, InferenceClientModel

agent = CodeAgent(
    tools=[],
    model=InferenceClientModel(),
    name="my_research_agent",
    description="Web 調査デモ用",
)

repo_url = agent.push_to_hub(
    "YOUR_USER/my-research-agent",
    commit_message="Upload agent from yukkuri-smolagents ch20",
    private=False,
)
print(repo_url)

霊夢: YOUR_USER自分の Hugging Face ユーザー名 ね。GitHub の hiromichinomata/yukkuri-smolagents とは別物よ。

魔理沙: その通り。環境変数に逃がすと安全だ。📦 examples/ch20/push_agent_to_hub.py

export HF_TOKEN="hf_..."
export SMOLAGENTS_DEMO_REPO="yourname/yukkuri-demo-agent"
python examples/ch20/push_agent_to_hub.py
# examples/ch20/push_agent_to_hub.py(リポジトリ同梱・全文)
"""第20章: エージェントを Hub Space に push する(任意・HF_TOKEN 要)"""
import os

from smolagents import CodeAgent, InferenceClientModel

# 自分の Hugging Face ユーザー名に置き換えてから実行(GitHub 本書 repo とは別)
REPO_ID = os.environ.get("SMOLAGENTS_DEMO_REPO", "YOUR_USER/yukkuri-demo-agent")

def build_agent() -> CodeAgent:
    model = InferenceClientModel()
    return CodeAgent(
        tools=[],
        model=model,
        name="yukkuri_demo",
        description="本書第20章用の最小デモエージェント",
    )

def main() -> None:
    if not os.environ.get("HF_TOKEN"):
        raise SystemExit("HF_TOKEN を設定してください")

    if "YOUR_USER" in REPO_ID:
        print("環境変数 SMOLAGENTS_DEMO_REPO に push 先 repo_id を設定してください")
        print('例: export SMOLAGENTS_DEMO_REPO="yourname/yukkuri-demo-agent"')
        return

    agent = build_agent()
    url = agent.push_to_hub(
        REPO_ID,
        commit_message="Upload yukkuri-smolagents ch20 demo agent",
        private=False,
    )
    print(f"Pushed agent Space: {url}")

if __name__ == "__main__":
    main()

20.3 from_hub で読み込む

霊夢: 他人のエージェントも動かせるの?

魔理沙: 信頼できる場合だけ だ。trust_remote_code=True は「リモートコード実行を承知した」意思表示。

from smolagents import CodeAgent

agent = CodeAgent.from_hub(
    "m-ric/agents_course_agent",
    trust_remote_code=True,
)

print(agent.run("1+2+3 の合計は?"))

📦 examples/ch20/load_agent_from_hub.py

python examples/ch20/load_agent_from_hub.py
# examples/ch20/load_agent_from_hub.py(リポジトリ同梱・全文)
"""第20章: Hub からエージェントを読み込む(trust_remote_code 必須)"""
import os

from smolagents import CodeAgent

def main() -> None:
    repo_id = os.environ.get(
        "SMOLAGENTS_DEMO_REPO",
        "m-ric/agents_course_agent",  # 公開デモの例(差し替え可)
    )

    agent = CodeAgent.from_hub(
        repo_id,
        trust_remote_code=True,
    )

    task = "1+2+3 の合計を計算して答えだけ返して"
    result = agent.run(task)
    print("=== 最終回答 ===")
    print(result)

if __name__ == "__main__":
    main()

⚠️ 第 7 章の load_tool と同じ 信頼境界 だ。


20.4 Space としての構成要素

魔理沙: push されるとだいたい次が含まれる。

ファイル 役割
エージェント設定 モデル ID、ツール一覧
requirements.txt 依存
Gradio UI チャット形式のデモ

README を Hub 上で整えると 再現性 が上がる。

# my-research-agent

- モデル: InferenceClientModel(...)
- 必要な秘密情報: HF_TOKEN(Space の Secrets)
- サンプル質問: 「smolagents の最新情報を調べて」

20.5 バージョン管理と更新

# 設定を変えたあと再 push
agent.push_to_hub(
    "YOUR_USER/my-research-agent",
    commit_message="Add visit_webpage tool",
)

霊夢: create_pr=True みたいなのもある?

魔理沙: API には PR 作成オプション がある。チーム運用では main 直 commit より PR の方が安全なことが多い。

agent.push_to_hub(
    "org/team-agent",
    commit_message="WIP: new tool",
    create_pr=True,
)

20.6 公開前セキュリティチェックリスト ⚠️

霊夢: 公開する前に見るリスト、欲しいわ。

魔理沙: 本章の核心のひとつだ。

コード・ツール

  • [ ] ツールに ハードコードされた API キー が無い
  • [ ] trust_remote_code 対象の 第三者ツール を棚卸しした
  • [ ] エージェントが実行できる import が最小限(additional_authorized_imports
  • [ ] 本番 DB・社内 API への 直結 が無い(デモ用エンドポイントのみ)

Space・Hub 設定

  • [ ] Space の SecretsHF_TOKEN 等を設定(リポジトリに平文コミットしない)
  • [ ] private=True で社内限定公開を検討した
  • [ ] README に 想定用途と禁止事項 を書いた

実行環境

  • [ ] サンドボックス(第 12 章)が要る用途では executor_type を検討した
  • [ ] レート制限・コスト上限(第 21 章)を理解した
# 悪い例(絶対に push しない)
OPENAI_API_KEY = "sk-live-xxxxxxxx"

# 良い例
import os
token = os.environ["HF_TOKEN"]

🖥️ ハンズオン 20-1 — 最小エージェントを push(任意)

霊夢: 本当に世界に出すの……緊張するわ。

魔理沙: 任意だ。repo 名を決めてから実行しろ。

export HF_TOKEN="hf_..."
export SMOLAGENTS_DEMO_REPO="myname/yukkuri-demo-agent"
python examples/ch20/push_agent_to_hub.py
# examples/ch20/push_agent_to_hub.py(リポジトリ同梱・全文)
"""第20章: エージェントを Hub Space に push する(任意・HF_TOKEN 要)"""
import os

from smolagents import CodeAgent, InferenceClientModel

# 自分の Hugging Face ユーザー名に置き換えてから実行(GitHub 本書 repo とは別)
REPO_ID = os.environ.get("SMOLAGENTS_DEMO_REPO", "YOUR_USER/yukkuri-demo-agent")

def build_agent() -> CodeAgent:
    model = InferenceClientModel()
    return CodeAgent(
        tools=[],
        model=model,
        name="yukkuri_demo",
        description="本書第20章用の最小デモエージェント",
    )

def main() -> None:
    if not os.environ.get("HF_TOKEN"):
        raise SystemExit("HF_TOKEN を設定してください")

    if "YOUR_USER" in REPO_ID:
        print("環境変数 SMOLAGENTS_DEMO_REPO に push 先 repo_id を設定してください")
        print('例: export SMOLAGENTS_DEMO_REPO="yourname/yukkuri-demo-agent"')
        return

    agent = build_agent()
    url = agent.push_to_hub(
        REPO_ID,
        commit_message="Upload yukkuri-smolagents ch20 demo agent",
        private=False,
    )
    print(f"Pushed agent Space: {url}")

if __name__ == "__main__":
    main()

成功時:

Pushed agent Space: https://huggingface.co/spaces/myname/yukkuri-demo-agent

20.7 push 前のローカル検証

from smolagents import CodeAgent, InferenceClientModel

agent = CodeAgent(tools=[], model=InferenceClientModel())
assert agent.run("2+2 は?")  # ローカルで動作確認
# agent.push_to_hub(...)
huggingface-cli whoami
user: your_hf_username

20.8 よくあるエラー ⚠️

401 Unauthorized

export HF_TOKEN="hf_..."
huggingface-cli login

リポジトリ名の形式

Repo id must be in the form 'namespace/name'
agent.push_to_hub("myname/yukkuri-demo-agent")  # OK
# agent.push_to_hub("yukkuri-demo-agent")       # NG

from_hub で trust 未設定

... trust_remote_code=True ...

20.9 本章のまとめ

霊夢: 整理するわ。

  1. push_to_hubGradio Space として共有
  2. from_hubtrust_remote_code が前提
  3. 公開前チェックリストで 鍵・ツール・権限 を点検
  4. README と Secrets で運用を楽にする

✅ 章末チェックリスト

  • [ ] push_to_hub の引数(repo_id, private, commit_message)を説明できる
  • [ ] from_hub(..., trust_remote_code=True) のリスクを説明できる
  • [ ] 公開前セキュリティチェックリストを自分用にコピーした
  • [ ] API キーをソースに含めていない
  • [ ] (任意)デモ Space を push した

次章へ

魔理沙: 公開は 信頼の設計 だ。次は本番運用の現実に入るぜ。


第21章 本番運用のヒント — 秘密・制限・観測・HITL

本章のゴール: シークレット管理レート制限とリトライオブザーバビリティ人間承認(HITL) の実務パターンをコードで押さえ、本番チェックリスト を完成させる。


21.1 本番とハンズオンの違い

霊夢: 手元では動いたのに、本番でコケるパターン、多そうね。

魔理沙: 典型はこの 4 つだ。

領域 手元 本番
秘密情報 .env 直書き シークレットマネージャ
外部 API たまに叩く レート制限・タイムアウト
デバッグ print 構造化ログ・メトリクス
危険操作 即実行 HITL で承認

21.2 API キーとシークレット管理 ⚠️

魔理沙: 原則は 環境変数、ローカルは .env、クラウドはマネージャ。

# ローカル開発(Git にコミットしない)
export HF_TOKEN="hf_..."
export OPENAI_API_KEY="sk-..."
import os

def require_env(name: str) -> str:
    value = os.environ.get(name)
    if not value:
        raise RuntimeError(f"{name} is not set")
    return value

hf_token = require_env("HF_TOKEN")

📦 examples/ch21/env_secrets_pattern.py

# examples/ch21/env_secrets_pattern.py(リポジトリ同梱・全文)
"""第21章: 環境変数と .env からシークレットを読むパターン"""
import os
from pathlib import Path

def load_hf_token() -> str:
    token = os.environ.get("HF_TOKEN")
    if token:
        return token

    env_file = Path(".env")
    if env_file.exists():
        for line in env_file.read_text(encoding="utf-8").splitlines():
            line = line.strip()
            if line.startswith("HF_TOKEN="):
                return line.split("=", 1)[1].strip().strip('"').strip("'")

    raise RuntimeError(
        "HF_TOKEN が未設定です。export HF_TOKEN=... または .env を使ってください"
    )

def main() -> None:
    token = load_hf_token()
    masked = token[:7] + "..." if len(token) > 10 else "(short)"
    print(f"HF_TOKEN loaded: {masked}")
    print("本番では AWS Secrets Manager / GCP Secret Manager 等を推奨")

if __name__ == "__main__":
    main()
環境 推奨
ローカル .env + .gitignore
CI GitHub Actions Secrets
K8s / Cloud Secrets Manager, Sealed Secrets
HF Space Space Settings → Secrets
.env
*.pem
secrets/

霊夢: ログにトークン出さないのも大事よね。

魔理沙: マスクしろ。

def mask_token(token: str) -> str:
    return token[:7] + "..." if len(token) > 10 else "***"

21.3 レート制限・リトライ・タイムアウト

魔理沙: Inference API は 429 が出る。指数バックオフが定石だ。

import time

def run_with_retry(fn, max_attempts=4, base_delay=2.0):
    for attempt in range(1, max_attempts + 1):
        try:
            return fn()
        except Exception as exc:
            if "rate limit" not in str(exc).lower() and "429" not in str(exc):
                raise
            delay = base_delay * (2 ** (attempt - 1))
            time.sleep(delay)
    raise RuntimeError("max attempts exceeded")

📦 examples/ch21/rate_limit_retry.py

# examples/ch21/rate_limit_retry.py(リポジトリ同梱・全文)
"""第21章: レート制限を想定したリトライラッパー"""
import time
from collections.abc import Callable
from typing import TypeVar

T = TypeVar("T")

def run_with_retry(
    fn: Callable[[], T],
    *,
    max_attempts: int = 4,
    base_delay_sec: float = 2.0,
) -> T:
    last_error: Exception | None = None
    for attempt in range(1, max_attempts + 1):
        try:
            return fn()
        except Exception as exc:  # noqa: BLE001 — デモ用に広く捕捉
            message = str(exc).lower()
            if "rate limit" not in message and "429" not in message:
                raise
            last_error = exc
            delay = base_delay_sec * (2 ** (attempt - 1))
            print(f"[retry] attempt={attempt} sleep={delay}s reason={exc}")
            time.sleep(delay)
    assert last_error is not None
    raise last_error

def main() -> None:
    calls = {"n": 0}

    def flaky_api() -> str:
        calls["n"] += 1
        if calls["n"] < 3:
            raise RuntimeError("Rate limit exceeded (demo)")
        return "ok"

    result = run_with_retry(flaky_api)
    print(f"result={result} calls={calls['n']}")

if __name__ == "__main__":
    main()

霊夢: タイムアウトは?

魔理沙: モデル側・HTTP クライアント側・ジョブ全体の 3 層 で決める。

# LiteLLM 等を使う場合のイメージ
# model = LiteLLMModel(..., timeout=60)
対策 用途
リトライ 一時的 429 / 5xx
サーキットブレーカ 連続失敗で停止
キュー 同時実行数の制限

21.4 オブザーバビリティ

魔理沙: smolagents は agent.logs にステップ履歴が残る。本番ではここから メトリクス を抜く。

def summarize_logs(agent):
    steps = 0
    input_tokens = 0
    output_tokens = 0
    for entry in agent.logs:
        step_type = getattr(entry, "step_type", None)
        if step_type == "action_step":
            steps += 1
        usage = getattr(entry, "token_usage", None)
        if usage:
            input_tokens += int(usage.get("input_tokens", 0))
            output_tokens += int(usage.get("output_tokens", 0))
    return {"steps": steps, "input_tokens": input_tokens, "output_tokens": output_tokens}

📦 examples/ch21/observability_log.py

python examples/ch21/observability_log.py
# examples/ch21/observability_log.py(リポジトリ同梱・全文)
"""第21章: agent.logs からステップ数・トークン目安を集計する"""
from smolagents import CodeAgent, InferenceClientModel

def summarize_logs(agent: CodeAgent) -> dict[str, int | float]:
    steps = 0
    input_tokens = 0
    output_tokens = 0

    for entry in agent.logs:
        step_type = getattr(entry, "step_type", None) or entry.get("step_type")
        if step_type == "action_step":
            steps += 1
        token_usage = getattr(entry, "token_usage", None) or entry.get("token_usage")
        if token_usage:
            input_tokens += int(token_usage.get("input_tokens", 0))
            output_tokens += int(token_usage.get("output_tokens", 0))

    return {
        "steps": steps,
        "input_tokens": input_tokens,
        "output_tokens": output_tokens,
    }

def main() -> None:
    agent = CodeAgent(tools=[], model=InferenceClientModel())
    agent.run("7 の階乗を計算して整数で答えて")
    metrics = summarize_logs(agent)
    print("=== observability summary ===")
    for key, value in metrics.items():
        print(f"{key}: {value}")

if __name__ == "__main__":
    main()
=== observability summary ===
steps: 2
input_tokens: 1234
output_tokens: 567

霊夢: コスト試算は?

魔理沙: モデルごとの 単価表 × token で概算する。正確な請求はプロバイダのダッシュボードを正とする。

PRICE_PER_1M_INPUT = 0.20   # 例: USD(モデルにより異なる)
PRICE_PER_1M_OUTPUT = 0.60

def estimate_cost_usd(input_tokens: int, output_tokens: int) -> float:
    return (
        input_tokens / 1_000_000 * PRICE_PER_1M_INPUT
        + output_tokens / 1_000_000 * PRICE_PER_1M_OUTPUT
    )

構造化ログ例:

import json
import logging

logger = logging.getLogger("smolagents.app")

def log_run_summary(metrics: dict) -> None:
    logger.info(json.dumps({"event": "agent_run_finished", **metrics}))

21.5 人間承認(HITL)の挿入ポイント

霊夢: 危ないツール、人間 OK 挟める?

魔理沙: 挟める。代表的な挿入ポイントは次の通り。

flowchart TD
  A[タスク開始] --> B{高リスクツール?}
  B -->|No| C[自動実行]
  B -->|Yes| D[人間承認 UI]
  D -->|拒否| E[中断・説明]
  D -->|承認| C
  C --> F[final_answer]
  1. ツール forward — 即席だが効く(本章デモ)
  2. マネージャーエージェント — 専門エージェント呼び出し前に承認(第 15 章)
  3. アプリ層agent.run の前後でワークフロー制御
from smolagents import Tool

class HumanApprovalTool(Tool):
    name = "dangerous_action"
    description = "承認が必要な操作(デモ)"
    inputs = {"command": {"type": "string", "description": "実行内容"}}
    output_type = "string"

    def forward(self, command: str) -> str:
        answer = input(f"実行しますか? [y/N] {command}: ")
        if answer.strip().lower() != "y":
            return "REJECTED"
        return f"APPROVED: {command}"

📦 examples/ch21/hitl_approval.py

python examples/ch21/hitl_approval.py
# examples/ch21/hitl_approval.py(リポジトリ同梱・全文)
"""第21章: ツール実行前に人間承認(HITL)を挟むラッパー"""
from smolagents import CodeAgent, InferenceClientModel, Tool

class HumanApprovalTool(Tool):
    name = "dangerous_action"
    description = "本番では承認が必要な操作のデモ。実行前に人間の y/n を求める。"
    inputs = {
        "command": {
            "type": "string",
            "description": "実行予定のコマンド説明",
        }
    }
    output_type = "string"

    def forward(self, command: str) -> str:
        print("\n=== HITL 承認リクエスト ===")
        print(command)
        answer = input("実行しますか? [y/N]: ").strip().lower()
        if answer != "y":
            return "REJECTED: 人間が実行を拒否しました"
        return f"APPROVED: {command} を実行しました(デモ)"

def main() -> None:
    agent = CodeAgent(
        tools=[HumanApprovalTool()],
        model=InferenceClientModel(),
    )
    result = agent.run(
        "dangerous_action ツールで「本番 DB をバックアップする」と報告してから結果を要約して"
    )
    print("=== 最終回答 ===")
    print(result)

if __name__ == "__main__":
    main()

本番では input() の代わりに Slack ボタン・チケットシステム と連携する。


21.6 max_steps と interrupt

from smolagents import CodeAgent, InferenceClientModel

agent = CodeAgent(
    tools=[],
    model=InferenceClientModel(),
    max_steps=10,
)

# 別スレッドから agent.interrupt() で現在ステップ後に停止(第13章)

霊夢: 無限ループ怖いわ……

魔理沙: max_steps + コスト上限 + アラートの三点セットだ。


🖥️ ハンズオン 21-1 — シークレット読み込み

python examples/ch21/env_secrets_pattern.py
# examples/ch21/env_secrets_pattern.py(リポジトリ同梱・全文)
"""第21章: 環境変数と .env からシークレットを読むパターン"""
import os
from pathlib import Path

def load_hf_token() -> str:
    token = os.environ.get("HF_TOKEN")
    if token:
        return token

    env_file = Path(".env")
    if env_file.exists():
        for line in env_file.read_text(encoding="utf-8").splitlines():
            line = line.strip()
            if line.startswith("HF_TOKEN="):
                return line.split("=", 1)[1].strip().strip('"').strip("'")

    raise RuntimeError(
        "HF_TOKEN が未設定です。export HF_TOKEN=... または .env を使ってください"
    )

def main() -> None:
    token = load_hf_token()
    masked = token[:7] + "..." if len(token) > 10 else "(short)"
    print(f"HF_TOKEN loaded: {masked}")
    print("本番では AWS Secrets Manager / GCP Secret Manager 等を推奨")

if __name__ == "__main__":
    main()

🖥️ ハンズオン 21-2 — リトライデモ

python examples/ch21/rate_limit_retry.py
# examples/ch21/rate_limit_retry.py(リポジトリ同梱・全文)
"""第21章: レート制限を想定したリトライラッパー"""
import time
from collections.abc import Callable
from typing import TypeVar

T = TypeVar("T")

def run_with_retry(
    fn: Callable[[], T],
    *,
    max_attempts: int = 4,
    base_delay_sec: float = 2.0,
) -> T:
    last_error: Exception | None = None
    for attempt in range(1, max_attempts + 1):
        try:
            return fn()
        except Exception as exc:  # noqa: BLE001 — デモ用に広く捕捉
            message = str(exc).lower()
            if "rate limit" not in message and "429" not in message:
                raise
            last_error = exc
            delay = base_delay_sec * (2 ** (attempt - 1))
            print(f"[retry] attempt={attempt} sleep={delay}s reason={exc}")
            time.sleep(delay)
    assert last_error is not None
    raise last_error

def main() -> None:
    calls = {"n": 0}

    def flaky_api() -> str:
        calls["n"] += 1
        if calls["n"] < 3:
            raise RuntimeError("Rate limit exceeded (demo)")
        return "ok"

    result = run_with_retry(flaky_api)
    print(f"result={result} calls={calls['n']}")

if __name__ == "__main__":
    main()

🖥️ ハンズオン 21-3 — logs 集計

export HF_TOKEN="hf_..."
python examples/ch21/observability_log.py
# examples/ch21/observability_log.py(リポジトリ同梱・全文)
"""第21章: agent.logs からステップ数・トークン目安を集計する"""
from smolagents import CodeAgent, InferenceClientModel

def summarize_logs(agent: CodeAgent) -> dict[str, int | float]:
    steps = 0
    input_tokens = 0
    output_tokens = 0

    for entry in agent.logs:
        step_type = getattr(entry, "step_type", None) or entry.get("step_type")
        if step_type == "action_step":
            steps += 1
        token_usage = getattr(entry, "token_usage", None) or entry.get("token_usage")
        if token_usage:
            input_tokens += int(token_usage.get("input_tokens", 0))
            output_tokens += int(token_usage.get("output_tokens", 0))

    return {
        "steps": steps,
        "input_tokens": input_tokens,
        "output_tokens": output_tokens,
    }

def main() -> None:
    agent = CodeAgent(tools=[], model=InferenceClientModel())
    agent.run("7 の階乗を計算して整数で答えて")
    metrics = summarize_logs(agent)
    print("=== observability summary ===")
    for key, value in metrics.items():
        print(f"{key}: {value}")

if __name__ == "__main__":
    main()

21.7 本番チェックリスト

魔理沙: 章末用に、デプロイ前の 統合リスト だ。

セキュリティ

  • [ ] シークレットは環境変数/マネージャのみ(ソース・ログに無し)
  • [ ] コード実行はサンドボックス or 制限付きローカル(第 12 章)
  • [ ] Hub ツールは信頼済みのみ trust_remote_code=True
  • [ ] SQL / シェルは読み取り専用・ホワイトリスト(第 18 章)

信頼性

  • [ ] レート制限用リトライと上限試行回数
  • [ ] max_steps とタイムアウト
  • [ ] 高リスク操作に HITL

運用

  • [ ] agent.logs または同等のトレース保存
  • [ ] トークン数・ステップ数のダッシュボード
  • [ ] コストアラート(日次上限)

品質

  • [ ] タスク文テンプレートのバージョン管理(第 14 章)
  • [ ] 回帰テスト用の固定プロンプトセット

21.8 よくあるエラー ⚠️

HF_TOKEN 未設定

Unauthorized ...
export HF_TOKEN="hf_..."

レート制限ループ

対処: バックオフ上限を設ける。モデル分散。

HITL で stdin が無い

対処: CI では HumanApprovalTool をモックに差し替える。

class AutoApproveTool(HumanApprovalTool):
    def forward(self, command: str) -> str:
        return f"APPROVED(test): {command}"

21.9 本章のまとめ

霊夢: 本番は「動く」だけじゃ足りないのね。

魔理沙: 秘密・制限・見える化・承認 の4本柱だ。次章で本書を締める。


✅ 章末チェックリスト

  • [ ] .env を Git に含めていない
  • [ ] env_secrets_pattern.py でトークン読み込みを確認した
  • [ ] rate_limit_retry.py の指数バックオフを理解した
  • [ ] observability_log.py で steps / tokens を取得した
  • [ ] hitl_approval.py で承認フローを体験した(または AutoApprove で CI 想定)
  • [ ] 本番チェックリストを自分のプロジェクトに貼った

次章へ

魔理沙: ここまで来たら、あとは現場のツールを足すだけだぜ。


第22章 エピローグ — 次に学ぶこと

本章のゴール: 本書の到達点を振り返りsmolagents ソースの読み方他フレームワークとの位置づけコミュニティ への道筋を掴み、次の学習計画を立てる。


22.1 これで一人立ちできる?

霊夢: 22 章も読んだわ。これで一人でエージェント作れる?

魔理沙: 土台は十分 だ。あとは 自分の業務用ツールを1つずつ足す 反復だ。公式 Examples と Issue を友達にしろ。

本書で扱ったこと
インストール・モデル 0–4
ツール・Hub・MCP 5–8
CodeAgent / 安全実行 9–12
品質・マルチエージェント 14–16
応用(Web・SQL・画像) 17–19
公開・本番 20–21

22.2 smolagents のソースを読む

魔理沙: 「軽量」の意味は ファイル行数 で実感するのが早い。📦 examples/ch22/explore_source.py

pip install smolagents
python examples/ch22/explore_source.py
# examples/ch22/explore_source.py(リポジトリ同梱・全文)
"""第22章: smolagents のコアが薄いことを確認するスクリプト"""
import inspect
from pathlib import Path

import smolagents
from smolagents import CodeAgent

def main() -> None:
    pkg_root = Path(smolagents.__file__).resolve().parent
    agents_py = pkg_root / "agents.py"
    line_count = len(agents_py.read_text(encoding="utf-8").splitlines())
    print(f"smolagents version: {smolagents.__version__}")
    print(f"agents.py lines (approx): {line_count}")
    print(f"CodeAgent defined in: {inspect.getfile(CodeAgent)}")
    print("次の学習: GitHub の examples/ と自分の業務ツールを1つずつ足す")

if __name__ == "__main__":
    main()
smolagents version: 1.x.x
agents.py lines (approx): 1700
CodeAgent defined in: .../smolagents/agents.py

霊夢: 意外とコンパクトね。

魔理沙: まずは次の順で読むと迷子になりにくい。

# 1. エージェントの入口
from smolagents import CodeAgent
import inspect
print(inspect.getfile(CodeAgent))
# 2. ローカルに clone して読む場合
git clone https://github.com/huggingface/smolagents.git
cd smolagents
ファイル 内容
src/smolagents/agents.py CodeAgent, run, push_to_hub
src/smolagents/tools.py @tool, Tool
src/smolagents/models.py *Model
src/smolagents/local_python_executor.py サンドボックス実行
# 3. run の流れを追うときのブレークポイント候補(IDE)
# - MultiStepAgent.run
# - CodeAgent.step
# - LocalPythonExecutor

22.3 公式リソースへの橋渡し

霊夢: 次に開く URL、教えて。

魔理沙: 優先度順だ。

1. https://huggingface.co/docs/smolagents/en/index
2. https://huggingface.co/docs/smolagents/en/guided_tour
3. https://huggingface.co/docs/smolagents/en/tutorials/building_good_agents
4. https://huggingface.co/blog/smolagents
5. https://github.com/huggingface/smolagents/tree/main/examples

ブログで 設計思想 を掴む:

# 例: GAIA マルチエージェント構成の解説
# https://huggingface.co/blog/beating-gaia

22.4 他フレームワークとの位置づけ(簡潔比較)

霊夢: LangGraph とか AutoGen とは違うの?

魔理沙: 目的が違う。全部を使う必要はない。1 段落で整理する。

フレームワーク 強み smolagents との関係
smolagents 薄い抽象・CodeAgent・HF 連携 本書の主役
LangGraph グラフ型ワークフロー・状態機械 複雑フローは LangGraph、単体エージェントは smolagents、併用も可
AutoGen 会話型マルチエージェント 役割分担の思想は第 15–16 章と共通
LangChain 広いエコシステム Tool.from_langchain() で橋渡し(第 8 章)
# smolagents: 最小のエージェント定義
from smolagents import CodeAgent, InferenceClientModel

agent = CodeAgent(tools=[my_tool], model=InferenceClientModel())
agent.run("業務タスク")
# 複雑な分岐はアプリ層 or LangGraph 等で orchestration
# smolagents は「1エージェント=1専門家」として埋め込むイメージ

霊夢: 押し付けないの、助かるわ。


22.5 自分のプロジェクトへの展開ステップ

魔理沙: 推奨ロードマップだ。

flowchart TD
  A[1. 業務タスクを1つ選ぶ] --> B[2. ツール1つ実装]
  B --> C[3. CodeAgent で PoC]
  C --> D[4. 第12章の安全策]
  D --> E[5. 第21章の本番チェック]
  E --> F[6. 必要なら Hub 公開]
# ステップ2の例: 社内 API ラッパーツール
from smolagents import tool

@tool
def lookup_customer(customer_id: str) -> str:
    """
    社内 CRM から顧客名を返す(読み取り専用)。

    Args:
        customer_id: 顧客 ID。
    """
    # return crm_client.get_name(customer_id)
    return f"Customer {customer_id}"

22.6 コミュニティ

霊夢: 詰まったとき、どこで聞けばいい?

魔理沙: このあたりだ。

場所 URL / 方法
GitHub Issues https://github.com/huggingface/smolagents/issues
HF フォーラム https://discuss.huggingface.co/
Discord Hugging Face 公式 Discord(#smolagents 等)
Hub ツール・Space を検索・共有

Issue を切るときのテンプレ:

## 環境
- smolagents version:
- Python:
- Model ID:

## 再現手順
1. ...
2. agent.run("...")

## 期待 / 実際
- 期待:
- 実際:

霊夢: 最小再現、大事ね。


🖥️ ハンズオン 22-1 — ソースの場所を確認

python examples/ch22/explore_source.py
# examples/ch22/explore_source.py(リポジトリ同梱・全文)
"""第22章: smolagents のコアが薄いことを確認するスクリプト"""
import inspect
from pathlib import Path

import smolagents
from smolagents import CodeAgent

def main() -> None:
    pkg_root = Path(smolagents.__file__).resolve().parent
    agents_py = pkg_root / "agents.py"
    line_count = len(agents_py.read_text(encoding="utf-8").splitlines())
    print(f"smolagents version: {smolagents.__version__}")
    print(f"agents.py lines (approx): {line_count}")
    print(f"CodeAgent defined in: {inspect.getfile(CodeAgent)}")
    print("次の学習: GitHub の examples/ と自分の業務ツールを1つずつ足す")

if __name__ == "__main__":
    main()

🖥️ ハンズオン 22-2 — 学習計画を書く

魔理沙: 次の 30 日プランを 自分用に 埋めろ(コピペ用)。

## 私の smolagents 30日プラン

- Week 1: 業務ドメインのタスク1つ + ツール1つ
- Week 2: 第12章サンドボックス適用可否の判断
- Week 3: 第21章チェックリストでステージング deploy
- Week 4: Hub または社内レジストリでチーム共有

22.7 本書の examples マップ

リポジトリ: https://github.com/hiromichinomata/yukkuri-smolagents

ディレクトリ
0–17 examples/ch00/examples/ch17/
18 examples/ch18/(Text-to-SQL)
19 examples/ch19/(マルチモーダル)
20 examples/ch20/(Hub push / from_hub)
21 examples/ch21/(本番運用)
22 examples/ch22/(ソース探索)
git clone https://github.com/hiromichinomata/yukkuri-smolagents.git
cd yukkuri-smolagents
ls examples/ch*/

22.8 最後に

霊夢: 長い旅だったわ。エージェントって、もう少し身近に感じる。

魔理沙: 覚えておけ。

  1. シンプルに始める(ツールを増やしすぎない)
  2. ログを読むagent.logs
  3. 安全と本番 を後回しにしない
  4. コミュニティ に返す(良いツールは Hub へ)

霊夢: 公式 Examples、これから追いかけるわ!

魔理沙: ああ。本書はここまでだ。ゆっくりしていこうな。


✅ 章末チェックリスト(本書総仕上げ)

  • [ ] explore_source.pyagents.py の場所を確認した
  • [ ] 公式ドキュメントの Guided tour をブックマークした
  • [ ] 30 日プラン(または同等)を書いた
  • [ ] 業務タスク + ツール 1 つの PoC 題材を決めた
  • [ ] GitHub Issues の書き方テンプレを保存した
  • [ ] 第 21 章本番チェックリストをデプロイ手順に組み込んだ

付録・目次へ

リンク 内容
目次 全章一覧
付録A クイックリファレンス
付録B トラブルシューティング
付録C ハンズオン解答
付録D 用語集
付録E 公式リンク集
第0章 プロローグ
第21章 本番運用

霊夢: おつかれさま! また新しいエージェント作ったら見せるわね。

魔理沙: 待ってるぜ、霊夢。


付録A クイックリファレンス

本書を読み進めながら 辞書代わり に使う。詳細は各章と 公式 API Reference を参照。


A.1 最小エージェント

from smolagents import CodeAgent, InferenceClientModel

model = InferenceClientModel()
agent = CodeAgent(tools=[], model=model)
result = agent.run("タスク文")
print(result)
# 標準ツール付き
agent = CodeAgent(
    tools=[],
    model=model,
    add_base_tools=True,
)

A.2 主要クラス一覧

エージェント

クラス 用途 本書
CodeAgent 行動を Python コード で書く 主役(第9章)
ToolCallingAgent JSON 形式のツール呼び出し 第10章
GradioUI Web UI で対話 第13章
from smolagents import CodeAgent, ToolCallingAgent, GradioUI

モデル(model= に渡す)

クラス 接続先 pip extra
InferenceClientModel HF Inference Providers (基本)
LiteLLMModel OpenAI / Anthropic / Ollama 等 litellm
TransformersModel ローカル transformers transformers
MLXModel Apple Silicon mlx-lm mlx-lm
AzureOpenAIModel Azure OpenAI openai
AmazonBedrockModel AWS Bedrock bedrock
from smolagents import (
    InferenceClientModel,
    LiteLLMModel,
    TransformersModel,
    MLXModel,
    AzureOpenAIModel,
    AmazonBedrockModel,
)

ツール

クラス / 関数 用途
@tool 関数をツール化
Tool サブクラスでツール定義
load_tool() Hub からツール読み込み
ToolCollection ツール集合(Hub / MCP)
MCPClient MCP サーバー接続
DuckDuckGoSearchTool / WebSearchTool Web 検索
VisitWebpageTool ページ取得
from smolagents import tool, Tool, load_tool, ToolCollection, MCPClient
from smolagents import WebSearchTool, VisitWebpageTool

A.3 CodeAgent よく使う引数

引数 説明
tools list ツールインスタンスのリスト
model Model LLM バックエンド
add_base_tools bool 標準ツールボックスを追加
managed_agents list サブエージェント(マルチエージェント)
max_steps int 最大ステップ数
verbosity_level int ログ詳細度
additional_authorized_imports list[str] コード実行で許可する import
executor_type str "local" / "e2b" / "docker"
final_answer_checks list[callable] 最終回答の検証関数
name / description str マネージドエージェント用メタデータ
from smolagents import CodeAgent, InferenceClientModel

agent = CodeAgent(
    tools=[],
    model=InferenceClientModel(),
    add_base_tools=True,
    max_steps=10,
    verbosity_level=2,
    additional_authorized_imports=["requests"],
)

agent.run() の引数

引数 説明
第1引数 task タスク文字列
reset False でメモリを引き継ぐ(会話継続)
additional_args タスクと一緒に渡す dict(URL・ファイルパス等)
stream ストリーミング(モデル・API による)
agent.run("質問", reset=False, additional_args={"url": "https://example.com"})

A.4 ToolCallingAgent よく使う引数

CodeAgent と共通: tools, model, add_base_tools, managed_agents, max_steps, verbosity_level, name, description

ないもの: additional_authorized_imports(コード実行しないため)

from smolagents import ToolCallingAgent, WebSearchTool, InferenceClientModel

agent = ToolCallingAgent(
    tools=[WebSearchTool()],
    model=InferenceClientModel(),
)

A.5 @tool デコレータ最小形

from smolagents import tool

@tool
def my_tool(arg: str) -> str:
    """
    ツールの説明(LLM 向け)。

    Args:
        arg: 引数の説明。
    """
    return f"result: {arg}"

Tool サブクラス:

from smolagents import Tool

class MyTool(Tool):
    name = "my_tool"
    description = "..."
    inputs = {"arg": {"type": "string", "description": "..."}}
    output_type = "string"

    def forward(self, arg: str) -> str:
        return f"result: {arg}"

A.6 実行後に触る属性・メソッド

名前 説明
agent.logs ステップごとの詳細ログ
agent.tools ツール名 → インスタンスの dict
agent.write_memory_to_messages() メモリをチャットメッセージ形式に
agent.interrupt() 実行中断(Gradio 等)
agent.push_to_hub() Hub にエージェント公開
CodeAgent.from_hub() Hub から読み込み
for step in agent.logs:
    print(step)
messages = agent.write_memory_to_messages()

A.7 環境変数一覧

変数 用途
HF_TOKEN Hugging Face API(Inference・Hub) 0, 2, 4
OPENAI_API_KEY OpenAI(LiteLLM 経由) 4, 10
ANTHROPIC_API_KEY Anthropic(LiteLLM) 4
AZURE_OPENAI_* Azure OpenAI 4
AWS_* Bedrock 4
E2B_API_KEY E2B サンドボックス 12
BL_API_KEY / BL_WORKSPACE Blaxel executor 12
# .env 例(Git にコミットしない)
HF_TOKEN=hf_...
OPENAI_API_KEY=sk-...
E2B_API_KEY=...
from dotenv import load_dotenv
load_dotenv()

A.8 pip extras 早見表

pip install 'smolagents[toolkit]'      # Web 検索等(本書デフォルト)
pip install 'smolagents[litellm]'      # LiteLLMModel
pip install 'smolagents[transformers]' # TransformersModel
pip install 'smolagents[openai]'     # AzureOpenAIModel
pip install 'smolagents[bedrock]'      # AmazonBedrockModel
pip install 'smolagents[mlx-lm]'       # MLXModel
pip install 'smolagents[gradio]'       # GradioUI

A.9 CLI 早見

# ワンショット
smolagent "タスク文" \
  --model-type InferenceClientModel \
  --model-id "Qwen/Qwen2.5-Coder-32B-Instruct" \
  --tools web_search \
  --imports "requests"

# 対話モード(引数なし)
smolagent

関連リンク

付録 内容
付録B トラブルシューティング
付録D 用語集
付録E 公式リンク集
目次 全章

付録B トラブルシューティング

エラーメッセージから 原因 → 対処 へたどる索引。霊夢向けに短く、魔理沙向けにコマンド付き。


B.1 診断の基本手順

# 1. 仮想環境
which python
python -V

# 2. パッケージ
python -c "import smolagents; print(smolagents.__version__)"

# 3. トークン(値は表示しない)
python -c "import os; print('HF_TOKEN set:', bool(os.environ.get('HF_TOKEN')))"
# 4. 最小再現
from smolagents import CodeAgent, InferenceClientModel

agent = CodeAgent(tools=[], model=InferenceClientModel())
print(agent.run("1+1を計算して"))

B.2 ImportError / ModuleNotFoundError

No module named 'smolagents'

原因: venv 未 activate、別 Python に install した。

source .venv/bin/activate
pip install 'smolagents[toolkit]'
which python

No module named 'litellm' / transformers / mcp

原因: optional extra 未インストール。

pip install 'smolagents[litellm]'
pip install mcp pydantic   # 第8章 MCP
pip install requests beautifulsoup4  # 第9章 Web

cannot import name 'InferenceClientModel'

原因: 古い smolagents(HfApiModel 時代の記事を参照している等)。

pip install -U 'smolagents[toolkit]'
python -c "from smolagents import InferenceClientModel; print('OK')"

B.3 認証エラー(401 / 403 / Unauthorized)

HF Inference / Hub

401 Unauthorized
Invalid username or password
export HF_TOKEN="hf_..."   # Read トークン
# 再ログイン確認: huggingface-cli whoami
from smolagents import InferenceClientModel
import os

model = InferenceClientModel(token=os.environ["HF_TOKEN"])

OpenAI / Anthropic(LiteLLM)

export OPENAI_API_KEY="sk-..."
export ANTHROPIC_API_KEY="sk-ant-..."

ゲート付きモデル

原因: モデルページで利用規約未同意。

→ Hub 上でモデルを開き Agree and access をクリック。


B.4 モデル未対応・レート制限

Model ... is not supported

対処: model_id を公式ドキュメントの推奨 ID に変更。

model = InferenceClientModel(model_id="Qwen/Qwen2.5-Coder-32B-Instruct")

Rate limit exceeded

対処 内容
待つ 数分後に再実行
モデル変更 より軽いモデル ID
ローカル化 Ollama + LiteLLMModel(第4章)
本番 リトライ・バックオフ(第21章 rate_limit_retry.py
import time
for attempt in range(3):
    try:
        result = agent.run(task)
        break
    except Exception as e:
        if "rate" in str(e).lower():
            time.sleep(2 ** attempt)
        else:
            raise

B.5 コード実行エラー(CodeAgent)

Import of 'foo' is not allowed

原因: additional_authorized_imports に未登録。

agent = CodeAgent(
    tools=[],
    model=model,
    additional_authorized_imports=["requests", "bs4"],
)

サブモジュール例: numpy.random を使うなら "numpy.random" または "numpy.*" を明示(第9章)。

Code execution failed / SyntaxError

原因: LLM が壊れたコードを生成。

対処
タスクを具体化 「答えは整数のみ」
max_steps を増やす 再試行の余地
モデル変更 コーダー向けモデル
final_answer_checks 形式検証(第9章)

意図的に危険な import を試した(第12章)

# ローカル executor では os.system 等は拒否される想定
agent.run("os モジュールでシェルを実行して")

→ 本番では executor_type="e2b" 等のサンドボックスを検討。


B.6 エージェントがループする / final_answer しない

症状

  • max_steps まで Step が続く
  • 同じツールを繰り返す
  • ログに final_answer が出ない

対処チェックリスト

- [ ] タスクが1文で完了可能か(範囲を狭める)
- [ ] ツール数を減らしたか(第6章・第14章)
- [ ] ツールの description / Args が明確か
- [ ] max_steps を一時的に下げて失敗ログを読んだか
- [ ] 弱いモデルではないか(コーダー系へ変更)
agent = CodeAgent(
    tools=[one_tool_only],
    model=model,
    max_steps=8,
    verbosity_level=2,
)

final_answer_checks が常に False

def is_short(s, agent_memory=None):
    return len(s) < 500

agent = CodeAgent(..., final_answer_checks=[is_short])

検証関数が厳しすぎるとエージェントが延々再試行する。


B.7 ツール関連

ツールを呼ばない

  • プロンプトに「必ず ○○ ツールを使え」と書く(最終手段)
  • ツール名・description を具体化(第5章・第14章)
  • add_base_tools=True で検索が使えるか確認

Hub load_tool / trust_remote_code

You must pass trust_remote_code=True
from smolagents import load_tool

t = load_tool("username/space-name", trust_remote_code=True)

⚠️ 信頼できるソースのみ。


B.8 MCP 接続失敗

Connection refused / サーバー起動していない

# 別ターミナルでサーバーを起動してからエージェント
python examples/ch08/mcp_weather_server.py
from smolagents import MCPClient, CodeAgent, InferenceClientModel
from mcp import StdioServerParameters

params = StdioServerParameters(
    command="python",
    args=["examples/ch08/mcp_weather_server.py"],
)

with MCPClient(params) as tools:
    agent = CodeAgent(tools=tools, model=InferenceClientModel())
    print(agent.run("東京の天気を教えて"))

uvx / pubmedmcp が見つからない

pip install mcp uv  # 環境による
# または command を python + ローカルスクリプトに変更

構造化出力が空

structured_output=True を付け忘れ、またはサーバーがスキーマ非対応。

with MCPClient(params, structured_output=True) as tools:
    ...

B.9 Gradio / CLI

Gradio が起動しない

pip install gradio
# または
pip install 'smolagents[gradio]'

smolagent: command not found

pip install 'smolagents[toolkit]'
# Scripts ディレクトリが PATH にあるか
python -m smolagents.cli  # バージョンにより異なる場合あり

B.10 ネットワーク・プロキシ

export HTTP_PROXY=http://proxy.example:8080
export HTTPS_PROXY=http://proxy.example:8080

企業プロキシ下では Inference API と Web 検索ツールの両方が失敗することがある。


B.11 Issue を出すときのテンプレ

## 環境
- OS:
- Python:
- smolagents: (pip show のバージョン)
- model_id:

## 再現コード
(最小 10 行程度)

## 期待する動作

## 実際のログ
(HF_TOKEN はマスク)

関連

付録 内容
付録A 引数・環境変数
付録C サンプルコード対応表
第21章 本番チェックリスト

付録C ハンズオン解答・完成コード

本リポジトリでは examples/chNN/ が完成コード(解答) です。別途 solutions/ に複製はせず、パスと役割をここで一覧化する。


C.1 方針

ディレクトリ 役割
examples/chNN/ 各章ハンズオンの 実行可能な完成形
solutions/ 将来、演習用スターターと解答を分離する場合に使用(現状は README のみ)
# 章03のサンプルを実行
python examples/ch03/fibonacci_no_tools.py

C.2 章ごと対応表

第0部〜第1部(第0〜4章)

ハンズオン 完成コード
0 0-1 環境 / 0-2 最初のエージェント examples/ch00/first_agent.py
1 1-1 環境確認 / 1-2 ログ要約 check_env.py, inspect_run_logs.py
2 2-1 import / 2-2 HF 接続 import_check.py, hf_connection.py
3 3-1 フィボナッチ / 3-2 logs fibonacci_no_tools.py, inspect_agent_logs.py
4 4-1 比較 / 4-2 接続 compare_models.py, connect_backend.py

第2部(第5〜8章)

ハンズオン 完成コード
5 5-1 Hub ツール / 5-2 差し替え hub_top_model_tool.py, swap_tools.py
6 6-1 Web 検索 / 6-2 手動 web_search_agent.py, manual_search_tool.py
7 7-1 push(任意)/ 7-2 load push_to_hub_optional.py, load_hub_tool.py, custom_downloads_tool.py
8 8-1 LangChain / 8-2 MCP langchain_search_tool.py, mcp_weather_server.py, mcp_weather_agent.py

第3部(第9〜11章)

ハンズオン 完成コード
9 9-1 Web タイトル / 9-2 checks web_title_agent.py, final_answer_checks.py, additional_args_demo.py
10 10-1 比較 / 10-2 LiteLLM compare_web_title.py, tool_calling_litellm_sketch.py
11 11-1 CLI 相当 cli_equivalent_agent.py

第4部(第12〜14章)

ハンズオン 完成コード
12 12-1 拒否 import / 12-2 Docker 任意 blocked_import_agent.py, local_executor_sandbox.py, docker_executor_optional.py
13 13-1 Gradio / 13-2 reset gradio_ui_agent.py, reset_false_demo.py, verbosity_max_steps.py, interrupt_demo.py, stream_run_demo.py
14 14-1 悪い→良いツール / 14-2 統合 bad_weather_tool.py, good_weather_tool.py, merged_spot_info_agent.py

第5部(第15〜16章)

ハンズオン 完成コード
15 15-1 マネージャー / 15-2 ログ追跡 manager_web_search.py, trace_managed_agent_logs.py
16 16-1 3 役レポート gaia_three_agent_report.py

第6部(第17〜20章)

ハンズオン 完成コード
17 17-1 Web リサーチ web_research_agent.py
18 18-1 Text-to-SQL setup_sample_db.py, readonly_sql_tool.py, text_to_sql_agent.py
19 19-1 画像 / 19-2 音声任意 image_prompt_loop.py, vision_additional_args.py, audio_summary_optional.py
20 20-1 Hub push 任意 push_agent_to_hub.py, load_agent_from_hub.py

第7部(第21〜22章)

ハンズオン 完成コード
21 本番パターン集 env_secrets_pattern.py, rate_limit_retry.py, observability_log.py, hitl_approval.py
22 ソース探索 explore_source.py

C.3 章別の実行順(依存あり)

第18章 Text-to-SQL

python examples/ch18/setup_sample_db.py    # 先に DB 作成
python examples/ch18/text_to_sql_agent.py

第8章 MCP

# ターミナル1(必要なら)
python examples/ch08/mcp_weather_server.py

# ターミナル2(stdio で起動する版は mcp_weather_agent.py 内で subprocess)
python examples/ch08/mcp_weather_agent.py

C.4 git tag で章単位のスナップショット(推奨運用)

執筆・学習用に 章完了時点 を tag しておくと、差分学習がしやすい。

# メンテナが tag を打つ例
git tag ch03 -m "第3章完了: fibonacci + logs"
git push origin ch03

# 読者: 第3章時点の examples だけ見る
git show ch03:examples/ch03/fibonacci_no_tools.py
tag 名(例) 内容
ch00 最初の CodeAgent
ch03 model + tools + logs
ch08 MCP デモ
ch18 SQLite + SQL エージェント

本リポジトリに tag が未設定の場合は、main ブランチの examples/ を参照すればよい。


C.5 自分の解答と diff を取る

# 演習用にコピーして編集
cp examples/ch05/hub_top_model_tool.py /tmp/my_tool.py
# 編集後
diff -u examples/ch05/hub_top_model_tool.py /tmp/my_tool.py
# または git で一時ブランチ
git checkout -b exercise-ch05
# examples/ch05/ を編集
git diff examples/ch05/

C.6 よくある質問

Q. solutions/ はいつ使う?

  1. 読者向けに「穴あきスターター」を配布したいとき。例: solutions/ch05/starter.py(未完成)と examples/ch05/hub_top_model_tool.py(解答)を分離。

Q. 本文のコードと examples が違う

  1. examples を正 とする。本文は説明用に短縮している場合がある。

関連

リンク 内容
solutions/README.md solutions ディレクトリの説明
付録B 実行エラー
README.md 全 examples 一覧

付録D 用語集

本書で繰り返し出る用語を 五十音順(英語見出し) で整理。初出章を併記。


A

Agent(エージェント)

LLM を核に、ツールとループでタスクを完遂するシステム。本書では主に CodeAgent / ToolCallingAgent(第0章、第3章)。

additional_args

agent.run(task, additional_args={...}) でタスクと一緒に渡す辞書。URL・DB スキーマ・ファイルパスなど(第9章、第18章)。

additional_authorized_imports

CodeAgent が生成コード内で import 可能なモジュール名のリスト(第9章、第12章)。


C

CodeAgent

行動を Python コード として生成・実行するエージェント。ループ・分岐・ツール合成に強い(第9章)。

Code agent(コードエージェント)

ツール呼び出しを JSON ではなくコードで表現する設計思想。smolagents の差別化ポイント(第0章)。


E

E2B

クラウド上で LLM 生成コードを隔離実行するサンドボックス。executor_type="e2b"(第12章)。

Executor(コード実行器)

CodeAgent が生成した Python を実行するバックエンド。ローカル / E2B / Docker 等(第12章)。

extras(pip extras)

pip install 'smolagents[toolkit]'[toolkit] 部分。オプション依存をまとめて入れる(第2章)。


F

final_answer

エージェントがタスク完了を宣言する関数。ログに Out - Final answer: として現れる(第3章)。

final_answer_checks

最終回答を検証するコールバックのリスト。False ならエージェントが続行(第9章)。


G

GradioUI

GradioUI(agent).launch() でエージェント用チャット UI を起動(第13章)。


H

HF_TOKEN

Hugging Face API トークン。Inference API・Hub 利用で使用(第0章、第1章)。⚠️ Git にコミットしない。

Hub(Hugging Face Hub)

モデル・データセット・Space・ツールを共有するプラットフォーム(第7章、第20章)。


I

Inference Providers(Inference プロバイダ)

Hub 経由で複数社の推論 API を統一的に呼ぶ仕組み。InferenceClientModel が利用(第4章)。

InferenceClientModel

本書デフォルトのモデルクラス。huggingface_hub.InferenceClient ベース(第4章)。


L

LiteLLM / LiteLLMModel

多プロバイダ向けルーティング。OpenAI・Anthropic・Ollama 等を統一 API で利用(第4章、第10章)。

LocalPythonExecutor

smolagents 組み込みの制限付き Python 実行環境(第12章)。


M

Managed agent(マネージドエージェント)

別の CodeAgentmanaged_agents=[...] で委譲するサブエージェント。namedescription が必須(第15章)。

MCP(Model Context Protocol)

ツールサーバーとクライアントを結ぶ標準。MCPClient / ToolCollection.from_mcp()(第8章)。

Model(モデル)

エージェントの「頭脳」となる LLM ラッパー。InferenceClientModel 等(第3章、第4章)。

Multi-agent(マルチエージェント)

複数エージェントが役割分担してタスクを処理する構成(第15章、第16章)。


R

reset(agent.run の引数)

reset=False で前回のメモリを引き継いで会話継続(第13章)。


S

smolagents

Hugging Face 製の軽量エージェントライブラリ(第0章)。transformers.agents の後継的位置づけ。

Step(ステップ)

エージェントループの 1 回分。ログの Step 0, Step 1, …(第1章、第3章)。

structured_output(MCP)

MCP ツールの JSON スキーマ付き出力を有効化。MCPClient(..., structured_output=True)(第8章)。

System prompt(システムプロンプト)

エージェント初期化時に LLM へ渡される指示。ツール説明が自動埋め込みされる(第5章)。


T

Tool(ツール)

エージェントが呼び出す関数のラッパー。@tool または Tool サブクラス(第5章)。

ToolCallingAgent

ツール呼び出しを JSON 等の構造化形式で行うエージェント(第10章)。

Tool collection

複数ツールの束。Hub コレクションや MCP から一括読み込み(第7章、第8章)。

transformers.agents

旧来の transformers 内エージェント API。新規は smolagents 推奨(第0章)。

trust_remote_code

Hub 上のカスタムツール読み込み時に必要なフラグ。信頼できるソースのみ(第7章)。⚠️


V

verbosity_level

ログの詳細度。大きいほど多く出力(第13章)。

VisitWebpageTool

URL のページ内容を取得し markdown 化するツール(第17章)。


略語一覧

略語 正式名称
HF Hugging Face
LLM Large Language Model
API Application Programming Interface
SQL Structured Query Language
UI User Interface
HITL Human In The Loop(人間承認)
PoC Proof of Concept
GAIA エージェントベンチマーク(第16章参照)

関連

付録 内容
付録A API 早見表
付録E 公式リンク
目次 章一覧

付録E 公式ドキュメント・リンク集

本書は smolagents v1.x 系 を想定。URL は執筆時点の英語ドキュメント(/en/)を掲載。


E.1 公式ドキュメント(入口)

リソース URL 用途
ドキュメントトップ https://huggingface.co/docs/smolagents/en/index インストール・Quickstart
Installation https://huggingface.co/docs/smolagents/en/installation extras・依存関係
Guided tour https://huggingface.co/docs/smolagents/en/guided_tour 本書の背骨と一致
API Reference https://huggingface.co/docs/smolagents/en/reference/index クラス・引数の正確な定義
推奨ブックマーク順:
1. Guided tour
2. API Reference(agents / models / tools)
3. Tutorials(トピック別)

E.2 コンセプト・チュートリアル

トピック URL 本書の章
Intro to agents https://huggingface.co/docs/smolagents/en/conceptual_guides/intro_agents 0
Tools https://huggingface.co/docs/smolagents/en/tutorials/tools 5–8
Secure code execution https://huggingface.co/docs/smolagents/en/tutorials/secure_code_execution 12
Building good agents https://huggingface.co/docs/smolagents/en/tutorials/building_good_agents 14
Multi-agents(guided tour 内) https://huggingface.co/docs/smolagents/en/guided_tour#multi-agents 15

E.3 API Reference(主要ページ)

カテゴリ URL
Agents https://huggingface.co/docs/smolagents/en/reference/agents
Models https://huggingface.co/docs/smolagents/en/reference/models
Tools https://huggingface.co/docs/smolagents/en/reference/tools
Default tools https://huggingface.co/docs/smolagents/en/reference/default_tools
# ドキュメントでクラス名を検索するときの例
# CodeAgent → reference/agents#smolagents.CodeAgent
# InferenceClientModel → reference/models#smolagents.InferenceClientModel

E.4 Examples(公式サンプル)

URL 本書の章
Text-to-SQL https://huggingface.co/docs/smolagents/en/examples/text_to_sql 18
Web browser / vision https://huggingface.co/docs/smolagents/en/examples/web_browser 19
Multi-agents https://huggingface.co/docs/smolagents/en/examples/multiagents 15–16

リポジトリ内の対応サンプル:

ls examples/ch17/ examples/ch18/ examples/ch15/

E.5 ブログ・発表

タイトル URL
Introducing smolagents https://huggingface.co/blog/smolagents
Beating GAIA(マルチエージェント) https://huggingface.co/blog/beating-gaia
Inference Providers https://huggingface.co/blog/inference-providers

E.6 周辺エコシステム

リソース URL 備考
Hugging Face Hub https://huggingface.co/ モデル・ツール共有
Access Tokens https://huggingface.co/settings/tokens HF_TOKEN 発行
MCP 仕様 https://modelcontextprotocol.io/ 第8章
LiteLLM ドキュメント https://docs.litellm.ai/ 第4章
E2B https://e2b.dev/docs 第12章
Gradio https://www.gradio.app/docs 第13章

E.7 ソースコード・コミュニティ

リソース URL
GitHub(smolagents) https://github.com/huggingface/smolagents
Issues https://github.com/huggingface/smolagents/issues
Discussions https://github.com/huggingface/smolagents/discussions
Hugging Face Discord https://hf.co/join/discord

Issue テンプレは 付録B を参照。


E.8 本書リポジトリ内リンク

リソース URL
GitHub(本書) https://github.com/hiromichinomata/yukkuri-smolagents
ライセンス LICENSE(proprietary)
git clone https://github.com/hiromichinomata/yukkuri-smolagents.git
種類 パス
目次 00-toc.md
第0章 00.md
付録A–D appendix-a.mdappendix-d.md
クイックリファレンス appendix-a.md
トラブルシュート appendix-b.md
サンプル対応表 appendix-c.md
# ローカルで docs をプレビュー(任意)
# pip install grip  # 等
ls docs/*.md docs/appendix-*.md

E.9 関連フレームワーク(参考)

本書の主題外だが、比較学習用。

名前 URL
LangGraph https://langchain-ai.github.io/langgraph/
Microsoft AutoGen https://microsoft.github.io/autogen/
LangChain Agents https://python.langchain.com/docs/concepts/agents/

位置づけは 第22章 を参照。


E.10 ドキュメントの読み方(推奨ルート)

flowchart TD
  A[本書 第0–3章] --> B[公式 Guided tour]
  B --> C[本書 第5–9章 + examples]
  C --> D[公式 Tutorials]
  D --> E[本書 第12–21章]
  E --> F[API Reference で穴埋め]
段階 やること
1 本書で手を動かす
2 公式 Guided tour で抜けを確認
3 API Reference で引数を確定
4 GitHub Issues / Discussions で最新情報

関連付録

付録 内容
付録A クラス・引数早見
付録B エラー対処
付録D 用語集