1
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?

Python で AI エージェントの履歴削減による情報欠落を検証する

1
Posted at

AI エージェントの実行履歴には、ユーザーの制約、変更対象、エラーの原因、テスト結果などが蓄積します。コンテキストの容量を制限するとき、古い履歴から削除する方法では、直近の操作記録を残しながら、作業を拘束する指示を失う場合があります。

この記事では、架空の実行履歴 12 件を使い、新しい履歴を優先する方式 と 必須情報を優先する方式 を同じ文字数上限で比較します。Python だけで、削減率、必須情報保持率、原文一致率を測定します。選択対象の違いが情報保持に与える影響を、実行可能なコードと単体テストで確認することが目的です。

履歴を短くすると必要な情報まで消える

例えば、作業の開始時に「本番環境への書き込みは担当者の承認後に実行する」と指定され、その後に調査結果やテスト結果が追加されたとします。末尾の履歴を優先する削減では、直近のテスト成功は残っても、最初に確定した承認条件が消えることがあります。

資源削減では、削減量に加えて、どの情報を残したかが後続処理に影響します。この観点は、計算、学習、コンテキストの資源配置を比較した原典でも整理されています[1]。

CliffCompaction は、長時間動作するコーディングエージェントの履歴圧縮において、保持する情報の原文を維持し、過去の要約を繰り返し要約する際の変形を抑える設計を報告しています[2]。本稿では、そのうち「原文のまま残す情報を選ぶ」という性質を独立した小規模実験に取り出します。公式実装の再現や、論文の性能数値の追試とは異なる検証です。

実行環境と検証条件

Python 3.11 以上と標準ライブラリを使用します。実際にコードと単体テストを実行した環境は Python 3.13.5 です。外部サービス、GPU、追加のパッケージは使用しません。

python3 --version

次の 2 ファイルを同じディレクトリに配置します。

context_retention.py
test_context_retention.py

履歴は古い順に並び、id は時系列順に増える整数とします。容量は Python の len() で測定する文字数とし、レコード間の改行 1 文字も加算します。これは API が消費するトークン数とは別の指標です。

エージェントの実行履歴を用意する

次の Record が履歴の 1 件を表します。required=True は、後続処理に必要だとあらかじめ指定した情報です。12 件中 4 件に設定します。

context_retention.py は、履歴データ、二つの削減方式、評価指標、結果表示をまとめた実行可能なスクリプトです。

context_retention.py
from dataclasses import dataclass


@dataclass(frozen=True)
class Record:
    id: int
    text: str
    required: bool = False


HISTORY = [
    Record(1, "本番環境への書き込みは担当者の承認後に実行する", True),
    Record(2, "リポジトリの構成を調査し、主要なディレクトリを列挙した"),
    Record(3, "作業対象は設定ファイル一つに限定する", True),
    Record(4, "既存のテスト一覧を取得した"),
    Record(5, "依存ライブラリのバージョンを確認した"),
    Record(6, "既存のエラーメッセージの文字列は変更しない", True),
    Record(7, "検証用の一時ディレクトリを作成した"),
    Record(8, "一度目のテストでは想定と異なる終了コードが返った"),
    Record(9, "検証は外部サービスへの通信を使わずローカルで完結させる", True),
    Record(10, "失敗の原因をテスト入力の順序に絞って確認した"),
    Record(11, "修正後の単体テストがすべて成功した"),
    Record(12, "最終的に変更ファイルの一覧を取得した"),
]


def length(records: list[Record]) -> int:
    return len("\n".join(record.text for record in records))


def recent_first(records: list[Record], capacity: int) -> list[Record]:
    selected: list[Record] = []
    for record in reversed(records):
        candidate = [record, *selected]
        if length(candidate) > capacity:
            break
        selected = candidate
    return selected


def required_first(records: list[Record], capacity: int) -> list[Record]:
    selected = [record for record in records if record.required]
    if length(selected) > capacity:
        raise ValueError("required records exceed capacity")

    for record in reversed(records):
        if record.required:
            continue
        candidate = sorted([*selected, record], key=lambda item: item.id)
        if length(candidate) > capacity:
            break
        selected = candidate
    return selected


