mcpField
A
ai-integration

ai-integration

github.com/mgd34msu開発656 回インストール10 週間前に更新
リポジトリ

ai-integration について

ai-integrationは、アプリ開発者向けのAI連携スキルです。チャットやストリーミングからRAG、ベクトル検索、ツール呼び出しまで扱い、安全性・テスト・検証も案内します。

主な機能

  1. 01AIプロバイダーとSDKの選定ガイド
  2. 02チャット画面とストリーミング応答
  3. 03RAG、埋め込み、ベクトル検索
  4. 04ツール呼び出し、レート制限、安全対策
  5. 05テストと連携検証の手順

活用シーン

  1. 01アプリへのLLMチャット機能の追加
  2. 02ドキュメント検索と質問応答の構築
  3. 03外部関数呼び出しのLLM連携
  4. 04AI機能の安全性とコスト管理の確認

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/mgd34msu/goodvibes-plugin --skill ai-integration

インストール後、Claude Code で/ai-integrationを入力して読み込まれたか確認します。

同梱ファイルreferences/ai-patterns.mdscripts/validate-ai-integration.sh

詳細

リソース

scripts/
  validate-ai-integration.sh
references/
  ai-patterns.md

AI 統合の実装

このスキルでは、基本的なチャットインターフェースから複雑な RAG パイプラインや関数呼び出しまで、アプリケーションに AI と LLM の機能を統合する方法を説明します。GoodVibes の高精度ツールとプロジェクト分析を活用し、本番環境に対応した AI 実装を実現します。

このスキルを使用する場面

次のような場合に、このスキルを使用してください。

  • LLM プロバイダーを使用したチャットインターフェースを実装する
  • ストリーミング応答機能を追加する
  • RAG(検索拡張生成)パイプラインを構築する
  • 埋め込みとベクトル検索を統合する
  • ツール/関数呼び出しのパターンを実装する
  • OpenAI、Anthropic、またはローカルモデルに接続する
  • AI API のレート制限とエラーからの復旧に対応する

ワークフロー

AI 統合では、次の手順に従ってください。

1. 既存の AI インフラストラクチャを確認する

AI 機能を実装する前に、現在の状態を把握してください。

detect_stack:
  project_root: "."
  categories: ["ai", "llm"]

次の項目を特定できます。

  • 使用中の AI SDK(Vercel AI SDK、LangChain、LlamaIndex)
  • LLM プロバイダー(OpenAI、Anthropic、Cohere)
  • ベクトルデータベース(Pinecone、pgvector、Weaviate)
  • 既存のチャットまたは補完エンドポイント
  • 埋め込み生成のパターン

AI に関する決定事項をプロジェクトのメモリで確認します。

precision_read:
  files:
    - path: ".goodvibes/memory/decisions.json"
    - path: ".goodvibes/memory/patterns.json"
  verbosity: minimal

次の項目を探します。

  • プロバイダーの選択(「品質を優先し、OpenAI GPT-4 を使用する」)
  • コスト最適化のパターン(「埋め込みを 24 時間キャッシュする」)
  • ストリーミングの設定(「チャット UI では常にストリーミングする」)
  • 既知の問題(「Claude Opus で Anthropic のレート制限が発生する」)

既存の AI パターンを検索します。

precision_grep:
  queries:
    - id: openai-usage
      pattern: "import.*openai|from 'openai'"
      glob: "**/*.{ts,tsx,js,jsx}"
    - id: streaming-endpoints
      pattern: "stream|StreamingTextResponse|toDataStreamResponse"
      glob: "**/*.{ts,tsx}"
    - id: embeddings
      pattern: "embed|embedding|vector"
      glob: "**/*.{ts,tsx}"
  output:
    format: files_only
2. プロバイダーを選択する

要件に基づいて、適切な AI プロバイダーと SDK を選択します。

参照:プロバイダーの比較については references/ai-patterns.md をご覧ください。

主な選択要因:

必要な項目推奨
最高品質のモデルOpenAI GPT-4 または Anthropic Claude Opus
最速の応答OpenAI GPT-4o または Anthropic Claude Haiku
コスト効率Anthropic Claude Haiku または GPT-4o-mini
ローカル/プライバシーLlama 3 または Mistral を搭載した Ollama
関数呼び出しOpenAI(最も充実したサポート)または Anthropic
長いコンテキストAnthropic Claude(200K トークン)
React との統合Vercel AI SDK
複雑なワークフローLangChain または LlamaIndex

