教學文件 / opencode-入門實作.html
HANDS-ON · 約 90 分鐘

OpenCode 入門實作課

從零開始,把一台乾淨的電腦變成可以跟 AI 一起寫程式的環境。你會依序完成 Node.js 安裝、OpenCode 安裝、基礎指令演練,最後親手做出兩個可重複使用的 Skill:擲骰子與快取查詢。

🎯 對象:第一次接觸 CLI 與 AI Agent 的學生 🧰 環境:Windows / macOS / Linux ✅ 版本基準:OpenCode v2、Node.js 22 LTS 以上

0課程介紹與學習地圖

先看清楚終點,再開始走路。

這份教材不談抽象理論,每個章節都以「能動起來」為驗收標準。你會建立一個自己的 專案資料夾,在裡面跟 OpenCode 對話,最後把手上的重複工作「固化」成 Skill, 讓 AI 每次都照同一套規矩做事。

1️⃣
環境

Node.js 與 OpenCode 裝好、版本驗證通過、模型連線成功。

2️⃣
操作

會開 TUI、會用 opencode run、看得懂背景服務狀態。

3️⃣
Skill

知道資料夾放哪、frontmatter 怎麼寫、什麼情況 AI 才會自動叫它。

4️⃣
實作

做出「擲骰子」與「快取查詢」兩個可執行的 Skill。

📌 學習路徑建議

不要跳著做。第 1、2 章是地基,第 4 章之後的實作都會直接呼叫 opencode 指令——如果它不在 PATH 裡,後面的範例全部會失敗。

i名詞速查

看懂這幾個詞,整份文件就不會卡。

名詞說明
Agent會自己決定「下一步做什麼」的 AI:能讀檔、跑指令、改程式。OpenCode 就是一個 agent 的宿主。
TUITerminal User Interface,在終端機裡的全螢幕介面。打 opencode 就會看到。
Session一次對話(工作階段)。每個 session 都有 ID,token 用量也記在裡面。
Service背景服務。OpenCode 走 client–server 架構,TUI 只是前端,session 與工具執行都在 server 裡。
Skill一個資料夾加上一份 SKILL.md。裡面寫「什麼時候該用我、步驟是什麼」,需要時才被載入,不佔每次對話的額度。
Frontmatter檔案最上方用 --- 包起來的設定區塊(YAML)。
Prompt cache供應商把你重複送出的前綴內容快取起來,下次直接讀取,價格與延遲都更低。

1安裝 Node.js

OpenCode 用 Node 生態發佈,所以 Node 是前置條件。

你需要的是 Node.js 22 以上的 LTS 版本(本教材撰寫時本機為 v22.20.0 / npm 10.9.3)。 請不要抓「Current」的奇數版本,LTS 比較穩。

👀 三種裝法,先看你要哪一種

下面三種方式選一種就好,不要混著裝。如果你不熟指令,或這是你的第一台開發機,直接走「方法 A」——用滑鼠點完就好,一行指令都不用打。

方式適合誰要做的事
方法 A
官方下載安裝檔
第一次安裝、不熟指令、只想快點開始開瀏覽器下載 → 下一步下一步 → 完成(不碰指令)
方法 B
套件管理器
已經有 winget / Homebrew 的人打一行安裝指令
方法 C
版本管理器(fnm / nvm)
電腦上會同時跑多個專案、需要切版本先裝管理器,再用它裝 Node

方法 A:官方下載安裝檔(推薦,最不用懂指令)

Node.js 官網就有做好的安裝程式,Windows 是 .msi、macOS 是 .pkg, 點兩下、一路按「下一步」就好。

  1. 打開官方下載頁 瀏覽器輸入 nodejs.org/zh-tw/download (英文版是 nodejs.org/en/download)。
    認明網址是 nodejs.org,不要從搜尋結果的廣告或第三方程式下載站抓。
  2. 按「LTS」那個按鈕,不要按「Current」 LTS 是長期維護版(現在是 22 或 24 系列),Current 是最新實驗版,可能踩到套件不支援的雷。
    Windows 選哪個檔案?

    下載頁的 Windows Installer (.msi),一般筆電/桌機選 x64;只有 ARM 筆電(例如 Snapdragon 系列)才選 ARM64。macOS 選 .pkg,Apple M 系列或 Intel 都有對應版本。

  3. 執行安裝檔,一路「Next / 繼續」
    • 授權條款 → 勾 I accept。
    • 安裝路徑 → 保持預設,不要改。
    • Custom Setup 那一頁 → 確認 Add to PATH 是「Will be installed on local hard drive」(預設就是),這樣等等才找得到 node 指令。
    • 「Tools for Native Modules」那個打勾的選項可以不用勾,它會多裝 Python 與 Visual Studio 工具,很花時間。
    • 按 Install → Finish。
  4. 關掉所有終端機視窗,再重新開一個 安裝程式只會更新「之後才開啟」的視窗的 PATH。舊視窗開著就還是找不到指令,這是最常見的「明明裝好了卻說找不到」。
  5. 驗證 照下一節 1.1 驗證安裝 打兩行指令確認。看到版本號就成功了。
