本番環境での運用
既定値は、一般的な SaaS の顧客向けに調整されています。以下は、トラフィックの傾向が変わったときに調整する項目です。レート制限、再試行のポリシー、監査ログ、依存先への到達性のプローブ、IHealthCheck との統合、冪等性、プロンプトテンプレートの上書き、BYOK の管理機能です。
AI の呼び出しのレート制限
POST /richtextbox/ai と POST /richtextbox/ai/stream は IP ごとのトークンバケットを共有しているので、暴走した 1 つのタブがプロバイダーの利用枠を数秒で使い切ることはありません。既定値は、IP ごとに 1 分あたり 30 回です。
builder.Services.AddRichTextBox(opts => { opts.AiRateLimit = 30; // calls per window opts.AiRateLimitWindow = TimeSpan.FromMinutes(1); // window length });
無効にするには AiRateLimit = 0 を設定します。または、Redis を使うリミッターに置き換えます。
builder.Services.AddSingleton<IRichTextBoxAiRateLimiter, RedisAiRateLimiter>(); builder.Services.AddRichTextBox();
プロバイダーの一時的な障害に対する HTTP の再試行
OpenAI / Anthropic / Azure OpenAI は、通常の負荷でも 429 や 503 を返します。組み込みのリゾルバーは、これら(および 502 / 504 と接続の切断)を、上限 30 秒の指数バックオフとフルジッターで再試行します。プロバイダーの Retry-After ヘッダーは尊重されます。
services.AddRichTextBoxOpenAiResolver(opts => { opts.ApiKey = builder.Configuration["OpenAI:ApiKey"]; opts.MaxRetryAttempts = 2; // 0 to disable opts.RetryBaseDelay = TimeSpan.FromMilliseconds(500); });
冪等性キー
POST /richtextbox/ai、/ai/stream、/upload は、いずれも Idempotency-Key を尊重します。重複した再試行には、キャッシュした本文をそのまま返すので、二重に課金されることも、孤立したファイルが残ることもありません。ストリーミングのレスポンスでは、SSE のバイトストリーム全体をバイト単位でそのまま再送します。
builder.Services.AddRichTextBox(opts => { opts.IdempotencyTtl = TimeSpan.FromHours(1); opts.IdempotencyPruneInterval = TimeSpan.FromMinutes(5); });
複数のインスタンスで動かすホストでは、既定のインメモリのものではなく、Redis を使う IIdempotencyStore を登録してください。Redis 自体の TTL を使えるので、定期的な削除処理は無効にできます。
セキュリティの監査ログ
セキュリティに関係するすべての拒否は、RichTextBox.Audit カテゴリで、変わることのない EventId を持つ構造化された LogWarning を出力します。
| EventId | 名前 | 発生するタイミング |
|---|---|---|
| 8001 | LicenseInvalid | .lic ファイルがないか無効なため、エンドポイントがリクエストを拒否したとき。 |
| 8101 | UploadMagicByteMismatch | マジックバイトが、申告された拡張子と一致しないとき。 |
| 8102 | UploadRejectedByValidator | IUploadValidator が Reject を返したとき。 |
| 8103 | UploadInvalidExtension | 許可リストにないため、アップロードが拒否されたとき。 |
| 8104 | UploadOversize | MaxUploadBytes を超えたとき。 |
| 8201 | AiRequestOversize | AI の本文が MaxRequestBytes を超えたとき。 |
| 8202 | AiRateLimitExceeded | 呼び出し元が、IP ごとの AI のレート制限を超えたとき。 |
| 8203 | AiResolverException | AI のリゾルバーが例外をスローしたとき。相関 ID とともに記録されます。 |
| 8204 | AiKeyVaultMiss | BYOK の保管庫がキーを返さなかったとき。 |
| 8205 | AiKeyVaultHit | BYOK の保管庫がキーを返したとき(KeyId は記録されますが、シークレットは決して記録されません)。 |
| 8206 | AiResponseFilterMatched | IRichTextBoxAiResponseFilter がレスポンスを変更したとき。 |
監査イベントを別のストリームに送る
IRichTextBoxAuditSink を使うと、コンプライアンスの規制を受けるワークロード(HIPAA / PCI / SOC2)で、監査イベントだけを書き込み専用の送信先(Splunk HEC、オブジェクトロック付きの S3、監査専用の DB)に送れます。オフラインや単一インスタンスでの利用向けに、JsonLinesAuditSink が同梱されています。
builder.Services.AddRichTextBox(); builder.Services.AddJsonLinesAuditSink("/var/log/myapp/rtb-audit.jsonl");
Channel とバックグラウンドの取り出し処理を使うので、RecordAsync がブロックすることはありません。キューがあふれた場合はイベントを捨て、DroppedCount のメトリクスに記録します。複数のシンクも自然に登録できるので、JsonLines とリアルタイムの Splunk のシンクを追加の手間なく組み合わせられます。依存先への到達性のプローブ
オプションの補助インターフェイスを使うと、RichTextBox のどの依存先でも、その背後のサービスに到達できるかどうかを報告できます。/richtextbox/health エンドポイントは、その結果をライセンスの状態と一緒に表示します。
| インターフェイス | 組み込みの実装 | プローブの対象 |
|---|---|---|
IRichTextBoxAiResolverProbe | OpenAI、Azure OpenAI(無料)。AnthropicResolverWithProbe ラッパー(トークン消費の少ない POST) | GET /v1/models / GET /openai/deployments / max_tokens=1 での POST /v1/messages |
IIdempotencyStoreProbe | 独自の Redis / SQL のストア(自分で実装) | PING / SELECT 1 |
IRichTextBoxUploadStoreProbe | 独自の S3 / Azure Blob / Cloudinary のストア | バケットへの HEAD / list-objects --max-items 1 |
builder.Services.AddRichTextBox(opts => { opts.AiResolverHealthProbeEnabled = true; opts.AiResolverHealthProbeTimeout = TimeSpan.FromSeconds(3); opts.AiResolverHealthProbeCacheTtl = TimeSpan.FromSeconds(30); opts.IdempotencyStoreHealthProbeEnabled = true; opts.UploadStoreHealthProbeEnabled = true; });
各プローブのキャッシュの TTL は、ロードバランサーからの集中的なアクセスを防ぎます。これがないと、5 秒ごとに /health にアクセスするバランサーによって、プローブに実際の費用がかかる場合でも 1 分あたり 12 回のプローブが発生します。
標準の IHealthCheck との統合
Microsoft.Extensions.Diagnostics.HealthChecks を使っているホストでは、アダプターを登録すると、RichTextBox の状態がほかのチェックと並んで表示されます。別の監視経路は必要ありません。
builder.Services.AddRichTextBox(); builder.Services.AddHealthChecks() .AddRichTextBox(name: "richtextbox", failureStatus: HealthStatus.Unhealthy, tags: new[] { "ready" } ); app.MapHealthChecks("/health");
ライセンスと、対象となるすべてのプローブが合格すると Healthy を返し、そうでない場合は失敗した依存先を示す説明とともに Unhealthy を返します。HealthCheckResult.Data には、/richtextbox/health の JSON ペイロードと同じ内容が入ります。
BYOK — テナントごとの API キー
マルチテナントの SaaS のホストは、IAiKeyVault と IAiKeyVaultAdmin を使って、AI のトラフィックを各テナント自身のプロバイダーのアカウントに請求できます。パッケージにはインメモリの参照実装が含まれており、本番環境では Azure Key Vault、Redis、または本物の SQL のストアを使います。
完全な仕様、管理用の REST エンドポイント、参照実装(Redis、Azure Key Vault、ディスク上で暗号化する FileBackedAiKeyVault)については、専用の BYOK キー保管庫のページを参照してください。
呼び出しごとのコストの記録
IRichTextBoxAiCostSink は、AI の呼び出しごとに 1 つの AiUsageRecord(プロバイダー、モデル、モード、トークン数(入力/出力/合計)、レイテンシー、ステータス、KeyId)を出力します。テナントへの費用の請求のために、課金システムに転送してください。
builder.Services.AddSingleton<IRichTextBoxAiCostSink, MyBillingApiCostSink>();トラフィックの多いテナントでは、バッファリングするシンクで包むと、内側のシンクは AI のリクエストごとに 1 回の HTTP/SQL の呼び出しを受けるのではなく、一定間隔ごとにまとめて 1 回のフラッシュを受けるようになります。内側のシンクが IRichTextBoxAiCostBatchSink も実装している場合、ラッパーは RecordBatchAsync を経由するので、N 回ではなく 1 回の INSERT で済みます。
builder.Services.AddBufferingAiCostSink<MyBillingApiCostSink>( flushInterval: TimeSpan.FromSeconds(30), maxBatchSize: 500);
出力のフィルタリング — 個人情報のマスキング、コンテンツのブロック
IRichTextBoxAiResponseFilter は、リゾルバーの後、レスポンスが送信される前に実行されます。参照実装の RegexAiResponseFilter で、実用上の個人情報のマスキングの約 80% をカバーできます。
var emailMask = new Regex(@"[\w.+-]+@[\w-]+\.[\w.-]+"); var ssnMask = new Regex(@"\b\d{3}-\d{2}-\d{4}\b"); builder.Services.AddSingleton<IRichTextBoxAiResponseFilter>( new RegexAiResponseFilter( (emailMask, "[email-redacted]"), (ssnMask, "[ssn-redacted]")));
ストリーミングのレスポンスでは、フィルターが登録されている場合、フレームワークがサーバー側でトークンをバッファーに溜めるので(既定は BufferStreamingForFilters = true)、複数の差分にまたがるクレジットカード番号の正規表現も実際に一致します。
プロンプトテンプレートの上書き
IRichTextBoxPromptTemplateProvider を使うと、組み込みのリゾルバーが送信するモードごとのシステムプロンプトを上書きできます。必要なメソッドだけを上書きするには、デコレーターの基底クラス PromptTemplateProviderBase を使ってください。
public sealed class FormalTonePromptProvider : PromptTemplateProviderBase { public override string BuildSystemPrompt(RichTextBoxAiRequest request, string? operatorSuffix) => base.BuildSystemPrompt(request, operatorSuffix) + "\n\nAlways reply in formal British English; avoid contractions."; }
プロバイダー全体を用意せずに特定のモードだけを調整する場合は、辞書を使います。
services.AddRichTextBoxOpenAiResolver(opts => { opts.ApiKey = "sk-..."; opts.SystemPromptOverridesByMode["proofread"] = "You are a hyper-pedantic copy-editor for legal documents."; });
プロンプトインジェクションの検査
IRichTextBoxAiPolicy は、AI のエンドポイントがリゾルバーを呼び出す前に確認する関門です。組み込みの DefaultPromptInjectionPolicy は、出回っているジェイルブレイクのテンプレート(「ignore previous instructions」「you are now DAN」など)を拒否します。個人情報のマスキングや、テナント単位のモードの許可リストのために、追加のポリシーを重ねて使えます。
builder.Services.AddSingleton<IRichTextBoxAiPolicy, DefaultPromptInjectionPolicy>(); builder.Services.AddSingleton<IRichTextBoxAiPolicy, MyTenantPolicy>();
分散トレーシング(OpenTelemetry)
すべての AI の呼び出し、DOCX のエクスポート、アップロードは、RichTextBox.AspNetCore の ActivitySource を通じてスパンを出力します。トレーサープロバイダーの設定で購読してください。
builder.Services.AddOpenTelemetry().WithTracing(t => t .AddSource(RichTextBoxDiagnostics.SourceName) // "RichTextBox.AspNetCore" .AddAspNetCoreInstrumentation() .AddOtlpExporter());
起動時のオプションの検証
RichTextBoxOptionsValidator と AiResolverOptionsValidator<T> は、最初の IOptions.Value の解決時、つまりホストが最初のリクエストを受け付ける前に実行されます。誤った設定(/ の付け忘れ、負のバイト数の上限、タイムアウトより長いハートビート、API キーの欠落、Azure のデプロイメントの欠落)があると、起動時に OptionsValidationException がスローされるので、デプロイははっきりと失敗します。
設定は不要です。バリデーターは AddRichTextBox() によって自動的に登録されます。