0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

AIエージェントのトークン消費を抑えるために、CIエラーログをCircleCI CLIで最適化する

0
Posted at

CI が失敗したとき、CircleCI の Web UI でパイプライン、ワークフロー、ジョブと画面をたどり、失敗したステップのログをコピーして AI エージェントに貼り付ける、という手順を踏んでいる方は多いはずです。この方法には 2 つの問題があります。画面をたどる手間がかかることと、コピーしたログに依存パッケージのインストール進捗やキャッシュの復元結果といった、原因分析に不要な出力が大量に含まれることです。

スクリーンショット: CircleCI の Web UI でジョブのログを表示した画面。コピー対象となるログ全体を示す

不要な出力は、AI エージェントのコンテキストウィンドウをそのまま消費します。CircleCI CLI に追加された --failure-report フラグは、失敗したステップだけを、AI エージェントが読むことを前提とした形式で出力します。

実際に 2 種類の失敗 run に対して、ログの渡し方を 3 通り用意し、それぞれのトークン数を計測しました。削減率は 55.3% と 87.8% で、失敗の種類によって大きく変わりました。

前提条件

--failure-report は CircleCI CLI の v1 系で利用できます。v1 系はプレビューリリース(preview (v1.x) CLI)であり、リポジトリの README には安定版までにコマンドとフラグが変更される可能性が明記されています。CLI は circleci.com と CircleCI Server の利用者が対象で、macOS、Windows、Linux をサポートします。

検証に使用した環境は次のとおりです。

項目
CLI バージョン circleci 1.0.46087-pre (0560a043ef86)
検証日 2026 年 8 月 3 日
OS / アーキテクチャ macOS / arm64(Homebrew: /opt/homebrew
対象プロジェクト gh/circleci/circleci-docs

出力を AI エージェントにパイプする例では、claude コマンドを使用します。これは Anthropic の Claude Code の CLI で、利用には Claude.ai または Claude Console のアカウントが必要です。インストール方法は公式ドキュメントを参照してください。CircleCI CLI 側の機能は claude に依存しないため、他の AI エージェントに渡す場合はパイプ先を差し替えるだけで動作します。

プレビュー版のため、記事内のコマンドやフラグ名は将来変更される可能性があります。最新の仕様は CLI リファレンス を確認してください。

--failure-report を実行する

Homebrew で CLI の v1 系をインストールします。

brew install circleci-public/circleci/circleci@next

安定版の circleci が既に入っている場合、バイナリが衝突するため、README では brew unlink circleci を事前に実行しましょう。
環境によって挙動が異なる可能性があるため、シンボリックリンクのエラーが出た場合は README の案内に従ってください。

https://github.com/CircleCI-Public/circleci-cli#homebrew

インストール後、CircleCI への認証を行います。このコマンドはブラウザを開いて OAuth の認可画面に遷移し、取得したトークンを OS のキーリングに保存します。

circleci auth login

認証が完了したら、プロジェクトのディレクトリに移動して、引数なしで実行します。このコマンドは、カレントディレクトリの git remote とチェックアウト中のブランチから最新の run を解決するため、run の ID を調べる必要がありません。

circleci run get --failure-report

CI を実行したリポジトリで作業している状況では、これが最短の呼び出しになります。過去の特定の run を対象にする場合は、run の ID を引数として渡します。

circleci run get 069e1b5c-2d77-4478-989c-c8980829f5b9 --failure-report

出力は、失敗したワークフロー、ジョブ、ステップの階層に沿った形式になります。ドキュメントリポジトリの Vale lint が失敗した run では、次のような出力が得られました。

## workflow: main

### job: vale/lint

#### step 102: Lint files [exit: 1]

Installing git...
(1/7) Installing libunistring (1.4.2-r0)pine...]
(2/7) Installing libidn2 (2.3.8-r0)
... (51 lines omitted)
--- error context ---


docs/server-admin-4.8/modules/installation/pages/upgrade-server.adoc
7:54    error       Use '\bCircleCI Server\b' instead of 'CircleCI server'.    Vale.Terms
25:29   error       Try to avoid using 'should'.                               circleci-docs.Avoid

✖ 2 errors, 10 warnings and 47 suggestions in 4 files.

ログが長くなるため、一部抜粋とさせていただいています。
実際の出力には、警告と提案のレベルの指摘行も含まれます。

見出しにステップ番号(step 102)と終了コード(exit: 1)が入るため、どのステップがどう失敗したかを追加のコマンドなしで判別できます。... (51 lines omitted) は CLI 自身が省略した行数を示しており、その下の --- error context --- 以降にエラーの本体が配置されます。

CIエラーのログを、Claude Codeに直接渡す

この出力を AI エージェントに渡す場合は、標準出力をそのままパイプします。--failure-report はプレーンテキストを出力するため、間に整形処理を挟む必要はありません。

circleci run get --failure-report | claude

run の ID を省略すると、直近の run を選択するピッカーが起動します。パイプやスクリプトで使う場合は --no-interactive で回避します。

circleci run get --failure-report --no-interactive

このフラグを付けると、ピッカーを表示せずに最新の run を直接解決します。対象を明示的に指定するオプションも用意されています。

オプション 内容
--project gh/org/repo git 管理下でないディレクトリからプロジェクトを指定する
--branch main ブランチを指定する(既定はカレントブランチ。--project 指定時は main
--mine 自分がトリガーした run に絞る
--no-interactive ピッカーを省略して最新の run を解決する

gitコマンドを組み合わせることで、例えば「今いるブランチの失敗ログを取得する」コマンドも作れます。

% circleci run get --failure-report --no-interactive --branch $(git branch --show-current)

トークン数を実測する

削減効果を確認するため、同一の run に対して 3 通りの渡し方を用意しました。

渡し方 内容
ジョブ全体のログ 失敗ジョブの全ステップのログ全文。ジョブをブラウザで開いて全体をコピーした状態に相当します
失敗ステップの生ログ 失敗したステップだけを切り出した、加工していないログ
--failure-report の出力 circleci run get <run-id> --failure-report の出力

前者 2 つは CLI のステップ出力取得コマンドで用意します。ジョブ全体のログは 1 コマンドでは取得できないため、ステップ番号を列挙して全ステップ分を連結します。

JOB_ID="<job-uuid>"

# ジョブ全体のログ
for step in $(circleci job output list "$JOB_ID" | grep '^##' | awk '{print $1}' | tr -d ':'); do
  echo "===== STEP $step =====" >> job-full.txt
  circleci job output get "$JOB_ID" --step-num "$step" >> job-full.txt
done

# 失敗ステップの生ログ
circleci job output get "$JOB_ID" --step-num 102 > step-raw.txt

# failure-report の出力
circleci run get 069e1b5c-2d77-4478-989c-c8980829f5b9 --failure-report > failure-report.txt

JOB_ID に渡すジョブの UUID は circleci run get --json の出力から取得できます。--step-num に指定する番号(上記では 102)は、--failure-report の出力の見出し(#### step 102)と対応しているため、失敗ステップの番号を探す手間はかかりません。

トークン数の計測には Anthropic の count_tokens API を使用しました。文字数ではなくトークン数で測る理由は、AI エージェントが実際に消費するのはトークンであり、日本語と英語が混在するログでは文字数とトークン数が比例しないためです。

import json, os, sys, urllib.request

def count_tokens(text):
    req = urllib.request.Request(
        "https://api.anthropic.com/v1/messages/count_tokens",
        data=json.dumps({
            "model": "claude-sonnet-4-6",
            "messages": [{"role": "user", "content": text}],
        }).encode(),
        headers={
            "content-type": "application/json",
            "x-api-key": os.environ["ANTHROPIC_API_KEY"],
            "anthropic-version": "2023-06-01",
        },
    )
    with urllib.request.urlopen(req) as res:
        return json.loads(res.read())["input_tokens"]

results = []
for path in sys.argv[1:]:
    text = open(path, encoding="utf-8").read()
    results.append((path, text.count("\n") + 1, len(text), count_tokens(text)))

baseline = max(r[3] for r in results)

print("| 渡し方 | 行数 | 文字数 | トークン数 | 削減率 |")
print("|---|---|---|---|---|")
for path, lines, chars, tokens in sorted(results, key=lambda r: -r[3]):
    cut = "—(基準)" if tokens == baseline else f"{100 - tokens / baseline * 100:.1f}%"
    print(f"| {path} | {lines:,} | {chars:,} | {tokens:,} | {cut} |")

計測対象のファイルを引数に渡して実行すると、最もトークン数の多いファイルを基準にした比較表が Markdown 形式で出力されます。ANTHROPIC_API_KEY は環境変数から読み込むため、スクリプト内にキーを記述する必要はありません。

計測対象には、出力の性質が対照的な 2 種類の失敗を選びました。1 つは違反内容そのものが出力の大半を占めるジョブ、もう 1 つは処理の進捗ログが出力の大半を占めるジョブです。

Vale lint 違反の場合

1 件目は、ドキュメントの文体チェック(Vale)が失敗した run です。4 ファイルに対して 2 errors、10 warnings、47 suggestions が報告されました。違反の一覧そのものが出力の中心を占めるケースです。

渡し方 行数 文字数 トークン数 削減率
ジョブ全体のログ 172 15,759 4,280 —(基準)
失敗ステップの生ログ 92 12,254 2,929 31.6%
--failure-report の出力 63 8,380 1,914 55.3%

外部リンク検証失敗の場合

2 件目は、htmltest による外部リンク検証が失敗した run です。509 ドキュメントに対して 130 errors が報告されました。このジョブは Go モジュールのダウンロード出力が繰り返し記録されるため、失敗の原因と無関係な行が大量に含まれます。

渡し方 行数 文字数 トークン数 削減率
ジョブ全体のログ 528 40,626 13,544 —(基準)
失敗ステップの生ログ 215 23,954 7,923 41.5%
--failure-report の出力 59 5,273 1,656 87.8%

結果の読み方

2 件の計測から 3 点が確認できます。

1 つ目は、削減率が失敗の種類によって大きく変わることです。Vale lint では 55.3%、リンク検証では 87.8% でした。リンク検証のジョブは繰り返しのダウンロード出力を含むため圧縮の余地が大きく、Vale lint は違反内容が出力の大半を占めるため圧縮の余地が小さくなります。自分のプロジェクトでどの程度削減されるかは、ジョブの出力の性質によって決まります。

2 つ目は、今回計測した 2 件ではいずれも、失敗ステップの生ログよりも --failure-report の出力の方がトークン数が少なかったことです。失敗ステップだけを手作業で切り出しても、--failure-report と同じ水準には届きませんでした。ステップ内のセットアップ出力が残るためです。

3 つ目は、削減量を絶対値で見ると、Vale lint で 2,366 トークン、リンク検証で 11,888 トークンだという点です。20 万トークンのコンテキストウィンドウを前提にすれば、1 回のやり取りで見た削減量は大きくありません。削減率の大きさと絶対量の小ささは、同じ計測結果から同時に読み取れます。

何が省略され、何が残るのか

圧縮されていることと、原因分析に必要な情報が残っていることは別の問題です。... (N lines omitted) が何を落としているかを確認しました。

Vale lint の run で、出力に含まれるものと含まれないものは次のように分かれていました。

内容 --- error context --- に含まれるか
エラーサマリー(✖ 2 errors... 含まれる
error レベルの違反を含むファイルの違反内容 含まれる
suggestion のみのファイルの違反内容 含まれない
大きなファイルの先頭付近の違反 含まれない(末尾寄りが優先される)
vale sync などのセットアップ出力 含まれない

省略の対象になっていたのは、依存パッケージのインストール進捗やツールの初期化出力といった、失敗の原因と直接関係しない部分でした。error レベルの違反はすべて保持されていました。リンク検証の run でも同様に、Go モジュールのダウンロード出力が ... (154 lines omitted) として圧縮され、リンクエラーの本体とサマリー(130 errors in 509 documents)は保持されていました。

エラーの本体が保持されるため、失敗原因の調査という用途では問題なく使えます。一方で、suggestion レベルの指摘まで含めて AI エージェントに修正させたい場合は、--failure-report の出力では情報が足りません。用途に応じて生ログを併用する判断が必要になります。

実際にパイプした結果も確認しました。claude-p オプションを渡すと、対話モードに入らずに単発の指示として処理されます。

circleci run get 069e1b5c-2d77-4478-989c-c8980829f5b9 --failure-report \
  | claude -p "このCI失敗の原因を1文で要約してください。日本語で。" --max-turns 1

返ってきた応答は次のとおりです。

Vale側のgitパッケージインストール処理が原因ではなく、docs/server-admin-4.8docs/server-admin-4.10upgrade-server.adoc内の用語・文体規約違反(Valeのlintエラー2件、警告・提案多数)によりlintジョブが失敗しています。

出力の冒頭に git のインストール進捗が残っているにもかかわらず、それが原因ではないと判断し、Vale 違反を原因として特定しています。

使うにあたっての注意点

試す中でいくつかはまったポイントがありました。ここからは使う際の注意点について紹介します。

--json および --jq とは併用できない

CircleCI CLI の v1 系はすべてのデータ取得コマンドで --json をサポートしていますが、--failure-report との併用はできません。実行すると次のエラーが返ります。

{
  "error": true,
  "code": "run.failure_report_no_json",
  "message": "--failure-report prints plain-text output for agent consumption and does not support JSON formatting.",
  "exit_code": 2
}

--jq を併用した場合も同じメッセージが返ります。--failure-report の出力を jq でさらに絞り込む使い方はできないため、加工が必要な場合はパイプ先の側で処理してください。

circleci run get --json --jq 自体は --failure-report なしで利用できますが、こちらが返すのはワークフローとジョブの状態です。失敗したジョブの名前を取得する用途には使えますが、失敗したステップのログは含まれません。

失敗したジョブ名やジョブIDだけを取得する場合は、次のように書けます。

% circleci run get --no-interactive --branch $(git branch --show-current) --json --jq '.workflows[].jobs[] | select(.outcome != "success").name, .id'
build_pr
93b38498-cfaf-4c10-a76d-68fe2ddbb088

--jq に渡す式は CLI に組み込まれた jq で評価されるため、jq を別途インストールする必要はありません。

成功した run では出力が空になる

--failure-report は失敗したステップを出力するコマンドのため、対象の run が成功している場合、出力は空になります。

$ circleci run get a130c924-bb98-490b-8305-5b8479c02dc6 --failure-report
$

引数なしで実行した場合も、最新の run が失敗していなければ同じ結果になります。

まとめ

circleci run get --failure-report は、失敗したステップだけを AI エージェント向けの形式で出力する CLI フラグです。ドキュメントリポジトリの 2 種類の失敗 run で計測した結果、ジョブのログ全文と比較して 55.3% と 87.8% のトークン削減が確認できました。省略の対象になるのはセットアップ出力やダウンロード進捗であり、error レベルの内容とサマリーは保持されるため、失敗原因の調査という用途では生ログを渡す必要はありません。

フラグの詳細は CircleCI Discuss のアナウンスCLI リファレンス を参照してください。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?