第08回 LLM分析の再現性¶
科目: ビッグデータ応用Ⅱ〔特徴量エンジニアリング〕
担当: 小野原彩香
対象: 3年生(後期)
学習目標¶
- LLMの非決定性(温度・サンプリング)が分析の再現性に与える影響を説明できる
- Gitを使ってプロンプトファイルをバージョン管理できる
- JSONLフォーマットでAPIのリクエスト・レスポンスをログとして記録できる
temperature=0やseedパラメータを使って出力を安定させられる- ログから同一プロンプトを再実行するPythonスクリプトを作成できる
1. LLM分析における再現性の課題¶
1.1 再現性とは¶
科学的な分析においては、同じ手順を踏めば同じ結果が得られることが求められます(再現性)。しかしLLMを使った分析では、同じプロンプトを同じモデルに送っても、毎回異なる出力が返ってくることがあります。
1.2 LLM固有の非決定性¶
LLMがテキストを生成する際、次のトークン(単語のかたまり)を確率的に選択します。この確率的な選択が「非決定性」の原因です。
1.3 温度パラメータ(temperature)¶
温度(temperature) は、確率分布の「尖り具合」を制御するパラメータです。
| temperature 値 | 挙動 | 使用場面 |
|---|---|---|
0.0 |
確率最大のトークンを常に選択(決定論的) | コマンド生成、分析の再現性が必要な場面 |
0.5〜0.7 |
バランスの取れた多様性 | 一般的なタスク |
1.0 |
デフォルト。高い多様性 | 創作、アイデア出し |
1.5〜2.0 |
非常に高い多様性(予測しにくい) | ブレインストーミング |
# temperature=0 は最も決定論的な出力(同じ入力に同じ出力)
# ただし、温度0でも完全な再現性が保証されるわけではない
# (並列処理のタイミング差など、実装依存の要因がある)
response = client.messages.create(
model="claude-opus-4-5",
max_tokens=256,
temperature=0, # 決定論的な出力
messages=[...]
)
1.4 再現性が重要な理由¶
- 論文・レポートでの引用:「このプロンプトでこの結果を得た」という記録が必要
- バグの再現:「なぜこのコマンドが生成されたか」を後から確認するため
- A/Bテスト:プロンプトを変更したときの差異を比較するため
- 監査・説明責任:どのプロンプトが使われたかを証明するため
2. プロンプトのバージョン管理¶
2.1 Gitによるプロンプト管理の基本¶
プロンプトをコードと同様にGitで管理することで、変更履歴を追跡できます。
# プロンプト用ディレクトリを作成
mkdir -p .prompts/v1
# ディレクトリ構成例
.prompts/
├── README.md # プロンプト一覧と用途の説明
├── v1/
│ ├── system_gdal.txt # GDALコマンド生成用システムプロンプト
│ ├── few_shot_ogr2ogr.txt # ogr2ogr のfew-shot例
│ └── task_reproject.txt # 投影変換タスク用プロンプト
└── v2/
├── system_gdal.txt # 改善版システムプロンプト
└── task_reproject.txt # 改善版タスクプロンプト
2.2 プロンプトファイルの例¶
# .prompts/v1/system_gdal.txt を作成
cat > .prompts/v1/system_gdal.txt << 'EOF'
あなたはGDAL/OGRの専門家です。
ユーザーの指示に従い、適切なGDAL/OGRコマンドを生成してください。
必ず以下のルールを守ってください:
1. コマンドのみを出力し、説明文・前置き・コードブロック記号(```)は書かない
2. ファイルパスは必ずダブルクォートで囲む
3. 出力ファイルが存在する場合に備えて -overwrite オプションを付ける
4. CRSは EPSG コードで指定する
EOF
# Git でバージョン管理
git add .prompts/
git commit -m "feat: GDALコマンド生成用プロンプト v1 を追加"
2.3 プロンプトのバージョン番号管理¶
# プロンプトのバージョンをメタデータとして管理
PROMPT_VERSIONS = {
"system_gdal": {
"version": "1.0.0",
"path": ".prompts/v1/system_gdal.txt",
"git_commit": "a1b2c3d", # 実際のコミットハッシュを記録
"created": "2025-10-08",
"description": "GDALコマンド生成用システムプロンプト(初版)"
}
}
def load_prompt(name: str) -> tuple[str, dict]:
"""プロンプトファイルを読み込み、メタデータとともに返す"""
meta = PROMPT_VERSIONS[name]
with open(meta["path"]) as f:
content = f.read()
return content, meta
3. LLMの出力ログ記録¶
3.1 JSONLフォーマットとは¶
JSONL(JSON Lines)は、1行に1つのJSONオブジェクトを記録するフォーマットです。大量のログを効率的に記録・読み込みできます。
{"timestamp": "2025-10-08T10:00:00Z", "prompt_version": "1.0.0", "request": {...}, "response": {...}}
{"timestamp": "2025-10-08T10:01:30Z", "prompt_version": "1.0.0", "request": {...}, "response": {...}}
{"timestamp": "2025-10-08T10:05:12Z", "prompt_version": "1.1.0", "request": {...}, "response": {...}}
3.2 ログ記録スクリプト¶
#!/usr/bin/env python3
"""
llm_logger.py
Anthropic APIのリクエスト・レスポンスをJSONLファイルに記録するモジュール
"""
import json
import hashlib
import datetime
import pathlib
import anthropic
from typing import Any
LOG_DIR = pathlib.Path("logs")
LOG_DIR.mkdir(exist_ok=True)
LOG_FILE = LOG_DIR / "llm_requests.jsonl"
def compute_prompt_hash(system: str, messages: list[dict]) -> str:
"""プロンプトのハッシュ値を計算(変更検知に使用)"""
content = json.dumps({"system": system, "messages": messages}, ensure_ascii=False)
return hashlib.sha256(content.encode()).hexdigest()[:16]
def call_and_log(
client: anthropic.Anthropic,
model: str,
system: str,
messages: list[dict],
max_tokens: int = 512,
temperature: float = 0.0,
prompt_version: str = "unknown",
task_id: str = "",
**kwargs: Any
) -> anthropic.types.Message:
"""
Claude APIを呼び出し、リクエスト・レスポンスをJSONLに記録する。
Args:
client: Anthropic クライアント
model: 使用するモデルID
system: システムプロンプト
messages: メッセージ履歴
max_tokens: 最大出力トークン数
temperature: 温度パラメータ(0.0=決定論的)
prompt_version: プロンプトのバージョン番号
task_id: タスクの識別子(ログ検索用)
**kwargs: その他のAPIパラメータ
Returns:
Anthropic APIのレスポンスオブジェクト
"""
# タイムスタンプ(UTC)
timestamp = datetime.datetime.utcnow().isoformat() + "Z"
# プロンプトのハッシュ(変更検知)
prompt_hash = compute_prompt_hash(system, messages)
# APIリクエスト
response = client.messages.create(
model=model,
max_tokens=max_tokens,
temperature=temperature,
system=system,
messages=messages,
**kwargs
)
# ログエントリの作成
log_entry = {
"timestamp": timestamp,
"task_id": task_id,
"prompt_version": prompt_version,
"prompt_hash": prompt_hash,
"model": model,
"temperature": temperature,
"max_tokens": max_tokens,
"request": {
"system": system,
"messages": messages
},
"response": {
"id": response.id,
"content": [{"type": b.type, "text": b.text} for b in response.content],
"stop_reason": response.stop_reason,
"usage": {
"input_tokens": response.usage.input_tokens,
"output_tokens": response.usage.output_tokens
}
}
}
# JSONLに追記
with open(LOG_FILE, "a", encoding="utf-8") as f:
f.write(json.dumps(log_entry, ensure_ascii=False) + "\n")
return response
# 使用例
if __name__ == "__main__":
client = anthropic.Anthropic()
system_prompt = open(".prompts/v1/system_gdal.txt").read()
task = "llm_access.geojson を EPSG:32654 に変換し output_utm.geojson に保存するコマンドを生成してください"
response = call_and_log(
client=client,
model="claude-opus-4-5",
system=system_prompt,
messages=[{"role": "user", "content": task}],
temperature=0.0,
prompt_version="1.0.0",
task_id="reproject_utm54n_001"
)
print(response.content[0].text)
print(f"\nログを {LOG_FILE} に記録しました")
4. 温度パラメータとシードの設定¶
4.1 temperature=0 の効果¶
import anthropic
client = anthropic.Anthropic()
# 同じプロンプトで temperature=0 と temperature=1.0 を比較
prompt = "GeoJSONをEPSG:32654に変換するogr2ogrコマンドをコマンドのみ出力してください"
print("=== temperature=0(決定論的) ===")
for i in range(3):
r = client.messages.create(
model="claude-opus-4-5",
max_tokens=128,
temperature=0,
messages=[{"role": "user", "content": prompt}]
)
print(f"試行 {i+1}: {r.content[0].text.strip()}")
print("\n=== temperature=1.0(デフォルト) ===")
for i in range(3):
r = client.messages.create(
model="claude-opus-4-5",
max_tokens=128,
temperature=1.0,
messages=[{"role": "user", "content": prompt}]
)
print(f"試行 {i+1}: {r.content[0].text.strip()}")
4.2 Anthropic APIのmetadataフィールド¶
Anthropic APIでは metadata フィールドを使ってリクエストに追加情報を付与できます。これにより、ログの追跡・管理が容易になります。
response = client.messages.create(
model="claude-opus-4-5",
max_tokens=512,
temperature=0,
system=system_prompt,
messages=messages,
metadata={
# user_id: ユーザーの識別子(匿名化されたID推奨)
"user_id": "student_anonymous_42"
}
)
# metadata はレスポンスには返ってこないが、
# Anthropic ダッシュボードでのフィルタリングに使用できる
print(f"レスポンスID: {response.id}")
# レスポンスIDをログに保存することで、Anthropic側でのトレースが可能
4.3 再現性に関するベストプラクティス¶
# 再現性の高いAPI呼び出しの設定例
REPRODUCIBLE_CONFIG = {
"model": "claude-opus-4-5", # モデルバージョンを固定
"temperature": 0, # 決定論的な出力
"max_tokens": 512,
}
# ※ seed パラメータについて
# OpenAI APIには seed パラメータがありますが、
# Anthropic Claude APIには現在 seed パラメータはありません。
# temperature=0 と プロンプトの固定が再現性確保の主な手段です。
5. 再現性チェックリスト¶
分析レポートや課題提出前に以下の項目を確認してください。
5.1 プロンプト管理¶
- [ ] プロンプトはファイルに保存し、Gitでバージョン管理されている
- [ ] プロンプトファイルには作成日・目的のコメントが記載されている
- [ ] プロンプトのバージョン番号がログに記録されている
- [ ] システムプロンプトとユーザープロンプトが分離されている
5.2 APIパラメータ¶
- [ ]
temperature=0に設定している(または設定値をログに記録) - [ ] モデル名(例:
claude-opus-4-5)を明示的に指定している - [ ]
max_tokensを適切な値に設定している
5.3 ログ記録¶
- [ ] リクエスト・レスポンスがJSONLファイルに記録されている
- [ ] ログにタイムスタンプが含まれている
- [ ] ログにプロンプトのハッシュが含まれている
- [ ] ログファイルをGitでバージョン管理している(または別途保管)
5.4 再実行可能性¶
- [ ] ログから元のリクエストを再送信できる仕組みがある
- [ ] データファイルのパスが絶対パスでなく相対パスで記録されている
- [ ] 必要なライブラリとバージョンが
requirements.txtに記載されている
6. ハンズオン手順¶
事前準備¶
# 必要なライブラリのインストール
pip install anthropic
# プロジェクト構造の作成
mkdir -p .prompts/v1 logs
# Git の初期化(まだの場合)
git init
echo "logs/*.jsonl" >> .gitignore # ログはGit管理外(容量が大きいため)
echo "*.jsonl" >> .gitignore
git add .gitignore
git commit -m "init: .gitignore にJSONLログを追加"
ステップ1: プロンプトファイルの作成¶
# システムプロンプトを作成
cat > .prompts/v1/system_gdal.txt << 'EOF'
# version: 1.0.0
# created: 2025-10-08
# purpose: GDALコマンド生成用システムプロンプト
あなたはGDAL/OGRの専門家です。ユーザーの指示に従い、適切なGDAL/OGRコマンドを生成してください。
必ず以下のルールを守ってください:
1. コマンドのみを出力し、説明文・前置き・コードブロック記号(```)は書かない
2. ファイルパスは必ずダブルクォートで囲む
3. 出力ファイルが存在する場合に備えて -overwrite オプションを付ける
4. CRSは EPSG コードで指定する
EOF
# Git にコミット
git add .prompts/
git commit -m "feat: GDALコマンド生成システムプロンプト v1.0.0 を追加"
ステップ2: ログ付きAPI呼び出し¶
llm_logger.py(セクション3.2)を実行してログを記録します。
ステップ3: ログからの再実行¶
次のセクション7のスクリプトを使ってログを再実行します。
7. 演習課題¶
評価対象課題です。 締め切りと提出方法は授業内で別途案内します。
課題内容¶
第07回で作成した geospatial_command_generator.py に、ログ記録と再実行機能を追加してください。
必須成果物¶
ログから同一プロンプトを再実行するPythonスクリプト(replay_from_log.py)
以下の機能を実装してください:
#!/usr/bin/env python3
"""
replay_from_log.py
JSONLログから指定したエントリを再実行し、結果を比較するスクリプト
使用方法:
python replay_from_log.py --log logs/llm_requests.jsonl --task-id reproject_utm54n_001
"""
import json
import argparse
import anthropic
from pathlib import Path
def load_log_entry(log_file: str, task_id: str) -> dict | None:
"""
JSONLログファイルから指定のtask_idのエントリを検索して返す。
Args:
log_file: JSONLログファイルのパス
task_id: 検索するタスクID
Returns:
マッチしたログエントリ、見つからない場合は None
"""
with open(log_file, encoding="utf-8") as f:
for line in f:
entry = json.loads(line.strip())
if entry.get("task_id") == task_id:
return entry
return None
def replay_request(entry: dict) -> str:
"""
ログエントリの内容を使って、まったく同じリクエストを再送信する。
Args:
entry: JSONLログの1エントリ(辞書型)
Returns:
新しいレスポンスのテキスト
"""
client = anthropic.Anthropic()
# 元のリクエストパラメータを復元
response = client.messages.create(
model=entry["model"],
max_tokens=entry["max_tokens"],
temperature=entry["temperature"],
system=entry["request"]["system"],
messages=entry["request"]["messages"]
)
return response.content[0].text.strip()
def compare_responses(original: str, replayed: str) -> None:
"""元の出力と再実行の出力を比較して表示する"""
print("\n=== 元の出力 ===")
print(original)
print("\n=== 再実行の出力 ===")
print(replayed)
print("\n=== 比較結果 ===")
if original.strip() == replayed.strip():
print("完全一致: 再現性が確認されました")
else:
# 差分を表示
import difflib
diff = list(difflib.unified_diff(
original.splitlines(),
replayed.splitlines(),
lineterm="",
fromfile="元の出力",
tofile="再実行"
))
if diff:
print("差分あり:")
print("\n".join(diff))
else:
print("空白の違いのみ(実質的に一致)")
def main():
parser = argparse.ArgumentParser(description="JSONLログからLLMリクエストを再実行する")
parser.add_argument("--log", required=True, help="JSONLログファイルのパス")
parser.add_argument("--task-id", required=True, help="再実行するタスクID")
args = parser.parse_args()
# ログエントリを検索
entry = load_log_entry(args.log, args.task_id)
if entry is None:
print(f"エラー: task_id='{args.task_id}' が見つかりません")
return
print(f"ログエントリを発見:")
print(f" タイムスタンプ: {entry['timestamp']}")
print(f" モデル: {entry['model']}")
print(f" temperature: {entry['temperature']}")
print(f" プロンプトバージョン: {entry['prompt_version']}")
print(f" プロンプトハッシュ: {entry['prompt_hash']}")
# 元の出力を取得
original_output = entry["response"]["content"][0]["text"]
# 再実行
print("\nリクエストを再実行中...")
replayed_output = replay_request(entry)
# 比較
compare_responses(original_output, replayed_output)
if __name__ == "__main__":
main()
提出物一覧¶
| ファイル名 | 説明 |
|---|---|
replay_from_log.py |
ログ再実行スクリプト(上記実装) |
llm_logger.py |
ログ記録付きAPI呼び出しモジュール |
logs/llm_requests.jsonl |
実際に記録したJSONLログ(最低3件のエントリ) |
.prompts/v1/system_gdal.txt |
バージョン管理されたシステムプロンプト |
report.md |
考察(temperature=0 での再現性の確認結果、ログ管理の工夫など)200〜300字 |
採点基準¶
| 項目 | 配点 |
|---|---|
replay_from_log.py が正しく動作する(task_id でエントリを検索・再実行) |
30点 |
llm_logger.py でJSONLログが正しく記録されている |
25点 |
| JSONLログに必須フィールド(timestamp, prompt_hash, model, temperature, request, response)が含まれる | 20点 |
| プロンプトファイルがGit管理されている(コミット履歴あり) | 15点 |
| 考察(再現性の観察、temperature の効果の考察) | 10点 |
実行例¶
# ログを記録しながらAPIを呼び出す
python llm_logger.py
# ログから再実行する
python replay_from_log.py \
--log logs/llm_requests.jsonl \
--task-id reproject_utm54n_001
# 期待される出力:
# ログエントリを発見:
# タイムスタンプ: 2025-10-08T10:00:00Z
# モデル: claude-opus-4-5
# temperature: 0
# ...
# === 比較結果 ===
# 完全一致: 再現性が確認されました
まとめ¶
- LLMの出力は確率的なサンプリングに基づいており、デフォルトでは同じプロンプトでも異なる出力が返ることがある
temperature=0を設定することで、出力を最も決定論的にできる(ただし完全な保証はない)- Anthropic Claude APIには
seedパラメータはないが、temperature=0とプロンプトの固定が再現性確保の主な手段 - プロンプトは
.prompts/ディレクトリに保存し、Gitでバージョン管理することで変更履歴を追跡できる - JSONLフォーマットでリクエスト・レスポンスを記録することで、後から同じ処理を再実行・比較できる
metadataフィールドを使うとリクエストに追加情報を付与でき、ログの管理が容易になる- 分析レポートには、使用したモデル名・プロンプトバージョン・temperature 値を必ず記載する
後期まとめ¶
第05回〜第08回では以下のことを学びました:
- 第05回:KDEによるポイントデータの密度推定とヒートマップ化
- 第06回:Tippecanoeを使った大規模地理データのベクトルタイル変換
- 第07回:プロンプトエンジニアリングによるGDALコマンド生成精度の改善
- 第08回:バージョン管理とログ記録によるLLM分析の再現性確保
これらの技術を組み合わせることで、地理空間情報とLLMを科学的・再現可能な形で活用できるようになります。
ビッグデータ応用Ⅱ〔特徴量エンジニアリング〕 第08回 © 小野原彩香