Claude Haiku 5.5入門——小さなAIタスクをFirestoreで原価管理する
Claude Haiku 5.5を題材に、Next.js + Firestoreで問い合わせ分類や要約などの高頻度AIタスクを低コストに回し、実行単価を監査する実践手順を解説します。

はじめに
AI機能をプロダクトに入れると、実は「大きな推論」よりも「小さな処理」が先に増えます。
| 小さな処理 | ありがちな課題 |
|---|---|
| 問い合わせの一次分類 | 1件あたりは軽いが、毎日数千件になる |
| チケット要約 | 失敗すると担当者の確認時間が増える |
| GitHub Issueの優先度付け | 判断根拠とコストが後から追えない |
| 長い会話の圧縮 | 便利だが、積み上がると月額が読みにくい |
Anthropicは2026年10月7日に Claude Haiku 5.5 を発表しました。公式発表では、高頻度でコストに敏感なタスク、要約、圧縮、データベースクエリ、分類、ライブカスタマーサポート、ブラウザ利用などに向いた小型モデルとして紹介されています。モデルIDは claude-haiku-5-5 です。
本記事では、架空の社内問い合わせ管理ツール「HelpPort」を題材に、Next.js App Router + Firestore + TypeScriptで「Haiku 5.5用の実行台帳」を作ります。この記事を読み終えると、AI分類を単に呼び出すだけでなく、入力トークン、出力トークン、推定コスト、confidence、人手確認の有無をFirestoreに残せるようになります。
参考にした一次情報は次の通りです。
- Anthropic: Claude Haiku 5.5
- Claude Platform Docs: Claude Haiku 5.5 migration guide
- Claude API Reference: Create a Message
- Claude Platform Docs: Structured outputs
- Claude API Reference: Count tokens in a Message
何が変わったのか
Haiku 5.5の価値は、「全部を高性能モデルへ投げる」設計を見直せる点にあります。Anthropicの発表では、Haiku 5.5はHaiku 4.5より大幅に安く、100,000トークン以下のプロンプトでは入力が100万トークンあたり0.10ドル、出力が100万トークンあたり0.50ドルと案内されています。さらに、Haikuクラスとして初めて調整可能な effort を持つため、軽い分類は低め、少し迷う要約は中程度、という運用に寄せられます。
ただし移行時の注意点もあります。公式の移行ガイドでは、Haiku 5.5に移る場合、モデルIDの差し替えだけでなく、トークン数の再計測、thinking の扱い、temperature / top_p / top_k の削除、assistant prefillの見直し、stop_reason: "refusal" の処理が必要だと説明されています。
HelpPortでは、Haiku 5.5を「安いから何でも任せるモデル」ではなく、「高頻度の小タスクを測りながら任せるモデル」として扱います。
事前準備
サーバー側だけでAPIキーを扱います。ここではSDKの便利メソッド名を推測せず、公式API Referenceで確認できる POST /v1/messages を fetch で呼びます。
yarn add firebase-admin
ANTHROPIC_API_KEY=sk-ant-...
Firestoreには、問い合わせ本体の supportTickets と、AI呼び出しの記録である aiTaskRuns を分けて保存します。本文を長期保存しすぎない方針なら、台帳側には問い合わせID、処理種別、要約、分類結果、トークン使用量だけを残す設計にしてください。
| コレクション | 用途 |
|---|---|
supportTickets | 問い合わせ本文、顧客プラン、現在の担当キュー |
aiTaskRuns | モデル、effort、入力種別、usage、推定コスト、判断結果 |
ハンズオン1: Haiku 5.5の分類関数を作る
まず、問い合わせを受け取り、構造化JSONで分類結果を返す関数を作ります。ClaudeのStructured outputsは output_config.format にJSON Schemaを渡す仕組みです。後続処理を壊さないよう、分類カテゴリは enum で固定します。
type TriageResult = {
queue: "billing" | "technical" | "success";
urgency: "low" | "medium" | "high" | "critical";
summary: string;
confidence: number;
needsHumanReview: boolean;
};
type ClaudeMessageResponse = {
stop_reason: "end_turn" | "max_tokens" | "stop_sequence" | "tool_use" | "refusal" | string;
content: Array<{ type: "text"; text: string } | { type: string }>;
usage?: {
input_tokens: number;
output_tokens: number;
};
};
export async function classifyTicketWithHaiku(input: {
ticketId: string;
subject: string;
message: string;
customerPlan: "free" | "team" | "enterprise";
}) {
const apiKey = process.env.ANTHROPIC_API_KEY;
if (!apiKey) {
throw new Error("ANTHROPIC_API_KEY is not configured");
}
const response = await fetch("https://api.anthropic.com/v1/messages", {
method: "POST",
headers: {
"x-api-key": apiKey,
"anthropic-version": "2023-06-01",
"content-type": "application/json",
},
body: JSON.stringify({
model: "claude-haiku-5-5",
max_tokens: 1024,
thinking: { type: "adaptive" },
output_config: {
effort: "low",
format: {
type: "json_schema",
schema: {
type: "object",
properties: {
queue: { type: "string", enum: ["billing", "technical", "success"] },
urgency: { type: "string", enum: ["low", "medium", "high", "critical"] },
summary: { type: "string" },
confidence: { type: "number" },
needsHumanReview: { type: "boolean" },
},
required: ["queue", "urgency", "summary", "confidence", "needsHumanReview"],
additionalProperties: false,
},
},
},
system:
"You classify support tickets for a SaaS product. Return concise operational labels only.",
messages: [
{
role: "user",
content:
`Ticket ID: ${input.ticketId}\n` +
`Customer plan: ${input.customerPlan}\n` +
`Subject: ${input.subject}\n` +
`Message: ${input.message}`,
},
],
}),
});
if (!response.ok) {
throw new Error(`Claude API failed: ${response.status}`);
}
const payload = (await response.json()) as ClaudeMessageResponse;
if (payload.stop_reason === "refusal") {
throw new Error("Claude refused this request");
}
const text = payload.content.find((block) => block.type === "text")?.text;
if (!text) {
throw new Error("Claude did not return a text block");
}
return {
result: JSON.parse(text) as TriageResult,
usage: payload.usage,
};
}
ここで temperature や top_p を指定していない点が重要です。Haiku 5.5の移行ガイドでは、これらのサンプリングパラメータを削除するよう案内されています。出力の安定性は、温度調整ではなく、スキーマ、プロンプト、effort、アプリ側の検証で担保します。
ハンズオン2: Firestoreに原価台帳を残す
次に、Route Handlerから分類を実行し、トークン使用量と推定コストを aiTaskRuns に保存します。ここでは100,000トークン以下の一般的な問い合わせを想定し、公式発表のHaiku 5.5価格を使って概算します。
// src/app/api/tickets/[ticketId]/triage/route.ts
import { FieldValue } from "firebase-admin/firestore";
import { NextRequest, NextResponse } from "next/server";
import { adminDb } from "@/lib/firebase/admin";
import { classifyTicketWithHaiku } from "@/lib/ai/classifyTicketWithHaiku";
type Params = {
params: Promise<{ ticketId: string }>;
};
function estimateHaiku55Usd(usage?: { input_tokens: number; output_tokens: number }) {
if (!usage) return null;
const inputUsd = (usage.input_tokens / 1_000_000) * 0.10;
const outputUsd = (usage.output_tokens / 1_000_000) * 0.50;
return Number((inputUsd + outputUsd).toFixed(8));
}
export async function POST(request: NextRequest, { params }: Params) {
const { ticketId } = await params;
const body = (await request.json()) as {
subject?: string;
message?: string;
customerPlan?: "free" | "team" | "enterprise";
};
if (!body.subject || !body.message || body.message.length > 8000) {
return NextResponse.json({ error: "invalid ticket" }, { status: 400 });
}
const runRef = adminDb.collection("aiTaskRuns").doc();
await runRef.set({
ticketId,
task: "support_triage",
provider: "anthropic",
model: "claude-haiku-5-5",
effort: "low",
status: "running",
createdAt: FieldValue.serverTimestamp(),
updatedAt: FieldValue.serverTimestamp(),
});
try {
const { result, usage } = await classifyTicketWithHaiku({
ticketId,
subject: body.subject,
message: body.message,
customerPlan: body.customerPlan ?? "free",
});
const needsHumanReview =
result.needsHumanReview ||
result.confidence < 0.72 ||
result.urgency === "critical";
await runRef.update({
status: "succeeded",
result,
needsHumanReview,
usage: usage ?? null,
estimatedUsd: estimateHaiku55Usd(usage),
updatedAt: FieldValue.serverTimestamp(),
});
await adminDb.collection("supportTickets").doc(ticketId).set(
{
triage: {
queue: result.queue,
urgency: result.urgency,
summary: result.summary,
aiTaskRunId: runRef.id,
needsHumanReview,
},
updatedAt: FieldValue.serverTimestamp(),
},
{ merge: true },
);
return NextResponse.json({ result, needsHumanReview });
} catch (error) {
await runRef.update({
status: "failed",
error: error instanceof Error ? error.message : "unknown error",
updatedAt: FieldValue.serverTimestamp(),
});
return NextResponse.json({ error: "triage failed" }, { status: 500 });
}
}
実務では、estimatedUsd だけでなく、ユーザー、顧客プラン、GitHub Issue番号、画面名も一緒に残すと便利です。1件ごとの単価が小さくても、月間10万件の分類になると意思決定に効いてきます。
ハンズオン3: GitHub Actionsで日次レポートを作る
最後に、Firestoreの台帳を日次で集計し、予算超過の兆候をGitHub Issueへ通知できる形にします。ここでは処理の考え方だけを示します。
| 監査したい値 | 見る理由 |
|---|---|
task ごとの合計コスト | どのAI機能が増えているか分かる |
confidence の分布 | 自動化しすぎていないか確認できる |
needsHumanReview の比率 | AIの品質低下や入力変化を検知できる |
stop_reason / エラー | refusalやmax_tokensを早期に見つけられる |
Haiku 5.5のような小型モデルは、1回の呼び出し単価だけを見ると「気にしなくてよさそう」に見えます。しかし、プロダクトに組み込むなら、数ではなく線で見た方が安全です。日次の推移、機能別の偏り、特定顧客での急増を追えるようにしておくと、モデル変更やプロンプト変更の判断がかなり楽になります。
運用で気をつけたいこと
Haiku 5.5への移行では、まず既存プロンプトのトークン数を数え直してください。移行ガイドでは、Haiku 5.5は新しいトークナイザーを使うため、同じテキストでもHaiku 4.5より多くのトークンとして数えられる場合があると説明されています。max_tokens と予算見積もりは、過去の数値をそのまま使わない方が安全です。
また、effort は「高いほど常に正義」ではありません。HelpPortなら、問い合わせ分類は low、要約は medium、法務やセキュリティに関わる判断はHaikuに閉じず人手確認または上位モデルへ回す、という分担にします。
まとめ
Claude Haiku 5.5は、AI機能を「たまに使う高級な推論」から「毎日大量に走る業務処理」へ広げるときに扱いやすいモデルです。
特にNext.js + Firestoreの構成では、API呼び出しをRoute Handlerに閉じ込め、aiTaskRuns にusageと推定コストを保存するだけで、AI機能の原価が見えるようになります。小さな分類、要約、圧縮から始め、confidenceと人手確認率を見ながら適用範囲を広げていくのがおすすめです。