0%

Claude Code Skill 撰寫指南:Anthropic 工程師親授實戰心得

本文整理自 Anthropic 技術成員 Thariq Shihipar 的實戰分享,探討如何有效撰寫 Claude Code Skills。
截至 2026 年 6 月,Anthropic 內部已有「數百個」skill 在活躍使用。

核心概念

Skill 是什麼?

Skill 可以理解成一種「脈絡工程」(context engineering):在對的時機,把對的資訊提供給 AI。

一、為什麼資料夾比單一檔案好用?

問題點

  • 如果把所有東西塞進一個檔案,等於每次都把整本說明書攤在 Claude 面前

解決方案

  • 改用資料夾結構,搭配「漸進揭露」(progressive disclosure)
  • 需要時才讀對應檔案,Claude 才不會被無關內容干擾

官方建議

把詳細 API 文件、範例、腳本等 supporting files 放在 skill 資料夾裡,並由 SKILL.md 說明何時讀取。


二、9 大 Skill 分類

Anthropic 盤點內部 skill 後,發現它們大致聚成 9 種類型。最好的 skill 通常只做一類;想做太多事的會橫跨多類,反而把 agent 搞混。

1. 函式庫與 API 參考

  • 教 Claude 正確使用某個函式庫、CLI 或 SDK
  • 附上常踩的雷

2. 產品驗證

  • 描述怎麼測試、驗證程式碼有沒有真的動
  • Anthropic 說這類對產出品質「可量測的影響最大
  • 值得花一週做到極好

3. 資料抓取與分析

  • 接上你的資料與監控系統
  • 內含抓資料的腳本、儀表板代號

4. 業務流程與團隊自動化

  • 把重複工作流收成一個指令
  • 例如:每日 standup、開票

5. 程式碼鷹架與範本

  • 自動生出新服務、新模組的樣板

6. 程式碼品質與審查

  • 強制團隊的程式風格
  • 協助 code review

7. CI/CD 與部署

  • 顧 PR、跑測試、漸進部署
  • 出問題自動回滾

8. Runbooks(故障排除手冊)

  • 接到一個症狀,走完多工具調查
  • 產出結構化報告

9. 基礎設施操作

  • 執行例行維運
  • 對破壞性動作加上護欄

三、最高價值的「Gotchas(陷阱)」區塊

核心觀念

任何 skill 裡訊號最高的內容,就是 Gotchas 區塊。

什麼是 Gotchas?

  • Claude 使用這個 skill 時最常踩的失敗點
  • 要隨時間累積、持續補充
  • 往往是「文件不會寫、但你踩過坑才知道」的細節

實例(以請假系統為例)

範例一:版本問題

「同一張假單可能改過好幾次,要抓『最後核准』那一版,不是最早送出的那張。」

範例二:術語對應

「業務部口中的『出差』,在人資系統裡叫『公假』,其實是同一件事,只是部門叫法不同。」

範例三:狀態判斷

「系統顯示『已送出』不代表假請成功了,還要看主管那關有沒有按下核准。」

重點

  • 把「平常沒人會特別告訴你、有出錯過而學到」的細節寫下來
  • skill 才會越用越準

四、描述(description)要寫給模型看

關鍵認知

description 欄位不是給人看的摘要,而是「什麼情況下該觸發我」的說明。

運作機制

  • Claude Code 啟動 session 時,會建立一份可用 skill 與 description 的清單
  • 用它判斷「使用者這個請求,有沒有對應的 skill 可以用」

實務技巧

把觸發詞直接寫進描述,命中率會更高

例如:

1
description: 彙整這週完成的工作,產生給主管看的週報。當使用者說「跑週報」「整理這週進度」時觸發。

五、給 Claude 腳本與檔案,讓它「組合」而不是「重造」

核心概念

把可重複的動作寫成腳本交給 Claude,它就能把每一回合花在「決定下一步做什麼」,而不是重打一次樣板。

進階技巧:隨選 hooks(on-demand hooks)

只在這個 skill 被呼叫時才生效

範例 1:/careful

  • 動正式環境時才開
  • 自動擋掉 rm -rfDROP TABLE、force-push 等危險指令

範例 2:/freeze

  • debug 時只准改特定資料夾
  • 避免手滑「修好」無關的程式碼

六、SKILL.md 完整範本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
---
name: weekly-report
description: 彙整這週完成的工作,產生給主管看的週報。當使用者說「跑週報」「整理這週進度」時觸發。
---

