Retrofit Automation Loop: lần đầu thử thật của tụi mình

TL;DR

  • Tụi mình viết sáu phần về vòng lặp, rồi rà soát chính bộ thiết lập của mình và nhận ra mình chẳng có cái nào. Chỉ là vài thói quen chạy bằng tay.
  • Tụi mình chuyển việc phân loại gap-log thiết kế trước tiên, vì nó có cái “done” nhỏ nhất mà máy kiểm được, rồi bọc nó bằng một file lưu trạng thái, một điều kiện dừng và một nhịp đập.
  • Công thức bốn bước ở cuối bài là thứ bạn tái dùng được. Báo cáo tuần đầu cũng nằm ở đây, gồm cả cái lỗi giả vờ thành công và cái ngày vòng lặp lặng lẽ rớt khỏi lịch chạy.

Mình dành sáu phần của loạt bài này để dạy về vòng lặp. Rồi mình chạy một buổi rà soát (audit) chính cái bộ thiết lập của tụi mình, và phát hiện một điều mình không vui gì khi phải viết ra: tụi mình chẳng có cái vòng lặp nào cả. Những thứ tụi mình quen gọi là vòng lặp thật ra chỉ là các thói quen thủ công, thứ mà sáng nào cũng có người khởi động rồi ngồi canh tới lúc xong. Đây là nhật ký hành trình thật về việc dựng cái retrofit automation loop đầu tiên của tụi mình: bọc một nhịp đập, một file lưu trạng thái và một điều kiện dừng quanh một thói quen tụi mình vốn vẫn làm tay, gồm cả những chỗ vỡ.

Cái khung mà cả loạt bài này chạy trên đó, nguyên văn từng chữ:

A loop is a recursive goal: you define a purpose and a verifiable stop condition, and the system iterates agents against it until the condition holds — without you prompting each turn.

Đọc lại định nghĩa đó mà soi vào cái “tự động hoá” của chính bạn, khoảng cách lộ ra rất nhanh. Của tụi mình thì lộ ngay. Lời hứa của phần này là một công thức bốn bước, được chứng minh qua một tuần chạy thật chứ không phải qua một sơ đồ. Nửa còn lại là khế ước trung thực: đây là một nhật ký hành trình, nên nó có cả những khởi đầu hụt, cái giá phải trả, và chỗ mà kế hoạch của chính mình đã sai. Phần vỡ của tuần đầu được ghi lại bên dưới, rút ra từ chính lịch sử chạy của vòng lặp, gồm cả phần mà mình không lường trước được.

Buổi rà soát: những thói quen trông như vòng lặp

Buổi rà soát chỉ là một câu hỏi đặt ra cho ba thói quen: cái này có chạy mà không cần mình nhắc từng lượt không? Cả ba đều trượt, và trượt theo cùng một kiểu. Chúng có một bước tạo ra thứ gì đó và một cảm giác mơ hồ về “done”, nhưng không có nhịp đập để đánh thức, không có file lưu trạng thái để nhớ tiến độ, và không có điều kiện dừng máy kiểm được. Lần nào mình cũng vừa là bộ lập lịch, vừa là bộ nhớ, vừa là điều kiện dừng. Đó không phải vòng lặp. Đó là một người cầm một danh sách việc với thiện chí tốt.

Đây lại là nghịch lý ràng buộc xuất hiện lần nữa: mấy thói quen kia tạo cảm giác làm việc hiệu quả chính bởi vì mình đang nằm trong vòng lặp, nên phần tự động hoá còn thiếu chẳng bao giờ tự lên tiếng. Chính việc viết bài kiểm tra đó ra giấy mới phơi nó ra.

Thói quenBài kiểm vòng lặp mà nó trượt
Phân loại gap-log thiết kếKhông có nhịp đập. Mình chạy nó “phiên sau”, mà chẳng biết phiên đó là khi nào.
Lượt rà soát nội dungKhông có điểm dừng máy kiểm được. Mình tự liếc mắt đánh giá “đủ tốt”.
Kiểm tra trôi lệch agent-mapCó người nhắc từng bước, từ đầu tới cuối.