依存関係をインストールします。

precision_exec:
  commands:
    - cmd: "npm install ai @ai-sdk/openai @ai-sdk/anthropic zod"
      expect:
        exit_code: 0
  verbosity: minimal
3. API キーを設定する

環境変数を使用して API キーを安全に管理します。

ドキュメントについては .env.example を確認します。

precision_read:
  files:
    - path: ".env.example"
  verbosity: minimal

必要な環境変数を追加します。

precision_edit:
  operations:
    - file: ".env.example"
      edits:
        - action: append
          content: |
            # AI Provider API Keys
            OPENAI_API_KEY=your_openai_api_key_here
            ANTHROPIC_API_KEY=sk-ant-...
            
            # Optional: Model configuration
            AI_MODEL=gpt-4o  # or gpt-4o-mini, claude-3-5-sonnet-20241022, etc.
            AI_TEMPERATURE=0.7
            AI_MAX_TOKENS=4096
  verbosity: minimal

ハードコードされたキーがないことを検証します。

precision_grep:
  queries:
    - id: hardcoded-keys
      pattern: "sk-[a-zA-Z0-9]{20,}"
      glob: "**/*.{ts,tsx,js,jsx}"
  output:
    format: count_only

想定される件数は 0 です。見つかった場合は、すぐにキーを環境変数に移してください。

4. チャットインターフェースを実装する

Vercel AI SDK のパターンを使用して、チャットエンドポイントと UI を作成します。

API ルートを作成します。

precision_write:
  files:
    - path: "src/app/api/chat/route.ts"
      mode: fail_if_exists
      content: |
        import { openai } from '@ai-sdk/openai';
        import { streamText } from 'ai';
        
        export const runtime = 'edge'; // Optional: Edge runtime for low-latency responses. Remove if using Node-only modules (ws, fs, etc.)
        
        // Note: Import zod at the top of the file
        // import { z } from 'zod';
        
        export async function POST(req: Request) {
          // Validate input with Zod (shown here, omitted in later examples for brevity)
          const RequestSchema = z.object({
            messages: z.array(z.object({
              role: z.enum(['user', 'assistant', 'system']),
              content: z.string().max(10000),
            })),
          });
          const body = RequestSchema.safeParse(await req.json());
          if (!body.success) {
            return new Response('Invalid request', { status: 400 });
          }
          const { messages } = body.data;
        
          const result = streamText({
            model: openai('gpt-4o'),
            messages,
            temperature: 0.7,
            maxTokens: 4096,
          });
        
          return result.toDataStreamResponse();
        }
  verbosity: minimal

チャット UI コンポーネントを作成します。

precision_write:
  files:
    - path: "src/components/chat.tsx"
      mode: fail_if_exists
      content: |
        'use client';
        
        import { useChat } from 'ai/react';
        
        export function Chat() {
          const { messages, input, handleInputChange, handleSubmit, isLoading } = useChat();
        
          return (
            <div className="flex flex-col h-screen">
              <div className="flex-1 overflow-y-auto p-4 space-y-4">
                {messages.map((m) => (
                  <div key={m.id} className={`flex ${m.role === 'user' ? 'justify-end' : 'justify-start'}`}>
                    <div className={`max-w-md px-4 py-2 rounded-lg ${
                      m.role === 'user' ? 'bg-blue-500 text-white' : 'bg-gray-200'
                    }`}>
                      {m.content}
                    </div>
                  </div>
                ))}
                {isLoading && <div className="text-gray-500">Thinking...</div>}
              </div>
              <form onSubmit={handleSubmit} className="border-t p-4">
                <input
                  value={input}
                  onChange={handleInputChange}
                  placeholder="Type a message..."
                  className="w-full px-4 py-2 border rounded-lg"
                  disabled={isLoading}
                />
              </form>
            </div>
          );
        }
  verbosity: minimal

実装を検証します。

precision_exec:
  commands:
    - cmd: "npm run typecheck"
      expect:
        exit_code: 0
  verbosity: minimal
