前回の記事で Evals の概念を書いた。今回は実装の話。
OpenAI の記事を読んで「で、どうやって書くの?」となった人向け。
なぜ Cucumber か
Evals のテストケースは普通 CSV で書く:
id,should_trigger,prompt
test-01,true,"一覧を表示して"
test-02,false,"README を要約して"
問題: should_trigger が何を意味するかコンテキストがないと分からない。
Cucumber だとこうなる:
シナリオ: 一覧表示のリクエスト
前提 プロンプト:
"""
一覧を表示して
"""
もし Codex でプロンプトを実行する
ならば list コマンドが実行されている
シナリオ: README 要約では list を実行しない
前提 プロンプト:
"""
README を要約して
"""
もし Codex でプロンプトを実行する
ならば list コマンドが実行されていない
何をテストしてるか一目で分かる。
日本語で書ける
Cucumber は多言語対応してる。ファイル先頭に # language: ja を書くだけ:
# language: ja
機能: doc-fetcher evals
doc-fetcher の list 実行が必要なプロンプトでは実行し、
不要なプロンプトでは実行しないことを検証する。
シナリオ: 追跡中 URL の一覧を表示する
前提 プロンプト:
"""
doc-fetcher で追跡中の URL 一覧を表示して
"""
もし Codex でプロンプトを実行する
ならば list コマンドが実行されている
Given/When/Then が「前提/もし/ならば」になる。
Step Definitions
裏側の実装。Ruby で書く。
Given(/^プロンプト:$/) do |doc_string|
@prompt = doc_string.to_s.strip
end
When('Codex でプロンプトを実行する') do
# Codex CLI を --json モードで実行
stdout, stderr, status = Open3.capture3(
'codex', 'exec', '--json', '--full-auto', @prompt
)
# 実行ログを保存(後で見返せる)
@run_path = "evals/runs/#{@eval_id}.jsonl"
File.write(@run_path, stdout)
@codex_status = status
end
ポイントは codex exec --json。これで実行トレースが JSONL で取れる。
コマンド実行の検出
JSONL から command_execution イベントを抽出する:
def extract_commands(run_path)
commands = []
File.foreach(run_path) do |line|
event = JSON.parse(line)
item = event['item']
next unless item['type'] == 'command_execution'
next unless item['status'] == 'completed'
commands << item['command']
end
commands
end
あとは正規表現でチェック:
LIST_CMD_RE = %r{doc_fetcher\.rb\s+list}i
Then('list コマンドが実行されている') do
commands = extract_commands(@run_path)
list_cmds = commands.select { |cmd| cmd.match?(LIST_CMD_RE) }
raise 'list command not found' if list_cmds.empty?
end
Then('list コマンドが実行されていない') do
commands = extract_commands(@run_path)
list_cmds = commands.select { |cmd| cmd.match?(LIST_CMD_RE) }
raise 'unexpected list command' unless list_cmds.empty?
end
実行
bundle exec cucumber features/doc_fetcher_evals.feature
失敗したシナリオがあれば、evals/runs/ に JSONL が残ってるから何が起きたか追える。
positive と negative
両方書く。これ大事。
positive: このプロンプトでは list を実行すべき
シナリオ: 追跡中 URL の一覧を表示する
...
ならば list コマンドが実行されている
negative: このプロンプトでは list を実行すべきでない
シナリオ: README 要約では list を実行しない
...
ならば list コマンドが実行されていない
negative がないと、何でも list 実行するようになっても気づけない。
まとめ
- CSV より Cucumber の方が読みやすい
# language: jaで日本語で書けるcodex exec --jsonで実行トレースを取る- JSONL から
command_executionを抽出してチェック - positive と negative 両方書く
概念は OpenAI の記事で、実装はこのパターンで。
参考: