AIコスト最適化戦略
💰 AIコスト最適化戦略:ポケモンセンターの節約術
Section titled “💰 AIコスト最適化戦略:ポケモンセンターの節約術”📝 この章について
Section titled “📝 この章について”AIコスト最適化を「ポケモンセンターでの回復費用削減」に例えて解説します。LLM APIは強力ですが、使い方次第で月額数百万円のコストが発生します。賢く使えば90%削減も可能です。
ポケモンでいえば
Section titled “ポケモンでいえば”- API料金 = ポケモンセンターでの回復費用
- プロンプト圧縮 = キズぐすり1個で済ませる(いいキズぐすりを使わない)
- キャッシング = 同じ技を何度も使うなら、1回だけPPを消費
- ローカルモデル = 自宅で回復(ポケモンセンターに行かない)
- バッチ処理 = まとめて回復して割引を受ける
🎯 この章で学べること
Section titled “🎯 この章で学べること”| 項目 | 内容 |
|---|---|
| コスト構造の理解 | トークン課金、コンテキスト課金、画像生成料金等 |
| ユーザーサービスの最適化 | ChatBot等の対外サービスで課金を抑える |
| 自社利用の最適化 | 社内AI活用でコストを削減 |
| モデル選択戦略 | GPT-4 vs GPT-3.5 vs Claude vs ローカルモデル |
| 実装テクニック | キャッシング、プロンプト圧縮、バッチ処理等 |
💸 AIコストの全体像
Section titled “💸 AIコストの全体像”主要LLMのコスト比較(2024年7月時点)
Section titled “主要LLMのコスト比較(2024年7月時点)”| モデル | 入力(1M tokens) | 出力(1M tokens) | コンテキスト長 | ポケモンの例え |
|---|---|---|---|---|
| GPT-4 Turbo | $10.00 | $30.00 | 128K | すごいキズぐすり(高いが確実) |
| GPT-4o | $5.00 | $15.00 | 128K | いいキズぐすり(バランス型) |
| GPT-3.5 Turbo | $0.50 | $1.50 | 16K | キズぐすり(安いが性能低い) |
| Claude 3.5 Sonnet | $3.00 | $15.00 | 200K | まんたんのくすり(大量回復に最適) |
| Gemini 1.5 Pro | $3.50 | $10.50 | 1M | かいふくのくすり(超大容量) |
| Llama 3 70B (local) | $0.00 | $0.00 | 8K | 自宅で回復(無料だが初期投資必要) |
コスト発生の内訳
Section titled “コスト発生の内訳”// 典型的なChatBotの1会話あたりコスト試算const costCalculator = { // システムプロンプト(毎回送信) systemPrompt: { tokens: 500, costPer1M: 5.00, // GPT-4o入力 costPerRequest: (500 / 1_000_000) * 5.00 // $0.0025 },
// ユーザーメッセージ(平均) userMessage: { tokens: 50, costPer1M: 5.00, costPerRequest: (50 / 1_000_000) * 5.00 // $0.00025 },
// 会話履歴(10往復分) conversationHistory: { tokens: 2000, costPer1M: 5.00, costPerRequest: (2000 / 1_000_000) * 5.00 // $0.01 },
// AI応答(平均) aiResponse: { tokens: 200, costPer1M: 15.00, // GPT-4o出力 costPerRequest: (200 / 1_000_000) * 15.00 // $0.003 },
// 合計: $0.01575 / リクエスト totalPerRequest: 0.01575,
// 月間100万リクエストで$15,750!};
// 実際の計算console.log(`月間10万リクエスト: $${(0.01575 * 100_000).toFixed(2)}`); // $1,575console.log(`月間100万リクエスト: $${(0.01575 * 1_000_000).toFixed(2)}`); // $15,750🎯 戦略1: プロンプト最適化(90%削減も可能)
Section titled “🎯 戦略1: プロンプト最適化(90%削減も可能)”教科書的な説明
Section titled “教科書的な説明”プロンプトを短くすれば、トークン数が減り、コストが下がります。
ポケモン版・マサカリ的実務視点
Section titled “ポケモン版・マサカリ的実務視点”テクニック1: システムプロンプトの圧縮
Section titled “テクニック1: システムプロンプトの圧縮”// ❌ 悪い例: 冗長なシステムプロンプト(500 tokens)const verbosePrompt = `You are a helpful, respectful, and honest assistant for ACME Corporation.Your primary goal is to provide accurate and helpful information to our customers.Please ensure that your answers are socially unbiased and positive in nature.If a question does not make any sense, or is not factually coherent,explain why instead of answering something not correct.If you don't know the answer to a question, please don't share false information.Always maintain a professional tone and be courteous to the user....(さらに続く)`;
// ✅ 良い例: 圧縮版(80 tokens、84%削減)const compressedPrompt = `You're ACME Corp's assistant. Be helpful, accurate, unbiased.If unsure, say so. Stay professional.`;
// コスト削減:// Before: 500 tokens × $5/1M = $0.0025/request// After: 80 tokens × $5/1M = $0.0004/request// 節約額: $0.0021/request → 月間10万req で $210削減テクニック2: Few-Shot例の動的選択
Section titled “テクニック2: Few-Shot例の動的選択”// ❌ 悪い例: 全てのFew-Shot例を毎回送信(2000 tokens)const allExamples = `例1: ユーザー「配送状況は?」 → AI「追跡番号をお知らせください」例2: ユーザー「返品したい」 → AI「返品理由を教えてください」...(50個の例)`;
// ✅ 良い例: ユーザー質問に類似した例だけを選択(200 tokens)import { cosineSimilarity } from './utils';
async function selectRelevantExamples(userQuery: string, allExamples: Example[], topK: number = 3) { const queryEmbedding = await getEmbedding(userQuery);
const similarities = allExamples.map(ex => ({ example: ex, similarity: cosineSimilarity(queryEmbedding, ex.embedding) }));
// 類似度が高い上位3例だけを使用 return similarities .sort((a, b) => b.similarity - a.similarity) .slice(0, topK) .map(s => s.example.text) .join('\n');}
// コスト削減:// Before: 2000 tokens × $5/1M = $0.01/request// After: 200 tokens × $5/1M = $0.001/request// 節約額: $0.009/request → 月間10万req で $900削減テクニック3: 会話履歴の圧縮
Section titled “テクニック3: 会話履歴の圧縮”interface Message { role: 'user' | 'assistant'; content: string; tokens: number;}
function compressConversationHistory(messages: Message[], maxTokens: number = 2000): Message[] { // 戦略1: 古いメッセージから削除 let totalTokens = messages.reduce((sum, m) => sum + m.tokens, 0); let compressed = [...messages];
while (totalTokens > maxTokens && compressed.length > 2) { const removed = compressed.shift(); // 最古のメッセージを削除 if (removed) totalTokens -= removed.tokens; }
// 戦略2: 長いメッセージを要約 compressed = compressed.map(msg => { if (msg.tokens > 500) { return { ...msg, content: summarize(msg.content, maxLength: 100), // LLMで要約 tokens: 100 }; } return msg; });
return compressed;}
// さらに高度な圧縮: LLMLingua(Microsoft Research)import { LLMLingua } from 'llmlingua';
async function advancedCompression(text: string, targetRatio: number = 0.5) { const compressor = new LLMLingua();
// 重要な情報を保持しながら50%圧縮 const compressed = await compressor.compress(text, { targetRatio, preserveEntities: true, // 固有名詞は保持 preserveNumbers: true // 数値は保持 });
return compressed;}
// 使用例const originalHistory = getConversationHistory(); // 5000 tokensconst compressed = await advancedCompression(originalHistory, 0.3); // 1500 tokens
// コスト削減:// Before: 5000 tokens × $5/1M = $0.025/request// After: 1500 tokens × $5/1M = $0.0075/request// 節約額: $0.0175/request → 月間10万req で $1,750削減🔄 戦略2: キャッシング(50~90%削減)
Section titled “🔄 戦略2: キャッシング(50~90%削減)”セマンティックキャッシング
Section titled “セマンティックキャッシング”import { createClient } from '@supabase/supabase-js';import { cosineSimilarity, getEmbedding } from './embeddings';
class SemanticCache { private supabase; private similarityThreshold = 0.95; // 95%以上類似ならキャッシュヒット
constructor() { this.supabase = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_KEY!); }
async get(query: string): Promise<string | null> { // クエリをベクトル化 const queryEmbedding = await getEmbedding(query);
// ベクトル類似検索(pgvectorを使用) const { data, error } = await this.supabase.rpc('match_cache', { query_embedding: queryEmbedding, similarity_threshold: this.similarityThreshold, match_count: 1 });
if (data && data.length > 0) { console.log(`✅ Cache HIT (similarity: ${data[0].similarity})`); return data[0].response; }
console.log('❌ Cache MISS'); return null; }
async set(query: string, response: string, ttl: number = 86400) { const queryEmbedding = await getEmbedding(query);
await this.supabase.from('llm_cache').insert({ query, query_embedding: queryEmbedding, response, expires_at: new Date(Date.now() + ttl * 1000) }); }}
// 使用例const cache = new SemanticCache();
async function cachedLLMCall(userQuery: string) { // キャッシュ確認 const cached = await cache.get(userQuery); if (cached) { return cached; // APIコール不要! }
// キャッシュミス時のみAPI呼び出し const response = await openai.chat.completions.create({ model: "gpt-4o", messages: [{ role: "user", content: userQuery }] });
const answer = response.choices[0].message.content!;
// キャッシュに保存 await cache.set(userQuery, answer, ttl: 86400); // 24時間
return answer;}
// コスト削減試算:// キャッシュヒット率50%を想定// Before: 100,000 requests × $0.01 = $1,000// After: 50,000 requests × $0.01 = $500 (残り50%はキャッシュから無料)// 節約額: $500/月Prompt Caching(Anthropic Claude)
Section titled “Prompt Caching(Anthropic Claude)”// Anthropic Claudeの新機能: Prompt Cachingを活用import Anthropic from '@anthropic-ai/sdk';
const anthropic = new Anthropic();
async function cachedClaudeCall(userQuery: string) { const systemPrompt = `あなたはACME社のカスタマーサポート担当です。以下の社内マニュアルに従って回答してください:
【マニュアル】... (1万トークンの長大なマニュアル)`;
const response = await anthropic.messages.create({ model: "claude-3-5-sonnet-20240620", max_tokens: 1024, system: [ { type: "text", text: systemPrompt, cache_control: { type: "ephemeral" } // このプロンプトをキャッシュ! } ], messages: [ { role: "user", content: userQuery } ] });
return response.content[0].text;}
// コスト削減:// キャッシュされたトークンは90%オフ// Before: 10,000 tokens × $3/1M = $0.03/request (システムプロンプト)// After: 10,000 tokens × $0.3/1M = $0.003/request (キャッシュヒット時)// 節約額: $0.027/request → 月間10万req で $2,700削減🚀 戦略3: モデル選択の最適化
Section titled “🚀 戦略3: モデル選択の最適化”タスク別モデル使い分け
Section titled “タスク別モデル使い分け”type TaskComplexity = 'simple' | 'medium' | 'complex';
function selectOptimalModel(task: string): { model: string; cost: number } { const complexity = analyzeTaskComplexity(task);
switch (complexity) { case 'simple': // 簡単なタスク: GPT-3.5 Turbo(10分の1のコスト) return { model: 'gpt-3.5-turbo', cost: 0.002 };
case 'medium': // 中程度: GPT-4o(バランス型) return { model: 'gpt-4o', cost: 0.01 };
case 'complex': // 複雑: GPT-4 Turbo(最高性能) return { model: 'gpt-4-turbo', cost: 0.03 }; }}
function analyzeTaskComplexity(task: string): TaskComplexity { // FAQ応答、挨拶、単純な分類 → simple const simplePatterns = [ /営業時間/, /こんにちは/, /ありがとう/, /はい|いいえ/ ];
if (simplePatterns.some(p => p.test(task))) { return 'simple'; }
// コード生成、長文要約、複雑な推論 → complex const complexPatterns = [ /コード/, /プログラム/, /実装/, /詳しく説明/ ];
if (complexPatterns.some(p => p.test(task))) { return 'complex'; }
return 'medium';}
// 使用例const userQuery = "営業時間を教えてください";const { model, cost } = selectOptimalModel(userQuery);console.log(`Selected model: ${model} (estimated cost: $${cost})`);
// コスト削減試算:// 30% simple, 50% medium, 20% complexと仮定// Before(全てGPT-4 Turbo): 100,000 req × $0.03 = $3,000// After: (30K × $0.002) + (50K × $0.01) + (20K × $0.03) = $60 + $500 + $600 = $1,160// 節約額: $1,840/月(61%削減)ローカルモデルとの併用
Section titled “ローカルモデルとの併用”import Replicate from 'replicate';
class HybridLLMRouter { private replicate = new Replicate(); private localModelThreshold = 0.7; // 信頼度70%以上ならローカルで完結
async query(userInput: string): Promise<string> { // ステップ1: ローカルモデル(Llama 3 8B)で試行 const localResponse = await this.localModelInference(userInput);
if (localResponse.confidence > this.localModelThreshold) { console.log('✅ Local model (cost: $0)'); return localResponse.text; }
// ステップ2: 信頼度が低ければGPT-4にフォールバック console.log('⚠️ Fallback to GPT-4'); const cloudResponse = await this.cloudModelInference(userInput); return cloudResponse; }
async localModelInference(input: string) { const output = await this.replicate.run( "meta/llama-3-8b-instruct", { input: { prompt: input } } );
// 信頼度を推定(簡易実装) const confidence = this.estimateConfidence(output);
return { text: output, confidence }; }
async cloudModelInference(input: string): Promise<string> { const response = await openai.chat.completions.create({ model: "gpt-4o", messages: [{ role: "user", content: input }] });
return response.choices[0].message.content!; }
estimateConfidence(output: any): number { // ヒューリスティックな信頼度推定 // - 回答が短すぎる(10単語未満)→ 低信頼 // - "I don't know" "わかりません" → 低信頼 // - 具体的な数値・固有名詞あり → 高信頼
const text = output.toString(); const wordCount = text.split(/\s+/).length;
if (wordCount < 10) return 0.3; if (/don't know|わかりません/.test(text)) return 0.4; if (/\d+|[A-Z][a-z]+/.test(text)) return 0.8;
return 0.6; }}
// コスト削減試算:// ローカルモデルでの処理率60%を想定// Before: 100,000 req × $0.01 = $1,000// After: (60,000 × $0) + (40,000 × $0.01) = $400// 節約額: $600/月(60%削減)📦 戦略4: バッチ処理とストリーミング
Section titled “📦 戦略4: バッチ処理とストリーミング”バッチAPI(50%オフ)
Section titled “バッチAPI(50%オフ)”import OpenAI from 'openai';const openai = new OpenAI();
async function createBatchJob(requests: Array<{ id: string; prompt: string }>) { // バッチファイルを作成 const batchContent = requests.map(req => ({ custom_id: req.id, method: "POST", url: "/v1/chat/completions", body: { model: "gpt-4o", messages: [{ role: "user", content: req.prompt }] } })).map(r => JSON.stringify(r)).join('\n');
// ファイルをアップロード const file = await openai.files.create({ file: Buffer.from(batchContent), purpose: "batch" });
// バッチジョブを作成 const batch = await openai.batches.create({ input_file_id: file.id, endpoint: "/v1/chat/completions", completion_window: "24h" // 24時間以内に完了 });
console.log(`Batch job created: ${batch.id}`); return batch.id;}
async function retrieveBatchResults(batchId: string) { // ジョブの完了を待つ let batch = await openai.batches.retrieve(batchId);
while (batch.status !== 'completed') { await new Promise(resolve => setTimeout(resolve, 60000)); // 1分待機 batch = await openai.batches.retrieve(batchId); }
// 結果を取得 const results = await openai.files.content(batch.output_file_id!); return results;}
// 使用例: 夜間バッチで10万件の商品説明を生成const products = await getProductsNeedingDescription(); // 100,000 items
const requests = products.map(p => ({ id: p.id, prompt: `商品名: ${p.name}\n特徴: ${p.features}\n\n魅力的な商品説明を100文字で書いてください。`}));
const batchId = await createBatchJob(requests);// 翌朝、結果を取得const results = await retrieveBatchResults(batchId);
// コスト削減:// Batch APIは通常料金の50%オフ// Before: 100,000 req × $0.01 = $1,000// After: 100,000 req × $0.005 = $500// 節約額: $500/月👤 ユースケース別コスト最適化
Section titled “👤 ユースケース別コスト最適化”ユースケース1: カスタマーサポートChatBot
Section titled “ユースケース1: カスタマーサポートChatBot”class CostOptimizedChatBot { private cache = new SemanticCache(); private faqDatabase = loadFAQs(); // よくある質問データベース
async respond(userMessage: string): Promise<string> { // ステップ1: FAQ検索(コスト$0) const faqAnswer = this.searchFAQ(userMessage); if (faqAnswer) { console.log('FAQ hit (cost: $0)'); return faqAnswer; }
// ステップ2: セマンティックキャッシュ(コスト$0) const cached = await this.cache.get(userMessage); if (cached) { console.log('Cache hit (cost: $0)'); return cached; }
// ステップ3: 簡単な質問はGPT-3.5(コスト激安) if (this.isSimpleQuery(userMessage)) { console.log('GPT-3.5 (cost: $0.002)'); return await this.callGPT35(userMessage); }
// ステップ4: 複雑な質問のみGPT-4o(通常コスト) console.log('GPT-4o (cost: $0.01)'); const response = await this.callGPT4o(userMessage);
// キャッシュに保存 await this.cache.set(userMessage, response);
return response; }
searchFAQ(query: string): string | null { // ベクトル検索でFAQにマッチ const match = this.faqDatabase.find(faq => cosineSimilarity(faq.embedding, getEmbedding(query)) > 0.9 ); return match?.answer || null; }}
// コスト削減試算:// - FAQ hit: 30% → $0// - Cache hit: 40% → $0// - GPT-3.5: 20% → $0.002/req// - GPT-4o: 10% → $0.01/req//// 月間100,000リクエスト:// Before(全てGPT-4o): 100,000 × $0.01 = $1,000// After: (30K × $0) + (40K × $0) + (20K × $0.002) + (10K × $0.01) = $0 + $0 + $40 + $100 = $140// 節約額: $860/月(86%削減!)ユースケース2: 社内AI活用(自分利用)
Section titled “ユースケース2: 社内AI活用(自分利用)”// 社内ツール: コードレビューAIclass CodeReviewAssistant { async reviewPullRequest(prDiff: string): Promise<string> { // 戦略1: 差分が小さければGPT-3.5で十分 const lineCount = prDiff.split('\n').length;
if (lineCount < 100) { return await this.quickReview(prDiff); // GPT-3.5 }
// 戦略2: 大規模PRはローカルモデルでドラフト → GPT-4で精査 const draftReview = await this.localModelReview(prDiff); // Llama 3 const refinedReview = await this.refineReview(draftReview); // GPT-4o
return refinedReview; }
async quickReview(diff: string): Promise<string> { // 小規模変更: GPT-3.5で高速・低コストレビュー const response = await openai.chat.completions.create({ model: "gpt-3.5-turbo", messages: [{ role: "system", content: "あなたはコードレビュアーです。バグ、セキュリティ問題、スタイル違反を指摘してください。" }, { role: "user", content: `以下のdiffをレビューしてください:\n\n${diff}` }] });
return response.choices[0].message.content!; }
async localModelReview(diff: string): Promise<string> { // ローカルモデルで初期レビュー(無料) const response = await fetch('http://localhost:11434/api/generate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ model: 'codellama:7b', prompt: `Review this code diff and suggest improvements:\n\n${diff}` }) });
return await response.text(); }}
// コスト削減試算(月間1,000PR):// Before(全てGPT-4): 1,000 × $0.05 = $50// After: (700 × $0.002) + (300 × $0.01) = $1.4 + $3 = $4.4// 節約額: $45.6/月(91%削減)🏆 コスト削減の実践ロードマップ
Section titled “🏆 コスト削減の実践ロードマップ”Phase 1: クイックウィン(1週間で50%削減)
Section titled “Phase 1: クイックウィン(1週間で50%削減)”| 施策 | 実装難易度 | 削減効果 | 実装時間 |
|---|---|---|---|
| システムプロンプト圧縮 | ★☆☆☆☆ | 20% | 2時間 |
| タスク別モデル選択 | ★★☆☆☆ | 30% | 1日 |
| 会話履歴の制限 | ★☆☆☆☆ | 10% | 1時間 |
// クイックウィン実装例const QUICK_WINS = { // 1. システムプロンプトを半分に systemPrompt: compressPrompt(originalPrompt, targetRatio: 0.5),
// 2. 会話履歴を最大10往復に制限 conversationHistory: messages.slice(-20),
// 3. 簡単な質問はGPT-3.5に振り分け model: isSimpleQuery(userInput) ? 'gpt-3.5-turbo' : 'gpt-4o'};Phase 2: 中期施策(1ヶ月で70%削減)
Section titled “Phase 2: 中期施策(1ヶ月で70%削減)”| 施策 | 実装難易度 | 削減効果 | 実装時間 |
|---|---|---|---|
| セマンティックキャッシュ | ★★★☆☆ | 40% | 3日 |
| FAQ検索の導入 | ★★☆☆☆ | 20% | 2日 |
| バッチAPI活用 | ★★☆☆☆ | 10% | 1日 |
Phase 3: 長期戦略(3ヶ月で90%削減)
Section titled “Phase 3: 長期戦略(3ヶ月で90%削減)”| 施策 | 実装難易度 | 削減効果 | 実装時間 |
|---|---|---|---|
| ローカルモデル併用 | ★★★★☆ | 50% | 2週間 |
| ファインチューニング | ★★★★★ | 60% | 1ヶ月 |
| 専用モデル構築 | ★★★★★ | 80% | 3ヶ月 |
📊 コスト監視ダッシュボード
Section titled “📊 コスト監視ダッシュボード”import { Gauge, Counter, Histogram } from 'prom-client';
// Prometheusメトリクスconst llmCostTotal = new Counter({ name: 'llm_cost_total_usd', help: 'Total LLM API cost in USD', labelNames: ['model', 'endpoint']});
const llmTokensUsed = new Counter({ name: 'llm_tokens_used_total', help: 'Total tokens consumed', labelNames: ['model', 'token_type'] // input or output});
const llmLatency = new Histogram({ name: 'llm_request_duration_seconds', help: 'LLM request latency', labelNames: ['model', 'cache_hit']});
// コスト記録function recordLLMCost(model: string, inputTokens: number, outputTokens: number) { const costs = { 'gpt-4o': { input: 5.00, output: 15.00 }, 'gpt-3.5-turbo': { input: 0.50, output: 1.50 }, };
const cost = (inputTokens * costs[model].input / 1_000_000) + (outputTokens * costs[model].output / 1_000_000);
llmCostTotal.labels(model, 'chat').inc(cost); llmTokensUsed.labels(model, 'input').inc(inputTokens); llmTokensUsed.labels(model, 'output').inc(outputTokens);}
// Grafanaダッシュボードクエリ例const GRAFANA_QUERIES = { // 今月の累計コスト monthlyCost: `sum(increase(llm_cost_total_usd[30d]))`,
// モデル別コスト比率 costByModel: ` sum by (model) (increase(llm_cost_total_usd[30d])) / sum(increase(llm_cost_total_usd[30d])) * 100 `,
// キャッシュヒット率 cacheHitRate: ` sum(rate(llm_request_duration_seconds_count{cache_hit="true"}[5m])) / sum(rate(llm_request_duration_seconds_count[5m])) * 100 `};コスト削減の優先順位
Section titled “コスト削減の優先順位”-
今すぐやるべき(1週間以内)
- プロンプト圧縮
- タスク別モデル選択
- 会話履歴制限
-
早めに実装(1ヶ月以内)
- セマンティックキャッシュ
- FAQ検索
- バッチ処理
-
中長期で検討(3ヶ月~)
- ローカルモデル導入
- ファインチューニング
- 専用インフラ構築
現実的な削減目標
Section titled “現実的な削減目標”| 期間 | 目標削減率 | 主な施策 |
|---|---|---|
| 1週間 | 30~50% | プロンプト最適化 + モデル選択 |
| 1ヶ月 | 60~70% | キャッシング + FAQ |
| 3ヶ月 | 80~90% | ローカルモデル + ファインチューニング |
次のステップ
Section titled “次のステップ”- 25. Federated Learning実装 - プライバシー保護しながら学習
- 22. AIセキュリティと敵対的攻撃 - コスト削減と同時にセキュリティ対策
- 10. MLOps実践 - コスト監視の自動化
ポケモンマスターの知恵
「ポケモンセンターに頼りすぎるな。キズぐすりで治せるなら、それで十分だ。リソース管理もバトルの一部だぞ。」