✅ 為什麼推薦這一種

不用先搞懂終端機、不用管 PATH 怎麼設、也不用處理權限。安裝程式會把該做的事做完,對第一次接觸的同學最不容易卡住。之後想學指令再回來看方法 B。

方法 B:套件管理器(一行指令)

已經有 winget 或 Homebrew 的人可以用這招,速度快、之後升級也方便。

Windows · PowerShell
# 用 winget 安裝 Node.js LTS
winget install OpenJS.NodeJS.LTS

# 裝完務必關掉終端機再重開,PATH 才會生效
macOS · Homebrew
brew install node

# 或者只裝 Node 24
brew install node@24

方法 C:用版本管理器(要同時跑多專案再用)

fnm 或 nvm 可以讓你一句話切換 Node 版本, 也能避開 macOS / Linux 上常見的權限問題(不需要 sudo npm install -g)。

fnm · Windows / macOS / Linux
# 1. 安裝 fnm
winget install Schniz.fnm        # Windows
brew install fnm                  # macOS

# 2. 讓「以後開的每個終端機」都會自動載入 fnm(只做一次)
#    PowerShell:建立 profile,再把啟動指令寫進去
if (-not (Test-Path $profile)) { New-Item $profile -Force }
Add-Content $profile 'fnm env --use-on-cd --shell powershell | Out-String | Invoke-Expression'

#    bash / zsh:把下面這行加進 ~/.zshrc 或 ~/.bashrc
#    eval "$(fnm env --use-on-cd --shell zsh)"

# 3. 重開終端機,安裝並啟用 Node 24 LTS
fnm use --install-if-missing 24
node -v
⚠️ 第 2 步不能省

fnm 只是個「版本切換器」,它不會自己把 node 放進 PATH。少了 fnm env 這行,新開的視窗會直接說找不到 node。

⚠️ 不要用 sudo npm install -g

用 sudo 裝全域套件會讓 ~/.npm 變成 root 擁有,之後每次安裝都會噴 EACCES。真的遇到權限問題,改用 fnm/nvm,或設定 npm config set prefix ~/.npm-global 並把該路徑加進 PATH。

1.1 驗證安裝

驗證要打兩行指令,所以先確認「終端機」在哪。它不是什麼可怕的東西,就是一個可以打字的黑視窗而已:

🪟 Windows

按 Win 鍵 → 直接輸入 powershell → Enter。
(cmd 也可以,本教材的指令兩邊通用。)

🍎 macOS

按 ⌘ + 空白 開啟 Spotlight → 輸入 Terminal → Enter。

開一個新的終端機視窗,把下面三行貼進去(一行一行貼、每行按 Enter):

驗證
node -v
npm -v
npm config get prefix

預期看到類似(版本號不用一樣):

預期輸出
v22.20.0
10.9.3
C:\Users\你的帳號\AppData\Roaming\npm

第三行是 npm 安裝全域套件的位置,依安裝方式會不一樣(用官方安裝檔多半是上面這個; 用 nvm4w 會是 C:\nvm4w\nodejs,用 fnm 會在 fnm 自己的資料夾)。 重點不是路徑長怎樣,而是它有在 PATH 裡。

✅ 驗收看兩件事

1. node -v 有印出 v22 或 v24 開頭的版本。
2. npm config get prefix 印出來的路徑有在你的 PATH 裡——Windows 通常是 %APPDATA%\npm,macOS/Linux 是 /usr/local 或 fnm 的 shim 目錄。

1.2 常見卡點

症狀原因與解法
node: command not foundPATH 沒生效。關掉終端機重開;Windows 可執行 where node 確認。使用 fnm 時要確認 shell 初始化設定(PowerShell 的 profile)已載入 fnm env。
npm WARN EBADENGINENode 版本太舊。升級到 22 LTS 以上;npm ls -g --depth=0 可看已裝的全域套件。
EACCES / EPERM: operation not permitted權限問題。改用 fnm/nvm,或改 npm prefix;Windows 請用「以管理員身分執行」的終端機一次就好,不要長期這樣用。
公司網路安裝卡住代理設定:npm config set proxy http://proxy:8080 與 npm config set https-proxy ...。

