自幹作業系統 - Context Switch


自幹作業系統 - Simple OS 學習筆記來到第五篇,上一篇整理了 Task 的定義、排程器概念、以及排程演算法,順著這樣的思路,往下深挖一個很重要的概念:Context Switch,中文翻譯成 上下文切換內容轉換環境切換,我自己習慣翻譯 上下文切換 。對應到 課程大綱 大約在 day31 ~ 36 的範圍。

本文將整理以下內容:

  1. 什麼是 Context Switch?它帶來的成本 負擔 (Overhead) 是什麼?
  2. Scheduler 怎麼決定 Switch 的?
  3. SimpleOS 是怎麼實作 Context Switch 的?

除了在作業系統裡,Context Switch 也可以用在人身上,代表人處於同時在很多任務之間切換,因而無法專注的現象,背後隱含著成本很高的意思。我很久以前寫的 Cost in Context Switch 就是在描述工作頻繁被干擾,造成無法專注,這種極端現象帶來的影響,以及可能的解決方法。類似概念在 Peopleware:腦力密集產業的人才管理之道 以及 心流:高手都在研究的最優體驗心理學 這些書裡也有提到。

最後再來聊聊 AI 話題,這次整理 關於 Harness Engineering 的想法與相關資料,因為概念跟我在自幹作業系統實在太像了。

自幹作業系統 系列:

註:內容僅是自學的一些筆記,如果有發現資訊不正確,或者描述錯誤,請不令給予指教,感謝。


一、什麼是 Context Switch?它帶來的成本 負擔 (Overhead) 是什麼?

定義

Context Switch (上下文切換) 是作業系統實現多工 (Multitasking) 的核心機制。它的本質是:

把 CPU 從一個行程「借走」,交給另一個行程使用,並讓雙方都以為自己一直在跑。

「Context (上下文) 」指的是一個行程在某個時間點的完整執行狀態,包含:

  • 通用暫存器:EAX、EBX、ECX、EDX、ESI、EDI、EBP
  • 程式計數器 (EIP) :下一條要執行的指令在哪
  • 旗標暫存器 (EFLAGS, Extended Flags Register) :運算結果與中斷開關狀態
  • 堆疊指標 (ESP, Extended Stack Pointer) :目前的呼叫堆疊深度
  • 記憶體空間 (CR3, Control Register 3) :存放 Page Directory 實體位址。虛擬位址對應到哪塊實體記憶體

這些資訊加起來,就是一個行程「靈魂」的全部。Context Switch 就是把靈魂從一個殼換到另一個殼。

通用暫存器名稱對照表參閱 Source Code 的 Terms.md 整理

觸發時機

在 Simple OS 裡,Context Switch 有兩種觸發方式:

  1. 搶佔式 (Preemptive) :由 PIT (可程式化間隔計時器,8254 晶片) 每 10ms 產生一次 IRQ0 中斷,強制叫醒 排程器 (Scheduler)。這讓任何一個行程 (Process) 都不能霸佔 CPU,即使它是個無限迴圈如下 timer.c,更多參閱 自幹作業系統 - 初探 Timer 原理 的詳細介紹。
    1
    2
    3
    4
    5
    6
    7
    // timer.c — 每一個 tick (10ms) 就強制執行一次排程
    void timer_handler(void) {
    tick++;
    // ... 略 ...
    outb(0x20, 0x20); // 通知 PIC:中斷處理完畢 (EOI)
    schedule(); // 強制切換,實現搶佔式多工
    }
  2. 主動讓出 (Voluntary) :行程呼叫 sys_wait()sys_sleep() 等 syscall,自願放棄 CPU 使用權。
    1
    2
    3
    4
    5
    6
    7
    8
    int sys_sleep(uint32_t ms) {
    // ... 略 ...
    current_task->wake_up_tick = tick + ticks_to_sleep;
    current_task->state = TASK_SLEEPING;

    schedule(); // 讓出 CPU,schedule 處理 cli/sti
    return 0;
    }