Không cái nào là làm cho có. Chúng chỉ là những thói quen thủ công tử tế, khoác lên mình chữ “loop” bởi vì tụi mình viết về vòng lặp nhiều tới mức mặc định là mình có chạy chúng thật.

Chọn thói quen đầu tiên để chuyển đổi, và vì sao

Thói quen nên chuyển đầu tiên là cái có “done” nhỏ nhất mà máy kiểm được, chứ không phải cái làm mình đau đầu nhất. Lượt rà soát nội dung gây nhức hơn và nếu tự động hoá được thì nghe sẽ rất oách. Nhưng điều kiện dừng của nó là một phán đoán cảm tính, máy khó mà kiểm cho trung thực. Việc phân loại gap-log thiết kế có hình hài gọn hơn: một hàng tồn (backlog) gồm các mục lỗ hổng (gap), và một “done” mà đoạn mã (script) kiểm được, tức là mỗi lỗ hổng đều mang một phán quyết. Nhỏ, thật, và kiểm được.

Điểm mấu chốt: Vòng lặp đầu tiên đúng đắn là thói quen có “done” nhỏ nhất mà máy kiểm được, không phải cái gây đau nhất.

Tiêu chí đó tự chọn giùm mình. Việc phân loại cũng có sẵn một hàng tồn thật đang chờ, 28 đơn vị lỗ hổng nằm rải trong bốn file GAPS, nên ngay lần thức dậy đầu tiên vòng lặp đã có việc thật để làm chứ không chạy không. Nếu bắt đầu bằng một thói quen có điều kiện dừng mơ hồ thì mình sẽ phải vừa gỡ lỗi vòng lặp vừa gỡ luôn định nghĩa của “done” cùng lúc. Mình muốn mỗi lúc chỉ có đúng một thứ mới có thể vỡ.

Cách cải tạo lại, từng bước: file lưu trạng thái, điều kiện dừng, lịch chạy

Việc cải tạo (retrofit) gồm ba mảnh theo thứ tự: một file lưu trạng thái để nhớ các phán quyết, một điều kiện dừng mà đoạn mã kiểm được, rồi tới lịch chạy đánh thức cả bộ. Dựng theo đúng trình tự đó, cái retrofit automation loop ráp lại khá êm, không kịch tính gì mấy. Mỗi mảnh đều tái dùng một mảnh ghép nguyên thuỷ mà một phần trước đó đã sở hữu, nên nhịp này là lắp ráp, không phải phát minh.

File lưu trạng thái. Mỗi lỗ hổng có một mục, được đánh khoá bằng một id ổn định, ghi lại nhóm (bucket), hướng xử lý (route) và bộ não nào đã quyết. Đây chính là file lưu trạng thái từ Phần 4, chẳng có gì lạ:

{
"schema": "gaplog-triage/v1",
"updated": "2026-06-16T09:15:27Z",
"triaged": {
"75e172c0cfdd": {
"title": "<gap row text>",
"file": "V0.25-SPRINT-2A-GAPS.md",
"line": 5,
"bucket": "deferred",
"route": "park to backlog",
"brain": "deterministic"
}
}
}

Điều kiện dừng. Vòng lặp xong khi mọi lỗ hổng phát hiện được đều có một phán quyết trong file đó. Một đoạn mã kiểm điều này và thoát với mã khác 0 chừng nào còn việc, đúng y cái “done” kiểm chứng được từ Phần 3. Ngày đầu tiên nó hiện ra thế này:

$ python3 triage.py stop
STOP-CHECK: 28 units, 8 triaged, 20 pending
RESULT: NOT DONE (loop iterates)

Tám mục được phân loại ở lượt đầu, 20 mục còn chờ, điều kiện dừng báo NOT DONE. Vòng lặp có lý do để mai thức dậy lần nữa.

Lịch chạy. Nhịp đập chạy trên master, vì lịch chạy của Actions chỉ kích hoạt từ branch mặc định. Bản thân gap-log nằm trên develop, nên vòng lặp dựng nó ra ở chế độ chỉ-đọc lúc chạy chứ không commit vào master. Đó là nhịp đập từ Phần 6, chĩa vào một bảng nằm trên branch khác.

Có hai bộ não đứng sau mỗi phán quyết. Một là bộ phân loại theo từ khoá tất định (deterministic), rẻ và chạy ngoại tuyến. Cái còn lại gọi claude -p để cho agent suy luận trên lỗ hổng, và được thiết kế để tự lui về (fail soft) bộ phân loại tất định nếu câu trả lời của mô hình không phân tích cú pháp được. Cái giá thật nằm ở đây: bộ não agent rút tín dụng Agent-SDK có tính phí ở mỗi lần gọi, nên nhịp đập hằng ngày mặc định dùng bộ não tất định miễn phí, còn bộ não agent thì phải tự bật (opt-in) qua một lần kích hoạt thủ công. Các lượt chạy rỗng phải rẻ, không thì lịch chạy thành một chỗ rò rỉ tiền chầm chậm. Còn chuyện bộ não agent có thật sự hành xử đúng như mình thiết kế hay không thì là chuyện của tuần đầu, và đó là nhịp tiếp theo.

Tuần đầu vỡ những gì

Thứ vỡ đầu tiên là chính cái sổ sách của mình. File lưu trạng thái ban đầu đánh khoá mỗi lỗ hổng bằng một hàm băm (hash) của đoạn chữ trên dòng đó, nghe có vẻ ổn định cho tới khi ba lỗ hổng khác nhau hoá ra mang đoạn chữ giống hệt từng ký tự: ba dòng “DEFERRED” đều nằm chờ chính mắt mình ngó qua. Chúng băm ra cùng một id, nên file lưu trạng thái ghi một phán quyết ở chỗ lẽ ra có ba lỗ hổng, và bộ đếm của điều kiện dừng âm thầm đếm thiếu. Cách sửa là đánh khoá mỗi lỗ hổng bằng file cộng số dòng thay vì bằng đoạn chữ. Sau khi đổi, ba dòng đó mang ba id phân biệt (af542ee2bb2a, c7f4c6659a4f, e004489ea229). Một con số đếm sai là loại lỗi tệ nhất trong một vòng lặp mà cả công việc của nó là đếm dần tới “done”.

Chỗ vỡ thứ hai là chỗ mình ít tự hào nhất, vì nó trông như một thành công. Mình đã đấu thêm một bộ não thứ hai đứng sau phán quyết, một agent gọi claude -p để suy luận trên từng lỗ hổng, và lượt đối chiếu đầu tiên báo 28 trên 28 trùng khớp với bộ phân loại tất định. Mình suýt ghi luôn con số đó xuống như một kết quả. Nó là một lời nói dối. Một biểu thức chính quy tham lam (greedy regex) đã chộp nguyên lớp vỏ kết quả (result envelope) của claude -p --output-format json thay vì phán quyết bên trong của mô hình, nên mỗi lần gọi agent đều dính KeyError rồi lui về bộ phân loại tất định (bảng đếm bộ não hiện fallback:KeyError đúng 28 lần).

Vòng lặp không hề sập. Nó đầy tự tin tạo ra một sự đồng thuận vô nghĩa, tất định gật đầu với tất định. Cách sửa thì nhỏ, một file, thêm tám dòng bớt hai dòng, đẩy đi trong PR #167 và merge tại 0f42761. Bài học lớn hơn miếng vá: một phương án dự phòng (fallback) âm thầm mà trông như thành công thì nguy hiểm hơn một thất bại ồn ào, vì chẳng có gì bắt bạn phải ngó lại.

Thứ thứ ba thì không nằm trong mã nguồn của mình chút nào. Bản tóm tắt Actions của lượt chạy số 2 quay về kèm một cảnh báo ngừng hỗ trợ (deprecation) của GitHub: mấy action mình ghim (checkout@v4, setup-python@v5, upload-artifact@v4) chạy trên Node.js 20, thứ mà GitHub đang ép lên Node.js 24, với môi trường chạy cũ bị xếp lịch gỡ bỏ vào ngày 16/9/2026. Chưa có gì vỡ ngay. Nhưng nó nhắc rằng một vòng lặp không người trông cũng tích tụ sự rệu rã hạ tầng (infra rot) bên dưới: nền tảng cứ dịch chuyển bất kể có ai đang nhìn lượt chạy hay không. Mình ghi nhận lại và chưa động tay, bản thân đó cũng là một kỷ luật nhỏ, vì không phải vấn đề nào nhìn thấy cũng là vấn đề của tuần này.

Nhưng câu trả lời thật cho chuyện gì đã vỡ thì không phải mấy thứ trên. Vòng lặp chưa bao giờ chạm tới DONE. Nó rút cạn gọn gàng, từ 20 mục chờ xuống còn 2 qua bốn lượt chạy ngày-thường, rồi nhịp đập đơn giản là ngừng. Không phải vì một lỗi, không phải vì một giới hạn (cap), không phải vì một cú đẩy hỏng. Trong lần hợp nhất khi phát hành v1.0.0, workflow cùng bộ công cụ của nó bị nhấc khỏi master qua một merge kiểu “release tree wins”, và vì cây phát hành không mang theo đường dẫn đó, vòng lặp biến mất mà chẳng có một commit xoá nào đánh dấu. Bốn lượt chạy vẫn là lịch sử thật, vẫn là tổ tiên của master, nhưng cái lịch chạy đẻ ra chúng thì đã không còn. Hai lỗ hổng đông cứng đúng chỗ lượt chạy cuối bỏ lại, và chẳng có gì báo rằng vòng lặp đã không còn sống. Đó là kịch bản thất bại (failure mode) lặng lẽ nhất trong tất cả: không phải một vòng lặp báo lỗi, mà một vòng lặp lặng lẽ không-còn-ở-đó nữa.

Cái tụi mình giữ, cái đổi, cái khiến tụi mình bất ngờ

Cái tụi mình giữ. Bộ não tất định vẫn là mặc định hằng ngày. Nó rẻ, chạy ngoại tuyến, và tái lập được: cùng một gap-log sẽ cho ra cùng những phán quyết ở mỗi lượt chạy, đúng cái bạn cần ở thứ tự thức dậy không người trông mỗi ngày trong tuần. Cái giữ lại thứ hai là một tính chất mình cố ý không tự-động-hoá-cho-mất-đi: vòng lặp không bao giờ tự hành động. Suốt cả bốn lượt chạy, tập passed luôn rỗng. Việc phân loại định tuyến mỗi lỗ hổng vào một hộp chờ của con người và tự nó chẳng merge gì cả. Một vòng lặp rút cạn hàng tồn thành những quyết định mà con người vẫn là người ra, đó là mức tự chủ vừa đủ để tin tưởng ở giai đoạn sớm này.

