---
title: "Vòng lặp giống nhau, chỗ thay được thì khác"
description: "Mọi harness đều có sổ ghi, sổ tool, ngân sách và tầng quyền. Chúng khác nhau ở chỗ cái nào bạn được phép thay — tôi đã đọc mã nguồn bốn harness để trả lời."
pubDate: 2026-09-24
author: tusgino
tags: [ai-engineering, architecture, agents, infrastructure]
series:
  id: tu-llm-den-harness
  title: "Từ LLM đến Harness"
  order: 6
cover: /images/covers/cung-mot-vong-lap-hai-kien-truc.svg
coverAlt: "Bốn vòng lặp giống hệt nhau, mỗi cái có một đường cắt ở vị trí khác nhau"
faq:
  - q: "Harness nào tốt nhất?"
    a: "Câu hỏi đó không trả lời được, và cũng không phải câu hỏi đúng. Câu đúng là: khi nó hỏng, tôi được phép thay cái gì mà không phải sửa lõi."
  - q: "Đưa vòng lặp thành plugin có phải điểm khác biệt không?"
    a: "Không, nếu chỉ nói về hình dạng. Codex có đúng cấu trúc đó nhưng để riêng tư. Khác biệt thật nằm ở chỗ cái seam ấy có với tới được từ bên ngoài hay không."
  - q: "Vì sao không so sánh tốc độ hay chất lượng?"
    a: "Vì tôi không đo được chúng. Tôi đọc mã nguồn và đối chiếu cấu trúc. Mọi so sánh ở đây là khác thế nào, không phải hơn thế nào."
  - q: "Cái giá của việc mọi thứ đều là plugin là gì?"
    a: "Muốn biết một hành vi đến từ đâu, bạn phải dump cả cây cấu hình. Bốn lớp patch chồng lên nhau, và lớp trên cùng thắng."
---

<TLDR>Mọi harness đều có bốn mảnh: sổ ghi, sổ đăng ký tool, ngân sách ngữ cảnh, tầng quyền. Chúng không khác nhau ở chỗ có mảnh nào. Chúng khác nhau ở chỗ mảnh nào bạn được phép thay.</TLDR>

## Câu hỏi đúng

Bài trước chốt rằng harness là bốn mảnh cắm rời được. Nếu điều đó đúng thì mọi harness đều có đủ bốn mảnh, và câu hỏi so sánh phải là câu khác.

Câu hỏi tôi dùng: **khi nó hỏng, tôi được phép thay cái gì mà không phải sửa lõi?**

Để trả lời, tôi đọc mã nguồn của bốn harness khác. Mỗi khẳng định dưới đây có `đường/dẫn:DÒNG` ở một commit đã ghim.

| Harness | Vòng lặp ở đâu | Thay được không |
|---|---|---|
| Codex | `codex-rs/core/src/session/turn.rs:163` | Một phần — xem bên dưới |
| Gemini CLI | `packages/core/src/core/client.ts:924` | Một phần — thân vòng lặp hard-code |
| Claude Code | Không công bố | Chỉ có hook |
| OpenHands | Không nằm trong repo này | Bằng ranh giới protocol |

## Phát hiện làm tôi phải sửa ghi chép của chính mình

Ghi chép cũ của tôi kết luận Codex **không** cho thay vòng lặp. Đúng, nhưng chưa đủ — và tôi chỉ phát hiện ra khi đi đọc nốt file tôi đã bỏ qua.

Codex có đúng cấu trúc "vòng lặp là plugin":

```rust title="codex-rs/core/src/tasks/mod.rs"
pub(crate) trait SessionTask: Send + Sync + 'static {
```

`pub(crate)`. Và chỗ duy nhất trong production gọi tới nó thì ghi cứng một kiểu cụ thể:

```rust title="codex-rs/core/src/tasks/mod.rs:532"
self.start_task(turn_context, Vec::new(), RegularTask::new())
```

Nên bên ngoài không cài được một `SessionTask` khác. Đó là lý do tôi viết "không".

Chỗ tôi sai nằm ở file tôi chưa đọc: hook `Stop`. Trong vòng lặp turn có đoạn này:

```rust title="codex-rs/core/src/session/turn.rs"
let stop_outcome = run_turn_stop_hooks(…).await;
…
if stop_outcome.should_block {
    // ghi một prompt mới vào phiên
    stop_hook_active = true;
    continue;          // vòng lặp chạy thêm một lượt
}
```

Một hook của bên thứ ba **chặn được việc dừng turn và bơm prompt cho lượt kế tiếp**. Nó không thay được vòng lặp, nhưng nó điều khiển được vòng lặp chạy tiếp hay không.