5. ストリーミング応答を実装する

トークン単位のストリーミングでチャットの UX を最適化します。

Vercel AI SDK には streamText() と useChat() によるストリーミング機能が組み込まれています。上記のパターンでは、すでにストリーミングが行われます。

独自のストリーミングを実装する場合:

precision_write:
  files:
    - path: "src/lib/streaming.ts"
      mode: fail_if_exists
      content: |
        import OpenAI from 'openai';
        
        const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
        
        export async function streamCompletion(prompt: string) {
          const stream = await openai.chat.completions.create({
            model: 'gpt-4o',
            messages: [{ role: 'user', content: prompt }],
            stream: true,
          });
        
          const encoder = new TextEncoder();
        
          return new ReadableStream({
            async start(controller) {
              for await (const chunk of stream) {
                const content = chunk.choices[0]?.delta?.content || '';
                if (content) {
                  controller.enqueue(encoder.encode(content));
                }
              }
              controller.close();
            },
          });
        }
  verbosity: minimal

Server-Sent Events(SSE)を使用する方法:

precision_write:
  files:
    - path: "src/app/api/stream/route.ts"
      mode: fail_if_exists
      content: |
        import OpenAI from 'openai';
        
        export const runtime = 'edge'; // Optional: Edge runtime for low-latency responses. Remove if using Node-only modules (ws, fs, etc.)
        
        export async function POST(req: Request) {
          // Add Zod validation as shown in first API route example
          const { prompt } = await req.json();
          const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
        
          const stream = await openai.chat.completions.create({
            model: 'gpt-4o',
            messages: [{ role: 'user', content: prompt }],
            stream: true,
          });
        
          const encoder = new TextEncoder();
        
          return new Response(
            new ReadableStream({
              async start(controller) {
                for await (const chunk of stream) {
                  const content = chunk.choices[0]?.delta?.content || '';
                  if (content) {
                    controller.enqueue(encoder.encode(`data: ${JSON.stringify({ content })}\n\n`));
                  }
                }
                controller.enqueue(encoder.encode('data: [DONE]\n\n'));
                controller.close();
              },
            }),
            {
              headers: {
                'Content-Type': 'text/event-stream',
                'Cache-Control': 'no-cache',
                'Connection': 'keep-alive',
              },
            }
          );
        }
  verbosity: minimal
6. RAG パイプラインを構築する

ドキュメントの取り込み、チャンク分割、埋め込み、検索を実装します。

ステップ 6.1:ドキュメントの取り込み

precision_write:
  files:
    - path: "src/lib/rag/ingestion.ts"
      mode: fail_if_exists
      content: |
        import { RecursiveCharacterTextSplitter } from 'langchain/text_splitter';
        
        export async function chunkDocument(text: string, metadata: Record<string, unknown> = {}) {
          const splitter = new RecursiveCharacterTextSplitter({
            chunkSize: 1000,
            chunkOverlap: 200,
            separators: ['\n\n', '\n', ' ', ''],
          });
        
          const chunks = await splitter.createDocuments([text], [metadata]);
          return chunks.map((chunk, index) => ({
            content: chunk.pageContent,
            metadata: { ...chunk.metadata, chunk_index: index },
          }));
        }
  verbosity: minimal

ステップ 6.2:埋め込みの生成

precision_write:
  files:
    - path: "src/lib/rag/embeddings.ts"
      mode: fail_if_exists
      content: |
        import { embed } from 'ai';
        import { openai } from '@ai-sdk/openai';
        
        export async function generateEmbedding(text: string): Promise<number[]> {
          const { embedding } = await embed({
            model: openai.embedding('text-embedding-3-small'),
            value: text,
          });
          return embedding;
        }
        
        export async function generateEmbeddings(texts: string[]): Promise<number[][]> {
          return Promise.all(texts.map(generateEmbedding));
        }
  verbosity: minimal

ステップ 6.3:ベクトルストレージ(Pinecone の例)

