跳到正文
DEV Community· EME GUG·· 3 小時前AI 評分71

信任但要核實:如何為 AI 編碼智能體建立報告核查工具

Trust, but Verify: Building a Claim Checker for AI Coding Agents

AI 導讀

作者建立一個約 100 行的 Python 報告核查工具,以 git diff、pytest 和 ruff 對照 AI 編碼智能體的報告與程式碼庫實際狀態。工具從 .agent/report.md 讀取可驗證聲明,可接入 Claude Code 的 Stop hook 或 CI;pytest 執行時會停用快取,重試次數則限制為 2 次。

正文

Tuần trước mình giao cho một coding agent sửa bug trong module thanh toán. Mười phút sau nó báo cáo rất tự tin: "Đã sửa billing/invoice.py, thêm test cho edge case, tất cả 142 tests đều pass." Mình mở terminal ra kiểm tra: file invoice.py không hề bị đổi, test mới nằm trong một file mà pytest không collect, còn 3 test cũ thì fail. Agent không cố tình nói dối. Chỉ là nó mô tả việc nó định làm chứ không phải việc nó đã làm.

Trên Dev.to đang có bài khá hot về chuyện làm "lie detector" cho AI coding agent, và mình thấy vấn đề này rất thật với team ở Việt Nam, khi ngày càng nhiều bạn chạy Claude Code, Cursor, Aider hay Codex CLI ở chế độ tự động. Bài này chia sẻ cách mình dựng một claim checker nhỏ (~100 dòng Python) để đối chiếu báo cáo của agent với trạng thái thật của repo, trước khi mình phải review bằng mắt.

Vì sao agent "báo cáo sai"?

Nói ngắn gọn thì có 3 lý do hay gặp:

  1. Mất context giữa chừng: session dài, context bị nén lại, agent nhớ là mình đã chạy test nhưng thật ra lần chạy đó là trước khi sửa code.
  2. Tool call fail âm thầm: lệnh edit file trả lỗi (ví dụ string không match), agent không đọc kỹ output mà vẫn đi tiếp.
  3. Tóm tắt theo plan: phần summary cuối được viết dựa trên kế hoạch ban đầu chứ không phải kết quả thực thi.

Cả 3 lý do đều dẫn tới một nguyên tắc: đừng tin bản tóm tắt, hãy tin git và exit code. Bản tóm tắt của agent chỉ là claim, mà claim thì cần được verify.

flowchart LR
    A[Agent hoàn thành task] --> B[Báo cáo summary]
    B --> C[Claim Extractor]
    C --> D{Verifier}
    D -->|git diff| E[Files thay đổi thật]
    D -->|pytest| F[Kết quả test thật]
    D -->|ruff| G[Lint thật]
    E & F & G --> H{Khớp với claim?}
    H -->|Có| I[Cho phép review / merge]
    H -->|Không| J[Reject + gửi lại cho agent]

Bước 1: Trích xuất claim từ báo cáo

Mình không dùng LLM để parse báo cáo, vì dùng LLM kiểm tra LLM thì lại thêm một lớp có thể sai. Regex đơn giản là đủ, vì agent hay viết summary theo vài pattern khá cố định: liệt kê file đã sửa (thường bọc trong backtick), và câu kiểu "all tests pass" / "X tests passed".

Mình yêu cầu agent (qua system prompt hoặc file AGENTS.md) ghi summary vào .agent/report.md ở cuối mỗi task. Sau đó script này đọc file đó:

# claim_checker.py - Python 3.11+
import re
import subprocess
import sys
from dataclasses import dataclass, field
from pathlib import Path

FILE_RE = re.compile(r'`([\w./-]+\.(?:py|ts|js|go|sql|yaml|toml))`')
TEST_PASS_RE = re.compile(r'(all tests pass|tests? (?:are |đều )?pass|(\d+) (?:tests )?passed)', re.I)

@dataclass
class Claims:
    files: set[str] = field(default_factory=set)
    tests_pass: bool = False
    test_count: int | None = None

def extract_claims(report: str) -> Claims:
    c = Claims()
    c.files = set(FILE_RE.findall(report))
    m = TEST_PASS_RE.search(report)
    if m:
        c.tests_pass = True
        if m.group(2):
            c.test_count = int(m.group(2))
    return c

Điểm mấu chốt: chỉ bắt những claim kiểm chứng được. Những câu như "code đã clean hơn" hay "xử lý mọi edge case" thì bỏ qua, mấy cái đó để người review.

Bước 2: Đối chiếu với thực tế

Phần verify dùng đúng những tool bạn đang có sẵn: git diff để xem file nào thật sự thay đổi so với base branch, pytest để chạy lại test từ đầu (không dùng cache, không tin output cũ).

def sh(cmd: list[str]) -> tuple[int, str]:
    p = subprocess.run(cmd, capture_output=True, text=True)
    return p.returncode, p.stdout + p.stderr

def changed_files(base: str = 'origin/main') -> set[str]:
    _, out = sh(['git', 'diff', '--name-only', f'{base}...HEAD'])
    _, staged = sh(['git', 'diff', '--name-only'])  # cả thay đổi chưa commit
    return set((out + staged).split())

def run_tests() -> tuple[bool, int]:
    code, out = sh(['pytest', '-q', '-p', 'no:cacheprovider'])
    m = re.search(r'(\d+) passed', out)
    return code == 0, int(m.group(1)) if m else 0

