一、Skill 原理回顾(国内大模型视角)

1.1 Skill 的本质

Skill 是给 Agent 装备的"技能包",包含说明书(Markdown)、可执行脚本和依赖资源。Agent 通过三层加载机制使用 Skill:

  1. 启动时:扫描 Skill 目录,读取元信息(名称+简介)注入系统提示词
  2. 触发时:按需加载完整的 Skill 说明书到上下文
  3. 执行时:根据说明书调用外部脚本/工具执行任务

1.2 国内大模型的工具调用能力

关键理解:大模型本身不需要理解工具的具体实现,只负责根据工具列表生成调用指令。

组件 职责
大模型 根据工具描述决定"是否调用"+“传什么参数”
开发者代码 实际执行工具调用(解析指令、调用API、返回结果)
工具定义 以 JSON Schema 形式描述工具名称、参数、功能

国内主流大模型(Qwen、DeepSeek、Kimi)均支持工具调用,且 API 接口兼容 OpenAI 格式。

二、环境准备

2.1 选择模型平台

本次 Demo 使用 阿里云通义千问 Qwen,原因:

  • API 兼容 OpenAI 格式,调用简单
  • 支持工具调用(Tool Calling)
  • 国内直连,无需特殊网络设置

2.2 获取 API Key

  1. 访问阿里云百炼平台
  2. 开通"模型服务",创建 API-KEY
  3. 将 API Key 保存到环境变量:
export DASHSCOPE_API_KEY="sk-xxxxxxxxxxxx"

2.3 安装依赖

# 创建虚拟环境
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)

# 网页内容总结器

## 元信息
- 名称: 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)

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)

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)

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

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 内容示例:

# 网页摘要

原始 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 国内大模型工具调用的特点

  1. API 兼容性:主流国内模型(Qwen、DeepSeek、Kimi)均兼容 OpenAI 格式,可使用同一套代码调用
  2. 工具定义方式:通过 tools 参数传递 JSON Schema 格式的工具定义
  3. 调用流程:模型返回 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 具有无限扩展的可能性。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