# 週報產生器

## 怎麼做

1.`config.json` 取得週報要寄給誰、用什麼格式;若還沒設定,先問使用者。
2. 彙整這週完成的事項,只列「跟上週相比的新進度」,不要把舊的重講一遍。
3. 把這次的週報存進 `reports.log`(只新增、不覆蓋),下次執行時讀自己的歷史,自動判斷哪些是新的。

## Gotchas(踩過的坑)

- 「標記完成」不等於真的結案,有些項目會被退回重做,狀態要看最新的那一版。
- 同一件事這週可能改過好幾次,只取最後定案的版本,別把過程中的草稿都列進去。

## 參考

- 欄位與格式範本見 `references/format.md`(需要時再讀,不必一開始全載入)。

七、動手前要知道的兩個前提

前提一:Skill 不是免費的

  • 一般 session 中,skill 的 description 會進入 context
  • 完整 SKILL.md 則是在使用者或 Claude 呼叫後才載入
  • 每多一個放進專案的 skill,都會增加一點模型需要掃描的脈絡

團隊規模建議

改用 plugin marketplace,讓成員自己挑要裝哪些,而不是全部塞進每個專案。

前提二:別想一次寫到完美

Anthropic 最好用的 skill 多半是從「寥寥幾行字加一個 gotcha」起步

迭代思維

  • 先做一個小的
  • 撞到坑就補一條 gotcha
  • 慢慢變強

核心價值

Skill 的價值不在你寫了多少,而在你有沒有告訴 Claude「它原本不知道、或老是做錯的那件事」。


八、總結與行動建議

撰寫 Skill 的黃金法則

  1. 用資料夾結構,不要單一檔案
  2. 一個 Skill 只做一類事
  3. Gotchas 區塊是最高價值內容
  4. description 要寫觸發詞,給模型看
  5. 提供腳本讓 Claude 組合,不是重造
  6. 從小開始,持續迭代

優先順序

產品驗證類 Skill 對品質影響最大,值得優先投入。


參考資料

這篇文章在談的是 JavaScript 的 Explicit Resource Management,也就是讓程式碼更一致地處理「資源的建立與清理」。

對前端工程師來說,這類問題很常出現在 WebSocket、fetch abort、Streams、IntersectionObserver、檔案處理、計時器或任何需要明確釋放的 API 上。文章的重點不是「控制垃圾回收」,而是讓我們用更一致、可預期的方式,告訴 JavaScript:這個資源什麼時候該收尾。

核心概念

作者把這件事拆成兩層:

  1. Implicit resource management
  2. Explicit resource management

前者是 JavaScript 既有機制,後者是新的語法與協定。

Read more »

在實際工作中,我發現同事在拿到名片後,仍然需要經過一連串人工流程:

  • 手機拍照、或是掃描名片
  • 傳到電腦
  • 手動輸入公司、姓名、電話
  • 還很容易漏資料或輸錯

因此我開始思考:
能不能在不架設伺服器、不導入複雜系統的情況下,把這件事自動化?

這篇文章記錄我實作的一套 名片自動建檔系統,核心完全建立在 Google 生態系上。

系統目標與整體概念

我的目標很單純:

只要用手機拍照上傳名片,就能自動解析內容,並寫入 Google Spreadsheet。

整體流程如下:

1.手機直接拍照,上傳到指定的 Google Drive 資料夾

2.由 Google Apps Script 定期掃描新圖片

3.使用 Gemini AI API 辨識名片內容(非單純 OCR),將結構化資料寫入 Google Spreadsheet

4.將處理完成的名片移動到封存資料夾,避免重複處理

使用到的工具與技術

  • Google Drive:名片圖片的儲存與狀態管理
  • Google Sheets:結構化資料表(輕量資料庫)
  • Google Apps Script:自動化中樞
  • Google Gemini Vision API:影像 + 語意理解

這個組合的優點是:
不需要額外主機
成本低
團隊成員幾乎都能上手
維護成本非常小

資料欄位設計

實際使用時,名片資訊往往比想像中複雜(多支電話、分機、圖示標註),
因此我最後採用以下欄位結構:

  • 狀態
  • 公司
  • 姓名
  • 職稱
  • Email
  • Mobile(手機)
  • Office Phone(公司電話 / 分機)
  • 地址
  • 建檔日期
  • 原始檔案網址

