---
title: "Harness bắt đầu từ chỗ nào"
description: "Vòng lặp chỉ là bộ điều phối. Thứ biến nó thành harness là bốn mảnh cắm rời được, và trong một harness thật cả bốn đều là plugin thay được từ cấu hình."
pubDate: 2026-09-23
author: tusgino
tags: [ai-engineering, architecture, agents, infrastructure]
series:
  id: tu-llm-den-harness
  title: "Từ LLM đến Harness"
  order: 5
cover: /images/covers/harness-bat-dau-tu-cho-nao.svg
coverAlt: "Cây plugin của một harness với bốn mảnh lõi được tô đậm giữa các plugin khác"
faq:
  - q: "Vòng lặp và harness khác nhau ở đâu?"
    a: "Vòng lặp chỉ điều phối: gọi model, chạy tool, lặp. Harness là phần còn lại — nơi lịch sử được ghi bền, tool được đăng ký, ngân sách ngữ cảnh được giữ, và quyền được kiểm."
  - q: "Vì sao phải tách thành plugin thay vì một khối?"
    a: "Vì bạn cần thay một mảnh mà không chạm ba mảnh còn lại. Một khối liền buộc bạn sửa vòng lặp mỗi lần đổi model, đổi cách lưu, hay đổi chính sách quyền."
  - q: "Có lõi đặc quyền nào phải vá không?"
    a: "Trong harness tôi đọc thì không. Mọi thứ kể cả model adapter, sổ ghi và chính vòng lặp đều là plugin, nên mỗi thứ thay được từ cấu hình."
  - q: "Bề mặt model thấy lớn cỡ nào?"
    a: "Nhỏ hơn nhiều so với khối đứng sau. Trong harness tôi đọc: 338 package trong workspace, nhưng chỉ 70 tên tool mà model nhìn thấy."
---

<TLDR>Vòng lặp chỉ là bộ điều phối. Bốn mảnh biến nó thành harness là sổ ghi bền, sổ đăng ký tool, đường ngân sách ngữ cảnh, và tầng quyền — và cả bốn đều phải cắm rời được.</TLDR>

## Vòng lặp không phải harness

Bài trước kết thúc ở chỗ vòng lặp dài 101 dòng và phần dôi ra là những chỗ nó từng vỡ. Nhưng 101 dòng đó vẫn chưa phải một harness.

Vòng lặp làm đúng một việc: điều phối. Nó gọi model, chạy tool, lặp. Nó không biết lịch sử được ghi ở đâu, cũng không biết tool đến từ đâu. Nó không biết ai giữ ngân sách ngữ cảnh, và không biết ai cho phép một lời gọi chạy.

Bốn câu hỏi đó là bốn mảnh. Trong một harness thật, cả bốn đều là plugin — nghĩa là cả bốn đều thay được mà không sửa vòng lặp.

## Bốn mảnh, và chúng nằm ở đâu

Trong harness tôi đọc, mỗi profile khởi động bằng cách nạp **94 dòng cấu hình plugin** (gọi tắt là row) từ một bundle nền. Bốn mảnh cốt lõi nằm rải trong danh sách đó, mỗi mảnh mang một `id` riêng để có thể ghi đè (patch) qua cấu hình:

| Mảnh | Mục cấu hình (Row) | Nhiệm vụ |
|---|---|---|
| Sổ ghi bền | `session`, `session-persistence-jsonl` | Lưu mọi sự kiện model từng thấy |
| Sổ đăng ký tool | `tool-*` (17 mục) | Khai báo tool nào tồn tại |
| Ngân sách ngữ cảnh | `compaction-basic` | Giữ request vừa cửa sổ |
| Tầng quyền | `approval`, `permission`, `sandbox` | Quyết định cái gì được chạy |

Bốn cái tên, bốn mục cấu hình độc lập, không cái nào nằm cứng trong thân vòng lặp.

Đó là điểm mấu chốt. Vòng lặp chỉ gọi `ctx.tools.execute()` mà không cần biết tool đó là lệnh bash nội bộ hay một MCP server ở nửa vòng trái đất. Nó ghi sự kiện vào session mà không bận tâm session đang nằm trên RAM hay file JSONL dưới đĩa.

## Thay một mảnh mà không sửa code

Đây là chỗ luận đề trở nên kiểm chứng được thay vì chỉ nghe hợp lý.

Mỗi mục cấu hình đều có một `id`. Muốn đổi hành vi của một plugin, bạn viết một bản vá (patch) trỏ vào đúng `id` đó. Bản vá sẽ thay **toàn bộ** cấu hình của plugin thay vì gộp lẫn lộn — nhờ vậy kết quả luôn xác định và không phụ thuộc thứ tự đọc file.

```yaml title="cordis.patch.yml — đổi một row, không chạm source"
# Row gốc trong bundle chỉ có tên, không có config:
#   - id: compaction-basic
#     name: '@deepseek-ai/dsh-compaction-basic'

- id: compaction-basic
  config:
    thresholdRatio: 0.8    # mặc định của package
    retainRatio: 0.16      # phần hội thoại gần nhất giữ nguyên văn
```

Hai khóa đó đều là tham số thật của package, và giá trị trên là mặc định của tác giả bundle. Cái bạn viết vào patch là của bạn.

Và tài liệu kiến trúc nói thẳng hệ quả:

<Quote cite="dsh, docs/architecture.md">There is no privileged core to patch: you extend dsh by mounting a plugin beside the others, and registrations are effects that unwind when their plugin unloads.</Quote>

