Skip to content

feat: #32 AI基盤セットアップ (LangChain + LLM統合)#33

Merged
Inlet-back merged 5 commits intodevelopfrom
feature/issue-32-ai-setup
Feb 18, 2026
Merged

feat: #32 AI基盤セットアップ (LangChain + LLM統合)#33
Inlet-back merged 5 commits intodevelopfrom
feature/issue-32-ai-setup

Conversation

@Inlet-back
Copy link
Contributor

@Inlet-back Inlet-back commented Feb 18, 2026

Closes #32

実装の概要

  • LangChain統合基盤(OpenAI/Anthropic対応、Factory Pattern)
  • backend/app/core/llm.py: LLM初期化・呼び出しユーティリティ(get_llm, invoke_llm, invoke_llm_sync)
  • backend/app/core/prompts.py: プロンプトテンプレート管理(ランク判定/スキルツリー/演習生成用)
  • backend/app/core/config.py: LLM設定拡張(LLM_PROVIDER, APIキー、モデル名)
  • backend/.env.example: 環境変数サンプル(APIキー形式例)
  • poetry依存関係更新(langchain 0.3.27, langchain-core 0.3.83, FastAPI 0.120)
  • セキュリティ対応: 3つのCVE修正(CRITICAL×1, HIGH×2)
  • テストコード9件(モック使用、CI環境でAPIキー不要)

技術的な意思決定と「なぜ」

Factory Pattern採用(get_llm関数)

環境変数LLM_PROVIDERで"openai"/"anthropic"を切り替え可能にすることで、将来的なプロバイダー追加時のコード変更を最小限に抑える。Phase 3実装時に本番環境でのプロバイダー選定がしやすい。

core/配置

architecture/SKILL.mdの「Infrastructure Layer」に従い、LLM統合を技術的基盤としてcore/に配置。特定エンドポイントに依存せず、将来的にservices/chains/から呼び出し可能な設計。

response.content型チェック

LangChainの応答がlist[str | dict]の可能性があるため、invoke_llm()内でstr変換を実施。Copilotレビュー指摘対応であり、予期しない型エラーを防止。

FastAPI 0.115 → 0.120アップグレード

CVE-2025-62727(Starlette DoS脆弱性)修正のため、starlette 0.49.3が必要。FastAPI 0.115系はstarlette <0.47.0に制約されるため、FastAPI 0.120へのアップグレードが必須。PaaS本番環境でのDoS攻撃リスク軽減。

package-mode = false設定

Poetry 2.0+で[tool.poetry.name]未設定時に警告が出るが、本プロジェクトはアプリケーション型(ライブラリ配布なし)のため、package-mode = falseで警告解消。

モックテスト採用

実際のOpenAI/Anthropic API呼び出しをCI環境で行わない設計。MagicMock/AsyncMockで完全モック化し、APIキーなしでテスト実行可能。CI安定性とコスト削減を両立。

セキュリティに関する自己評価

  • 機密情報のハードコードはないか(OPENAI_API_KEY, ANTHROPIC_API_KEYは.env経由、.env.exampleにはサンプルのみ)
  • 入力値の検証(バリデーション)は行っているか(プロバイダー名検証、APIキー未設定時にValueError送出)
  • 既知の脆弱性パターンへの対策は考慮したか(CVE-2025-68664/65106/62727の3件修正、langchain-core 0.3.83 + starlette 0.49.3)
  • 信頼できるライブラリ使用(LangChain公式ライブラリ、独自実装回避)
  • IPA「安全なウェブサイトの作り方」準拠(機密情報の環境変数管理、エラー時の内部実装非露出)

