tiny-agent 從第一性原理打造可靠 Agent

第二部|能力邊界 · 第 4 章

Context、AGENTS.md 與 Skills

把專案規則與領域知識放入正確層級,使用 progressive loading 控制 context 成本。

約 15 分鐘 5 / 10

一句話掌握本章核心:Context Engineering 不是「把所有資料都塞給模型」,而是建立結構化分層(Layering)與漸進式載入(Progressive Loading)機制:讓通用規則常駐、專案規範明確隔離、領域知識(Skills)僅在命中時動態載入。

Context 的四種組裝來源

在組裝每次 API Request 時,可把 tiny-agent 的輸入理解為四種來源。這是 Host 端的組裝模型,不是 provider 協定原生的四級權限:Project Rules 與 Skill Metadata 實際上都被串進同一則 system message,Conversation 則是後續 transcript。

Context 四層金字塔與 Skill 漸進式按需載入架構圖
圖 4.1:Context 組裝來源與 Progressive Loading——圖中的層次是教學分類,不代表 provider 額外提供四級 priority arbitration。
Context 層級資料來源生命週期與特性Token 佔用成本
1. Base Instructions內建 Runtime核心 Agent 身分、基本 Workflow 準則。組入 system message固定文字,但 token 數依 tokenizer/model 而異
2. Project Rules./AGENTS.md目前專案特定的 Coding Style、禁忌與指令。同樣組入 system message隨檔案長度增加。穩定 prefix 可能符合 Prompt Cache 條件
3. Skill Metadata.tiny-agent/skills/**/SKILL.md 與 --skill索引只放 Name、Description 與 Location,不預載正文仍會隨 skill 數量、描述與路徑長度線性增加,但通常遠小於全文
4. Conversation即時對話歷程動態 Append 的 User 指令、Tool Calls 與 Observations持續成長(需透過 Compaction 治理)

AGENTS.md:專案規則的結構化隔離

專案規則不應隨意散落在 System Prompt 之中。tiny-agent 啟動時讀取工作目錄下的 ./AGENTS.md,並將其包裝在具備語意邊界的 XML 標籤內:

<project_context>
Project-specific instructions and guidelines:
<project_instructions path="/workspace/AGENTS.md">
- 遵循 LBYL 原則,禁止無意義 try/catch
- 檔案工具使用 4 空格縮排
- 測試覆蓋率必須維持 100%
</project_instructions>
</project_context>

Skills 的漸進式載入 (Progressive Loading)

核心設計哲學:不要在啟動時把所有 Skill 的正文全數灌入!

系統啟動時遞迴掃描目錄,以小型 parser 從 frontmatter 抽取單行 name 與 description,再加上檔案 location 建立輕量索引。它不是完整 YAML parser。多行 scalar、巢狀結構等一般 YAML 語法不在此教學實作的 contract 內:

---
name: i-have-adhd
description: 針對 ADHD 讀者最佳化輸出結構,提供結論先行、高對比度表格與精簡清單。
---
比較項目全部預先載入 (Eager All-in)漸進式按需載入 (Progressive Loading)
初始 Token 消耗隨 Skill 全文總長線性增加也會隨 metadata 數量線性增加,但斜率通常較低。應以目標 model tokenizer 實測
模型注意力聚焦❌ 充斥大量無關領域規則,干擾推論✅ 保持上下文純淨,模型只關注當前任務
可稽核性 (Auditability)正文從 request 一開始就存在,較難分辨何時真正使用Autonomous route 若呼叫 read(SKILL.md),該 tool call/result 會進 durable Session。這能證明曾讀取,不證明模型確實遵循或正確理解

兩種合法的 Skill 載入路徑

  1. 模型自主觸發(Autonomous Route):
    模型閱讀 Metadata 後,判斷當前任務匹配該 Skill,主動呼叫 read(path: ".tiny-agent/skills/.../SKILL.md")。System prompt 要求缺少能力時說明限制,而 runtime 能保證的是「未提供的 tool 不會被派發」。它無法保證模型一定誠實、一定選中正確 Skill 或一定產生特定措辭。
  2. 使用者顯式指定(CLI Slash Command Route):
    使用者輸入 /skill:i-have-adhd 說明架構。CLI 作為 Trusted Host 會直接讀取本機 Skill 檔案,將全文附加至本次 User Message 送入,此路徑完全不需要賦予模型 read 權限。

Context 膨脹與治理防線

在多輪對話中,每一次 Tool 的執行結果(尤其是大型 Log 或檔案讀取)都會使 Transcript 迅速膨脹:

Round 1: [System] + [User] ───────────────→  500 Tokens
Round 2: ... + [Assistant] + [Tool: read] ─→ 4,500 Tokens
Round 3: ... + [Tool: bash test] ─────────→ 12,000 Tokens (持續累積...)

