Skip to content

Backend: AI実装 - ランク判定(概念実証) #36

@MasachikaUra

Description

@MasachikaUra

目的

ユーザーの外部サービス活動実績とポートフォリオを元に、LLMを使用してエンジニアランク(0-9)を判定する機能を実装。
概念実証優先で、まず動くことを重視し、判定精度の向上は後続Issueで対応。

背景

  • Issue #32でLangChain基盤完成
  • product-spec SKILLの「ランク判定ロジック (AI主導)」を実装
  • ユーザーフローの最初のステップ(オンボーディング後の初回分析)

ランク定義(10段階)

0: 種子 (初心者/未経験)
1: 苗木
2: 若木
3: 巨木
4: 母樹
5: 林
6: 森
7: 霊樹
8: 古樹
9: 世界樹 (Top Tier)

判定ロジック(AI主導)

  • 相対評価: 取得した情報を総合的に分析し、「エンジニア全体の上位何%に位置するか」をAIが推定
  • ランクマッピング: パーセンタイル → 10段階ランクに変換

実装内容

1. プロンプトテンプレート(app/core/prompts.py

RANK_ANALYSIS_TEMPLATEを実装済みの基本構造から、実際のプロンプトに拡張:

RANK_ANALYSIS_TEMPLATE = """
あなたはエンジニアのスキルレベルを評価する専門家です。
以下の情報を元に、このエンジニアが全体の上位何%に位置するかを推定してください。

## 評価対象の情報
- GitHub Username: {github_username}
- ポートフォリオ: {portfolio_text}
- Qiita ID: {qiita_id}
- その他活動: {other_info}

## 評価基準
1. 技術の幅(使用言語・フレームワークの多様性)
2. 実装の深さ(プロジェクトの複雑さ、設計力)
3. 継続性(コミット頻度、学習習慣)
4. アウトプット(記事執筆、コミュニティ貢献)

## 出力形式(JSON)
{{
  "percentile": 65.0,
  "rank": 4,
  "rank_name": "巨木",
  "reasoning": "判定理由を200文字程度で説明"
}}

percentileは0.0〜100.0の数値で推定してください。
rankは以下のマッピングで決定:
- 0-10%: rank 0 (種子)
- 10-20%: rank 1 (苗木)
- 20-35%: rank 2 (若木)
- 35-50%: rank 3 (巨木)
- 50-65%: rank 4 (母樹)
- 65-75%: rank 5 (林)
- 75-85%: rank 6 (森)
- 85-92%: rank 7 (霊樹)
- 92-97%: rank 8 (古樹)
- 97-100%: rank 9 (世界樹)
"""

2. サービス層(app/services/rank_service.py 新規作成)

from app.core.llm import get_llm, invoke_llm
from app.core.prompts import RANK_ANALYSIS_TEMPLATE
import json

async def analyze_user_rank(
    github_username: str,
    portfolio_text: str = "",
    qiita_id: str = "",
    other_info: str = ""
) -> dict:
    """
    LLMを使用してユーザーのランクを判定
    
    Returns:
        {
            "percentile": float,
            "rank": int,
            "rank_name": str,
            "reasoning": str
        }
    """
    llm = get_llm()
    
    prompt = RANK_ANALYSIS_TEMPLATE.format(
        github_username=github_username,
        portfolio_text=portfolio_text or "未入力",
        qiita_id=qiita_id or "未入力",
        other_info=other_info or "未入力"
    )
    
    response = await invoke_llm(llm, prompt)
    
    # JSONパース(エラーハンドリング付き)
    try:
        result = json.loads(response)
        return result
    except json.JSONDecodeError:
        # LLMがJSON以外を返した場合のフォールバック
        return {
            "percentile": 50.0,
            "rank": 3,
            "rank_name": "巨木",
            "reasoning": "判定結果の解析に失敗したため、デフォルト値を返却"
        }

3. APIエンドポイント(app/api/endpoints/analyze.py 新規作成)

from fastapi import APIRouter, HTTPException
from app.schemas.analyze import RankAnalysisRequest, RankAnalysisResponse
from app.services.rank_service import analyze_user_rank

router = APIRouter()

@router.post("/rank", response_model=RankAnalysisResponse)
async def analyze_rank(request: RankAnalysisRequest):
    """
    ユーザーのランクを判定
    """
    try:
        result = await analyze_user_rank(
            github_username=request.github_username,
            portfolio_text=request.portfolio_text,
            qiita_id=request.qiita_id,
            other_info=request.other_info
        )
        return RankAnalysisResponse(**result)
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"Rank analysis failed: {str(e)}")

4. スキーマ定義(app/schemas/analyze.py 新規作成)

from pydantic import BaseModel, Field

class RankAnalysisRequest(BaseModel):
    github_username: str = Field(..., min_length=1, max_length=100)
    portfolio_text: str = Field(default="", max_length=5000)
    qiita_id: str = Field(default="", max_length=100)
    other_info: str = Field(default="", max_length=2000)

class RankAnalysisResponse(BaseModel):
    percentile: float = Field(..., ge=0.0, le=100.0)
    rank: int = Field(..., ge=0, le=9)
    rank_name: str
    reasoning: str

5. ルーター登録(app/api/api.py

from app.api.endpoints import analyze

api_router.include_router(analyze.router, prefix="/analyze", tags=["analyze"])

MVP制限(概念実証優先)

  • GitHub API統合なし: まずはユーザー入力の文字列のみで判定(GitHub APIは後続Issue)
  • DB保存なし: まずはAPIレスポンス返却のみ(DB保存は後続Issue)
  • 判定精度は問わない: とにかく動くことを優先、プロンプト調整は後続Issue

実装タスク

  • app/core/prompts.py: RANK_ANALYSIS_TEMPLATE拡張
  • app/services/rank_service.py: analyze_user_rank実装
  • app/api/endpoints/analyze.py: POST /analyze/rank実装
  • app/schemas/analyze.py: Request/Responseスキーマ
  • app/api/api.py: ルーター登録
  • テスト: tests/test_services/test_rank_service.py(モック使用)
  • テスト: tests/test_api/test_analyze.py(エンドポイント動作確認)
  • .env.example: OPENAI_API_KEY設定確認
  • Swagger UI動作確認(実APIキー使用)

セキュリティ考慮

  • Request Bodyサイズ制限(portfolio_text: 5KB、全体: 10KB)
  • Rate Limiting方針をコメント記載(将来実装)
  • LLMレスポンスのサニタイズ(JSONパースエラー時のフォールバック)

テスト要件

  • モックテスト: LLM呼び出しをモック化し、CIでAPIキー不要
  • 手動テスト: 実際のOpenAI APIを使用してSwagger UIで動作確認

完了条件(DoD)

  • POST /analyze/rank実装完了
  • Swagger UIで実際のLLM呼び出し成功確認(スクリーンショット)
  • テスト2件以上、全パス(モック使用)
  • PR説明にEvidence添付(Swagger UIスクリーンショット + レスポンス例)

依存

次のステップ(後続Issue)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions