Anthropic SDKでチャットボットを実装した話|n8n・LangGraph・SDKの比較

中小企業向けの業務システムを開発していると、AIエージェントを実装方法が複数あって迷うことがあります。n8nのようなGUIワークフローツール、LangGraphのようなコードフレームワーク、そしてLLMプロバイダーの公式SDKを直接使う方法。

本記事では、3つ目の選択肢であるAnthropic SDKを直接使ったAIエージェント実装を試してみました。題材はチャットとメモリだけの最小構成。

実装した結果見えてきたのは、最小構成ではフレームワークを使ってもSDK直叩きでも、コード量や複雑性にほとんど差が出ないという事実です。本記事ではこの体験を踏まえて、n8n・LangGraph・SDK直叩きの使い分けの境界線を整理していきます。

なお、本記事は「n8nのAIエージェントノードをLangGraphで書き直してみた」の続編でもあります。LangGraph版の実装が気になる方はあわせてご覧ください。

目次

この記事で作るもの

n8nで言うところの、

Chat Trigger → AI Agent (Claude) → Simple Memory

という3つのノードに相当する処理を、Anthropic SDKだけで実装します。使うライブラリは anthropic(公式SDK)と python-dotenv のみ。LangChain系のライブラリは一切使いません。

完成形は、ターミナル上で対話ができ、複数ターンにわたって会話履歴が保持される、シンプルなチャットボットです。

なぜSDK直叩きという選択肢を試すのか

業務システム開発の現場では、フレームワークを選ぶこと自体が技術判断のひとつです。

LangChainやLangGraphのようなフレームワークは便利ですが、依存ライブラリの追加・バージョン互換性の管理・フレームワーク独自の概念学習といったコストが必ず発生します。プロジェクトが小規模なら、フレームワークの恩恵よりもこれらのコストの方が大きくなる場合があります。

SDKを直接叩く目的は3つあります。

  • フレームワークが内部でやっている処理を可視化する
  • 依存ライブラリを最小化したときに保守性がどう変わるかを体感する
  • 自分たちが本当にフレームワークを必要としているかを判断する材料を持つ

特に最後の点が重要です。中小企業向けの業務システムを設計する立場としては、お客様の運用フェーズ・社内のIT人材・予算規模に応じて、「n8n 」「LangGraphか」「SDK直叩き」「あるいは別の選択肢」を判断できる必要があります。そのためには、それぞれの実装を実際に書いて感触を確かめておくのが一番確実です。

環境構築

検証環境はWindows 11、Python 3.12、VS Codeです。Macでもコマンドを少し読み替えれば同じように動作します。

プロジェクトフォルダと仮想環境

Bash
mkdir anthropic-sdk-chat-minimum
cd anthropic-sdk-chat-minimum
python -m venv .venv
.venv\Scripts\activate

Mac/Linuxなら source .venv/bin/activate で有効化します。

パッケージのインストール

必要なパッケージは2つだけです。

Bash
pip install anthropic python-dotenv
パッケージ役割
anthropicClaude APIを直接呼ぶための公式SDK
python-dotenv.env ファイルからAPIキーを読み込む

LangGraphやLangChainを使う場合と比べて、依存ライブラリが2つ減ることになります。この差は本番運用での保守性に直結します。依存が少ない=壊れにくい、アップデートが楽、というメリットがあります。

APIキーの設定

プロジェクト直下に .env ファイルを作成します。

Bash
ANTHROPIC_API_KEY=sk-ant-api03-xxxxxxxxxxxxx

