0

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

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.
    1. 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.
    1. 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 đó:
                                    
                                    ```python
                                    # 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ũ).
                                                                                                
                                                                                                ```python
                                                                                                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`.
                                                                                                                                                                                                                                                                    
                                                                                                                                                                                                                                                                    ```bash
                                                                                                                                                                                                                                                                    #!/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.
                                                                                                                                                                                                                                                                                
                                                                                                                                                                                                                                                                                ```mermaid
                                                                                                                                                                                                                                                                                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.

All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí