Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Viết tiêu chí hoàn tất mà AI có thể kiểm chứng

Câu hỏi bài này trả lời: làm sao biết một AI agent đã làm xong đúng yêu cầu, thay vì chỉ nghe nó báo “xong”?

Cần biết trước: đọc được code Python cơ bản và biết giao việc có phạm vi (xem Chọn context khi sửa code). Phần lab chỉ dùng thư viện chuẩn của Python 3 (đã chạy trên 3.14), không cài thêm gì.

Agent thường dừng khi việc “trông như đã xong”. Tài liệu best practices của Claude Code nói đúng điều này: nếu không có phép kiểm nào để chạy thì “trông như xong” là tín hiệu duy nhất, và bạn trở thành vòng kiểm chứng. Bài của Anthropic về harness cho agent chạy dài ghi nhận một dạng khác của cùng vấn đề: một phiên agent sau thấy đã có tiến độ nên tuyên bố xong. Bài này đi từ một yêu cầu mơ hồ tới tiêu chí có verifier, rồi kiểm chính verifier, vì một verifier chưa từng thất bại thì chưa chứng minh được gì.

Ba mệnh đề hay bị coi là một

Mệnh đềBằng chứng cho nóKhông chứng minh được
File tồn tạitest -f users.csvNội dung đúng, đủ dòng, đọc lại được
Test chạy đượcMột lệnh kiểm kết thúc với exit code 0Rằng phép kiểm phủ yêu cầu; nó có thể rỗng hoặc chỉ kiểm điều dễ
Hành vi đúng yêu cầuVerifier đọc kết quả thật, so với kỳ vọng viết độc lập với code, và đã từng thất bại trước lỗi mẫuCác yêu cầu chưa được viết thành tiêu chí

Câu “xong rồi, file CSV đã được tạo” chỉ nói tới mệnh đề đầu. Hai mệnh đề sau cần tiêu chí và verifier.

Từ yêu cầu mơ hồ đến tiêu chí quan sát được

Yêu cầu ban đầu: “Thêm chức năng xuất danh sách người dùng ra CSV.” Câu này không nói “xong” nghĩa là gì. Viết lại thành tiêu chí mà bạn quan sát được từ kết quả, không mô tả cách làm:

  • R1: dòng đầu là id,name,created_at.
  • R2: mỗi người dùng một dòng dữ liệu, giữ thứ tự đầu vào.
  • R3: created_at có dạng 2026-01-31T09:05:00Z (UTC, ISO 8601 theo hồ sơ RFC 3339).
  • R4: tên chứa dấu phẩy, nháy kép hoặc xuống dòng vẫn đọc lại đúng bằng một CSV parser chuẩn (quy tắc nháy theo RFC 4180, tài liệu thông tin chứ không phải tiêu chuẩn).
  • R5: danh sách rỗng cho file chỉ có dòng tiêu đề.
  • R6: tiếng Việt có dấu giữ nguyên, file UTF-8.
  • Ngoài phạm vi: Excel và BOM, file rất lớn, phân quyền truy cập.

Liệt kê ca biên (rỗng, giá trị đặc biệt, Unicode, múi giờ) từ yêu cầu trước khi có code. Điều vắng mặt không tự lộ ra: câu trả lời cho “đã đủ chưa?” chỉ phản ánh những ca người trả lời đã nghĩ tới.

Hai verifier cho cùng một yêu cầu

Giả sử agent báo: “Xong, users.csv đã được tạo.” Nó có thể đã viết một trong bốn bản cài: một bản đúng và ba bản, mỗi bản sai đúng một tiêu chí (R4, R5, R3). Tạo thư mục trống, ví dụ csv-lab, và các file sau.

export_csv.py chứa bốn bản cài. export_correct dùng csv.writer và mở file với newline="" theo tài liệu module csv:

import csv
from datetime import timezone

HEADER = ["id", "name", "created_at"]