即使 Provider 支援 Prompt Cache 降低費用,Context Window 的物理上限依然存在。完整的 Context 治理必須包含:

  • Tool 邊界截斷:單次 read 最多 2,000 行 / 50KB。bash 輸出限制長度。
  • Skill 漸進載入:維持極小 Initial Context。
  • Durable Compaction(歷史壓縮):適時將舊對話總結為精確 Facts(詳見第 7 章)。

深度解析:Frontmatter 小型解析器的邊界與設計取捨

tiny-agent 在讀取 SKILL.md 時,並未引入肥大的外部 YAML 解析套件,而是採用了一個幾十行的輕量解析器。這個設計取捨貫徹了「教學專案保持核心清晰」的原則,但也確立了嚴格的語法契約:

// tiny-agent 內建 Skill Frontmatter 解析契約
// 1. 僅辨識檔案最前端以 --- 包裹的標頭
// 2. 僅抽取單行鍵值對:name 與 description
// 3. 不支援巢狀結構、多行字串、錨點 (Anchors) 或複雜類型
function parseSkillMetadata(content: string): { name: string; description: string } | null {
    const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
    if (!match) return null;
    
    // 嚴格比對頂層的 name 與 description 欄位
    const nameMatch = match[1].match(/^name:\s*(.+)$/m);
    const descMatch = match[1].match(/^description:\s*(.+)$/m);
    if (!nameMatch || !descMatch) return null;

    return {
        name: nameMatch[1].trim(),
        description: descMatch[1].trim(),
    };
}

為什麼不用標準 YAML 套件?
在 Agent 的啟動流程中,可能需要掃描專案內數十個 Skills。引入完整的 YAML Parser 會帶來額外的相依性包袱,甚至引入潛在的 Prototype Pollution 或 ReDoS 安全風險。透過限制語法為極簡契約,系統在啟動階段只需花費數毫秒即可完成所有 Skill 中繼資料的抽取。

動手驗證 (Hands-on Lab)

本實驗將引導你動手打造一個專屬的自訂 Skill,並親自驗證 Agent 如何透過漸進式按需載入(Progressive Loading)來維持極簡的初始上下文:

實驗一:撰寫自訂 Skill 並觀察中繼資料索引

在當前目錄建立一個符合 tiny-agent 規範的 Skill 定義檔:

# 1. 建立技能目錄結構
mkdir -p .tiny-agent/skills/taiwan-locale

# 2. 撰寫 SKILL.md,包含 Frontmatter 標頭與具體操作指引
cat <<'EOF' > .tiny-agent/skills/taiwan-locale/SKILL.md
---
name: taiwan-locale
description: 協助校驗文字符合臺灣繁體資工習慣用語,例如呼叫、記憶體、行程。
---

# 臺灣資工詞彙校正指南
當使用者詢問用語時,請務必依據下列規範:
- 「調用」請轉換為「呼叫 (Call)」
- 「內存」請轉換為「記憶體 (Memory)」
- 「進程 / 線程」請轉換為「行程 (Process) / 執行緒 (Thread)」
EOF

實驗二:自主觸發載入 vs. 顯式指令載入

現在我們分別以兩種路徑觀察系統的行為差異:

# 路徑 A:模型自主觸發(需要 read 工具權限)
# 模型看見中繼資料後,主動呼叫 read 讀取 SKILL.md 全文
tiny-ts --plugin read "請確認這段程式碼中是否包含非臺灣用語:'這個函數會調用系統底層進程'"

# 路徑 B:Trusted Host 顯式載入(完全不需 read 工具)
# CLI 作為信任外殼,在啟動前直接將 SKILL.md 注入至 User Message
tiny-ts --plugin write "/skill:taiwan-locale 校驗這句話:'釋放未使用的內存'"

實驗觀察重點:

  1. 在路徑 A 中,檢視 .tiny-agent/sessions/ 內的 JSONL 日誌,你會發現一筆具體的 read Tool Call 及其 Observation,這留下了完整的可稽核軌跡。
  2. 在路徑 B 中,模型在未被賦予 read 權限的狀態下,依然成功獲得了領域知識。這證明了「Host 端 Trusted Seam」可以在不對模型放寬系統特權的前提下,靈活注入必要的情境。

至此,我們已經掌握了 Agent 的大腦(Model)、手腳(Tools)與注意力(Context)。實際 CLI 在開始工作前就會建立或開啟 JSONL Session,並在每一步追加 facts。記憶體中的 messages 只是可由 durable log 重建的 active view。下一章將把這條已存在的持久化路徑拆開,說明先記 Intent,再發動 Effect的寫入順序、torn-tail 修復與明確的 durability 限制。