1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

AIを活用した就職支援プラットフォームをフルスタックで作った話

1
Last updated at Posted at 2026-03-22

AIを活用した就職支援プラットフォームをフルスタックで作った話

はじめに

「自分に合った企業がわからない」「面接が怖い」「ポートフォリオをどう見せればいいかわからない」

就活生が抱えるこんな悩みをAIで解決しようと、チームで就職支援プラットフォーム SOC AI Agent を開発しました。

この記事では、システム全体の構成と、それぞれの機能をどう実装したかを紹介します。


作ったもの:SOC AI Agent

就職活動に必要な4つをまるごとカバーするWebアプリです。

機能 内容
AIチャット分析 質問に答えると職種・興味をAIが分析して企業マッチング
音声面接練習 リアルタイム音声対話 + 3Dアバターで面接練習
職務経歴書レビュー RAGでAIがフィードバックを生成
GitHubポートフォリオ READMEをAIが要約してスキルを可視化

システム全体の構成

┌──────────────────────────────────────────────┐
│  Frontend: Next.js 16 + React 19 + MUI       │
│  (TypeScript, Three.js, React Flow)           │
├──────────────────────────────────────────────┤
│  Backend: Go + GORM + net/http                │
│  (Controllers / Services / Repositories)      │
├───────────────┬──────────────────────────────┤
│ RAG: Python   │  Scraper: Go                 │
│ FastAPI       │  Mynavi / Rikunabi /         │
│ + CrewAI      │  gBizINFO etc.               │
├───────────────┴──────────────────────────────┤
│  DB: MySQL 8.0                                │
├──────────────────────────────────────────────┤
│  AI: OpenAI (gpt-4o-mini, Realtime API)       │
│  Cloud: AWS ECS / RDS / S3 / Secrets Manager  │
└──────────────────────────────────────────────┘

バックエンド(Go)、RAGサービス(Python/FastAPI)、スクレイパー(Go)がそれぞれ独立して動き、Docker Composeで統合します。本番環境はAWS ECS Fargateで動かしています。


機能①:AIチャット分析 → 企業マッチング

ユーザーがチャット形式の質問に答えると、AIが職種や興味を分析して企業をマッチングする機能です。

データフロー

ユーザーが質問に回答
    ↓
ChatService が回答を記録
    ↓
OpenAI で次の質問を自動生成
    ↓
AnalysisScoringService が職種スコアを更新
    ↓
分析完了 → MatchingService が企業マッチング
    ↓
EmailService でレポートをメール配信

バックエンド(Go)の実装ポイント

バックエンドは Controller → Service → Repository の3層構成です。

// controllers/chat_controller.go
func (c *ChatController) SendMessage(w http.ResponseWriter, r *http.Request) {
    var req ChatMessageRequest
    json.NewDecoder(r.Body).Decode(&req)

    response, err := c.chatService.ProcessMessage(r.Context(), req.UserID, req.Message)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(response)
}
// services/chat_service.go
func (s *ChatService) ProcessMessage(ctx context.Context, userID uint, message string) (*ChatResponse, error) {
    // 回答を保存
    if err := s.chatRepo.SaveMessage(userID, message); err != nil {
        return nil, err
    }

    // OpenAI で次の質問を生成
    nextQuestion, err := s.generateNextQuestion(ctx, userID)
    if err != nil {
        return nil, err
    }

    // スコア更新
    s.scoringService.UpdateScores(userID, message)

    return &ChatResponse{NextQuestion: nextQuestion}, nil
}

機能②:音声面接練習(Realtime API + 3Dアバター)

OpenAI の Realtime API を使ったリアルタイム音声対話と、Three.js + wawa-lipsync による3Dアバター表示を組み合わせた機能です。

仕組み

ユーザーが話す
    ↓
WebRTC で音声をリアルタイム送信
    ↓
OpenAI Realtime API が処理
    ↓
AI の音声応答 + テキスト
    ↓
wawa-lipsync でアバターの口を同期
    ↓
Three.js で3Dアバターとして表示

フロントエンド側の実装ポイント

Realtime API のトークンはバックエンドで発行し、フロントエンドに渡します。