成本分析

Context Switch 不是免費的,它的代價主要來自三個層面:

  1. 暫存器存取: pusha / popa 指令需要讀寫 8 個通用暫存器,加上 EFLAGS
  2. TLB 失效: 切換 CR3 (分頁目錄) 會清空 TLB (Translation Lookaside Buffer),導致後續記憶體存取全部 Page Miss,要重新走一遍 Page Table
  3. 快取污染: 新行程的 工作資料集 (Working Set) 和舊行程完全不同,L1/L2 Cache 大量失效

TLB 是 Context Switch 最昂貴的部分。TLB 是 CPU 用來快取 虛擬 / 實體 位址對應的硬體,容量很小 (通常 64–1024 條) 。一旦切換到不同位址空間,整個 TLB 失效,接下來幾百條記憶體存取都得重新查 Page Table,速度驟降。

這也是為什麼現代 OS 盡量減少不必要的 Context Switch,帶表 每次切換都是一筆看不見的效能稅


二、排程器如何決定「切換給誰」?

schedule() 是 Context Switch 的大腦,在每一個 timer tick 或行程主動讓出時被呼叫。它的工作分三步:

步驟 1:垃圾回收 + 喚醒睡眠任務

在真正排程之前,先清理戰場:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void schedule() {
// 關閉中斷,避免 Race Condition
__asm__ volatile("cli");

task_gc(); // 釋放已標記為 TASK_DEAD 的 PCB 與 Kernel Stack

// 掃描睡眠任務,時間到了就喚醒
if (ready_queue) {
task_t *iter = (task_t*)ready_queue;
do {
if (iter->state == TASK_SLEEPING && tick >= iter->wake_up_tick) {
iter->state = TASK_RUNNING;
}
iter = iter->next;
} while (iter != first && iter != 0);
}
// ...
}

步驟 2:Round-Robin 輪轉找下一個行程

所有行程串成一個環狀鏈結串列 (Circular Linked List) 。排程器從當前行程的「下一個」開始掃,找到第一個 TASK_RUNNING 的就選它:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 從當前行程的 next 開始找,公平輪轉
task_t *start_node = (current_task == idle_task)
? (task_t*)ready_queue
: current_task->next;

task_t *iter = start_node;
do {
if (iter->state == TASK_RUNNING) {
next_run = iter;
break;
}
iter = iter->next;
} while (iter != start_node);

如果沒有任何行程可以跑,就切換到 idle_task (PID 9999) ,它只會執行 sti; hlt,讓 CPU 省電等待下一個中斷。

步驟 3:執行 Context Switch

找到目標行程後,更新 Task State Segment (TSS) 的 kernel stack 指標,然後把控制權交給組合語言:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
task_t *prev = (task_t*)current_task;
current_task = next_run;

if (current_task != prev) {
// 更新 TSS.esp0
// @ref: src/kernel/lib/gdt.h#set_kernel_stack
set_kernel_stack(current_task->kernel_stack);

// switch_task() 由組合語言處理
// @ref: src/kernel/arch/x86/switch_task.S
switch_task(
(uint32_t*)&prev->esp, // 儲存舊行程的 ESP
(uint32_t*)&current_task->esp, // 載入新行程的 ESP
current_task->page_directory // 新行程的 CR3
);
}

set_kernel_stack() 的作用是更新 TSS (Task State Segment) 裡的 esp0 欄位。當新行程從 Ring 3 觸發系統呼叫進入 Ring 0 時,CPU 會用這個值來設定 Kernel Stack,確保每個行程都有自己獨立的核心堆疊空間。


三、組合語言如何完成最後一步切換?

為什麼需要組合語言?

