composition-patterns (vercel-labs/agent-skills)
把 React 元件重構為複合元件與可重用 API,避免布林 prop 不斷增加。
vercel-composition-patterns:把布林 prop 的 React 元件改成複合元件 API(對照組 3.7、技能 4.4),Token 沒有增加,但把同樣建議寫成一句話的提示得分 4.7
Codex CLI · gpt-6-astra
25.1 / 30
相對未使用技能的提升:+0.68 · 視情況推薦 · 18 次執行
視情況推薦:提升幅度 +0.68 低於 +1.0。推薦條件為總分至少 22/30、產出品質至少 3.5、提升幅度至少 +1.0。
SOTA
透過使用 Codex 測試。
涵蓋 Codex, Claude Code
Codex CLI 0.166(SOTA 等級):18 次評分執行。依我們的等級規則,在這裡通過也涵蓋 Claude Code;但這不代表第二等級的結果。
分數
每個維度為 0 到 5 分,六個維度相加即為滿分 30 的總分。每個維度都是 5 分最好。成本 5 分代表便宜又快速;安裝 5 分代表容易安裝。
- 觸發可靠性
- 5.0
- 相對未使用技能的提升
- 2.7
- 產出品質
- 4.4
- Token 與時間成本
- 5.0
- 安裝與可攜性
- 4.0
- 安全性
- 4.0
安全審查
安全靜態掃描:0 CRITICAL / 0 HIGH / 0 MEDIUM。人工審查:可在無網路沙箱中安全執行(未發現問題)。掃描結果乾淨,不代表技能一定安全。
安裝
npx skills@latest add vercel-labs/agent-skills --skill composition-patterns
固定 Commit: 1a2c79d4e6c4a2283dc2c614b6538cbe1ed5ca62
測試日期: 2026-11-02
我們測了什麼
- 以組合取代布林 prop — Composer.tsx 已經長出一堆布林 prop。把它重構成不需要這些 prop 的有彈性元件 API。保留在單一檔案 Composer.tsx,並維持行為不變。
- 新增變體而不新增 prop — 新增「快速回覆」輸入框:單行輸入、按 Enter 送出,沒有附件或表情符號按鈕。不要為 Composer 增加更多布林 prop。編輯 Composer.tsx。
- 審查元件 API — 審查 Composer.tsx 的元件 API:哪裡有問題、你會怎麼改?不要修改任何檔案。
優點
- 相對未使用技能,平均分數從 3.71 提升到 4.39(+0.68)。
- 技能即使未被點名也會被採用:6/6 次實驗組執行都讀取了技能檔案。
- 沒有 Token 額外負擔:是未使用技能的 1.03 倍。
缺點
- 一句話提示得分 4.7,使用技能則為 4.39(-0.31)。
- 每個任務的每個組別 n=2;約 1 分以內的差異屬於雜訊。
評測
Codex CLI 0.166(gpt-6-astra,低推理)在專用的 Windows 測試電腦上執行,每次執行都使用全新的暫存專案與暫存的 HOME/TMPDIR/快取;技能已安裝(實驗組)、未安裝(對照組),或未安裝但把技能的核心要求濃縮成一句話附在提示詞後(一句話提示組)。每個任務的每個組別執行 2 次,關閉網路,封鎖 /tmp 寫入。提示詞從未提及技能(下方註明的措辭除外)。一個聊天輸入元件(Composer.tsx)帶有一堆布林 prop。任務:在不改變行為的前提下把它重構成有彈性的 API、新增「快速回覆」單行輸入框而不增加更多布林 prop,以及審查元件 API(不修改)。提示詞沒有點名任何模式。一句話提示組則點名這些模式(從複合零件提供明確變體、以共用 context 管理 state/actions、用 children 取代 renderX、把 state 提升到 provider)。
三組對照
| 組別 | 平均分數(0-5) | 每次執行 Token 用量 |
|---|---|---|
| 未使用技能 | 3.71 | 103k |
| 一句話提示 | 4.7 | 105k |
| 使用技能 | 4.39 | 105k |
以 Codex CLI 執行 18 次,採規則式檢查評分,不經 LLM 評審。總分 25.1 / 30,評級為 視情況推薦。
判定:視情況而定(25.1 / 30)。 來源:vercel-labs/agent-skills,skills/composition-patterns,固定於提交 1a2c79d。安裝:npx skills@latest add vercel-labs/agent-skills --skill composition-patterns。授權條款: MIT(儲存庫 LICENSE)。
這是什麼
vercel-composition-patterns 技能(vercel-labs/agent-skills):React 組合指引:避免布林屬性氾濫、建立透過 context 共享狀態的複合元件(通用的狀態/動作介面)、建立明確的變體元件、偏好 children 而非 renderX 屬性、將狀態提升到 provider;包含 React 19 的注意事項。一份 SKILL.md 加上一個規則資料夾。
我們如何測試
在專用 Windows 測試 PC 上使用 Codex CLI 0.166(gpt-6-astra,低推理),每次執行都使用全新的暫存專案與暫存 HOME/TMPDIR/快取,安裝技能(實驗組)、未安裝(對照組),或未安裝但將技能核心要求濃縮成一句話附加在提示詞後(一句話提示組),每個任務每組執行 2 次,網路關閉,/tmp 寫入封鎖。提示詞從未提及技能(以下註明的措辭除外)。
一個聊天輸入元件(Composer.tsx),帶著一堆布林屬性。任務:將其重構成保留行為的彈性 API、在不增加布林屬性的情況下加入「快速回覆」單行輸入元件,以及審查該元件的 API(不編輯)。提示詞沒有指名任何模式。一句話提示則指名這些模式(從複合部件建立明確變體、以狀態/動作共享 context、用 children 取代 renderX、將狀態提升到 provider)。
- 以組合取代布林屬性 — Composer.tsx 已經累積了一堆布林屬性。將它重構成不需要這些屬性的彈性元件 API。保留在單一檔案 Composer.tsx 中,並保持行為不變。
- 在不增加屬性的情況下加入變體 — 加入一個「快速回覆」輸入元件:單行輸入,按 Enter 送出,沒有附件或表情符號按鈕。不要為 Composer 增加更多布林屬性。編輯 Composer.tsx。
- 審查元件 API — 審查 Composer.tsx 的元件 API:它有什麼問題,你會怎麼改?不要變更任何檔案。
評分方式為程式化檢查(隱藏測試/植入問題召回/限制條件檢查/視情況進行渲染與 OCR),每次執行 0–5 分。沒有任何 LLM 評審決定分數。
結果
提供給第三組的一句話提示:「避免布林屬性氾濫:將模式拆成由複合部件建立的明確變體元件,這些部件共享一個帶有通用狀態/動作介面的 context,使用 children 而非 renderX 屬性,並將狀態提升到 provider。」
| 任務 | 對照組分數 | 一句話提示 | 使用技能 | 對照組 Token 數 | 一句話提示 Token 數 | 使用技能 Token 數 | 技能讀取 |
|---|---|---|---|---|---|---|---|
| t1-refactor | 3.0 | 4.5 | 4.0 | 126k | 127k | 144k | 2/2 |
| t2-add-variant | 4.38 | 5.0 | 5.0 | 99k | 106k | 100k | 2/2 |
| t3-review | 3.75 | 4.58 | 4.17 | 81k | 80k | 70k | 2/2 |
平均分數:未使用技能 3.71,使用技能 4.39(差異 +0.68)。每次執行 Token 數:103k 對 105k(1.03 倍);時間 53 秒對 49 秒。一句話提示組:4.7(技能相較一句話提示的增益:-0.31;一句話提示相較無技能的增益:+0.99);一句話提示每次執行 105k Token,因此技能的成本是一句話提示的 1.00 倍。
實際發生了什麼
- 對照組 3.71、技能組 4.39、一句話提示組 4.70(各 n=6)。在沒有指引的情況下,對照組的重構確實使用了複合部件,但兩次執行的共享 context 都沒有提供狀態/動作介面,而兩次快速回覆執行中,新的輸入元件都沒有按 Enter 送出。
- 技能的重構具有包含狀態與動作的 context(2/2),其快速回覆輸入元件也會按 Enter 送出(2/2)。但兩次實驗組重構與對照組一樣,都沒有建立明確的變體元件(一句話提示組 2/2 有),而在審查任務中,兩次實驗組與兩次對照組都漏掉「布林屬性氾濫」這一點(檢查模式偏弱:審查者可能用不同措辭表達)。
- 技能、一句話提示與對照組的 Token 數相同(105k、105k、103k):技能在 6/6 次實驗組執行中被讀取且未被指名,也沒有額外成本。
- 實驗組會將規則資料夾複製到專案中(每次執行 14.7 個新檔案)。
各面向分數
| 面向 | 分數 | 原因 |
|---|---|---|
| 1_輸出品質 | 4.39 | 使用技能時平均分數 4.39/5(程式化檢查,見各任務 表格)。 |
| 2_相較無技能的提升 | 2.68 | 相較未使用技能的 3.71,+0.68 分(每項任務 n=2;約 1 分以下 屬於雜訊)。 |
| 3_觸發 | 5.0 | 技能檔案在 6/6 次實驗組執行中被讀取。 |
| 4_指令可攜性 | 4 | SKILL.md 加上 AGENTS.md 索引與規則資料夾;僅有文件。 |
| 5_效率 | 5 | 1.03 倍 Token(每次執行 105k 對 103k)與 0.93 倍時間(49 秒對 53 秒)。 |
| 6_安全性與衛生 | 4 | 僅有文字;見 security.md。 |
這項測試的限制
- 以規則檢查程式碼文字(context/provider 模式、變體元件、key 屬性);TypeScript 並未編譯或渲染(沙箱中沒有安裝 React)。
- 審查任務的其中一項檢查偏弱:它尋找字面上的「boolean prop」措辭,而審查者可能用不同方式表達。
- 單一元件 fixture;每個儲存格 n=2。
- 每項任務每組 n = 2:約 1 分以下的差異屬於雜訊。
- 規則式評分只能衡量檢查看得到的部分;無法評判品味,也無法判斷結果是否為最佳可能。
安全性
見 security.md。掃描:CRITICAL 0 / HIGH 0 / MEDIUM 0。判定:可安全執行(網路關閉的沙箱)。乾淨的掃描結果不能證明技能安全。
總結
一份還不錯的模式指南:有技能時複合元件結構可靠地出現,沒有技能時則很少出現,而且不花成本。一句話指名這些模式在這裡表現更好,因此技能的效益在於便利與廣度,而不是品質上限。
完整紀錄
執行日誌:composition-patterns (Codex CLI, network off, per-run temp HOME/TMPDIR, one row per graded run.) 欄位:任務、組-輪次、得分(0-5)、token、秒、通過的檢查、代理開啟的 skill 檔案。 t1-refactor control-1: 得分 3.0, token 130032, 秒 72.5, 檢查 3/5, 未開啟 t1-refactor control-2: 得分 3.0, token 123317, 秒 64.3, 檢查 3/5, 未開啟 t1-refactor oneliner-1: 得分 4.0, token 127545, 秒 101.8, 檢查 4/5, 未開啟 t1-refactor oneliner-2: 得分 5.0, token 127024, 秒 89.6, 檢查 5/5, 未開啟 t1-refactor treatment-1: 得分 4.0, token 158276, 秒 67.7, 檢查 4/5, 開啟 composition-patterns t1-refactor treatment-2: 得分 4.0, token 130234, 秒 59.2, 檢查 4/5, 開啟 composition-patterns t2-add-variant control-1: 得分 4.38, token 99946, 秒 40.3, 檢查 3/4, 未開啟 t2-add-variant control-2: 得分 4.38, token 100013, 秒 39.2, 檢查 3/4, 未開啟 t2-add-variant oneliner-1: 得分 5.0, token 106946, 秒 70.9, 檢查 4/4, 未開啟
18 次執行、3 個產出檔案
範例產出
SOTA · 装了 skill:Composer.tsx(t1-refactor,第 1 次,Codex)
import React, { createContext, use, useState } from "react"; type ComposerProps = { initialText?: string; onSubmit: (text: string) => void; children?: React.ReactNode; }; type ComposerContextValue = { state: { text: string }; actions: { setText: (text: string) => void; submit: () => void; }; }; const ComposerContext = createContext<ComposerContextValue | null>(null); export function useComposer() { const context = use(ComposerContext); if (!context) { throw new Error("Composer components must be inside Composer.Provider or Composer."); } return context; } function ComposerProvider({ initialText = "", onSubmit, children }: ComposerProps) { const [text, setText] = useState(initialText); function submit() { onSubmit(text); setText(""); } return ( <ComposerContext value={{ state: { text }, actions: { setText, submit } }}> {children} </ComposerContext> ); } function ComposerFrame({ children }: { children: React.ReactNode }) { const { actions } = useComposer(); return ( <form onSubmit={(event) => { event.preventDefault(); actions.submit(); }}> {children} </form> ); } function ComposerInput({ placeholder = "Message" }: { placeholder?: string }) { const { state, actions } = useComposer(); return ( <textarea value={state.text} onChange={(event) => actions.setText(event.target.value)} placeholder={placeholder} /> ); } function ComposerFormatting() { return ( <div className="toolbar"> <button type="button">B</button> <button type="button">I</button> </div> ); } function ComposerAttachments() { return <button type="button">Attach</button>; } function ComposerEmojis() { return <button type="button">😀</button>; } function ComposerSubmit({ children = "Send" }: { children?: React.ReactNode }) { return <button type="submit">{children}</button>; } function ComposerRoot({ children, ...props }: ComposerProps) { return ( <ComposerProvider {...props}> <ComposerFrame> {children === undefined ? <><ComposerInput /><ComposerSubmit /></> : children} </ComposerFrame> </ComposerProvider> ); } /** * Compose controls in their display order; footer content is ordinary children: * * <Composer initialText="Draft" onSubmit={send}> * <Composer.Formatting /> * <Composer.Input placeholder="Reply in thread" /> * <Composer.Attachments /> * <Composer.Emojis /> * <Composer.Submit /> * <CustomFooter /> * </Composer> * * Message: use the default input placeholder and submit label. * Thread: use "Reply in thread" and "Send". * Forward: use "Add a note" and "Forward". * Edit: use "Edit message" and "Save", and omit Attachments. * When migrating overlapping modes, edit takes precedence over forward, then thread. * Include Formatting, Attachments, and Emojis only where needed. * Provider and Frame can also be composed separately to share state with siblings. */ export const Composer = Object.assign(ComposerRoot, { Provider: ComposerProvider, Frame: ComposerFrame, Input: ComposerInput, Formatting: ComposerFormatting, Attachments: ComposerAttachments, Emojis: [... excerpt: first 3200 of 3247 characters]