// interview/page.tsx
const startInterview = async () => {
    // バックエンドからRealtimeトークンを取得
    const res = await fetch(`${BACKEND_URL}/api/realtime/token?user_id=${userId}`)
    const { token } = await res.json()

    // WebRTC接続
    const pc = new RTCPeerConnection()
    const stream = await navigator.mediaDevices.getUserMedia({ audio: true })
    pc.addTrack(stream.getTracks()[0])

    // データチャネルでテキスト受信
    const dc = pc.createDataChannel('oai-events')
    dc.onmessage = (e) => {
        const event = JSON.parse(e.data)
        if (event.type === 'response.audio.delta') {
            // アバターの口パクを更新
            lipsync.update(event.delta)
        }
    }
}

ハマりポイント: 面接動画の録画データはS3にアップロードしますが、ファイルサイズが大きくなりがちです。バックエンドで 32MBの上限チェックを設けています。

面接動画のS3管理

// services/interview_service.go
func (s *InterviewService) UploadVideo(userID uint, sessionID uint, file multipart.File) error {
    // ファイルサイズチェック(32MB上限)
    buf := make([]byte, 32<<20+1)
    n, _ := file.Read(buf)
    if n > 32<<20 {
        return errors.New("file size exceeds 32MB limit")
    }

    key := fmt.Sprintf("%s/%d/%d/video.webm", s.s3Prefix, userID, sessionID)
    _, err := s.s3Client.PutObject(ctx, &s3.PutObjectInput{
        Bucket: &s.bucket,
        Key:    &key,
        Body:   bytes.NewReader(buf[:n]),
    })
    return err
}

管理者が動画を視聴するときは、15分間有効な Presigned URL を発行してセキュアにアクセスします。


機能③:職務経歴書レビュー(RAG)

RAG(Retrieval-Augmented Generation)を使って、企業情報を検索しながら職務経歴書のフィードバックを生成します。

RAGの構成(Python / FastAPI)

# rag/main.py
@app.post("/resume/review")
async def review_resume(request: ReviewRequest):
    # Chromadb でキャッシュ確認
    cached = chroma_collection.query(
        query_texts=[request.company_name],
        n_results=1
    )

    if cached['distances'][0][0] < 0.2:
        company_info = cached['documents'][0][0]
    else:
        # DuckDuckGo でリアルタイム検索
        results = ddg_search.run(f"{request.company_name} 企業情報 事業内容")
        company_info = results

        # 次回のためにキャッシュ保存
        chroma_collection.add(
            documents=[company_info],
            ids=[request.company_name]
        )

    # CrewAI エージェントでレビュー生成
    crew = Crew(
        agents=[reviewer_agent],
        tasks=[review_task],
    )
    result = crew.kickoff(inputs={
        "resume": request.resume_text,
        "company_info": company_info,
        "job_type": request.job_type,
    })
    return {"review": result}

ポイント:Chromadbキャッシュで高速化

同じ企業への問い合わせは2回目以降キャッシュから返すため、レスポンスが大幅に高速化されます。ベクトルの類似度が0.2以下なら「同じ企業の情報あり」と判断してキャッシュを使います。


機能④:GitHubポートフォリオ AI要約

GitHubのREADMEをAIが読んで、3つの観点で技術サマリーを自動生成します(直近で実装した機能です)。

項目 内容
技術選定の理由 なぜその技術スタックを選んだか
技術的チャレンジ 難しかった部分や工夫した点
実績・学び 得られた成果や学習内容

詳しい実装はこちらの記事で解説しています。
GitHubのREADMEをAIで要約してポートフォリオに表示する機能を作った話


機能⑤:企業関係図(React Flow + スクレイピング)

複数の就活サイトからデータを取得して、企業間の関係図をインタラクティブに表示します。

スクレイピングパイプライン(Go)

// tools/company-graph/pipeline.go
func (p *Pipeline) Run() ([]Company, error) {
    var all []Company

    // 複数ソースから並行取得
    sources := []Scraper{
        &MynaviScraper{},
        &RikunabiScraper{},
        &CareerTasuScraper{},
        &GBizInfoScraper{}, // 政府公開API
    }

    for _, s := range sources {
        companies, err := s.Fetch()
        if err != nil {
            continue // 1つ失敗しても続行
        }
        all = append(all, companies...)
    }

    // 重複排除・スコアリング
    return p.deduplicate(all), nil
}

フロントエンドでReact Flowを使った関係図表示