precision_write:
  files:
    - path: "src/lib/rag/vector-store.ts"
      mode: fail_if_exists
      content: |
        import { Pinecone } from '@pinecone-database/pinecone';
        import { generateEmbedding } from './embeddings';
        
        // Validate env vars at startup (example shown here, subsequent ! usage for brevity)
        const apiKey = process.env.PINECONE_API_KEY;
        if (!apiKey) throw new Error('PINECONE_API_KEY is required');
        const pinecone = new Pinecone({ apiKey });
        const index = pinecone.index('documents');
        
        export async function storeDocument(id: string, text: string, metadata: Record<string, unknown>) {
          const embedding = await generateEmbedding(text);
          await index.upsert([{
            id,
            values: embedding,
            metadata: { text, ...metadata },
          }]);
        }
        
        export async function searchSimilar(query: string, topK: number = 5) {
          const queryEmbedding = await generateEmbedding(query);
          const results = await index.query({
            vector: queryEmbedding,
            topK,
            includeMetadata: true,
          });
          return results.matches || [];
        }
  verbosity: minimal

ステップ 6.4:RAG 補完エンドポイント

precision_write:
  files:
    - path: "src/app/api/rag/route.ts"
      mode: fail_if_exists
      content: |
        import { openai } from '@ai-sdk/openai';
        import { streamText } from 'ai';
        import { searchSimilar } from '@/lib/rag/vector-store';
        
        export const runtime = 'edge'; // Optional: Edge runtime for low-latency responses. Remove if using Node-only modules (ws, fs, etc.)
        
        export async function POST(req: Request) {
          // Add Zod validation as shown in first API route example
          const { messages } = await req.json();
          const lastMessage = messages[messages.length - 1];
        
          // Retrieve relevant context
          const context = await searchSimilar(lastMessage.content, 3);
          const contextText = context
            .map((match) => match.metadata?.text || '')
            .join('\n\n');
        
          // Augment prompt with context
          const augmentedMessages = [
            {
              role: 'system',
              content: `You are a helpful assistant. Use the following context to answer the user's question:\n\n${contextText}`,
            },
            ...messages,
          ];
        
          const result = streamText({
            model: openai('gpt-4o'),
            messages: augmentedMessages,
          });
        
          return result.toDataStreamResponse();
        }
  verbosity: minimal
7. ベクトル検索を実装する

セマンティック検索用にベクトルデータベースを設定します。

プロバイダーの比較:

ベクトルデータベースの比較については references/ai-patterns.md をご覧ください。

Pinecone(マネージド):

precision_exec:
  commands:
    - cmd: "npm install @pinecone-database/pinecone"
      expect:
        exit_code: 0
  verbosity: minimal

pgvector(PostgreSQL 拡張機能):

precision_write:
  files:
    - path: "src/lib/vector/pgvector.ts"
      mode: fail_if_exists
      content: |
        import { sql } from 'drizzle-orm';
        import { db } from '@/lib/db';
        
        export async function enablePgvector() {
          await db.execute(sql`CREATE EXTENSION IF NOT EXISTS vector`);
        }
        
        export async function searchSimilar(embedding: number[], limit: number = 5) {
          return db.execute(sql`
            SELECT id, content, metadata, 1 - (embedding <=> ${embedding}::vector) AS similarity
            FROM documents
            ORDER BY embedding <=> ${embedding}::vector
            LIMIT ${limit}
          `);
        }
  verbosity: minimal

Weaviate(ハイブリッド検索):

precision_write:
  files:
    - path: "src/lib/vector/weaviate.ts"
      mode: fail_if_exists
      content: |
        import weaviate, { WeaviateClient } from 'weaviate-ts-client';
        
        const client: WeaviateClient = weaviate.client({
          scheme: 'https',
          host: process.env.WEAVIATE_HOST!,
          apiKey: { apiKey: process.env.WEAVIATE_API_KEY! },
        });
        
        export async function hybridSearch(query: string, limit: number = 5) {
          return client.graphql
            .get()
            .withClassName('Document')
            .withHybrid({ query, alpha: 0.5 })
            .withFields('content metadata _additional { score }')
            .withLimit(limit)
            .do();
        }
  verbosity: minimal
8. ツール/関数呼び出しを実装する

LLM が外部関数やツールを呼び出せるようにします。

Zod スキーマを使用してツールを定義します。

