mcpField
L
llm-integration

llm-integration

github.com/rohitg00開発2.8k184 回インストール25 週間前に更新
リポジトリ

llm-integration について

API ベースのアプリを開発する人向けの LLM 統合スキルガイドです。ストリーミング、ツール利用、RAG、文書分割、モデル選択、コスト管理を扱います。

主な機能

  1. 01LLM API クライアントのパターン
  2. 02ストリーミング応答
  3. 03関数呼び出しとツールスキーマ
  4. 04RAG パイプラインと文書分割
  5. 05タスクに応じたモデル選択
  6. 06リトライ、検証、コスト管理

活用シーン

  1. 01LLM アプリへのストリーミング追加
  2. 02文書検索パイプラインの構築
  3. 03モデル選択と重複リクエストの削減

インストール

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

$npx skills add https://github.com/rohitg00/awesome-claude-code-toolkit --skill llm-integration

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

詳細

LLM 統合

API クライアントパターン

import Anthropic from "@anthropic-ai/sdk";

const client = new Anthropic();

async function generateResponse(
  systemPrompt: string,
  userMessage: string,
  options?: { maxTokens?: number; temperature?: number }
): Promise<string> {
  const response = await client.messages.create({
    model: "claude-sonnet-4-20250514",
    max_tokens: options?.maxTokens ?? 1024,
    temperature: options?.temperature ?? 0,
    system: systemPrompt,
    messages: [{ role: "user", content: userMessage }],
  });

  const textBlock = response.content.find(block => block.type === "text");
  return textBlock?.text ?? "";
}

ストリーミング応答

async function streamResponse(
  messages: Array<{ role: "user" | "assistant"; content: string }>,
  onChunk: (text: string) => void
): Promise<string> {
  const stream = client.messages.stream({
    model: "claude-sonnet-4-20250514",
    max_tokens: 4096,
    messages,
  });

  let fullText = "";

  for await (const event of stream) {
    if (event.type === "content_block_delta" && event.delta.type === "text_delta") {
      onChunk(event.delta.text);
      fullText += event.delta.text;
    }
  }

  return fullText;
}

const response = await streamResponse(
  [{ role: "user", content: "Explain async/await in TypeScript" }],
  (chunk) => process.stdout.write(chunk)
);

関数呼び出し(ツールの使用)

const tools: Anthropic.Tool[] = [
  {
    name: "search_database",
    description: "Search the product database by name, category, or price range",
    input_schema: {
      type: "object" as const,
      properties: {
        query: { type: "string", description: "Search query" },
        category: { type: "string", description: "Product category filter" },
        max_price: { type: "number", description: "Maximum price" },
      },
      required: ["query"],
    },
  },
];

async function agentLoop(userMessage: string): Promise<string> {
  const messages: Anthropic.MessageParam[] = [
    { role: "user", content: userMessage },
  ];

  while (true) {
    const response = await client.messages.create({
      model: "claude-sonnet-4-20250514",
      max_tokens: 4096,
      tools,
      messages,
    });

    if (response.stop_reason === "end_turn") {
      const text = response.content.find(b => b.type === "text");
      return text?.text ?? "";
    }

    const toolUse = response.content.find(b => b.type === "tool_use");
    if (!toolUse || toolUse.type !== "tool_use") break;

    const result = await executeToolCall(toolUse.name, toolUse.input);

    messages.push({ role: "assistant", content: response.content });
    messages.push({
      role: "user",
      content: [{ type: "tool_result", tool_use_id: toolUse.id, content: result }],
    });
  }

  return "";
}

RAG パイプライン

import { embed } from "./embeddings";

interface Chunk {
  id: string;
  text: string;
  metadata: Record<string, string>;
  embedding: number[];
}

async function retrieveAndGenerate(query: string): Promise<string> {
  const queryEmbedding = await embed(query);

  const relevantChunks = await vectorDb.search({
    vector: queryEmbedding,
    topK: 5,
    filter: { source: "documentation" },
  });

  const context = relevantChunks
    .map((chunk, i) => `[${i + 1}] ${chunk.text}`)
    .join("\n\n");

  const response = await client.messages.create({
    model: "claude-sonnet-4-20250514",
    max_tokens: 2048,
    system: `Answer questions using the provided context. Cite sources with [n] notation. If the context doesn't contain the answer, say so.`,
    messages: [
      {
        role: "user",
        content: `Context:\n${context}\n\nQuestion: ${query}`,
      },
    ],
  });

  return response.content[0].type === "text" ? response.content[0].text : "";
}

ドキュメントのチャンク分割

function chunkDocument(
  text: string,
  options: { chunkSize: number; overlap: number }
): string[] {
  const { chunkSize, overlap } = options;
  const chunks: string[] = [];
  const sentences = text.split(/(?<=[.!?])\s+/);
  let current = "";

  for (const sentence of sentences) {
    if (current.length + sentence.length > chunkSize && current.length > 0) {
      chunks.push(current.trim());
      const words = current.split(" ");
      const overlapWords = words.slice(-Math.floor(overlap / 5));
      current = overlapWords.join(" ") + " " + sentence;
    } else {
      current += (current ? " " : "") + sentence;
    }
  }

  if (current.trim()) chunks.push(current.trim());
  return chunks;
}

コスト最適化

function selectModel(task: TaskType): string {
  switch (task) {
    case "classification":
    case "extraction":
      return "claude-haiku-4-20250514";
    case "analysis":
    case "coding":
      return "claude-sonnet-4-20250514";
    case "complex-reasoning":
      return "claude-opus-4-5-20251101";
    default:
      return "claude-sonnet-4-20250514";
  }
}

許容できる品質を達成できる最小のモデルを使用します。可能な場合は、埋め込みと応答をキャッシュします。レイテンシーが重要でない場合は、リクエストをバッチ処理します。

アンチパターン

  • 関連するチャンクだけが必要なのに、ドキュメント全体を送信する
  • API 呼び出しで指数バックオフによる再試行ロジックを実装しない
  • トークン使用量の追跡を無視する(予期しないコストにつながります)
  • 単純な分類タスクに最も高価なモデルを使用する
  • コードで使用する前に、LLM の出力を検証またはサニタイズしない
  • まず検索品質を評価せずに RAG を構築する

チェックリスト

  • API 呼び出しに再試行ロジックとエラー処理を実装する
  • ユーザー向けの応答にはストリーミングを使用する
  • 関数呼び出しのスキーマに明確な説明を含める
  • RAG のチャンクを適切なサイズ(500~1000 トークン)にし、オーバーラップを設ける
  • タスクの複雑さに基づいてモデルを選択する
  • コスト管理のためにトークン使用量を追跡・監視する
  • 後続処理で使用する前に LLM の出力を検証する
  • 重複する API 呼び出しを避けるために埋め込みをキャッシュする

コメント