AIP-C01 07: コスト・性能・オブザーバビリティ
Mix.install([
{:kino, "~> 0.19"},
{:kino_vega_lite, "~> 0.1"}
])
概要
最終確認日: 2026-09-22
対応範囲: 4.1〜4.3, 1.2, 2.2, 5.2
このノートで身につけること
- トークン、モデル、キャッシュ、バッチ、スループットのコストを比較する
- TTFT、end-to-end latency、throughput、error rate を区別する
- 技術メトリクスと品質・ビジネスメトリクスを結びつける
- invocation logging の価値と機密データリスクを評価する
最新仕様の要点
Bedrock Runtime は CloudWatch の AWS/Bedrock namespace に呼び出し数、レイテンシー、トークン、エラー、Prompt Cache などのメトリクスを出します。Model Invocation Logging は既定で無効で、設定すると request / response / metadata を CloudWatch Logs または S3 に送れます。全文ログは診断に有用ですが、PII や秘密情報、保持、アクセス制御、コストのリスクがあります。
Prompt Caching は長く繰り返す prefix の入力コストとレイテンシーを下げる候補です。Batch inference は非同期の大量処理、Provisioned Throughput は安定した高負荷、cross-Region inference は利用可能容量の拡大、Prompt Router は品質とコストの動的選択に使います。
ハンズオン1: 単価を変数としてコストを比較する
価格は変更されるため、現在の Bedrock Pricing から対象リージョン・モデルの「1,000 トークン当たり単価」を入力してください。
input_price = Kino.Input.number("入力 $ / 1K tokens", default: 0.0001, step: 0.0001)
output_price = Kino.Input.number("出力 $ / 1K tokens", default: 0.0005, step: 0.0001)
requests_input = Kino.Input.number("月間リクエスト数", default: 100_000)
avg_input = Kino.Input.number("平均入力 tokens", default: 800)
avg_output = Kino.Input.number("平均出力 tokens", default: 200)
Kino.Layout.grid([input_price, output_price, requests_input, avg_input, avg_output], columns: 3)
requests = Kino.Input.read(requests_input)
input_tokens = requests * Kino.Input.read(avg_input)
output_tokens = requests * Kino.Input.read(avg_output)
input_cost = input_tokens / 1_000 * Kino.Input.read(input_price)
output_cost = output_tokens / 1_000 * Kino.Input.read(output_price)
%{
monthly_input_tokens: input_tokens,
monthly_output_tokens: output_tokens,
input_cost_usd: Float.round(input_cost, 2),
output_cost_usd: Float.round(output_cost, 2),
total_usd: Float.round(input_cost + output_cost, 2)
}
ハンズオン2: 最適化シナリオ
baseline = %{requests: 100_000, input_tokens: 800, output_tokens: 200}
scenarios = [
%{name: "baseline", request_ratio: 1.0, input_ratio: 1.0, output_ratio: 1.0},
%{name: "prompt compression", request_ratio: 1.0, input_ratio: 0.7, output_ratio: 1.0},
%{name: "semantic cache 25% hit", request_ratio: 0.75, input_ratio: 1.0, output_ratio: 1.0},
%{name: "maxTokens tuning", request_ratio: 1.0, input_ratio: 1.0, output_ratio: 0.65},
%{name: "combined", request_ratio: 0.75, input_ratio: 0.7, output_ratio: 0.65}
]
unit_input = Kino.Input.read(input_price)
unit_output = Kino.Input.read(output_price)
cost_rows =
scenarios
|> Enum.map(fn scenario ->
request_count = baseline.requests * scenario.request_ratio
in_tokens = request_count * baseline.input_tokens * scenario.input_ratio
out_tokens = request_count * baseline.output_tokens * scenario.output_ratio
cost = in_tokens / 1_000 * unit_input + out_tokens / 1_000 * unit_output
%{scenario: scenario.name, input_tokens: trunc(in_tokens), output_tokens: trunc(out_tokens), cost_usd: Float.round(cost, 2)}
end)
cost_chart =
VegaLite.new(width: 680, height: 270, title: "最適化シナリオ別の月額推定")
|> VegaLite.data_from_values(cost_rows)
|> VegaLite.mark(:bar, tooltip: true)
|> VegaLite.encode_field(:y, "scenario", type: :nominal, title: "シナリオ", sort: "-x")
|> VegaLite.encode_field(:x, "cost_usd", type: :quantitative, title: "月額推定 (USD)")
|> Kino.VegaLite.new()
Kino.Layout.tabs([
{"コスト比較", cost_chart},
{"トークン内訳", Kino.DataTable.new(cost_rows)}
])
キャッシュは「同じ意味なら過去回答を返す」ため、権限、tenant、鮮度、モデル/prompt version、Guardrail version を cache key と invalidation に含めます。コスト削減だけで品質事故を起こさないようにします。
ハンズオン3: SLO とアラーム判定
slo = %{
success_rate: 0.995,
p95_latency_ms: 3_000,
grounded_rate: 0.97,
cost_per_success_usd: 0.01
}
window = %{
requests: 20_000,
successes: 19_860,
p95_latency_ms: 2_850,
grounded_responses: 19_100,
evaluated_responses: 19_500,
cost_usd: 170.0
}
observed = %{
success_rate: window.successes / window.requests,
p95_latency_ms: window.p95_latency_ms,
grounded_rate: window.grounded_responses / window.evaluated_responses,
cost_per_success_usd: window.cost_usd / window.successes
}
Enum.map(observed, fn {metric, value} ->
target = Map.fetch!(slo, metric)
direction = if metric in [:p95_latency_ms, :cost_per_success_usd], do: :max, else: :min
pass = if direction == :max, do: value <= target, else: value >= target
%{metric: metric, observed: value, target: target, pass: pass}
end)
|> Kino.DataTable.new()
成功率だけでは「高速に誤答する」サービスを見逃します。運用、品質、安全、コスト、ビジネス成果の指標を並べます。
ハンズオン4: invocation log を安全に要約する
log_record = %{
request_id: "req-123",
model_id: "example-model",
input_tokens: 820,
output_tokens: 190,
latency_ms: 2_340,
status: 200,
prompt: "顧客 taro@example.com の契約を要約して",
response: "taro@example.com の契約は..."
}
safe_log =
log_record
|> Map.drop([:prompt, :response])
|> Map.put(:content_logged, false)
|> Map.put(:prompt_fingerprint, :crypto.hash(:sha256, log_record.prompt) |> Base.encode16(case: :lower))
safe_log
全文が必要な調査環境では、別ロググループ、KMS、厳格な IAM、短い保持、マスキング、監査を使います。CloudTrail は API 操作の監査、CloudWatch はメトリクス/ログ/アラーム、X-Ray はサービス境界のトレースという役割を区別します。
ハンズオン5: Prompt Cache の損益分岐を測る
Converse の usage.inputTokens はキャッシュ利用時、キャッシュから読んだ/書いたトークンを含まないため、総入力は inputTokens + cacheReadInputTokens + cacheWriteInputTokens で求めます。料金比率はモデルごとに変わるので、現在の価格表から入力してください。
cache_usage = [
%{request: 1, input: 180, cache_read: 0, cache_write: 4_000, output: 220, latency_ms: 2_900},
%{request: 2, input: 190, cache_read: 4_000, cache_write: 0, output: 210, latency_ms: 1_700},
%{request: 3, input: 175, cache_read: 4_000, cache_write: 0, output: 205, latency_ms: 1_620},
%{request: 4, input: 4_180, cache_read: 0, cache_write: 0, output: 215, latency_ms: 2_850}
]
# 価格そのものではなく、通常入力を 1.0 とした相対単価。対象モデルの料金で置き換える。
rates = %{input: 1.0, cache_read: 0.1, cache_write: 1.25}
cache_analysis =
Enum.map(cache_usage, fn row ->
total_input = row.input + row.cache_read + row.cache_write
relative_cost = row.input * rates.input + row.cache_read * rates.cache_read + row.cache_write * rates.cache_write
Map.merge(row, %{
total_input: total_input,
cache_hit_rate: if(total_input == 0, do: 0.0, else: row.cache_read / total_input),
relative_input_cost: relative_cost
})
end)
cache_rows =
Enum.flat_map(cache_analysis, fn row ->
[
%{request: row.request, metric: "latency_ms", value: row.latency_ms},
%{request: row.request, metric: "relative_input_cost", value: row.relative_input_cost}
]
end)
cache_chart =
VegaLite.new(width: 650, height: 260, title: "キャッシュ hit / miss がコストとレイテンシーへ与える影響")
|> VegaLite.data_from_values(cache_rows)
|> VegaLite.mark(:bar, tooltip: true)
|> VegaLite.encode_field(:x, "request", type: :ordinal, title: "request")
|> VegaLite.encode_field(:x_offset, "metric")
|> VegaLite.encode_field(:y, "value", type: :quantitative, title: "値(単位は凡例参照)")
|> VegaLite.encode_field(:color, "metric", type: :nominal, title: "metric")
|> Kino.VegaLite.new()
Kino.Layout.tabs([
{"比較", cache_chart},
{"明細", Kino.DataTable.new(cache_analysis)}
])
1回目の cache write は通常入力より高価な場合があり、再利用が少ない prefix では逆効果です。静的な system / tool definition / 長い参照文書を前、動的なユーザー入力を後ろへ置き、cacheReadInputTokens、hit rate、TTL 内の再利用回数、p95 latency を同時に確認します。暗黙キャッシュは best effort で、同じ入力でも hit は保証されません。
ハンズオン6: 429 をメトリクスから切り分ける
diagnose_throttle = fn sample ->
cond do
sample.input_tpm >= sample.input_tpm_quota -> :input_tokens_per_minute
sample.output_tpm >= sample.output_tpm_quota -> :output_tokens_per_minute
sample.rpm >= sample.rpm_quota -> :requests_per_minute
sample.concurrent >= sample.concurrent_quota -> :concurrency
sample.max_tokens_reserved > sample.output_p95 * 4 -> :excessive_max_tokens
true -> :inspect_error_type_and_service_quota
end
end
throttle_samples = [
%{window: "10:00", input_tpm: 88_000, input_tpm_quota: 100_000, output_tpm: 9_000, output_tpm_quota: 20_000, rpm: 102, rpm_quota: 100, concurrent: 4, concurrent_quota: 10, max_tokens_reserved: 1_000, output_p95: 400},
%{window: "10:01", input_tpm: 101_000, input_tpm_quota: 100_000, output_tpm: 8_000, output_tpm_quota: 20_000, rpm: 80, rpm_quota: 100, concurrent: 5, concurrent_quota: 10, max_tokens_reserved: 1_000, output_p95: 380},
%{window: "10:02", input_tpm: 70_000, input_tpm_quota: 100_000, output_tpm: 7_000, output_tpm_quota: 20_000, rpm: 60, rpm_quota: 100, concurrent: 10, concurrent_quota: 10, max_tokens_reserved: 4_000, output_p95: 350}
]
throttle_samples
|> Enum.map(&Map.put(&1, :likely_bottleneck, diagnose_throttle.(&1)))
|> Kino.DataTable.new()
429 へ無条件にモデル変更や Provisioned Throughput を適用しません。まず dimension(model ID / inference profile / region)、エラー型、RPM / TPM / concurrency、maxTokens、retry 増幅を突き合わせます。安定した高利用率と予測可能な需要を測定してから Provisioned Throughput を比較し、突発負荷では cross-Region inference、キュー、同時実行制御も候補にします。
監視する代表指標
| 分類 | 指標例 |
|---|---|
| 利用 | Invocations、input/output/cache tokens |
| 信頼性 | throttles、4xx/5xx、fallback rate、queue depth |
| 性能 | TTFT、p50/p95/p99 latency、tokens/sec |
| RAG | retrieval latency、Recall@k、empty retrieval、rerank gain |
| 品質・安全 | grounded rate、hallucination rate、Guardrail intervention、schema failure |
| Agent | task completion、tool error、steps、loop termination、approval rate |
| ビジネス | containment、conversion、time saved、cost per successful task |
判断問題
- オンデマンドから Provisioned Throughput に切り替える前に、何を測定しますか。
- Prompt Caching と semantic response cache の違いを説明してください。
totalTokensが一定でもコストやレイテンシーが変わる理由を挙げてください。- invocation logging を有効化しない判断が妥当になる条件は何ですか。