第1章 前提知識(Prerequisites)
1.1 概要
Section titled “1.1 概要”AIエージェント(AI Agent)の開発は、しばしば「プロンプトを書く仕事」だと誤解される。しかし実態は逆に近い。エージェントとは、LLMという確率的なコンポーネントを中心に据えた分散システムである。その周囲を固めているのはHTTPクライアント、リトライ処理、状態管理、認証、ロギングといった、ごく普通のバックエンド技術である。
ロードマップが前提知識として「バックエンド開発の基礎」「Gitとターミナル」「REST APIの知識」の3つを挙げているのは、この構造を反映している。エージェントの品質を最終的に決めるのは、モデルの賢さよりも、その周囲の配管(plumbing)の堅牢さであることが多い。ツール呼び出しが5%の確率でタイムアウトするなら、10ステップのエージェントループは約4割の確率でどこかで失敗する。この計算はプロンプトの巧拙とは無関係だ。
本章は、その配管の最低ラインを確認するための章である。すでに実務でWeb APIを書いている読者は、1.5節のチェックリストだけ確認して第2章に進んでよい。
1.2 バックエンド開発の基礎(Basic Backend Development)
Section titled “1.2 バックエンド開発の基礎(Basic Backend Development)”1.2.1 なぜ必要か
Section titled “1.2.1 なぜ必要か”エージェントを動かすコードを分解すると、LLM固有の処理は驚くほど少ない。典型的なエージェントの構成要素とその実体は次のようになる。
| エージェントの構成要素 | 実体 |
|---|---|
| ツール呼び出し | 外部APIへのHTTPリクエスト、DBクエリ、サブプロセス実行 |
| エージェントループ | while文と例外処理、タイムアウト管理 |
| 会話履歴の保持 | セッションストア(Redis / RDB / ファイル) |
| 長期記憶 | ベクトルDBまたはRDBへのCRUD |
| 並行処理 | 複数ツールの並列実行、非同期I/O |
| デプロイ | コンテナ化、環境変数によるシークレット管理 |
つまり「バックエンドが書ける」という前提が崩れていると、エージェント開発のほぼ全工程で足を取られる。
1.2.2 具体的に必要なスキル
Section titled “1.2.2 具体的に必要なスキル”言語: 本書ではPythonを主に用いる。エージェント関連のエコシステム(公式SDK、LangChain、LlamaIndex、評価ツール群)がPythonで最も充実しているためである。ただしTypeScript/Node.jsも各社が公式SDKを提供しており、フロントエンドと一体化したエージェントを作る場合はこちらが有利になる。
必要なPythonの水準は、おおむね次のとおりである。
- 型ヒント(type hints)とdataclass / Pydanticによるデータ構造定義
- 例外処理とカスタム例外の設計
async/awaitによる非同期処理(複数ツールの並列実行で必須になる)- パッケージ管理と仮想環境(
uv、poetry、venvのいずれか)
非同期処理の重要性は強調しておきたい。エージェントは実行時間の大半を「LLMのレスポンス待ち」と「外部APIの応答待ち」に費やしており、CPUはほぼ遊んでいる。同期的に書くと3つのツールを順番に呼ぶだけで待ち時間が3倍になる。
import asyncioimport httpx
# 悪い例: 3つのツールを順番に実行(合計待ち時間 = 3つの合計)def call_tools_sync(queries: list[str]) -> list[dict]: results = [] with httpx.Client(timeout=10.0) as client: for q in queries: results.append(client.get("https://api.example.com/search", params={"q": q}).json()) return results
# 良い例: 並列実行(合計待ち時間 ≒ 最も遅い1つ)async def call_tools_async(queries: list[str]) -> list[dict]: async with httpx.AsyncClient(timeout=10.0) as client: tasks = [ client.get("https://api.example.com/search", params={"q": q}) for q in queries ] responses = await asyncio.gather(*tasks, return_exceptions=True) # 例外はそのまま返さず、エージェントに返せる形に整形する(第7章で詳述) return [ {"error": str(r)} if isinstance(r, Exception) else r.json() for r in responses ]// 悪い例: 3つのツールを順番に実行(合計待ち時間 = 3つの合計)// JavaScript には同期版のHTTPクライアントが無いため、// Python の同期版に相当するのは「1つずつ await する」書き方になるasync function callToolsSequential(queries: string[]): Promise<unknown[]> { const results: unknown[] = [] for (const q of queries) { const url = new URL('https://api.example.com/search') url.searchParams.set('q', q) // タイムアウトはクライアント単位ではなくリクエスト単位で指定する const response = await fetch(url, { signal: AbortSignal.timeout(10_000) }) results.push(await response.json()) } return results}
// 良い例: 並列実行(合計待ち時間 ≒ 最も遅い1つ)async function callToolsParallel(queries: string[]): Promise<unknown[]> { const tasks = queries.map(async (q) => { const url = new URL('https://api.example.com/search') url.searchParams.set('q', q) const response = await fetch(url, { signal: AbortSignal.timeout(10_000) }) return response.json() }) // asyncio.gather(..., return_exceptions=True) に相当するのが allSettled。 // Promise.all は1つでも失敗すると全体が失敗する const responses = await Promise.allSettled(tasks) // 例外はそのまま返さず、エージェントに返せる形に整形する(第7章で詳述) return responses.map((r) => r.status === 'rejected' ? { error: String(r.reason) } : r.value )}データストア: リレーショナルDB(PostgreSQL等)の基本操作と、キーバリューストア(Redis等)の使いどころを理解していること。ベクトルDBは第8章で扱うが、その前提としてSQLでのCRUDができる必要がある。
環境変数とシークレット管理: APIキーをコードにハードコードしない、.env を .gitignore に入れる、といった基本が徹底されていること。エージェントは複数の外部サービスのキーを扱うため、この規律が崩れると事故が起きやすい。
import osfrom dotenv import load_dotenv
load_dotenv()
# 起動時に必須の環境変数を検証しておくと、# エージェントが10ステップ進んだ後で落ちる事故を防げるREQUIRED = ["ANTHROPIC_API_KEY", "TAVILY_API_KEY", "DATABASE_URL"]missing = [k for k in REQUIRED if not os.environ.get(k)]if missing: raise RuntimeError(f"必須の環境変数が設定されていません: {', '.join(missing)}")// .env の読み込みは Node の起動オプションで行う(`node --env-file=.env app.ts`)。// Python の load_dotenv() に相当するコードは不要
// 起動時に必須の環境変数を検証しておくと、// エージェントが10ステップ進んだ後で落ちる事故を防げるconst REQUIRED = ['ANTHROPIC_API_KEY', 'TAVILY_API_KEY', 'DATABASE_URL']const missing = REQUIRED.filter((k) => !process.env[k])if (missing.length > 0) { throw new Error(`必須の環境変数が設定されていません: ${missing.join(', ')}`)}1.2.3 サーバーフレームワーク
Section titled “1.2.3 サーバーフレームワーク”エージェントを他システムから呼び出せるようにするには、HTTPサーバーとして公開する必要がある。Pythonでは FastAPI が事実上の標準で、非同期対応とストリーミング応答(第3章)の扱いやすさから、エージェントのホスティングに向いている。
1.3 Gitとターミナルの利用(Git and Terminal Usage)
Section titled “1.3 Gitとターミナルの利用(Git and Terminal Usage)”1.3.1 エージェント開発特有の事情
Section titled “1.3.1 エージェント開発特有の事情”Gitとターミナルは一般的な開発スキルだが、エージェント開発では次の3点で特に重みが増す。
第一に、プロンプトはコードである。 システムプロンプトの一文を変えるだけでエージェントの挙動が大きく変わる。したがってプロンプトはコードと同じくバージョン管理下に置き、変更履歴を追える状態にしておく必要がある。「先週は動いていたのに」という事態が起きたとき、git log でプロンプトの差分を追えるかどうかが復旧速度を決める。
プロンプトをPythonの文字列リテラルに直接埋め込むのではなく、独立したファイルに切り出しておくとdiffが読みやすくなる。
prompts/├── system_v1.md├── system_v2.md ← 変更点がgit diffで一目でわかる└── tool_descriptions/ └── web_search.md第二に、エージェント自身がターミナルを使う。 コード実行ツールやシェルツールを持つエージェントを作る場合、開発者自身がシェルの挙動を理解していないと、安全なサンドボックス設計ができない。ここでいう挙動とは、終了コード、標準出力と標準エラーの分離、パイプ、タイムアウトといった事柄である。第13章のセキュリティで扱う「ツールのサンドボックス化」は、この理解を前提としている。
第三に、実験の並行管理が必要になる。 エージェントのアーキテクチャを変えて比較評価する場面が頻繁にある。ブランチを切って複数の実装を並行させ、評価結果(第11章)を突き合わせるワークフローが基本になる。
1.3.2 最低限押さえておくコマンド
Section titled “1.3.2 最低限押さえておくコマンド”日常的に使うのは以下の範囲である。
git branch/switch/merge— 実験の並行管理git diff/log -p— プロンプト変更の追跡git stash— 検証の途中で別ブランチに移るgit bisect— 「いつから精度が落ちたか」の二分探索(評価スクリプトと組み合わせると強力)
ターミナル側では、curl でAPIの挙動を直接確認できること、jq でJSONレスポンスを整形できること、環境変数の設定とプロセスのタイムアウト制御ができることが実用ラインである。
# ツールとして組み込む前に、APIの生の挙動をcurlで確認する習慣は重要curl -s https://api.example.com/search \ -H "Authorization: Bearer $API_KEY" \ -G --data-urlencode "q=AI agents" \ | jq '.results[0] | {title, url}'1.4 REST APIの知識(REST API Knowledge)
Section titled “1.4 REST APIの知識(REST API Knowledge)”1.4.1 二重の意味で必要になる
Section titled “1.4.1 二重の意味で必要になる”REST APIの理解は、エージェント開発において2つの方向で必要になる。この二重性を意識すると学ぶべき内容がはっきりする。
① クライアントとして: LLM APIそのものがREST APIであり、ツールの実体も多くはREST APIである。リクエストの組み立て、認証ヘッダ、レスポンスのパース、そしてエラーハンドリングを正しく書けることが求められる。
② サーバーとして: 作ったエージェントを他システムから使えるようにするとき、REST(あるいはストリーミング用のSSE)で公開することになる。
1.4.2 特に重要なポイント
Section titled “1.4.2 特に重要なポイント”HTTPステータスコードと再試行の判断は、エージェント開発で最も実害が出やすい領域である。LLM APIは高負荷時に 429(レート制限)や 529(過負荷)を返すことがあり、これらを再試行せずに落とすとエージェントが頻繁に途中で死ぬ。
| ステータス | 意味 | エージェントでの扱い |
|---|---|---|
| 400 | リクエスト不正 | 再試行しない。プロンプト/スキーマの修正が必要 |
| 401 / 403 | 認証・認可エラー | 再試行しない。設定の問題 |
| 404 | 対象なし | 再試行しない。ツールの結果として「見つからなかった」と返す |
| 408 / 504 | タイムアウト | 再試行する(バックオフ付き) |
| 429 | レート制限 | 再試行する。Retry-After ヘッダを尊重する |
| 5xx / 529 | サーバー側障害 | 再試行する(指数バックオフ + ジッター) |
指数バックオフ(exponential backoff)にジッター(jitter、ランダムなゆらぎ)を加えるのは、複数のリクエストが同じタイミングで再試行して再び輻輳するのを防ぐためである。
import asyncioimport randomimport httpx
RETRYABLE = {408, 429, 500, 502, 503, 504, 529}
async def request_with_retry( client: httpx.AsyncClient, method: str, url: str, *, max_attempts: int = 5, **kwargs,) -> httpx.Response: """指数バックオフ + ジッター付きのHTTPリクエスト。""" for attempt in range(max_attempts): try: response = await client.request(method, url, **kwargs) except (httpx.TimeoutException, httpx.ConnectError): if attempt == max_attempts - 1: raise else: if response.status_code not in RETRYABLE: return response # 成功、または再試行しても無駄なエラー if attempt == max_attempts - 1: return response
# サーバーが Retry-After を指定していればそれに従う retry_after = response.headers.get("Retry-After") if retry_after and retry_after.isdigit(): await asyncio.sleep(int(retry_after)) continue
# 指数バックオフ + ジッター(0.5〜1.0倍のゆらぎ) delay = (2 ** attempt) * random.uniform(0.5, 1.0) await asyncio.sleep(delay)
raise RuntimeError("到達不能")import { setTimeout as sleep } from 'node:timers/promises'
const RETRYABLE = new Set([408, 429, 500, 502, 503, 504, 529])
/** 指数バックオフ + ジッター付きのHTTPリクエスト。 */async function requestWithRetry( method: string, url: string, init: RequestInit = {}, maxAttempts: number = 5): Promise<Response> { for (let attempt = 0; attempt < maxAttempts; attempt++) { let response: Response | undefined try { response = await fetch(url, { ...init, method }) } catch (e) { // fetch はタイムアウトも接続失敗も TypeError / DOMException で投げるため、 // Python 版のように例外の型で再試行の可否を判断できない。 // 最後の試行だったときだけ、そのまま呼び出し元に投げ直す if (attempt === maxAttempts - 1) throw e } if (response !== undefined) { if (!RETRYABLE.has(response.status)) { return response // 成功、または再試行しても無駄なエラー } if (attempt === maxAttempts - 1) { return response }
// サーバーが Retry-After を指定していればそれに従う const retryAfter = response.headers.get('Retry-After') if (retryAfter !== null && /^\d+$/.test(retryAfter)) { await sleep(Number(retryAfter) * 1000) continue } }
// 指数バックオフ + ジッター(0.5〜1.0倍のゆらぎ) const delay = 2 ** attempt * (0.5 + Math.random() * 0.5) await sleep(delay * 1000) }
throw new Error('到達不能')}補足: 各社の公式SDK(
openai、anthropic等)は再試行ロジックを内蔵しており、通常はmax_retriesパラメータで制御できる。上記のような実装を自前で書く必要があるのは、自作ツールが叩く外部API側であることが多い。
認証方式については、Bearerトークン、APIキーヘッダ、OAuth 2.0の3つを押さえておけば実務の大半をカバーできる。エージェントにユーザーの代理として外部サービスを操作させる場合はOAuth 2.0が絡み、これは第13章の権限管理と直結する。
冪等性(idempotency) も重要な概念である。エージェントは再試行によって同じ操作を複数回実行してしまう可能性がある。「メールを送る」「決済する」といった副作用のあるツールでは、冪等キー(idempotency key)を用いて二重実行を防ぐ設計が必要になる。これは第7章のツール設計で再度扱う。
1.4.3 JSON Schemaの理解
Section titled “1.4.3 JSON Schemaの理解”REST APIと並んで、JSON Schema の読み書きができることが実質的な前提条件になっている。LLMに渡すツール定義(第7章)は、いずれのプロバイダでもパラメータの記述にJSON Schemaを用いるためである。
ただし、JSON Schemaを包む外側の構造はプロバイダごとに異なる。下の例はAnthropic形式で、name / description / input_schema をトップレベルに置く。一方、OpenAIでは {"type": "function", "function": {"name": ..., "parameters": {...}}} のようにキー名も入れ子も異なる。共通なのは「パラメータをJSON Schemaで書く」という点までであり、そのまま流用するとエラーになる。
{ "name": "search_orders", "description": "顧客の注文履歴を検索する", "input_schema": { "type": "object", "properties": { "customer_id": { "type": "string", "description": "顧客ID(例: CUS-00123)" }, "status": { "type": "string", "enum": ["pending", "shipped", "delivered", "cancelled"], "description": "絞り込む注文ステータス。省略時は全件" }, "limit": { "type": "integer", "minimum": 1, "maximum": 100, "default": 20 } }, "required": ["customer_id"] }}PythonではPydanticのモデルからJSON Schemaを自動生成できるため、実務では手書きよりもモデル定義から生成する方式が主流である。
1.5 セットアップチェックリスト
Section titled “1.5 セットアップチェックリスト”第2章に進む前に、以下が満たされているか確認してほしい。
環境
- Python 3.11以上が使える(
asyncまわりの改善が入っているため3.11以上を推奨) - 仮想環境を作れる(
uv venv/python -m venvなど) - Gitリポジトリを初期化し、
.gitignoreに.envを入れてある
アカウントとキー
- LLMプロバイダのAPIキーを1つ以上取得済み(OpenAI / Anthropic / Google のいずれか)
- 利用上限(ハードリミット)を設定してある — 実験中のループ暴走で高額請求が発生する事故は珍しくない
- キーを環境変数として読み込めている
動作確認
- 公式SDKで「Hello」を送って応答が返ってくる
-
curlで同じことができる(SDKがブラックボックスにならないため)
# 動作確認用の最小コードimport anthropic
client = anthropic.Anthropic() # ANTHROPIC_API_KEY を環境変数から読むresponse = client.messages.create( model="claude-sonnet-5", max_tokens=64, messages=[{"role": "user", "content": "1行で自己紹介してください"}],)print(response.content[0].text)print(f"使用トークン: 入力={response.usage.input_tokens} " f"出力={response.usage.output_tokens}")// 動作確認用の最小コードimport Anthropic from '@anthropic-ai/sdk'
const client = new Anthropic() // ANTHROPIC_API_KEY を環境変数から読むconst response = await client.messages.create({ model: 'claude-sonnet-5', max_tokens: 64, messages: [{ role: 'user', content: '1行で自己紹介してください' }],})// content[0] はテキストとは限らない(思考やツール使用も入りうる)ため、// Python 版のように .text を直接読まず、型を確認してから取り出すconst first = response.content[0]console.log(first?.type === 'text' ? first.text : '')console.log( `使用トークン: 入力=${response.usage.input_tokens} ` + `出力=${response.usage.output_tokens}`)理解
- HTTPステータスコードのうち、再試行すべきものとすべきでないものを区別できる
- JSON Schemaで
type/properties/required/enumの意味がわかる - 同期処理と非同期処理の違いを説明できる
1.6 まとめ
Section titled “1.6 まとめ”- AIエージェントは、LLMを部品として組み込んだバックエンドシステムである。エージェント固有のコードは全体の一部にすぎず、大半は通常のバックエンド技術で構成される
- 実行時間の大半がI/O待ちであるため、非同期処理の理解が実用上ほぼ必須になる
- プロンプトはコードと同じくバージョン管理の対象である。挙動の退行を追跡できる状態を最初から作っておく
- REST APIは「LLM APIとツールを呼ぶクライアント側」と「エージェントを公開するサーバー側」の両方で必要になる
- エラーハンドリングと再試行の設計は、エージェントの成功率に直接効く。ステップ数が増えるほど、個々のツールが失敗する確率が全体の成功率を指数的に押し下げる
問1 あるエージェントが1回のタスク完遂に平均12回ツールを呼び出すとする。各ツール呼び出しが独立に2%の確率で失敗し、失敗するとタスク全体が中断されるとき、タスクが最後まで完遂される確率を求めよ。また、再試行機構を入れて個々の失敗確率を0.5%に下げた場合の完遂率と比較し、この章で再試行設計を強調した理由を説明せよ。
問2
1.4.2 の表を参考に、次の状況でエージェントが取るべき挙動を述べよ。
(a) 社内の在庫APIが 404 を返した
(b) LLM APIが 429 を返し、レスポンスに Retry-After: 30 が含まれていた
(c) ツールに渡したJSONがスキーマ違反で 400 が返った
問3 「顧客に確認メールを送信する」ツールを実装するとき、再試行によってメールが二重送信されるのを防ぐにはどのような設計が考えられるか。冪等キーを用いる方法と、それ以外の方法を1つずつ挙げよ。
参考文献・出典
Section titled “参考文献・出典”| 出典 | 内容 | 参照日 |
|---|---|---|
| roadmap.sh — AI Agents Roadmap | 本書全体の構成の基準としたロードマップ | 2026-07-29 |
| Anthropic — Streaming Messages | Messages APIの基本的な呼び出し形式 | 2026-07-29 |
| FastAPI 公式ドキュメント | エージェントのHTTP公開に用いるフレームワーク | 2026-07-29 |
| JSON Schema 公式サイト | ツール定義に用いるスキーマ仕様 | 2026-07-29 |
| httpx 公式ドキュメント | 非同期HTTPクライアント | 2026-07-29 |
次章予告: 第2章では、エージェントの中核部品であるLLMそのものを扱う。トークン化とコンテキストウィンドウという「コストと制約の正体」、そして温度やTop-pといった生成制御パラメータが、エージェントの挙動をどう変えるかを見ていく。
Built with Astro ・ Deployed on Cloudflare Pages
© 2026 watakumi — made with 💜 & ☕ ・watakumi.page