precision_write:
  files:
    - path: "src/lib/ai/tools.ts"
      mode: fail_if_exists
      content: |
        import { z } from 'zod';
        import { tool } from 'ai';
        
        export const weatherTool = tool({
          description: 'Get the current weather for a location',
          parameters: z.object({
            location: z.string().describe('City name or zip code'),
            unit: z.enum(['celsius', 'fahrenheit']).default('celsius'),
          }),
          execute: async ({ location, unit }) => {
            // Call weather API using URL constructor for safe param encoding
            const url = new URL('https://api.weather.com/v1/current');
            url.searchParams.set('location', location);
            url.searchParams.set('unit', unit);
            const response = await fetch(url);
            return response.json();
          },
        });
        
        export const calculatorTool = tool({
          description: 'Perform mathematical calculations',
          parameters: z.object({
            expression: z.string().describe('Mathematical expression to evaluate'),
          }),
          execute: async ({ expression }) => {
            // Safe eval using mathjs
            try {
              const { evaluate } = await import('mathjs');
              const result = evaluate(expression);
              return { result };
            } catch (error) {
              return { error: 'Invalid expression' };
            }
          },
        });
  verbosity: minimal

補完処理でツールを使用します。

precision_write:
  files:
    - path: "src/app/api/chat-tools/route.ts"
      mode: fail_if_exists
      content: |
        import { openai } from '@ai-sdk/openai';
        import { streamText } from 'ai';
        import { weatherTool, calculatorTool } from '@/lib/ai/tools';
        
        export const runtime = 'edge'; // Optional: Edge runtime for low-latency responses. Remove if using Node-only modules (ws, fs, etc.)
        
        export async function POST(req: Request) {
          // Add Zod validation as shown in first API route example
          const { messages } = await req.json();
        
          const result = streamText({
            model: openai('gpt-4o'),
            messages,
            tools: {
              weather: weatherTool,
              calculator: calculatorTool,
            },
            maxSteps: 5, // Allow multi-step tool use
          });
        
          return result.toDataStreamResponse();
        }
  verbosity: minimal

UI でツールの結果を処理します。

