跳到正文
DEV Community· tappers·· 2 小時前AI 評分58

結構化 JSON 測試過濾器如何令編碼智能體輸入 token 減少 94.1%

Stop Dumping Raw Pytest Logs Into Your Coding Agent: How a Structured JSON Test Filter Cuts Input Tokens by 94.1%

AI 導讀

作者在隔離 VM 中比較兩種編碼智能體測試流程,結構化 JSON 過濾器只保留失敗測試資訊,將累計輸入 token 從 92,385 降至 5,445,減少 94.1%。兩種流程均通過隱藏評測套件的 20/20 個案例;過濾流程的 API 成本為 $0.0100,執行時間為 16.88 sec,原始輸出流程則分別為 $0.0770 和 25.94 sec。

正文

💡 AI Labo Benchmark Series — Experiment E16 (Ranked #3 out of 41 Patterns)

This post is part of our empirical benchmark series testing AI coding agent harness patterns inside network-isolated Proxmox VMs against a hidden 20-case grading suite.


Metadata & Experiment Identity

  • Experiment ID: E16 (Backlog Candidate C012)
  • Run ID: e16-20261028-vm220-01
  • Sandbox Environment: Proxmox Isolated VM VM220 (ai-e16-220, 0 NICs, virtio-serial QGA relay, systemd UID separation)
  • Model: gemini-3.8-flash (thinkingLevel: LOW)
  • Pattern Under Test: RTK (rtk-ai/rtk, Rust Token Killer) & SWE-agent Output Compression — "Raw CLI Terminal Output vs. Semantic JSON Failure-Diff Filter"

1. Executive Summary (TL;DR)

What is the single biggest token sink in an autonomous coding agent loop? Surprisingly, it isn't the source code itself—it is unfiltered CLI test runner output.

When an agent executes pytest -vv --tb=long, cargo test, or npm test on a realistic repository where 76 tests pass and 4 fail, dumping the raw terminal output (ANSI color codes, environment banners, 76 [PASS] lines, captured DEBUG stdout, and multi-frame internal tracebacks) directly into chat history adds 60,000+ characters (28,000+ tokens) per test run.

To measure the impact of deterministic CLI output filtering, we benchmarked a 3-module payment settlement pipeline (validator.py, settlement.py, statement.py) across two harness modes inside isolated VM VM220:

  • Mode A (RawTerminalOutputRunner): Appends raw pytest terminal stdout (60,749 chars on Turn 1; 117,391 chars cumulative) directly to the conversation history.
  • Mode B (TokenKillerFilterRunner): Wraps the test runner in a deterministic preprocessor that strips ANSI escape codes, environment banners, all 76 passing tests, and internal framework frames—returning only a 1,506-character structured JSON summary of the 4 failed assertions.

Key Result

Compressing raw CLI test output from 117,391 chars ➔ 1,604 chars (-98.6% reduction, 73.2x compression) enabled Mode B to fix all three modules in a single edit turn (2 LLM calls total vs. 3 in Mode A). Mode B cut cumulative input tokens by -94.1% (92,385 ➔ 5,445 tokens, 16.97x efficiency), API cost by -87.0% ($0.0770 ➔ $0.0100), and wall-clock time by -35.0% (1.54x faster) while achieving 20 / 20 (100%) on the hidden grading suite.

Metric Mode A: Raw Terminal Output (RawTerminalOutput) Mode B: Semantic JSON Filter (TokenKillerFilter) Delta / Improvement
Hidden Grading Suite Score (20 Cases) 20 / 20 (100%) 20 / 20 (100%) Both passed all 20 cases
Cumulative CLI Tool Output (tool_output_chars) 117,391 chars 1,604 chars -98.6% (73.19x compression)
First Test Run Output Size 60,749 chars 1,506 chars -97.5% (76 PASS lines & ANSI stripped)
LLM Calls (model_calls) 3 calls 2 calls (1 test run + 1 full fix) -33.3% (No intermediate partial patch)
Turn 1 Input / Output Tokens 2,406 in / 10 out 2,415 in / 10 out Identical initial test invocation
Turn 2 Input / Output Tokens 31,170 in / 484 out 3,030 in / 1,561 out Turn 2 input cut by -90.3%
Turn 3 Input / Output Tokens 58,809 in / 1,574 out — (Completed on Turn 2) Avoided 58,809-token log snowball
Cumulative Input Tokens (promptTokenCount) 92,385 tokens 5,445 tokens -94.1% (16.97x token efficiency)
Cumulative Output Tokens (candidatesTokenCount) 2,068 tokens 1,571 tokens -24.0% reduction
Elapsed Execution Time (elapsed_seconds) 25.94 sec 16.88 sec -35.0% (1.54x faster)
Estimated API Cost (estimated_micro_usd) $0.0770 (77,045 micro-USD) $0.0100 (9,976 micro-USD) -87.0% (7.72x cheaper)

2. Experiment Architecture & Workflow Comparison

+-------------------------------------------------------------------------+
| Proxmox Isolated VM220 (0 NICs, virtio-serial QGA, systemd UID split)   |
|  Target Suite: 80-Test Suite (76 PASS / 4 FAIL across 3 modules)        |
|                                                                         |
|  +-------------------------------------------------------------------+  |
|  | Mode A: RawTerminalOutputRunner (Unfiltered pytest -vv --tb=long) |  |
|  |  - Turn 1: run_raw_pytest     --> Returns 60,749 chars raw log    |  |
|  |  - Turn 2: stage_partial_fix  --> Input jumps to 31,170 tokens!   |  |
|  |                                   Returns 56,544 chars raw log    |  |
|  |  - Turn 3: submit_settlement  --> Input hits 58,809 tokens!       |  |
|  |                                   (Total: 92,385 input tokens)    |  |
|  +-------------------------------------------------------------------+  |
|                                                                         |
|  +-------------------------------------------------------------------+  |
|  | Mode B: TokenKillerFilterRunner (RTK-Style Semantic JSON Filter)  |  |
|  |  - Turn 1: run_filtered_tests --> Strips 76 PASS & ANSI noise;    |  |
|  |                                   returns 1,506-char failure JSON |  |
|  |  - Turn 2: submit_settlement  --> Input is only 3,030 tokens!     |  |
|  |                                   Fixes all 3 files in 1 shot     |  |
|  |                                   (Total: 5,445 input tokens)     |  |
|  +-------------------------------------------------------------------+  |
+-------------------------------------------------------------------------+

3. Inside the Trace: How Raw CLI Logs Snowball Across Turns

3.1 Mode A (RawTerminalOutput): How 2 Test Runs Burned 92,385 Input Tokens

Tracking execution-traces.json and usage.json reveals how fast raw terminal output compounds in an agent's message array:

  1. Turn 1 (2,406 in / 10 out): The agent calls {"tool": "run_raw_pytest"}. The runner executes the 80-test suite and returns 60,749 characters containing ANSI color codes, platform headers, 76 verbose [PASS] lines with AUDIT_TRACE dumps, and long multi-frame tracebacks for the 4 failures.
  2. Turn 2 (31,170 in / 484 out): Appending that single raw stdout dump causes the prompt size to jump by +28,764 tokens (2,406 ➔ 31,170). Distracted by the 60KB wall of text, the agent only patches validator.py (stage_partial_fix), triggering a second test run that appends another 56,544 characters (now 77 PASS lines + 3 FAIL tracebacks) to the conversation history.
  3. Turn 3 (58,809 in / 1,574 out): Now carrying 117,293 characters of raw terminal logs across two turns, Turn 3 alone requires 58,809 input tokens before the agent finally submits all 3 modules (92,385 cumulative input tokens).

3.2 Mode B (TokenKillerFilter): 1,506 Characters of Pure Signal

When Mode B called {"tool": "run_filtered_tests"} on Turn 1, the harness preprocessor stripped all 76 passing tests, ANSI escape sequences, and DEBUG noise—returning a 1,506-character (615-token) JSON payload containing only the 4 failing tests (test, file, line, assertion, expected_vs_actual):

{
  "summary": {"total": 80, "passed": 76, "failed": 4, "filtered_noise_chars_removed": 59243},
  "failures": [
    {
      "test": "test_validator_contract",
      "file": "validator.py",
      "line": 14,
      "assertion": "ALLOWED_CURRENCIES must include 'JPY'; reject empty list & bool; allow unit_cents==0 & chargeback_bps==1000; do not mutate input dict."
    },
    {
      "test": "test_settlement_ordering_and_reserve",
      "file": "settlement.py",
      "line": 22,
      "assertion": "Preserve first-seen (merchant_id, currency) order (never sorted(order)); apply 10% reserve strictly when chargeback_bps > 300."
    },
    {
      "test": "test_settlement_discounts_and_rebate_cap",
      "file": "settlement.py",
      "line": 39,
      "assertion": "Discount boundary is subtotal >= 15000 (GROWTH 10%, ENTERPRISE 20% else 10%); cap rebate_applied = min(fee_rebate, after_discount)."
    },
    {
      "test": "test_statement_format_and_signed_zero",
      "file": "statement.py",
      "line": 11,
      "assertion": "Use '#' delimiter ({merchant_id}#{currency}:) and preserve leading '-' on zero discount/rebate (TIER_DISCOUNT: -0, REBATE_APPLIED: -0)."
    }
  ]
}

Because all four failure root causes were immediately visible without scrolling through 76 passing tests, Mode B fixed all three files in a single Turn 2 call (3,030 input tokens)—eliminating the intermediate rework turn entirely.


4. All 20 Hidden Grading Suite Results (labjudge16)

Both modes passed all 20 hidden unit and E2E test cases verified by labjudge16:

# Test Case ID Invariant / Boundary Verified Mode A (Raw CLI) Mode B (Token Killer)
1–6 validator_01 to validator_06 Trim/uppercase, non-mutation, JPY, bool rejection, 0 cents & 1000 bps bounds 6 / 6 PASS 6 / 6 PASS
7–15 settlement_01 to settlement_09 First-seen order, conflict check, 300 vs 301 bps, 14999 vs 15000 tier, rebate cap 9 / 9 PASS 9 / 9 PASS
16–20 e2e_01 to e2e_05 # separator, signed zero -0, $0 auth statement, line order, error propagation 5 / 5 PASS 5 / 5 PASS
Total 20 Hidden Cases — 20 / 20 (100%) 20 / 20 (100%)

5. Practical Takeaway: Wrap Your Agent's Test Runner Today

Never wire raw subprocess.run(["pytest"]) stdout directly into your LLM agent's tool response. Instead:

  1. Use machine-readable reporters: Run pytest --tb=short -q --no-header --no-summary (or pytest-json-report) inside your tool wrapper.
  2. Strip ANSI codes and passing tests: If 500 tests pass and 2 fail, the LLM only needs "passed": 500 as a single integer plus the exact file, line number, and assertion diff for the 2 failures.
  3. Adopt CLI proxy filters: Tools like rtk (Rust Token Killer) or a 30-line Python wrapper around your test runner immediately cut agent token bills by 85%–95% while speeding up multi-file bug fixes.

🔗 Continue Reading the Series

來源:DEV Community · dev.to