這是核心問題。理論上,C 語言可以操作大部分硬體,但有幾件事 C 語言做不到或做了會出問題:

  1. 直接操作 ESP (堆疊指標) :一旦 C 函式修改了 esp (堆疊指標暫存器),整個呼叫堆疊就垮了——區域變數找不到、返回位址不見了。這件事只能用組合語言在 C 的呼叫慣例之外完成。
  2. 直接寫入 CR3:CR3 是分頁目錄的實體位址,寫入 CR3 會立即切換虛擬位址空間並清空 TLB。這是特權指令 (只有 Ring 0 能執行) ,且必須在正確時序下操作,中間不能有任何 C 的堆疊假設。
  3. 精確控制暫存器pusha/popa 一次存取 8 個暫存器,語意清晰、不帶副作用。C 語言的 inline asm 雖然也能操作暫存器,但要在 C 函式「外部」保存/恢復全部暫存器,只有純組合語言才能確保不汙染任何狀態。

switch_task.S 逐行解析

1
2
3
4
; extern void switch_task(uint32_t *current_esp, uint32_t *next_esp, uint32_t next_cr3);
switch_task:
pusha ; 一次壓入 8 個通用暫存器 (EAX, ECX, EDX, EBX, ESP, EBP, ESI, EDI)
pushf ; 壓入 EFLAGS (包含中斷開關狀態 IF)

此時舊行程的完整 CPU 狀態已全部壓在它的 Kernel Stack 上。

1
2
3
; 在換掉 ESP 之前,先把第三個參數 (next_cr3) 存到 EDX
; 換掉 ESP 之後就再也找不到舊堆疊上的參數了!
mov edx, [esp + OFFSET_NEXT_CR3] ; OFFSET = 40(pusha) + 4(pushf) + 4(return EIP) = 48

這是整個函式最微妙的地方。函式參數是放在「呼叫時的舊堆疊」上,一旦 ESP 切換到新行程的堆疊,就再也讀不到這些參數了。所以必須在切換 ESP 之前next_cr3 偷渡到暫存器 EDX 裡。

1
2
3
4
5
6
7
; 把當前 ESP 值存入 prev->esp (第一個參數是指向 prev->esp 的指標) 
mov eax, [esp + OFFSET_CURRENT_ESP]
mov [eax], esp

; 載入 next->esp (切換到新行程的 Kernel Stack)
mov eax, [esp + OFFSET_NEXT_ESP]
mov esp, [eax] ; ← 這一行過後,我們已經在新行程的堆疊上了

mov esp, [eax] 這一行是整個切換的「宇宙奇點」。執行前是舊行程,執行後 CPU 就活在新行程的堆疊裡了。

1
2
3
4
5
6
7
; 現在把暫存的 next_cr3 寫入 CR3,切換虛擬位址空間
mov cr3, edx ; ← TLB 全部失效,位址空間正式切換

popf ; 恢復新行程的 EFLAGS
popa ; 恢復新行程的 8 個通用暫存器

ret ; 返回到新行程上次被中斷的位置

ret 指令從堆疊彈出返回位址 (EIP) ,而這個 EIP 是新行程上次被 switch_task 呼叫時壓入的,所以 CPU 就這樣神不知鬼不覺地回到了新行程「上次停下來的地方」,繼續執行。

新行程第一次被排程:child_ret_stub

對於一個剛被 fork()create_user_task() 建立、從未執行過的行程,它的 Kernel Stack 上沒有真實的歷史返回位址,需要手動偽造。

在建立行程時,task.c#create_user_task(), fork() 會在 Kernel Stack 上手工佈置一個完整的堆疊幀:

1
2
3
4
5
6
7
8
9
10
11
12
13
// create_user_task() / sys_fork() 中的堆疊佈置
*(--kstack) = USER_DS; // User SS (iret 用)
*(--kstack) = (uint32_t)ustack; // User ESP (iret 用)
*(--kstack) = EFLAGS_DEFAULT; // EFLAGS (iret 用)
*(--kstack) = USER_CS; // User CS (iret 用)
*(--kstack) = entry_point; // User EIP (iret 用)