def rates(original: list[Record], selected: list[Record]) -> tuple[float, float, float]:
    required_ids = {record.id for record in original if record.required}
    selected_ids = {record.id for record in selected}
    original_text = {record.id: record.text for record in original}
    reduction = 1 - length(selected) / length(original)
    retention = len(required_ids & selected_ids) / len(required_ids)
    fidelity = (
        sum(record.text == original_text[record.id] for record in selected) / len(selected)
        if selected else 1.0
    )
    return reduction, retention, fidelity


def main() -> None:
    total = length(HISTORY)
    required = [record for record in HISTORY if record.required]
    print(f"original={total} chars; required_min={length(required)} chars")
    print("budget  method    used  reduction  required  exact")
    for percentage in (100, 70, 50, 25):
        capacity = total * percentage // 100
        for label, selector in (("recent", recent_first), ("required", required_first)):
            try:
                selected = selector(HISTORY, capacity)
            except ValueError:
                print(f"{percentage:>3}%    {label:<8}  INFEASIBLE (required records exceed capacity)")
                continue
            reduction, retention, fidelity = rates(HISTORY, selected)
            print(
                f"{percentage:>3}%    {label:<8}  {length(selected):>4}  "
                f"{reduction:>8.1%}  {retention:>8.1%}  {fidelity:>5.1%}"
            )


if __name__ == "__main__":
    main()

新しい履歴から残す方式を実装する

recent_first() は、履歴を末尾から順に採用します。採用候補を加えた時点で上限を超える場合は停止し、残したレコードを元の時系列順で返します。

この方式は最新の操作状況を保持しやすい一方、古い required=True の指示も削除対象に入ります。今回の入力では、id=1 の承認条件と id=3 の変更対象制約が、時間の経過によって削除される可能性があります。

必須情報を優先して残す方式を実装する

required_first() は、最初に必須レコード全件の容量を確保します。残りの容量には、新しい通常レコードを順に追加します。保持する各レコードの文字列は変更せず、結果は元の時系列順で出力します。

必須レコードだけで容量を超える場合は ValueError を送出します。今回のデータでは、必須レコード 4 件とその間の改行を合わせて 92 文字 が最低容量です。選択方式によってこの下限を解消することはできません。

この方法の前提は、required の値が適切に設定されていることです。実運用では将来必要になる情報の識別自体が別の課題になります。

同じ容量で二つの方式を比較する

次のコマンドを実行します。

python3 -B context_retention.py

-B は Python のバイトコードキャッシュ生成を抑止する指定です。全履歴は改行を含めて 256 文字 です。上限をその 100%、70%、50%、25% とし、各方式で同じ容量を使います。

実測結果は次のとおりです。used は保持後の文字数、reduction は元履歴からの文字数削減率、required は必須情報保持率、exact は保持されたレコードの原文一致率です。

original=256 chars; required_min=92 chars
budget  method    used  reduction  required  exact
100%    recent     256      0.0%    100.0%  100.0%
100%    required   256      0.0%    100.0%  100.0%
 70%    recent     171     33.2%     50.0%  100.0%
 70%    required   177     30.9%    100.0%  100.0%
 50%    recent     112     56.2%     25.0%  100.0%
 50%    required   111     56.6%    100.0%  100.0%
 25%    recent      59     77.0%      0.0%  100.0%
 25%    required  INFEASIBLE (required records exceed capacity)

50% 条件では、単純な新しい履歴優先方式が 112 文字を残し、必須情報保持率は 25% でした。必須情報優先方式は 111 文字を残し、必須情報保持率は 100% でした。今回の履歴では、文字数がほぼ同じでも、残す位置によって必要な情報の保持状況が変わりました。

70% 条件でも、必須情報保持率は前者が 50%、後者が 100% です。この差は、あらかじめ必須情報を指定した今回の入力と削減規則から生じたものであり、一般的なエージェントの作業成功率を表す数値ではありません。

必須情報を保持できない境界を確認する

25% 条件の文字数上限は 256 * 25 // 100 = 64 文字です。必須情報だけで 92 文字が必要なため、required_first() は required records exceed capacity を報告します。

この場合、必要な情報すべての保持と 64 文字への削減は、同時には成立しません。必須情報を部分的に削る代わりに、容量不足を検出する設計にしています。