2安裝 OpenCode

三種安裝方式,挑一種就好。

先確認你的 Node 已經就緒(第 1 章),然後選擇一種方式安裝 CLI。

方法 A:npm(跨平台最通用,Windows 首選)

npm
npm install -g @opencode/cli

方法 B:官方安裝腳本(macOS / Linux)

shell
curl -fsSL https://opencode.ai/v2/install | bash

方法 C:Homebrew 或獨立執行檔

macOS · brew / Windows · zip
# macOS
brew install anomalyco/tap/opencode-v2

# Windows 沒有套件管理器支援時,下載獨立執行檔:
# https://opencode.ai/update/api/latest/cli/opencode/opencode-windows-x64.zip
# 解壓後把 opencode.exe 放到 PATH 內的資料夾(例如 %LOCALAPPDATA%\opencode)
⚠️ Windows 使用者注意

官方明確表示 Windows 的套件管理器(winget / choco / scoop)尚未支援 OpenCode。請用 npm install -g @opencode/cli,或下載上面的獨立執行檔手動放進 PATH。

2.1 驗證與首次啟動

驗證
opencode --version
opencode debug paths

第一行會印出類似 opencode v2.0.32;第二行印出 config、data、log、DB 等實際路徑, 之後要備份設定或查 log 都靠它。

接著建立你的練習專案並啟動:

第一次啟動
mkdir my-agent-lab
cd my-agent-lab
opencode

進入 TUI 之後,先連接模型供應商:

TUI 內
# 在輸入框打斜線就會出現指令清單
/connect

# 選你的供應商(OpenCode Console、Anthropic、OpenAI…)完成登入
# 想不起來有哪些指令?直接打「/」看完整清單
💡 沒有 API key 怎麼辦

可以先走 OpenCode Console 取得模型存取權,或用本機的 Ollama 當 provider。這門課的練習都不需要昂貴模型,小模型就能跑完。

2.2 背景服務(很重要,但常被忽略)

OpenCode 的架構是 client + server。TUI 只是前端,session、權限、工具執行都在一個常駐的背景服務裡。 平常它會自動啟動,但當你遇到「卡住不動」「指令沒反應」,第一個要檢查的就是它。

服務檢查
opencode service status     # 看服務活著沒
opencode service restart    # 卡住就先重啟
opencode api get /api/info  # 真的打得通 API 嗎
opencode --standalone       # 用私有 server 開,隔離問題

如果懷疑是某個專案或某個 plugin 搞壞的,用 --standalone 跑一次。 它能動、共用服務不能動 → 問題在共用服務或它的設定;兩邊都不能動 → 問題在安裝或模型設定。


3基礎指令

背這 10 個就夠你活一整個學期。

指令用途
opencode在目前目錄開啟全螢幕 TUI。
opencode ~/code/app指定別的目錄當專案根。
opencode run "說明這個專案"非互動模式:送出一個 prompt、拿輸出、結束。適合腳本與 CI。
opencode mini迷你介面,不想被全螢幕畫面吃掉時用。
opencode pair開網頁版介面,會印出 URL 與帳號密碼。
opencode service status|restart檢查/重啟背景服務。
opencode api get /api/info直接打自己的 HTTP API,用 OpenAPI 的 operation ID 也可以。
opencode mcp add <name> --url <url>新增 MCP 伺服器(加 --global 給全使用者用)。
opencode debug paths [db]印出設定、資料、log、DB 的實際路徑。
opencode uninstall --dry-run預覽卸載會刪掉什麼(session 與設定不會被刪)。

3.1 TUI 操作要點

3.2 三個暖身練習

練習 1:非互動模式

shell
cd my-agent-lab
opencode run "用三句話解釋什麼是 prompt cache"

你應該會直接在終端機看到答案,然後回到 shell。這就是「自動化」的入口。

練習 2:讓它讀你的檔案

在專案裡建一個 hello.js,貼上幾行程式,然後問它:

TUI 內
幫我看 @hello.js,指出可以改進的地方,先不要改檔案

@ 是引用檔案的語法。注意最後那句「先不要改檔案」——限制行動範圍是 prompt 的基本功。

練習 3:確認服務健康

shell
opencode service status
opencode api get /api/info