*(--kstack) = 0; // EBP (child_ret_stub 會 pop 這些)
*(--kstack) = 0; // EBX
*(--kstack) = 0; // ESI
*(--kstack) = 0; // EDI

*(--kstack) = (uint32_t) child_ret_stub; // ← switch_task 的 ret 跳到這裡

switch_taskret 彈出返回位址時,CPU 就跳進 child_ret_stub

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
child_ret_stub:
pop edi ; 彈出手工放的暫存器初始值
pop esi
pop ebx
pop ebp

; 換上 User Data Segment (Ring 3 的資料段)
mov cx, 0x23
mov ds, cx
mov es, cx
mov fs, cx
mov gs, cx

mov eax, 0 ; 子行程 fork() 返回值 = 0

; [很重要的關鍵]
; iret:從 Ring 0 跳回 Ring 3
; CPU 從 Stack 取出 EIP, CS, EFLAGS, ESP, SS
; 完成 Kernel → User Space 的特權降級
iret

iret 是降級的魔法咒語。它一次從堆疊彈出 EIP、CS、EFLAGS (以及跨特權時的 ESP 和 SS) ,讓 CPU 從 Ring 0 飛回 Ring 3 的程式碼,開始執行使用者程式。


整體流程回顧

Context Switch 最神奇的地方在於:

對每一個行程來說,它從未「感覺」自己被暫停過:它只是「從 switch_task 的呼叫點返回了」,而這個返回,可能已經過了幾十個 tick、幾百個毫秒。

下圖是用 ChatGPT 整理的整體流程:


小結

Context Switch 是作業系統裡 C 與組合語言最精密的合作:C 語言負責「想清楚」,組合語言負責「做乾淨」。理解這個界線,就理解了為什麼作業系統必須從最底層開始寫起。

下圖用 ChatGPT 整理硬體、排程器、組合語言、偽造堆疊幀四個層次彼此之間的角色以及關鍵程式碼:

心得筆記

越到底層,整理相關資訊越是辛苦。即使已經有 AI 幫忙,但有不少概念看了好幾次還是不太懂,特別是那一堆通用暫存器,如 EIP、ESP、EFLAGS … 等。但有趣的是這些正式研究作業系統背後的主要動機,看到不懂的,或者反覆出現的,代表後面越是有故事可以說。挖下去就一堆東西出來 XD

然後整理探索筆記一開始只是想了解一下 sleep 實作,然後就越挖約多,越挖越有趣。sleep 已經實作好了,但是 sleep 其實站在很多基礎點上,這些點如果沒搞清楚,這個實作意義就不大了。反正 AI 瞬間就可以寫出一個類似的東西。

回到我自己的動機:用 AI 輔助我學習。我也在 Gemini CLI / Claude Code / Codex 這三個主流的 Agentic 來回嘗試,當遇到問題 Gemini 回答得不好、或者我看不懂,就把問題或者回答結果,丟給 ChatGPT / Claude,如此反覆,從而探索到更多有趣的知識。最後 ChatGPT 幫忙畫圖 (剛好趕上 Image2 出來),我在文字與圖反覆調整與更正,從而對整個作業系統更有感。

下一篇終於可以整理 sleep 的實作了 XDD

用 AI 學習的過程,有沒遇到幻覺問題?

自幹作業系統到現在,已經一個多月,我整個過程都是在 Gemini (Web) 上,同一個對話,以 Prompt 的方式學習。同一個對話匡的對話內容,從 Day1 ~ Day99 所有的課程內容,包含 Debug 的過程、章節總結、段落總結 … 等。那過程中有沒出現幻覺?

其實是有,但嚴格來講,對我來講,影響不大。我的方法是,過程中我會反覆把現在程式碼、課程的學習重點與目標、在我本機測試的結果與截圖,反覆貼給 Gemini,反覆強化他上下文的資料,其實也在加深我自己的記憶與理解。因為這樣反覆地貼 Source Code 給他,反覆地餵他最新的資料與現況,所以我並沒有遇到最近 (2026/02~04) 很多人在 Complain Gemini 一直出現幻覺的問題。

