0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

PythonとClaude APIでAIエージェントの基礎的なものを作ってみた

0
Posted at

Supershipの名畑です。ところざわサクラタウン角川武蔵野ミュージアムで開催の「銀河鉄道999 THE GALAXY EXPERIENCE あの旅は、まだ続いている。」は、今後の映画体験はこうなっていくのかなと未来に思いを馳せざるを得ない、刺激的な内容でした。

はじめに

AIエージェントという言葉を毎日のように耳にする昨今ですが、Anthropicは公式ブログにおいて、エージェントを次のように定義しています。

Since we wrote that post, we’ve gravitated towards a simple definition for agents: LLMs autonomously using tools in a loop.

参考:Effective context engineering for AI agents \ Anthropic

ループの中で自律的にツールを使用するLLM」。とてもシンプルな定義です。

今回はClaude APIのtool useを使い、Pythonでシンプルなエージェントを実装してみようと思います。

3年前にも「OpenAIによるChat APIの新機能Function callingをPythonで使ってみた」という記事を書いているので、また似たような内容にはなるのですが、普段はClaudeについてはCodeをCLI経由かDesktop経由で使うことばかりなので、API経由も記録に残しておこうかと思いました。

tool useとは

Tool use (also called function calling) lets Claude call functions that you define or that Anthropic provides. Claude determines when to call a tool based on the user's request and the tool's description. It then returns a structured call that your application executes (client tools) or that Anthropic executes (server tools).

参考:Tool use with Claude - Claude Platform Docs

日本語訳すると以下です。

tool use(function callingとも呼ばれます)はClaudeにユーザーが定義した関数やAnthropicが提供する関数を呼び出させます。Claudeは、ユーザーの要求とツールの説明に基づいて、いつツールを呼び出すべきかを決定します。そして、構造化された呼び出し情報を返します。この呼び出しは、アプリケーション側で実行されるもの(クライアントツール)か、Anthropic側で実行されるもの(サーバーツール)となります。

実際、どのように挙動させるかは、以下のように記載されています。

The canonical shape is a while loop keyed on stop_reason:

1.Send a request with your tools array and the user message.
2.Claude responds with stop_reason: "tool_use" and one or more tool_use blocks.
3.Execute each tool. Format the outputs as tool_result blocks.
4.Send a new request containing the original messages, the assistant's response, and a user message with the tool_result blocks.
5.Repeat from step 2 while stop_reason is "tool_use".

参考:How tool use works - Claude Platform Docs

日本語訳すると以下です。

正準形は、stop_reason をキーにしたwhileループです。

1.tools の配列とユーザーメッセージを含むリクエストを送信する
2.Claudeが stop_reason: "tool_use" と1つ以上の tool_use ブロックで応答する
3.各ツールを実行し、その出力を tool_result ブロックとして整形する
4.元のメッセージ群、アシスタントの応答、tool_result ブロックを含むユーザーメッセージをまとめて新しいリクエストとして送信する
5.stop_reason が "tool_use" である間、ステップ2から繰り返す

事前準備

Claude APIは従量課金です。

Claude Consoleでアカウントを作成し、クレジットを購入します。モデル毎の使用料はModel pricingをご覧ください。

今回は最初に5ドル購入しておきました。実際に使った金額は、後述もしますが、0.17ドルでした。現在の為替だと30円弱ですね。

messaging_1.png

恐いので自動リロードはオフにしておきました。

次に、同じくClaude ConsoleでAPIキーを発行します。
APIキーの内容は発行時のみしか取得できず、後から確認できないので注意してください。

また、なにかのワークスペースに紐づけることも忘れないようにしてください。私は最初、忘れていて、実行時に以下のエラーが出ました。

anthropic.BadRequestError: Error code: 400 - {'type': 'error', 'error': {'type': 'invalid_request_error', 'message': 'anthropic-workspace-id is required when authenticating with an identity-linked API key; send the id of the workspace this request acts in.'}, 'request_id': None}

環境変数など、コードからアクセスできる場所に設定します。
macOSの.zshrcであれば以下です。

export ANTHROPIC_API_KEY=発行したAPIキー

環境

macOSです。

Pythonはインストール済みです。

% python --version
Python 3.14.3

公式のPython SDKをインストールします。

% pip install anthropic

略

実装

今回のエージェントに持たせるツールは「サイコロを振る」の1つだけとしました。シンプルな方が要点を掴みやすいので。

以下をagent.pyとして保存します。これで全部です。

import random
import anthropic

# クライアントの作成(APIキーは環境変数ANTHROPIC_API_KEYから自動で読み込まれる)
client = anthropic.Anthropic()

# エージェントに持たせるツールの定義
# Claudeはこのdescriptionを読んで、いつツールを使うかを自分で判断する
tools = [
    {
        "name": "roll_dice",
        "description": "指定した面数のサイコロを1回振り、出た目を返します。",
        "input_schema": {
            "type": "object",
            "properties": {
                "sides": {"type": "integer", "description": "サイコロの面数(例:6)"}
            },
            "required": ["sides"],
        },
    },
]

# ツールの実体
def execute_tool(name, tool_input):
    if name == "roll_dice":
        return str(random.randint(1, tool_input["sides"])) # 渡された値を元にして乱数を生成
    return f"unknown tool: {name}"

# ユーザーからの指示を受け取り、会話履歴の最初のメッセージとする
messages = [{"role": "user", "content": input("指示をどうぞ: ")}]