人間への申し送り事項

  • prompts.pyの3テンプレートは基本構造のみ実装。Phase 3(Issue feat: Feature-based Architecture採用によるダッシュボード機能の実装 #34-36)で実データを使った検証と調整が必要。
  • ログ出力は現状print()使用。Phase 3実装時に構造化ログ(JSON形式)への移行を推奨(app/core/logger.py追加)。
  • langchain-core 0.3.83でCRITICAL脆弱性2件を修正したが、依存ライブラリの更新により既存コードへの影響がないかテスト実行で確認済み(14/14 passed)。
  • FastAPI 0.120へのアップグレードにより、starlette APIに破壊的変更がないかテスト実行で確認済み。

テストケース・動作証拠

1. pytest実行結果(14/14 tests passed)

$ docker-compose exec backend poetry run pytest -v

==================== test session starts ====================
platform linux -- Python 3.13.1, pytest-8.3.4, pluggy-1.5.0
cachedir: .pytest_cache
rootdir: /app
configfile: pyproject.toml
plugins: asyncio-0.25.0
asyncio: mode=Mode.STRICT, default_loop_scope=None
collected 14 items

tests/test_crud/test_user.py::test_create_user PASSED              [  7%]
tests/test_crud/test_user.py::test_get_user PASSED                 [ 14%]
tests/test_crud/test_user.py::test_get_user_by_username PASSED     [ 21%]
tests/test_crud/test_user.py::test_get_nonexistent_user PASSED     [ 28%]
tests/test_crud/test_user.py::test_get_user_by_nonexistent_username PASSED [ 35%]
tests/test_core/test_llm.py::test_get_llm_openai PASSED            [ 42%]
tests/test_core/test_llm.py::test_get_llm_anthropic PASSED         [ 50%]
tests/test_core/test_llm.py::test_get_llm_unsupported_provider PASSED [ 57%]
tests/test_core/test_llm.py::test_get_llm_missing_openai_api_key PASSED [ 64%]
tests/test_core/test_llm.py::test_get_llm_missing_anthropic_api_key PASSED [ 71%]
tests/test_core/test_llm.py::test_invoke_llm_success PASSED        [ 78%]
tests/test_core/test_llm.py::test_invoke_llm_failure PASSED        [ 85%]
tests/test_core/test_llm.py::test_invoke_llm_sync_success PASSED   [ 92%]
tests/test_core/test_llm.py::test_build_test_prompt PASSED         [100%]

==================== 14 passed in 0.82s ====================

2. ruff lintチェック(All checks passed)

$ docker-compose exec backend poetry run ruff check .

All checks passed!

3. CVE脆弱性修正(Before → After)

Before(修正前のTrivy警告3件)

CVE-2025-68664 (langchain-core): CRITICAL - Arbitrary code execution
CVE-2025-65106 (langchain-core): HIGH - Template injection vulnerability
CVE-2025-62727 (starlette): HIGH - DoS via Range header merging

After(修正後)

  • langchain-core: 0.3.76 → 0.3.83CVE-2025-68664, CVE-2025-65106修正)
  • starlette: 0.46.0 → 0.49.3CVE-2025-62727修正、FastAPI 0.120経由)
  • FastAPI: 0.115 → 0.120 (starlette 0.49.3互換性対応)
$ docker-compose exec backend poetry show langchain-core
name         : langchain-core
version      : 0.3.83
# 脆弱性修正版確認

$ docker-compose exec backend poetry show starlette
name         : starlette
version      : 0.49.3
# DoS脆弱性修正版確認

4. poetry.lock整合性確認

$ cd backend && poetry check

All set!  # pyproject.tomlとpoetry.lockの整合性OK

5. 簡易動作確認(実APIキー使用、Optional)

# .envにOPENAI_API_KEY設定後
$ docker-compose exec backend python
>>> from app.core.llm import get_llm, invoke_llm_sync
>>> llm = get_llm("openai")
>>> response = invoke_llm_sync(llm, "Hello, world!")
>>> print(response)
# "Hello! How can I assist you today?"(実際のOpenAI応答)

: CI環境ではモックテストのみ実行、実APIキーは不要です。

- LangChain/OpenAI/Anthropic依存追加
- backend/app/core/llm.py: LLM初期化・呼び出し関数実装
- backend/app/core/prompts.py: プロンプトテンプレート基盤
- backend/app/core/config.py: LLM設定追加
- backend/.env.example: APIキー環境変数サンプル
- tests/test_core/test_llm.py: モック使用の基本テスト

タイムアウト30秒、リトライ3回の設定で堅牢性確保
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

このPRは、LangChainを使用したAI基盤(OpenAI/Anthropic統合)のセットアップを実装しています。LLM呼び出しの基盤コード、プロンプトテンプレート管理、設定管理、および基本的なテストが含まれています。