APIキーはAnthropic Console(https://console.anthropic.com/)で発行できます。

合わせて、APIキーの流出を防ぐため .gitignore も作成しておきます。

Bash
.env
.venv/
__pycache__/
*.pyc

これを忘れるとGit公開時にAPIキーが流出するので、必須です。

実装:SDKで最小構成のチャットボットを作る

それではコードを書いていきます。

プロジェクト直下に chat.py を作成します。完成形のコード全体は以下のとおりです。

Python
from dotenv import load_dotenv
import anthropic

load_dotenv()
client = anthropic.Anthropic()

# thread_idごとにメッセージリストを保持する辞書
conversations = {}  # 例:{"session-1": [...messages...]}

def get_messages(thread_id):
    if thread_id not in conversations:
        conversations[thread_id] = []
    return conversations[thread_id]

def add_message(thread_id, role, content):
    messages = get_messages(thread_id)
    messages.append({"role": role, "content": content})

def call_claude(messages):
    response = client.messages.create(
        model="claude-opus-4-7",
        max_tokens=1024,
        messages=messages,
    )
    return response.content[0].text

def chat_turn(thread_id, user_input):
    # ユーザー発言を履歴に追加
    add_message(thread_id, "user", user_input)
    
    # 履歴全体をClaudeに渡して応答取得
    messages = get_messages(thread_id)
    assistant_response = call_claude(messages)
    
    # 応答を履歴に追加
    add_message(thread_id, "assistant", assistant_response)
    
    return assistant_response

thread_id = "session-1"
print("チャットを開始します。終了するには 'quit' または 'exit' と入力してください。\n")

while True:
    user_input = input("あなた: ")
    if user_input.lower() in ["quit", "exit"]:
        print("チャットを終了します。")
        break
    
    response = chat_turn(thread_id, user_input)
    print(f"Claude: {response}\n")

約45行です。コードの中身を、要素ごとに見ていきます。

クライアントの初期化

Python
client = anthropic.Anthropic()

load_dotenv() で読み込んだ ANTHROPIC_API_KEY を、anthropic.Anthropic() が環境変数から自動的に取得します。

モデル名や max_tokens などのパラメータは、クライアント作成時ではなく呼び出しごとに指定する設計になっています。LangChain系の ChatAnthropic(model=..., max_tokens=...) のような事前バインドではなく、毎回必要な情報を渡すスタイルです。

メモリの自作(n8nのSimple Memoryに相当する部分)

ここが今回の実装のポイントです。

Python
conversations = {}

def get_messages(thread_id):
    if thread_id not in conversations:
        conversations[thread_id] = []
    return conversations[thread_id]

def add_message(thread_id, role, content):
    messages = get_messages(thread_id)
    messages.append({"role": role, "content": content})

この部分がn8nのSimple Memoryノード、あるいはLangGraphのMemorySaver(Checkpointer)に相当する処理です。

実装内容はシンプルです。conversations という辞書を用意し、thread_id をキーにしてメッセージリストを保持しているだけです。新しいメッセージが来たら、その thread_id に紐づくリストに追加していきます。

n8nのSimple MemoryノードもLangGraphのMemorySaverも、本質的にやっていることはこれと同じです。ただし、それぞれフレームワークの設計に応じて追加機能(ウィンドウサイズ制御、State全体のスナップショット保存、永続化バックエンドの切り替えなど)が付いています。

今回のように「メッセージリストだけ持てればいい」ならば、自前で実装でも全く問題ありません。

LLM呼び出し関数

Python
def call_claude(messages):
    response = client.messages.create(
        model="claude-opus-4-7",
        max_tokens=1024,
        messages=messages,
    )
    return response.content[0].text

ここがLLMを呼び出す部分です。

注目していただきたいのは messages 引数の形式です。SDK直叩きでは、メッセージは辞書形式 {"role": "user", "content": "..."} で渡します。これはAnthropic APIの公式仕様そのままです。LangChain版のように HumanMessage クラスを使う必要はありません。

応答の取り出しも response.content[0].text という素直な構造です。content がリストになっているのは、将来的に画像入力やツール使用結果が混ざる可能性を考慮した設計です。今回はテキスト応答だけなので [0] で最初の要素を取り出しています。

1ターン処理の全体像

Python
def chat_turn(thread_id, user_input):
    add_message(thread_id, "user", user_input)
    messages = get_messages(thread_id)
    assistant_response = call_claude(messages)
    add_message(thread_id, "assistant", assistant_response)
    return assistant_response

ここがLangGraph版で言うところの「グラフを実行する」処理に相当します。

処理の流れは、ユーザー発言を履歴に追加 → 履歴全体をClaudeに渡す → 応答を取得 → 応答を履歴に追加 → 応答を呼び出し元に返す、という5ステップです。

LangGraph版ではこれらが全て graph.invoke(...) の中で自動的に行われていました。SDK直叩きでは1つ1つの手順を自分で書く必要があります。ただし、書いている処理の中身そのものは変わりません。フレームワークが隠していた処理が見えているだけです。

対話ループ

Python
thread_id = "session-1"
while True:
    user_input = input("あなた: ")
    if user_input.lower() in ["quit", "exit"]:
        break
    response = chat_turn(thread_id, user_input)
    print(f"Claude: {response}\n")

thread_id を固定値で持ち回しているので、起動中は同一の会話として扱われます。複数のユーザーや複数の会話セッションを扱うときは、ここを動的に切り替えることになります。

動かして試してみる

ターミナルで実行します。

Python
python chat.py

3ターン試してみます。

あなた: こんにちは。僕の名前はヨシキです。
Claude: こんにちは、ヨシキさん!😊 はじめまして。今日はどんなことをお話ししましょうか?(以下省略)

あなた: Anthropic SDKでSDK直叩きのチャットボットを作っています。
Claude: お、いいですね!Anthropic SDKを直接使ってチャットボットを作るのは、仕組みを理解する上でもとても勉強になりますよね。何かお手伝いできることはありますか?(以下省略)

あなた: 僕の名前を覚えていますか?
Claude: はい、ですね。

3ターン目で名前を覚えています。自作メモリが正しく機能していることが確認できます。

n8n・LangGraph・SDK直叩きを比較する

ここまで動いたところで、n8n・LangGraph・SDK直叩き、それぞれのパターンを整理しておきます。

項目n8nLangGraph版SDK直叩き版
実装形態GUIノード接続PythonコードPythonコード
必要な技術スキル低(GUI操作)中(Python + フレームワーク学習)中(Python + API仕様理解)
コード行数0行約50行約45行
メッセージ管理Simple MemoryノードMemorySaver辞書とgetter/setterを自前実装
透明性低(ノード内部はブラックボックス)中(フレームワークが一部隠す)高(全ての処理が見える)
カスタマイズ性低(用意された機能の範囲内)中(フレームワークの作法に従う)高(何でも書ける)
学習コスト高(独自概念が多い)中(Python標準知識)

正直な感想として、LangGraph版とSDK直叩き版の差はそれほど大きくないです。行数も依存ライブラリ数もほぼ同じで、書く処理の中身もあまり変わりません。「LangGraphが裏でやっていたこと」を書くつもりが、書く対象そのものがそもそも少なかった、というのが実際のところです。

n8n版は「ノードを3つ繋ぐだけ」で同じことが実現できるので、こちらの方が圧倒的に手軽です。

LangGraphの真価が出る場面

視点を変えてみましょう。LangGraphはどういった場面で真価を発揮するのか?

LangGraphが本来活躍するのは、以下のような複雑性を持つワークフローです。

複数ノードの条件分岐がある
「ユーザー入力 → 意図判定 → 適切なツール選択 → ツール実行 → 結果統合」のような分岐を含むワークフローでは、グラフ構造で明示的に流れを定義できるLangGraphが圧倒的に読みやすくなります。SDK直叩きで同じことをやろうとすると、if 文と関数呼び出しが入り組んで、コード全体の見通しが急に悪化します。

状態管理が複雑
メッセージ履歴だけでなく、ユーザー情報、検索結果、ツール実行ログなど、複数のフィールドを持ち回す必要があるとき。LangGraphのStateは型付きの構造を持てるため、各ノードが「どのフィールドを読み、どこを更新するか」を明示的に書けます。SDK直叩きで同じことをやろうとすると、自前で巨大な辞書を管理することになります。

ループや再帰が必要
エージェントが「タスクが完了するまでツール呼び出しを繰り返す」ような実装は、LangGraphの条件分岐エッジで自然に書けます。SDK直叩きでは while ループと終了条件判定、エラー時の継続/中断判断を自分で組み立てる必要があります。

人によるインタラクションを挟む
「ある段階で人間のレビューを待ち、承認されたら次に進む」というワークフローは、LangGraphのCheckpointer + interrupt機能で実装できます。SDK直叩きで同じ機能を作ろうとすると、状態の永続化、再開ロジック、UI連携などを全て自前で書くことになり、相当な実装量になります。

今回の「チャット + メモリ」は、これらのいずれにも該当しません。ノードは1つ、Stateは単純なメッセージリスト、ループも分岐も人間介入もありません。つまり、LangGraphが用意している機能のほとんどを使っていない状態です。これでは差が出ないのも当然でした。

SDK直叩きで見えてきたこと

ここまでの実装と比較を踏まえて、率直に感じたことを整理しておきます。

良かった点
依存ライブラリが少ない安心感、Anthropic APIの公式仕様にそのまま触れられる学習効果、コードの挙動が完全に予測できる透明性、の3つが挙げられます。
「公式仕様にそのまま触れる」点は、Anthropicの新機能がリリースされたときに、LangChain側の対応を待たずに即座に試せるというメリットがあります。

引っかかった点
便利機能(リトライ、ストリーミング、ツール使用ループなど)を自前で書く必要がある点、Stateの設計を自分で考える必要がある点、エラーハンドリングのパターンを毎回書く必要がある点、の3つです。複雑な要件になるほど、これらの「自前実装の量」が雪だるま式に増えていきます。

まとめ

本記事では、Anthropic SDKを直接使ったチャットボット実装を試してみました。

結果として見えてきたのは、「最小構成ではLangGraphとSDK直叩きにほとんど差が無く、フレームワークの真価は複雑な要件が出てから初めて現れる」という事実でした。これは技術選定の現場では当たり前のように語られることですが、実際に同じ要件を両方で書いてみることで、肌感覚として実感できました。

中小企業の業務システムを設計する立場としては、「とりあえずLangGraphを使う」でも「最初からSDK直叩きで頑張る」でもなく、要件の複雑性に応じて段階的にツールを選ぶのが正解だと思います。最初はn8n、複雑になったらLangGraph、さらに細かい制御が必要になったらSDK直叩きの部分実装を組み合わせる、という流れが現実的です。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

1981年生まれ、名古屋出身。

2008年よりドイツ・ベルリンに在住。
ドイツの国家資格である職業訓練プログラム「アプリケーション開発専門IT技術者」を修了後、医療系自社開発企業にてデスクトップ・Webアプリケーションの開発に4年間従事。
2022年よりドイツの大手SIer「Adesso SE」にて、フルスタックエンジニアとしてリードポジションを務める。

2026年6月、AIエージェントと業務アプリ開発を軸とする株式会社ニューロシンクを設立。2027年に日本へ帰国し、日本の中小企業へのAI導入支援を本格的に開始予定。

著書「AI時代の海外移住戦略

コメント

コメントする

目次