兩個都正常回應,才代表你的「地基」是穩的。之後 Skill 的腳本會用到 opencode api,所以這步不能跳。


4什麼是 Skill

把「你每次都要講一遍的規矩」變成可重複使用的模組。

Skill 就是一個資料夾,裡面有一份 SKILL.md,加上它需要的腳本與參考文件。 平常 OpenCode 只把每個 skill 的 ID、名稱、description 放進模型的可用清單; 當任務真的符合時,模型才呼叫 skill 工具,把完整的 Markdown 內容載入。

🧠
省 context

沒用到就不載入,長篇 SOP 不會擠佔每次對話的額度。

🔁
行為一致

同樣的流程每次都照做,不會因為換 session 就忘記。

📦
可分享

一個資料夾就能傳給同學,或放進 repo 讓整組一起用。

4.1 檔案結構與存放位置

建議使用「資料夾 + SKILL.md」的形式,因為這樣才能把腳本放在旁邊:

.opencode/skills/ └── git-release/ ├── SKILL.md # 唯一必要的檔案 ├── scripts/ │ └── changelog.ts # 需要時才被執行 └── references/ └── release-policy.md # 給模型查閱的資料

OpenCode 會自動搜尋這些位置:

範圍路徑
全域(OpenCode)~/.config/opencode/skills
全域(相容)~/.claude/skills、~/.agents/skills
專案.opencode/skills
專案(相容).claude/skills、.agents/skills
📁 兩種擺法都可以

skills/review.md(單檔,必須在來源根目錄)或 skills/git-release/SKILL.md(資料夾,可放任意深度)。
檔名一定要是 SKILL.md(全大寫),或位於來源根目錄的 *.md。

想放別的地方,就在 opencode.jsonc 的 skills 陣列加路徑(相對路徑以當前工作目錄為基準,不是設定檔所在位置):

opencode.jsonc
{
  "$schema": "https://opencode.ai/config.json",
  "skills": [
    "./team-skills",
    "~/shared/opencode-skills"
  ]
}

4.2 寫好 SKILL.md 與 description

最小可用的檔案長這樣:

.opencode/skills/git-release/SKILL.md
---
name: Git Release
description: 準備 release notes、版號升級與 GitHub release
---

## 流程

1. 讀 `references/release-policy.md`。
2. 整理上一個 tag 之後合併的變更。
3. 先提出建議版號,使用者同意後才動檔案。
4. 使用者核可版號後才執行 `scripts/changelog.ts`。

Frontmatter 可用欄位:

欄位作用
name顯示名稱(只是標籤,不是 ID)。
description給模型看的摘要。沒有 description 的 skill 不會被廣告出去,模型也就無從得知它的存在。
metadata.opencode/autoinvoke設 false 可把 skill 從模型清單隱藏(仍可用 ID 手動載入)。
disable-model-invocation設 true,效果同上。兩者都寫時以 opencode/autoinvoke 為準。

其他 frontmatter(例如 version、license)可以寫,方便人看與分享,但 V2 不會解讀它們的語意。

description 決定成敗

模型只靠 description 判斷「現在該不該用這個 skill」。所以它不只是摘要,而是觸發條件。 最好把使用者真的會講的話寫進去,並明確寫出「不要觸發」的情況。

❌ 太模糊✅ 可判斷
description: 擲骰子工具 description: 擲骰子。當使用者說「擲骰」「骰個 d20」「roll 3d6」「幫我隨機決定」時使用。不要用於:寫程式的亂數、統計模擬、樂透程式開發。
✅ 檢查清單

① 有沒有寫「使用者會怎麼說」?② 有沒有寫「什麼情況不要用」?③ 名稱與資料夾一致嗎?④ 步驟是否具體到可以照做(含要跑的指令)?

4.3 ID、載入與權限

ID 由路徑決定,不是 frontmatter 的 name。而且大小寫敏感:

檔案路徑得到的 ID
<source>/git-release.mdgit-release
<source>/git-release/SKILL.mdgit-release
<source>/teams/release/SKILL.mdrelease

可攜性起見,建議用全小寫 kebab-case,長度 1–64 字元,並跟資料夾同名:^[a-z0-9]+(-[a-z0-9]+)*$。

怎麼被載入

權限

預設都能用。想限制某些 skill,用 permissions(規則依序套用,最後一筆符合的生效):

opencode.jsonc
{
  "permissions": [
    { "action": "skill", "resource": "*", "effect": "allow" },
    { "action": "skill", "resource": "internal-*", "effect": "deny" }
  ]
}

