Skill 的版本管理與迭代

從語意化版本、迭代流程到 A/B 測試與灰度發布,理解如何讓 Skill 像軟體一樣持續進化。

Skill 的版本管理與迭代

上一篇我們談了 Skill 的組合與編排,讓多個 Skill 能協同完成複雜任務。

但隨著系統演進,你會遇到一個現實問題:Skill 會改變

你今天優化了 research_topic 的 Prompt,明天可能調整 code_review 的評分邏輯。
當 Skill 越改越多,你需要一套機制來確保:改進不會帶來退步,新版本不會破壞舊功能

這一篇,我們要談 Skill 的版本管理與迭代:如何為 Skill 建立版本控制、如何設計迭代流程、如何進行 A/B 測試,以及如何管理多個版本在生產環境中共存。

Version Control + Iteration + A/B Testing = Reliable Skills

一、為什麼 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修正錯字、微調參數、修復 bug1.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
2Skill 的結構指令、工具、資源、範例、漸進式揭露
3設計一個可重用的 Skill以程式碼審查為例,從需求到實作
4Skill 的組合與編排順序、協調者、平行、條件、迭代、階層
5Skill 的版本管理與迭代版本控制、A/B 測試、灰度發布

下一篇預告

《Skill 的權限與安全邊界》

我們會談如何為 Skill 設計權限控制、如何隔離危險操作、如何審核高風險行為,以及如何在多 Skill 系統中維持安全性。