プロジェクト

全般

プロフィール

AIタスク #10469

Redmine Admin さんが14日前に更新

# LLM対策診断ツール実装 詳細仕様 

 ## 背景 
 集客ページとして、URL入力→AI検索(LLM)に引用されやすいかを自動診断するツールをvend01(/root/website-vending-machine/vend01)に追加する。 

 ## スコアリング方針 
 ゲート判定(クロール可否)→本体スコア(重み付け加重平均)の2段階。重みはAIクローラー(GPTBot/PerplexityBot/ClaudeBot/Google-Extended)の実挙動とGEO研究ベースの確度で設定(表示速度・UXは除外、llms.txtは重み最小)。 

 ## ファイル構成(既存vend01の少ファイル主義を尊重、2ファイルに限定) 
 - crawlability.js: safeFetch(url)単一実装(本文fetchとrobots.txt取得の両方が必ずこれを経由)、gate_crawlable/gate_robots_blocked判定 
 - scoring.js: スコア集計(機械的判定+LLM呼び出し) 

 ## SSRF対策(safeFetch必須仕様) 
 - スキームはhttp/httpsのみ許可 
 - redirect: 'manual'を指定、3xx応答を自前でハンドルしLocationを再度safeFetchに通して各ホップごとにIP検証(最大リダイレクト3回) 
 - DNS rebinding対策: dns.lookup()で名前解決したIPを検証後、undiciのカスタムdispatcherでconnect先IPを固定し同一接続内で再解決させない(ただしTLSのSNI/証明書検証は元ホスト名ベースを使うこと) 
 - プライベートレンジ拒否(IPv4): 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 127.0.0.0/8, 169.254.0.0/16, 0.0.0.0/8, 100.64.0.0/10 
 - プライベートレンジ拒否(IPv6): ::1, fe80::/10, fc00::/7, ::ffff:127.0.0.1等 
 - proxy-network内コンテナ名解決も拒否、AbortSignal.timeout(8000)をsafeFetch内に内蔵 

 ## 判定方式の分離 
 機械的判定: gate_crawlable, gate_robots_blocked, score_schema(cheerioでJSON-LD解析), score_freshness(sitemap lastmod), score_authority(外部リンクユニークドメイン数、nofollow除外) 
 LLM判定: 抽出済み本文をLLMに渡しscore_direct_answerとscore_original_infoを1回の呼び出しでJSON一括出力 

 ## LLM接続先(重要) 
 既存vend01のcallClaude(Anthropic API直呼び)は使わず、VPS-root内部APIを使用: 
 - 通常時: http://mcp-gateway-v2:8000/v1/chat/completions (proxy-network内、OpenAI Chat Completions互換)。modelに`claude-3-sonnet`を渡すと内部で`sonnet`エイリアスにマッピングされ常に最新Sonnetに自動追従。認証はX-API-Keyヘッダ(.envのCLAUDE_API_KEYS)。注意: 内部でClaude Code CLIをサブプロセス起動する重い処理でレイテンシ数秒〜。messagesはフラット化されるため単発呼び出し前提の実装にすること。 
 - 同一IPから3回以上のトライがあった場合: Gemini APIの無料枠に切り替える(IP別試行回数カウントが必要)。Gemini無料枠の容量制限に到達した場合はエラーメッセージとして「システムが混雑しています。日を改めてご利用ください」を表示する。 
 - error_reasonはgate:/llm:の接頭辞で区別し、llm:claude / llm:geminiのようにどちらで判定したかも記録する。 

 ## LLM失敗フォールバック 
 タイムアウト/例外/JSONパース失敗はすべてfetch失敗と同じパターン(total_score=null, error_reason記録, エンドポイントは200)に合流させる。 

 ## DBスキーマ(既存単一DBファイルにdb.js内でCREATE TABLE IF NOT EXISTSで追記、別ファイルにしない) 
 ```sql 
 CREATE TABLE IF NOT EXISTS diagnosis_results ( 
   id INTEGER PRIMARY KEY AUTOINCREMENT, 
   url TEXT NOT NULL, 
   gate_crawlable INTEGER, 
   gate_robots_blocked INTEGER, 
   score_direct_answer REAL, 
   score_authority REAL, 
   score_original_info REAL, 
   score_schema REAL, 
   score_freshness REAL, 
   total_score REAL, 
   error_reason TEXT, 
   created_at TEXT DEFAULT (datetime('now')) 
 ); 
 ``` 
 UNIQUE制約なしで同一URLの再診断は毎回新規行として履歴保持。raw_html本文はDB保存しない。 

 ## APIエンドポイント 
 - POST /api/diagnosis body:{url} → {gate_crawlable, gate_robots_blocked, breakdown:{...}, total_score, error_reason} 
 - GET /api/diagnosis/:id → 過去診断結果の再取得 
 - 既存/sessions系と同じプレフィックスなし規約(Nginx側で/vend/01/api/を付与する前提) 

 ## Dockerfile/package.jsonの明示変更 
 - Dockerfile COPY行に crawlability.js, scoring.js を明示追記 
 - package.jsonに cheerio を追加(robots.txtパースは自前実装、依存追加不要) 

 ## 受入基準(検収条件) 
 server.js/crawlability.js/scoring.js/db.js/Dockerfile/package.jsonが仕様通りに実装されていること。safeFetchが本文fetch/robots.txt/llms.txtの全てを必ず経由していること。異常系が全て200+error_reasonで返っていること(500にならない)。既存/sessions系の動作を壊さないこと。total_scoreが上記重み式(direct_answer0.30+authority0.25+original_info0.20+schema0.10+freshness0.10+llms_txt0.05)で算出されていること。diagnosis_resultsにscore_llms_txt列が存在すること。rate_limit_ipテーブルが存在し、24時間ウィンドウでリセットされ、attempt_count>=3でGeminiに切り替わること。diagnosis_resultsにllm_provider列が存在し、成功時に'claude'または'gemini'が記録されること。Gemini容量制限時のerror_reasonが`llm:gemini_capacity`で固定されていること。 


 ## レビュー反映(2回目) 
 1. rate_limit_ipテーブル新規: CREATE TABLE IF NOT EXISTS rate_limit_ip (ip TEXT PRIMARY KEY, attempt_count INTEGER NOT NULL DEFAULT 0, window_start TEXT NOT NULL); 
 2. リセットは日次(24時間)ウィンドウ。window_startから24h経過でattempt_count=1・window_startリセット。未経過ならインクリメント。attempt_count>3でGemini切替。 
 3. diagnosis_resultsにllm_provider TEXT列追加('claude'/'gemini')。 
 4. Gemini容量制限時のerror_reasonは`llm:gemini_capacity`で固定。 
 5. Gemini APIキーは.envのGEMINI_API_KEY。 

 ## レビュー反映(3回目) 
 1. 重み確定: total_score = direct_answer*0.30 + authority*0.25 + original_info*0.20 + schema*0.10 + freshness*0.10 + llms_txt*0.05(各score_*は0〜100、合計重み100)。 
 2. llms_txt実装: crawlability.jsのsafeFetchで/llms.txtを取得、2xxで取得できればscore_llms_txt=100、それ以外(404含む)は0。diagnosis_resultsテーブルにscore_llms_txt REAL列を追加。 
 3. Gemini切替え閾値を統一: attempt_count>=3でGemini切替え(本文の「3回以上」に合わせ、以前の「attempt_count>3」記述を上書き訂正)。 

 ## スコープ確定宣言(これ以上の仕様化は行わない) 
 以下は確定事項(必須実装): total_score重み(direct_answer0.30/authority0.25/original_info0.20/schema0.10/freshness0.10/llms_txt0.05)、diagnosis_resultsのscore_llms_txt列とllm_provider列、rate_limit_ipテーブル(attempt_count>=3でGemini切替え、24時間ウィンドウ)、LLM呼び出し失敗時のtotal_score=null+error_reason+200応答。DBスキーマのCREATE TABLE本体ブロックにこれら全列を統合して実装すること(追記別ブロックではなく本体の1つのCREATE TABLE文にまとめる)。 

 以下はCodexの裁量に委ねる(レビューで大きく外れていなければ差し戻さないこと): score_authority/score_freshness/score_schemaの0〜100正規化の具体的な閾値・分割点、および機械判定系(freshness/schema/authority)の取得失敗時の細部挿作は実装者(Codex)自身の合理的な判断に委ねる(根拠はコードコメントに残す)。異常系は必ず200+error_reasonで返すという大方針のみ遵守すればよい。 

戻る