allow 直接載入、ask 載入前詢問、deny 完全隱藏並拒絕。要只套用在某個 agent,放進 agents.<id>.permissions。


5實作 A:擲骰子 Skill

第一個 skill,順便學會「為什麼數字一定要來自腳本」。

為什麼不讓 AI 自己想一個數字?

語言模型產生的「隨機」數字有系統性偏誤:結果會擠在中間值、極端值偏少,而且會被上下文暗示 (你愈說「我需要高一點」,它愈容易給高)。對擲骰來說這是致命的——1d20 就不再是均勻分布。 所以正確做法是:模型只負責解析需求,數字交給程式用 secrets 模組產生。

步驟 1:建立資料夾

shell
# 專案內(跟同學共用)
mkdir -p .opencode/skills/dice-roll/scripts

# 或全域(自己每個專案都能用)
# Windows PowerShell
mkdir "$env:USERPROFILE\.config\opencode\skills\dice-roll\scripts"
# macOS / Linux
mkdir -p ~/.config/opencode/skills/dice-roll/scripts
.opencode/skills/dice-roll/ ├── SKILL.md └── scripts/ └── roll.py

步驟 2:寫 SKILL.md

重點不是寫得長,而是把「不可以做什麼」寫清楚——那才是模型最容易出錯的地方。

.opencode/skills/dice-roll/SKILL.md
---
name: dice-roll
description: |
  擲骰子:用系統級隨機源擲出 XdY±Z 形式的骰子,回報每顆骰子的點數、加總與範圍。
  觸發時機:使用者說「擲骰」「骰一下」「丟個 d20」「roll 3d6」「幫我隨機決定」,
  或出現 d4/d6/d8/d10/d12/d20/d100、骰子、dice、roll 字樣的場合。
  不要觸發:寫程式需要的亂數/UUID、統計模擬、樂透或賭博程式開發、資料抽樣。
---

# 擲骰子

## 一句話原則

**數字只能來自腳本,你不能自己生成。** 你的工作是解析使用者要什麼骰子、
執行腳本、把結果講清楚。

## 怎麼跑

```bash
python "<skill-dir>/scripts/roll.py" 3d6+2
python "<skill-dir>/scripts/roll.py" d20 --pretty
```

- `<skill-dir>` 就是這個 SKILL.md 所在的資料夾。
- 一次要多組獨立結果時用 `--times N`,不要呼叫腳本 N 次。
- 輸出是 JSON:`dice[].rolls` 是每顆骰子點數、`total` 是最終值、`min`/`max` 是理論範圍。

## 使用者沒講清楚時

| 使用者說 | 怎麼處理 |
| --- | --- |
| 「骰一下」 | 用 1d6,並說明「用 d6,要換別顆跟我說」 |
| 「骰個數字決定」 | 用 1d6 或 1d100,把點數對應到什麼講出來 |
| 「幫我檢定」 | 追問是哪個系統的哪顆骰(D&D 是 d20,CoC 是 d100),別猜 |

## 輸出格式

先給數據,再給一句描述:

```
🎲 3d6+2 → **12**
`3, 3, 4` +2
```

- 一定要列出**每一顆**骰子的點數,不能只給總和。
- 修正值跟骰子分開列,別把 +2 混進骰子點數裡。

## 不可以做的事

- **不要重擲**:結果不理想是結果的一部分。
- **不要挑結果**:不能骰完看到 3 就假裝沒看到。
- **不要微調**:不能把 12 寫成 13 讓它「剛好過關」。
- **腳本失敗就直說**,不要改用自行生成的數字充數。

步驟 3:寫 scripts/roll.py

下面是可以直接跑的簡化版(支援 d20、3d6+2、2d6+1d8-1):

.opencode/skills/dice-roll/scripts/roll.py
#!/usr/bin/env python3
"""擲骰子:XdY±Z,用 secrets 取無偏隨機數。預設輸出 JSON,--pretty 給人看。"""
import argparse, json, re, secrets

TERM = re.compile(r"([+-]?)(?:(\d*)d(\d+)|(\d+))")

