SkillOpt 深度拆解:当微软决定「像训练神经网络一样训练 Skill」——从轨迹驱动优化到文本空间搜索,一个 3.1K Star 的框架如何用「不改权重只改文档」重新定义 Agent 技能优化的终极形态
引言:Agent 的"技能焦虑"
2026 年,AI Agent 已经从概念走向了生产。Claude Code、Codex、Cursor、DeerFlow……几乎每周都有新的 Agent 框架刷屏。但用过的人都知道一个残酷的事实:Agent 的表现极不稳定。
同一个 Agent,面对相似的任务,有时候完美解决,有时候犯低级错误。你花了一整个下午写的 System Prompt,第二天换个模型版本就废了。你精心设计的 Skills 文档,换个执行环境就失效了。
传统的改进思路是微调模型(Fine-tuning),但这有四个致命问题:
- 成本高昂:需要 GPU 集群、大量标注数据,一次微调可能烧掉几千美元
- 技术门槛高:得有机器学习专业知识,普通开发者根本搞不定
- 泛化能力差:在一个任务上训好了,换个任务可能就不行
- 更新麻烦:模型权重一变,整个部署和验证流程都得重来
就在整个社区头疼的时候,微软研究院丢出了一个革命性的项目——SkillOpt。它的核心假设极其大胆:智能体的能力主要取决于它的"技能文档",而不是模型本身。
不需要改权重,不需要 GPU,不需要标注数据。只需要优化一份几百到几千 token 的 Markdown 文件,就能让 Agent 的表现提升 20%+。
这篇文,我们深度拆解 SkillOpt 的每一个技术细节。
一、核心概念:技能即文档,训练即优化
1.1 什么是 Skill?
在 SkillOpt 的语境下,Skill 不是代码,不是权重,不是 API 调用。Skill 就是一份 Markdown 文档。
比如一个搜索问答 Skill 可能长这样:
# 搜索问答技能
## 任务描述
根据提供的文档内容回答问题。
## 工作流程
1. 仔细阅读文档,提取关键信息
2. 分析问题,确定需要哪些信息
3. 在文档中定位答案
4. 给出准确、简洁的回答
## 注意事项
- 如果文档中没有相关信息,明确说明
- 不要编造文档中没有的内容
- 保持回答简洁,不要过度展开
这个文档会被注入到 LLM 的上下文中,作为 Agent 执行任务的"操作手册"。文档的质量直接决定了 Agent 的表现。
1.2 传统方式 vs SkillOpt
传统方式改进 Agent 表现的路径:
写 Prompt → 测试 → 手动修改 → 再测试 → 再手动修改 → ...
这是一个人工循环,效率极低,且依赖人类的直觉和经验。
SkillOpt 的路径:
初始 Skill → 自动执行 → 自动分析错误 → 自动优化 Skill → 自动验证 → 循环
这是一个自动化的、系统化的、可复现的优化循环。就像训练神经网络一样,只不过"参数"从浮点数变成了文本。
1.3 训练概念映射
SkillOpt 精妙地将深度学习训练的概念映射到了文本优化领域:
| 神经网络训练 | SkillOpt 技能训练 |
|---|---|
| 调整权重参数 | 优化 Markdown 技能文档 |
| Epoch(轮次) | 多轮迭代优化技能 |
| Batch size(批次) | 每轮处理的任务数量 |
| Learning rate(学习率) | 技能更新的激进程度 |
| Validation(验证) | 在验证集上测试技能效果 |
| Loss function(损失函数) | 任务完成准确率 |
| Gradient descent(梯度下降) | LLM 驱动的文本编辑 |
| Checkpoint(检查点) | 技能文档快照 |
这个映射不是简单的类比——SkillOpt 真正实现了这些训练语义。它有 epoch 控制、有 batch 并行、有 validation gate、有 checkpoint 恢复。
二、架构分析:双模型协作的精妙设计
2.1 整体架构
SkillOpt 的核心架构是双模型协作:
┌─────────────────────────────────────────────┐
│ SkillOpt │
│ │
│ ┌──────────────┐ ┌──────────────────┐ │
│ │ 目标模型 │ │ 优化器模型 │ │
│ │ (Target) │ │ (Optimizer) │ │
│ │ │ │ │ │
│ │ 执行任务 │←──│ 分析错误 │ │
│ │ 记录轨迹 │──→│ 生成修改 │ │
│ │ 返回结果 │ │ 优化 Skill │ │
│ └──────────────┘ └──────────────────┘ │
│ ↑ ↑ │
│ │ │ │
│ ┌──────┴──────────────────────┴──────┐ │
│ │ Skill 文档 (Markdown) │ │
│ │ 可训练的"参数",纯文本 │ │
│ └────────────────────────────────────┘ │
└─────────────────────────────────────────────┘
目标模型(Target Model):实际执行任务的模型。它的权重完全不会被修改。可以是任何支持 API 调用的 LLM——GPT-4、Claude、通义千问、甚至本地部署的开源模型。
优化器模型(Optimizer Model):负责分析和改进 Skill 文档的模型。通常是一个更强的模型。它的任务是从执行结果中学习,提出对 Skill 文档的改进建议。
2.2 四步训练循环
SkillOpt 的训练由四个步骤组成一个完整循环:
Rollout → Reflect → Edit → Gate
↑ │
└────────────────────────┘
Step 1: Rollout(执行)
目标模型使用当前 Skill 文档,在训练集的任务上执行。记录完整的执行轨迹(包括每一步的输入、输出、工具调用)和最终得分。
# Rollout 阶段的核心逻辑
def rollout(skill_doc, tasks, target_model):
"""让目标模型用当前 Skill 执行一批任务"""
trajectories = []
for task in tasks:
# 将 Skill 文档注入系统提示
system_prompt = f"""你是一个AI助手。请按照以下技能文档完成任务:
{skill_doc}
---
当前任务:{task.description}"""
# 执行任务,记录完整轨迹
trajectory = target_model.execute(
system_prompt=system_prompt,
tools=task.available_tools,
max_steps=task.max_steps
)
# 评估结果
score = evaluate(trajectory, task.expected_result)
trajectories.append({
'task': task,
'trajectory': trajectory,
'score': score
})
return trajectories
Step 2: Reflect(反思)
优化器模型分析 Rollout 阶段的成功和失败案例,找出:
- 可复用的成功模式:哪些做法在多个任务上都有效
- 系统性失败原因:哪些错误反复出现
- Skill 文档的具体缺陷:文档哪里写得不清楚或有遗漏
# Reflect 阶段的核心逻辑
def reflect(trajectories, skill_doc, optimizer_model):
"""优化器分析执行轨迹,找出改进方向"""
# 分离成功和失败案例
successes = [t for t in trajectories if t['score'] >= threshold]
failures = [t for t in trajectories if t['score'] < threshold]
# 构造反思提示
reflect_prompt = f"""你是一个技能优化专家。请分析以下执行结果,找出 Skill 文档的改进方向。
当前 Skill 文档:
{skill_doc}
成功案例({len(successes)}个):
{format_trajectories(successes)}
失败案例({len(failures)}个):
{format_trajectories(failures)}
请分析:
1. 成功案例中可复用的模式
2. 失败案例的根本原因
3. Skill 文档的具体改进建议"""
# 优化器生成反思结果
reflection = optimizer_model.generate(reflect_prompt)
return reflection
Step 3: Edit(编辑)
基于反思结果,优化器模型生成结构化的 Skill 文档修改。这些修改以 diff 的形式呈现:添加、删除、替换。
# Edit 阶段的核心逻辑
def edit(skill_doc, reflection, optimizer_model, learning_rate):
"""基于反思结果,生成 Skill 文档的修改"""
edit_prompt = f"""你是一个技能文档优化专家。请根据以下分析结果,生成 Skill 文档的修改。
当前 Skill 文档:
{skill_doc}
分析结果:
{reflection}
请生成结构化的修改(以 diff 格式):
- 保留仍然有效的部分
- 添加缺失的关键信息
- 删除冗余或误导性的内容
- 改进表述不清的部分
注意:控制修改幅度,不要过度改动。学习率:{learning_rate}"""
# 优化器生成修改建议
proposed_edit = optimizer_model.generate(edit_prompt)
# 应用修改
new_skill_doc = apply_edit(skill_doc, proposed_edit)
return new_skill_doc
Step 4: Gate(验证)
候选 Skill 只有在验证集上性能严格提升时才会被接受。这是一个硬性门槛,防止优化器"过拟合"。
# Gate 阶段的核心逻辑
def gate(new_skill_doc, current_skill_doc, val_tasks, target_model):
"""在验证集上验证新 Skill 是否严格优于旧 Skill"""
# 在验证集上测试新 Skill
new_scores = evaluate_skill(new_skill_doc, val_tasks, target_model)
# 在验证集上测试旧 Skill(作为基线)
old_scores = evaluate_skill(current_skill_doc, val_tasks, target_model)
# 计算平均分
new_avg = sum(new_scores) / len(new_scores)
old_avg = sum(old_scores) / len(old_scores)
# 严格提升门控
if new_avg > old_avg:
return True, new_skill_doc, new_avg
else:
return False, current_skill_doc, old_avg
2.3 文本学习率(Text Learning Rate)
这是 SkillOpt 最精妙的设计之一。在神经网络中,learning rate 控制参数更新的步长。在 SkillOpt 中,文本学习率控制 Skill 文档每次修改的幅度。
# 文本学习率的实现
class TextLearningRate:
"""控制 Skill 文档每次修改的 token 预算"""
def __init__(self, base_lr=0.1, decay=0.95):
self.base_lr = base_lr
self.decay = decay
self.current_lr = base_lr
def get_budget(self, skill_doc_length):
"""计算本次修改的 token 预算"""
# 基础预算 = 文档长度 × 学习率
budget = int(skill_doc_length * self.current_lr)
# 至少允许修改 50 个 token
return max(budget, 50)
def step(self):
"""每轮衰减学习率"""
self.current_lr *= self.decay
def apply_constraint(self, edit, budget):
"""约束编辑操作在预算内"""
# 按优先级排序修改
edits = sorted(edit.modifications, key=lambda x: x.impact, reverse=True)
# 贪心选择,直到用完预算
selected = []
tokens_used = 0
for e in edits:
if tokens_used + e.token_cost <= budget:
selected.append(e)
tokens_used += e.token_cost
return selected
为什么要衰减?和神经网络训练一样,初期需要大步探索,后期需要小步精调。过大的修改幅度会导致 Skill 文档在震荡中退化。
三、代码实战:从零搭建 SkillOpt 训练流程
3.1 环境安装
# 克隆仓库
git clone https://github.com/microsoft/SkillOpt.git
cd SkillOpt
# 安装核心依赖
pip install -e .
# 如果需要 ALFWorld 基准测试(具身智能)
pip install -e ".[alfworld]"
alfworld-download
# 如果需要 WebUI 监控面板
pip install -e ".[webui]"
3.2 配置 API 凭证
SkillOpt 支持多种 LLM 提供商:
# Azure OpenAI(推荐,性能最稳定)
export AZURE_OPENAI_ENDPOINT="https://your-resource.openai.azure.com/"
export AZURE_OPENAI_API_KEY="your-key"
# OpenAI 直接调用
export OPENAI_API_KEY="sk-..."
# Anthropic Claude
export ANTHROPIC_API_KEY="sk-ant-..."
# 通义千问(本地 vLLM 部署)
export QWEN_CHAT_BASE_URL="http://localhost:8000/v1"
export QWEN_CHAT_MODEL="Qwen/Qwen3.5-4B"
3.3 数据准备
SkillOpt 期望数据按以下结构组织:
data/my_split/
├── train/items.json # 训练集
├── val/items.json # 验证集
└── test/items.json # 测试集
以 SearchQA 为例,每个 JSON 文件的格式:
[
{
"id": "item_001",
"question": "谁写了《百年孤独》这部小说?",
"context": "[DOC] 加夫列尔·加西亚·马尔克斯是哥伦比亚作家,1967年出版了《百年孤独》……",
"answers": ["加夫列尔·加西亚·马尔克斯", "加西亚·马尔克斯"]
},
{
"id": "item_002",
"question": "《百年孤独》的故事发生在哪个虚构的小镇?",
"context": "[DOC] 马孔多是《百年孤独》中虚构的小镇,象征着拉丁美洲的历史……",
"answers": ["马孔多"]
}
]
3.4 编写自定义 Skill 文档
在训练之前,你需要准备一个初始 Skill 文档。这是优化的起点:
# 搜索问答技能 v1.0
## 角色定义
你是一个专业的搜索问答助手。你的任务是根据提供的文档内容,准确回答用户的问题。
## 工作流程
### 第一步:理解问题
- 仔细阅读用户的问题
- 识别问题的核心意图
- 确定需要查找的信息类型
### 第二步:搜索文档
- 在提供的文档中逐段扫描
- 寻找与问题直接相关的关键词和句子
- 注意同义词和近义词匹配
### 第三步:提取答案
- 从匹配的段落中提取精确答案
- 如果有多个候选答案,选择最相关的
- 保持答案简洁,直接回答问题
### 第四步:验证和输出
- 检查答案是否直接来自文档
- 确认答案的完整性和准确性
- 以清晰的格式输出最终答案
## 边界条件处理
- 如果文档中没有相关信息,回答"根据提供的文档,无法找到相关信息"
- 不要编造或推测文档中没有的内容
- 如果问题模糊,基于文档中最可能的解释回答
## 示例
**输入**:谁发明了电话?
**文档**:[DOC] 亚历山大·格拉汉姆·贝尔于1876年获得了电话的专利……
**输出**:亚历山大·格拉汉姆·贝尔
3.5 启动训练
# 在 SearchQA 上训练
python scripts/train.py \
--config configs/searchqa/default.yaml \
--split_dir data/searchqa_split \
--azure_openai_endpoint https://your-resource.openai.azure.com/ \
--optimizer_model gpt-5.5 \
--target_model gpt-5.5 \
--num_epochs 4 \
--batch_size 40 \
--workers 8 \
--out_root outputs/searchqa_run
3.6 训练过程监控
启动 WebUI 查看实时训练状态:
python -m skillopt_webui.app
# 或者创建公共分享链接
python -m skillopt_webui.app --share
WebUI 会展示:
- 每一步的训练指标(准确率、Loss)
- Skill 文档的版本演进(从 v1 到 vN)
- 每次修改的具体 diff
- 验证集上的表现曲线
3.7 评估已训练的 Skill
# 在测试集上评估
python scripts/eval_only.py \
--config configs/searchqa/default.yaml \
--skill outputs/searchqa_run/best_skill.md \
--split test \
--split_dir data/searchqa_split \
--azure_openai_endpoint https://your-resource.openai.azure.com/
3.8 输出结构
每次训练会生成完整的输出目录:
outputs/searchqa_run/
├── config.json # 运行配置(扁平化)
├── history.json # 每步的训练历史
├── runtime_state.json # 恢复检查点(中断后可恢复)
├── best_skill.md # 最佳验证 Skill 文档 ★
├── skills/
│ ├── skill_v0001.md # 初始 Skill
│ ├── skill_v0002.md # 第1轮优化后
│ ├── skill_v0003.md # 第2轮优化后
│ └── ...
├── steps/
│ ├── step_0001/ # 每步的产物
│ │ ├── rollout.json # 执行轨迹
│ │ ├── reflection.json # 反思结果
│ │ ├── edit.json # 修改建议
│ │ └──