這些欄位會在 Google Spreadsheet 的第一列先設定好,後續 Apps Script 會依照固定順序寫入。

第一步:Google Drive 環境準備

先在 Google Drive 建立一個主資料夾,例如:名片管理系統

並在裡面建立兩個子資料夾:

  • Inbox(待處理):
    手機拍照後,名片圖片一律上傳到這裡

  • Archived(已建檔):
    名片處理完成後會自動移動到這個資料夾

重要

請記下這兩個資料夾網址列中 folders/ 後面的 ID,後續 Apps Script 需要使用。

Read more »

You are given two integer arrays nums1 and nums2, sorted in non-decreasing order, and two integers m and n, representing the number of elements in nums1 and nums2 respectively.

Merge nums1 and nums2 into a single array sorted in non-decreasing order.

The final sorted array should not be returned by the function, but instead be stored inside the array nums1. To accommodate this, nums1 has a length of m + n, where the first m elements denote the elements that should be merged, and the last n elements are set to 0 and should be ignored. nums2 has a length of n.

給定兩個整數陣列 nums1nums2,都按照非遞減順序排列,以及兩個整數 mn,分別代表 nums1nums2 中元素的個數。
請將 nums1nums2 合併為一個按非遞減順序排列的陣列。
最終的排序陣列不應該由函數返回,而是要儲存在陣列 nums1 內部。為了實現這一點,nums1 的長度為 m + n,其中前 m 個元素表示應該合併的元素,後 n 個元素設為 0 並且應該被忽略。nums2 的長度為 n

目標:將 nums2 合併於 nums1

  • 必須在 nums1 內部完成合併,不能使用額外的陣列空間
  • 不返回值:函數不需要 return,直接修改 nums1
Read more »

最近在部署專案到 Vercel 時,遇到了一個讓我困擾的錯誤訊息:「Unexpected token ‘<’」。這個錯誤通常表示我們預期收到的是 JSON 格式的資料,但實際上卻收到了 HTML 頁面。本文將分享我遇到這個問題的背景、原因,以及兩種解決方案,幫助大家在使用 Vercel 部署時能夠順利取得 API 資料。

部署至 Vercel 遇到「Unexpected token ‘<’」錯誤:原因與解法

在將專案部署到 Vercel 時,當我透過瀏覽器或 extension background fetch 呼叫 API,卻出現以下錯誤:

1
取得資料夾失敗: Unexpected token '<', "<!doctype "... is not valid JSON

那代表請求並沒有真正拿到 JSON,而是得到了 HTML 頁面。本文將說明這個問題的原因、背後的背景,以及兩種完整的解法。

問題說明

這次在 Arc 瀏覽器 的開發過程中,我嘗試從部署於 Vercel 的 API 端點取得資料,但 Console 顯示:

1
index.iife_dev.js:24922 取得資料夾失敗: Unexpected token '<', "<!doctype "... is not valid JSON

進一步到 Arc 的 Service-Worker DevToolsarc://inspect/#service-workers)中,打開 background worker 的 Console,可以看到請求的回應實際上是一份 HTML 登入頁面,而非 JSON 資料。


背景補充:Vercel 的 Deployment Protection

這個錯誤並非來自 CORS,而是因為 Vercel 的 Deployment Protection 機制。
當專案開啟「Vercel Authentication」後,所有 Preview Deployments 都需要登入才能訪問。

因此,extension background 在發出請求時,被導向至登入頁面(HTML),導致解析 JSON 時出現:

Unexpected token '<'

Read more »

題目描述

You are given the heads of two sorted linked lists list1 and list2.

Merge the two lists into one sorted list. The list should be made by splicing together the nodes of the first two lists.

Return the head of the merged linked list.

給定兩個已排序鏈結串列的頭節點 list1 和 list2。
將兩個串列合併為一個已排序的串列。該串列應透過拼接前兩個串列的節點來建立。
回傳合併後鏈結串列的頭節點。

Example 1:
Input: list1 = [1,2,4], list2 = [1,3,4]
Output: [1,1,2,3,4,4]

Example 2:
Input: list1 = [], list2 = []
Output: []

Example 3:
Input: list1 = [], list2 = [0]
Output: [0]

解題思路