另外過程中,我全程都是使用 Gemini Pro 的模式,每個問題都讓他做深度的思考與應對。過程也有過不小心點到快捷的模式,就會出現亂答的現象,只要切過去,然後同樣問題讓他重答就可以了。

底下截圖是 day34_fork 課程內容的 debug 截圖,有興趣可以店點進去看看當時我怎麼跟他對話的:


閒聊 AI - Harness Engineering

最近除了自己持續研究作業系統實作相關議題,跟 AI 趨勢的概念也是沒有停下來。Harness Engineering 是最近我持續在關注的課題,相關的技術也是持續在爆炸的生出來。

知道 Harness (馬具) 這個字,是在我 Designing Test Architecture and Framework 時期,就有整合一個叫做 CLI Harness 自動化程序,他主要負責測試 網路防火牆路由器 (待測產品) 的所有 CLI 指令是否正常運作,我記得有 400+ 的 Test Cases,全部透過 ssh / telnet 方式,透過比對輸出文字的方式驗證功能是否正常。這是每個版本出貨前都要測試的,當時有 4 個 LTS branch + main version (Trunk-Based) + 40+ 多個客戶版,所以只要改動 (feature or bug fix) 有影響的範圍都要跑,而這個產品的客戶都是歐美日各大銀行。所以這產品的 Regression Test 是非常重要的。

映像很深刻的是當時團隊的工作流程,用現在的眼光來看,其實就是一種 TDD 的概念,但在當時團隊的結構,是由開發者實作功能邏輯,由 QA 團隊實作自動化測試 (BlackBox),透過 CLI Harness 方式驗收功能是否正常。當時團隊開發與測試是同步作業,所以大家其實平常工作都是一起進行的。這個經驗放到現在來看更有意思 — 現在 Agent 系統幾乎都在強調 Eval-Driven Development (EDD, 評估集驅動開發) ,本質上就是把當年我們做 BlackBox 測試的思維,直接內建到 Agent 的執行循環裡。

回到現在 AI 時代的 Harness Engineering,會跑出這樣的概念從整個歷史趨勢來看,一點都不意外。因為 Prompt Engineering、Context Engineering,都缺乏了一個可以控制 AI 能夠穩定的、持續輸出可靠品質的方法。最核心的 LLM 每個月都會有新的突破,誰又超越誰了。下圖整理過去 2023 - 2026 相關 AI 發展演進的時間點:

寫這篇文章的時間點是 2026/04/27,我相信很快地又會有誰超越誰,誰是 OOXX 之王的稱號 (媒體炒作)。同時,Open Source / Local LLM (Gemma4, Qwen, MiniMax, DeepSeek) 也同步在嘗試能否改變,兩邊整天打來打去。

所以,然後呢???

