OpenCode 入門實作課
從零開始,把一台乾淨的電腦變成可以跟 AI 一起寫程式的環境。你會依序完成 Node.js 安裝、OpenCode 安裝、基礎指令演練,最後親手做出兩個可重複使用的 Skill:擲骰子與快取查詢。
0課程介紹與學習地圖
先看清楚終點,再開始走路。
這份教材不談抽象理論,每個章節都以「能動起來」為驗收標準。你會建立一個自己的 專案資料夾,在裡面跟 OpenCode 對話,最後把手上的重複工作「固化」成 Skill, 讓 AI 每次都照同一套規矩做事。
Node.js 與 OpenCode 裝好、版本驗證通過、模型連線成功。
會開 TUI、會用 opencode run、看得懂背景服務狀態。
知道資料夾放哪、frontmatter 怎麼寫、什麼情況 AI 才會自動叫它。
做出「擲骰子」與「快取查詢」兩個可執行的 Skill。
不要跳著做。第 1、2 章是地基,第 4 章之後的實作都會直接呼叫 opencode 指令——如果它不在 PATH 裡,後面的範例全部會失敗。
i名詞速查
看懂這幾個詞,整份文件就不會卡。
| 名詞 | 說明 |
|---|---|
| Agent | 會自己決定「下一步做什麼」的 AI:能讀檔、跑指令、改程式。OpenCode 就是一個 agent 的宿主。 |
| TUI | Terminal 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,
點兩下、一路按「下一步」就好。
-
打開官方下載頁
瀏覽器輸入 nodejs.org/zh-tw/download
(英文版是 nodejs.org/en/download)。
認明網址是nodejs.org,不要從搜尋結果的廣告或第三方程式下載站抓。 -
按「LTS」那個按鈕,不要按「Current」
LTS 是長期維護版(現在是 22 或 24 系列),Current 是最新實驗版,可能踩到套件不支援的雷。
Windows 選哪個檔案?
下載頁的 Windows Installer (.msi),一般筆電/桌機選 x64;只有 ARM 筆電(例如 Snapdragon 系列)才選 ARM64。macOS 選 .pkg,Apple M 系列或 Intel 都有對應版本。
-
執行安裝檔,一路「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。
- 關掉所有終端機視窗,再重新開一個 安裝程式只會更新「之後才開啟」的視窗的 PATH。舊視窗開著就還是找不到指令,這是最常見的「明明裝好了卻說找不到」。
- 驗證 照下一節 1.1 驗證安裝 打兩行指令確認。看到版本號就成功了。
不用先搞懂終端機、不用管 PATH 怎麼設、也不用處理權限。安裝程式會把該做的事做完,對第一次接觸的同學最不容易卡住。之後想學指令再回來看方法 B。
方法 B:套件管理器(一行指令)
已經有 winget 或 Homebrew 的人可以用這招,速度快、之後升級也方便。
# 用 winget 安裝 Node.js LTS winget install OpenJS.NodeJS.LTS # 裝完務必關掉終端機再重開,PATH 才會生效
brew install node
# 或者只裝 Node 24
brew install node@24
方法 C:用版本管理器(要同時跑多專案再用)
fnm 或 nvm 可以讓你一句話切換 Node 版本,
也能避開 macOS / Linux 上常見的權限問題(不需要 sudo npm install -g)。
# 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
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 found | PATH 沒生效。關掉終端機重開;Windows 可執行 where node 確認。使用 fnm 時要確認 shell 初始化設定(PowerShell 的 profile)已載入 fnm env。 |
npm WARN EBADENGINE | Node 版本太舊。升級到 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 install -g @opencode/cli
方法 B:官方安裝腳本(macOS / Linux)
curl -fsSL https://opencode.ai/v2/install | bash
方法 C:Homebrew 或獨立執行檔
# 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 的套件管理器(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 之後,先連接模型供應商:
# 在輸入框打斜線就會出現指令清單 /connect # 選你的供應商(OpenCode Console、Anthropic、OpenAI…)完成登入 # 想不起來有哪些指令?直接打「/」看完整清單
可以先走 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 操作要點
- 斜線指令:在輸入框打
/會列出可用指令,例如/connect(連接供應商)、/mcps(管理 MCP 與登入)。清單以你當下的版本為主。 - 設定面板:按 Ctrl + P 選 Open settings,可改主題、鍵位等終端機偏好。這些偏好存在
~/.config/opencode/cli.json,跟專案的opencode.json是兩回事。 - 權限詢問:agent 執行有風險的動作(寫檔、跑指令)會停下來問你。看清楚再按同意,這是你的安全網。
- 上下文:對話太長時 OpenCode 會做 compaction(摘要壓縮)。想省錢的話,把大檔用「指定行號」或先 grep 再讀,而不是整包丟進去。
3.2 三個暖身練習
練習 1:非互動模式
cd my-agent-lab opencode run "用三句話解釋什麼是 prompt cache"
你應該會直接在終端機看到答案,然後回到 shell。這就是「自動化」的入口。
練習 2:讓它讀你的檔案
在專案裡建一個 hello.js,貼上幾行程式,然後問它:
幫我看 @hello.js,指出可以改進的地方,先不要改檔案
@ 是引用檔案的語法。注意最後那句「先不要改檔案」——限制行動範圍是 prompt 的基本功。
練習 3:確認服務健康
opencode service status opencode api get /api/info
兩個都正常回應,才代表你的「地基」是穩的。之後 Skill 的腳本會用到 opencode api,所以這步不能跳。
4什麼是 Skill
把「你每次都要講一遍的規矩」變成可重複使用的模組。
Skill 就是一個資料夾,裡面有一份 SKILL.md,加上它需要的腳本與參考文件。
平常 OpenCode 只把每個 skill 的 ID、名稱、description 放進模型的可用清單;
當任務真的符合時,模型才呼叫 skill 工具,把完整的 Markdown 內容載入。
沒用到就不載入,長篇 SOP 不會擠佔每次對話的額度。
同樣的流程每次都照做,不會因為換 session 就忘記。
一個資料夾就能傳給同學,或放進 repo 讓整組一起用。
4.1 檔案結構與存放位置
建議使用「資料夾 + SKILL.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 陣列加路徑(相對路徑以當前工作目錄為基準,不是設定檔所在位置):
{
"$schema": "https://opencode.ai/config.json",
"skills": [
"./team-skills",
"~/shared/opencode-skills"
]
}
4.2 寫好 SKILL.md 與 description
最小可用的檔案長這樣:
--- 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.md | git-release |
<source>/git-release/SKILL.md | git-release |
<source>/teams/release/SKILL.md | release |
可攜性起見,建議用全小寫 kebab-case,長度 1–64 字元,並跟資料夾同名:^[a-z0-9]+(-[a-z0-9]+)*$。
怎麼被載入
- 自動:模型看到任務符合 description,就呼叫
skill工具載入。 - 手動:在 prompt 裡打
@dice-roll之類的@<skill-id>,直接指定。 - 載入後,OpenCode 會把 Markdown 內容(不含 frontmatter)加進對話,並附上 skill 的資料夾路徑與最多 10 個支援檔案路徑。支援檔案不會自動讀取,要由 SKILL.md 指示模型去讀。
權限
預設都能用。想限制某些 skill,用 permissions(規則依序套用,最後一筆符合的生效):
{
"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:建立資料夾
# 專案內(跟同學共用) 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
步驟 2:寫 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):
#!/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:先自己測腳本
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
如果 python 不在 PATH,Windows 請改用 py -3,並把 SKILL.md 裡的指令一起改掉。腳本本身跑不起來,skill 一定失敗,而且錯誤訊息會很難查。
步驟 5:在 OpenCode 裡驗收
重開 TUI(讓它重新掃描 skill 清單),然後直接用人話測試:
幫我骰 3d6+2
# 或手動指定,確認它真的載入
@dice-roll 骰個 d20
合格的回覆長這樣:
🎲 3d6+2 → 14
`4, 2, 6` +2
- 每一顆骰子的點數都有列出來,不是只有總和。
- 修正值
+2跟骰子分開顯示。 - 連續骰 5 次,結果有明顯變化(不是每次都 10~11)。
- 問它「用亂數幫我產生 UUID」,它不會觸發這個 skill。
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
mkdir -p .opencode/skills/cache-stats/scripts
步驟 2: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 → 算比例 → 印表格。
#!/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, ",")))
不要在腳本後面接 | Out-String 或 | Format-Table,PowerShell 5.1 會把 UTF-8 轉成亂碼。讓腳本直接輸出即可;真的遇到亂碼時(尤其是把結果導到檔案或管線),先設 $env:PYTHONIOENCODING='utf-8' 再跑,或改用 Windows Terminal / PowerShell 7。
步驟 4:測試與驗收
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 裡用自然語言問:
這個對話的快取命中率多少?
- 它有實際執行腳本(你會看到一次工具呼叫),而不是直接回答「大約九成」。
- 數字有千分位,百分比只有一位小數。
- 只有一個 session 時,不要硬做表格。
- 問「幫我查 UUID 產生器」時不會觸發。
加上 --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")。 |
| 想找 log | opencode debug paths log,看 opencode.log;role=cli 是前端、role=server 是後端。必要時設 OPENCODE_LOG_LEVEL=DEBUG 重現一次。 |
8作業與自我檢核
做完這三題,你就真的會了。
作業 1:換你寫一個 skill
挑一個你每週都要重複做的事,做成 skill。題目範例:
- 工作日誌:把今天的 git commits 整理成三條 bullet,輸出固定格式。
- 檔名整理:給一個資料夾,依副檔名分類,先列出計畫再動手。
- 週報產生器:讀
notes/*.md,產出一份 Markdown 週報。
要求:一定要有 description(含觸發與不觸發)、有「不可以做的事」、至少一個可執行的 scripts/ 檔案。
作業 2:驗證快取真的有效
開一個新 session,先只問一句簡短問題,記下命中率;接著貼一份 500 行的 log 再問一次,比較命中率變化。並用一句話解釋為什麼會這樣。
作業 3:寫一份 200 字的操作筆記
假設同學的電腦完全空白,用你自己的話寫出安裝到第一個 skill 生效的步驟。寫不出來的地方,就是你還沒搞懂的地方。
最終檢核表
node -v顯示 22 以上,且npm -v正常。opencode --version有版本號,opencode service status正常。- 能在 TUI 裡用
@檔案讓 agent 讀取內容。 - 用
opencode run "..."拿到非互動的答案。 - 自製的 skill 放在正確路徑,ID 與資料夾一致。
- 用自然語言就能觸發 skill,且輸出格式符合 SKILL.md 的規定。
- 知道 skill 沒生效時,要從哪三個地方開始查。
9延伸資源
官方文件是唯一真相,其他都是二手資料。
| 主題 | 連結 |
|---|---|
| 文件首頁 | opencode.ai/v2/docs |
| 安裝與平台支援 | Intro / Install |
| CLI 指令 | v2/docs/cli |
| Skills | v2/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 的舊頁面。