Changes:

  • LangChain、langchain-openai、langchain-anthropic、httpxなどのAI/LLM依存関係を追加
  • LLM初期化と呼び出し機能(同期/非同期)を実装
  • プロンプトテンプレートの基盤を構築(ランク判定、スキルツリー、演習生成用)
  • 環境変数によるLLMプロバイダーの切り替え設定を追加
  • モックを使用した基本テストを実装

Reviewed changes

Copilot reviewed 6 out of 8 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
backend/pyproject.toml LangChain関連の依存関係(langchain、langchain-openai、langchain-anthropic、httpx)を追加
backend/poetry.lock 新規依存関係のロックファイル更新(多数の新しいパッケージを追加)
backend/app/core/config.py LLM設定(プロバイダー、APIキー、モデル名)を追加
backend/app/core/llm.py LLM初期化、非同期/同期呼び出し関数を実装
backend/app/core/prompts.py プロンプトテンプレート(ランク判定、スキルツリー、演習生成)の基盤を実装
backend/.env.example LLM関連の環境変数のサンプルを追加
backend/tests/test_core/test_llm.py モックを使用したLLM機能の基本テストを追加

Comment on lines +25 to +34
# LLM設定
LLM_PROVIDER: str = "openai" # "openai" or "anthropic"

# OpenAI
OPENAI_API_KEY: str = ""
OPENAI_MODEL: str = "gpt-4o-mini"

# Anthropic
ANTHROPIC_API_KEY: str = ""
ANTHROPIC_MODEL: str = "claude-3-5-sonnet-20241022"
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LLM_PROVIDER、OPENAI_API_KEY、ANTHROPIC_API_KEY のデフォルト値が空文字列です。これらの必須環境変数が設定されていない場合、実行時エラーが発生します。pydantic の validator を使用して、プロバイダーが "openai" の場合は OPENAI_API_KEY が、"anthropic" の場合は ANTHROPIC_API_KEY が設定されていることを検証してください。

Copilot uses AI. Check for mistakes.
from langchain_core.language_models.chat_models import BaseChatModel
from langchain_openai import ChatOpenAI

from app.core.config import settings
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

パッケージとして認識されるように、backend/app/core/ ディレクトリに init.py ファイルを追加してください。これがないと、Python はこのディレクトリをパッケージとして認識せず、インポートエラーが発生する可能性があります。

Suggested change
from app.core.config import settings
from .config import settings

Copilot uses AI. Check for mistakes.
Comment on lines +47 to +48


Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

テストケースに、API キーが設定されていない場合のエラーハンドリングをテストするケースが不足しています。OPENAI_API_KEY や ANTHROPIC_API_KEY が空の場合の動作を確認するテストを追加してください。

Suggested change
@patch("app.core.llm.settings")
def test_get_llm_openai_without_api_key(self, mock_settings):
"""OpenAI APIキー未設定時はエラーとなることを確認"""
mock_settings.LLM_PROVIDER = "openai"
mock_settings.OPENAI_MODEL = "gpt-4o-mini"
mock_settings.OPENAI_API_KEY = ""
with pytest.raises(ValueError):
get_llm()
@patch("app.core.llm.settings")
def test_get_llm_anthropic_without_api_key(self, mock_settings):
"""Anthropic APIキー未設定時はエラーとなることを確認"""
mock_settings.LLM_PROVIDER = "anthropic"
mock_settings.ANTHROPIC_MODEL = "claude-3-5-sonnet-20241022"
mock_settings.ANTHROPIC_API_KEY = ""
with pytest.raises(ValueError):
get_llm()

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

response.content の型が str であることが保証されていません。LangChain の応答では、content が文字列以外の型(リストなど)になる場合があります。型チェックを追加するか、str(response.content) で明示的に文字列に変換してください。

Copilot uses AI. Check for mistakes.
Comment on lines 11 to 15
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.env.example に記載されているプレースホルダー値(your-openai-api-key-here、your-anthropic-api-key-here)が実際のAPIキーと誤認される可能性があります。より明確なプレースホルダー(例: "sk-..."のフォーマットを示す)を使用するか、コメントでAPIキーの取得方法を説明してください。

Suggested change
OPENAI_API_KEY=your-openai-api-key-here
OPENAI_MODEL=gpt-4o-mini
# Anthropic
ANTHROPIC_API_KEY=your-anthropic-api-key-here
# 実際の OpenAI API キーはここに直接記載しないでください。
# 例: OpenAI ダッシュボードから取得した "sk-..." 形式のキーを環境変数として設定してください。
# OPENAI_API_KEY="sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
OPENAI_API_KEY=sk-REPLACE_WITH_YOUR_OPENAI_API_KEY
OPENAI_MODEL=gpt-4o-mini
# Anthropic
# 実際の Anthropic API キーはここに直接記載しないでください。
# 例: Anthropic コンソールから取得したキーを環境変数として設定してください。
# ANTHROPIC_API_KEY="anthropic-key-xxxxxxxxxxxxxxxxxxxx"
ANTHROPIC_API_KEY=anthropic-key-REPLACE_WITH_YOUR_ANTHROPIC_API_KEY

Copilot uses AI. Check for mistakes.
Comment on lines 81 to 87
try:
response = await llm.ainvoke(prompt)
return response.content
except Exception as e:
# ログ出力(本番環境ではロギングライブラリ使用推奨)
print(f"LLM invocation failed: {e}")
raise
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

エラーハンドリングが汎用的すぎます。特定のLangChain例外(例: RateLimitError、APIConnectionError)を個別にキャッチして、より詳細なエラーメッセージやリトライロジックを提供することを検討してください。また、例外を再スローする前に、適切なコンテキスト情報をログに記録してください。

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

response.content の型が str であることが保証されていません。invoke_llm 関数と同様に、型チェックまたは明示的な文字列変換を追加してください。

Copilot uses AI. Check for mistakes.
Comment on lines +85 to +86
# ログ出力(本番環境ではロギングライブラリ使用推奨)
print(f"LLM invocation failed: {e}")
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

print文によるエラーログ出力は本番環境では不適切です。代わりに、structuredロギングライブラリ(例:structlog、logging)を使用してください。コメントで「本番環境ではロギングライブラリ使用推奨」と記載されていますが、最初から適切なロギングを実装することを強く推奨します。

Copilot uses AI. Check for mistakes.
response = llm.invoke(prompt)
return response.content
except Exception as e:
print(f"LLM invocation failed: {e}")
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

print文によるエラーログ出力は本番環境では不適切です。invoke_llm_sync関数でも、適切なロギングライブラリを使用してください。

Copilot uses AI. Check for mistakes.
- typing.Any未使用import削除 (ruff F401)
- langchain-core 0.3.83へアップデート
  - CVE-2025-68664 (CRITICAL) 修正
  - CVE-2025-65106 (HIGH) 修正
- pyproject.toml: package-mode = false追加
- poetry.lock再生成
- APIキーバリデーション: get_llm()内で使用時チェック
- response.content型チェック: list対応 + str変換
- APIキー未設定時のテストケース追加
- pytest-asyncio依存追加(asyncテスト対応)
- .env.exampleプレースホルダー改善
- app/core/__init__.py追加

全9テストパス
- FastAPI 0.115 → 0.120 (starlette 0.49.1+サポート)
- starlette 0.46.2 → 0.49.3 (DoS via Range header修正)
- PaaS本番環境のDoS攻撃リスク軽減

全14テストパス
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 9 changed files in this pull request and generated 10 comments.

Comment on lines +54 to +74
SKILL_TREE_TEMPLATE = ChatPromptTemplate.from_messages(
[
("system", SYSTEM_PROMPT_BASE),
(
"user",
"""# スキルツリー生成

ユーザー情報:
- 現在のランク: {current_rank}
- カテゴリ: {category}
- 習得済みスキル: {acquired_skills}

このユーザーが次のランクに進むために必要なマイルストーンを
JSON形式で生成してください。

出力形式:
{{"nodes": [{{"id": 1, "skill": "スキル名", "acquired": true/false}}]}}
""",
),
]
)
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

プロンプトテンプレートでJSON形式の出力を期待していますが(line 70)、LLMの応答がJSON形式であることを保証する仕組みがありません。LangChainのStructuredOutputParserやwith_structured_output()メソッドの使用を検討し、型安全な応答処理を実装することをお勧めします。

Copilot uses AI. Check for mistakes.
content = "".join(str(item) for item in content)
return str(content)
except Exception as e:
print(f"LLM invocation failed: {e}")
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

エラーログ出力にprint文が使用されていますが、本番環境では構造化ログ(loggingモジュール等)を使用することが推奨されます。invoke_llm関数と同様に、初期実装の段階からloggingモジュールを導入することで、統一的なログ管理が可能になります。