Cái tụi mình đổi. Hai thứ, cả hai đều do chỗ vỡ bên trên ép phải làm. Lược đồ id chuyển từ hàm băm của đoạn chữ sang file cộng số dòng, nên những lỗ hổng trông giống nhau thôi gộp nhầm vào một phán quyết. Và bộ phân tích cú pháp (parser) của bộ não agent được sửa để đọc đúng phán quyết bên trong của mô hình từ lớp vỏ kết quả của claude -p thay vì chộp luôn cả lớp vỏ (PR #167). Cả hai thay đổi đều không đụng tới hành vi của nhịp đập hằng ngày, vì mặc định hằng ngày là tất định còn bộ não agent thì vẫn phải tự bật.

Cái khiến tụi mình bất ngờ. Một khi bộ phân tích cú pháp được sửa và bộ não agent thật sự tự đưa ra phán quyết, nó không chỉ chép lại bộ phân loại theo từ khoá. Trên một tập con nhỏ các lỗ hổng, nó bất đồng với những hướng xử lý tất định, và khi mình đọc các điểm bất đồng đó thì hướng của agent lại là hướng tốt hơn: có một ca nó nhận ra rằng hình dạng của một prop là đúng theo đặc tả (spec) và định tuyến lỗ hổng đó sang won’t-fix, trong khi bộ não từ khoá thì dừng lại ở chữ “defer”. Agent suy luận trên lỗ hổng; bộ não từ khoá thì khớp chuỗi ký tự. Đó là phát hiện bền vững.

Kèm theo nó phải là một lưu ý trung thực: tập con chỉ có 6 lỗ hổng, một lượt chạy duy nhất, và claude -p chạy không có kiểm soát seed hay temperature, nên con số chính xác về các điểm bất đồng dao động từ lượt này qua lượt khác. Khẳng định về mặt định tính (suy luận so với khớp) thì vững. Con số cụ thể thì không, nên mình sẽ không in nó ra như thể nó ổn định.

Mình cũng nên cẩn thận y vậy với chi phí. Khoản chi có tính phí của mấy lượt chạy agent đó chỉ nằm trong vùng nháp của phiên làm việc của mình và đồng hồ đo (meter) Pool 2, nên mình chỉ có thể báo cáo nó một cách ước lượng: cỡ khoảng vài chục lần gọi claude -p có tính phí trong lượt đối chiếu đầy đủ 28 lỗ hổng đầu tiên, và thêm một nhúm nữa trong lượt chạy lại 6 lỗ hổng đã vá. Hãy coi mấy con số đó là được thuật lại, không phải được đo. Mình sẽ không nâng một cái liếc ước chừng trên đồng hồ đo thành một con số chắc nịch, và các phán quyết agent theo từng lỗ hổng đứng sau chúng cũng nằm cùng một chỗ, nên chúng nhận cùng một mức thận trọng.

Bất ngờ lớn nhất. Nó không nằm ở bất kỳ lượt chạy đơn lẻ nào. Nó là cái chết lặng lẽ ở mục trước: vòng lặp rút gần tới done rồi biến mất, mà chẳng có gì báo cho mình. Mình đã dựng bộ nhớ, một điều kiện dừng, và một nhịp đập, nhưng mình lại chưa dựng đúng cái phép kiểm tra lẽ ra bắt được chuyện này, là vòng lặp có còn được lên lịch hay không. Một vòng lặp không người trông cần một phép tự kiểm (meta-check) lên chính mạch đập của nó. “Nhịp đập còn sống không?” tự nó là một thứ phải theo dõi, và đó là mảnh mình sẽ mang theo vào vòng lặp tiếp theo mà mình cải tạo.

Retrofit automation loop: công thức bốn bước

Công thức ngắn thôi, vì phần việc chủ yếu là nhận ra. Một retrofit automation loop là bất kỳ thói quen thủ công nào cộng thêm ba thứ biến một thói quen thành vòng lặp: bộ nhớ, một điểm dừng kiểm được, và một nhịp đập. Tìm ra thói quen đó, rồi thêm ba mảnh theo thứ tự.

  • 1. Tìm thói quen thủ công lặp lại. Cái bạn làm “phiên sau”, theo một nhịp độ, bằng tay. Nếu bạn chính là bộ lập lịch của nó, nó là một ứng viên.
  • 2. Cho nó một file lưu trạng thái. Một bảng nó đọc và ghi, để tiến độ sống sót giữa các lượt chạy và lượt thứ hai không làm lại lượt đầu.
  • 3. Cho nó một điều kiện dừng máy kiểm được. Một đoạn mã trả lời done hay not-done mà không cần bạn phán đoán. Chọn thói quen có “done” nhỏ nhất và dễ kiểm nhất.
  • 4. Cho nó một nhịp đập. Một lịch chạy đánh thức nó. Làm cho các lượt chạy rỗng thật rẻ, để nhịp độ gần như chẳng tốn gì khi không có việc.

Điều rút ra gọn trong một dòng: hãy tìm cái thói quen thủ công lặp lại trong chính harness của bạn, vì thói quen đó là một vòng lặp đang chờ một nhịp đập.

Chỗ này để lại cho tụi mình điều gì

Lần đầu tiên một trong những “loop” của tụi mình thật sự vượt qua bài kiểm tra mà tụi mình viết ra cho nó. Nó có một nhịp đập, một file lưu trạng thái, và một điểm dừng kiểm được, và nó chạy không người trông suốt bốn lượt ngày-thường, rút hàng tồn từ 20 mục chờ về phía 2 mà mình không nhắc một lượt nào. Cái đó là thật, và nó nhỏ hơn cũng đã đời hơn cái bản màn ăn mừng chiến thắng mà mình suýt viết thay vào.

Rồi nó dạy bài học cuối cùng bằng cách biến mất. Bản phát hành v1.0.0 nhấc workflow khỏi master, và nhịp đập ngừng lại mà không có một lỗi nào báo ra. Nên cái kết trung thực không phải là “vòng lặp đang chạy”, mà là bài học cấp trên: một vòng lặp không người trông cần một phép kiểm tra rằng bản thân vòng lặp có còn sống, vì kịch bản thất bại lặng lẽ nhất chính là khi nhịp đập đơn giản là biến mất. Phần kế tiếp đóng gói cái hình hài này thành một bộ công cụ tái dùng được, loop-devkit-fixkit-blueprint, và phép kiểm mạch đập đó là thứ đầu tiên nó phải mang theo.

FAQ

Retrofit automation loop là gì?

Đó là một thói quen thủ công có sẵn được chuyển thành một vòng lặp thật bằng cách thêm ba thứ: một file lưu trạng thái để làm bộ nhớ, một điều kiện dừng máy kiểm được, và một nhịp đập đánh thức nó theo lịch. Bạn không dựng tự động hoá từ con số không. Bạn đang bọc một thói quen bạn vốn đã chạy để nó tự chạy được.

Làm sao phân biệt một vòng lặp thật với một thói quen thủ công?

Hỏi đúng một câu: nó có chạy mà không cần bạn nhắc từng lượt không? Nếu bạn là bộ lập lịch, là bộ nhớ, hay là thứ quyết định khi nào xong, thì đó là một thói quen. Một vòng lặp thật tự đánh thức mình, nhớ tiến độ trong một file lưu trạng thái, và dừng theo một điều kiện mà đoạn mã kiểm được.

Nên chuyển thói quen nào trước?

Cái có “done” nhỏ nhất mà máy kiểm được, không phải cái đau nhất. Một thói quen mà “done” của nó là phán đoán cảm tính sẽ buộc bạn vừa gỡ lỗi vòng lặp vừa gỡ định nghĩa của done cùng lúc. Chọn một thói quen mà đoạn mã trả lời được done hay not-done, và có sẵn một hàng tồn thật đang chờ để lượt chạy đầu không rỗng.

Tuần đầu của một vòng lặp lên lịch thường vỡ những gì?

Những kịch bản thất bại đáng canh chừng ở bất kỳ vòng lặp lên lịch nào là: những cái “done” giả, sự trôi lệch của file lưu trạng thái giữa các lượt, chạm phải giới hạn vòng lặp hay ngân sách, và những phương án dự phòng âm thầm mà trông như thành công. Tuần đầu của chính tụi mình dính bốn trong số đó ở những hình hài khác nhau: một con số đếm sai vì va chạm id, một phương án dự phòng âm thầm trông như thành công, sự rệu rã hạ tầng chầm chậm dưới trình chạy (runner), và cái lặng lẽ nhất, vòng lặp bị nhấc hẳn ra khỏi lịch chạy trong một bản phát hành chẳng liên quan. Quy luật là: những thất bại nguy hiểm đều là những cái âm thầm, những chỗ vỡ không hề báo lỗi. Xem mục “Tuần đầu vỡ những gì” bên trên để có toàn bộ chi tiết từng cái.

Mình có cần một branch riêng cho trạng thái của vòng lặp không?

Còn tuỳ lịch chạy kích hoạt từ đâu. Nhịp đập của tụi mình chạy trên master, vì lịch chạy của Actions chỉ kích hoạt từ branch mặc định, trong khi dữ liệu nguồn nằm trên develop. Nên vòng lặp đọc dữ liệu ở chế độ chỉ-đọc từ develop lúc chạy và giữ trạng thái của riêng nó trên master. Nếu dữ liệu và lịch chạy của bạn vốn đã chung một branch, bạn không cần tách chúng ra.

Đọc tiếp gì