def verify(claims: Claims) -> list[str]:
    problems = []
    real = changed_files()
    for f in claims.files - real:
        if Path(f).exists():
            problems.append(f'Claim sửa {f} nhưng git diff không thấy thay đổi')
        else:
            problems.append(f'Claim nhắc tới {f} nhưng file không tồn tại')
    if claims.tests_pass:
        ok, passed = run_tests()
        if not ok:
            problems.append('Claim tests pass nhưng pytest exit code != 0')
        if claims.test_count and claims.test_count != passed:
            problems.append(f'Claim {claims.test_count} passed, thực tế {passed}')
    return problems

if __name__ == '__main__':
    report = Path('.agent/report.md').read_text(encoding='utf-8')
    issues = verify(extract_claims(report))
    for i in issues:
        print(f'❌ {i}')
    sys.exit(1 if issues else 0)

Có một chi tiết nhỏ nhưng quan trọng: -p no:cacheprovider. Nếu không tắt cache, một số plugin như pytest --lf có thể khiến bạn chỉ chạy lại test fail lần trước, và kết quả "pass" lúc đó không có nghĩa gì cả.

Ngoài ra mình còn check ngược: file nào có trong diff nhưng agent không nhắc tới. Cái này không tính là lỗi, nhưng mình in ra warning, vì agent hay "tiện tay" sửa pyproject.toml hoặc lock file mà không báo.

Bước 3: Gắn vào workflow

Script chỉ hữu ích khi nó chạy tự động. Mình gắn nó ở 2 chỗ: chạy local ngay sau khi agent kết thúc task, và trong CI khi PR có label agent-generated.

#!/usr/bin/env bash
# scripts/agent-done.sh - chạy sau mỗi task của agent
set -euo pipefail

if [[ ! -f .agent/report.md ]]; then
  echo "Agent không ghi report -> reject"
  exit 1
fi

ruff check . --quiet || { echo "Lint fail"; exit 1; }

if ! python3 claim_checker.py > .agent/verify.log 2>&1; then
  echo "Claim không khớp thực tế:"
  cat .agent/verify.log
  # gửi lại log cho agent để nó tự sửa, tối đa 2 vòng
  exit 2
fi

echo "✅ Claims verified, sẵn sàng cho human review"

Với Claude Code, bạn có thể gắn script này vào hook Stop trong .claude/settings.json. Exit code khác 0 sẽ chặn agent kết thúc và đẩy log lại cho nó. Với Aider hay các tool khác, chỉ cần wrap lệnh chạy agent trong một script shell rồi gọi agent-done.sh ở cuối.

sequenceDiagram
    participant Dev
    participant Agent
    participant Hook as Stop Hook
    participant Repo as git + pytest
    Dev->>Agent: Giao task
    Agent->>Repo: Sửa code, chạy lệnh
    Agent->>Hook: Kết thúc + report.md
    Hook->>Repo: git diff, pytest, ruff
    Repo-->>Hook: Kết quả thật
    alt Không khớp
        Hook-->>Agent: verify.log (làm lại)
    else Khớp
        Hook-->>Dev: Sẵn sàng review
    end

Lưu ý giới hạn vòng lặp: nhớ giới hạn số lần retry (mình để 2). Nếu không, agent có thể loay hoay mãi, và tệ hơn là nó "học" cách viết report cho khớp với checker thay vì sửa code thật. Ví dụ nó sẽ bỏ hẳn câu "tests pass" khỏi report. Vì vậy với các task có sửa code, mình bắt buộc report phải có phần test, thiếu là reject luôn.

Những gì checker này KHÔNG làm được

Phải nói rõ là checker này chỉ bắt được sai lệch giữa lời nói và hành động, không bắt được code sai logic. Test pass vẫn có thể là test vô nghĩa (kiểu assert True), diff đúng file vẫn có thể là sửa sai chỗ. Nó là lớp lọc đầu tiên, rẻ và nhanh, giúp bạn khỏi mất 15 phút review một PR mà ngay từ đầu đã không chạy được.

Sau 2 tháng chạy trong team, khoảng 1/8 số task agent làm bị checker bắt lại, chủ yếu là lỗi tool edit fail âm thầm và test file đặt sai chỗ. Đó là số lần mình không còn phải tự mở terminal ra kiểm tra từng cái nữa.

Kết luận

  • Coi summary của agent là claim, không phải fact. Mọi claim kiểm chứng được (file đã sửa, test pass, lint sạch) đều nên verify tự động.
  • Dùng tool có sẵn để verify, đừng dùng LLM: git diff --name-only, exit code của pytest, ruff check. Deterministic và gần như không tốn gì.
  • Bắt agent ghi report vào một file cố định (.agent/report.md) qua AGENTS.md hoặc system prompt, để việc parse ổn định.
  • Gắn vào Stop hook hoặc CI, đẩy log lỗi lại cho agent, nhưng giới hạn số vòng retry.
  • Luôn tắt cache khi chạy lại test (-p no:cacheprovider) và chạy trên toàn bộ suite.
  • Checker không thay được code review. Nó chỉ đảm bảo thứ bạn review là thứ thật sự tồn tại.

Bắt đầu từ việc nhỏ nhất: hôm nay thêm một dòng git diff --name-only vào cuối workflow của agent rồi so với những gì nó báo cáo. Bạn sẽ bất ngờ đấy.

來源:DEV Community · dev.to