一、Skill 原理回顾(国内大模型视角)
1.1 Skill 的本质
Skill 是给 Agent 装备的"技能包",包含说明书(Markdown)、可执行脚本和依赖资源。Agent 通过三层加载机制使用 Skill:
- 启动时:扫描 Skill 目录,读取元信息(名称+简介)注入系统提示词
- 触发时:按需加载完整的 Skill 说明书到上下文
- 执行时:根据说明书调用外部脚本/工具执行任务
1.2 国内大模型的工具调用能力
关键理解 :大模型本身不需要理解工具的具体实现,只负责根据工具列表生成调用指令。
| 组件 | 职责 |
|---|---|
| 大模型 | 根据工具描述决定"是否调用"+"传什么参数" |
| 开发者代码 | 实际执行工具调用(解析指令、调用API、返回结果) |
| 工具定义 | 以 JSON Schema 形式描述工具名称、参数、功能 |
国内主流大模型(Qwen、DeepSeek、Kimi)均支持工具调用,且 API 接口兼容 OpenAI 格式。
二、环境准备
2.1 选择模型平台
本次 Demo 使用 阿里云通义千问 Qwen,原因:
- API 兼容 OpenAI 格式,调用简单
- 支持工具调用(Tool Calling)
- 国内直连,无需特殊网络设置
2.2 获取 API Key
- 访问阿里云百炼平台
- 开通"模型服务",创建 API-KEY
- 将 API Key 保存到环境变量:
bash
export DASHSCOPE_API_KEY="sk-xxxxxxxxxxxx"
2.3 安装依赖
bash
# 创建虚拟环境
python -m venv venv
source venv/bin/activate # Linux/Mac
# venv\Scripts\activate # Windows
# 安装依赖
pip install openai python-dotenv requests
三、完整代码实现
3.1 项目结构
skill_demo/
├── .env # 环境变量配置
├── main.py # 主程序入口
├── skill_loader.py # Skill 加载器
├── tools.py # 工具函数定义
├── skills/ # Skill 存放目录
│ └── web_summarizer/ # 网页总结 Skill
│ ├── skill.md # 说明书
│ └── script.py # 爬虫脚本
└── output/ # 输出目录
3.2 Skill 说明书示例 (skills/web_summarizer/skill.md)
markdown
# 网页内容总结器
## 元信息
- 名称: web_summarizer
- 简介: 抓取指定 URL 的网页内容并生成摘要
## 使用说明
当你需要总结一个网页的内容时,使用此技能。
## 执行步骤
1. 调用脚本 `script.py` 传入 URL 参数
2. 脚本会抓取网页正文内容并保存到 output 目录
3. 读取生成的 Markdown 文件进行总结
## 依赖
- Python 包: requests, beautifulsoup4
## 脚本调用方式
```bash
python skills/web_summarizer/script.py --url <URL> --output output/summary.md
### 3.3 爬虫脚本 (skills/web_summarizer/script.py)
```python
#!/usr/bin/env python3
import requests
import argparse
from bs4 import BeautifulSoup
import re
def fetch_webpage(url):
"""抓取网页并提取正文内容"""
headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'}
try:
response = requests.get(url, headers=headers, timeout=10)
response.encoding = 'utf-8'
soup = BeautifulSoup(response.text, 'html.parser')
# 移除 script 和 style 标签
for script in soup(["script", "style"]):
script.decompose()
# 获取文本并清理
text = soup.get_text()
lines = (line.strip() for line in text.splitlines())
chunks = (phrase.strip() for line in lines for phrase in line.split(" "))
text = ' '.join(chunk for chunk in chunks if chunk)
# 限制长度
return text[:5000] + ("..." if len(text) > 5000 else "")
except Exception as e:
return f"抓取失败: {str(e)}"
def main():
parser = argparse.ArgumentParser()
parser.add_argument('--url', required=True, help='目标 URL')
parser.add_argument('--output', required=True, help='输出文件路径')
args = parser.parse_args()
content = fetch_webpage(args.url)
with open(args.output, 'w', encoding='utf-8') as f:
f.write(f"# 网页摘要\n\n")
f.write(f"原始 URL:{args.url}\n\n")
f.write(f"## 内容\n\n{content}\n")
print(f"内容已保存到 {args.output}")
if __name__ == '__main__':
main()
3.4 Skill 加载器 (skill_loader.py)
python
import os
import glob
from pathlib import Path
class SkillLoader:
"""Skill 加载器:负责扫描、读取元信息和加载详细说明书"""
def __init__(self, skill_dirs=None):
self.skill_dirs = skill_dirs or ['./skills']
self.skills_meta = {} # name -> {name, description, path}
self._scan_skills()
def _scan_skills(self):
"""扫描所有 Skill 目录,读取元信息"""
for base_dir in self.skill_dirs:
base_path = Path(base_dir)
if not base_path.exists():
continue
# 查找所有包含 skill.md 的子目录
skill_files = glob.glob(str(base_path / '**' / 'skill.md'), recursive=True)
for skill_file in skill_files:
skill_dir = Path(skill_file).parent
skill_name = skill_dir.name
# 解析 skill.md 获取元信息
with open(skill_file, 'r', encoding='utf-8') as f:
content = f.read()
# 简单解析:从 Markdown 提取标题和简介
name = skill_name
description = ""
lines = content.split('\n')
for i, line in enumerate(lines):
if line.startswith('# '): # 一级标题
name = line[2:].strip()
if '简介' in line and i < len(lines)-1:
description = lines[i+1].strip()
break
if not description:
description = f"使用 {name} 技能"
self.skills_meta[skill_name] = {
'name': skill_name,
'display_name': name,
'description': description,
'path': str(skill_dir),
'md_path': skill_file
}
def get_skills_prompt(self):
"""生成注入系统提示词的技能列表"""
if not self.skills_meta:
return "当前没有可用技能。"
prompt = "\n## 可用技能列表\n"
for name, meta in self.skills_meta.items():
prompt += f"- {meta['display_name']}({name}):{meta['description']}\n"
prompt += "\n当需要使用某个技能时,调用 load_skill 工具加载详细说明。\n"
return prompt
def load_skill(self, skill_name):
"""加载指定技能的完整 Markdown 说明书"""
if skill_name not in self.skills_meta:
return f"错误:未找到技能 '{skill_name}'"
md_path = self.skills_meta[skill_name]['md_path']
with open(md_path, 'r', encoding='utf-8') as f:
return f.read()
3.5 工具函数定义 (tools.py)
python
import subprocess
import os
import json
class Tools:
"""Agent 可用的工具集合"""
def __init__(self, skill_loader):
self.skill_loader = skill_loader
def load_skill(self, skill_name: str) -> str:
"""
加载技能说明书
Args:
skill_name: 技能名称
Returns:
技能 Markdown 内容
"""
return self.skill_loader.load_skill(skill_name)
def run_script(self, command: str) -> str:
"""
执行 Shell 命令
Args:
command: 要执行的命令
Returns:
命令输出结果
"""
try:
result = subprocess.run(
command,
shell=True,
capture_output=True,
text=True,
timeout=30
)
if result.returncode == 0:
return result.stdout
else:
return f"执行失败:{result.stderr}"
except Exception as e:
return f"执行异常:{str(e)}"
def read_file(self, file_path: str) -> str:
"""
读取文件内容
Args:
file_path: 文件路径
Returns:
文件内容
"""
try:
if not os.path.exists(file_path):
return f"文件不存在:{file_path}"
with open(file_path, 'r', encoding='utf-8') as f:
return f.read()
except Exception as e:
return f"读取失败:{str(e)}"
def get_tool_definitions(self):
"""获取工具定义列表(用于传递给大模型)"""
return [
{
"type": "function",
"function": {
"name": "load_skill",
"description": "加载指定技能的详细说明书,返回 Markdown 格式内容",
"parameters": {
"type": "object",
"properties": {
"skill_name": {
"type": "string",
"description": "技能名称,如 'web_summarizer'"
}
},
"required": ["skill_name"]
}
}
},
{
"type": "function",
"function": {
"name": "run_script",
"description": "执行 Shell 命令,用于运行技能脚本",
"parameters": {
"type": "object",
"properties": {
"command": {
"type": "string",
"description": "要执行的完整命令"
}
},
"required": ["command"]
}
}
},
{
"type": "function",
"function": {
"name": "read_file",
"description": "读取文件内容,用于获取脚本执行结果",
"parameters": {
"type": "object",
"properties": {
"file_path": {
"type": "string",
"description": "文件路径"
}
},
"required": ["file_path"]
}
}
}
]
3.6 主程序 (main.py)
python
import os
import json
from dotenv import load_dotenv
from openai import OpenAI
from skill_loader import SkillLoader
from tools import Tools
load_dotenv()
class SkillAgent:
"""支持 Skill 的 Agent 实现"""
def __init__(self, api_key=None, base_url=None):
self.api_key = api_key or os.getenv("DASHSCOPE_API_KEY")
if not self.api_key:
raise ValueError("请设置 DASHSCOPE_API_KEY 环境变量")
# 使用阿里云 DashScope 的 OpenAI 兼容接口
self.client = OpenAI(
api_key=self.api_key,
base_url=base_url or "https://dashscope.aliyuncs.com/compatible-mode/v1"
)
# 初始化 Skill 加载器
self.skill_loader = SkillLoader()
# 初始化工具
self.tools = Tools(self.skill_loader)
# 构建系统提示词
self.system_prompt = self._build_system_prompt()
# 对话历史
self.messages = []
def _build_system_prompt(self):
"""构建系统提示词(注入技能列表)"""
base_prompt = """你是一个可以帮助用户完成各种任务的 AI 助手。
你可以使用以下工具来获取信息或执行操作:
- load_skill:加载技能说明书,了解如何使用某个技能
- run_script:执行 Shell 命令,运行脚本
- read_file:读取文件内容
当用户请求需要使用特定技能时,请遵循以下流程:
1. 调用 load_skill 加载该技能的说明书
2. 根据说明书中的步骤,调用 run_script 执行相应脚本
3. 调用 read_file 读取执行结果
4. 基于结果回答用户问题
"""
# 注入技能元信息
base_prompt += self.skill_loader.get_skills_prompt()
return base_prompt
def run(self, user_input):
"""处理用户输入,执行工具调用循环"""
print(f"\n👤 用户: {user_input}\n")
# 初始化消息
if not self.messages:
self.messages = [
{"role": "system", "content": self.system_prompt},
{"role": "user", "content": user_input}
]
else:
self.messages.append({"role": "user", "content": user_input})
# 工具调用循环
max_iterations = 5
for i in range(max_iterations):
print(f"🔄 第 {i+1} 次推理...")
# 调用大模型
response = self.client.chat.completions.create(
model="qwen-plus", # 使用 qwen-plus 模型
messages=self.messages,
tools=self.tools.get_tool_definitions(),
tool_choice="auto"
)
message = response.choices[0].message
# 如果没有工具调用,直接返回
if not message.tool_calls:
print("✅ 完成回答\n")
self.messages.append({"role": "assistant", "content": message.content})
return message.content
# 处理工具调用
print(f"🔧 需要调用工具...")
self.messages.append(message)
for tool_call in message.tool_calls:
function_name = tool_call.function.name
arguments = json.loads(tool_call.function.arguments)
print(f" 调用: {function_name}, 参数: {arguments}")
# 执行工具函数
if hasattr(self.tools, function_name):
func = getattr(self.tools, function_name)
result = func(**arguments)
else:
result = f"错误:未知工具 {function_name}"
print(f" 结果: {result[:100]}...\n")
# 添加工具响应
self.messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": result
})
return "已达到最大迭代次数,请重试。"
def main():
"""主函数:交互式运行"""
agent = SkillAgent()
print("="*50)
print("Skill Agent Demo (使用 Qwen 国内大模型)")
print("="*50)
print("可用技能:网页总结器 (web_summarizer)")
print("示例输入:请帮我总结这个网页的内容:https://www.example.com")
print("输入 'exit' 退出\n")
while True:
user_input = input("\n请输入: ").strip()
if user_input.lower() in ['exit', 'quit']:
break
response = agent.run(user_input)
print(f"\n🤖 助手:\n{response}\n")
if __name__ == "__main__":
main()
四、运行演示
4.1 启动 Agent
bash
python main.py
4.2 交互过程
==================================================
Skill Agent Demo (使用 Qwen 国内大模型)
==================================================
可用技能:网页总结器 (web_summarizer)
示例输入:请帮我总结这个网页的内容:https://www.example.com
输入 'exit' 退出
请输入: 请帮我总结这个网页的内容:https://www.baidu.com
👤 用户: 请帮我总结这个网页的内容:https://www.baidu.com
🔄 第 1 次推理...
🔧 需要调用工具...
调用: load_skill, 参数: {'skill_name': 'web_summarizer'}
结果: # 网页内容总结器
## 元信息
- 名称: web_summarizer
- 简介: 抓取指定 URL 的网页内容并生成摘要
...
🔄 第 2 次推理...
🔧 需要调用工具...
调用: run_script, 参数: {'command': 'python skills/web_summarizer/script.py --url https://www.baidu.com --output output/summary.md'}
结果: 内容已保存到 output/summary.md...
🔄 第 3 次推理...
🔧 需要调用工具...
调用: read_file, 参数: {'file_path': 'output/summary.md'}
结果: # 网页摘要
原始 URL:https://www.baidu.com
## 内容
百度一下,你就知道 关于百度 ...
🔄 第 4 次推理...
✅ 完成回答
🤖 助手:
根据网页内容,这是百度的首页,主要提供搜索引擎服务。页面包含以下主要元素:
- 搜索框,支持网页、资讯、贴吧等搜索
- 百度产品入口,如地图、视频、百科等
- 新闻资讯栏目,显示热门新闻标题
- 登录和设置选项
由于该页面主要是搜索入口,实际内容较少,主要功能是引导用户进行搜索操作。
4.3 生成的输出文件
output/summary.md 内容示例:
markdown
# 网页摘要
原始 URL:https://www.baidu.com
## 内容
百度一下,你就知道 关于百度 网页 资讯 贴吧 知道 视频 图片 音乐 地图 文库 更多>> 登录 百度首页 设置 百度新闻 热点要闻 国内 国际 体育 娱乐 财经 科技 互联网 游戏 军事 汽车 房产 时尚 社会 情感 大家都在搜 1 人工智能 2 新能源汽车 3 国产芯片 ...
五、原理图说明
5.1 Skill 三层加载机制
执行时
触发时
启动时
扫描 skills 目录
读取每个 skill.md
提取元信息
名称+简介
注入系统提示词
用户请求
总结网页
Agent 推理
需要 web_summarizer 技能
调用 load_skill 工具
加载完整 skill.md
解析 skill.md
发现需运行脚本
调用 run_script 工具
执行 script.py
抓取网页内容
调用 read_file 工具
读取输出文件
生成最终回答
5.2 工具调用流程
外部脚本 工具函数 大模型(Qwen) Agent 用户 外部脚本 工具函数 大模型(Qwen) Agent 用户 推理需要 web_summarizer 解析 skill.md,需要运行脚本 需要读取输出文件 基于内容生成总结 总结网页:https://... 发送消息+工具列表 调用 load_skill(skill_name) 执行 load_skill 返回 skill.md 内容 发送工具结果 调用 run_script(command) 执行 run_script 执行 python script.py 返回执行状态 返回执行结果 发送工具结果 调用 read_file(path) 执行 read_file 返回文件内容 发送工具结果 返回最终回答 显示总结结果
六、关键点总结
6.1 国内大模型工具调用的特点
- API 兼容性:主流国内模型(Qwen、DeepSeek、Kimi)均兼容 OpenAI 格式,可使用同一套代码调用
- 工具定义方式 :通过
tools参数传递 JSON Schema 格式的工具定义 - 调用流程 :模型返回
tool_calls字段,开发者解析后执行并回传结果
6.2 Skill 系统的核心优势
- 按需加载:启动时只加载元信息,使用时才加载详细说明书,节省 Token
- 可扩展性:新增 Skill 只需添加文件夹,无需修改 Agent 代码
- 灵活性:Skill 可包含任意脚本,突破 LLM 自身能力限制
6.3 代码要点
| 组件 | 作用 |
|---|---|
SkillLoader |
扫描 Skill 目录、管理元信息、提供加载功能 |
Tools |
封装可被模型调用的工具函数 |
main.py |
Agent 主循环,处理消息和工具调用 |
skill.md |
Skill 说明书,包含元信息和执行步骤 |
script.py |
实际执行任务的脚本 |
七、扩展建议
7.1 添加更多 Skill
只需在 skills/ 目录下新建文件夹,包含:
skill.md:说明书- 任意脚本文件(Python/Node.js/Shell)
- 依赖清单
7.2 使用其他国内大模型
| 平台 | API 地址 | 模型示例 |
|---|---|---|
| 阿里云百炼 | https://dashscope.aliyuncs.com/compatible-mode/v1 |
qwen-max, qwen-plus |
| 硅基流动 | https://api.siliconflow.cn/v1 |
deepseek-coder, qwen2.5 |
| Moonshot | https://api.moonshot.cn/v1 |
kimi-k2-turbo-preview |
7.3 高级优化
- MCP 集成:使用模型上下文协议标准化工具接入
- 流式输出:实现逐字返回,提升用户体验
- 会话记忆:保存对话历史,支持多轮复杂任务
总结 :通过国内大模型的工具调用能力,我们可以实现与视频案例完全相同的 Skill 系统。核心在于理解大模型只负责生成调用指令,实际执行由开发者代码完成,这一机制使得 Skill 具有无限扩展的可能性。