PythonとOpenAI APIで実践！はじめてのMCP開発入門【第9回】MCP活用プログラミング(1) - ユーザー嗜好コンテキストでパーソナライズする「おすすめ記事生成AI」

# はじめに：AIよ、我が心を読め！嗜好コンテキストで切り拓くパーソナライズの神髄

皆さん、こんにちは！AI開発の興奮冷めやらぬ第9回です。前回（第8回）では、OpenAI APIからの応答JSONを徹底的に解剖し、堅牢なエラーハンドリングを実装することで、私たちのAIアプリケーションをよりプロフェッショナルなレベルへと引き上げました。これで、AIとの安定した対話の土台は万全です。

さあ、ここからは **「モデルコンテキストプロトコル（MCP）」** の真価を発揮させる、より具体的な応用プログラミングの世界に足を踏み入れます！AIが単に一般的な応答を返すだけでなく、**「あなた」という個人の特性や好みを深く理解し、それに最適化された情報を提供してくれる**としたら、どれほど素晴らしい体験になるでしょうか？

今回はその第一弾として、**ユーザーの嗜好情報（好きな技術トピック、専門レベル、好みの文体など）を「コンテキスト」として設計し、OpenAI APIに連携することで、そのユーザーにパーソナライズされた「おすすめ技術記事」とその推薦理由をAIに生成させる** という、実践的なプログラムを構築します。

このプロセスを通じて、
* **ユーザー嗜好コンテキストのJSONスキーマ設計**
* **Pythonでの動的なプロンプト生成テクニック**
* **コンテキスト情報を活用したAIの応答制御**
* **リッチなコンテキスト送信時のトークン効率とコストへの配慮**

といった、より高度な技術要素を体験的に学んでいきましょう。2025年5月26日、ここ東京から発信するこの記事が、皆さんの「AIに心を読む」ようなアプリケーション開発のヒントとなれば幸いです！

