「AI 引用率の最適化」を、勘に頼った prompt 調整から、測定可能・再現可能・スケール可能なエンジニアリングシステムへ変える。
Ch 3 の七次元スコアリングは「ブランド引用率はいくつか」に答えるが、「次の記事をどう書けば引用率が上がるのか」は教えてくれない。商業顧客は実際にスコアを手にしてこう問う:
「AI 引用率が 32 点だと分かった。次の blog をどう直せば 60 まで上がる?」
「この fact-check は、なぜ ChatGPT は引用するのに Gemini は引用しないのか?」
「同じ内容を書き方を変えると、引用率はどれだけ変わるのか?」
スコアリングシステムは「測定」しか担わず、「処方」は担わない。処方を marketing のコピーライターや外注に丸投げするのは、SaaS の核心価値を人間の経験に漏らすのと同じ——そして人間の経験は 1 万テナント scale で一つ一つ手調整することは不可能である。
V1 以前(2026 年 1〜2 月)、プラットフォームには「LLM コンテンツ書き換え」の internal tool があり、エンジニアが ad-hoc に OpenAI playground で顧客コンテンツを回していた。3 ヶ月で頓挫した。3 つの失敗:
F12 という名前は Web DevTools の F12 キーに由来する——押すとブラウザの「構造インスペクタ」が開く。この比喩を LLM 引用率最適化に持ち込んだ:あらゆるコンテンツは「F12 一発」で三層の構造スコアを見られ、必要に応じて書き換えられるべきである。
| Ch 3 スコアリング | F12 | |
|---|---|---|
| 入力 | ブランド次元 + AI プラットフォーム次元 | 一段のコンテンツ(URL / テキスト / Markdown) |
| 出力 | 0–100 点 + シグナル分項 | 三層スコア + 最適化後コンテンツ |
| 用途 | 「自ブランドは健全か」 | 「この記事をどう直せば引用されるか」 |
| 対象 | brand-level | content-level |
| 頻度 | 毎日 / 週次スキャン | 記事書き込みごとにトリガー |
| コスト | scan platform call(LLM 1 回/クエリ) | analyzer + optimizer(規則 + 必要に応じて LLM) |
F12 の核心的な約束は、同一のコンテンツを与えたとき、システムが複数のバリアントを生成し、LLM が最も引用しそうなものを選び出せることである。
SurferSEO / Clearscope などの SEO ツールの核心は依然として「keyword density / TF-IDF」であり、目標は Google 検索順位である。F12 の目標は LLM 引用であり、両者のシグナルは重ならない:
両者は対立しない——良い SEO コンテンツはたいてい良い GEO コンテンツでもあるが、逆は成り立たない。F12 が補うのは「LLM-specific」なシグナルであり、これは従来の SEO ツールが全く見られないものである。
| 層 | 粒度 | 検査内容 |
|---|---|---|
| Macro | 文書レベル | ≥1 篇の fact-check / FAQ / comparison があるか?明確な H1 があるか?entity を宣言しているか? |
| Meso | 段落レベル | 段落長の分布、TL;DR が前にあるか、LLM の摘録を誘う lead-in 文があるか |
| Micro | 文レベル | 数字 / 日付 / ソースリンク密度、実体(entity)マークアップ、引用可能な atomic fact があるか |
各層は独立して採点(0–100)、最終的に overall = 0.4 × macro + 0.35 × meso + 0.25 × micro(重みは scoring_configs.f12_thresholds_<brand_type> SSOT に格納、admin がコード変更なしに調整可能)。
なぜ三層か:LLM が「何を引用するか決める」とき、経験的に見るのは 3 つの尺度である:
いずれかの層が欠けると、LLM 引用確率は 30〜50% 落ちる。2026 Q1 に 3 ヶ月の内部実験を行い、三層がそれぞれ独立して有効であることを確認した——macro を外すと引用率は 31% 落ち、meso を外すと 47% 落ち、micro を外すと 38% 落ちた。
V1 のアナライザは純粋に規則式である:正規表現 + AST 解析 + 統計密度、LLM を使わない。毎秒 30 篇以上を分析でき、batch の全プラットフォームスキャンに適する。
設計理由:
具体的な規則の例(抜粋):
macro:
- has_fact_check: 1 if brand has any fact-check page
- has_clear_h1: 1 if first <h1> within 100 chars and ≥10 chars
- has_entity_declaration: 1 if Schema.org Person/Org/Service in JSON-LD
meso:
- avg_paragraph_chars: weighted by closeness to ideal range
- has_tldr_in_first_300: 1 if "TL;DR" / "簡言之" / "重點" within first 300 chars
- lead_in_score: count of paragraphs starting with question/statement hook
micro:
- number_density: count(<digit>+) / total_chars
- date_density: count(YYYY-MM-DD | YYYY 年 X 月 | etc) / total_chars
- source_link_density: count(<a href>) / paragraph_count
- entity_mention_count: count of Schema.org-marked entities in text
各規則の閾値(paragraph_ideal_min、number_density_target など)はすべて scoring_configs SSOT にあり、admin はコードを変えずに調整できる。踏んだ落とし穴:閾値をコードにハードコードすると、変更のたびに deploy + 再起動が必要で、顧客は新しいスコアを見るまで 30 分待つ;SSOT に変えてからは即時反映になった。
スコアが < 70(scoring_configs.f12_score_thresholds.low_score)の場合、自動的にキューに入り LLM Optimizer を通る:
[低スコアページ] → axp_pages.content_md を取得 → LLM へ投入
→ 与える:原文 + 三層分析 issues + 参考 templates
→ 要求:スコアを ≥ 20 上げるよう書き換えるが、cosine 類似度 ≥ 0.90 で脱線しない
→ axp_page_history に書き込む(双方向 rollback 可)
→ analyzer を再実行し、新スコア ≥ 元スコア + 15 を確認
→ axp_pages に書き込み needs_recompile をマーク
LLM prompt の構造(簡略版):
你是 GEO 內容優化專家。下面是一篇分數 X 的內容,issue 為:[列表]
請改寫,目標:
1. 修補列出的 issue
2. 保持原意(別偏題,別加客戶沒說的事實)
3. 段落結構偏好 TL;DR + 條列 + 來源連結
可參考的 5 個 template 風格:[列表]
原文:
[content_md]
改寫後輸出 JSON:{ rewritten: "...", reasoning: "..." }
min_similarity ≥ 0.90 は spec の定錨点である(0.85 から 0.90 へ引き上げ、migration 186)。もとの 0.85 は一見妥当に思えたが、実測で 2 つの落とし穴を踏んだ:
0.90 に上げた後、前者は 0.83 に下がってブロックされ、後者も 0.89 に下がってブロックされた。代償は一部の妥当な書き換えもブロックされること(約 12% の false negative)だが、「違法コンテンツが本番に出る」リスクに比べれば、この寛容度の狭さは許容できる。
axp_page_history テーブルは Optimizer による各書き換え前の snapshot を保存し、admin UI からワンクリックで rollback できる。設計動機:
V1 の初期には history テーブルがなく、あるとき cron rerun のバグで 30 brand の fact-check ページがすべて書き換えられた。調べると、RAG 失効で空の ground_truth を受け取った Optimizer が空疎な内容を生成していた。当時は rollback できず、force-refresh RAG + pipeline 再実行しかなく、30 brand × 22 page = 660 LLM call をすべて回し直し、5 時間後にようやく復旧した。
axp_page_history を追加してからは、同種の事象は 30 秒で rollback できる。代償は DB が年間 ~40 GB 増えること(content_md 平均 8 KB × 30 brand × 22 page × 365 日 × 1.5 のリラン率)だが、一度の障害で 5 時間の downtime に比べれば、それだけの価値がある。
axp_pages に書き込むあらゆる経路(9 個の generators + 4 legacy + admin 手動編集)はすべて F12 analyze を hook し、新しいコンテンツが即座に採点され、閾値を下回れば即座に optimizer にキューされることを保証する。カバレッジ検証:
axpPageWriter.service.js#upsertAxpPage(主経路)hybridCoordinator.service.js(6 核心類)factCheckGenerator.js#refreshFactCheckPage(直 SQL、hook を追加)axp.controller.js#updatePageContent(admin 編集)漏れは weekly backfill cron(0 2 * * 0)がセーフティネットとして拾う。
なぜ hook 全カバレッジが難しいか:9 個の generators は同じチームが同じ sprint で書いたものではない——overviewGenerator は V1、comparisonGenerator は V2、factCheckGenerator は V2.5、featuresGenerator は V3 が書いた。各 generator の「axp_pages への書き込み」ロジックは少しずつ異なる(service を使うもの、直 SQL を使うもの、service を迂回して直接 INSERT するもの)。
コードベース全体を 1 週間 grep して「axp_pages への書き込み」全経路を洗い出した:
grep -rn "INSERT INTO axp_pages\|UPDATE axp_pages\|axp_pages.*INSERT\|axp_pages.*UPDATE" backend/src/
13 個の入口を見つけ、一つずつ F12 hook を補った。最後に weekly backfill をセーフティネットとして追加——将来 hook 漏れの新 generator が加わっても、backfill が日曜 02:00 に全プラットフォームを再分析し、漏れを補う。実測では weekly backfill は一度 ~12 分(30 brand × 22 page = 660 篇、約 1 篇/秒)。
| Cron | スケジュール | 用途 |
|---|---|---|
f12-weekly-backfill |
0 2 * * 0 |
全 brand × 22 page F12 再分析 + brand_faq 健全性チェック |
f12-trends-refresh |
30 2 * * * |
REFRESH MATERIALIZED VIEW structural_score_trends |
f12-low-score-optimizer |
0 4 * * * |
LLM rewrite 低スコアページ、双方向可逆 |
f12-immediate-optimize |
priority queue | axpPageWriter hook トリガー、score < 70 で即キュー |
f12-retention-cleanup |
50 3 * * * |
30 日で古い optimization runs を掃除(後に v3.24 retention に吸収) |
時間帯選定のロジック:
priority queue は即時トリガーで cron 表には入らない——axpPageWriter hook が score < 70 の新コンテンツを検知すると即座に enqueue し、worker が 1〜2 分以内に処理を完了する。
V1 が 4 ヶ月走った後、2026 年に発表された 2 篇の arxiv 論文から「もう 1 桁上げる」手がかりを見つけた。
AutoGEO(arXiv:2510.11438, github.com/cxcscmu/AutoGEO):
CMU チームが発表。核心手法は何千もの「LLM に引用された vs されなかった」コンテンツの対比から、汎化可能な書き換え規則を自動 mining することである。実験範囲:
長所:実際の対比実験から derived された、直感ではない 短所:rule 集合が相対的に狭い(25 条のみ)、かつ大部分が Gemini 向け
E-GEO(arXiv:2511.20867, github.com/psbagga17/E-GEO):
独立研究者が発表。核心手法は異なる prompt スタイル(authoritative / technical / unique / fluent / clickable / diverse / quality / competitive / trick / format / FAQ / advertisement / language / minimalist / storytelling)から生成することで、各スタイルが異なる LLM 上でどれだけ引用率が違うかを実測する。
長所:template が多様で、異なる use case に適合 短所:template ごとの uplift 数値が示されていない(全体的な実験 macro 結論のみ)
2 篇の方法論は補完的である:
両者を字面通り(line-by-line)にシステムへ port し、いかなる paper_table_ref / expected_uplift も捏造しない(原論文は逐条の mapping を示しておらず、書けばエンジニアリング憲法 #1「データを模擬しない」に違反する)。
⚠️ 重要:V3.1 の
autogeo_rulesテーブルの初版 seed は捏造された paper_table_ref('Table 3'など)+ 捏造された expected_uplift(1.32-1.92)を含み、エンジニアリング憲法 #1 の trigger に阻止されて、実際の arxiv ソースコードから再 port した。後に DB レベルの placeholder guard trigger を追加し永久に再発を防いだ。14.3.5 を参照。
┌──────────────────┐
│ adaptiveRouter │ ← 依 brand tier / page_type / use case 路由
└─────────┬────────┘
│
┌──────────────────┼──────────────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ AutoGEO │ │ E-GEO │ │ Hybrid │ ← parallel call,挑 improvement 高的
│ engine │ │ engine │ │ engine │
└──────────┘ └──────────┘ └──────────┘
│ │ │
└──────────────────┼──────────────────┘
▼
┌─────────────┐
│ ME custom │ ← personal_ip 專屬,從 personal_profiles 守門
│ engine │
└─────────────┘
4 つの engine はすべて標準化された EngineResult schema を吐く:
interface EngineResult {
engine: 'autogeo' | 'egeo' | 'hybrid' | 'me_custom';
rule_or_template_id: string; // arxiv source 或 internal id
source: string; // 'arXiv:2510.11438' 等
original_content: string;
rewritten_content: string;
diff: { additions, deletions, similarity };
scores: { before, after, delta };
llm_call: { provider, model, tokens, cost };
accepted: boolean; // optimizer 自決(score 提升 ≥ 15 + similarity ≥ 0.90)
}
executeRoute が統一して audit + persist + cache する。Hybrid engine の「並行」実装:
const [autogeoResult, egeoResult] = await Promise.allSettled([
Promise.race([autogeoEngine.optimize(input), timeout(30_000)]),
Promise.race([egeoEngine.optimize(input), timeout(30_000)]),
]);
const candidates = [autogeoResult, egeoResult]
.filter(r => r.status === 'fulfilled' && r.value.accepted)
.map(r => r.value);
if (candidates.length === 0) {
return { engine: 'hybrid', accepted: false, reason: 'all_engines_failed' };
}
return candidates.reduce((best, c) =>
c.scores.delta > best.scores.delta ? c : best
);
Promise.allSettled は単一 engine の失敗が他方に影響しないことを保証する;30 秒 timeout は LLM のハングを防ぐ(プラットフォーム憲法「LLM call は必ず ≤60s timeout を持つ」に対応)。
adaptiveRouter.decide({ brandId, contentType, ... }) は以下の優先順位で { path, template, target_engine } を返す:
authoritative(コスト最低);Pro でようやく sync API を使える;Enterprise+ でようやく Hybrid と AutoGEO を使えるf12_page_type_to_template SSOT(22+1 page_type → 5 template category)authoritative;FAQ は FAQ;product は clickableme_custom(この 1 条だけが baiyuan internal template)完全な decision tree:
adaptiveRouter.decide(input)
├── if brand.tier === 'starter':
│ return { path: 'egeo', template: 'authoritative' }
├── if input.contentType in ['whitepaper', 'case_study', 'fact_check']:
│ return { path: 'egeo', template: 'authoritative' } ← 強制 authoritative
├── if input.contentType === 'faq':
│ return { path: 'egeo', template: 'FAQ' }
├── if brand.brand_type === 'personal_ip':
│ return { path: 'me_custom' } ← ME 守門
├── if brand.tier in ['enterprise', 'group']:
│ return { path: 'hybrid' } ← 雙引擎並行
└── default (Pro tier general content):
template = mapPageTypeToTemplate(input.page_type);
return { path: 'egeo', template };
なぜ plan gate が router + executeRoute の 2 層なのか:router 側で決定時にブロックするが、executeRoute 側は defensive な第 2 層である——router のバグや SSOT config の誤設定があっても、executeRoute が再度 brand.tier と path を照合し、合致しなければ直接 reject する。一度踏んだ:あるとき router config を変更した際 starter の path を変え忘れ、starter brand が 3 回 hybrid call を回してしまった(3× LLM cost)が、executeRoute の第 2 層が止められなかった。defensive plan_gate を追加してからは同種の事象は起こりえなくなった。
V3.1 の autogeo_rules テーブルの初版 seed(migration 189)は重大事象を起こした——捏造された paper_table_ref。
当時の spec には「各 rule は paper Table 3 の第 N 行に対応」と書かれており、エンジニアは直感で 'Table 3' / 'Table 4' を入れたが、実際に arXiv:2510.11438 論文をめくると「rule X が Table Y の第 Z 行に対応」という mapping は存在しなかった。同時に expected_uplift 欄にも 1.32-1.92 の範囲を入れたが、論文実験は全体的な実験 uplift しか示しておらず、逐条ではなかった。
エンジニアリング憲法 #1「データを模擬しない」の trigger が阻止した:
CREATE OR REPLACE FUNCTION autogeo_rules_placeholder_guard()
RETURNS trigger AS $$
BEGIN
IF NEW.paper_table_ref IS NOT NULL
AND NEW.source NOT LIKE 'arXiv_%'
AND NEW.source NOT LIKE 'arXiv:%' THEN
RAISE EXCEPTION 'paper_table_ref must be NULL or source must be arXiv (憲法 #1)';
END IF;
-- 6 patterns 偵測 placeholder
IF NEW.rule_description ~ '(待填入|To be filled|Actual Data Pending|TODO:|FIXME:|XXX)' THEN
RAISE EXCEPTION 'placeholder pattern detected in rule_description (憲法 #1)';
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trg_autogeo_rules_placeholder_guard
BEFORE INSERT OR UPDATE ON autogeo_rules
FOR EACH ROW EXECUTE FUNCTION autogeo_rules_placeholder_guard();
migration 191 は TRUNCATE ... RESTART IDENTITY CASCADE で捏造 seed を消し、migration 192 で trigger を追加し、実際の arxiv ソースコードから再 port した。autogeo_rules テーブルの 25 条はすべて source = 'arXiv:2510.11438' で、以下の実データを含む:
target_engine ∈ {gemini, gpt, claude}target_domain ∈ {researchy_geo, geo_bench, ecommerce}paper_table_ref はすべて NULL(arxiv は逐条の mapping を示さない、憲法 #1 は捏造しない)expected_uplift はすべて NULL(全体的な実験 uplift であり逐条ではない — V2 の実際の ML でようやく埋める)egeo_templates の 15 条はすべて source = 'arXiv:2511.20867' で、template 名は論文の表 1 と完全に一致(authoritative / technical / unique / fluent / clickable / diverse / quality / competitive / trick / format / FAQ / advertisement / language / minimalist / storytelling)。
百原内部の 1 条 me_personal_ip は明示的に source = 'baiyuan_internal_v3_1' とし、arxiv の命名に混ぜない。
この trigger は「後知恵」である——一度捏造されて初めて取り付けたものだ。しかし取り付けた後は、将来のいかなる admin 編集 / 新 generator / cron 障害も入り込めない。1 万テナント scale ではこれが必要なハードウェア層の保障である。
V3.1 は 9 個の batch で規模化基盤を敷いた(v3.21 → v3.27 の 8 日間)。各 batch は 1 つのボトルネックを解決し、9 個が合わさってシステムを「30 brand を回せる」から「設計上 10000 brand を回せる」へ進化させた。
TenantQuotaService(monthly_optimizations / max_content_size_kb)
starter: 100 ops, 50 KB
pro: 1000, 200
enterprise: 10000, 1000
group: ∞, 5000
BillingTracker(每次 LLM call 記 cost)
9 model × per-token pricing(claude_haiku $0.0003 / opus $0.015 / 4o-mini $0.0002 ...)
in/out 倍率 4×
f12_quota_usage テーブルの monthly counter UPSERT(tenant_id+brand_id+action+period が一意)、f12_billing_records は年間 12M 行の単一テーブルで十分(1 万 × 100 opt × 12 月)。quota_enforcement_enabled flag はデフォルト false(観察期は usage を記録するだけでブロックしない)、billing_recording_enabled はデフォルト true(常に記録)。
「観察期」の設計:リリース直後は顧客に「今月 X / 上限 Y を使用」と見せるがブロックしない。分布が妥当(starter 顧客が月 5000 回使う outlier が出ない)であることを ~2 週間観察してから enforcement に切り替える。実測の観察期データ:
enforcement に切り替えた後、ブロックされた顧客は 0 個で、クォータ設計が妥当であることを示す。
f12_plan_features SSOT は 4 tier × 6 flag(scoring_configs テーブル内の JSONB):
egeo_template_count: starter=0 pro=5 enterprise=15 group=15
autogeo_rules: 'none' 'none' 'full' 'full'
engine_specialization: false false true true
hybrid_engine: false false true true
sync_api: false true true true
非 Enterprise が whitepaper / case_study / fact-* を走らせる場合は強制で E-GEO authoritative に切り替わる(AutoGEO は使わない);Enterprise+ のみ Hybrid + 三エンジン特化を使える。adaptiveRouter 内の plan gate + executeRoute の defensive plan_gate の 2 層で阻止する。
設計動機:starter / pro 顧客は月額が低く($99 / $499)、最も高価なエンジン(Hybrid は一度に 2 つの LLM を呼ぶ)を使わせられない、さもなくば粗利がマイナスになる。enterprise+ は月額 $2000+ で Hybrid のコストを負担できる。
egeo_template_count の設計:starter 0(デフォルトの authoritative のみ)、pro 5(authoritative / FAQ / clickable / fluent / diverse)、enterprise+ は全 15。顧客は「より多くの template を有効にするには Enterprise へアップグレード」という upsell を見る。
L1 Redis (5 min TTL) ← 同 content + decision 重複 query
L2 PG f12_result_cache (7d) ← 跨 process / 跨 instance
L3 S3 hook (TBD Phase 3) ← cross-region replication 需要時
cache key = sha256(content + decision.path + template + target_engine)、tenant_id を含まずクロステナントで共有(spec が明文で許可、内容が同じ + 規則が同じ → 結果は同じはずで、共有すれば LLM cost を N 倍削減できる)。
accepted=true の結果のみ cache する(cache poisoning を避ける)。f12_cache_metrics は hourly に L1/L2/miss を集計 → admin が 70% hit rate 目標を監視。
クロステナント共有のプライバシー境界:一見 sketchy に見える(A 顧客のコンテンツ cache を B 顧客が取れるのか?)が、実際に分析すると:
実測で 1 ヶ月の cache hit rate は 38%(L1+L2)、70% 目標を下回る。原因:content が brand-specific 情報(brand name、entity など)を帯びるため、ロジックが同じでも内容がすでに unique 化している。Phase 3 でやりたいのは「cache key の正規化」——brand-specific token を <<BRAND_NAME>> placeholder に置換してから hash すれば、理論上は類似 brand が cache を共有できる。これは V2 の作業である。
per-model のプラットフォーム層 RPM 制限(in-memory sliding window 60s):
claude_haiku: 10000 / min
sonnet: 2000
opus: 500
gpt-4o: 5000
gpt-4o-mini: 10000
gemini-flash: 20000
qwen_direct: 30000
deepseek_v4: 8000
5 個の F12 LLM call site はすべて gatewayAiCall を通る:autogeo / egeo / meCustom / V1 optimizer / hybrid は前 2 者を経由して連なる。opts.billing が提供されれば自動的に record する(後方互換)。
なぜプラットフォーム層の RPM 制限が必要か:Anthropic / OpenAI の API key は per-organization で、1 万テナントが 1 本の key を共有し、単一顧客の burst が全プラットフォームの quota を使い切る。Gateway は LLM call の前に現在の RPM をチェックし、超えたらキューまたは reject する。
実測シナリオ:あるとき admin UI のバグで 1 つの brand が 100 回 hybrid optimize を繰り返しトリガーし、各回 2 つの LLM call、200 個の call が同時に Claude Sonnet を打った。Gateway がなければ直接 Anthropic の 30 RPM 制限を超え、全プラットフォームの全 brand がブロックされる;Gateway があれば我々の側でブロックされ、その brand だけが影響を受け、他は正常に継続した。
制限:in-memory bucket は per-process で、単一 backend container なら OK;multi instance では Redis sliding-window が必要(hook は残してあるが未実装)。1 万テナント scale では backend が少なくとも 5〜10 個の instance を持つ見込みで、Phase 3 では必ず Redis-based に切り替える必要がある。
| 機構 | 作用 |
|---|---|
Tier-based Job Priority(group=1 最高 → starter=4) |
BullMQ priority、大口顧客を優先処理 |
| Sync/Async Dual API | /optimize は非同期で jobId を返す / /optimize/sync は Pro+ 限定、内容 ≤5KB |
| Concurrent Jobs Enforcement | starter の無限積み上げによる worker 過負荷を防ぐ(starter=1、group=100) |
| Per-Tenant API RPM | Redis sliding-window per brand、fail-open(Redis 失効時はブロックしない) |
| Retention TTL SSOT | 6 テーブルを daily 05:00 cron で期限切れ row を掃除、scoring_configs.f12_retention_config を admin が調整可 |
| Budget Alerts | tier threshold(starter $1 / group $5000 月)warn@80%、critical@block_pct |
| Scale-up Trigger | daily 04:30 に 8 次元 metrics(tenant_count / monthly_ops / p99 / cache_hit / cost)を snapshot、spec に合わせて phase 1→2→3 を上げる |
| Tenant Isolation Strategy | spec/actual の 2 欄位を書き、enterprise/group 顧客の isolation gap を admin に見せる |
| L3 S3 Cache hook(Phase 3 予約) | F12_S3_CACHE_BUCKET env 設定時に動的に @aws-sdk/client-s3 を import |
完全な SSOT は scoring_configs.f12_* シリーズ 14 個の key にあり、admin UI /dashboard/admin/f12-dashboard が 4 個の stat card + 4 個の detail section を統合する。
相互依存関係:9 点セットは独立していない——Concurrent Jobs Enforcement が worker 過負荷を防ぐ → Job Priority が高 tier 顧客が低 tier に押しのけられないようにする → Per-Tenant RPM が burst を防ぐ → LLM Gateway が全プラットフォームの quota 爆発を防ぐ → Cache が LLM cost を減らす → Quota / Billing が金を管理する → Budget Alerts が金の暴走を防ぐ → Scale-up Trigger がいつ phase を上げるべきかを示す。いずれか 1 つが欠けても 1 万テナント scale で事故になる。
V3.1 は spec Wave 0(microservice contract OpenAPI)+ Wave 1(in-process 実装 + SDK + admin)を完成させた:
backend/src/openapi/f12-v3-1.json は 7 paths × 14 schemas、/api/v1/f12/openapi.json を公開(router auth の前にマウント)し、AI agents / サードパーティが machine-discover できるようにするservices/f12/f12Client.js は 5 つの主入口(diagnose / optimize / optimizeSync / getJob / health)、F12_SERVICE_URL env 設定時は HTTP を通り、未設定なら in-process、caller signature は不変で将来のマイクロサービス分割に hook を残すデプロイ順序(時系列):
各 batch の完全な流れ:マイグレーション SQL → ビジネスロジック → 規格 test → admin UI → CLAUDE.md ドキュメント → PROD へ push。
リリース直後は quota_enforcement_enabled=false(usage を記録するだけ)、顧客は「今月の使用量 / 上限」を見られるがブロックされない。分布が妥当であることを ~2 週間観察してから admin がワンクリックで true に切り替え、以後は超過が直接 HTTP 429 + reject reason 'quota_exceeded' になる。
切り替え前の準備:
false に戻し 5 秒で反映(SSOT は DB にあり deploy 不要)BillingTracker.recordCall(model, tokens, runId) は LLM call のたびに記録する。月末に admin が /admin/f12-billing-summary?period=YYYY-MM を回して top 50 cost brand を見る。
ダブルライト戦略:Redis(f12:billing:tenant:{id}:{period} INCRBYFLOAT + 35 日 TTL)+ DB(f12_billing_records)。当月は getBrandMonthlyCost が Redis O(1) GET を優先し、過去月または Redis miss のときだけ DB SUM する。1 万テナントが月度 cost を query しても DB を打たず、sub-ms で応答する。
35 日 TTL は意図的な設計である:当月 + 前月をカバーし、顧客が 7 日に前月の請求を見たくても瞬時に照会できる。35 日後に自動失効し、常に DB を真相源とする。
monthly batch が「コスト vs 月額」の対比を回し、粗利がマイナスの brand を自動 alert する(価格設定ミス / 顧客の誤用 / バグの可能性)、admin がレビューする。
Hybrid engine は AutoGEO + E-GEO を並行実行し、improvement が高いほうを選ぶ。しかし実際の商業シナリオでは、improvement は単一の指標ではない:
V3.1 は「engine improvement score が高い」を選択基準にしているが、実際には不十分である。次バージョンでは「accepted by client」シグナルを加えたいが、現状 admin 側の編集インターフェースはまだオンライン化していない(Phase 3)。
「字面通り arxiv を port する」はエンジニアリング憲法 #1 の副作用である——論文が逐条の table mapping を示していないので、システムは捏造を禁止する。しかし admin UI が「なぜこの rule を選んだか」を表示するとき、データがなければ空白になる。
V2 でやりたいのは自家 measurement である:各 rule に対して実際の ML eval を回し(同 prompt × 3 LLM × 1000 brand、citation_rate uplift を measure)、実数で埋め戻す。時間 + Anthropic / OpenAI quota が必要:
コストは高くないが、1000 brand の実際の baseline citation rate データが必要である。現状は 30 brand だけで、brand 数が足りてから回せる——Phase 3 で 5000+ brand を見込む頃に行う。
Phase 3 でようやく必要(5000+ tenant 規模と推定)。現状 L1+L2 の hit rate は約 35〜45% で、70% 目標にはまだ距離がある。L3 で更に 15〜20% 上がると見込むが、S3 cost vs LLM 再計算 cost の tradeoff 分析が必要で、まだ済んでいない。
概算:L3 S3 PUT $0.005/1000 obj、GET $0.0004/1000 obj、storage $0.023/GB/月。1 万 brand × 100 ops × 5 KB / op = 5 GB、storage $0.115/月は無視できる。しかし GET ごとに 0.4 ms latency で、L1 Redis の 0.05 ms に比べ L3 は 8 倍遅い。Phase 3 では必ず L3 を cross-region(US + JP + EU)に置き、latency を平準化する必要がある。
personal_profiles テーブルの metadata が全て空 → meta_incomplete で reject。実際に観察すると ME プラットフォームの 17 個の会員 brand のうち 5 個が metadata 不完全(全名 / known_for 未記入)で、この 5 個は ME custom engine を通れず E-GEO authoritative に fallback するが、結果は企業寄りの語調で個人 IP に適さない。カスタマーサポートが顧客に metadata の補完を促す必要があるが、これは「自動化」の約束に反する。
次バージョンで検討:LLM が brand_documents から personal_profiles の欠落欄を自動推論するが、hallucination を防ぐ high-confidence guard が必要。例えば「Tina Chou の jobTitle は?」を LLM が彼女の著作 / メディア報道から推論し、推論結果の confidence > 0.95 のときだけ埋める。
V3.1 の全基盤は「1 万テナントに耐えうる」設計に合わせているが、実際の負荷テストは 17 個の ME brand + 21 個の GEO brand までである。Scale-up Trigger の phase_1_to_2 / phase_2_to_3 ロジックは spec の line 1183-1193 に字面通り合わせているが、一度も実際にアップグレードしたことがない。理論上、phase 跨ぎのアップグレードには:tenant schema 隔離の切り替え(Phase 1 row_level → Phase 2 schema)、Redis cluster sharding、DB の cross-region replication が必要で、これらは spec hook に用意してある(schemaProvisioner / dbConnectionRouter / redisClusterAdapter / shardRouter)が、実戦を経ていない。
今後 1 年の作業項目:
これらはすべて production で直接はできず、staging 環境 + 追加のハードウェア予算が必要である。
14.4.3 で cache key が tenant_id を含まず、クロステナント共有で LLM cost を減らすと述べた。一見プライバシー懸念を招きうるが、設計時にすでに分析している:
sha256(content + decision) で、content は KB 規模、2 顧客が全く同じ content を書く確率は 0(盗用でない限り)V2 では audit log を加える計画である:cache hit のたびに「from brand A → to brand B」を記録し、顧客はクロステナント共有を opt-out できる(引き換えに自身の LLM cost が高くなる)。
cache key = sha256(content + decision.path + template + target_engine) には暗黙の前提がある:LLM call は deterministic(同 input → 同 output)。実際には:
temperature=0 は大部分 deterministic だが、OpenAI / Anthropic のどちらも 100% は保証しない(infrastructure 層に微小な noise がある)cache hit 時は直接古い結果を返し、顧客は差に気づかない、95% deterministic はすでに非常に高い一貫性だからである。しかし将来 LLM API が「明確に non-deterministic」になれば(例えば OpenAI Sora / Gemini Veo の動画生成)、cache は versioning と invalidation を加える必要がある。
実装上は予約してある:f12_result_cache に cache_version 欄があり、Phase 3 で LLM 世代交代時にワンクリックで version を bump して全プラットフォームの cache を失効できる。
V1→V3.1 の進化(7 ヶ月、5 回の PROD 障害)から、5 条の教訓を整理した:
当初は自分で GEO 規則を発明したかった(internal な観察に基づく)が、すぐに分かった:
「字面通り arxiv ソースコードを port」に切り替えてから:
しかし代償は paper_table_ref / expected_uplift が NULL、UI が空白を表示すること。空白でも捏造しない——これが憲法 #1 の代償である。
V1 の初期はすべての閾値をコードにハードコードしていた(const LOW_SCORE_THRESHOLD = 70)、変更のたびに deploy + 再起動。顧客が「なぜこのページは 65 点なのに最適化されないのか」と問い、エンジニアがコードを grep すると hard-coded 70 だと分かり、65 に変えて deploy。全プロセスで 30 分。
SSOT(scoring_configs.f12_score_thresholds.low_score)に切り替えてから:
代償は indirection 層が 1 つ増え、新人はコードを見るのに 1 回多く grep する必要があること。しかし deploy 速度に比べれば、それだけの価値がある。
placeholder guard は当初 application レベルでやろうとした(INSERT のたびに application がチェック):
INSERT INTO axp_pages を書くと service layer を迂回するDB trigger に切り替えてから:
代償:trigger の debug は困難(error message が不明瞭)だが、1 万テナント scale ではこの代償に見合う。
quota_enforcement_enabled / billing_recording_enabled などの flag はすべてデフォルトで「観察期」(記録するだけでブロックしない)にし、分布が妥当であることを確認してから「強制期」に切り替える。観察期 ≥ 2 週間、その間:
V1 の初期はこの機構がなく、直接 enforcement をリリースした。結果:
観察期を加えてからは同種の事象は二度と起きていない。
axpPageWriter F12 hook は 100% カバレッジでなければならないが、100% は保証しにくい(13 個の入口、新 generator がいつでも追加される):
三層のどれも欠かせない——hook だけではカバレッジ漏れ → 永久に未分析;backfill だけでは間に合わない(最悪 7 日後の反応);manual だけでは規模化しない。
V1 の初期は hook だけを作っていた。あるとき fact-check generator が hook を漏らし、4 個の brand の fact-check ページのスコアが異常に低いまま 3 ヶ月発見されなかった。backfill を加えてからは同種の事象は最悪でも 7 日で必ず反応する。
min_similarity ≥ 0.90 で脱線防止)、5 個の cron を連結して全プラットフォームをカバーF12_TLSO_spec_v3_1.md(repo root)| 日付 | バージョン | 説明 |
|---|---|---|
| 2026-05-03 | v1.1 | 新章 — V1 + V3.1 の完全なアーキテクチャ進化を扱う |
| 2026-05-03 | v1.1.1 | 章を ~7200 字に拡充 — 14.1.2 初期の手動調整の失敗モード、14.2.4 双方向可逆の設計、14.2.5 hook 全カバレッジの難しさ、14.2.6 cron 時系列、14.3.5 placeholder guard の物語、14.4 相互依存関係、14.5 デプロイ順序、14.6.6/7 cache のプライバシーと前提、14.7 エンジニアリング教訓 5 条を追加 |
ナビゲーション:← 第 13 章: マルチモーダル GEO · 📖 目次 · 第 15 章: rag-backend-v2 の堅牢化 →