AIエージェントは、目標を与えると自律的にツールを使い・計画を立て・結果を評価しながら問題を解決するプログラムです。単純なチャットボットとの最大の違いは「自分で次の行動を決める」点にあります。2025年現在、AIエージェントは「次のAIシステムの主流形態」として急速に普及しており、OpenAI・Anthropic・Googleいずれも本格的なエージェントフレームワークを整備しています。
この記事では、TypeScriptでAIエージェントをゼロから実装する方法を体系的に解説します。ReActループ・型安全なツール設計・Plan-and-Execute・Reflection(自己改善)・メモリ管理・マルチエージェントシステムまで、実装コードつきで網羅します。Claude APIとVercel AI SDKの両方に対応した実装を紹介します。
「複数ステップのタスクを自動化したい」「LLMにWebを検索させたい」「コードを自動生成→実行→デバッグまでやらせたい」そんな方に最適な内容です。
- AIエージェントの構成要素と主要なアーキテクチャパターン
- 型安全なツール(Function Calling)の設計と実装方法
- ReActループ(Reason + Act)の完全実装
- Plan-and-Executeパターン(計画→実行→検証)の実装
- Reflectionパターン(自己評価・自己改善)の実装
- 短期メモリ(会話履歴)・長期メモリ(ベクトルDB)の実装
- Orchestrator + Workerによるマルチエージェントシステムの設計
- ループ無限化・ハルシネーション・コスト爆発への安全策
AIエージェントとは何か
AIエージェントはPlanning(計画)・Tools(ツール)・Memory(記憶)・Action(行動)の4要素で構成されます。LLMが「頭脳」として機能し、ツールを通じて外部世界と接続することで、単純なテキスト生成を超えた複雑なタスクを自律的に遂行できます。
| 要素 | 役割 | 実装例 |
|---|---|---|
| Planning | タスクを分解して実行順序を決める | LLMによるステップ生成・CoT・ReAct |
| Tools | 外部システムとのインターフェース | Web検索・API呼び出し・DB操作・コード実行 |
| Memory | 過去の情報を保持・参照する | 会話履歴・ベクトルDB・KVストア |
| Action | 決定した行動を実際に実行する | ツール呼び出し・ファイル操作・メール送信 |
エージェントアーキテクチャの比較
| パターン | 仕組み | 適したタスク | 難易度 |
|---|---|---|---|
| ReAct | 思考→行動→観察のループ | 調査・Q&A・順序不定のタスク | 低 |
| Plan-and-Execute | 全体計画を立ててから実行 | 明確な手順があるタスク | 中 |
| Reflection | 出力を自己評価して改善 | コード生成・文章作成品質向上 | 中 |
| Multi-Agent | 複数エージェントが協調 | 並列処理・役割分担が必要なタスク | 高 |
ツールシステムの設計と実装
ツールはエージェントの「手足」です。型安全・エラー安全・副作用の管理という3点を意識して設計すると、堅牢なエージェントを構築できます。
import { z } from "zod";
import type Anthropic from "@anthropic-ai/sdk";
import { zodToJsonSchema as convert } from "zod-to-json-schema"; // npm install zod-to-json-schema
// ツールの型定義
export interface Tool<TInput extends z.ZodType = z.ZodType, TOutput = unknown> {
name: string;
description: string; // LLMへの説明(精度に直結する重要な項目)
parameters: TInput;
execute: (input: z.infer<TInput>) => Promise<TOutput>;
maxRetries?: number; // リトライ回数
timeoutMs?: number; // タイムアウト
}
// ツールを安全に実行するラッパー
export async function executeTool<T extends z.ZodType>(
tool: Tool<T>,
rawInput: unknown
): Promise<{ success: true; output: unknown } | { success: false; error: string }> {
// 入力バリデーション
const parseResult = tool.parameters.safeParse(rawInput);
if (!parseResult.success) {
return {
success: false,
error: `入力バリデーションエラー: ${parseResult.error.message}`,
};
}
// タイムアウト付きで実行
const timeoutMs = tool.timeoutMs ?? 30_000;
const maxRetries = tool.maxRetries ?? 1;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const output = await Promise.race([
tool.execute(parseResult.data),
new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error(`タイムアウト (${timeoutMs}ms)`)), timeoutMs)
),
]);
return { success: true, output };
} catch (error) {
if (attempt === maxRetries) {
return {
success: false,
error: error instanceof Error ? error.message : "不明なエラー",
};
}
// リトライ前に少し待機
await new Promise((r) => setTimeout(r, 1000 * attempt));
}
}
return { success: false, error: "最大リトライ回数に達しました" };
}
// ツールレジストリ(名前でルックアップ)
export class ToolRegistry {
private tools = new Map<string, Tool>();
register(tool: Tool): this {
this.tools.set(tool.name, tool);
return this;
}
get(name: string): Tool | undefined {
return this.tools.get(name);
}
getAll(): Tool[] {
return [...this.tools.values()];
}
// Anthropic/OpenAI APIに渡すツール定義を生成
toAnthropicFormat(): Anthropic.Tool[] {
return this.getAll().map((tool) => ({
name: tool.name,
description: tool.description,
input_schema: zodToJsonSchema(tool.parameters),
}));
}
}
// zodスキーマをJSON Schemaに変換する
function zodToJsonSchema(schema: z.ZodType): Anthropic.Tool["input_schema"] {
return convert(schema) as Anthropic.Tool["input_schema"];
}
import type Anthropic from "@anthropic-ai/sdk";
実用的なツール実装例
import { z } from "zod";
import type { Tool } from "./tools";
// Web検索ツール(Brave Search API使用)
export const webSearchTool: Tool = {
name: "web_search",
description:
"インターネットを検索して最新情報を取得します。リアルタイムの情報や知識カットオフ以降の情報が必要な場合に使います。",
parameters: z.object({
query: z.string().describe("検索クエリ"),
count: z.number().int().min(1).max(10).default(5).describe("取得件数"),
}),
timeoutMs: 10_000,
execute: async ({ query, count }) => {
const resp = await fetch(
`https://api.search.brave.com/res/v1/web/search?q=${encodeURIComponent(query)}&count=${count}`,
{ headers: { "X-Subscription-Token": process.env.BRAVE_API_KEY ?? "" } }
);
if (!resp.ok) throw new Error(`検索API エラー: ${resp.status}`);
const data = await resp.json() as {
web?: { results?: Array<{ title: string; url: string; description: string }> };
};
return (data.web?.results ?? []).map((r) => ({
title: r.title,
url: r.url,
snippet: r.description,
}));
},
};
// コード実行ツール(サンドボックス環境)
export const codeExecutorTool: Tool = {
name: "execute_code",
description:
"TypeScript/JavaScriptコードをNode.js環境で実行して結果を返します。計算・データ変換・テストに使います。",
parameters: z.object({
code: z.string().describe("実行するTypeScript/JavaScriptコード"),
language: z.enum(["typescript", "javascript"]).default("typescript"),
}),
timeoutMs: 15_000,
execute: async ({ code, language }) => {
// 実際のサンドボックス実行(例: e2b.devやDockerコンテナを使用)
// ここではシミュレーション
const safeCode = sanitizeCode(code); // 危険なAPIの呼び出しをブロック
try {
// eval は実際には使わずサンドボックスサービスを利用すること
return { stdout: `コード実行結果: ${safeCode.slice(0, 100)}...`, stderr: "", exitCode: 0 };
} catch (e) {
return { stdout: "", stderr: String(e), exitCode: 1 };
}
},
};
function sanitizeCode(code: string): string {
// 危険なAPIをブロック(最低限の例)
const forbidden = ["process.exit", "child_process", "fs.rm", "fs.unlink"];
for (const f of forbidden) {
if (code.includes(f)) throw new Error(`禁止されたAPI: ${f}`);
}
return code;
}
// テキストファイル読み取りツール
export const fileReaderTool: Tool = {
name: "read_file",
description: "ローカルファイルの内容を読み取ります。コードやドキュメントの分析に使います。",
parameters: z.object({
path: z.string().describe("ファイルパス(相対パス)"),
maxLines: z.number().int().min(1).max(500).default(100),
}),
execute: async ({ path: filePath, maxLines }) => {
const { readFileSync } = await import("fs");
const { resolve } = await import("path");
const safePath = resolve(process.cwd(), filePath);
// パストラバーサル防止
if (!safePath.startsWith(process.cwd())) {
throw new Error("許可されていないパスです");
}
const content = readFileSync(safePath, "utf-8");
const lines = content.split("\n");
return {
content: lines.slice(0, maxLines).join("\n"),
totalLines: lines.length,
truncated: lines.length > maxLines,
};
},
};
ReActエージェントの完全実装
プロンプトエンジニアリングの記事でも触れたReActパターンを、本格的なエージェントとして実装します。Thought(思考)→ Action(行動)→ Observation(観察)を繰り返し、ゴール達成まで自律的に動作します。
import Anthropic from "@anthropic-ai/sdk";
import { ToolRegistry, executeTool } from "../lib/tools";
interface AgentConfig {
model?: string;
maxSteps?: number;
maxTokensPerStep?: number;
systemPrompt?: string;
}
interface StepLog {
step: number;
thought: string;
action?: { tool: string; input: unknown };
observation?: string;
isFinished: boolean;
}
export interface AgentResult {
answer: string;
steps: StepLog[];
totalSteps: number;
stoppedReason: "goal_reached" | "max_steps" | "error";
}
export class ReActAgent {
private client = new Anthropic();
private config: Required<AgentConfig>;
constructor(
private registry: ToolRegistry,
config: AgentConfig = {}
) {
this.config = {
model: config.model ?? "claude-opus-4-6",
maxSteps: config.maxSteps ?? 10,
maxTokensPerStep: config.maxTokensPerStep ?? 1024,
systemPrompt: config.systemPrompt ?? this.defaultSystemPrompt(),
};
}
private defaultSystemPrompt(): string {
return `あなたは自律的に問題を解決するAIエージェントです。
以下のツールを使って目標を達成してください。
利用可能なツール:
${this.registry.getAll().map((t) => `- ${t.name}: ${t.description}`).join("\n")}
行動原則:
1. まず現在の状況と次のステップを考える(Thought)
2. 必要なツールを選んで実行する(Action)
3. 結果を評価して次の行動を決める(Observation)
4. 目標を達成したら「FINAL ANSWER: [回答]」と返す
5. ツールなしで回答できる場合も「FINAL ANSWER: [回答]」と返す`;
}
async run(task: string): Promise<AgentResult> {
const messages: Anthropic.MessageParam[] = [
{ role: "user", content: task },
];
const steps: StepLog[] = [];
let stoppedReason: AgentResult["stoppedReason"] = "max_steps";
for (let step = 1; step <= this.config.maxSteps; step++) {
console.log(`\n--- Step ${step}/${this.config.maxSteps} ---`);
const response = await this.client.messages.create({
model: this.config.model,
max_tokens: this.config.maxTokensPerStep,
system: this.config.systemPrompt,
tools: this.registry.toAnthropicFormat(),
messages,
});
const stepLog: StepLog = { step, thought: "", isFinished: false };
// テキストブロックから思考を抽出
const textBlock = response.content.find((b) => b.type === "text");
if (textBlock) {
stepLog.thought = (textBlock as { type: "text"; text: string }).text;
console.log("Thought:", stepLog.thought.slice(0, 200));
// FINAL ANSWERが含まれていたら終了
if (stepLog.thought.includes("FINAL ANSWER:")) {
stepLog.isFinished = true;
steps.push(stepLog);
const answer = stepLog.thought.split("FINAL ANSWER:")[1].trim();
stoppedReason = "goal_reached";
return { answer, steps, totalSteps: step, stoppedReason };
}
}
// ツール呼び出しがなければ終了
if (response.stop_reason === "end_turn") {
stepLog.isFinished = true;
steps.push(stepLog);
stoppedReason = "goal_reached";
return {
answer: stepLog.thought,
steps,
totalSteps: step,
stoppedReason,
};
}
// ツール呼び出しを処理
messages.push({ role: "assistant", content: response.content });
const toolResults: Anthropic.ToolResultBlockParam[] = [];
for (const block of response.content) {
if (block.type !== "tool_use") continue;
const tool = this.registry.get(block.name);
if (!tool) {
toolResults.push({
type: "tool_result",
tool_use_id: block.id,
content: `エラー: ツール "${block.name}" が見つかりません`,
is_error: true,
});
continue;
}
stepLog.action = { tool: block.name, input: block.input };
console.log(`Action: ${block.name}(${JSON.stringify(block.input).slice(0, 100)})`);
const result = await executeTool(tool, block.input);
const observation = result.success
? JSON.stringify(result.output, null, 2)
: `エラー: ${result.error}`;
stepLog.observation = observation.slice(0, 500);
console.log("Observation:", stepLog.observation);
toolResults.push({
type: "tool_result",
tool_use_id: block.id,
content: observation,
is_error: !result.success,
});
}
messages.push({ role: "user", content: toolResults });
steps.push(stepLog);
}
// 最大ステップ数に達した場合
const lastStep = steps[steps.length - 1];
return {
answer: lastStep?.thought ?? "最大ステップ数に達しました",
steps,
totalSteps: this.config.maxSteps,
stoppedReason,
};
}
}
// 使用例
const registry = new ToolRegistry()
.register(webSearchTool)
.register(codeExecutorTool);
const agent = new ReActAgent(registry, { maxSteps: 8 });
const result = await agent.run(
"TypeScriptの最新バージョンを調べて、主要な新機能を3つ教えてください。"
);
console.log("\n=== 最終回答 ===\n", result.answer);
console.log(`\n合計ステップ数: ${result.totalSteps}, 終了理由: ${result.stoppedReason}`);
import { webSearchTool, codeExecutorTool } from "../lib/builtin-tools";
Plan-and-Executeパターン:計画してから実行する
ReActは柔軟ですが、複雑なタスクでは途中で方向を見失うことがあります。Plan-and-Executeは最初に全体計画を立て、ステップごとに実行するパターンで、長いタスク・依存関係のあるサブタスクに向いています。
import Anthropic from "@anthropic-ai/sdk";
import { generateObject } from "ai";
import { anthropic as aiSdkAnthropic } from "@ai-sdk/anthropic";
import { z } from "zod";
import { ToolRegistry, executeTool } from "../lib/tools";
const PlanSchema = z.object({
goal: z.string().describe("最終目標の明確な定義"),
steps: z.array(
z.object({
id: z.number(),
description: z.string().describe("このステップで何をするか"),
tool: z.string().optional().describe("使用するツール名(ツール不要ならundefined)"),
dependsOn: z.array(z.number()).describe("依存するステップのID"),
expectedOutput: z.string().describe("期待される出力の説明"),
})
),
estimatedSteps: z.number().int(),
});
type Plan = z.infer<typeof PlanSchema>;
interface StepResult {
stepId: number;
output: string;
success: boolean;
}
export class PlanExecuteAgent {
private client = new Anthropic();
constructor(private registry: ToolRegistry) {}
// フェーズ1: 計画を生成
async plan(task: string): Promise<Plan> {
const toolDescriptions = this.registry
.getAll()
.map((t) => `- ${t.name}: ${t.description}`)
.join("\n");
const { object: plan } = await generateObject({
model: aiSdkAnthropic("claude-opus-4-6"),
schema: PlanSchema,
prompt: `以下のタスクを達成するための実行計画を立ててください。
タスク: ${task}
利用可能なツール:
${toolDescriptions}
計画は具体的で実行可能なステップに分解してください。
依存関係を明確にし、並列実行できるステップは依存関係なしとしてください。`,
});
console.log("計画:", JSON.stringify(plan, null, 2));
return plan;
}
// フェーズ2: ステップを実行
async execute(plan: Plan): Promise<StepResult[]> {
const results: StepResult[] = [];
const completedSteps = new Set<number>();
// トポロジカルソートで実行順を決定
const sorted = this.topologicalSort(plan.steps);
for (const step of sorted) {
// 依存ステップが完了しているか確認
const depsCompleted = step.dependsOn.every((id) => completedSteps.has(id));
if (!depsCompleted) {
results.push({ stepId: step.id, output: "依存ステップが失敗したためスキップ", success: false });
continue;
}
console.log(`\nステップ${step.id}: ${step.description}`);
// 前のステップの結果をコンテキストとして収集
const previousContext = results
.filter((r) => step.dependsOn.includes(r.stepId))
.map((r) => `[ステップ${r.stepId}の結果] ${r.output}`)
.join("\n");
if (step.tool) {
// ツールを使うステップ
const tool = this.registry.get(step.tool);
if (!tool) {
results.push({ stepId: step.id, output: `ツール ${step.tool} が見つかりません`, success: false });
continue;
}
// LLMにツールの入力を決めさせる
const inputResponse = await this.client.messages.create({
model: "claude-opus-4-6",
max_tokens: 512,
messages: [{
role: "user",
content: `以下のステップを実行するためのツール入力を JSON で返してください。
ステップ: ${step.description}
使用ツール: ${step.tool}
ツールのパラメータ: ${JSON.stringify(tool.parameters._def)}
前のステップの結果:
${previousContext || "なし"}
JSONのみ返してください:`,
}],
});
const inputText = (inputResponse.content[0] as { text: string }).text;
const jsonMatch = inputText.match(/\{[\s\S]*\}/);
const toolInput = jsonMatch ? JSON.parse(jsonMatch[0]) : {};
const result = await executeTool(tool, toolInput);
results.push({
stepId: step.id,
output: result.success ? JSON.stringify(result.output) : `エラー: ${result.error}`,
success: result.success,
});
} else {
// ツールなし(LLMのみで処理)
const response = await this.client.messages.create({
model: "claude-opus-4-6",
max_tokens: 1024,
messages: [{
role: "user",
content: `以下のステップを実行してください。
ステップ: ${step.description}
期待される出力: ${step.expectedOutput}
前のステップの結果:
${previousContext || "なし"}`,
}],
});
results.push({
stepId: step.id,
output: (response.content[0] as { text: string }).text,
success: true,
});
}
completedSteps.add(step.id);
}
return results;
}
// フェーズ3: 結果を統合して最終回答を生成
async synthesize(task: string, plan: Plan, results: StepResult[]): Promise<string> {
const summary = results
.map((r) => `[ステップ${r.stepId}] ${r.success ? "成功" : "失敗"}: ${r.output.slice(0, 300)}`)
.join("\n");
const response = await this.client.messages.create({
model: "claude-opus-4-6",
max_tokens: 1024,
messages: [{
role: "user",
content: `以下の実行結果をもとに、最終的な回答を生成してください。
元のタスク: ${task}
実行結果:
${summary}
ユーザーへの最終回答を生成してください:`,
}],
});
return (response.content[0] as { text: string }).text;
}
async run(task: string): Promise<string> {
const plan = await this.plan(task);
const results = await this.execute(plan);
return this.synthesize(task, plan, results);
}
private topologicalSort(
steps: Plan["steps"]
): Plan["steps"] {
const sorted: Plan["steps"] = [];
const visited = new Set<number>();
const visit = (step: (typeof steps)[0]) => {
if (visited.has(step.id)) return;
visited.add(step.id);
for (const depId of step.dependsOn) {
const dep = steps.find((s) => s.id === depId);
if (dep) visit(dep);
}
sorted.push(step);
};
steps.forEach((s) => visit(s));
return sorted;
}
}
Reflectionパターン:自己評価と自己改善
Reflectionはエージェントが自分の出力を批評し、改善を繰り返すパターンです。コード生成・文章作成・プランニングなど、品質の定義が可能なタスクで大きな効果があります。
import Anthropic from "@anthropic-ai/sdk";
interface ReflectionConfig {
maxIterations?: number; // 最大改善回数
model?: string;
critiqueModel?: string; // 批評用モデル(異なるモデルも可)
}
interface Iteration {
draft: string;
critique: string;
score: number; // 0〜10
improved: boolean;
}
export async function reflectionAgent(
task: string,
config: ReflectionConfig = {}
): Promise<{ finalOutput: string; iterations: Iteration[] }> {
const {
maxIterations = 3,
model = "claude-opus-4-6",
critiqueModel = "claude-opus-4-6",
} = config;
const client = new Anthropic();
const iterations: Iteration[] = [];
let currentDraft = "";
// 初回ドラフト生成
const initialResponse = await client.messages.create({
model,
max_tokens: 2048,
messages: [{ role: "user", content: task }],
});
currentDraft = (initialResponse.content[0] as { text: string }).text;
for (let i = 0; i < maxIterations; i++) {
console.log(`\n--- Reflection 第${i + 1}回 ---`);
// 批評フェーズ:現在のドラフトを評価
const critiqueResponse = await client.messages.create({
model: critiqueModel,
max_tokens: 1024,
system: `あなたはコードレビュー・品質評価の専門家です。
以下の観点で評価し、JSON形式で返してください:
{
"score": 0〜10の整数(10が最高品質),
"strengths": ["良い点1", "良い点2"],
"weaknesses": ["改善点1", "改善点2"],
"suggestions": ["具体的な改善提案1", "改善提案2"]
}`,
messages: [{
role: "user",
content: `以下の出力を評価してください。
元のタスク: ${task}
出力:
${currentDraft}`,
}],
});
const critiqueText = (critiqueResponse.content[0] as { text: string }).text;
const jsonMatch = critiqueText.match(/\{[\s\S]*\}/);
const critique = jsonMatch
? JSON.parse(jsonMatch[0]) as { score: number; strengths: string[]; weaknesses: string[]; suggestions: string[] }
: { score: 5, strengths: [], weaknesses: [], suggestions: [] };
console.log(`スコア: ${critique.score}/10`);
console.log("改善点:", critique.weaknesses);
iterations.push({
draft: currentDraft,
critique: critiqueText,
score: critique.score,
improved: false,
});
// スコアが十分高ければ終了
if (critique.score >= 9) {
console.log("品質基準を達成。終了します。");
break;
}
// 改善フェーズ:批評を元に改善
const improvementResponse = await client.messages.create({
model,
max_tokens: 2048,
messages: [{
role: "user",
content: `元のタスク: ${task}
現在のドラフト:
${currentDraft}
以下のフィードバックを元に改善してください:
- 改善点: ${critique.weaknesses.join(", ")}
- 提案: ${critique.suggestions.join(", ")}
改善されたバージョンを出力してください:`,
}],
});
const improvedDraft = (improvementResponse.content[0] as { text: string }).text;
// 改善されたかチェック(スコア上昇 or 明確な変更あり)
if (improvedDraft !== currentDraft) {
iterations[iterations.length - 1].improved = true;
currentDraft = improvedDraft;
} else {
console.log("これ以上の改善が難しい。終了します。");
break;
}
}
return { finalOutput: currentDraft, iterations };
}
// 使用例:TypeScriptコードの自動生成と自己改善
const result = await reflectionAgent(
"TypeScriptでイベントエミッターを実装してください。型安全・メモリリーク防止・エラーハンドリングを考慮してください。",
{ maxIterations: 3 }
);
console.log("\n=== 最終出力 ===\n", result.finalOutput);
console.log(`\n改善回数: ${result.iterations.length}`);
result.iterations.forEach((it, i) => {
console.log(`第${i + 1}回 スコア: ${it.score}/10, 改善: ${it.improved}`);
});
エージェントのメモリ管理
エージェントが長期間にわたって有効に機能するためには、適切なメモリ管理が欠かせません。メモリには短期・作業・長期の3種類があり、それぞれ異なる実装アプローチがあります。
| 種類 | 特徴 | 実装方法 | 用途 |
|---|---|---|---|
| 短期メモリ | 現在の会話内のみ有効 | messagesの配列 | 会話の文脈維持 |
| 作業メモリ | タスク実行中のみ有効 | エージェントの変数 | 中間結果・ステップ状態 |
| 長期メモリ | セッションをまたいで永続 | ベクトルDB・KVストア | 知識・ユーザー情報・過去の経験 |
import { generateEmbedding } from "./embeddings"; // RAGガイドの実装を再利用
interface MemoryEntry {
id: string;
content: string;
importance: number; // 0〜1: 重要度(長期記憶への昇格判断に使用)
createdAt: Date;
tags: string[];
}
// 短期メモリ:会話履歴の管理(コンテキスト長を超えないよう制御)
export class ShortTermMemory {
private messages: Array<{ role: "user" | "assistant"; content: string }> = [];
private maxMessages: number;
constructor(maxMessages: number = 20) {
this.maxMessages = maxMessages;
}
add(role: "user" | "assistant", content: string): void {
this.messages.push({ role, content });
// 超過した分を古い順に削除(ただし最初のシステムメッセージは保持)
if (this.messages.length > this.maxMessages) {
this.messages.splice(1, this.messages.length - this.maxMessages);
}
}
getMessages() {
return [...this.messages];
}
clear(): void {
this.messages = [];
}
}
// 長期メモリ:重要な情報をベクトルDBに保存・検索
export class LongTermMemory {
private entries: MemoryEntry[] = []; // 実際はpgvector等に保存
async remember(content: string, tags: string[] = [], importance = 0.5): Promise<void> {
const entry: MemoryEntry = {
id: crypto.randomUUID(),
content,
importance,
createdAt: new Date(),
tags,
};
// 実際はEmbeddingを生成してDBに保存
// const embedding = await generateEmbedding(content);
// await db.insert(memoryTable).values({ ...entry, embedding });
this.entries.push(entry);
console.log(`記憶に保存: "${content.slice(0, 50)}..." (importance: ${importance})`);
}
async recall(query: string, topK: number = 5): Promise<MemoryEntry[]> {
// 実際はベクトル検索(RAGガイドのhybridSearchを流用)
// return await vectorSearch(query, topK);
// ここでは簡易的なキーワードマッチ
return this.entries
.filter((e) => e.content.toLowerCase().includes(query.toLowerCase()))
.sort((a, b) => b.importance - a.importance)
.slice(0, topK);
}
// エピソード記憶:過去の会話セッションを圧縮して保存
async summarizeAndStore(session: { role: string; content: string }[]): Promise<void> {
if (session.length < 4) return; // 短いセッションはスキップ
const Anthropic = (await import("@anthropic-ai/sdk")).default;
const client = new Anthropic();
const response = await client.messages.create({
model: "claude-opus-4-6",
max_tokens: 256,
messages: [{
role: "user",
content: `以下の会話から重要な情報(決定事項・ユーザーの好み・学んだこと)を3点以内で箇条書きにしてください:\n\n${
session.map((m) => `${m.role}: ${m.content}`).join("\n")
}`,
}],
});
const summary = (response.content[0] as { text: string }).text;
await this.remember(summary, ["session-summary"], 0.8);
}
}
マルチエージェントシステムの設計
タスクが複雑になるほど、単一エージェントでは限界があります。Orchestrator(司令塔)が複数のWorker(専門エージェント)に指示するマルチエージェントパターンで、複雑なワークフローを実現します。
import Anthropic from "@anthropic-ai/sdk";
import { generateObject } from "ai";
import { anthropic as aiSdkAnthropic } from "@ai-sdk/anthropic";
import { z } from "zod";
// 専門エージェントの定義
interface WorkerAgent {
name: string;
description: string;
capabilities: string[];
run: (task: string, context?: string) => Promise<string>;
}
// Orchestrator:タスクを分析して適切なWorkerに委譲
export class OrchestratorAgent {
private client = new Anthropic();
constructor(private workers: WorkerAgent[]) {}
async route(task: string): Promise<string> {
// どのWorkerが最適かをLLMに判断させる
const WorkerSelectionSchema = z.object({
selectedWorker: z.string().describe("担当するWorker名"),
subTask: z.string().describe("Workerに渡す具体的な指示"),
reasoning: z.string().describe("選択理由"),
});
const workerDescriptions = this.workers
.map((w) => `- ${w.name}: ${w.description}(得意: ${w.capabilities.join(", ")})`)
.join("\n");
const { object: selection } = await generateObject({
model: aiSdkAnthropic("claude-opus-4-6"),
schema: WorkerSelectionSchema,
prompt: `以下のタスクを処理するのに最も適したWorkerを選んでください。
タスク: ${task}
利用可能なWorker:
${workerDescriptions}`,
});
console.log(`\nOrchestrator: "${selection.selectedWorker}" に委譲 (理由: ${selection.reasoning})`);
const worker = this.workers.find((w) => w.name === selection.selectedWorker);
if (!worker) throw new Error(`Worker "${selection.selectedWorker}" が見つかりません`);
return worker.run(selection.subTask);
}
// 並列実行:独立したサブタスクを同時に処理
async runParallel(tasks: string[]): Promise<string[]> {
console.log(`\n${tasks.length}タスクを並列実行...`);
return Promise.all(tasks.map((task) => this.route(task)));
}
// 逐次実行:前のタスクの結果を次のタスクに渡す
async runSequential(tasks: string[]): Promise<string> {
let context = "";
let lastResult = "";
for (const task of tasks) {
const worker = this.workers[0]; // 簡略化
lastResult = await worker.run(task, context);
context += `\n前のステップ結果: ${lastResult.slice(0, 500)}`;
}
return lastResult;
}
}
// 専門エージェントの実装例
// リサーチャーエージェント(情報収集専門)
export function createResearcherAgent(searchTool: (q: string) => Promise<string>): WorkerAgent {
const client = new Anthropic();
return {
name: "researcher",
description: "インターネットを検索して情報を収集・整理します",
capabilities: ["Web検索", "情報収集", "要約", "ファクトチェック"],
run: async (task, context) => {
const searchResult = await searchTool(task);
const response = await client.messages.create({
model: "claude-opus-4-6",
max_tokens: 1024,
messages: [{
role: "user",
content: `以下の検索結果を整理して、タスクに回答してください。\n\nタスク: ${task}\n\n検索結果: ${searchResult}\n\n${context ?? ""}`,
}],
});
return (response.content[0] as { text: string }).text;
},
};
}
// コーダーエージェント(コード生成専門)
export function createCoderAgent(): WorkerAgent {
const client = new Anthropic();
return {
name: "coder",
description: "TypeScript/JavaScriptコードの設計・実装・レビューを行います",
capabilities: ["コード生成", "バグ修正", "リファクタリング", "型定義"],
run: async (task, context) => {
const response = await client.messages.create({
model: "claude-opus-4-6",
max_tokens: 2048,
system: "あなたはTypeScriptの専門家です。型安全で実用的なコードを生成してください。",
messages: [{
role: "user",
content: `${task}\n\n${context ? `参考情報: ${context}` : ""}`,
}],
});
return (response.content[0] as { text: string }).text;
},
};
}
// マルチエージェントシステムの使用例
const orchestrator = new OrchestratorAgent([
createResearcherAgent(async (q) => `${q}の検索結果`),
createCoderAgent(),
]);
// タスクを自動的に適切なWorkerに振り分け
const result = await orchestrator.route(
"TypeScript 5.5の新機能を調べて、サンプルコードを実装してください"
);
console.log(result);
安全策とガードレール
エージェントは自律的に動作するため、適切なガードレールがないと意図しない動作やコスト爆発につながります。以下の対策を必ず実装してください。
// エージェントの安全策まとめ
interface GuardrailConfig {
maxSteps: number; // 最大実行ステップ数
maxCostUsd: number; // 最大コスト上限(USD)
maxTimeMs: number; // タイムアウト(ms)
forbiddenTools: string[]; // 実行禁止ツール
requireHumanApproval: string[]; // 人間の承認が必要なツール
}
export class AgentGuardrails {
private steps = 0;
private estimatedCostUsd = 0;
private startTime = Date.now();
constructor(private config: GuardrailConfig) {}
// ステップ数チェック
checkStep(): void {
this.steps++;
if (this.steps > this.config.maxSteps) {
throw new Error(`最大ステップ数(${this.config.maxSteps})に達しました`);
}
}
// コスト上限チェック(Claude claude-opus-4-6: input $15/M, output $75/M tokens)
addCost(inputTokens: number, outputTokens: number): void {
const cost = (inputTokens * 15 + outputTokens * 75) / 1_000_000;
this.estimatedCostUsd += cost;
if (this.estimatedCostUsd > this.config.maxCostUsd) {
throw new Error(
`コスト上限($${this.config.maxCostUsd})を超えました。推定コスト: $${this.estimatedCostUsd.toFixed(4)}`
);
}
}
// タイムアウトチェック
checkTimeout(): void {
const elapsed = Date.now() - this.startTime;
if (elapsed > this.config.maxTimeMs) {
throw new Error(`タイムアウト(${this.config.maxTimeMs}ms)に達しました`);
}
}
// ツール使用の承認
async approveToolUse(toolName: string, input: unknown): Promise<boolean> {
if (this.config.forbiddenTools.includes(toolName)) {
throw new Error(`ツール "${toolName}" の使用は禁止されています`);
}
if (this.config.requireHumanApproval.includes(toolName)) {
// 実際は対話的な承認UIを呼び出す
console.warn(
`⚠️ ツール "${toolName}" の実行には承認が必要です。\n入力: ${JSON.stringify(input, null, 2)}`
);
// デモ: stdioで確認
const readline = await import("readline");
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
return new Promise((resolve) => {
rl.question("承認しますか? (y/n): ", (answer) => {
rl.close();
resolve(answer.toLowerCase() === "y");
});
});
}
return true;
}
getStats() {
return {
steps: this.steps,
estimatedCostUsd: this.estimatedCostUsd,
elapsedMs: Date.now() - this.startTime,
};
}
}
// デフォルト設定
export const defaultGuardrails = new AgentGuardrails({
maxSteps: 15,
maxCostUsd: 0.50, // $0.50上限
maxTimeMs: 5 * 60 * 1000, // 5分
forbiddenTools: ["delete_file", "drop_database"],
requireHumanApproval: ["send_email", "publish_content"],
});
- 最大ステップ数を必ず設定する(設定なしは無限ループの危険あり)
- コスト上限を設ける(特にReflectionエージェントは多くのAPIコールを発生させる)
- 破壊的な操作(ファイル削除・DB操作・外部送信)は人間の承認を必須にする
- サンドボックスでコードを実行する(eval()の直接使用は禁止)
- 入出力をすべてログに残す(デバッグ・監査のため)
エージェントのデバッグと評価
// エージェントの実行を詳細にログする
export class AgentLogger {
private logs: Array<{
timestamp: Date;
type: "thought" | "action" | "observation" | "error" | "final";
content: string;
metadata?: Record<string, unknown>;
}> = [];
log(
type: (typeof this.logs)[0]["type"],
content: string,
metadata?: Record<string, unknown>
): void {
const entry = { timestamp: new Date(), type, content, metadata };
this.logs.push(entry);
// カラー付きコンソール出力
const colors = {
thought: "\x1b[36m", // シアン
action: "\x1b[33m", // 黄色
observation: "\x1b[32m", // 緑
error: "\x1b[31m", // 赤
final: "\x1b[35m", // マゼンタ
};
const reset = "\x1b[0m";
console.log(`${colors[type]}[${type.toUpperCase()}]${reset} ${content.slice(0, 200)}`);
}
exportTrace(): string {
return JSON.stringify(this.logs, null, 2);
}
// LLMを使って実行トレースを分析・改善提案
async analyzeTrace(goal: string): Promise<string> {
const Anthropic = (await import("@anthropic-ai/sdk")).default;
const client = new Anthropic();
const traceText = this.logs
.map((l) => `[${l.type}] ${l.content.slice(0, 200)}`)
.join("\n");
const response = await client.messages.create({
model: "claude-opus-4-6",
max_tokens: 512,
messages: [{
role: "user",
content: `以下のエージェント実行トレースを分析してください。
目標: ${goal}
実行トレース:
${traceText}
分析してください:
1. 無駄なステップはあったか
2. 失敗した箇所とその原因
3. 改善できる点`,
}],
});
return (response.content[0] as { text: string }).text;
}
}
まとめ
AIエージェントは「目標 → 計画 → ツール実行 → 評価 → 改善」のループで動作します。アーキテクチャを適切に選ぶことが品質とコスト効率の鍵です。
| パターン | 最適なタスク | コスト | 実装難易度 |
|---|---|---|---|
| ReAct | 探索的な調査・Q&A・順序不定タスク | 低〜中 | 低 |
| Plan-and-Execute | 明確な手順・依存関係があるタスク | 中 | 中 |
| Reflection | 品質が重要な生成タスク(コード・文章) | 中〜高 | 中 |
| Multi-Agent | 並列処理・専門性の分業が有効なタスク | 高 | 高 |
まずReActエージェントで動くものを作り、品質が足りなければReflectionを追加、複雑化したらMulti-Agentに分解する段階的なアプローチが実務では現実的です。ツールの設計・ガードレール・ログの整備は最初から行うことで、後の保守コストを大幅に下げられます。
ツール呼び出しの基本はプロンプトエンジニアリング完全ガイドのReActセクション、Vercel AI SDKを使ったエージェント実装はVercel AI SDK完全ガイド、エージェントの知識基盤にはRAG完全実装ガイドが参考になります。
よくある質問
QReActとPlan-and-Executeはどう使い分けますか?
A事前に全体の手順が明確でないタスク(調査・Q&A等)はReActが向いています。一方、「①データ収集 → ②分析 → ③レポート作成」のように手順が決まっているタスクはPlan-and-Executeが効率的です。実務では最初にReActで試し、パフォーマンスが安定しないときにPlan-and-Executeへ移行するパターンが多いです。
Qエージェントが同じツールを何度も呼び続けて止まらない場合はどうすればよいですか?
A主な原因は①ツールの出力がLLMに正しく伝わっていない、②システムプロンプトに終了条件が明示されていない、の2つです。対策として、maxStepsを設定する(必須)、ツールの出力を簡潔にする、システムプロンプトに「同じツールを連続3回呼んだら別のアプローチを試す」などの制約を追加する、の3点が効果的です。
Qコード実行ツールを安全に実装するにはどうすればよいですか?
Aeval()やFunction()を直接使うのは危険です。代わりにe2b.dev(サンドボックスAPI)やDockerコンテナを使った分離実行環境を推奨します。e2b.devはTypeScript SDKを提供しており、`npm install @e2b/code-interpreter`で簡単に導入できます。実行できる言語・利用可能なパッケージ・CPU/メモリ上限も設定できるため、本番環境でも安心して使えます。
Qエージェントのコストが高くなりすぎる場合の対処法は?
Aまずモデルの選定を見直しましょう。Reflectionの批評フェーズや単純なルーティングにはclaude-haiku-4-5のような軽量モデルで十分なケースが多いです。また、中間結果をキャッシュする(同じツール呼び出しの重複排除)、プロンプトを短縮する(不要な説明を削除)、maxTokensを適切に制限する、の3点で大幅なコスト削減ができます。
QマルチエージェントでWorker間のコミュニケーションはどう設計すればよいですか?
Aシンプルなケースは「文字列を渡すだけ」で十分です。複雑なケースでは構造化データ(JSON)を使います。Workerの出力をそのまま次のWorkerに渡すのではなく、Orchestratorが一度要約・整形してから渡すと品質が安定します。将来的にスケールする場合はメッセージキュー(Redis・Bull)を使った非同期アーキテクチャが適しています。
Qエージェントのテストはどうやって書けばよいですか?
A単体テストはツールの実行ロジック(executeTool)を直接テストします。統合テストはツールをモック化して(jest.mock等)、エージェントの制御フローをテストします。エンドツーエンドテストはコストがかかるため、重要なシナリオを数個に絞って定期的に実行するのがバランスが良いです。AgentLoggerで実行トレースを保存し、問題発生時に再現できる環境を整備することが最も重要です。