Hai vế, và vế thứ hai quan trọng ngang vế thứ nhất. Plugin đăng ký thứ gì thì khi gỡ nó ra, thứ đó **tự rút lại**. Không còn tool mồ côi, không còn listener treo lại sau khi plugin chết. Đây là điều kiện để việc thay mảnh diễn ra lúc chạy, chứ không chỉ lúc build.

## 338 package, 70 tên tool

Bây giờ tới con số làm rõ cả bài.

Harness tôi đọc có **338 package** trong workspace, trải trên **55 nhóm** package, và **14.004 file** được track. Nhưng bề mặt mà model nhìn thấy chỉ là **70 tên tool** trong catalog.

Khoảng cách đó không phải lãng phí. Nó chính là định nghĩa của harness.

Một harness tốt giữ cho bề mặt hướng ra model **nhỏ và ổn định**, trong khi khối đứng sau lớn và thay đổi được. Model thấy một danh sách tool ổn định. Phía sau, mỗi tool trong đó có thể đến từ bundle nền, từ plugin ngoài cây, hoặc từ một MCP server. Model không phân biệt được, và không cần phân biệt.

Nếu bề mặt đó lớn lên theo khối đứng sau, bạn không có harness. Bạn có một đống package lộ ra ngoài.

```diagram
type: architecture
title: Cây plugin của một harness
desc: Từ 338 package workspace, một profile cắm 94 row plugin; bốn mảnh lõi nằm rải trong đó và đều thay được từ cấu hình
nodes:
  - id: profile
    label: Profile (web, headless, sdk)
    kind: external
  - id: bundle
    label: Bundle nền, 94 row
    kind: input
  - id: log
    label: Sổ ghi bền
    kind: store
    focal: true
  - id: registry
    label: Sổ đăng ký tool
    focal: true
  - id: budget
    label: Ngân sách ngữ cảnh
  - id: perm
    label: Tầng quyền
    kind: security
  - id: surface
    label: 70 tên tool model thấy
    kind: external
edges:
  - [profile, bundle]
  - [bundle, log]
  - [bundle, registry]
  - [bundle, budget]
  - [bundle, perm]
  - [registry, surface]
  - [perm, surface]
```

## Bốn profile, một bundle nền

Một chi tiết đáng chú ý: các profile không phải bốn bản sao. Chúng xếp lớp trên **cùng một bundle nền** — `dsh-base`, 94 row — rồi mỗi profile thêm phần của mình.

| Profile | Số row riêng | Thêm gì |
|---|---|---|
| web | 112 | Ứng dụng trình duyệt |
| sdk-minimal | 32 | Cây SDK tối giản, cố ý không dùng bundle nền |
| sdk | 7 | Server JSON-RPC |
| headless | 5 | Runner một lần, không server |
| acp | 5 | Server tự động hoá |

Nhìn `headless`: năm row. Một harness chạy một lần rồi thoát chỉ cần thêm năm dòng cấu hình lên trên bundle nền. Không có nhánh `if (headless)` nào trong vòng lặp.

Đó là bài kiểm tra tôi dùng cho mọi thiết kế harness: **thêm một chế độ chạy mới tốn bao nhiêu dòng trong lõi?** Nếu câu trả lời lớn hơn không, các mảnh chưa thật sự rời.

<Callout type="warn" title="Đừng nhầm cấu hình được với plugin được">
Một file YAML toàn tham số vẫn là một khối liền nếu bạn không chèn được thành phần mới vào đó. Phép thử không phải "đổi được con số không", mà là "cắm được một package chưa từng tồn tại vào không, mà không sửa mã nguồn".
</Callout>

## Làm được ngay

<Steps>
<Step title="Kể tên bốn mảnh trước khi viết vòng lặp">Chốt sổ ghi, sổ đăng ký, ngân sách, và quyền là bốn interface riêng. Nếu bạn chưa gọi tên được chúng, vòng lặp sẽ tự nuốt chúng.</Step>
<Step title="Cho mỗi mảnh một cái tên trong cấu hình">Một `id` ổn định cho mỗi thành phần, để patch trỏ vào được. Không có tên thì không có chỗ để thay.</Step>
<Step title="Đo bề mặt model thấy, và giữ nó nhỏ">Đếm số tool và số trường model nhìn thấy. Con số đó nên tăng chậm hơn nhiều so với số package đứng sau.</Step>
<Step title="Thử gỡ một plugin xem có sạch không">Cắm rồi gỡ. Nếu còn tool mồ côi hay listener treo, việc đăng ký chưa phải một effect rút lại được.</Step>
</Steps>

## Nguyên tắc

Vòng lặp là phần dễ nhất của một harness, và cũng là phần ít quyết định nhất. Thứ quyết định là bốn mảnh quanh nó có cắm rời được hay không.

<Quote cite="tusgino">Nếu bạn không thay được model adapter mà không sửa vòng lặp, bạn chưa có harness. Bạn có một vòng lặp với đèn báo.</Quote>

<KeyTakeaways>
- Vòng lặp chỉ điều phối; harness là sổ ghi, sổ đăng ký, ngân sách ngữ cảnh và tầng quyền.
- Cho mỗi mảnh một `id` ổn định trong cấu hình, để thay hành vi bằng patch thay vì sửa mã nguồn.
- Giữ bề mặt hướng ra model nhỏ và ổn định: 338 package đứng sau, 70 tên tool model thấy.
- Đăng ký phải là một effect rút lại được, nếu không thì gỡ plugin sẽ để lại rác.
- Phép thử cuối: thêm một chế độ chạy mới tốn bao nhiêu dòng trong lõi? Câu trả lời đúng là không dòng nào.
</KeyTakeaways>