def roll(expr):
    e = re.sub(r"\s+", "", expr).lower()
    dice, mod, pos = [], 0, 0
    for m in TERM.finditer(e):
        if m.start() != pos:                       # 中間有看不懂的字元
            raise ValueError("cannot parse: " + e[pos:m.start()])
        pos = m.end()
        sign = -1 if m.group(1) == "-" else 1
        if m.group(3):                             # 骰子項,例如 3d6
            n = int(m.group(2) or 1)
            sides = int(m.group(3))
            if sides < 2:
                raise ValueError("dice needs at least 2 sides")
            rolls = [secrets.randbelow(sides) + 1 for _ in range(n)]
            dice.append({"count": n, "sides": sides, "sign": sign,
                         "rolls": rolls, "subtotal": sum(rolls) * sign})
        else:                                      # 修正值,例如 +2
            mod += sign * int(m.group(4))
    if pos != len(e):
        raise ValueError("cannot parse: " + e[pos:])
    total = sum(d["subtotal"] for d in dice) + mod
    low = sum((d["count"] if d["sign"] > 0 else -d["count"] * d["sides"]) for d in dice) + mod
    high = sum((d["count"] * d["sides"] if d["sign"] > 0 else -d["count"]) for d in dice) + mod
    return {"expression": e, "dice": dice, "modifier": mod,
            "total": total, "min": low, "max": high}

def pretty(r):
    lines = ["ROLL " + r["expression"]]
    for d in r["dice"]:
        sign = "-" if d["sign"] < 0 else ""
        faces = ", ".join(str(x) for x in d["rolls"])
        lines.append("  %s%dd%d -> %s = %d" % (sign, d["count"], d["sides"], faces, d["subtotal"]))
    if r["modifier"]:
        lines.append("  modifier %+d" % r["modifier"])
    lines.append("  total %d  (range %d..%d)" % (r["total"], r["min"], r["max"]))
    return "\n".join(lines)

if __name__ == "__main__":
    p = argparse.ArgumentParser(description="roll XdY+Z dice")
    p.add_argument("expression", help="e.g. 3d6+2, d20, 2d6+1d8-1")
    p.add_argument("--times", type=int, default=1, help="repeat N times (default 1)")
    p.add_argument("--pretty", action="store_true", help="human-readable output")
    a = p.parse_args()
    try:
        results = [roll(a.expression) for _ in range(a.times)]
    except ValueError as exc:
        raise SystemExit("error: %s" % exc)
    if a.pretty:
        print("\n\n".join(pretty(r) for r in results))
    elif a.times == 1:
        print(json.dumps(results[0], ensure_ascii=False))
    else:
        print(json.dumps(results, ensure_ascii=False))

步驟 4:先自己測腳本

shell
python .opencode/skills/dice-roll/scripts/roll.py 3d6+2
# {"expression": "3d6+2", "dice": [{"count": 3, "sides": 6, "sign": 1,
#  "rolls": [4, 2, 6], "subtotal": 12}], "modifier": 2, "total": 14}

python .opencode/skills/dice-roll/scripts/roll.py 2d6+1d8-1 --times 3
⚠️ 先測腳本,再測 skill

如果 python 不在 PATH,Windows 請改用 py -3,並把 SKILL.md 裡的指令一起改掉。腳本本身跑不起來,skill 一定失敗,而且錯誤訊息會很難查。

步驟 5:在 OpenCode 裡驗收

重開 TUI(讓它重新掃描 skill 清單),然後直接用人話測試:

TUI 內
幫我骰 3d6+2
# 或手動指定,確認它真的載入
@dice-roll 骰個 d20

合格的回覆長這樣:

預期結果
🎲 3d6+2 → 14
`4, 2, 6` +2

6實作 B:查詢快取命中率 Skill

把「我這個月到底省了多少 token」變成一句話就能問的事。

原理:三個數字

每次呼叫模型都會回報 token 用量。其中 prompt 的部分有三類: input(真正送進去的)、 cache.read(從供應商快取直接讀到的)、 cache.write(寫入快取的)。 命中率就是「有多少比例的 prompt token 沒花全額」:

公式
命中率 = cache.read ÷ (cache.read + input + cache.write)

資料來源不是資料庫,而是 OpenCode 自己的 API: opencode api get /api/session 會回傳所有工作階段, 每個 session 都有 tokens.input / output / reasoning 與 tokens.cache.read / write。

真實回傳(節錄)
{
  "id": "ses_ea8e8727affeopFLqDCKlY4QTa",
  "title": "設定排程查詢台中天氣並於1分鐘後通知",
  "model": { "id": "deepseek-v4.1-flash-free", "providerID": "opencode" },
  "tokens": {
    "input": 76101,
    "output": 5586,
    "reasoning": 8800,
    "cache": { "read": 578816, "write": 0 }
  },
  "location": { "directory": "C:\\Users\\dpes8" }
}

以上面這筆為例:578,816 ÷ (578,816 + 76,101 + 0) ≈ 88.4%。 也就是說,這個 session 有將近九成的 prompt token 是從快取讀到的。

