第 28 期 | 告别 Prompt 工程,步入 Skills 工程
🎯 学习目标
- 理解传统 Prompt Engineering 的局限性及其难以持续的根本原因。
- 掌握 Agent Skills 的核心概念,以及它如何实现模块化和可复用性。
- 深入解析 Agent Skills 中 YAML Frontmatter (元数据) 和 Instructions (指令) 的底层运行机制。
- 学会如何设计和实现高效的 Agent Skills,以应对复杂任务。
📖 核心概念讲解
28.1 为什么 Prompt 工程难以为继?
在大型语言模型 (LLM) 发展的早期阶段,"Prompt Engineering" 是一个炙手可热的领域。我们通过精心构造的文本指令(即 Prompt)来引导模型完成特定任务。然而,随着模型能力的增强和应用场景的复杂化,Prompt Engineering 的局限性日益凸显,使其成为一个难以规模化和维护的瓶颈:
可维护性差与脆性 (Brittleness):
- 单个 Prompt 通常是长而复杂的文本块,其中包含了任务描述、角色设定、约束条件、示例等。一旦任务需求发生微小变化,可能需要对整个 Prompt 进行大量修改,甚至导致模型行为的剧烈变化。
- 不同模型版本、不同上下文窗口大小、甚至微小的措辞调整都可能影响 Prompt 的效果,使得维护变得异常困难。
可复用性低:
- 每个 Prompt 都是针对特定任务定制的。即使两个任务有相似的子步骤,也很难直接复用 Prompt 中的某个片段。这意味着每当需要新功能时,都倾向于从头开始编写 Prompt。
可组合性差:
- 对于需要多步操作或结合多种工具的复杂任务,传统的 Prompt Engineering 往往需要将所有逻辑硬编码在一个巨大的 Prompt 中,或者通过外部编排逻辑进行多次 Prompt 调用。这使得任务流难以清晰表达,且容易超出模型的上下文窗口限制。
上下文窗口限制与成本:
- 随着任务复杂度的增加,Prompt 往往会变得越来越长,这不仅消耗更多的 Token 导致更高的成本,也更容易触及模型的上下文窗口上限,从而影响模型理解和表现。
调试与可解释性挑战:
- 当模型输出不符合预期时,很难准确判断是 Prompt 的哪个部分出了问题,或者模型在哪个环节理解偏差。庞大的文本块使得调试如同大海捞针。
人机协作效率低下:
- Prompt Engineering 本质上是人类通过自然语言与模型进行“对话”,试图让模型理解并执行任务。这种方式效率较低,且高度依赖工程师的经验和直觉。
简而言之,Prompt Engineering 是一种非结构化的、命令式的编程方式。它就像在没有函数、模块和类概念的语言中编写所有代码。当项目规模变大时,这种方式必然会走向崩溃。我们需要一种更结构化、模块化、可编程的方式来与 LLM 交互,这就是 Agent Skills 出现的原因。
28.2 Agent Skills: 模块化智能的基石
Agent Skills 是 Anthropic 推出的一种新的范式,旨在解决传统 Prompt Engineering 的诸多痛点,将 LLM 的能力从简单的指令执行者提升为智能代理 (Agent)。一个 Agent Skill 可以被理解为 Agent 所掌握的特定能力或工具,它封装了执行某个任务所需的所有知识和逻辑。
每个 Agent Skill 主要由两部分组成:
28.2.1 YAML Frontmatter (元数据)
YAML Frontmatter 是技能的声明性部分,它以结构化的数据格式(YAML)描述了技能的“是什么”以及“能做什么”。这部分信息是机器可读的,主要用于 Agent 的规划 (Planning) 阶段。
它通常包含以下关键信息:
name: 技能的唯一标识符。description: 技能的简短描述,清晰地说明了技能的用途和适用场景。这是 Agent 在决定是否使用该技能时的主要参考。parameters: 技能所需的输入参数列表,包括每个参数的名称、类型、描述以及是否为必需项。Agent 会根据用户请求从对话中提取这些参数。returns: 技能可能返回的数据类型和描述,帮助 Agent 理解技能的输出。dependencies(可选): 技能可能依赖的其他技能或工具。
示例结构:
# skill_name.yaml
name: get_github_repo_info
description: 获取指定 GitHub 仓库的详细信息,例如星标数、描述、最新版本等。
parameters:
repo_owner:
type: string
description: GitHub 仓库的所有者(例如:Anthropic)。
required: true
repo_name:
type: string
description: GitHub 仓库的名称(例如:claude-sdk-python)。
required: true
returns:
type: object
description: 包含仓库名称、描述、星标数、URL 和最新发布版本等信息的 JSON 对象。
28.2.2 Instructions (指令)
Instructions 是技能的指令性部分,通常以 Markdown 格式编写。它详细描述了技能“如何做”的执行逻辑。这部分信息是人类和 LLM 可读的,主要用于 Agent 的执行 (Execution) 阶段。
Instructions 通常包含:
- 详细的任务步骤:如何一步步地完成任务。
- 工具调用指南:明确指出需要调用哪些外部工具(Tools),以及如何构造工具调用的参数。这通常通过
tool_code或特定的语法糖来表示。 - 内部推理逻辑:在没有外部工具的情况下,Agent 应该如何进行内部推理、数据处理或生成内容。
- 错误处理:当工具调用失败或遇到异常情况时,Agent 应该如何响应。
- 输出格式要求:明确最终结果应该以何种格式呈现给用户。
示例结构:
# skill_name_instructions.md
当用户需要获取 GitHub 仓库的详细信息时,请使用此技能。
**步骤:**
1. 从用户输入中提取 `repo_owner` 和 `repo_name` 参数。
2. 使用 `get_github_repo_data` 工具来查询 GitHub API。
* **工具签名:** `get_github_repo_data(owner: str, repo: str)`
* **用途:** 调用 GitHub REST API 获取仓库数据。
* **示例:** `get_github_repo_data(owner="Anthropic", repo="claude-sdk-python")`
3. 如果工具调用成功,解析返回的 JSON 数据,并提取以下关键信息:
* `full_name`
* `description`
* `stargazers_count` (星标数)
* `html_url` (仓库链接)
* `latest_release_tag` (最新发布版本标签,如果存在)
4. 将提取的信息以简洁明了的方式呈现给用户,例如:
```
仓库名称:[full_name]
描述:[description]
星标数:[stargazers_count] ⭐
仓库链接:[html_url]
最新版本:[latest_release_tag]
```
5. 如果工具调用失败(例如,仓库不存在或 API 错误),请告知用户查询失败,并提示他们检查仓库名称是否正确。
概念图:Agent Skills 的双重角色
+---------------------+ +---------------------+
| 用户请求 / 目标 |<-----+ Agent (Claude) |
+---------------------+ | |
| | 1. 感知 (Perceive) |
v +----------^----------+
+---------------------+ |
| 所有可用 Skills | |
| | |
| [ Skill A ] | |
| - YAML Frontmatter| |
| (name, desc, | |
| params, ...) | |
| | |
| [ Skill B ] | |
| - YAML Frontmatter| |
| (name, desc, | |
| params, ...) | |
| | |
| ... | |
+---------------------+ |
| | 2. 规划 (Plan)
| (Agent 阅读 YAML descriptions)
v |
+-------------------------------------------------+
| Claude 内部决策:哪个 Skill 最适合当前任务? |
| - 匹配用户意图 |
| - 提取并绑定参数 |
| - 确定执行顺序 |
| -> 生成 tool_code / tool_use 意图 |
+-------------------------------------------------+
| |
| (Agent 根据规划,加载选定 Skill 的 Instructions)
v | 3. 执行 (Execute)
+-------------------------------------------------+
| Claude 执行选定 Skill 的 Instructions |
| - 调用外部工具 (根据 Instructions 中的 tool_code) |
| - 根据 Instructions 进行内部推理和数据处理 |
| - 生成响应或进一步的行动 |
+-------------------------------------------------+
| |
v | 4. 观察 (Observe)
+-------------------------------------------------+
| 工具输出 / 内部处理结果 |
+-------------------------------------------------+
| |
v | 5. 反思 / 完善 (Reflect / Refine)
+-------------------------------------------------+
| Claude 评估结果,决定是否完成任务或需要下一步行动 |
+-------------------------------------------------+
通过这种分离,Agent Skills 实现了:
- 规划与执行的分离:YAML 元数据用于高效的规划和工具选择,而详细指令用于精确的执行。
- 模块化和可复用性:每个 Skill 都是一个独立的功能单元,可以在不同的任务中被调用。
- 可维护性:修改一个 Skill 的内部逻辑不会影响其他 Skill,易于更新和调试。
- 可组合性:复杂任务可以通过组合多个 Skill 来完成,提高了 Agent 的能力边界。
28.3 Skills 的底层运行机制:规划与执行
现在我们更深入地探讨 Agent 是如何利用 Skills 的 YAML Frontmatter 和 Instructions 来工作的。这个过程可以被概括为一个迭代的“感知-规划-执行-观察-反思”循环。
28.3.1 规划阶段:基于 YAML Frontmatter 的决策
当 Agent 收到用户请求时,它首先进入规划 (Planning) 阶段。在这个阶段,Agent 会:
- 感知用户意图 (Perception): Agent 解析用户输入的自然语言,理解其核心目标和所需信息。
- 遍历可用 Skills 的 YAML Frontmatter: Agent 会查看其所有已注册 Skills 的
name和description字段。这些字段是高度概括性的,旨在帮助 Agent 快速判断哪个 Skill 可能与当前用户意图相关。- 例如,如果用户问“给我 Anthropic 的 GitHub 仓库信息”,Agent 会在所有 Skills 的
description中寻找能“获取 GitHub 仓库信息”的 Skill。
- 例如,如果用户问“给我 Anthropic 的 GitHub 仓库信息”,Agent 会在所有 Skills 的
- 参数提取与绑定: 一旦 Agent 识别出潜在的 Skill,它会进一步检查该 Skill 的
parameters定义。Agent 尝试从用户请求中提取所需参数的值,并将其与 Skill 的参数进行绑定。- 例如,对于
get_github_repo_infoSkill,Agent 会从“Anthropic 的 GitHub 仓库”中提取repo_owner="Anthropic"和repo_name(如果用户明确给出)。如果repo_name未明确给出,Agent 可能会尝试从上下文推断或请求用户提供。
- 例如,对于
- 生成工具使用意图 (Tool Use Intent): 基于匹配的 Skill 和提取的参数,Agent 内部会生成一个结构化的“工具使用意图”。这通常以
tool_code或类似的内部表示形式体现,指示 Agent 接下来需要调用哪个工具,以及带什么参数。- 例如,它可能生成
tool_code: get_github_repo_data(owner="Anthropic", repo="claude-sdk-python")。 - 关键点: 在这个阶段,Agent 还没有真正“读取”或“执行”Skill 的 Instructions 部分。它只依赖于 YAML 中的
name,description,parameters来进行高层决策。这大大提高了规划效率,避免了每次都加载并解析冗长的自然语言指令。
- 例如,它可能生成
28.3.2 执行阶段:遵循 Instructions 的具体操作
一旦 Agent 确定了要使用的 Skill 并生成了工具使用意图,它就进入执行 (Execution) 阶段。在这个阶段,Agent 会:
- 加载并理解 Skill 的 Instructions: Agent 会加载并深入理解所选 Skill 的完整 Instructions 内容。这些 Instructions 提供了详细的执行蓝图。
- 执行指令中的步骤:
- 工具调用: 如果 Instructions 中包含
tool_code或明确的工具调用指示,Agent 会根据这些指示实际调用外部工具(例如,通过 API 网关执行get_github_repo_data函数)。 - 内部推理与数据处理: Instructions 也可以指导 Agent 进行纯粹的内部思考、数据转换、条件判断等,而无需调用外部工具。例如,对工具返回的数据进行解析、过滤、格式化。
- 生成响应: 最终,Instructions 会指导 Agent 如何将处理后的信息组织成一个用户友好的响应。
- 工具调用: 如果 Instructions 中包含
- 观察工具输出 (Observation): Agent 接收并处理工具调用的返回结果或内部处理的中间状态。这些结果成为 Agent 下一步行动的输入。
- 反思与完善 (Reflection/Refinement): Agent 根据 Observations 评估任务是否完成。如果任务完成,则生成最终响应。如果需要进一步操作(例如,根据工具输出决定调用另一个 Skill,或者需要用户提供更多信息),Agent 会回到规划阶段,开始新的循环。
规划与执行的“对话”
Agent Skills 的核心在于,它将 LLM 的能力从一个被动的“Prompt 处理器”转变为一个主动的“问题解决者”。YAML Frontmatter 赋予 Agent 快速理解和选择工具的能力,而详细的 Instructions 则指导 Agent 精确地执行任务。这种分层设计,使得 Agent 能够更高效、更可靠地处理复杂多变的用户请求,真正实现“告别 Prompt 工程,步入 Skills 工程”。
💻 模拟 Claude TUI 交互
以下模拟展示了 Claude Agent 如何利用 get_github_repo_info Skill 来响应用户请求。
场景 1: 查询特定 GitHub 仓库信息
用户希望获取 Anthropic 的 Claude SDK Python 仓库信息。
> 请帮我查询 GitHub 上 'Anthropic/claude-sdk-python' 项目的最新版本号和星标数。
╭─ Claude ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ 正在分析您的请求,并匹配合适的技能... │
│ │
│ 🧐 **思考 (Planning):** │
│ 我检测到用户需要查询 GitHub 仓库的详细信息。我有一个名为 `get_github_repo_info` 的技能,其描述是 "获取指定 GitHub 仓库 │
│ 的详细信息,例如星标数、描述、最新版本等。" 这与用户的请求高度匹配。 │
│ │
│ **参数提取:** │
│ - `repo_owner`: "Anthropic" │
│ - `repo_name`: "claude-sdk-python" │
│ │
│ **行动 (Action):** │
│ 决定调用 `get_github_repo_info` 技能。 │
│ │
│ 🛠️ **调用工具 (Executing Skill 'get_github_repo_info'):** │
│ ```tool_code │
│ print(get_github_repo_data(owner="Anthropic", repo="claude-sdk-python")) │
│ ``` │
│ │
│ **工具输出 (Observation):** │
│ ```json │
│ { │
│ "name": "claude-sdk-python", │
│ "full_name": "Anthropic/claude-sdk-python", │
│ "description": "The official Python SDK for interacting with the Claude API.", │
│ "stargazers_count": 2540, │
│ "forks_count": 310, │
│ "html_url": "https://github.com/Anthropic/claude-sdk-python", │
│ "latest_release_tag": "v0.12.0" │
│ } │
│ ``` │
│ │
│ ✨ **整理与回应 (Reflection):** │
│ 工具已成功获取仓库信息。我将根据技能的 Instructions 格式化这些信息并回复用户。 │
│ │
│ Anthropic 的 GitHub 仓库 `Anthropic/claude-sdk-python` 的详细信息如下: │
│ 仓库名称:claude-sdk-python │
│ 描述:The official Python SDK for interacting with the Claude API. │
│ 星标数:2540 ⭐ │
│ 仓库链接:https://github.com/Anthropic/claude-sdk-python │
│ 最新版本:v0.12.0 │
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
场景 2: 查询不存在的 GitHub 仓库信息 (错误处理)
用户查询一个不存在的仓库,展示技能的错误处理机制。
> 查一下 GitHub 上 'NonExistentOrg/NonExistentRepo' 的信息。
╭─ Claude ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ 正在分析您的请求,并匹配合适的技能... │
│ │
│ 🧐 **思考 (Planning):** │
│ 用户请求查询 GitHub 仓库信息,`get_github_repo_info` 技能最匹配。 │
│ │
│ **参数提取:** │
│ - `repo_owner`: "NonExistentOrg" │
│ - `repo_name`: "NonExistentRepo" │
│ │
│ **行动 (Action):** │
│ 决定调用 `get_github_repo_info` 技能。 │
│ │
│ 🛠️ **调用工具 (Executing Skill 'get_github_repo_info'):** │
│ ```tool_code │
│ print(get_github_repo_data(owner="NonExistentOrg", repo="NonExistentRepo")) │
│ ``` │
│ │
│ **工具输出 (Observation):** │
│ ```error │
│ Error: GitHub API returned 404 Not Found. Repository 'NonExistentOrg/NonExistentRepo' not found. │
│ ``` │
│ │
│ ✨ **整理与回应 (Reflection):** │
│ 工具调用失败,根据技能的 Instructions,我需要告知用户查询失败。 │
│ │
│ 抱歉,我无法查询到 GitHub 仓库 'NonExistentOrg/NonExistentRepo' 的信息。 │
│ 请检查仓库所有者和仓库名称是否正确,或者该仓库可能不存在。 │
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
💻 代码演示
这里我们将演示一个完整的 Agent Skill 定义,包括 YAML Frontmatter 和 Instructions,以及一个模拟的 Python 工具函数,用于理解其工作原理。
1. Skill YAML Frontmatter (get_github_repo_info.yaml)
# skills/get_github_repo_info.yaml
name: get_github_repo_info
description: 获取指定 GitHub 仓库的详细信息,例如星标数、描述、最新版本等。
parameters:
repo_owner:
type: string
description: GitHub 仓库的所有者(例如:Anthropic)。
required: true
repo_name:
type: string
description: GitHub 仓库的名称(例如:claude-sdk-python)。
required: true
returns:
type: object
description: 包含仓库名称、描述、星标数、URL 和最新发布版本等信息的 JSON 对象。
2. Skill Instructions (get_github_repo_info_instructions.md)
# skills/get_github_repo_info_instructions.md
当用户需要获取 GitHub 仓库的详细信息时,请使用此技能。
**任务目标:**
获取用户指定的 GitHub 仓库的公开信息,并以结构化、用户友好的方式呈现。
**步骤:**
1. **参数校验与准备:**
* 确保 `repo_owner` 和 `repo_name` 参数已从用户请求中成功提取。
* 如果任一参数缺失,告知用户需要提供完整的仓库所有者和名称。
2. **工具调用:**
* 使用 `get_github_repo_data` 工具来查询 GitHub API。
* **工具签名:** `get_github_repo_data(owner: str, repo: str)`
* **用途:** 调用 GitHub REST API 获取指定仓库的详细数据。
* **示例调用 (内部表示):**
```tool_code
print(get_github_repo_data(owner=repo_owner, repo=repo_name))
```
* **注意:** Claude 会自动将 `repo_owner` 和 `repo_name` 的值替换为实际提取到的参数。
3. **结果处理与格式化:**
* **成功场景:**
* 如果 `get_github_repo_data` 工具返回一个 JSON 对象,表示查询成功。
* 从返回的 JSON 中提取以下关键信息:
* `full_name` (完整仓库名,例如 `Anthropic/claude-sdk-python`)
* `description` (仓库描述)
* `stargazers_count` (星标数)
* `forks_count` (Fork 数)
* `html_url` (仓库的 Web 链接)
* `latest_release_tag` (最新发布版本标签,如果工具能提供)
* 将提取的信息以以下格式呈现给用户:
```
GitHub 仓库信息:
- 完整名称:[full_name]
- 描述:[description]
- 星标数:[stargazers_count] ⭐
- Fork 数:[forks_count]
- 仓库链接:[html_url]
- 最新版本:[latest_release_tag]
```
* 如果 `latest_release_tag` 不存在,则在输出中省略该行。
* **失败场景:**
* 如果 `get_github_repo_data` 工具返回错误(例如,HTTP 404 Not Found),表示仓库不存在或查询失败。
* 向用户报告查询失败,并建议他们检查仓库所有者和名称是否正确。
* **示例错误信息:** "抱歉,我无法查询到 GitHub 仓库 '[repo_owner]/[repo_name]' 的信息。请检查仓库所有者和仓库名称是否正确,或者该仓库可能不存在。"
3. 模拟的 Python 工具函数 (tools.py)
这个 Python 函数模拟了 get_github_repo_data 工具,它会被 Agent 在执行 get_github_repo_info 技能时调用。在真实场景中,这会是一个与外部 API 交互的实际函数。
# tools.py
import requests
import json
def get_github_repo_data(owner: str, repo: str) -> str:
"""
模拟一个通过 GitHub API 获取仓库数据的工具。
在真实场景中,这会调用 requests.get 并处理认证。
"""
url = f"https://api.github.com/repos/{owner}/{repo}"
headers = {
"Accept": "application/vnd.github.v3+json",
# 在实际应用中,这里需要添加 GitHub Personal Access Token 进行认证
# "Authorization": "token YOUR_GITHUB_TOKEN"
}
try:
response = requests.get(url, headers=headers)
response.raise_for_status() # 如果是 4xx 或 5xx 状态码,抛出 HTTPError
data = response.json()
# 模拟获取最新 release tag
release_url = f"https://api.github.com/repos/{owner}/{repo}/releases/latest"
release_response = requests.get(release_url, headers=headers)
latest_release_tag = None
if