連載: AIに仕事を奪われる不安から始めるハーネス作成入門
第14回 ← 前回(第13回): AIハーネスにログ設計が必要な理由
はじめに:ログ項目は決めた、次は実装
前回(第13回)で、AIハーネスに必要なログ項目表を設計しました。今回は、それを実際のファイルとして残すためのMarkdownテンプレートとJSONスキーマを作成します。
「何を記録するかは決めたけど、実際にどうファイルに書き出せばいいのか」という疑問に、具体的なコード例とともに答えるのが本記事の目的です。
MarkdownとJSONを併用する理由
ログの保存形式にはMarkdownとJSONを併用するアプローチを提案します。それぞれの役割は次の通りです。
| 形式 | 主な用途 | 対象者 |
|---|---|---|
| Markdown | 手動レビュー、共有、報告 | 人間(開発者・管理者) |
| JSON | 自動集計、検索、ダッシュボード | プログラム(分析スクリプト) |
SE業務でたとえると、Markdownは「障害報告書」、JSONは「ログDBのレコード」に相当します。同じ実行データを異なるビューで見られるようにすることで、運用の柔軟性が大きく向上します。
Markdownログテンプレート(成果物①)
以下が、AIエージェント実行ログのMarkdownテンプレートです。
# AIエージェント実行ログ
## 基本情報
| 項目 | 値 |
|---|---|
| 実行ID | exec_20240101_001 |
| 開始時刻 | 2024-01-01T10:00:00+09:00 |
| 終了時刻 | 2024-01-01T10:00:15+09:00 |
| ステータス | success |
| モデル | gpt-4o |
| トークン数(入力) | 450 |
| トークン数(出力) | 320 |
## プロンプト
以下の要件でコードレビューを実施してください。
- セキュリティ観点でのチェック
- パフォーマンスの懸念点指摘
## コンテキスト(RAG取得)
- ソース: coding_guidelines_v2.md
- 関連度: 0.85
## モデル応答
(応答内容)
## ツール呼び出し
なし
## 備考
特記事項なし
テンプレートの設計ポイント
- 基本情報を表形式でまとめる: 一覧性を確保
- プロンプトと応答を分離: 入出力の対応を明確に
- コンテキスト情報を含める: RAGで何を参照したかの追跡性
- ツール呼び出しを記録: MCPツールの利用履歴
JSONスキーマ(成果物②)
以下が、同じ実行データをJSONで表現したスキーマです。
{
"execution_id": "exec_20240101_001",
"timestamp": {
"start": "2024-01-01T10:00:00+09:00",
"end": "2024-01-01T10:00:15+09:00"
},
"status": "success",
"model": {
"name": "gpt-4o",
"version": "2024-01-01"
},
"tokens": {
"input": 450,
"output": 320,
"total": 770
},
"prompt": "以下の要件でコードレビューを実施...",
"context": [
{
"source": "coding_guidelines_v2.md",
"relevance_score": 0.85
}
],
"response": "モデルの応答内容...",
"tool_calls": [],
"metadata": {
"trigger": "manual",
"user_id": "developer_01",
"environment": "development"
}
}
JSONスキーマの設計ポイント
-
フラットよりネスト構造:
timestamp.startとtimestamp.endのように関連項目をグルーピング -
配列で可変長データに対応:
contextやtool_callsは配列で複数件に対応 - metadataで拡張性を確保: 後から項目を追加しやすい設計
Python実装例:ログ出力ユーティリティ
以下に、MarkdownとJSONの両方を出力するPythonコード例を示します。
import json
from datetime import datetime
from pathlib import Path
from dataclasses import dataclass, asdict
from typing import Optional
@dataclass
class ExecutionLog:
"""AIエージェント実行ログのデータクラス"""
execution_id: str
start_at: str
end_at: str
status: str
model_name: str
tokens_input: int
tokens_output: int
prompt: str
response: str
tool_calls: list
context_sources: list
trigger: str = "manual"
user_id: str = ""
notes: str = ""
def to_json(self) -> str:
"""JSON形式で出力"""
data = {
"execution_id": self.execution_id,
"timestamp": {
"start": self.start_at,
"end": self.end_at,
},
"status": self.status,
"model": {"name": self.model_name},
"tokens": {
"input": self.tokens_input,
"output": self.tokens_output,
"total": self.tokens_input + self.tokens_output,
},
"prompt": self.prompt,
"response": self.response,
"tool_calls": self.tool_calls,
"context": self.context_sources,
"metadata": {
"trigger": self.trigger,
"user_id": self.user_id,
},
}
return json.dumps(data, ensure_ascii=False, indent=2)
def to_markdown(self) -> str:
"""Markdown形式で出力"""
total = self.tokens_input + self.tokens_output
lines = [
"# AIエージェント実行ログ",
"",
"## 基本情報",
"",
"| 項目 | 値 |",
"|---|---|",
f"| 実行ID | {self.execution_id} |",
f"| 開始時刻 | {self.start_at} |",
f"| 終了時刻 | {self.end_at} |",
f"| ステータス | {self.status} |",
f"| モデル | {self.model_name} |",
f"| トークン数 | {total} ({self.tokens_input} + {self.tokens_output}) |",
"",
"## プロンプト",
"",
self.prompt,
"",
"## モデル応答",
"",
self.response,
"",
"## 備考",
"",
self.notes if self.notes else "特記事項なし",
]
return "\n".join(lines)
def save_log(
log: ExecutionLog,
output_dir: str = "./logs"
) -> None:
"""MarkdownとJSONの両方を保存"""
path = Path(output_dir)
path.mkdir(parents=True, exist_ok=True)
# JSON保存
json_path = path / f"{log.execution_id}.json"
json_path.write_text(log.to_json(), encoding="utf-8")
# Markdown保存
md_path = path / f"{log.execution_id}.md"
md_path.write_text(log.to_markdown(), encoding="utf-8")
コードの設計意図
- dataclassでデータ構造を明確化: ログ項目のスキーマを型で表現
- to_jsonとto_markdownを分離: 用途に応じた出力形式の切り替え
-
save_logで両方同時保存: 保存漏れを防止
このコードは、そのまま自分のプロジェクトにコピーして使うことができます。
ファイル構成の提案
ログファイルの配置について、以下の構成を提案します。
logs/
├── 2024-01/
│ ├── exec_20240101_001.json
│ ├── exec_20240101_001.md
│ ├── exec_20240101_002.json
│ └── exec_20240101_002.md
├── 2024-02/
│ └── ...
└── index.json ← 検索用インデックス
設計の判断基準
| 観点 | 判断基準 |
|---|---|
| ディレクトリ分割 | 月単位(ファイル数が1000件/月を超えるなら日単位) |
| ファイル名 | 実行IDをそのまま使用(一意性保証) |
| インデックス | 集計・検索用に別途作成(大規模運用時) |
MarkdownとJSONの使い分けガイド
運用シーンごとの使い分けを整理します。
| シーン | 使う形式 | 理由 |
|---|---|---|
| 障害対応中の確認 | Markdown | 人が素早く読める |
| 月次コスト集計 | JSON | プログラムで自動化 |
| 品質トレンド分析 | JSON | 統計処理に適する |
| チームへの報告 | Markdown | そのまま共有可能 |
| 監査対応 | 両方 | JSONで証跡、Markdownで説明 |
ログローテーションの考え方
AIエージェントのログは、従来システムのログよりもサイズが大きくなりがちです(プロンプトと応答の全文を含むため)。ローテーションの判断基準を示します。
| レベル | 保存期間の目安 | 考慮点 |
|---|---|---|
| DEBUG(全文) | 7日 | ストレージコストとのバランス |
| INFO(要約) | 90日 | トレンド分析に必要な期間 |
| WARN/ERROR | 1年 | 監査・コンプライアンス要件 |
これも、従来システムのログローテーションと同じ考え方です。あなたの経験がそのまま活きます。
実装時の注意点
機密情報の取り扱い
プロンプトや応答に機密情報が含まれる場合、ログにそのまま記録するとセキュリティリスクになります。第19回で詳しく扱いますが、現時点では「機密情報をマスクしてからログに書く」という原則を意識してください。
ファイルサイズの制御
プロンプトと応答の全文を含めると、ログファイルが大きくなりがちです。INFOレベルではプロンプトの先頭200文字とトークン数のみを記録するなど、レベルに応じた情報量の制御が重要です。
SE経験者が追加できる工夫
既存のログ運用経験から、以下の工夫が考えられます。
- アラート設定: トークン消費が閾値を超えたら通知(監視システムと同じ)
- ダッシュボード: JSONログを集計して日次レポートを生成(BIツールの経験が活きる)
- ログ相関分析: 複数ログを突き合わせて問題パターンを発見(障害分析のスキル)
まとめ
- Markdownは人が読むため、JSONはプログラムが処理するために併用
- dataclassでログスキーマを定義し、両形式に変換する実装がシンプル
- ファイル構成、ローテーション、セキュリティは従来システムの知見をそのまま活用
- まずは小さく始め、運用しながら拡張する
次回予告:Human-in-the-loopはどこに入れるべきか
第15回では、AIエージェントのワークフローに人間の承認ポイント(Human-in-the-loop)をどこに配置するかを設計します。
具体的には以下の内容を扱います。
- AIエージェントの処理フローにおける承認ポイントの配置基準
- リスクレベルに応じた承認の粒度設計
- 今回作成したログと承認フローの連携
- 承認フローの実装パターン
「ログで記録するだけでなく、重要な処理の前に人間がチェックする仕組みがあれば、もっと安心できるのに」という考えに答える回です。ぜひお楽しみに。
連載: AIに仕事を奪われる不安から始めるハーネス作成入門
次回(第15回): Human-in-the-loopはどこに入れるべきか