def iso_utc(moment):
    return moment.astimezone(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")


def export_correct(users, path):
    with open(path, "w", encoding="utf-8", newline="") as handle:
        writer = csv.writer(handle)
        writer.writerow(HEADER)
        for user in users:
            writer.writerow([user["id"], user["name"], iso_utc(user["created_at"])])


def export_without_quoting(users, path):
    lines = [",".join(HEADER)]
    for user in users:
        lines.append(f"{user['id']},{user['name']},{iso_utc(user['created_at'])}")
    with open(path, "w", encoding="utf-8") as handle:
        handle.write("\n".join(lines) + "\n")


def export_no_header_when_empty(users, path):
    with open(path, "w", encoding="utf-8", newline="") as handle:
        writer = csv.writer(handle)
        if users:
            writer.writerow(HEADER)
        for user in users:
            writer.writerow([user["id"], user["name"], iso_utc(user["created_at"])])


def export_datetime_as_text(users, path):
    with open(path, "w", encoding="utf-8", newline="") as handle:
        writer = csv.writer(handle)
        writer.writerow(HEADER)
        for user in users:
            writer.writerow([user["id"], user["name"], user["created_at"]])


IMPLEMENTATIONS = {
    "correct": export_correct,
    "without_quoting": export_without_quoting,
    "no_header_when_empty": export_no_header_when_empty,
    "datetime_as_text": export_datetime_as_text,
}

fixtures.py giữ dữ liệu mẫu và kết quả mong đợi viết thẳng thành giá trị, không tính từ code đang kiểm: nếu kỳ vọng sinh từ chính code thì lỗi và kỳ vọng sai cùng nhau. Bốn ca ứng với tiêu chí: normal (R1–R3), special_chars (R4), empty (R5), unicode (R6).

from datetime import datetime, timezone

UTC = timezone.utc
HEADER = ["id", "name", "created_at"]

CASES = {
    "normal": (
        [
            {"id": 1, "name": "An", "created_at": datetime(2026, 1, 31, 9, 5, tzinfo=UTC)},
            {"id": 2, "name": "Binh", "created_at": datetime(2026, 2, 1, 0, 0, tzinfo=UTC)},
        ],
        [["1", "An", "2026-01-31T09:05:00Z"], ["2", "Binh", "2026-02-01T00:00:00Z"]],
    ),
    "special_chars": (
        [{"id": 3, "name": 'Le, "Van"\nTran', "created_at": datetime(2026, 3, 1, 12, 30, 45, tzinfo=UTC)}],
        [["3", 'Le, "Van"\nTran', "2026-03-01T12:30:45Z"]],
    ),
    "unicode": (
        [{"id": 4, "name": "Nguyễn Thị Bình", "created_at": datetime(2026, 4, 2, 6, 0, tzinfo=UTC)}],
        [["4", "Nguyễn Thị Bình", "2026-04-02T06:00:00Z"]],
    ),
    "empty": ([], []),
}

check_weak.py là kiểu kiểm agent hay viết: chạy export rồi xác nhận file tồn tại.

import os
import sys
import tempfile

from export_csv import IMPLEMENTATIONS
from fixtures import CASES

users, _ = CASES["special_chars"]
with tempfile.TemporaryDirectory() as folder:
    path = os.path.join(folder, "users.csv")
    IMPLEMENTATIONS[sys.argv[1]](users, path)
    assert os.path.exists(path)
print("file tồn tại")

verify_behavior.py đóng vai người dùng file: đọc lại bằng CSV parser chuẩn rồi so với kỳ vọng độc lập. Cùng tinh thần với khuyến nghị kiểm đầu-cuối “như người dùng thật” trong bài harness của Anthropic. Nó chỉ ghi vào thư mục tạm.

import csv
import sys
import tempfile
from pathlib import Path

from export_csv import IMPLEMENTATIONS
from fixtures import CASES, HEADER


def read_back(export, users):
    with tempfile.TemporaryDirectory() as folder:
        path = Path(folder) / "users.csv"
        export(users, path)
        with open(path, encoding="utf-8", newline="") as handle:
            return list(csv.reader(handle))


def check(name):
    passed = True
    for label, (users, expected) in CASES.items():
        rows = read_back(IMPLEMENTATIONS[name], users)
        wanted = [HEADER, *expected]
        if rows == wanted:
            print(f"ok   {label}")
        else:
            passed = False
            print(f"FAIL {label}: mong đợi {wanted!r}")
            print(f"     đọc lại   {rows!r}")
    return passed


if __name__ == "__main__":
    sys.exit(0 if check(sys.argv[1]) else 1)

Chạy cả hai verifier trên từng bản cài:

for impl in correct without_quoting no_header_when_empty datetime_as_text; do
  if python3 check_weak.py "$impl" >/dev/null 2>&1; then weak=pass; else weak=FAIL; fi
  if python3 verify_behavior.py "$impl" >/dev/null 2>&1; then behavior=pass; else behavior=FAIL; fi
  printf '%-22s weak=%s behavior=%s\n' "$impl" "$weak" "$behavior"
done
correct                weak=pass behavior=pass
without_quoting        weak=pass behavior=FAIL
no_header_when_empty   weak=pass behavior=FAIL
datetime_as_text       weak=pass behavior=FAIL

Verifier yếu pass cả bốn bản, nên “pass” của nó không mang thông tin: nó không thể thất bại trước ba lỗi này. Verifier hành vi pass bản đúng và thất bại trước từng bản lỗi. Đó mới là lý do để tin “pass” của nó, trong phạm vi bốn ca. Với verifier do agent viết hoặc sửa, vòng lặp trên là bước kiểm nên có: đưa vào vài bản lỗi mẫu và xem verifier có đỏ không. Đây là dạng tối giản của mutation testing, tức cố ý đưa lỗi vào code rồi xem test có bắt được không.

Bảng yêu cầu, verifier, kỳ vọng, thực tế, bằng chứng

Lấy bản without_quoting (agent báo xong vì file tồn tại) và chạy verifier hành vi:

python3 verify_behavior.py without_quoting
ok   normal
FAIL special_chars: mong đợi [['id', 'name', 'created_at'], ['3', 'Le, "Van"\nTran', '2026-03-01T12:30:45Z']]
     đọc lại   [['id', 'name', 'created_at'], ['3', 'Le', ' "Van"'], ['Tran', '2026-03-01T12:30:45Z']]
ok   unicode
ok   empty
Yêu cầuVerifierKỳ vọngThực tếBằng chứng
R1 Dòng đầu là id,name,created_atverify_behavior.py, ca normalHàng đầu là id, name, created_atKhớpok normal
R2 Một dòng mỗi người dùng, đúng thứ tựCa normalHai dòng dữ liệu theo thứ tự đầu vàoKhớpok normal
R3 created_at dạng …T…ZCa normal2026-01-31T09:05:00ZKhớpok normal
R4 Dấu phẩy, nháy kép, xuống dòng không phá cộtCa special_charsMột dòng dữ liệu, tên nguyên vẹnHai dòng dữ liệu, tên bị tách thành Le, "Van" và TranFAIL special_chars, exit code 1
R5 Danh sách rỗng chỉ có dòng tiêu đềCa emptyMột hàng: tiêu đềKhớpok empty
R6 Tiếng Việt có dấu giữ nguyênCa unicodeNguyễn Thị BìnhKhớpok unicode

Kết luận cho bản này: R1–R3 và R5–R6 đạt, R4 không đạt, nên chưa xong dù file tồn tại. Hai bản còn lại sai ở tiêu chí khác: no_header_when_empty sai R5 (đọc lại được 0 hàng thay vì 1), datetime_as_text sai R3 ở mọi ca có dữ liệu (2026-01-31 09:05:00+00:00 thay vì …T…Z).

Kiểm chỉ đọc và hành động thật

LoạiVí dụDùng làm bằng chứng “xong”?
Kiểm chỉ đọc hoặc cô lậpChạy test trong thư mục tạm; đọc lại file xuất; dry-run hoặc plan; truy vấn đọc trên database thửCó
Hành động đổi môi trường thậtDeploy, chạy migration trên database thật, gửi email, xóa dữ liệu, pushKhông: cần quyền và phê duyệt riêng
  • Tiêu chí “xong” chỉ dựa vào kiểm chỉ đọc hoặc cô lập. Nếu việc cần deploy hay migration, viết tiêu chí riêng cho nó và theo dõi trạng thái riêng.
  • Một lệnh vừa kiểm vừa hành động (ví dụ “chạy test, pass thì deploy”) không phải verifier; tách làm hai.
  • Báo cáo tách ba trạng thái: đã kiểm (lệnh và kết quả), chưa kiểm (vì sao), đã chạy hành động (ai phê duyệt). “Chưa kiểm” không bao giờ là “đạt”. Tài liệu Claude Code tóm tắt: nếu không kiểm được thì đừng phát hành.

Ai viết tiêu chí và verifier? Người giao việc viết hoặc duyệt tiêu chí; agent chạy verifier và báo output. Lahiri (arXiv:2603.17150, preprint 03/2026) viết rằng ngoài người dùng không có oracle nào cho độ đúng của đặc tả, nên không ai kiểm hộ bạn tiêu chí có đúng ý hay không. Có thêm lý do thực tế để đặt verifier ngoài tầm sửa của agent:

  • METR (2025-06-05) ghi nhận ở một số tác vụ có chấm điểm, các model tiên phong có lúc sửa hoặc né mã chấm điểm thay vì giải bài, với tỷ lệ khác nhau rất xa giữa các tác vụ. Chính báo cáo ghi rằng hai phương pháp lọc tự động của họ có tỷ lệ dương tính giả rất cao nên chỉ dùng để chọn lượt chạy xem tay, và số lần phát hiện có thể thấp hơn thực tế.
  • Anthropic cho agent chạy dài một quy tắc: không được xóa hoặc sửa test vì có thể dẫn tới chức năng bị thiếu hoặc lỗi. Tài liệu Claude Code mô tả mẫu reviewer riêng trong ngữ cảnh mới, để agent làm việc không phải là agent chấm.

Nếu agent buộc phải sửa verifier, xem diff của verifier trước diff của code.

Bằng chứng do máy ghi, không do lời kể

Bằng chứng gồm lệnh, exit code, đoạn output liên quan, môi trường và thời điểm. Tài liệu Claude Code khuyến nghị yêu cầu agent đưa bằng chứng thay vì khẳng định thành công, vì đọc bằng chứng nhanh hơn chạy lại phép kiểm. Cách đơn giản nhất để bằng chứng không phụ thuộc lời kể là chạy verifier qua một lớp ghi lại:

import json
import platform
import subprocess
import sys
import time

command = sys.argv[1:]
started = time.strftime("%Y-%m-%dT%H:%M:%S%z")
done = subprocess.run(command, capture_output=True, text=True)
record = {
    "command": command,
    "exit_code": done.returncode,
    "output_tail": (done.stdout + done.stderr).splitlines()[-5:],
    "python": platform.python_version(),
    "started_at": started,
}
print(json.dumps(record, ensure_ascii=False, indent=2))
sys.exit(done.returncode)

Chạy verifier hành vi trên bản đúng qua lớp ghi:

python3 evidence.py python3 verify_behavior.py correct
{
  "command": [
    "python3",
    "verify_behavior.py",
    "correct"
  ],
  "exit_code": 0,
  "output_tail": [
    "ok   normal",
    "ok   special_chars",
    "ok   unicode",
    "ok   empty"
  ],
  "python": "3.14.3",
  "started_at": "2026-10-02T17:17:10+0700"
}

Bản ghi do chương trình tạo ra từ lần chạy thật. Chạy tay như trên mới là bước đầu: đặt bước ghi ở nơi agent không sửa được (CI hoặc hook của công cụ) thì mạnh hơn.

Mẫu dùng lại

Từ yêu cầu tới tiêu chí:

Yêu cầu: <một câu>

Tiêu chí (quan sát được từ kết quả, không mô tả cách làm)
- R1: <điều kiện>
- R2: <điều kiện>

Ca biên: <rỗng · giá trị đặc biệt · Unicode · múi giờ · ...>
Ngoài phạm vi: <điều không làm>

Verifier: <lệnh> — kỳ vọng: <kết quả>
Phải đỏ trước các bản lỗi mẫu: <lỗi mẫu 1, lỗi mẫu 2>

Báo cáo hoàn tất mà agent phải trả:

Tiêu chí: <R1…Rn>, mỗi tiêu chí đạt hoặc không đạt
Lệnh: <lệnh đã chạy>
Kết quả: exit code <n>; <các dòng output liên quan>
Môi trường: <hệ điều hành, phiên bản>
Đã kiểm: <việc đã chạy thật>
Chưa kiểm: <deploy, dữ liệu thật, môi trường khác, kèm lý do>

Checklist trước khi chấp nhận “xong”:

  • Mỗi tiêu chí mô tả kết quả quan sát được, không mô tả cách làm?
  • Ca biên được liệt kê từ yêu cầu trước khi viết code?
  • Kỳ vọng được viết độc lập với code đang kiểm?
  • Mỗi verifier đã đỏ trước ít nhất một bản lỗi mẫu?
  • Có cách nào làm tiêu chí này xanh mà không giải bài toán không? Nếu có, viết lại tiêu chí.
  • Có ít nhất một verifier đọc kết quả như người dùng tiêu thụ nó?
  • Verifier chỉ đọc hoặc chạy trong môi trường cô lập; deploy và migration có tiêu chí và trạng thái riêng?
  • Báo cáo có lệnh, exit code, output, môi trường và phần chưa kiểm?
  • Verifier nằm ngoài vùng agent được sửa, hoặc diff của nó được xem trước?

Giới hạn và lỗi thường gặp

Giới hạn của bài

  • Lab giả lập và không gọi model nào. Bài cho thấy verifier yếu không phân biệt được bản đúng với bản lỗi và verifier hành vi phân biệt được; nó không đo agent nào hay lách verifier thường đến đâu.
  • “Pass” của verifier hành vi chỉ có nghĩa là bốn ca đó đạt. Ba bản lỗi mẫu không phải mọi lỗi; công cụ mutation testing phủ rộng hơn nhiều.
  • Bài không bàn Excel, BOM hay dấu phân cách theo locale. Lab chạy ngày 2026-10-02 trên Linux (Python 3.14.3) và macOS 27.0.1 arm64 (Python 3.14.8); Windows và Python 3.10–3.13 chưa thử.
  • Nguồn METR đo trên các model và tác vụ riêng, và Lahiri là preprint; bài chỉ dùng nhận định định tính, không dùng số liệu của họ.

Lỗi thường gặp

LỗiHậu quảCách tránh
Tiêu chí mô tả cách làm (“dùng csv.writer”)Đạt khi làm đúng cách nhưng kết quả vẫn saiViết kết quả quan sát được
Verifier chỉ kiểm file tồn tại hoặc exit codePass với mọi lỗiĐọc lại kết quả và so với kỳ vọng độc lập
Kỳ vọng sinh từ chính code đang kiểmLỗi và kỳ vọng sai cùng nhauViết kỳ vọng thành dữ liệu độc lập
Không có ca rỗng và ca đặc biệtLỗi ca biên lọt quaLiệt kê ca biên từ yêu cầu trước khi viết code
Agent tự sửa verifier cho dễ passVerifier bị nới lỏngXem diff của verifier trước; giữ nó ngoài vùng agent được sửa
Gộp kiểm và deploy“Xong” lẫn với “đã phát hành”Tách verifier chỉ đọc khỏi hành động; báo trạng thái riêng
Tin “đã chạy test” khi không có outputKhông kiểm lại đượcĐòi lệnh, exit code và output

Học tiếp

Nguồn tham khảo