「毎回同じことを説明するの、もう疲れた」
Claude CodeやCursorを使っていて、この苛立ちを感じたことがあるはずだ。
前回の記事でJitRLの理論を解説したが、今回は実際にClaude Codeに導入する完全な手順を示す。
この記事を読み終える頃には、あなたのClaude Codeは:
- 過去の成功/失敗パターンを記憶する
- 類似状況で過去の経験を自動参照する
- プロジェクト固有の知識を蓄積する
ようになる。
目次
- 現状の課題を理解する
- JitRLアーキテクチャをClaude Codeに適用する
- 実装ステップ1:経験ストレージを構築する
- 実装ステップ2:Hooksで経験を自動収集する
- 実装ステップ3:類似経験の検索と注入
- 実装ステップ4:アドバンテージ計算と推薦
- 運用とチューニング
- 既存プラグインとの比較
1. 現状の課題を理解する
Claude Codeの標準メモリシステム
Claude CodeにはCLAUDE.mdというメモリシステムがある:
# CLAUDE.md
## プロジェクト情報
- PostgreSQL 15使用
- テストは `npm run test:unit`
問題点:
- 手動更新が必要
- 成功/失敗の文脈が保存されない
- 類似状況の検索ができない
JitRLとの決定的な違い
| 機能 | CLAUDE.md | JitRLアプローチ |
|---|---|---|
| 経験の保存 | 手動 | 自動 |
| 文脈 | なし | state→action→reward |
| 検索 | なし | ベクトル類似度 |
| 学習 | なし | 成功パターンを重視 |
2. JitRLアーキテクチャをClaude Codeに適用する
全体アーキテクチャ
┌─────────────────────────────────────────────────────────────┐
│ Claude Code Session │
├─────────────────────────────────────────────────────────────┤
│ UserPromptSubmit Stop SessionEnd │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌──────────┐ ┌───────────┐ ┌────────────┐ │
│ │ Context │ │ Experience │ │ Eval & │ │
│ │ Inject │ │ Capture │ │ Store │ │
│ └────┬─────┘ └─────┬─────┘ └──────┬─────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Experience Memory (Faiss + JSON) │ │
│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │
│ │ │Triplet 1│ │Triplet 2│ │Triplet 3│ │ ... │ │ │
│ │ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
必要なコンポーネント
- Experience Storage:Faiss + JSONL
- Hooks:Claude Codeのライフサイクルイベント
- Retriever:類似経験の検索
- Scorer:経験の評価とスコアリング
3. 実装ステップ1:経験ストレージを構築する
ディレクトリ構造
mkdir -p ~/.claude-jitrl/{experiences,indexes,cache}
~/.claude-jitrl/
├── experiences/
│ └── {project_hash}/
│ ├── episodes.jsonl # 経験データ
│ └── step_metadata.pkl # Faissメタデータ
├── indexes/
│ └── {project_hash}/
│ ├── state_vectors.index # 状態ベクトルインデックス
│ └── action_vectors.index
├── cache/
│ └── embeddings/ # エンベディングキャッシュ
└── config.yaml # 設定ファイル
経験ストレージクラス
# ~/.claude-jitrl/src/experience_store.py
import os
import json
import hashlib
import numpy as np
import pickle
from pathlib import Path
from typing import List, Dict, Any, Optional
from datetime import datetime
try:
import faiss
except ImportError:
print("pip install faiss-cpu")
faiss = None
class ExperienceStore:
"""
JitRL式経験ストレージ for Claude Code
Policy Triplet: <state, action, outcome>
- state: 現在のコンテキスト(ファイル、エラー、目標)
- action: Claudeが取ったアクション(コード変更、コマンド実行等)
- outcome: 結果(成功/失敗、ユーザーフィードバック)
"""
def __init__(self, project_path: str, gamma: float = 0.95):
self.project_hash = self._hash_project(project_path)
self.gamma = gamma
self.vector_dim = 1536 # OpenAI ada-002
# パス設定
self.base_dir = Path.home() / ".claude-jitrl"
self.project_dir = self.base_dir / "experiences" / self.project_hash
self.index_dir = self.base_dir / "indexes" / self.project_hash
# ディレクトリ作成
self.project_dir.mkdir(parents=True, exist_ok=True)
self.index_dir.mkdir(parents=True, exist_ok=True)
# ファイルパス
self.episodes_path = self.project_dir / "episodes.jsonl"
self.metadata_path = self.project_dir / "step_metadata.pkl"
self.state_index_path = self.index_dir / "state_vectors.index"
# 初期化
self._init_vector_db()
self._load_metadata()
def _hash_project(self, path: str) -> str:
"""プロジェクトパスをハッシュ化"""
return hashlib.md5(path.encode()).hexdigest()[:12]
def _init_vector_db(self):
"""Faissインデックスを初期化"""
if faiss is None:
self.state_index = None
return
if self.state_index_path.exists():
self.state_index = faiss.read_index(str(self.state_index_path))
else:
self.state_index = faiss.IndexFlatIP(self.vector_dim)
def _load_metadata(self):
"""メタデータをロード"""
if self.metadata_path.exists():
with open(self.metadata_path, 'rb') as f:
self.step_metadata = pickle.load(f)
else:
self.step_metadata = []
def _save(self):
"""インデックスとメタデータを保存"""
if self.state_index is not None:
faiss.write_index(self.state_index, str(self.state_index_path))
with open(self.metadata_path, 'wb') as f:
pickle.dump(self.step_metadata, f)
def add_experience(self,
state: Dict[str, Any],
action: Dict[str, Any],
outcome: Dict[str, Any],
state_embedding: np.ndarray):
"""経験を追加"""
experience = {
"timestamp": datetime.now().isoformat(),
"state": state,
"action": action,
"outcome": outcome,
"score": self._calculate_score(outcome)
}
with open(self.episodes_path, 'a') as f:
f.write(json.dumps(experience, ensure_ascii=False) + "\n")
if self.state_index is not None:
norm_embedding = state_embedding / (np.linalg.norm(state_embedding) + 1e-8)
self.state_index.add(norm_embedding.reshape(1, -1).astype('float32'))
self.step_metadata.append({
"experience": experience,
"idx": len(self.step_metadata)
})
self._save()
return experience
def _calculate_score(self, outcome: Dict[str, Any]) -> float:
"""成果からスコアを計算"""
base_score = 5 if outcome.get("success", False) else -2
feedback = outcome.get("user_feedback", "")
if "perfect" in feedback.lower() or "great" in feedback.lower():
base_score += 5
elif "good" in feedback.lower():
base_score += 2
elif "wrong" in feedback.lower() or "bad" in feedback.lower():
base_score -= 3
if not outcome.get("follow_up_needed", True):
base_score += 2
return base_score
def search_similar(self,
query_embedding: np.ndarray,
k: int = 5,
threshold: float = 0.7) -> List[Dict[str, Any]]:
"""類似経験を検索"""
if self.state_index is None or self.state_index.ntotal == 0:
return []
norm_query = query_embedding / (np.linalg.norm(query_embedding) + 1e-8)
scores, indices = self.state_index.search(
norm_query.reshape(1, -1).astype('float32'),
min(k, self.state_index.ntotal)
)
results = []
for score, idx in zip(scores[0], indices[0]):
if score >= threshold and idx < len(self.step_metadata):
result = self.step_metadata[idx].copy()
result["similarity"] = float(score)
results.append(result)
results.sort(key=lambda x: (
x["similarity"] * 0.3 +
x["experience"]["score"] / 10 * 0.7
), reverse=True)
return results
def get_action_advantages(self, similar_experiences: List[Dict]) -> Dict[str, float]:
"""JitRL式アドバンテージ計算"""
if not similar_experiences:
return {}
action_scores = {}
for exp in similar_experiences:
action_type = exp["experience"]["action"].get("tool_name", "unknown")
score = exp["experience"]["score"]
if action_type not in action_scores:
action_scores[action_type] = []
action_scores[action_type].append(score)
action_avg = {k: sum(v)/len(v) for k, v in action_scores.items()}
all_scores = [s for scores in action_scores.values() for s in scores]
baseline = sum(all_scores) / len(all_scores) if all_scores else 0
advantages = {k: v - baseline for k, v in action_avg.items()}
return advantages
4. 実装ステップ2:Hooksで経験を自動収集する
hooks.jsonの設定
~/.claude/settings.json に追加:
{
"hooks": {
"UserPromptSubmit": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "python3 ~/.claude-jitrl/hooks/on_prompt.py"
}
]
}
],
"Stop": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "python3 ~/.claude-jitrl/hooks/on_stop.py"
}
]
}
],
"SessionEnd": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "python3 ~/.claude-jitrl/hooks/on_session_end.py"
}
]
}
]
}
}
UserPromptSubmit Hook(コンテキスト注入)
#!/usr/bin/env python3
# ~/.claude-jitrl/hooks/on_prompt.py
import os
import sys
import json
from pathlib import Path
sys.path.insert(0, str(Path.home() / ".claude-jitrl" / "src"))
from experience_store import ExperienceStore
from embedder import get_embedding
def main():
hook_data = json.loads(os.environ.get("CLAUDE_HOOK_DATA", "{}"))
cwd = hook_data.get("cwd", os.getcwd())
prompt = hook_data.get("prompt", "")
store = ExperienceStore(cwd)
query_embedding = get_embedding(prompt)
similar = store.search_similar(query_embedding, k=3, threshold=0.6)
if not similar:
return
advantages = store.get_action_advantages(similar)
context = generate_context_injection(similar, advantages)
print(context)
def generate_context_injection(experiences: list, advantages: dict) -> str:
lines = ["## 💡 過去の類似経験からの学び\n"]
successes = [e for e in experiences if e["experience"]["score"] > 0]
if successes:
lines.append("### ✅ 成功パターン")
for exp in successes[:2]:
action = exp["experience"]["action"]
lines.append(f"- **{action.get('tool_name', 'action')}**: {action.get('summary', '')}")
failures = [e for e in experiences if e["experience"]["score"] < 0]
if failures:
lines.append("\n### ⚠️ 過去の失敗パターン(避けるべき)")
for exp in failures[:2]:
action = exp["experience"]["action"]
outcome = exp["experience"]["outcome"]
lines.append(f"- **{action.get('tool_name', 'action')}**: {outcome.get('error_summary', '')}")
if advantages:
lines.append("\n### 📊 推奨アプローチ(経験ベース)")
sorted_adv = sorted(advantages.items(), key=lambda x: x[1], reverse=True)
for tool, adv in sorted_adv[:3]:
emoji = "👍" if adv > 0 else "👎"
lines.append(f"- {emoji} **{tool}**: アドバンテージ {adv:+.2f}")
return "\n".join(lines)
if __name__ == "__main__":
main()
5. エンベディングモジュール
# ~/.claude-jitrl/src/embedder.py
import os
import hashlib
import numpy as np
from pathlib import Path
try:
from openai import OpenAI
client = OpenAI()
EMBEDDING_MODEL = "text-embedding-3-small"
except ImportError:
client = None
CACHE_DIR = Path.home() / ".claude-jitrl" / "cache" / "embeddings"
def get_embedding(text: str, use_cache: bool = True) -> np.ndarray:
if not text.strip():
return np.zeros(1536, dtype=np.float32)
cache_key = hashlib.md5(text.encode()).hexdigest()
cache_path = CACHE_DIR / f"{cache_key}.npy"
if use_cache and cache_path.exists():
return np.load(cache_path)
if client is None:
return _fallback_embedding(text)
try:
response = client.embeddings.create(
model=EMBEDDING_MODEL,
input=text[:8000]
)
embedding = np.array(response.data[0].embedding, dtype=np.float32)
CACHE_DIR.mkdir(parents=True, exist_ok=True)
np.save(cache_path, embedding)
return embedding
except Exception as e:
print(f"Embedding error: {e}")
return _fallback_embedding(text)
def _fallback_embedding(text: str) -> np.ndarray:
words = text.lower().split()
embedding = np.zeros(1536, dtype=np.float32)
for i, word in enumerate(words[:500]):
idx = hash(word) % 1536
embedding[idx] += 1.0 / (i + 1)
norm = np.linalg.norm(embedding)
if norm > 0:
embedding /= norm
return embedding
6. 運用とチューニング
設定ファイル
# ~/.claude-jitrl/config.yaml
gamma: 0.95
similarity_threshold: 0.6
max_experiences_per_search: 5
use_llm_evaluation: false
evaluation_model: "gpt-4o-mini"
cache_embeddings: true
max_cache_size_mb: 500
max_experiences: 10000
experience_ttl_days: 90
CLIツール
#!/usr/bin/env python3
# ~/.claude-jitrl/cli.py
import click
from pathlib import Path
import sys
sys.path.insert(0, str(Path.home() / ".claude-jitrl" / "src"))
from experience_store import ExperienceStore
@click.group()
def cli():
"""JitRL for Claude Code - 経験管理CLI"""
pass
@cli.command()
@click.option('--project', '-p', default='.', help='プロジェクトパス')
def stats(project):
"""経験ストアの統計を表示"""
store = ExperienceStore(project)
s = store.get_stats()
click.echo(f"📊 JitRL Statistics")
click.echo(f" Experiences: {s['total_experiences']}")
@cli.command()
@click.argument('query')
@click.option('--project', '-p', default='.', help='プロジェクトパス')
@click.option('-k', default=5, help='取得数')
def search(query, project, k):
"""類似経験を検索"""
from embedder import get_embedding
store = ExperienceStore(project)
embedding = get_embedding(query)
results = store.search_similar(embedding, k=k)
click.echo(f"🔍 Found {len(results)} similar experiences")
if __name__ == "__main__":
cli()
7. 既存プラグインとの比較
| 機能 | CLAUDE.md | claude-mem | claude-supermemory | JitRL式 |
|---|---|---|---|---|
| 経験の自動収集 | ❌ | ✅ | ✅ | ✅ |
| ベクトル検索 | ❌ | ❌ | ✅ | ✅ |
| 成功/失敗スコアリング | ❌ | ❌ | ❌ | ✅ |
| アドバンテージ計算 | ❌ | ❌ | ❌ | ✅ |
| 推奨アプローチ提示 | ❌ | ❌ | ❌ | ✅ |
8. 実践例:エラー修正パターンの学習
実際の動作
User: この型エラーを修正して
# JitRLが自動注入するコンテキスト:
## 💡 過去の類似経験からの学び
### ✅ 成功パターン
- **Edit**: interface定義を修正してexportを追加
- 類似度: 0.82, スコア: +7
### ⚠️ 過去の失敗パターン(避けるべき)
- **Write**: 型定義ファイルを新規作成
- 理由: 既存の型定義と競合した
### 📊 推奨アプローチ(経験ベース)
- 👍 **Edit**: アドバンテージ +3.2
- 👎 **Write**: アドバンテージ -1.5
まとめ:今すぐ始める手順
# 1. ディレクトリ作成
mkdir -p ~/.claude-jitrl/{src,hooks,cache,experiences,indexes}
# 2. 依存関係インストール
pip install faiss-cpu openai numpy click
# 3. ファイルをコピー(この記事のコードを保存)
# 4. Hooksを設定
# 5. 動作確認
python3 ~/.claude-jitrl/cli.py stats
期待される効果
| 指標 | 導入前 | 導入後(目安) |
|---|---|---|
| 同じ説明の繰り返し | 毎回 | 60%減 |
| エラー修正時間 | 基準 | 40%短縮 |
| 成功パターンの再現 | 手動 | 自動 |
この記事が参考になったら、いいねとストックをお願いします!
質問:あなたのプロジェクトで最も「毎回説明が必要」なパターンは何ですか?コメントで教えてください!