単体テストでは、容量超過の検出、保持できる場合の必須情報全件の保存、元の時系列順と原文の維持を確認します。

test_context_retention.py
import unittest

from context_retention import HISTORY, length, recent_first, required_first, rates


class RetentionTests(unittest.TestCase):
    def test_full_budget_keeps_all_records(self):
        for selector in (recent_first, required_first):
            self.assertEqual(selector(HISTORY, length(HISTORY)), HISTORY)

    def test_both_methods_respect_capacity(self):
        for percentage in (100, 70, 50, 25):
            capacity = length(HISTORY) * percentage // 100
            self.assertLessEqual(length(recent_first(HISTORY, capacity)), capacity)
            try:
                selected = required_first(HISTORY, capacity)
            except ValueError:
                self.assertLess(capacity, length([r for r in HISTORY if r.required]))
                continue
            self.assertLessEqual(length(selected), capacity)

    def test_required_records_are_preserved_when_feasible(self):
        required = [record for record in HISTORY if record.required]
        capacity = length(required)
        self.assertEqual(required_first(HISTORY, capacity), required)

    def test_insufficient_capacity_is_reported(self):
        required = [record for record in HISTORY if record.required]
        with self.assertRaisesRegex(ValueError, "required records exceed capacity"):
            required_first(HISTORY, length(required) - 1)

    def test_selection_keeps_original_order_and_text(self):
        for selector in (recent_first, required_first):
            records = selector(HISTORY, length(HISTORY) * 70 // 100)
            ids = [record.id for record in records]
            self.assertEqual(ids, sorted(ids))
            self.assertEqual(rates(HISTORY, records)[2], 1.0)

    def test_protected_selection_improves_required_retention(self):
        capacity = length(HISTORY) * 50 // 100
        recent = recent_first(HISTORY, capacity)
        protected = required_first(HISTORY, capacity)
        self.assertGreater(rates(HISTORY, protected)[1], rates(HISTORY, recent)[1])


if __name__ == "__main__":
    unittest.main()

次のコマンドで実行します。

python3 -B -m unittest -v test_context_retention.py

Python 3.13.5 では 6 件のテストが成功しました。

Ran 6 tests in 0.000s

OK

履歴削減の限界と留意事項

今回の原文一致率は、両方式とも保持したレコードの文字列を書き換えないため 100% になります。この値だけでは、削除された情報の量を評価できません。削減後に 必要なレコードが存在するか を、必須情報保持率として独立に測定します。

また、required は検証用データに固定した正解ラベルです。実際のエージェントでは、今後の操作によって新たに必要になる過去情報もあります。事前指定が不正確なら、必須情報優先方式でも判断材料を失います。

入力容量は文字数で測っています。実際の LLM では、トークン分割、メッセージの付帯情報、履歴を選ぶ処理の計算費用が別途関係します。API 費用とタスク成功率を評価する際は、対象モデルと実運用の入力形式で測定する必要があります。

CliffCompaction が扱う再要約による情報変形の問題について、今回測定したのは原文を保持する方式だけです。要約を繰り返した際の意味の変化や、長時間の開発作業での成功率はこの実験の評価対象に含まれません。

まとめ

12 件の履歴を同じ文字数上限で削減したところ、50% 条件では、最新履歴を優先する方式の必須情報保持率が 25%、必須情報を優先する方式が 100% になりました。一方、25% 条件では必須情報の合計 92 文字が容量 64 文字を超え、全件保持は実現できませんでした。

履歴の削減方針を決める際は、文字数削減率だけでなく、保持すべき指示や検証結果の残存状況を測定する必要があります。そのうえで、必要な情報が容量に収まる条件と、容量不足として処理を止める条件を区別できます。

参考文献

  1. id774, AI の性能を守りながら資源を減らすには残す場所を選ぶ(2026-10-18). https://blog.id774.net/entry/2026/10/18/5729/
  2. Trang Nguyen, Eulrang Cho, Bingqing Chen, Tim Dettmers, CliffCompaction: Cost-Efficient Compaction for Long-Horizon Coding Agents(2026-09-22). https://arxiv.org/abs/2609.26779
1
0
1

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
1
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?