![# はじめに：AIよ、我が心を読め!嗜好コンテキストで切り拓くパーソナライズの神髄 - visual selection.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4058228/ca4ed609-4714-49e4-94e4-1d287cdf8cb9.png)


# パーソナライゼーションの威力：なぜ「あなた向け」情報は価値が高いのか？

情報が溢れる現代において、一般的な情報はもはやコモディティ化しています。その中で、個々のユーザーにとって本当に価値があるのは、「自分に関係がある」「自分のレベルに合っている」「自分の好みに合致する」といった、**パーソナライズされた情報**です。

従来のレコメンデーションシステム（例：協調フィルタリング、コンテンツベース）も一定の成果を上げてきましたが、LLMと構造化コンテキストを活用することで、さらに一歩進んだ、**きめ細やかで、理由付けが明確なパーソナライゼーション**が可能になります。
* **ノイズの除去:** 興味のないトピックやレベルに合わない情報をフィルタリング。
* **関連性の最大化:** 関心の高い情報をピンポイントで提示。
* **理解の促進:** ユーザーの知識レベルや好みの説明スタイルに合わせて情報を加工・要約。

これらを実現する鍵が、ユーザーの嗜好を的確に捉えた「コンテキスト」の設計と、それをAIに理解させるプロンプトエンジニアリングです。

# ステップ1：「ユーザー嗜好コンテキスト」の設計（JSONスキーマとPython辞書）

まずは、AIに伝えるためのユーザー嗜好情報を、どのような構造（スキーマ）で表現するかを定義します。ここでは、技術記事の推薦に特化した情報をPythonの辞書として設計し、これがAPIコール時にはJSONとして機能することを想定します。

```python
# user_preference_schema.py (イメージ)

def get_sample_user_preferences():
    """
    ユーザーの嗜好情報を格納したPython辞書を返す関数。
    これがMCPにおける「コンテキスト」の具体的なデータ構造となる。
    """
    preferences = {
        "user_id": "user_ai_dev_2025",
        "technical_interests": [
            {"topic": "大規模言語モデル (LLM)", "priority": "high", "current_knowledge": "intermediate"},
            {"topic": "Pythonバックエンド開発 (FastAPI, Django)", "priority": "high", "current_knowledge": "advanced"},
            {"topic": "クラウドネイティブ技術 (Kubernetes, Docker)", "priority": "medium", "current_knowledge": "intermediate"},
            {"topic": "WebAssembly (WASM)", "priority": "low", "current_knowledge": "beginner"}
        ],
        "preferred_article_style": {
            "length": "medium (5-10 min read)", # short, medium, long
            "tone": "technical_and_practical",    # technical_deep_dive, conceptual_overview, practical_tutorial
            "code_examples": "preferred_if_relevant (Python)" # preferred, not_necessary
        },
        "topics_to_avoid": ["Blockchain全般", "暗号資産関連の投機的な話題"],
        "recent_learning_goal": "LLMの最新の応用事例と、FastAPIでの効率的なAPI設計パターンについて学びたい"
    }
    return preferences

# 確認用 (実際にAPIコールする際はこれは不要)
# print(json.dumps(get_sample_user_preferences(), ensure_ascii=False, indent=2))
```
## 設計のポイント
* `technical_interests`: 単なるトピックリストではなく、各トピックに対する**関心の優先度（priority）**や**現在の知識レベル（current_knowledge）**を含めることで、より精度の高いマッチングが可能になります。
* `preferred_article_style`: 記事の長さ、トーン、コード例の有無といった、**コンテンツの形式やスタイル**に関する好みも重要なコンテキストです。
* `topics_to_avoid`: 明確に興味がない、あるいは見たくないトピックを指定することで、不快な推薦を避けることができます。
* `recent_learning_goal`: ユーザーが**今まさに達成したい学習目標**をコンテキストとして与えることで、AIはよりタイムリーで目的志向の強い推薦を行えます。

# ステップ2：「推薦対象となる記事」のモックデータ準備

AIに推薦させるための記事リストを準備します。実際のアプリケーションでは、これはデータベースや外部APIから動的に取得することになりますが、今回は演習のため、Pythonのリストと辞書でシンプルなモックデータを作成します。

```python
# コードの実行はgpt-4o-miniで実行確認済み
# mock_articles.py (イメージ)

def get_available_tech_articles():
    """推薦対象となる技術記事のモックデータを返す関数"""
    articles = [
        {
            "id": "ARTICLE_001",
            "title": "gpt-4o-mini登場！マルチモーダルAIの最前線とビジネスインパクト徹底解説",
            "tags": ["LLM", "gpt-4o-mini", "マルチモーダルAI", "ビジネス応用"],
            "estimated_read_time_min": 15,
            "difficulty": "intermediate",
            "summary_for_ai": "OpenAIが発表した最新モデルgpt-4oは、テキスト、音声、画像を統合的に扱えるマルチモーダル性能が特徴。応答速度の向上とAPIコスト削減も実現し、多様な産業での応用が期待される。本記事ではその技術的特徴とユースケースを詳説。"
        },
        {
            "id": "ARTICLE_002",
            "title": "FastAPIとPydanticで作る！型安全なPython Web API開発実践入門",
            "tags": ["Python", "FastAPI", "Pydantic", "Web API", "型ヒント"],
            "estimated_read_time_min": 10,
            "difficulty": "beginner_to_intermediate",
            "summary_for_ai": "PythonのモダンなWebフレームワークFastAPIと、データバリデーションライブラリPydanticを組み合わせることで、生産性が高く、型安全で、ドキュメントも自動生成されるAPIを効率的に開発する手法をコード例と共に解説。"
        },
        {
            "id": "ARTICLE_003",
            "title": "Kubernetesにおけるマイクロサービスのオブザーバビリティ戦略2025",
            "tags": ["Kubernetes", "マイクロサービス", "オブザーバビリティ", "Prometheus", "Grafana"],
            "estimated_read_time_min": 20,
            "difficulty": "advanced",
            "summary_for_ai": "コンテナオーケストレーションの標準であるKubernetes上で稼働するマイクロサービス群の健全性を維持し、問題発生時に迅速に対応するための、ログ、メトリクス、トレースを組み合わせた包括的なオブザーバビリティ（可観測性）戦略と、その実現ツールについて詳述。"
        },
        {
            "id": "ARTICLE_004",
            "title": "知っておきたい！SOLID原則に基づいた読みやすいPythonコードの書き方",
            "tags": ["Python", "ソフトウェア設計", "SOLID原則", "コード品質"],
            "estimated_read_time_min": 8,
            "difficulty": "intermediate",
            "summary_for_ai": "オブジェクト指向設計の基本であるSOLID原則（単一責任、オープン/クローズド、リスコフの置換、インターフェース分離、依存性逆転）をPythonコードでどのように実践し、保守性が高く読みやすいコードを書くかを具体例で解説。"
        },
        {
            "id": "ARTICLE_005",
            "title": "WebAssembly (WASM) とは何か？ブラウザを超えて広がる可能性",
            "tags": ["WebAssembly", "WASM", "フロントエンド", "パフォーマンス"],
            "estimated_read_time_min": 12,
            "difficulty": "beginner_to_intermediate",
            "summary_for_ai": "WebAssembly（WASM）の基本概念、なぜ注目されているのか、JavaScriptとの違い、そしてブラウザ上での高速実行だけでなくサーバーサイドや組込みシステムなど、その多様な応用可能性について初心者にも分かりやすく解説。"
        }
    ]
    return articles
```
## ポイント
* `summary_for_ai`: AIが記事内容を把握しやすいように、簡潔な要約を含めています。実際のRAGシステムでは、記事全文の**ベクトル埋め込み**を用いて類似度検索を行いますが、今回は簡易的に要約をコンテキストとして利用します。

# ステップ3：コンテキスト活用型プロンプトの設計 - AIへの「オーダーシート」

ユーザー嗜好コンテキストと記事リストをAIに渡し、パーソナライズされた推薦を行わせるためのプロンプトを設計します。ここでもシステムメッセージを活用して、AIの役割と処理の枠組みを定義します。

```python
# prompt_templates_for_recommendation.py (イメージ)

RECOMMENDATION_SYSTEM_PROMPT_TEMPLATE = """
あなたは、ユーザーの技術的な好みや知識レベルを深く理解し、最適な技術記事を推薦する高度なAIキュレーターです。
以下の情報を正確に把握し、ユーザーにとって価値のある推薦を行ってください。

<user_preferences>
{user_preferences_json_string}
</user_preferences>

<available_articles_list>
{available_articles_json_string}
</available_articles_list>

ユーザーの現在の学習目標や興味関心、知識レベル、好みの記事スタイル、避けたいトピックを総合的に考慮し、
上記<available_articles_list>の中から、最大3件まで、最も適切と思われる記事を選び出してください。

選んだ各記事について、以下のJSON形式で情報を整理して応答してください：
[
  {{
    "article_id": "（記事ID）",
    "article_title": "（記事タイトル）",
    "personalized_recommendation_reason": "（このユーザーにこの記事がなぜおすすめなのか、嗜好情報を具体的に参照しながら2-3文で説明）",
    "estimated_read_time_min": （記事の推定読了時間（分））
  }},
  // ... 他のおすすめ記事 ...
]

推薦理由は、必ずユーザーの嗜好情報（例：興味のあるトピック、知識レベル、学習目標、好みのスタイルなど）のどの部分に合致しているのかを明確に述べてください。
<topics_to_avoid>で指定されたトピックに関連する記事は絶対に推薦しないでください。
応答は上記で指定されたJSON形式のみとし、それ以外のテキストは含めないでください。
"""

# ユーザーからの具体的なリクエストはシンプルでOK
USER_REQUEST_FOR_RECOMMENDATION = "私に最適なおすすめ技術記事を教えてください。"
```
## プロンプト設計のポイント
* **AIの役割定義:** 「高度なAIキュレーター」として、ユーザーの嗜好を深く理解する役割を与えています。
* **コンテキストの明示:** `<user_preferences>`と`<available_articles_list>`という区切り文字（デリミタ）で、どこにコンテキスト情報が埋め込まれるかをAIに明確に示します。
* **タスクの具体的指示:** 記事の選定基準（最大3件、嗜好合致度）、応答形式（JSON）、推薦理由の具体性、禁止事項（`topics_to_avoid`の考慮）などを細かく指示しています。**AIに応答をJSON形式で返すよう指示する**のは、プログラムで後処理しやすくするための重要なテクニックです。
* **プレースホルダー:** `{user_preferences_json_string}`と`{available_articles_json_string}`は、後ほどPythonで実際のJSON文字列に置き換えられます。

# ステップ4：Pythonスクリプトの実装 - すべてを繋ぎ合わせる

これまでの要素を統合し、実際にAIに記事推薦を行わせるPythonスクリプトを作成します。第8回で作成した堅牢なAPIコール関数をベースにすると良いでしょう。

```python
# main_personalized_recommender.py

import os
import json
from dotenv import load_dotenv
from openai import OpenAI

# --- (ステップ1, 2の関数定義をここにコピー or インポート) ---
# (get_sample_user_preferences, get_available_tech_articles)
# --- (ステップ3のプロンプトテンプレート定義をここにコピー or インポート) ---
# (RECOMMENDATION_SYSTEM_PROMPT_TEMPLATE, USER_REQUEST_FOR_RECOMMENDATION)

# (第8回で作成したエラーハンドリング付きAPIコール関数をここにコピー or インポート)
# (ここでは簡略化のため、主要部分のみ記載。実際は第8回の関数を再利用してください)
def call_openai_api_with_context(system_prompt, user_prompt, model="gpt-4o-mini", max_tokens=1000, temperature=0.2):
    load_dotenv()
    client = OpenAI()
    messages = [
        {"role": "system", "content": system_prompt},
        {"role": "user", "content": user_prompt}
    ]
    try:
        print("\n--- OpenAI APIにリクエスト送信中 ---")
        # (送信内容のログ出力などは適宜追加)
        completion = client.chat.completions.create(
            model=model,
            messages=messages,
            max_tokens=max_tokens,
            temperature=temperature
        )
        ai_response_json_str = completion.choices[0].message.content
        
        print("--- AIからの応答 (JSON文字列) ---")
        print(ai_response_json_str)
        
        # JSON文字列をPythonオブジェクト(辞書orリスト)にパース
        parsed_response = json.loads(ai_response_json_str)
        
        print("\n--- パースされたAIの推薦内容 ---")
        for item in parsed_response: # 応答がリスト形式であることを期待
            print(f"  記事ID: {item.get('article_id')}")
            print(f"  タイトル: {item.get('article_title')}")
            print(f"  推薦理由: {item.get('personalized_recommendation_reason')}")
            print(f"  読了時間: {item.get('estimated_read_time_min')}分")
            print("-" * 20)
            
        print(f"\n(使用トークン数 - Prompt: {completion.usage.prompt_tokens}, Completion: {completion.usage.completion_tokens}, Total: {completion.usage.total_tokens})")
        return parsed_response
    except json.JSONDecodeError as e:
        print(f"JSONパースエラー: {e}")
        print(f"AIが返した非JSON文字列: {ai_response_json_str}")
        return None
    except Exception as e:
        print(f"APIリクエストエラー: {e}") # 第8回の詳細なエラーハンドリングを参考に
        return None

# --- メイン処理 ---
if __name__ == "__main__":
    print("パーソナライズド技術記事推薦AIを開始します。")
    
    # 1. コンテキストデータを準備
    user_prefs_py = get_sample_user_preferences()
    articles_py = get_available_tech_articles()
    
    # 2. Python辞書をJSON文字列に変換
    user_prefs_json_str = json.dumps(user_prefs_py, ensure_ascii=False, indent=2)
    articles_json_str = json.dumps(articles_py, ensure_ascii=False, indent=2)
    
    # 3. システムメッセージをコンテキストでフォーマット
    system_prompt = RECOMMENDATION_SYSTEM_PROMPT_TEMPLATE.format(
        user_preferences_json_string=user_prefs_json_str,
        available_articles_json_string=articles_json_str
    )
    
    # 4. ユーザーリクエストを設定
    user_request = USER_REQUEST_FOR_RECOMMENDATION
    
    # 5. AIに推薦をリクエスト (gpt-4oなど、JSONモード対応かつ長文コンテキスト理解に優れたモデル推奨)
    recommendations = call_openai_api_with_context(system_prompt, user_request, model="gpt-4o-mini", temperature=0.1, max_tokens=2000)
    
    if recommendations:
        print("\n--- 推薦処理完了 ---")
    else:
        print("\n--- 推薦処理失敗 ---")
```
## コードのポイント
* **`response_format={"type": "json_object"}`:** `client.chat.completions.create`メソッドにこのパラメータを指定することで、AIに対して応答を**必ずJSON形式で返すよう指示**できます（対応モデルのみ、例：`gpt-4o`, `gpt-3.5-turbo-0125`以降）。これにより、AIが自由形式のテキストを返してしまいパースに失敗するリスクを大幅に低減できます。
* **`json.loads()`:** AIが返したJSON形式の**文字列**を、Pythonの辞書やリストといった**オブジェクトに変換（パース）**しています。これにより、プログラムでデータにアクセスしやすくなります。
* **エラーハンドリング:** `json.JSONDecodeError`をキャッチすることで、AIが万が一JSON形式でない文字列を返した場合のエラーにも対処しています。実際には、第8回で学んだOpenAI API固有のエラーハンドリングも厚く実装すべきです。
* **モデル選択:** このような複雑な指示とコンテキスト理解、そして構造化されたJSON出力を求めるタスクには、`gpt-4o`のような高性能なモデルが適しています。`temperature`を低め（例: 0.1-0.3）に設定することで、指示に忠実で安定したJSON出力を得やすくなります。

# ステップ5：パーソナライズされた結果の分析と考察

上記のスクリプトを実行すると、AIは指定されたユーザーの嗜好（`technical_interests`の優先度や知識レベル、`recent_learning_goal`など）と記事リスト（各記事の`tags`や`summary_for_ai`など）を照合し、`topics_to_avoid`を避けながら、最適な記事を選び出し、その推薦理由をユーザーの嗜好に紐付けて説明してくれるはずです。

例えば、`user_preferences_context`のユーザーが`LLM`と`FastAPI`に高い関心を示していれば、AIは`ARTICLE_001`（gpt-4o-mini）と`ARTICLE_002`（FastAPI）を優先的に推薦し、その理由として「あなたの学習目標であるLLMの最新応用事例に合致し、中級レベルの知識でも理解しやすい内容です」や「FastAPIに関するあなたの高い関心と専門知識レベルを考慮し、さらに深い設計パターンを学べるこの記事を選びました」といった、パーソナルな説明を付与してくれるでしょう。

これが、単なるキーワードマッチングや一般的な人気度に基づくレコメンデーションとは一線を画す、**コンテキストアウェアAIによる真のパーソナライゼーション**です。

## 技術的考察：リッチコンテキストとトークン効率

今回の例では、ユーザープロファイルと記事リスト全体をコンテキストとしてAPIに送信しています。これによりAIは全体像を把握しやすくなりますが、情報量が増えれば**プロンプトトークン数も増加し、APIコストと応答時間に影響**します。
* **大量の記事データを扱う場合:**
    * **事前フィルタリング:** まずはユーザーの嗜好キーワードと記事タグなどで簡易的なフィルタリングを行い、候補となる記事数を絞り込んでからLLMに渡す。
    * **ベクトル検索（RAGの要素）:** 記事内容とユーザーの興味関心を**ベクトル化（Embeddings）**し、コサイン類似度などで関連性の高い少数の記事を特定。その記事情報だけをコンテキストとしてLLMに渡し、最終的な推薦理由の生成やパーソナライズを行わせる。これは、非常に大規模なドキュメントセットを扱う際の標準的なアプローチ（RAG - Retrieval Augmented Generation）です。
* **ユーザープロファイルが膨大な場合:**
    * 全てを送るのではなく、現在のユーザーの具体的なリクエストや直近の行動に最も関連性の高い部分プロファイルだけを選択してコンテキストとして利用する。

これらの最適化は、アプリケーションの要件や扱うデータ量に応じて検討すべき重要な技術的課題です。

# おわりに：MCP的アプローチで、AIを「あなた専属」の賢いパートナーに

今回は、ユーザーの嗜好という「コンテキスト」をMCPの考え方に基づいてJSONで設計し、それをOpenAI APIと連携させることで、パーソナライズされた技術記事推薦AIを構築する実践的なプログラミング例を紹介しました。

**構造化されたコンテキスト情報の設計、それを活用するプロンプトエンジニアリング、そしてAPIとの確実な連携と応答処理。**これらは、AIを単なる「おしゃべりボット」から、ユーザー一人ひとりの状況やニーズを深く理解し、真に価値ある情報を提供してくれる「賢いパートナー」へと進化させるための核心技術です。

皆さんのような技術探求心旺盛な方々にとって、このようなコンテキスト・ドリブンなAIアプリケーション開発は、非常にエキサイティングな挑戦領域ではないでしょうか。ぜひ、今回のコードをベースに、様々なコンテキスト、様々なAIタスクで実験し、その知見をコミュニティで共有してください！

![# はじめに：AIよ、我が心を読め!嗜好コンテキストで切り拓くパーソナライズの神髄 - visual selection (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4058228/a22ac5ee-15a9-4df5-b6d9-62f19a3abaae.png)


---

## 次回予告
ユーザーの「好み」をAIに伝える方法を学びました。次は、ユーザーとの「継続的な対話」においてAIが文脈を保つための技術です。
次回、第10回「MCP活用プログラミング(2) - 会話履歴コンテキストで文脈を維持する「インテリジェント・チャットボット」の基礎」では、ユーザーとの過去のやり取り（会話履歴）をコンテキストとしてAIに渡し、自然で一貫性のある対話を実現するチャットボット開発の基本を探求します。お楽しみに！