其實大家慢慢都發現了,單純擴大模型參數帶來的邊際效益正在遞減 — 注意,這不等於 LLM 整體能力觸頂,重點是已經轉移到別的地方:

  • 推理時計算 (test-time compute):讓模型在回答前多想一下,像是 延長思考鏈、self-consistency` 那種做法。OpenAI o1/o3 系列就是這條路線的代表。
  • 工具使用與 Agent 化:讓模型不只「回答」,而是真的「動手做」 — 呼叫 API、跑程式碼、操作檔案系統。
  • 後訓練階段的 RL (Post-training RL):這是個關鍵變化。LLM 訓練分兩階段,先是 Pre-training (預訓練) 用海量網路文本學會「下一個 token 是什麼」,再來是 Post-training 用 RL 把模型雕琢成能聽指令、會推理、夠安全的樣子。常見手法包含:
    • RLHF (Reinforcement Learning from Human Feedback):人類標註員對模型多個回答排序,訓練 reward model,再用 PPO 把 LLM 往「人類偏好」推。ChatGPT 早期就是靠這套紅起來的。
    • RLVR (Reinforcement Learning with Verifiable Rewards):reward 不靠人標,而是靠「可驗證的答案」 — 數學題對不對、程式碼能不能跑過測試。OpenAI o1/o3、DeepSeek-R1 這些「推理模型」能突飛猛進就是靠這個。
    • DPO (Direct Preference Optimization):跳過訓練 reward model,直接從偏好資料學,實作更簡單、迭代更快。

也就是說,現在 LLM 的能力提升主軸,已經從 「pre-training 加大模型」 轉移到 「post-training 用 RL 雕琢推理與工具使用」 。這也是為什麼模型參數沒變多少,但 Agent 表現一直在跳。

同時不管是哪一個 LLM 用久了,就開始會有幻覺、記憶力短缺,所以出現了 Context Engineering,試圖要處理這些問題,出現了一堆工具,像是 OpenClaw。特別是 Cloud 版本的 LLM,幻覺問題越來越嚴重,像是 Opus 4.6 在 2026/02 開始被一堆工程師靠北一直前後文不對。

來來回回,現在的狀況大概就是:

  1. 有個聰明的大腦 (LLM, Ph.D),不管這顆腦是在雲上、還是在本機 / 移動端
  2. LLM 只要用一段時間,就開始忘東忘西,跟老年癡呆沒啥兩樣
  3. 大家發現「把模型包好」帶來的效益,可能比「再訓練一個更大的模型」還高

LangChain 團隊就有個經典案例:固定使用 GPT-5.2-Codex 模型不動,只調整 Harness 三個變數 (system prompt、tools、middleware hooks),就讓 deepagents-cli 在 TerminalBench 2.0 從 52.8% 提升到 66.5% (+13.7 個百分點),名次從 30 名外飆升到第 5 名。最有趣的是他們發現的失敗模式很「人性」 — Agent 寫完 code 自己 review 一下覺得 OK 就停手,所以解法是強迫 Agent 對著原始 spec 驗證,而不是 self-review。詳細參閱 LangChain Blog - Improving Deep Agents with harness engineering

Harness Engineering 就是要嘗試解決類似的問題,而比喻的概念就是像 作業系統,如下圖:


source: Agent Harness十二大模块完全解析

這個「Harness 像作業系統」的比喻其實不只是「資源調度」這麼簡單,作業系統真正的核心價值在於 抽象化與隔離

把不可靠、無狀態、會幻覺的模型,包裝成一個對外看起來穩定可預測的系統。

回頭看我當年做的 CLI Harness 也是同樣思路:

把待測產品的不確定行為,透過 SSH/Telnet 的標準介面包起來,讓上層測試案例可以穩定執行。

本質上都是在處理「不確定性的封裝」。

我想了三個問題,來思考 Harness Engineering:

  1. Harness Engineering 解決的問題是什麼?
  2. 它如何解決?
  3. 現在有哪些用 Harness Engineering 實作的系統?

這三個問題,只要丟到任何一個 AI 或者利用 NotebookLM 就可以整理出很多資料了。底下是我搜集讀過的資料之後,透過 NotebookLM / Claude 討論後的整理:

1) Harness Engineering 解決的問題是什麼?

核心就一句話:讓 LLM 從「玩具級 Demo」變成「生產級應用」,也就是讓 Jarvis 成真。

要拆開來看的話,主要是三個痛點:

長時任務的「模型漂移」與累積錯誤

LLM 在單次問答、靜態排行榜上看起來很厲害,但只要任務拉長到幾百次工具調用的多步驟流程,就會在過程中慢慢走鐘。這是個簡單的數學問題 — 一個 10 步的任務,即便每步有 99% 的成功率,端到端的成功率也只剩 90.4%;如果是 100 步的任務,那只剩 36.6%。這就跟我當年做網通產品 Regression Test 一樣,單一 test case 通過不代表什麼,400+ 個全部跑過才是出貨標準。

Lost in the Middle (迷失在中間)Context Rot (上下文腐爛)

這兩個概念相關但不完全相同,常常被混在一起:

  • Lost in the Middle (迷失在中間) (Liu et al., 2023 提出的學術名稱):單次長 context 內,落在開頭和結尾的資訊模型記得很清楚,但中間位置的關鍵資訊會被忽略,推理準確率與指令遵循能力會大幅暴跌(超過 30%)。這是個架構層面的限制。
  • Context Rot (上下文腐爛) :多輪對話累積之後的延伸現象 — context 塞了太多雜訊、過時資訊、矛盾指令,模型表現逐漸劣化。這比較像是「使用上的退化」。

兩個合在一起就是為什麼 Opus 4.6 在 2026/02 開始一堆人靠北前後文對不上 — 不是模型笨了,是 context 管理出問題。

玩具級 Demo生產級應用 的落差

裸奔的 LLM 就像沒有記憶體、硬碟、設備驅動的 CPU,只會輸出文字而且容易出錯。要落地到真實世界,缺的不是模型本身的能力,而是穩定性、持久運行、錯誤恢復、安全管控這些基礎設施。Harness Engineering 就是在補這塊。

2) 它如何解決?

簡單講就是 打造 AI 專屬的「作業系統」

把模型當 CPU,Harness 自己當 OS,負責資源調度、記憶體 (上下文) 管理、設備驅動 (工具集成),提供穩定的執行環境給上層 Agent 用。

實際上有幾個關鍵機制:

編排循環與工具調用:這是核心引擎,推動 Agent 的「思考 → 行動 → 觀察」循環。標準化的 Tool Schema 讓模型能精準呼叫外部能力 (檔案、API、資料庫) 跟現實世界互動。

分層記憶系統:業界目前比較成熟的分法是三到四層,這個分法很像 OS 的記憶體階層 (cache / RAM / disk):

  • Working memory — 當前對話窗口,類比 CPU cache
  • Episodic memory — 這次任務的歷史與檢查點,類比 process memory
  • Semantic memory — 跨會話的知識與偏好,類比持久化儲存
  • Procedural memory — 學會的工作流、技能 (例如 Anthropic 的 Skills、OpenAI 的 GPTs),類比可載入的 module

搭配壓縮摘要、觀察屏蔽、動態檢索、委派子 Agent 等策略,把 context 維持在「最小但高信噪比」的狀態,避免資訊過載。

狀態保存與錯誤處理:設立檢查點 (Checkpointing) 讓長週期任務崩潰後可以斷點續跑,這個概念跟 OS 的 process snapshot / VM 的 checkpoint 完全一樣。錯誤分類 (瞬時錯誤、邏輯錯誤、不可恢復錯誤) 後分別用退避重試、回傳模型自主修復、人工介入等策略處理,避免錯誤滾雪球。

護欄與驗證回饋:三層安全紅線 (輸入、輸出、工具) 防範越權;多重驗證 (規則測試、視覺截圖比對、AI judge) 大幅提升輸出準確率。LangChain 那個案例的關鍵發現就在這裡 — 強制 Agent 對著原始 spec 驗證而不是自己 review 自己。

確定性約束 + AI 彈性:這點蠻有意思。Harness 為 AI 劃定明確的架構邊界 (硬規則),同時部署「巡檢 Agent」做垃圾回收 — 自動掃描修復系統中的不一致 (彈性處理)。這個搭配讓 Agent 既不會亂跑,又能應對沒寫死的狀況。

更進一步看,Harness Engineering 不只是「補 LLM 的弱點」,而是把軟體工程的三個傳統強項重新引入到 AI 系統

  • 可觀測性 (Observability) — 每一步工具呼叫、每一次 context 變化都可追蹤
  • 可重現性 (Reproducibility) — checkpoint + 確定性約束讓任務可重跑、可除錯
  • 可驗證性 (Verifiability) — 規則測試 + AI judge + 人類 review 多層把關

這也是為什麼我覺得有過 Regression Test 經驗的工程師,理解 Harness Engineering 反而比較快。銀行客戶不能容忍出錯,所以當年我們的 400+ test case 不是裝飾,是出貨保險。現在 Agent 系統如果要進企業生產環境,Harness 就是那個讓它「可以被信任」的基礎建設。

3) 現在有哪些用 Harness Engineering 實作的系統?

現在百花齊放,各種大亂鬥的年代,我整理幾個比較有能見度與代表性的:

Anthropic Claude Code (Claude Agent SDK) :通用型 Harness 的標竿代表,採用極簡的信任模型哲學。提供三級記憶層級、Git-based 的檢查點回溯機制,內建檔案操作、終端機指令、程式碼修改等生產級工具。最讓人驚艷的是 checkpoint 機制,寫到一半發現方向錯了可以直接 git revert,Agent 不會迷失。

OpenAI Agents SDK / 內部 Codex 管控框架 :採用程式碼優先設計,支援多種狀態儲存與運行模式。OpenAI 內部用同一套理念,5-7 名工程師、5 個月內、用 AI 生成與維護超過百萬行程式碼的大型應用。這個數字蠻誇張的,等於每人每月平均產出 25,000+ 行有效程式碼。

LangChain LangGraph 與 DeepAgents :把 Harness 建模為明確的狀態圖結構,支援中斷後恢復甚至「時光倒流」式除錯。前面提過他們在 TerminalBench 2.0 純靠 Harness 優化從 30 名外衝到第 5 名,這就是最直接的證據。

CrewAI 與 AutoGen (微軟):兩個走不同路線。CrewAI 主打基於角色的多 Agent 協作 — 像是組一個團隊,有 PM、有 engineer、有 reviewer,適合需要分工的場景。AutoGen 則是對話驅動的協作協議 — Agents 之間透過對話達成共識,適合開放式互動。

Manus 與 Vercel 內部系統:這兩個團隊實踐了 Harness Engineering 中很反直覺的原則 — 「為刪除而建 (Build for Deletion)」。Manus 在半年內重構五次 Harness 來移除剛性假設;Vercel 砍掉 80% 的手工工具。結果反而是 token 消耗更少、任務成功率更高。這個現象其實跟我做作業系統研究的觀察一樣 — 好的抽象往往是「拿掉了什麼」,而不是「加了什麼」。

小結

整體看下來,Harness Engineering 還在很早期的階段,現在大家在做的事情,比較像是 1970 年代各家在探索作業系統該長什麼樣子。等到這個領域成熟,可能會出現類似 POSIX 那樣的標準介面,讓不同 Harness 之間可以互通。但在那之前,每個團隊都還在用自己的方式試,這也是這個領域目前最有趣的地方。

我正在整理 自幹作業系統 的相關學習筆記時,也剛好在思考,是否要把 POSIX 標準加入 (真的只是想想而已 XD),其實也是同樣的過程。寫 OS 的人,學的是「怎麼建立穩定可靠的執行環境」這套思維。而 Harness Engineering 本質上就是把這套思維搬到 LLM 上:

scheduler -> 編排循環
memory hierarchy -> 分層記憶
process checkpoint -> 任務檢查點
syscall -> tool calling
IPC -> multi-agent 通訊

所以我 剛好 等於同時在做這兩件事?

一邊在「最底層」用手親自把這些概念刻出來,一邊在「最上層」觀察這些概念怎麼被重新發明來服務 AI。


延伸閱讀

自幹作業系統 系列

站內文章

參考資料



Comments

2026/04/28 10:30:00





  • 全站索引
  • 關於這裏
  • 關於作者
  • 學習法則
  • 思考本質
  • 一些領悟
  • 分類哲學
  • ▲ TOP ▲