步驟 1:建立 skill

shell
mkdir -p .opencode/skills/cache-stats/scripts
.opencode/skills/cache-stats/ ├── SKILL.md └── scripts/ └── cache_stats.py

步驟 2:SKILL.md

.opencode/skills/cache-stats/SKILL.md
---
name: cache-stats
description: 查詢 OpenCode 的 LLM prompt cache 命中率與 token 用量。當使用者問
  「快取命中率多少」「查快取」「cache hit rate」「這個對話省了多少 token」
  「哪個 session 快取效率差」,都用這個 skill 跑腳本拿真實數字,不要憑印象估算。
---

# 查快取命中率

## 一句話原則

**數字只能來自腳本。** 你的工作是把數據講清楚,不是心算或憑印象給一個「大概九成」。

## 怎麼跑

```bash
python "<skill-dir>/scripts/cache_stats.py"
```

- 這支腳本只接受這一個用法,不要自己發明參數。
- 想看更多工作階段、指定 session、輸出 JSON,屬於進階練習(見課程最後的挑戰)。

## 數字怎麼解讀

- 命中率 = cache read ÷ (cache read + input + cache write)
- `cache write = 0` 是正常的:這個 provider 走隱式快取,不另外回報寫入量
- 單一請求通常很高(98–99%);整場被拉低幾乎都是一次性內容造成的:
  剛貼的圖片、大量 log、整份檔案、`openapi.json` 這種大 dump
- 命中率 < 80% 時可以補一句可能原因,**不要編造成本數字**

## 回覆格式

先給整體命中率,需要時再附表格:

```
**整體命中率 88.4%**(cache read 578,816 / prompt 654,917 tokens)
```

- 百分比取一位小數,token 加千分位
- 只有一個工作階段時不要硬做表格,一行講完就好

## 出錯時

照實說,不要改用猜的數字:

- 找不到 CLI → 確認 `opencode` 在 PATH,或設定 `OPENCODE_BIN`
- `opencode api` 逾時 → 背景服務可能卡住,建議 `opencode service restart`
- 沒有任何 session → 直接說目前沒有資料

步驟 3:scripts/cache_stats.py(精簡版)

核心邏輯不到 40 行:呼叫 API → 取 tokens → 算比例 → 印表格。

.opencode/skills/cache-stats/scripts/cache_stats.py
#!/usr/bin/env python3
"""列出各工作階段的 prompt cache 命中率。"""
import json, shutil, subprocess, sys

def cli():
    """Windows 上 opencode 常是 .ps1 / .cmd,Python 直接呼叫會找不到。"""
    exe = shutil.which("opencode") or "opencode"
    low = exe.lower()
    if low.endswith(".ps1"):
        return ["powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", exe]
    if low.endswith((".cmd", ".bat")):
        return ["cmd", "/c", exe]
    return [exe]

def api_get(path):
    out = subprocess.run(cli() + ["api", "get", path], capture_output=True)
    if out.returncode != 0:
        msg = out.stderr.decode("utf-8", "replace") or out.stdout.decode("utf-8", "replace")
        sys.exit("opencode api failed:\n" + msg[:300] + "\ntry: opencode service restart")
    return json.loads(out.stdout.decode("utf-8-sig"))

def nums(s):
    t = s.get("tokens") or {}
    c = t.get("cache") or {}
    return int(t.get("input") or 0), int(c.get("read") or 0), int(c.get("write") or 0)

def pct(r):
    return "—" if r is None else "%.1f%%" % (r * 100)

sessions = api_get("/api/session").get("data") or []
if not sessions:
    sys.exit("目前沒有任何工作階段。")

sessions.sort(key=lambda s: (s.get("time") or {}).get("updated") or 0, reverse=True)

print("| 工作階段 | 模型 | 命中率 | cache read | 未命中 input |")
print("|---|---|---:|---:|---:|")
tr = ti = tw = 0
for s in sessions[:10]:
    i, r, w = nums(s)
    tr += r
    ti += i
    tw += w
    total = i + r + w
    model = (s.get("model") or {}).get("id") or "—"
    print("| %s | %s | %s | %s | %s |" % (
        s.get("title") or "(untitled)", model, pct(r / total if total else None),
        format(r, ","), format(i, ",")))

total = tr + ti + tw
print("\n**整體命中率 %s**(cache read %s / prompt %s tokens)" % (
    pct(tr / total if total else None), format(tr, ","), format(total, ",")))