precision_edit:
  operations:
    - file: "src/components/chat.tsx"
      edits:
        - action: replace
          old_text: "export function Chat() {"
          new_text: |
            export function Chat() {
              const { messages, input, handleInputChange, handleSubmit, isLoading } = useChat({
                onToolCall: ({ toolCall }) => {
                  // Use proper logger in production: logger.info('Tool called:', toolCall.toolName, toolCall.args);
                  console.log('Tool called:', toolCall.toolName, toolCall.args);
                },
              });
  verbosity: minimal
9. セキュリティとレート制限の追加

不正利用を防ぎ、コストを管理します。

レート制限ミドルウェア:

precision_write:
  files:
    - path: "src/middleware/rate-limit.ts"
      mode: fail_if_exists
      content: |
        import { Ratelimit } from '@upstash/ratelimit';
        import { Redis } from '@upstash/redis';
        
        const redis = new Redis({
          url: process.env.UPSTASH_REDIS_REST_URL!,
          token: process.env.UPSTASH_REDIS_REST_TOKEN!,
        });
        
        export const ratelimit = new Ratelimit({
          redis,
          limiter: Ratelimit.slidingWindow(10, '60 s'), // 10 requests per minute
          analytics: true,
        });
        
        export async function checkRateLimit(identifier: string) {
          const { success, limit, remaining, reset } = await ratelimit.limit(identifier);
          
          if (!success) {
            throw new Error(`Rate limit exceeded. Try again in ${Math.ceil((reset - Date.now()) / 1000)}s`);
          }
          
          return { limit, remaining, reset };
        }
  verbosity: minimal

ルートへのレート制限の適用:

precision_edit:
  operations:
    - file: "src/app/api/chat/route.ts"
      edits:
        - action: replace
          old_text: "export async function POST(req: Request) {"
          new_text: |
            import { checkRateLimit } from '@/middleware/rate-limit';
            
            export async function POST(req: Request) {
              const ip = req.headers.get('x-forwarded-for') || 'anonymous';
              
              try {
                await checkRateLimit(ip);
              } catch (error) {
                const message = error instanceof Error ? error.message : 'Rate limit exceeded';
                return new Response(message, { status: 429 });
              }
  verbosity: minimal

プロンプトインジェクションの防止:

precision_write:
  files:
    - path: "src/lib/ai/safety.ts"
      mode: fail_if_exists
      content: |
        // Detect prompt injection attempts
        export function detectInjection(input: string): boolean {
          const injectionPatterns = [
            /ignore (previous|above|all) (instructions|prompts)/i,
            /you are now/i,
            /new instructions:/i,
            /system: /i,
            /<\w+>/, // HTML-like tags
          ];
          
          return injectionPatterns.some((pattern) => pattern.test(input));
        }
        
        // Sanitize user input
        export function sanitizeInput(input: string): string {
          // Remove potential injection markers
          return input
            .replace(/[<>]/g, '')
            .replace(/system:/gi, '')
            .trim()
            .slice(0, 10000); // Limit length
        }
  verbosity: minimal

コンテンツフィルタリング:

precision_write:
  files:
    - path: "src/lib/ai/moderation.ts"
      mode: fail_if_exists
      content: |
        import OpenAI from 'openai';
        
        const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
        
        export async function moderateContent(text: string) {
          const moderation = await openai.moderations.create({ input: text });
          const result = moderation.results[0];
          
          if (result.flagged) {
            const categories = Object.entries(result.categories)
              .filter(([_, flagged]) => flagged)
              .map(([category]) => category);
            
            throw new Error(`Content flagged for: ${categories.join(', ')}`);
          }
          
          return result;
        }
  verbosity: minimal
10. AI 統合のテスト

決定論的なテスト戦略を実装します。

LLM の応答をモックする:

precision_write:
  files:
    - path: "src/lib/ai/__tests__/chat.test.ts"
      mode: fail_if_exists
      content: |
        import { describe, it, expect, vi } from 'vitest';
        import { streamText } from 'ai';
        
        vi.mock('ai', () => ({
          streamText: vi.fn(),
        }));
        
        describe('Chat API', () => {
          it('should return streamed response', async () => {
            const mockStream = {
              toDataStreamResponse: () => new Response('mocked response'),
            };
            
            vi.mocked(streamText).mockResolvedValue(mockStream);
            
            import { POST } from '@/app/api/chat/route';
            
            const req = new Request('http://localhost/api/chat', {
              method: 'POST',
              body: JSON.stringify({ messages: [{ role: 'user', content: 'Hello' }] }),
            });
            const response = await POST(req);
            
            expect(response.ok).toBe(true);
          });
        });
  verbosity: minimal

決定論的なシードを使用したテスト:

precision_write:
  files:
    - path: "src/lib/ai/__tests__/embeddings.test.ts"
      mode: fail_if_exists
      content: |
        import { describe, it, expect } from 'vitest';
        import { generateEmbedding } from '../embeddings';
        
        describe('Embeddings', () => {
          // Mock AI SDK to prevent real API calls in tests
          vi.mock('ai', () => ({
            embed: vi.fn().mockResolvedValue({ embedding: new Array(1536).fill(0.1) })
          }));
          
          it('should generate consistent embeddings', async () => {
            const text = 'Hello world';
            const embedding1 = await generateEmbedding(text);
            const embedding2 = await generateEmbedding(text);
            
            // Embeddings should be very similar (cosine similarity)
            const similarity = cosineSimilarity(embedding1, embedding2);
            expect(similarity).toBeGreaterThan(0.99);
          });
        });
        
        function cosineSimilarity(a: number[], b: number[]): number {
          const dotProduct = a.reduce((sum, val, i) => sum + val * b[i], 0);
          const magnitudeA = Math.sqrt(a.reduce((sum, val) => sum + val * val, 0));
          const magnitudeB = Math.sqrt(b.reduce((sum, val) => sum + val * val, 0));
          return dotProduct / (magnitudeA * magnitudeB);
        }
  verbosity: minimal

コストの追跡:

precision_write:
  files:
    - path: "src/lib/ai/cost-tracker.ts"
      mode: fail_if_exists
      content: |
        import { Redis } from '@upstash/redis';
        
        const redis = new Redis({
          url: process.env.UPSTASH_REDIS_REST_URL!,
          token: process.env.UPSTASH_REDIS_REST_TOKEN!,
        });
        
        const MODEL_COSTS = {
          // Prices as of early 2026 -- verify current rates at provider sites
          'gpt-4o': { input: 0.0025, output: 0.01 }, // per 1K tokens
          'gpt-4o-mini': { input: 0.00015, output: 0.0006 },
          'claude-opus-4': { input: 0.015, output: 0.075 },
          'claude-sonnet-4-5': { input: 0.003, output: 0.015 },
          'claude-haiku-4-5': { input: 0.0008, output: 0.004 },
        };
        
        export async function trackUsage(
          model: keyof typeof MODEL_COSTS,
          inputTokens: number,
          outputTokens: number
        ) {
          const costs = MODEL_COSTS[model];
          const cost = (inputTokens / 1000) * costs.input + (outputTokens / 1000) * costs.output;
          
          const today = new Date().toISOString().split('T')[0];
          const key = `ai:cost:${today}`;
          
          await redis.incrbyfloat(key, cost);
          await redis.expire(key, 86400 * 90); // 90 days retention
          
          return cost;
        }
        
        export async function getTodayCost(): Promise<number> {
          const today = new Date().toISOString().split('T')[0];
          const cost = await redis.get<number>(`ai:cost:${today}`);
          return cost || 0;
        }
  verbosity: minimal
11. 検証

検証スクリプトを実行して、AI 統合に関するすべての要件が満たされていることを確認します。

precision_exec:
  commands:
    - cmd: "bash plugins/goodvibes/skills/outcome/ai-integration/scripts/validate-ai-integration.sh ."
      expect:
        exit_code: 0
  verbosity: standard

検証スクリプトでは、次の項目を確認します。

  1. AI SDK または LLM ライブラリがインストールされている
  2. API キーが .env.example に記載されている
  3. ストリーミングエンドポイントが存在する
  4. ソースコードに API キーがハードコードされていない
  5. レート制限が設定されている
  6. AI の呼び出しにエラーハンドリングが実装されている
  7. トークン/コストの追跡パターンが実装されている

デプロイする前に、違反項目を修正してください。

ベストプラクティス

エラーハンドリング
  • AI の呼び出しは必ず try/catch ブロックで囲みます
  • 指数バックオフを使用してレート制限に対処します
  • 外部 API にサーキットブレーカーを実装します
  • API キーを漏えいさせずにエラーをログに記録します
パフォーマンス
  • 頻繁にアクセスされるコンテンツの埋め込みをキャッシュします
  • 長い応答を生成する場合はストリーミングを使用します
  • 可能であれば、埋め込みの生成をバッチ処理します
  • リクエストの重複排除を実装します
コスト管理
  • リクエストごとに最大トークン数を設定します
  • 日次/月次の支出を追跡します
  • 単純なタスクには、より安価なモデルを使用します
  • 適切な場合は、生成結果をキャッシュします
セキュリティ
  • API キーをソース管理に絶対にコミットしないでください
  • ユーザー/IP ごとにレート制限を実装します
  • LLM に送信する前にユーザー入力をサニタイズします
  • ユーザーが作成したプロンプトにはコンテンツモデレーションを使用します
  • ツールの実行結果を検証します
モニタリング
  • トークンの使用量とコストを追跡します
  • 応答のレイテンシを監視します
  • 成功したリクエストと失敗したリクエストをログに記録します
  • 異常な支出パターンを検知したらアラートを出します

よくある落とし穴

  1. ストリーミングを忘れる:チャットインターフェースの UX を向上させるため、常にストリーミングを使用します
  2. レート制限を無視する:バックオフと再試行のロジックを実装します
  3. プロンプトをハードコードする:保守性を高めるため、テンプレートシステムを使用します
  4. ドキュメントをチャンク分割しない:RAG で大きなドキュメントを扱うにはチャンク分割が必要です
  5. コスト追跡を省略する:AI のコストはすぐに膨らむ可能性があります
  6. ユーザー入力を信頼する:必ずサニタイズして検証します
  7. エラーバウンダリがない:障害が発生しても、機能を適切に縮退させる必要があります
  8. コンテキストが大きすぎる:トークンの上限内に収まるよう、コンテキストを制限します

追加リソース

プロバイダーの詳細な比較とアーキテクチャパターンについては、references/ai-patterns.md を参照してください。

コメント