ai

Harness engineering: anh em đã làm mà chưa biết tên

OpenAI dùng Codex agents để ship phần lớn code production không viết tay, và gọi framework đằng sau là 'harness'. CLAUDE.md và hooks của anh em chính là một harness đang chạy.

Tháng 2 năm nay mình đọc bài Harness engineering của OpenAI về cách họ dùng Codex agent để xây một sản phẩm nội bộ. Cái mình để ý không phải số dòng code, mà là tên họ đặt cho framework bao quanh agent — harness. Đọc xong mình nhìn lại đống CLAUDE.md và hooks mình đang có, thấy mình đã làm cái này từ lâu rồi.

Agent = Model + Harness

Cách OpenAI nhìn một AI agent:

Agent = Model + Harness

Model là phần AI — GPT-4o, Claude Sonnet, Gemini. Phần này mình không build, chỉ chọn và dùng thôi.

Harness là phần còn lại: context, tools, constraints, feedback loops, lifecycle — tất cả những gì anh em xây xung quanh model để nó làm đúng việc. Khác với model, harness hoàn toàn trong tầm tay mình.

Nói nôm na: model quyết định AI “thông minh” đến đâu, harness quyết định nó có “ngoan” không.

harness/
├── context/     # CLAUDE.md, system prompts, conventions
├── tools/       # File read/write, bash, search — phân permission tier
├── sensors/     # Linters, tests, coverage checks
└── lifecycle/   # Context window management, handoff protocol

Anh em đã build harness mà chưa biết

Ví dụ điển hình nhất là CLAUDE.md — file mà nhiều anh em dùng Claude Code đã quen:

# CLAUDE.md

## Stack
Next.js 14, TypeScript, Prisma + PostgreSQL, Tailwind.
Deploy trên Vercel, branch main = production.

## Code style
- Dùng `const` thay `let` khi không cần reassign
- Không dùng `any`, phải có type rõ ràng
- Error handling: throw Error với message tiếng Anh, log tiếng Việt để debug

## Không được làm
- Không xóa migration cũ
- Không commit trực tiếp lên main
- Không dùng `console.log` trong production code

Đây là context layer của harness. Viết một lần, AI cứ theo đó mỗi khi làm việc trong project — không cần nhắc lại từng session. Mỗi lần anh em ngồi viết CLAUDE.md, custom instruction cho Cursor hay system prompt là đang build cái này rồi đó.

Skills trong Claude Code cũng vậy — blog-write.md, blog-review.md là những quy trình đóng gói vào harness, gọi tên là chạy.

Sensors là feedback loop của harness

Context tĩnh chỉ nói agent nên làm gì. Harness bắt đầu có feedback loop khi mình thêm thứ đo output và trả kết quả lại cho agent — test, type checker, linter, browser assertion. Tạm gọi lớp này là sensors.

Ví dụ đơn giản nhất là hook trong Claude Code. Hook nhận input qua JSON trên stdin, không phải qua biến môi trường — nên phải đọc field tool_input.file_path ra bằng jq (hoặc tương đương) trước khi dùng:

{
  "hooks": {
    "PostToolUse": [{
      "matcher": "Write",
      "hooks": [{
        "type": "command",
        "command": "file=$(jq -r '.tool_input.file_path // empty'); if [ -n \"$file\" ]; then npx eslint --fix -- \"$file\"; fi"
      }]
    }]
  }
}

Dùng npx eslint để chạy đúng bản eslint khai trong package.json của project, không phụ thuộc global install. Mỗi lần AI write file, hook tự chạy và eslint tự fix những gì fix được — AI thấy lỗi còn lại và tự sửa tiếp, anh em không cần nhắc “check linting nhé”.

Permission tier hoạt động ngược lại — không đo output mà chặn input trước khi chạy:

{
  "permissions": {
    "allow": ["Read", "Grep", "Glob"],
    "deny": ["Bash(git push --force*)"]
  }
}

Rule này giảm khả năng agent lỡ tay chạy lệnh nguy hiểm khi làm việc bình thường — không phải một security boundary chặt. Deny pattern match theo string command, nên vẫn có thể bị lách qua alias, wrapper script, hay cách gọi git khác không khớp pattern. Muốn có boundary thật (ví dụ ngăn agent đọc secret, ghi ra ngoài thư mục project) thì cần kiểm soát ở tầng OS/container, không chỉ dựa vào danh sách deny trong config.

Điểm mấu chốt: những rule này nằm trong harness, không phải trong model. Khi đổi model, constraints và feedback loop vẫn còn đó; mức độ tuân thủ có thể khác, nhưng mình không phải giải thích lại toàn bộ project từ đầu.

Three-agent pattern khi task đủ lớn

Một agent duy nhất với task lớn thường không ổn. Context window tích lũy lịch sử thì agent bị phân tâm, output kém dần. Một pattern mình thấy lặp lại ở nhiều nơi viết về multi-agent workflow (không riêng gì một công ty nào) là chia thành ba agent riêng biệt:

# Planner — decompose spec thành tasks rời
# Mỗi task phải self-contained: agent mới pick up cold vẫn hiểu
def planner(spec: str) -> list[Task]:
    return tasks  # → task_list.json

# Generator — implement từng task trong một context window sạch
# Context reset sau mỗi task: tránh "context anxiety"
def generator(task: Task) -> Artifact:
    return artifact  # → code + claude-progress.txt

# Evaluator — grade output bằng Playwright MCP
# Không share context với Generator: tránh self-evaluation bias
def evaluator(artifact: Artifact) -> Feedback:
    return feedback  # → specific critiques, pass/fail

Generator reset context sau mỗi task vì lý do đơn giản: context cũ tích lũy thì agent bị nhiễu. Mỗi task mới cần context sạch, chỉ có task hiện tại trong đó.

Evaluator tách riêng vì nếu để Generator tự check output của nó, nó sẽ nói “ok rồi” hầu như lúc nào cũng vậy. Evaluator không biết gì về quá trình build — nên nó khắt khe hơn nhiều.

Kết

  • CLAUDE.md, hooks, skills và permission settings anh em đang có đã là một harness. Không cần làm lại từ đầu — cải thiện từng layer có chủ đích hơn là được.
  • Thêm sensors vào: eslint --fix sau mỗi file write, tsc --noEmit sau mỗi TypeScript change. Agent tự điều chỉnh, không cần anh em nhắc.
  • Task đủ lớn thì tách Planner và Evaluator ra khỏi Generator. Context sạch và self-evaluation bias là vấn đề thực, không phải lý thuyết.