⚠️ Windows 的編碼陷阱

不要在腳本後面接 | Out-String 或 | Format-Table,PowerShell 5.1 會把 UTF-8 轉成亂碼。讓腳本直接輸出即可;真的遇到亂碼時(尤其是把結果導到檔案或管線),先設 $env:PYTHONIOENCODING='utf-8' 再跑,或改用 Windows Terminal / PowerShell 7。

步驟 4:測試與驗收

shell
python .opencode/skills/cache-stats/scripts/cache_stats.py

# Windows 若中文變亂碼,加上 UTF-8 環境變數再跑一次:
# $env:PYTHONIOENCODING='utf-8'; python .opencode/skills/cache-stats/scripts/cache_stats.py

預期輸出(數字會隨你的使用變化):

預期結果
| 工作階段 | 模型 | 命中率 | cache read | 未命中 input |
|---|---|---:|---:|---:|
| 設定排程查詢台中天氣並於1分鐘後通知 | deepseek-v4.1-flash-free | 88.4% | 578,816 | 76,101 |
| Greeting | deepseek-v4.1-flash-free | 65.5% | 32,256 | 16,971 |

**整體命中率 86.9%**(cache read 611,072 / prompt 703,978 tokens)

接著在 TUI 裡用自然語言問:

TUI 內
這個對話的快取命中率多少?
✅ 進階挑戰

加上 --session <ID 前綴> 與 --json 兩個參數,並在 SKILL.md 的「怎麼跑」補上說明。加參數不難,難的是讓 skill 知道什麼時候該用哪一個——記得同步更新 description 或對應表格。


7排錯手冊

照順序查,通常前三條就解決九成的問題。

症狀先檢查什麼
opencode: command not found全域 npm 的 bin 目錄在不在 PATH?npm config get prefix 看一下,然後重開終端機。
啟動後毫無反應/一直轉圈opencode service status → opencode service restart → 再不行用 opencode --standalone 隔離問題。
模型回 401 / 429重新 /connect;確認額度、速率限制與供應商狀態。
Skill 完全沒被叫到① 檔名是不是 SKILL.md?② 有沒有 description?③ ID 大小寫對嗎?④ 被後面的來源覆蓋了?⑤ 權限是不是 deny?⑥ 重開 TUI 讓它重新掃描。
Skill 載入了但腳本跑不動先脫離 skill 手動跑一次那個指令。多半是 PATH、python vs py -3、或相對路徑搞錯。
Skill 有兩份、行為不一致同 ID 時後註冊的來源勝出(專案 .opencode/skills 優先於全域)。~/.claude/skills、~/.agents/skills 也會被掃到,別忘了它們。
終端機中文變亂碼先設 $env:PYTHONIOENCODING='utf-8';別把腳本輸出接 Out-String / Format-Table;改用 Windows Terminal 或 PowerShell 7。Python 端可加 sys.stdout.reconfigure(encoding="utf-8")。
想找 logopencode debug paths log,看 opencode.log;role=cli 是前端、role=server 是後端。必要時設 OPENCODE_LOG_LEVEL=DEBUG 重現一次。

8作業與自我檢核

做完這三題,你就真的會了。

作業 1:換你寫一個 skill

挑一個你每週都要重複做的事,做成 skill。題目範例:

要求:一定要有 description(含觸發與不觸發)、有「不可以做的事」、至少一個可執行的 scripts/ 檔案。

作業 2:驗證快取真的有效

開一個新 session,先只問一句簡短問題,記下命中率;接著貼一份 500 行的 log 再問一次,比較命中率變化。並用一句話解釋為什麼會這樣。

作業 3:寫一份 200 字的操作筆記

假設同學的電腦完全空白,用你自己的話寫出安裝到第一個 skill 生效的步驟。寫不出來的地方,就是你還沒搞懂的地方。

最終檢核表


9延伸資源

官方文件是唯一真相,其他都是二手資料。

主題連結
文件首頁opencode.ai/v2/docs
安裝與平台支援Intro / Install
CLI 指令v2/docs/cli
Skillsv2/docs/skills
設定檔(opencode.json)v2/docs/config
終端機偏好(cli.json)v2/docs/cli/config
MCP 伺服器v2/docs/mcp-servers
排錯v2/docs/troubleshooting
API 參考v2/docs/api
📎 版本提醒

本文件以 OpenCode v2 為基準(撰寫時本機實測 v2.0.32)。指令與設定欄位可能隨版本調整,遇到不一致時,以官方 v2 文件為準,不要參考 v1 的舊頁面。