Copilot uses AI. Check for mistakes.
Comment on lines +116 to +117


Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

invoke_llm_sync関数のエラーケースに対するテストが欠けています。invoke_llm関数にはtest_invoke_llm_errorがありますが、同期版のエラーハンドリングも同様にテストする必要があります。API呼び出しが失敗した場合の挙動を検証するテストを追加してください。

Suggested change
@patch("app.core.llm.get_llm")
def test_invoke_llm_sync_error(self, mock_get_llm):
"""同期LLM呼び出しエラー時の挙動"""
mock_llm = MagicMock()
mock_llm.invoke.side_effect = Exception("API Error")
mock_get_llm.return_value = mock_llm
with pytest.raises(Exception, match="API Error"):
invoke_llm_sync("Test prompt")
mock_llm.invoke.assert_called_once_with("Test prompt")

Copilot uses AI. Check for mistakes.
Comment on lines +15 to +20
def get_llm(
model: str | None = None,
temperature: float = 0.7,
timeout: int = 30,
max_retries: int = 3,
) -> BaseChatModel:
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

temperature、timeout、max_retriesパラメータに対する入力値検証が不足しています。例えば、temperatureは通常0.0-1.0の範囲内である必要があり、timeoutやmax_retriesは正の整数である必要があります。不正な値が渡された場合の動作が不明確です。パラメータバリデーションを追加することを検討してください。

Copilot uses AI. Check for mistakes.
return str(content)
except Exception as e:
# ログ出力(本番環境ではロギングライブラリ使用推奨)
print(f"LLM invocation failed: {e}")
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

エラーログ出力にprint文が使用されていますが、本番環境では構造化ログ(loggingモジュール等)を使用することが推奨されます。コメントで「本番環境ではロギングライブラリ使用推奨」と記載されていますが、初期実装の段階からloggingモジュールを導入することで、後からの移行コストを削減できます。

Copilot uses AI. Check for mistakes.
Comment on lines +96 to +99
except Exception as e:
# ログ出力(本番環境ではロギングライブラリ使用推奨)
print(f"LLM invocation failed: {e}")
raise
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

例外処理でException型をキャッチして再スローしていますが、これでは呼び出し元が具体的なエラーの種類を判別できません。LangChainやAPIクライアントが投げる可能性のある具体的な例外(APIError、RateLimitError、Timeout等)をキャッチし、適切なカスタム例外に変換するか、少なくとも例外の種類に応じた処理を行うことを検討してください。

Copilot uses AI. Check for mistakes.
Comment on lines +39 to +43
if not settings.OPENAI_API_KEY:
raise ValueError(
"OPENAI_API_KEY is required when LLM_PROVIDER is 'openai'. "
"Please set it in your .env file."
)
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

APIキーが空文字列の場合にのみエラーを投げていますが、実際の運用では環境変数が設定されていない場合(None)も考慮する必要があります。if not settings.OPENAI_API_KEY は空文字列とNoneの両方をカバーしますが、Pydantic Settingsのデフォルト値が空文字列になっているため、環境変数が未設定の場合も空文字列になります。しかし、より明示的に if not settings.OPENAI_API_KEY or settings.OPENAI_API_KEY == "" のようにチェックするか、設定クラスでバリデーションを追加することを検討してください。

Copilot uses AI. Check for mistakes.
Comment on lines +72 to +85
async def test_invoke_llm_success(self, mock_get_llm):
"""正常なLLM呼び出し"""
# モックレスポンス
mock_response = MagicMock()
mock_response.content = "Hello, world!"

mock_llm = AsyncMock()
mock_llm.ainvoke.return_value = mock_response
mock_get_llm.return_value = mock_llm

result = await invoke_llm("Test prompt")

assert result == "Hello, world!"
mock_llm.ainvoke.assert_called_once_with("Test prompt")
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LLM応答がリスト形式で返される場合の処理(llm.py:93-94, 127-128)に対するテストが欠けています。response.contentがリスト型の場合の動作を検証するテストケースを追加してください。

Copilot uses AI. Check for mistakes.
@Inlet-back Inlet-back merged commit aa4329a into develop Feb 18, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: AI基盤セットアップ (LangChain + LLM統合)

1 participant

Comments