Đây là chỗ tôi phải tách một khái niệm thành hai:

<Versus aTitle="Thay vòng lặp" bTitle="Điều khiển vòng lặp">
  <div slot="a">
    <ul>
      <li>Nghĩa là: viết một implementation khác của cái vòng lặp.</li>
      <li>Codex: không. Trait là <code>pub(crate)</code>, chỗ gọi ghi cứng kiểu.</li>
      <li>Gemini CLI: không. Ba thân vòng lặp hard-code trong lõi.</li>
      <li>Claude Code: không đọc được, engine đóng.</li>
    </ul>
  </div>
  <div slot="b">
    <ul>
      <li>Nghĩa là: quyết định khi nào vòng lặp dừng, và lượt sau chứa gì.</li>
      <li>Codex: <strong>có</strong>. Hook <code>Stop</code> trả <code>should_block</code> và bơm prompt.</li>
      <li>Gemini CLI: một phần, qua hook <code>BeforeAgent</code>/<code>AfterAgent</code>.</li>
      <li>Đây mới là trục phân biệt thật.</li>
    </ul>
  </div>
</Versus>

## Ba câu trả lời khác cho cùng một câu hỏi

**Gemini CLI công bố sổ đăng ký tool, nhưng hard-code vòng lặp.** Phương thức đăng ký là công khai và có kiểu:

```ts title="packages/core/src/tools/tool-registry.ts"
registerTool(tool: AnyDeclarativeTool): void {
```

Bên thứ ba thêm được tool mà không sửa lõi. Nhưng thân vòng lặp nằm ở **ba** chỗ khác nhau trong lõi:

| Chỗ | Dòng |
|---|---|
| `packages/core/src/core/client.ts` | 924 |
| `packages/core/src/agent/legacy-agent-session.ts` | 184 |
| `packages/core/src/agents/local-executor.ts` | 685 |

Mỗi chỗ là một `while (true)` hoặc một hàm đệ quy riêng, và không chỗ nào đăng ký được.

**Claude Code công bố cái seam nhưng không công bố engine.** Repo có một file khai báo TypeScript 13.044 dòng sinh ra từ binary, liệt kê các event `turn.start`, `turn.step`, `tool.call`. Bạn móc vào được chuỗi hook. Nhưng cái chạy vòng lặp là binary đóng, và chính file khai báo đó ghi: *EARLY ACCESS: this surface may change between releases without notice.*

**OpenHands trả lời bằng ranh giới version.** Ở commit tôi đọc, repo là một client — 2.059 file TypeScript so với 9 file Python đều là CI. Vòng lặp nằm trong một image riêng:

```json title="config/defaults.json"
"versions": { "agentServer": "1.49.6" },
"compatibility": { "minimumAgentServer": "1.28.0" },
"images": { "agentServer": "ghcr.io/openhands/agent-server" }
```

Thay vì làm vòng lặp thành plugin, họ đặt nó sau một protocol và một số version. Đó là câu trả lời thứ ba, và khung "không có lõi đặc quyền" không mô tả được nó.

## Bốn lớp chồng lên nhau

Phía harness tôi đọc, cấu hình không phải một file mà là bốn lớp áp lên nhau theo thứ tự:

```diagram
type: layers
title: Bốn lớp cấu hình chồng lên nhau
desc: Một profile khởi động bằng cách áp bốn lớp patch lên một danh sách rỗng; lớp sau thắng lớp trước
nodes:
  - id: bundles
    label: Các bundle theo thứ tự profile
    kind: input
  - id: profile
    label: cordis.patch.yml của profile
  - id: home
    label: Bản patch ở cấp home
  - id: overlay
    label: Cờ --patch khi chạy
    kind: optional
    focal: true
  - id: result
    label: Cây plugin cuối cùng
    focal: true
edges:
  - [bundles, profile]
  - [profile, home]
  - [home, overlay]
  - [overlay, result]
```

Đây là cái giá đầu tiên, và nó thật: muốn biết một hành vi đến từ đâu, bạn phải dump cả cây. Bốn lớp, lớp sau ghi đè lớp trước, và không có chỗ nào liệt kê sẵn câu trả lời.

Nó cũng là lợi ích: lớp trên cùng luôn thắng, nên bạn không bao giờ bị kẹt vì một bundle đặt sai mặc định.

## Năm cái giá nữa

Đây là phần tôi muốn một bài so sánh phải có: không chỉ kể thứ chạy tốt, mà kể cả thứ phải trả.

