Skill 的版本管理與迭代
從語意化版本、迭代流程到 A/B 測試與灰度發布,理解如何讓 Skill 像軟體一樣持續進化。
Skill 的版本管理與迭代
上一篇我們談了 Skill 的組合與編排,讓多個 Skill 能協同完成複雜任務。
但隨著系統演進,你會遇到一個現實問題:Skill 會改變。
你今天優化了 research_topic 的 Prompt,明天可能調整 code_review 的評分邏輯。
當 Skill 越改越多,你需要一套機制來確保:改進不會帶來退步,新版本不會破壞舊功能。
這一篇,我們要談 Skill 的版本管理與迭代:如何為 Skill 建立版本控制、如何設計迭代流程、如何進行 A/B 測試,以及如何管理多個版本在生產環境中共存。
一、為什麼 Skill 需要版本管理?
問題一:Prompt 是程式碼,但沒有版控
大多數團隊把 Prompt 寫在程式碼裡,或是存在資料庫的某個欄位。
當有人修改了 Prompt,你很難知道:
- 這是誰改的?
- 這什麼時候改的?
- 改了什麼?
- 為什麼要改?
- 改完之後變好還是變差?
# 不好的例子:Prompt 散落在程式碼各處,沒有版控
def research(topic):
prompt = "請研究這個主題..." # 上週有人改過,但沒人知道
return call_llm(prompt)
問題二:改進可能帶來退步
你優化了 code_review 的 Prompt,讓它更擅長找出安全問題。
但你可能沒發現,它對風格問題的判斷變差了。
沒有系統性的測試,你根本不知道改進是全面的還是局部的。
問題三:多個版本需要共存
在生產環境中,你可能需要:
- A/B 測試:同時跑兩個版本,比較效果。
- 灰度發布:先讓 10% 的使用者用新版本。
- 回滾:新版本出問題時,快速切回舊版本。
沒有版本管理,這些都做不到。
問題四:不同場景需要不同版本
同一個 Skill,可能在不同場景需要不同行為:
- 客服場景:
research_topic要簡短、快速。 - 研究場景:
research_topic要深入、完整。
如果只有一個版本,你很難同時滿足兩者。
二、Skill 的版本號設計
語意化版本(Semantic Versioning)
我們可以借用軟體工程的語意化版本規則:
MAJOR.MINOR.PATCH
│ │ │
│ │ └── 修正錯誤,不改變行為
│ └──────── 新增功能,向後相容
└─────────────── 重大變更,不相容
對 Skill 來說:
| 版本變更 | 說明 | 範例 |
|---|---|---|
| MAJOR | 輸入輸出格式改變、流程重大調整 | 1.0.0 → 2.0.0 |
| MINOR | 新增步驟、改進 Prompt、新增工具 | 1.0.0 → 1.1.0 |
| PATCH | 修正錯字、微調參數、修復 bug | 1.0.0 → 1.0.1 |
版本 metadata
每個 Skill 版本都應該記錄完整的資訊:
@dataclass
class SkillVersion:
name: str
version: str
created_at: str
created_by: str
changelog: str
prompt_hash: str # Prompt 內容的雜湊值
input_schema: dict
output_schema: dict
performance: dict # 效能指標
status: str # draft, testing, active, deprecated
版本檔案結構
skills/
└── research_topic/
├── versions/
│ ├── 1.0.0/
│ │ ├── SKILL.md
│ │ ├── config.yaml
│ │ └── examples/
│ ├── 1.1.0/
│ │ ├── SKILL.md
│ │ ├── config.yaml
│ │ └── examples/
│ └── 2.0.0/
│ ├── SKILL.md
│ ├── config.yaml
│ └── examples/
├── current -> versions/1.1.0 # 符號連結
└── changelog.md
三、實作:Skill 版本管理器
讓我們實作一個版本管理系統。
import os
import json
import shutil
import hashlib
from datetime import datetime
from dataclasses import dataclass, field, asdict
@dataclass
class SkillVersion:
"""Skill 的版本資訊"""
name: str
version: str
created_at: str
created_by: str
changelog: str
prompt_hash: str
status: str = "draft" # draft, testing, active, deprecated
performance: dict = field(default_factory=dict)
def to_dict(self) -> dict:
return asdict(self)
class SkillVersionManager:
"""Skill 版本管理器"""
def __init__(self, skills_dir: str = "skills"):
self.skills_dir = skills_dir
def _skill_path(self, skill_name: str) -> str:
return os.path.join(self.skills_dir, skill_name)
def _version_path(self, skill_name: str, version: str) -> str:
return os.path.join(self._skill_path(skill_name), "versions", version)
def _metadata_path(self, skill_name: str, version: str) -> str:
return os.path.join(self._version_path(skill_name, version), "metadata.json")
# ========== 建立版本 ==========
def create_version(
self,
skill_name: str,
version: str,
content: dict,
changelog: str,
created_by: str = "system",
) -> SkillVersion:
"""建立一個新的 Skill 版本"""
version_path = self._version_path(skill_name, version)
if os.path.exists(version_path):
raise ValueError(f"版本 {version} 已存在")
os.makedirs(version_path, exist_ok=True)
# 寫入 SKILL.md
with open(os.path.join(version_path, "SKILL.md"), "w", encoding="utf-8") as f:
f.write(content.get("instructions", ""))
# 寫入 config.yaml
with open(os.path.join(version_path, "config.yaml"), "w", encoding="utf-8") as f:
import yaml
yaml.dump(content.get("config", {}), f, allow_unicode=True)
# 寫入 examples
examples_dir = os.path.join(version_path, "examples")
os.makedirs(examples_dir, exist_ok=True)
for i, example in enumerate(content.get("examples", [])):
with open(os.path.join(examples_dir, f"example_{i}_input.json"), "w") as f:
json.dump(example.get("input", {}), f, ensure_ascii=False)
with open(os.path.join(examples_dir, f"example_{i}_output.json"), "w") as f:
json.dump(example.get("output", {}), f, ensure_ascii=False)
# 計算 Prompt 雜湊
prompt_hash = hashlib.sha256(
content.get("instructions", "").encode()
).hexdigest()[:12]
# 建立 metadata
metadata = SkillVersion(
name=skill_name,
version=version,
created_at=datetime.now().isoformat(),
created_by=created_by,
changelog=changelog,
prompt_hash=prompt_hash,
)
with open(self._metadata_path(skill_name, version), "w") as f:
json.dump(metadata.to_dict(), f, ensure_ascii=False, indent=2)
return metadata
# ========== 讀取版本 ==========
def get_version(self, skill_name: str, version: str) -> SkillVersion:
"""讀取特定版本的 metadata"""
path = self._metadata_path(skill_name, version)
if not os.path.exists(path):
raise ValueError(f"版本 {version} 不存在")
with open(path) as f:
data = json.load(f)
return SkillVersion(**data)
def list_versions(self, skill_name: str) -> list:
"""列出所有版本"""
versions_dir = os.path.join(self._skill_path(skill_name), "versions")
if not os.path.exists(versions_dir):
return []
versions = []
for v in sorted(os.listdir(versions_dir)):
try:
versions.append(self.get_version(skill_name, v))
except Exception:
continue
return versions
def get_active_version(self, skill_name: str) -> str:
"""取得當前啟用的版本"""
link_path = os.path.join(self._skill_path(skill_name), "current")
if os.path.islink(link_path):
return os.path.basename(os.readlink(link_path))
return None
def get_skill_content(self, skill_name: str, version: str) -> dict:
"""取得特定版本的完整內容"""
version_path = self._version_path(skill_name, version)
content = {}
# 讀取 SKILL.md
skill_md = os.path.join(version_path, "SKILL.md")
if os.path.exists(skill_md):
with open(skill_md, encoding="utf-8") as f:
content["instructions"] = f.read()
# 讀取 config.yaml
config_path = os.path.join(version_path, "config.yaml")
if os.path.exists(config_path):
import yaml
with open(config_path, encoding="utf-8") as f:
content["config"] = yaml.safe_load(f) or {}
# 讀取 examples
examples = []
examples_dir = os.path.join(version_path, "examples")
if os.path.exists(examples_dir):
for filename in sorted(os.listdir(examples_dir)):
if filename.endswith("_input.json"):
input_path = os.path.join(examples_dir, filename)
output_path = input_path.replace("_input.json", "_output.json")
if os.path.exists(output_path):
with open(input_path) as f:
inp = json.load(f)
with open(output_path) as f:
out = json.load(f)
examples.append({"input": inp, "output": out})
content["examples"] = examples
return content
# ========== 啟用版本 ==========
def activate_version(self, skill_name: str, version: str):
"""啟用特定版本"""
version_path = self._version_path(skill_name, version)
if not os.path.exists(version_path):
raise ValueError(f"版本 {version} 不存在")
link_path = os.path.join(self._skill_path(skill_name), "current")
# 移除舊的符號連結
if os.path.islink(link_path):
os.unlink(link_path)
# 建立新的符號連結
os.symlink(version_path, link_path)
# 更新 metadata 狀態
metadata = self.get_version(skill_name, version)
metadata.status = "active"
with open(self._metadata_path(skill_name, version), "w") as f:
json.dump(metadata.to_dict(), f, ensure_ascii=False, indent=2)
# ========== 比較版本 ==========
def diff_versions(self, skill_name: str, v1: str, v2: str) -> dict:
"""比較兩個版本的差異"""
content1 = self.get_skill_content(skill_name, v1)
content2 = self.get_skill_content(skill_name, v2)
diff = {
"version_1": v1,
"version_2": v2,
"instructions_changed": content1.get("instructions") != content2.get("instructions"),
"config_changed": content1.get("config") != content2.get("config"),
"examples_count": {
v1: len(content1.get("examples", [])),
v2: len(content2.get("examples", [])),
},
}
# 計算 Prompt 相似度
import difflib
instructions1 = content1.get("instructions", "")
instructions2 = content2.get("instructions", "")
similarity = difflib.SequenceMatcher(
None, instructions1, instructions2
).ratio()
diff["similarity"] = round(similarity, 3)
return diff
# ========== 回滾 ==========
def rollback(self, skill_name: str, target_version: str):
"""回滾到特定版本"""
self.activate_version(skill_name, target_version)
# 更新 metadata
versions = self.list_versions(skill_name)
for v in versions:
if v.version == target_version:
v.status = "active"
elif v.status == "active":
v.status = "deprecated"
with open(self._metadata_path(skill_name, v.version), "w") as f:
json.dump(v.to_dict(), f, ensure_ascii=False, indent=2)
return {"rolled_back_to": target_version}
使用範例
manager = SkillVersionManager("skills")
# 建立新版本
manager.create_version(
skill_name="research_topic",
version="1.0.0",
content={
"instructions": "請研究給定主題並產出摘要...",
"config": {"tools": ["search_web", "read_webpage"]},
"examples": [
{"input": {"topic": "AI"}, "output": {"summary": "..."}},
],
},
changelog="初始版本",
created_by="alice",
)
# 建立改進版本
manager.create_version(
skill_name="research_topic",
version="1.1.0",
content={
"instructions": "請研究給定主題並產出結構化摘要,包含交叉驗證...",
"config": {"tools": ["search_web", "read_webpage", "extract_entities"]},
"examples": [
{"input": {"topic": "AI"}, "output": {"summary": "..."}},
{"input": {"topic": "Blockchain"}, "output": {"summary": "..."}},
],
},
changelog="加入交叉驗證步驟與新範例",
created_by="bob",
)
# 啟用新版本
manager.activate_version("research_topic", "1.1.0")
# 比較版本
diff = manager.diff_versions("research_topic", "1.0.0", "1.1.0")
print(json.dumps(diff, indent=2, ensure_ascii=False))
# 回滾
manager.rollback("research_topic", "1.0.0")
四、Skill 的迭代流程
版本管理是基礎,但真正的價值在於如何系統性地迭代。
迭代流程的五個階段
1. 觀察(Observe)
↓ 收集失敗案例、使用者回饋、效能指標
2. 假設(Hypothesize)
↓ 提出改進假設
3. 實驗(Experiment)
↓ 建立新版本,在測試集上評估
4. 驗證(Validate)
↓ A/B 測試,比較新舊版本
5. 發布(Deploy)
↓ 灰度發布,監控,必要時回滾
階段一:觀察
收集改進的訊號來源:
- 失敗案例:哪些輸入讓 Skill 表現不好?
- 使用者回饋:使用者抱怨什麼?
- 效能指標:成功率、延遲、成本有沒有異常?
- Trace 分析:哪些步驟最常出錯?
class SkillObserver:
"""收集 Skill 的表現資料"""
def __init__(self):
self.failures = []
self.feedback = []
self.metrics = []
def record_failure(self, skill_name: str, input_data: dict,
output: dict, expected: dict = None):
"""記錄失敗案例"""
self.failures.append({
"skill": skill_name,
"input": input_data,
"output": output,
"expected": expected,
"timestamp": datetime.now().isoformat(),
})
def record_feedback(self, skill_name: str, user_id: str,
rating: int, comment: str = ""):
"""記錄使用者回饋"""
self.feedback.append({
"skill": skill_name,
"user_id": user_id,
"rating": rating,
"comment": comment,
"timestamp": datetime.now().isoformat(),
})
def analyze_failures(self, skill_name: str) -> dict:
"""分析失敗模式"""
skill_failures = [
f for f in self.failures if f["skill"] == skill_name
]
# 簡單分類
categories = defaultdict(int)
for failure in skill_failures:
# 可以用 LLM 分類失敗原因
categories["unknown"] += 1
return {
"total_failures": len(skill_failures),
"categories": dict(categories),
"recent_failures": skill_failures[-10:],
}
階段二:假設
根據觀察結果,提出改進假設:
@dataclass
class ImprovementHypothesis:
skill_name: str
current_version: str
problem: str # 觀察到的問題
hypothesis: str # 改進假設
proposed_change: str # 具體的修改
expected_impact: str # 預期影響
success_criteria: dict # 成功標準
例如:
hypothesis = ImprovementHypothesis(
skill_name="research_topic",
current_version="1.0.0",
problem="研究結果缺乏交叉驗證,容易包含錯誤資訊",
hypothesis="加入交叉驗證步驟,要求至少兩個來源一致才納入結論",
proposed_change="在執行流程中加入步驟 6:交叉驗證事實",
expected_impact="提升準確率,但可能增加執行時間",
success_criteria={
"accuracy": ">= 0.9",
"latency": "<= 10 秒",
},
)
階段三:實驗
建立新版本,在測試集上評估。
class SkillExperiment:
"""Skill 實驗:比較不同版本"""
def __init__(self, version_manager: SkillVersionManager):
self.manager = version_manager
def run_experiment(
self,
skill_name: str,
version: str,
test_cases: list,
evaluator,
) -> dict:
"""在測試集上評估特定版本"""
content = self.manager.get_skill_content(skill_name, version)
skill = self._build_skill(skill_name, content)
results = []
for case in test_cases:
try:
output = skill.execute(**case["input"])
score = evaluator(output, case.get("expected"))
results.append({
"input": case["input"],
"output": output,
"score": score,
"success": True,
})
except Exception as e:
results.append({
"input": case["input"],
"error": str(e),
"score": 0.0,
"success": False,
})
# 計算統計
scores = [r["score"] for r in results]
success_rate = sum(1 for r in results if r["success"]) / len(results)
return {
"version": version,
"total_cases": len(results),
"success_rate": round(success_rate, 3),
"avg_score": round(sum(scores) / len(scores), 3),
"min_score": round(min(scores), 3),
"max_score": round(max(scores), 3),
"details": results,
}
def _build_skill(self, skill_name: str, content: dict):
"""根據版本內容建立 Skill 實例"""
# 實作略:根據 content 動態建立 Skill
pass
def compare_versions(
self,
skill_name: str,
v1: str,
v2: str,
test_cases: list,
evaluator,
) -> dict:
"""比較兩個版本"""
result1 = self.run_experiment(skill_name, v1, test_cases, evaluator)
result2 = self.run_experiment(skill_name, v2, test_cases, evaluator)
# 逐案例比較
wins = 0
losses = 0
ties = 0
for r1, r2 in zip(result1["details"], result2["details"]):
if r2["score"] > r1["score"]:
wins += 1
elif r2["score"] < r1["score"]:
losses += 1
else:
ties += 1
return {
"version_1": v1,
"version_2": v2,
"v1_avg_score": result1["avg_score"],
"v2_avg_score": result2["avg_score"],
"v1_success_rate": result1["success_rate"],
"v2_success_rate": result2["success_rate"],
"wins": wins,
"losses": losses,
"ties": ties,
"winner": v2 if result2["avg_score"] > result1["avg_score"] else v1,
}
階段四:驗證
在生產環境中進行 A/B 測試。
import random
class ABTestRouter:
"""A/B 測試路由器:根據流量比例分配版本"""
def __init__(self):
self.experiments = {}
def create_experiment(
self,
skill_name: str,
control_version: str,
treatment_version: str,
traffic_split: float = 0.1,
min_samples: int = 100,
):
"""建立 A/B 測試"""
self.experiments[skill_name] = {
"control": control_version,
"treatment": treatment_version,
"traffic_split": traffic_split,
"min_samples": min_samples,
"results": {
"control": [],
"treatment": [],
},
}
def route(self, skill_name: str, user_id: str) -> str:
"""決定使用哪個版本"""
if skill_name not in self.experiments:
return None
exp = self.experiments[skill_name]
# 根據 user_id 雜湊,確保同一使用者 consistently 使用同一版本
hash_val = hash(user_id) % 100 / 100.0
if hash_val < exp["traffic_split"]:
return exp["treatment"]
return exp["control"]
def record_result(self, skill_name: str, group: str,
score: float, metadata: dict = None):
"""記錄實驗結果"""
if skill_name not in self.experiments:
return
self.experiments[skill_name]["results"][group].append({
"score": score,
"metadata": metadata or {},
"timestamp": datetime.now().isoformat(),
})
def analyze(self, skill_name: str) -> dict:
"""分析實驗結果"""
if skill_name not in self.experiments:
return {}
exp = self.experiments[skill_name]
control = exp["results"]["control"]
treatment = exp["results"]["treatment"]
if not control or not treatment:
return {"status": "insufficient_data"}
control_scores = [r["score"] for r in control]
treatment_scores = [r["score"] for r in treatment]
control_avg = sum(control_scores) / len(control_scores)
treatment_avg = sum(treatment_scores) / len(treatment_scores)
# 簡單的顯著性判斷
improvement = (treatment_avg - control_avg) / control_avg
return {
"status": "complete" if (
len(control) >= exp["min_samples"] and
len(treatment) >= exp["min_samples"]
) else "running",
"control": {
"samples": len(control),
"avg_score": round(control_avg, 3),
},
"treatment": {
"samples": len(treatment),
"avg_score": round(treatment_avg, 3),
},
"improvement": round(improvement, 3),
"winner": "treatment" if improvement > 0.05 else "control",
}
階段五:發布
灰度發布與監控。
class GradualRollout:
"""灰度發布管理器"""
def __init__(self, version_manager: SkillVersionManager):
self.manager = version_manager
self.rollouts = {}
def start_rollout(
self,
skill_name: str,
new_version: str,
stages: list = None,
):
"""開始灰度發布"""
stages = stages or [
{"percentage": 5, "duration_minutes": 30},
{"percentage": 20, "duration_minutes": 60},
{"percentage": 50, "duration_minutes": 120},
{"percentage": 100, "duration_minutes": 0},
]
self.rollouts[skill_name] = {
"new_version": new_version,
"old_version": self.manager.get_active_version(skill_name),
"stages": stages,
"current_stage": 0,
"started_at": datetime.now().isoformat(),
"status": "in_progress",
}
def get_current_percentage(self, skill_name: str) -> int:
"""取得當前灰度比例"""
if skill_name not in self.rollouts:
return 100
rollout = self.rollouts[skill_name]
stage = rollout["stages"][rollout["current_stage"]]
return stage["percentage"]
def should_use_new_version(self, skill_name: str, user_id: str) -> bool:
"""判斷是否使用新版本"""
percentage = self.get_current_percentage(skill_name)
hash_val = hash(user_id) % 100
return hash_val < percentage
def promote(self, skill_name: str):
"""推進到下一個階段"""
if skill_name not in self.rollouts:
return
rollout = self.rollouts[skill_name]
rollout["current_stage"] += 1
if rollout["current_stage"] >= len(rollout["stages"]):
# 完成發布
rollout["status"] = "completed"
self.manager.activate_version(
skill_name, rollout["new_version"]
)
def rollback(self, skill_name: str):
"""回滾"""
if skill_name not in self.rollouts:
return
rollout = self.rollouts[skill_name]
rollout["status"] = "rolled_back"
self.manager.activate_version(
skill_name, rollout["old_version"]
)
五、實作:完整的迭代流程
把上述所有元件整合起來。
class SkillIterationPipeline:
"""Skill 迭代流程管理器"""
def __init__(self, version_manager: SkillVersionManager):
self.version_manager = version_manager
self.observer = SkillObserver()
self.experiment = SkillExperiment(version_manager)
self.ab_router = ABTestRouter()
self.rollout = GradualRollout(version_manager)
def iterate(
self,
skill_name: str,
hypothesis: ImprovementHypothesis,
new_content: dict,
test_cases: list,
evaluator,
verbose: bool = True,
) -> dict:
"""執行完整的迭代流程"""
current_version = self.version_manager.get_active_version(skill_name)
new_version = self._bump_version(
current_version, hypothesis.proposed_change
)
if verbose:
print(f"[迭代] {skill_name} {current_version} → {new_version}")
# 1. 建立新版本
self.version_manager.create_version(
skill_name=skill_name,
version=new_version,
content=new_content,
changelog=hypothesis.proposed_change,
created_by="iteration_pipeline",
)
# 2. 在測試集上評估
if verbose:
print(f"[評估] 在 {len(test_cases)} 個測試案例上評估...")
comparison = self.experiment.compare_versions(
skill_name=skill_name,
v1=current_version,
v2=new_version,
test_cases=test_cases,
evaluator=evaluator,
)
if verbose:
print(f"[比較] 舊版分數:{comparison['v1_avg_score']}")
print(f"[比較] 新版分數:{comparison['v2_avg_score']}")
print(f"[比較] 勝負:{comparison['wins']}勝 "
f"{comparison['losses']}負 {comparison['ties']}平")
# 3. 判斷是否值得發布
if comparison["winner"] != new_version:
if verbose:
print(f"[結果] 新版本未通過評估,不發布")
return {
"status": "rejected",
"reason": "新版本表現不如舊版本",
"comparison": comparison,
}
# 4. 開始灰度發布
if verbose:
print(f"[發布] 開始灰度發布...")
self.rollout.start_rollout(skill_name, new_version)
return {
"status": "rolling_out",
"new_version": new_version,
"old_version": current_version,
"comparison": comparison,
"rollout_stages": self.rollout.rollouts[skill_name]["stages"],
}
def _bump_version(self, current: str, change_type: str) -> str:
"""根據變更類型遞增版本號"""
parts = current.split(".")
major, minor, patch = int(parts[0]), int(parts[1]), int(parts[2])
if "重大" in change_type or "不相容" in change_type:
return f"{major + 1}.0.0"
elif "新增" in change_type or "改進" in change_type:
return f"{major}.{minor + 1}.0"
else:
return f"{major}.{minor}.{patch + 1}"
使用範例
manager = SkillVersionManager("skills")
pipeline = SkillIterationPipeline(manager)
# 定義改進假設
hypothesis = ImprovementHypothesis(
skill_name="research_topic",
current_version="1.0.0",
problem="研究結果缺乏交叉驗證",
hypothesis="加入交叉驗證步驟可提升準確率",
proposed_change="新增交叉驗證步驟",
expected_impact="準確率提升",
success_criteria={"accuracy": ">= 0.9"},
)
# 定義新版本內容
new_content = {
"instructions": "請研究給定主題,並進行交叉驗證...",
"config": {"tools": ["search_web", "read_webpage", "extract_entities"]},
"examples": [...],
}
# 定義測試案例
test_cases = [
{
"input": {"topic": "AI Agent"},
"expected": {"summary": "..."},
},
# ...
]
# 定義評估函式
def evaluator(output, expected):
# 實作略
return 0.85
# 執行迭代
result = pipeline.iterate(
skill_name="research_topic",
hypothesis=hypothesis,
new_content=new_content,
test_cases=test_cases,
evaluator=evaluator,
)
print(json.dumps(result, indent=2, ensure_ascii=False))
六、版本管理的最佳實踐
1. Prompt 與程式碼分離
# 錯誤的例子:Prompt 寫在程式碼裡
def research(topic):
prompt = "請研究..."
return call_llm(prompt)
# 正確的例子:Prompt 存在版本化的檔案中
def research(topic):
prompt = load_skill_prompt("research_topic", version="1.1.0")
return call_llm(prompt)
2. 每次變更都有記錄
manager.create_version(
skill_name="research_topic",
version="1.2.0",
content=new_content,
changelog="""
- 新增交叉驗證步驟
- 改進來源篩選邏輯
- 修正範例中的錯誤
""",
created_by="alice",
)
3. 用資料驅動決策
不要憑感覺判斷新版本好不好。
用測試集、A/B 測試、生產指標來決定。
4. 灰度發布,不要一次全上
# 好的灰度策略
stages = [
{"percentage": 5, "duration_minutes": 30}, # 先 5%
{"percentage": 20, "duration_minutes": 60}, # 再 20%
{"percentage": 50, "duration_minutes": 120}, # 再 50%
{"percentage": 100, "duration_minutes": 0}, # 全量
]
5. 隨時可以回滾
# 一鍵回滾
manager.rollback("research_topic", "1.0.0")
6. 保留歷史版本
不要刪除舊版本。
它們是除錯、比較、回滾的基礎。
7. 自動化迭代流程
# CI/CD 整合
def on_skill_change(skill_name: str):
# 1. 跑測試
result = run_tests(skill_name)
if not result["passed"]:
return {"status": "failed", "reason": "測試未通過"}
# 2. 比較版本
comparison = compare_with_active(skill_name)
if comparison["winner"] != "new":
return {"status": "rejected", "reason": "新版本未勝出"}
# 3. 灰度發布
start_rollout(skill_name)
return {"status": "rolling_out"}
七、常見的迭代陷阱
1. 沒有基準線
沒有基準線,你無法知道新版本是變好還是變差。
解法:每次迭代都保留舊版本,並在相同測試集上比較。
2. 測試集太小或太偏
只用 5 個測試案例,無法代表真實使用情況。
解法:建立涵蓋正常、邊界、對抗案例的測試集,至少 50-100 個案例。
3. 只看平均分數
平均分數提升,但某些重要案例變差了。
解法:看分數分佈、看最差案例、看特定類別。
4. 一次改太多
一次改了 Prompt、工具、流程,無法知道哪個改動有效。
解法:一次只改一個變數,用 A/B 測試隔離影響。
5. 忽略成本與延遲
新版本準確率提升 5%,但成本增加 3 倍、延遲增加 10 倍。
解法:把成本與延遲納入評估指標。
6. 沒有監控生產環境
測試集表現好,不代表生產環境表現好。
解法:灰度發布 + 生產監控 + 快速回滾。
7. 版本爆炸
每個小改動都建立新版本,版本數量失控。
解法:明確定義版本號規則,PATCH 層級的改動可以合併。
八、總結:讓 Skill 持續進化
讓我們回顧這一篇的核心:
- 為什麼需要版本管理:Prompt 是程式碼、改進可能退步、多版本需共存、不同場景需不同版本。
- 版本號設計:語意化版本
MAJOR.MINOR.PATCH。 - 版本管理器:建立、讀取、啟用、比較、回滾版本。
- 迭代流程:觀察 → 假設 → 實驗 → 驗證 → 發布。
- 觀察:收集失敗案例、使用者回饋、效能指標。
- 假設:提出改進假設與成功標準。
- 實驗:在測試集上評估新版本。
- 驗證:A/B 測試,比較新舊版本。
- 發布:灰度發布,監控,必要時回滾。
- 最佳實踐:Prompt 與程式碼分離、每次變更都有記錄、資料驅動決策、灰度發布、隨時可回滾、保留歷史版本、自動化迭代。
- 常見陷阱:沒有基準線、測試集太小、只看平均、一次改太多、忽略成本、沒有監控、版本爆炸。
版本管理與迭代,是 Skill 從「能用」到「好用」的關鍵。
沒有它們,Skill 會停在某個版本,無法持續改進。
有了它們,Skill 就能像軟體一樣,持續進化、越來越好。
到這裡,我們的 Agent Skills 系列已經涵蓋了從概念、結構、設計、組合到版本管理的完整脈絡。
你現在已經具備設計、實作、組合、迭代 Skill 的完整能力。
系列回顧
| 篇號 | 主題 | 核心概念 |
|---|---|---|
| 1 | 什麼是 Agent Skill? | 工具 vs Skill、為什麼需要 Skill |
| 2 | Skill 的結構 | 指令、工具、資源、範例、漸進式揭露 |
| 3 | 設計一個可重用的 Skill | 以程式碼審查為例,從需求到實作 |
| 4 | Skill 的組合與編排 | 順序、協調者、平行、條件、迭代、階層 |
| 5 | Skill 的版本管理與迭代 | 版本控制、A/B 測試、灰度發布 |
下一篇預告
《Skill 的權限與安全邊界》
我們會談如何為 Skill 設計權限控制、如何隔離危險操作、如何審核高風險行為,以及如何在多 Skill 系統中維持安全性。