# EchoCache デプロイ手順(Hugging Face Spaces 無料枠) 所要時間 約10分。コピペで完了します。 前提: ブラウザ、`git`、`python3`。有料プラン・GPU・モデルのダウンロードは一切不要です。 > English version: [`deploy.md`](deploy.md) --- ## 1. Space を新規作成する 1. を開く 2. 次のとおり入力する | 項目 | 値 | |---|---| | **Owner** | 自分のアカウント | | **Space name** | `EchoCache`(任意。以降 `/EchoCache` と表記) | | **License** | `apache-2.0` | | **SDK** | **Gradio** | | **Space hardware** | **CPU basic · 2 vCPU · 16 GB · FREE** | | **Visibility** | Public または Private(どちらでも動作します) | 3. **Create Space** を押す。空のリポジトリが作られます。 > Gradio のテンプレート選択画面が出た場合は「Blank」で構いません。どうせ全ファイルを差し替えます。 --- ## 2. ファイルを配置して push する まず書き込みトークンで一度だけログインします(`write` 権限のトークンを で発行)。 ```bash hf auth login # 古い環境では: huggingface-cli login ``` 以下を**そのまま**実行してください(`` だけ自分のアカウント名に置換)。 ```bash USER=; SPACE=EchoCache; SRC="$(pwd)" git clone https://huggingface.co/spaces/$USER/$SPACE ~/$SPACE-space && \ cp "$SRC/app.py" "$SRC/requirements.txt" "$SRC/deploy.md" "$SRC/deploy.ja.md" ~/$SPACE-space/ && \ cp "$SRC/README.gradio.md" ~/$SPACE-space/README.md && \ cd ~/$SPACE-space && git add app.py requirements.txt README.md deploy.md deploy.ja.md && \ git commit -m "Deploy EchoCache: semantic cache + cost observability" && git push ``` `git clone` で認証を求められたら、ユーザー名に自分のアカウント名、パスワードに**アクセストークン**を入力します。 push すると Space が自動でビルドされます(**Building** → **Running**、初回1〜2分)。 ビルドログは Space ページの右上 **Logs** から確認できます。 > **`README.gradio.md` を `README.md` として配置する**点に注意してください(公開リポジトリの `README.md` は `sdk: static` です)。 > `sdk_version` でビルドが失敗する場合は、`README.md` の `sdk_version:` を Space の Settings に表示されている選択肢のバージョンへ書き換えて push し直してください。それ以外は変更不要です。 --- ## 3. Secrets に `HF_TOKEN` を設定する(**任意**・設定しなくても全機能が動く) 埋め込みAPIによる再ランキングを試したいときだけ設定します。 1. Space ページ → **Settings** → **Variables and secrets** 2. **New secret** → Name: `HF_TOKEN` / Value: 自分のトークン → **Save** (`HUGGINGFACE_HUB_TOKEN` / `HUGGINGFACEHUB_API_TOKEN` も同じ意味で読み取られます。 ローカル実行時にこれらをシェルに export していると、意図せず `local+embedding` で起動して 外部通信が発生します。UIヘッダーと `Health` タブの `mode` で必ず確認してください) 3. **Restart this Space** | | HF_TOKEN なし | HF_TOKEN あり | |---|---|---| | 類似判定 | ローカル完結(既定・推奨) | ローカル + 埋め込みの加重平均 | | 主機能 | **すべて動作** | すべて動作 | | 失敗時 | — | 402/429/タイムアウトでも自動でローカルのみに戻る | 無料アカウントの推論クレジットは月 **$0.10 程度**しかありません。**設定しない運用が既定**だと考えてください。 UI のヘッダーに `local-only` / `local+embedding` のどちらで動いているかが常時表示されます。 同じ画面で任意の環境変数も設定できます(よく使うもの): ``` PRICE_IN_PER_1K = 0.003 # 自社の入力単価。設定しないと節約額は $0 のまま PRICE_OUT_PER_1K = 0.015 # 自社の出力単価 MAX_ENTRIES = 5000 # テナント毎の上限 MAX_TOTAL_ENTRIES = 20000 # 全テナント合計の上限(メモリの実質的な歯止め) MAX_TENANTS = 256 # 区画数の上限。超えると最も古いテナントの区画が丸ごと消える DEFAULT_THRESHOLD = 0.92 # 既定は保守的。Sweep を見てから動かす ``` --- ## 4. 動作確認(UI で3分) 1. **Space を開く**(`https://-echocache.hf.space`)。ヘッダーに `Mode: local-only` と出ていれば正常です。 2. **Store で1件登録** - `Store` タブ → `tenant_id` に `acme`、`route` に `support` - `prompt`: `How do I reset my password?` - `response`: `Open Settings > Security > Reset password.` - **Store** を押す → `"stored": true` と `entry_id` が返ります。 - (`Seed 3 demo entries` ボタンで3件まとめて投入することもできます) 3. **Lookup でヒットを確認** - `Lookup` タブ → `tenant_id` に `acme` - `prompt`: `Hi team, how do I reset my password? Thanks in advance!` - **Lookup** → `"hit": true, "stage": "exact"`。挨拶と結びが正規化で除去され、完全一致キーに落ちています。 - 次に `How can I reset my password?` を threshold `0.80` で試すと `"stage": "cosine"` のヒットになります(実測の類似度は約 **0.85**。既定の `0.92` のままだと miss になり、`best_similarity` に 0.85 が返ります。**この数字を見てから閾値を決める**のが正しい手順です)。 - **誤ヒット防止の確認**: `Why do I need to reset my password?` を threshold `0.70` で実行 → `"reason": "guard_rejected"`、`guard_reasons` に `question_type_mismatch:q={why},c={how}`。類似度が高くてもヒットしないことが確認できます。 - **テナント分離の確認**: `tenant_id` を `globex` に変えて同じ質問 → `"reason": "empty_index"`(他テナントの登録内容は一切見えません)。 4. **Dashboard で記録を確認** - `Dashboard` タブ → **Refresh** - `requests / hits / misses / hit rate / saved tokens / estimated saving` の1行サマリ、ヒット率の時系列、テナント別の累積節約、p90レイテンシ、再利用上位、索引の健全性が出ます。 - **Export events CSV** でイベントログを持ち出せます(既定ではプロンプト本文は記録されません)。 5. **Sweep で閾値を決める** - `Sweep` タブ → **Download a sample log** でサンプルCSVを取得 → そのまま `request log CSV` にアップロード - 単価(`price in/out`)を入れて **Run sweep** - 「この閾値なら何%再利用でき、いくら浮くか」が文章と表とグラフで出ます。MismatchGuard **on/off** の2本が並ぶので、**保守的な設定でいくら浮くか**がそのまま稟議資料になります。 6. **実際のAPIパスを取得** - `API Docs` タブ: 稼働中のサーバーから組み立てた実パス(curl と gradio_client)が表示されます。 - 画面最下部の **Use via API** リンク(Gradio 自身が生成)が常に正典です。 ### CLI で確認したい場合 ```bash SPACE="https://-echocache.hf.space" EVENT_ID=$(curl -s -X POST "$SPACE/gradio_api/call/store" -H "Content-Type: application/json" \ -d '{"data": ["acme","How do I reset my password?","Open Settings > Security > Reset password.","support",86400]}' \ | python3 -c "import sys,json; print(json.load(sys.stdin)['event_id'])") curl -s -N "$SPACE/gradio_api/call/store/$EVENT_ID" EVENT_ID=$(curl -s -X POST "$SPACE/gradio_api/call/lookup" -H "Content-Type: application/json" \ -d '{"data": ["acme","Hi team, how do I reset my password? Thanks!",0.92,"support"]}' \ | python3 -c "import sys,json; print(json.load(sys.stdin)['event_id'])") curl -s -N "$SPACE/gradio_api/call/lookup/$EVENT_ID" ``` ```bash pip install gradio_client python3 - <<'PY' from gradio_client import Client c = Client("/EchoCache") print(c.predict("acme", "how do i reset my password", 0.92, "support", api_name="/lookup")) print(c.predict(api_name="/health")) PY ``` --- ## 5. 運用の型 ### 5-1. スリープ前にエクスポート、復帰後にインポート 無料枠は**ディスク非永続・48時間無操作でスリープ**(スリープ時間は変更不可)。索引はメモリ上にしかないので、**明示的に持ち出す**のが運用の型です。 1. `Backup` タブ → `include response bodies` にチェック → **Export index** 2. `index.json` をダウンロードして手元(または S3 / Drive / リポジトリ外の安全な場所)に保管 3. スリープや再起動のあと、Space を開く → `Backup` タブ → `index.json` をアップロード → **Import index** 4. 結果の `imported / skipped_no_response / expired / rejected_by_safety` を確認 * インポート時はベクトルを**再計算**するので、`VECTOR_DIM` を変えていても取り込めます。 * インポート時に安全検査が**再実行**されます(`skip_safety` は原則オフのまま)。 * 応答本文を含めずにエクスポートしたファイルは、分析用です。取り込んでもキャッシュとしては使えません(`skipped_no_response` に計上されます)。 日次でバックアップしたいなら、手元から叩くだけです: ```bash python3 - <<'PY' from gradio_client import Client import datetime c = Client("/EchoCache") path, receipt = c.predict(True, "", api_name="/export_index") dst = "echocache-%s.json" % datetime.date.today() open(dst, "w").write(open(path).read()) print(dst, receipt) PY ``` ### 5-2. 閾値は Sweep の結果を見てから決める * 既定の `0.92` は**保守的**です。この値では主に「完全一致+表記ゆれ(挨拶・敬語・句読点・大文字小文字)」が再利用されます。 * 言い回しが変わる本当の言い換えは、文字 n-gram では **0.80〜0.90** に落ちるのが普通です。 * **本番のログを1日分 CSV(`tenant_id, prompt, response, route`)にして Sweep にかけ**、再利用率・節約額・境界ヒット件数・Guard 棄却件数を見てから決めてください。勘で下げないこと。 * 下げる前に、まず `Audit` タブで Guard の棄却理由を見ます。「棄却が多い=閾値が高すぎる」とは限らず、「そもそも意味が違う質問が多い」だけのこともあります。 * 公開ベンチマーク([`echocache-guard-benchmark`](https://huggingface.co/datasets/NagaYu/echocache-guard-benchmark))では **閾値0.80 + Guard有効**が正答率99.2%・再利用率100%・誤再利用1/90でした。ただしこれは合成データ1セットの結果です。**自分のログで測ってください。** ### 5-3. 定期的に見る場所 | 頻度 | 見るもの | 判断 | |---|---|---| | 毎日 | Dashboard の hit rate / saved cost | 期待どおり伸びているか | | 毎日 | Audit の Guard rejections | 棄却理由の傾向。誤ヒットの予兆 | | 週次 | Dashboard の索引健全性(`evicted` / `expired`) | 追い出しが多いなら TTL か上限を見直す | | 週次 | Sweep を最新ログで再実行 | 閾値の再確認 | | 変更時 | Health の `mode` | `local-only` から意図せず変わっていないか | --- ## 6. よくある失敗と対処 ### メモリを食う / Space が落ちる 索引メモリはほぼ `エントリ数 × VECTOR_DIM × 4 バイト`で、**`MAX_ENTRIES` はテナント毎**です。既定では 1テナント満杯で約 80 MB、全体は `MAX_TOTAL_ENTRIES=20000` で約 330 MB に抑えられています。 対処(Settings → Variables and secrets、上から順に効きます): ``` MAX_TOTAL_ENTRIES = 8000 # 全体の歯止め。まずここを下げる MAX_ENTRIES = 2000 # テナント毎の上限 MAX_TEXT_CHARS = 4000 # 1エントリの保存文字数 VECTOR_DIM = 2048 # メモリ半減。精度低下はわずか EVENT_BUFFER = 2000 # イベントのリングバッファ ``` `Health` タブの `approx_memory_mb` と `vector_matrix_mb_per_full_tenant` で実測値を確認できます。 `VECTOR_DIM` を変えたら、**既存のエクスポートは再インポートで作り直されます**(ベクトルは再計算されるので互換性の心配は不要です)。 ### 保存したはずのテナントが空になっている テナント数が `MAX_TENANTS`(既定256)に達すると、**新しいテナントの初回書き込み時に、 最も長く使われていないテナントの区画が丸ごと破棄されます**(削除のみで、テナント間でデータが 混ざることはありません)。実際のテナント数より十分大きい値を設定してください。 全体のエントリ数が `MAX_TOTAL_ENTRIES` に達した場合は、最大の区画から1件ずつ古い順に追い出されます。 ### ヒットしない **下げる前に順番に確認してください。** 1. `Lookup` の結果の `reason` を見る * `empty_index` … そのテナントに1件も入っていない(`tenant_id` の打ち間違いが最多) * `no_candidates` … SimHash の帯に候補なし。文面が大きく違う * `below_threshold` … `best_similarity` が出ます。この数字を見てから閾値を判断する * `guard_rejected` … **意味が違うと判定された**。次を見る 2. `Audit` タブで Guard の棄却理由を見る(`numeric_mismatch` / `negation_mismatch` / `proper_noun_mismatch` / `temporal_mismatch` / `question_type_mismatch`)。理由が妥当なら、それは**正しい miss** です。閾値を下げても直りません。 3. 正規化の効きを確認する: `Store` したプロンプトと `Lookup` のプロンプトが、挨拶・敬語・記号を除いて本当に同じか。`cached_prompt_preview` が返ってくるので見比べられます。 4. それでも取りこぼしているなら、Sweep で **0.86 → 0.82** のように段階的に下げ、`borderline_hits` と `guard_blocks` の増え方を見る。 5. 特定の検査が業務に合わないと確信できた場合のみ、`GUARD_DISABLE=proper_noun` のように**個別に**外す(全部外すのは非推奨)。 ### 誤ヒットが出た(最優先で対処) 1. **閾値を上げる**(例: `0.92` → `0.95`)。即効性があります。 2. `Audit` タブ、または `Dashboard` → *most reused entries* で該当の `entry_id` を特定する。 3. `Audit` タブ下部の **Invalidate entry** に `tenant_id` と `entry_id`(先頭16文字で十分)を入れて失効させる。 * ルート単位なら `key_prefix` にルート名(例: `support`) * テナント全体なら `*` 4. 同じ型の誤ヒットが再発するなら、その事例を CSV に追加して Sweep を回し直し、**その型を弾ける閾値**を採用する。 5. 意味の差が Guard の5種(否定・数量・固有名詞・時制・疑問種別)で説明できない型なら、その質問カテゴリは**キャッシュ対象から外す**(`route` を分けて、その route を定期的に `invalidate` する運用が簡単です)。 ### 埋め込みAPIが 402 / 429 になる * `402 Payment Required` … 無料枠の推論クレジット(月 $0.10 程度)が尽きています。 * `429 Too Many Requests` … レート制限。 > `HF_TOKEN` を消したのに `local+embedding` のままなら、`HUGGINGFACE_HUB_TOKEN` / > `HUGGINGFACEHUB_API_TOKEN` が残っていないか確認してください(同じ意味で読まれます)。 **どちらでも主機能は止まりません。** 例外は握りつぶされ、ローカルベクトルのみで判定が続きます。連続失敗が閾値を超えるとサーキットブレーカーが開き、`EMBED_COOLDOWN_SEC`(既定600秒)は呼び出しを止めて無駄な課金を防ぎます。 `Health` タブの `embedding.last_error` と `cooldown_remaining_sec` で状態を確認できます。恒久的に止めるなら **Secrets から `HF_TOKEN` を削除して Restart** してください(`local-only` に戻ります)。 ### Space が 48時間で寝てしまう / 起きたら索引が空 無料枠の仕様です(スリープ時間は変更不可、ディスクは非永続)。**5-1 の運用(エクスポート→インポート)で引き継いでください。** 常時起動が必要なら有料ハードウェアか、自前ホストへ同じ `app.py` を置くことになります(`python app.py` で動きます)。 ### `/store` が `stored: false` で返る 安全検査で弾かれています。`reason` を見てください。 | reason | 意味 | |---|---| | `credit_card_luhn` | Luhn 検証を通った数字列(カード番号の可能性)。注文番号などは通過します | | `credential_prefix` | `sk-` / `ghp_` / `AKIA` / `xoxb-` / `AIza` / `hf_` などの資格情報 | | `jwt_structure` | JWT の三部構造 | | `high_entropy_secret` | 長い高エントロピー英数字列(識別子らしい構造のものは除外されます) | | `contact_pii_excess` | メールアドレスや電話番号が規定数を超過 | | `private_key_block` | PEM 秘密鍵ブロック | **これは正常動作です。** 秘密情報をキャッシュに入れないための最後の防波堤なので、閾値を緩める前に、そもそもその本文をキャッシュに入れてよいのかを検討してください。 ### API のパスが見つからない / 404 Gradio のバージョンでパスが変わります(Gradio 5・6 は `/gradio_api/call/`、4 は `/call/`)。 **ハードコードせず**、`API Docs` タブか画面最下部の **Use via API** リンクから取得してください。`gradio_client` を使えばパスの違いは吸収されます。 --- ## 付録: ローカルで動かす ```bash pip install -r requirements.txt python3 app.py # http://localhost:7860 ``` Space と同じ挙動です。環境変数はそのままシェルで渡せます: ```bash PRICE_IN_PER_1K=0.003 PRICE_OUT_PER_1K=0.015 MAX_ENTRIES=1000 python3 app.py ```