| Lựa chọn | Cái giá |
|---|---|
| Log là nguồn, history suy ra từ nó | Không có bản sao nào để đọc nhanh |
| Event lạ thì từ chối dựng lại session | Resume hỏng to tiếng thay vì im lặng rỗng ruột |
| Sandbox chỉ áp cho filesystem | Mạng không được bảo vệ ở tầng này |
| Coverage 100% mỗi file | Chi phí này không repo nhỏ nào kham nổi |
| Format session đi trước một generation | v4 đã chốt, v3 mới là bản phát hành |

Hai dòng đầu đáng nói thêm.

Session là **append-only log**, và history gửi cho model được suy ra từ đó, không lưu riêng. Nghĩa là không có mảng nào để bạn đọc nhanh — mọi câu hỏi về quá khứ đều là một lần dựng lại.

Và chỗ này là một trong những quyết định thiết kế hay nhất tôi đọc được:

<Quote cite="dsh, docs/subsystems/session.md">Absent means required: a reader meeting an unrecognized type without this marker MUST refuse to reconstruct the session instead of silently dropping the event, because an unrecognized required event may change how the rest of the log is interpreted. … defaulting to required means a forgotten marker over-refuses (an inconvenience) rather than silently resuming a gutted session.</Quote>

Họ chọn hỏng to tiếng. Một event lạ mà thiếu cờ thì phải **từ chối** dựng lại, thay vì bỏ qua và resume một session đã bị rút ruột. Mặc định nghiêng về phía từ chối, vì từ chối là phiền, còn resume rỗng là thảm hoạ im lặng.

## Tiêu chí trước, kết luận sau

Tôi đọc ba bài Anthropic viết về thiết kế harness. Họ kể rất tốt, nhưng kể **thứ đã chạy tốt**. Ví dụ: harness đầy đủ chạy 6 giờ hết 200 đô, so với 20 phút hết 9 đô. Đó là n=1, và không lặp lại được.

Nên bài này công bố tiêu chí trước:

- **Đo bằng gì:** đọc mã nguồn ở commit đã ghim, đối chiếu cấu trúc. Không đo tốc độ, không đo chất lượng.
- **Trả lời được gì:** khác thế nào. **Không** trả lời được hơn thế nào.
- **Giới hạn:** Claude Code không đọc được engine. OpenHands không có vòng lặp trong repo. Nên hai dòng đó là suy luận, không phải quan sát.

<Callout type="warn" title="Đừng đọc bài này như một bảng xếp hạng">
Bốn harness này không chạy cùng một cuộc đua. Chúng trả lời cùng một câu hỏi theo bốn cách, và mỗi cách có một cái giá khác nhau. Cái nào đắt hơn phụ thuộc vào việc bạn sẽ phải sửa cái gì.
</Callout>

## Làm được ngay

<Steps>
<Step title="Viết ra bạn được phép thay cái gì">Không phải bạn cấu hình được gì. Mà là khi hệ thống hỏng lúc 2 giờ sáng, bạn sửa được chỗ nào mà không phải build lại lõi.</Step>
<Step title="Tách 'thay' khỏi 'điều khiển'">Hai câu hỏi khác nhau. Codex cho điều khiển vòng lặp nhưng không cho thay nó. Biết mình đang ở ô nào giúp chọn đúng cách mở rộng.</Step>
<Step title="Công bố tiêu chí trước khi so sánh">Nếu bạn không nói mình đo bằng gì và bỏ qua gì, mọi so sánh của bạn là kể chuyện.</Step>
<Step title="Kể cả cái giá, không chỉ cái hay">Mỗi lựa chọn kiến trúc đều có một hoá đơn. Bài học chỉ có giá trị khi hoá đơn được ghi kèm.</Step>
</Steps>

## Nguyên tắc

Bốn harness, bốn câu trả lời cho cùng một câu hỏi. Không cái nào đúng cho mọi người, vì chúng tối ưu cho bốn loại người dùng khác nhau.

<Quote cite="tusgino">Câu hỏi không phải "harness nào tốt nhất". Câu hỏi là "cái gì tôi được phép thay khi nó hỏng, lúc 2 giờ sáng, mà không phải build lại lõi".</Quote>

<KeyTakeaways>
- Mọi harness đều có bốn mảnh; trục phân biệt là mảnh nào bạn được phép thay, không phải có mảnh nào.
- "Vòng lặp là plugin" không độc nhất về hình dạng — Codex có đúng cấu trúc đó nhưng để `pub(crate)`.
- Tách "thay vòng lặp" khỏi "điều khiển vòng lặp": Codex cho cái thứ hai qua hook `Stop`.
- Công bố tiêu chí trước kết luận, và nói rõ chỗ nào bạn suy luận thay vì quan sát.
- Mỗi lựa chọn kiến trúc có một cái giá; bài so sánh không kể giá là bài kể chuyện.
</KeyTakeaways>