// app/Correlation-diagram/page.tsx
export default function CorrelationDiagram() {
    const [nodes, setNodes] = useState<Node[]>([])
    const [edges, setEdges] = useState<Edge[]>([])

    useEffect(() => {
        fetch(`${BACKEND_URL}/api/company-relation`)
            .then(res => res.json())
            .then(data => {
                setNodes(data.companies.map(c => ({
                    id: String(c.id),
                    data: { label: c.name },
                    position: { x: c.x, y: c.y },
                })))
                setEdges(data.relations.map(r => ({
                    id: `${r.from}-${r.to}`,
                    source: String(r.from),
                    target: String(r.to),
                })))
            })
    }, [])

    return <ReactFlow nodes={nodes} edges={edges} fitView />
}

インフラ:AWS ECS Fargate で本番運用

本番環境はすべてAWSで動かしています。

ユーザー
  ↓
ALB (Application Load Balancer)
  ↓
ECS Fargate タスク
  ├── Backend コンテナ (Go / ポート8080)
  │     DB接続: RDS MySQL
  │     動画: S3
  │     シークレット: Secrets Manager
  └── Frontend コンテナ (Next.js / ポート3000)

ECS タスク定義のポイント

機密情報(DBパスワード、APIキー)はSecrets Managerから取得します。

{
  "secrets": [
    {
      "name": "DB_PASSWORD",
      "valueFrom": "arn:aws:secretsmanager:ap-northeast-1:XXXX:secret:soc/db:password::"
    },
    {
      "name": "OPENAI_API_KEY",
      "valueFrom": "arn:aws:secretsmanager:ap-northeast-1:XXXX:secret:soc/openai:api_key::"
    }
  ]
}

ハマりポイント: Secrets Managerのシークレットは secret_name:json_key:: の形式でJSONのフィールドを指定します。末尾の :: を忘れるとエラーになります。


認証:Google / GitHub OAuth2

どちらもOAuth2でシングルサインオンできます。

// config/oauth.go
func LoadOAuthConfig() *OAuthConfig {
    return &OAuthConfig{
        Google: &oauth2.Config{
            ClientID:     os.Getenv("GOOGLE_CLIENT_ID"),
            ClientSecret: os.Getenv("GOOGLE_CLIENT_SECRET"),
            RedirectURL:  baseURL + "/api/auth/google/callback",
            Scopes: []string{
                "https://www.googleapis.com/auth/userinfo.email",
                "https://www.googleapis.com/auth/userinfo.profile",
            },
            Endpoint: google.Endpoint,
        },
        GitHub: &oauth2.Config{
            ClientID:     os.Getenv("GITHUB_CLIENT_ID"),
            ClientSecret: os.Getenv("GITHUB_CLIENT_SECRET"),
            RedirectURL:  baseURL + "/api/auth/github/callback",
            Scopes: []string{
                "user:email",
                "read:user",
                "read:org", // 組織リポジトリ取得に必要
            },
            Endpoint: github.Endpoint,
        },
    }
}

Docker Composeでローカル開発

# compose.yml(抜粋)
services:
  app:
    build: ./Backend
    ports: ["8080:8080"]
    depends_on: [db]

  frontend:
    build: ./frontend
    ports: ["3000:3000"]

  db:
    image: mysql:8.0
    ports: ["3306:3306"]

  rag-review:
    build: ./rag
    ports: ["9000:9000"]
    profiles: ["rag"] # オプションで起動

  company-graph:
    build: ./tools/company-graph
    ports: ["9100:9100"]
    profiles: ["company-graph"] # オプションで起動

必要なサービスだけ起動できる profiles を活用することで、開発時のリソース消費を抑えています。


使った技術スタックまとめ

カテゴリ 技術
フロントエンド Next.js 16, React 19, TypeScript, MUI, Three.js, React Flow
バックエンド Go, GORM, net/http
RAG Python, FastAPI, CrewAI, Chromadb, DuckDuckGo Search
スクレイピング Go, Chromedp
データベース MySQL 8.0
AI OpenAI (gpt-4o-mini, Realtime API)
クラウド AWS ECS, RDS, S3, Secrets Manager, CloudWatch
コンテナ Docker, Docker Compose
IaC Terraform (ECS タスク定義)
認証 OAuth2 (Google, GitHub)

おわりに

このプロジェクトを通して、単にAIを使うだけでなく、どこにAIを組み込むと価値が生まれるかを考える経験ができました。

  • チャット分析 → 企業マッチング(構造化データ生成)
  • 音声面接 → Realtime APIで低遅延を実現
  • RAG → キャッシュで速度と精度を両立
  • ポートフォリオ要約 → 開発者の技術力を言語化

それぞれ異なるAIの使い方をしており、学びも多かったです。同じような機能を作ろうとしている方の参考になれば幸いです。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?