我一開始想法,會需要 2 迴圈去比較,然後組出一個新的 list node,最後再將新的 list node 回傳。
但是這樣的話,時間複雜度會是 O(n^2),因為每次都要從頭開始比較。
後來想說,因為兩個 list 都是已排序好的,所以可以用雙指標的方式,一次比較兩個 list 的頭節點,將較小的節點加入新的 list node,然後將該指標往後移動一位,這樣時間複雜度就會是 O(n),因為每個節點只會被比較一次。

範例程式碼

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
/**
* Definition for singly-linked list.
* function ListNode(val, next) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
*/
/**
* @param {ListNode} list1
* @param {ListNode} list2
* @return {ListNode}
*/
var mergeTwoLists = function(list1, list2) {
let head = null
let nowNode = null
let list1pointer = list1;
let list2pointer = list2;
// 兩兩比較
if(list1 === null && list2 === null) return head
if(list1 === null && list2 !== null) return list2
if(list1 !== null && list2 === null) return list1


// 決定 head
// 因為當你選擇了 list1pointer 作為頭節點後,你應該:
// 移動 list1pointer 指針:list1pointer = list1pointer.next
// 設定 nowNode 為當前的尾節點:nowNode = head
if(list1pointer.val <= list2pointer.val ){
head = list1pointer
list1pointer = list1pointer.next
}else{
head = list2pointer
list2pointer = list2pointer.next
}
nowNode = head

//進入迴圈檢查
while(list1pointer !== null && list2pointer !== null){
if(list1pointer.val <= list2pointer.val){
nowNode.next = list1pointer
list1pointer = list1pointer.next
}else {
nowNode.next = list2pointer
// 移動指針
list2pointer = list2pointer.next
}
nowNode = nowNode.next;
}
// 最後要再檢查。其中一不為 null
if(list1pointer !== null){
nowNode.next = list1pointer
}else{
nowNode.next = list2pointer
}

return head

};
Read more »

Given an array of meeting time intervals where intervals[i] = [startᵢ, endᵢ], determine if a person could attend all meetings.

給定一個會議時間 intervals 陣列,其中 intervals[i] = [startᵢ, endᵢ],判斷一個人是否能夠參加所有會議。

Example 1:

Input:

intervals = [[0,30],[5,10],[15,20]]

Output:

false

Example 2:

Input:

intervals = [[7,10],[2,4]]

Output:

true

Constraints:

  • 0 <= intervals.length <= 10⁴
  • intervals[i].length == 2
  • 0 <= startᵢ < endᵢ <= 10⁶

解題思路

要判斷一個人是否能夠參加所有會議,關鍵在於檢查會議時間是否有重疊。若有任何兩個會議的時間區間重疊,則無法參加所有會議。

重疊情況分析

[1,5][2,4] 為範例:

  1. 假設會議時間按開始時間排序:將所有會議按照開始時間由早到晚排列
  2. 檢查相鄰會議:排序後,只需檢查相鄰的會議是否重疊
  3. 重疊條件:前一個會議的結束時間 > 下一個會議的開始時間

步驟

  1. 將所有會議按開始時間排序
  2. 遍歷排序後的會議陣列
  3. 檢查每個會議的結束時間是否晚於下一個會議的開始時間
  4. 若發現重疊,回傳 false;否則回傳 true

這樣的方法時間複雜度為 O(n log n)(主要是排序的時間),空間複雜度為 O(1)。

Read more »

列表的動態排序和插入為常見的需求。然而,當面對大量資料時,傳統的「刪除重建」策略容易會成為效能瓶頸。本文將分享我在專案中如何透過 SeqNo 管理機制,將列表插入效能提升,同時保持資料一致性和系統穩定性。

在專案內有一下拉列表的功能,當使用者選擇某個選項後,會將該選項插入到列表中,並且會依據使用者的操作順序來進行排序。最初的實作方式是每次插入新選項時,都會重新計算整個列表的排序,這在資料量較大時,導致效能明顯下降。為了解決這個問題,引入了 SeqNo 管理機制。

原有問題

當使用者要在列表中間插入新的資料時,使系統面臨挑戰,並且在沒有加入 seqNo 管理機制前,容易遇到排序衝突的問題。

1
2
3
4
5
6
7
// 原有狀態
[
{ id: 'A', seqNo: 1 },
{ id: 'B', seqNo: 2 }, // 要在 B 後面插入新項目
{ id: 'C', seqNo: 3 },
{ id: 'D', seqNo: 4 }
]

如果直接將新項目設為 seqNo: 3,會與現有的項目 C 發生衝突。