# エージェントループ:Claudeがツールを使い終わるまで繰り返す
while True:
    # 会話履歴の全体とツール定義を渡してClaudeを呼び出す
    response = client.messages.create(
        model="claude-opus-5",  # モデル
        max_tokens=16000,
        tools=tools,
        messages=messages,
    )

    # ツールの使用要求でなければ(=回答が完成したら)ループを抜ける
    if response.stop_reason != "tool_use":
        break

    # Claudeの応答(tool_useブロックを含む)を会話履歴に追加
    messages.append({"role": "assistant", "content": response.content})

    # 要求されたツールを実行し、結果をtool_resultブロックとして集める
    tool_results = []
    for block in response.content:
        if block.type == "tool_use":
            print(f"[ツール実行] {block.name} {block.input}")
            try:
                result = execute_tool(block.name, block.input)
            except Exception as e:
                result = f"エラー: {e}"
            tool_results.append(
                {"type": "tool_result", "tool_use_id": block.id, "content": result}
            )

    # ツールの実行結果をユーザーメッセージとして履歴に追加し、次のループへ
    messages.append({"role": "user", "content": tool_results})

# 最終的な回答のテキストを表示
for block in response.content:
    if block.type == "text":
        print(block.text)

実行 1

実装したPythonファイルを実行してみます。

% python agent.py
指示をどうぞ: 

「指示をどうぞ」と聞かれるので

指示をどうぞ: 6面サイコロを2個振って、合計を教えて

こう入力してみました。

すると以下のように処理されます。

指示をどうぞ: 6面サイコロを2個振って、合計を教えて
[ツール実行] roll_dice {'sides': 6}
[ツール実行] roll_dice {'sides': 6}
結果はこちらです!

🎲 1個目:**3**
🎲 2個目:**1**

**合計:4** です。

少し控えめな出目になりましたね。もう一度振ってみますか?

指示文面をLLMが解釈して、設定した関数を呼び出していることがわかります。

実行 2

せっかくなのでもう一つ試してみましょう。

指示をどうぞ: 合計が100以上になるまで、6面サイコロを振って、それぞれの目が何回ずつ出たか教えてください。

結果は以下です。

[ツール実行] roll_dice {'sides': 6}
[ツール実行] roll_dice {'sides': 6}
[ツール実行] roll_dice {'sides': 6}
[ツール実行] roll_dice {'sides': 6}
[ツール実行] roll_dice {'sides': 6}
[ツール実行] roll_dice {'sides': 6}
[ツール実行] roll_dice {'sides': 6}
[ツール実行] roll_dice {'sides': 6}
[ツール実行] roll_dice {'sides': 6}
[ツール実行] roll_dice {'sides': 6}
[ツール実行] roll_dice {'sides': 6}
[ツール実行] roll_dice {'sides': 6}
[ツール実行] roll_dice {'sides': 6}
[ツール実行] roll_dice {'sides': 6}
[ツール実行] roll_dice {'sides': 6}
[ツール実行] roll_dice {'sides': 6}
[ツール実行] roll_dice {'sides': 6}
[ツール実行] roll_dice {'sides': 6}
[ツール実行] roll_dice {'sides': 6}
[ツール実行] roll_dice {'sides': 6}
[ツール実行] roll_dice {'sides': 6}
[ツール実行] roll_dice {'sides': 6}
[ツール実行] roll_dice {'sides': 6}
[ツール実行] roll_dice {'sides': 6}
[ツール実行] roll_dice {'sides': 6}
[ツール実行] roll_dice {'sides': 6}
🎲 **合計103に到達しました!**(26回振りました)

## 出目の記録(振った順)
```
6, 6, 6, 5, 4, 3, 4, 1, 6, 6, 2, 2, 3,
3, 3, 2, 5, 6, 5, 2, 5, 1, 3, 2, 6, 6
```

## 各目の出現回数

| 出目 | 回数 | グラフ | 小計 |
|:---:|:---:|:---|---:|
| 1 | 2回 | ██ | 2 |
| 2 | 5回 | █████ | 10 |
| 3 | 5回 | █████ | 15 |
| 4 | 2回 | ██ | 8 |
| 5 | 4回 | ████ | 20 |
| 6 | **8回** | ████████ | 48 |
| **計** | **26回** | | **103** |

## まとめ
- **合計**:103(100以上を達成、最後の一振りは6でした)
- **最多**:6が8回で断トツ。期待値(26÷6 ≈ 4.3回)の約2倍という強運ぶりです
- **最少**:1と4がそれぞれ2回だけ
- **平均出目**:103 ÷ 26 ≈ **3.96**(理論値3.5より高め)

6が出まくったおかげで、期待回数(約29回)より少ない26回でゴールできました!

こちらの意図する通りの動きでツールを呼び出してくれていることがわかります。

余談

この一連の処理で、消費クレジットは0.17ドルでした。Opusでかつ、一定数サイコロを振ったので、こんなものかと。

messaging_2.png

最後に

変化が激しいからこそ、基礎的な部分はできるだけこの手で触っておきたい。

宣伝

SupershipのQiita Organizationを合わせてご覧いただけますと嬉しいです。他のメンバーの記事も多数あります。

Supershipではプロダクト開発やサービス開発に関わる方を絶賛募集しております。
興味がある方はSupership株式会社 採用サイトよりご確認ください。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?