傳統解決方案的困境

直觀的解決方案是重新排序所有項目:

1
2
3
4
5
6
7
8
9
10
// 刪除重建
async function insertListData(afterId: string,
newListData: ListData) {
// 1. 刪除所有後續項目 (N-M 次刪除)
await deleteListDataAfter(afterId);

// 2. 重新建立所有項目 (N-M+1 次建立)
await recreateListDataWithNewSeqNo(newListData,
subsequentListData);
}
  • 操作複雜度: O(2N+1) - 其中 N 為列表總長度
  • 資料庫壓力: 大量刪除和建立操作
  • 交易風險: 操作步驟過多,失敗機率高
  • 併發問題: 長時間鎖定,容易產生競爭條件

SeqNo 管理機制

只更新真正需要變動的項目,將複雜度進行改善,是插入點之後的項目數量。

  1. 排序基礎函式
    確保所有項目能依照 seqNo 正確排序。
1
2
3
4
5
6
7
8
// 排序工具
export function sortListDataBySeqNo(listData: ListData[]): ListData[] {
return [...listData].sort((a, b) => {
const aSeqNo = a.seqNo || 0;
const bSeqNo = b.seqNo || 0;
return aSeqNo - bSeqNo;
});
}
  1. 插入策略計算

只計算「插入點之後」需要調整的項目,並產生更新操作清單。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
 export function calculateInsertStrategy(
existingListData: ListData[],
afterListDataId: string
): {
insertSeqNo: number;
affectedListData: ListData[];
updateOperations: Array<{ listDataId: string; newSeqNo: number }>;
} {
const sortedListData = sortListDataBySeqNo(existingListData);
const afterIndex = sortedListData.findIndex(data => data.id === afterListDataId);

if (afterIndex === -1) {
throw new Error('afterListDataId not found');
}

// 關鍵:插入點的 seqNo + 1
const insertSeqNo = sortedListData[afterIndex].seqNo! + 1;
// 只影響插入點之後的項目
const affectedListData = sortedListData.slice(afterIndex + 1);

// 產生更新操作清單
const updateOperations = affectedListData.map(data => ({
listDataId: data.id,
newSeqNo: data.seqNo! + 1
}));

return { insertSeqNo, affectedListData, updateOperations };
}

  1. 交易執行
    確保所有更新操作能在同一個 Transaction 中完成。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
export async function executeSeqNoUpdates(
transaction: Transaction,
operations: Array<{ listDataId: string; newSeqNo: number }>
): Promise<void> {
for (const operation of operations) {
// 更新每個受影響的項目
const listDataRef = adminDb.collection('listData').doc(operation.listDataId);
transaction.update(listDataRef, {
seqNo: operation.newSeqNo,
updatedAt: new Date()
});
}
}

API 實現範例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
  // POST /api/v1/listData
export async function POST(req: Request) {
// ... 驗證邏輯

if (afterListDataId) {
try {
// 步驟1: 計算最小更新策略
const { updateOperations, insertSeqNo } =
calculateInsertStrategy(existingListData, afterListDataId);

// 步驟2: Transaction 執行
const result = await adminDb.runTransaction(async (transaction) => {
await executeSeqNoUpdates(transaction, updateOperations);

// 插入新項目
const listDataRef = adminDb.collection('listData').doc();
transaction.set(listDataRef, {
...newListData,
seqNo: insertSeqNo
});

return { id: listDataRef.id, seqNo: insertSeqNo };
});

return NextResponse.json(result, { status: 201 });
} catch (error) {
if (error.message === 'afterListDataId not found') {
return NextResponse.json({ message: 'afterListDataId not found' }, { status: 404 });
}
throw error;
}
}
}

此專案的應用回顧與心得

這個問題最初是在「插入列表中間」時被發現的。
當時經常出現排序錯亂,追查後才發現是因為缺乏 SeqNo 管理機制,導致插入時 seqNo 發生衝突。

一開始的解法是 刪除重建,但這帶來兩個明顯的問題:
1.當資料量大時,對後端資料庫造成極大負擔。
2.使用者操作無法預測,若頻繁在列表中間插入或刪除,效能會快速下降。

引入 SeqNo 管理機制 後:

  • 插入效能明顯提升
  • 保持了資料一致性
  • 系統穩定性也大幅改善
    這樣的設計不僅解決了排序衝突,也大幅減少了資料庫的操作次數,讓整體效能更加穩定可