(二)大模型实操- Skill 入门:从原理到第一个可调用工具

-- 引题: 在 AI 应用开发中,"Skill"(技能)是让大语言模型(LLM)突破纯文本限制、学会调用外部工具完成特定任务的核心机制。本文将从通俗理解出发,结合可运行的 Python 代码,带你一步步实现并管理自己的 Skill。

一、什么是 Skill?通俗理解

1.1 为什么大模型需要"技能"?

大模型就像一个知识渊博但"手不能提、脚不能走"的学者:它能聊古诗、解释成语,但没法查数据库、不知道当前时间、也不会发邮件。为了让模型动起来,我们给它配备一套"外挂工具"------这就是 Skill。

Skill = 一个 Python 函数 + 一份给模型的说明书

当用户问题需要工具时,模型不直接回答,而是输出特殊的调用指令(如 JSON)。应用程序解析指令、执行函数,再将结果返回给模型,最终生成完整答案。

1.2 生活化类比

想象你有一个私人助手,他不会查天气,但你可以这样做:

  1. 在助手旁边贴一张字条:"如果需要查天气,请对我说 {tool: "天气", city: "北京"}"
  2. 你作为"系统",听到这句话后就自己去打开天气 APP,然后把结果念给助手听。
  3. 助手再根据结果组织语言告诉你:"今天北京晴,23°C。"

这个字条就是工具描述 ,你执行的查询动作就是Skill 函数

二、Skill 的核心原理

Skill 系统的运行包含四个步骤:

text 复制代码
用户提问 → 模型判断是否需要工具 → 输出调用指令(JSON)
    ↓
系统解析指令,执行 Python 函数
    ↓
将执行结果拼回对话历史
    ↓
模型基于结果生成最终答案

这称为 Tool UseReAct 模式。许多框架(如 LangChain、OpenAI Function Calling)都基于这一思想,但本质上都是这个循环。

三、Skill结构

3.1 Markdown 文件结构

Markdown 复制代码
---
# ===== 必填字段 =====
name: string                     # 技能英文名,唯一标识
description: string              # 一句话功能说明

# ===== 调用规范 =====
parameters: list<object>         # 输入参数列表
  - name: string                 #   参数名
    type: string                 #   数据类型(string / integer / float / boolean)
    description: string          #   参数含义说明
    required: boolean            #   是否必填 (true / false)
    default: any                 #   默认值(可选)
    enum: list<any>              #   可选值列表(可选)
    example: string              #   示例值(可选)

return_type: string              # 返回值类型(string / number / object / void 等)
return_description: string       # 返回值含义说明(可选)

# ===== 触发与示例 =====
trigger_keywords: list<string>   # 触发该工具的典型用户输入片段(可选,可用于路由)
examples: list<object>           # 调用示例列表(至少一个)
  - query: string                #   用户可能的提问
    parameters: object           #   期望传入的参数值
    expected_call: string        #   期望模型输出的 JSON 调用指令
    note: string                 #   可选的解释说明

# ===== 执行与限制 =====
timeout_seconds: integer         # 工具超时时间(可选,默认 30)
max_concurrent: integer          # 最大并发调用数(可选,默认 1)
requires_confirmation: boolean   # 某些危险操作是否需要用户二次确认(可选,默认 false)
tags: list<string>               # 标签,用于分类(可选,如:"语文","工具")

# ===== 维护信息 =====
author: string                   # 创建者(可选)
version: string                  # 版本号(可选)
---

# 技能名称(可选正文,用于生成更详细的工具说明书)

这里写技能的详细描述、注意事项、调用逻辑等,可以直接被拼接到 System Prompt 中。

3.2、字段详细说明

必填字段

字段 类型 含义 示例
name string 技能的唯一标识符,使用小写字母 + 下划线,与注册的 Python 函数名一致 search_knowledge
description string 用简短的中文说明该技能的功能,会直接展示给模型 "在小学语文知识库中检索与查询相关的资料"

调用规范

字段 类型 含义 示例
parameters list<object> 输入参数数组,每个参数包含子字段 见下方
parameters.name string 参数名,JSON 调用时的键名 query
parameters.type string 数据类型,帮助模型理解应传什么类型。常用:string, integer, float, boolean string
parameters.description string 参数的业务含义,越具体模型越不容易出错 "查询字符串,例如'高兴的近义词'"
parameters.required boolean 是否必填,truefalse true
parameters.default any 可选,当用户未提供时的默认值 2
parameters.enum list<any> 可选,限制参数的合法取值 ["唐诗","宋词"]
parameters.example string 可选,提供一个典型值,可用于生成 Few-shot 示例 "狐假虎威"
return_type string 返回值的数据类型,模型用它确定下一步如何处理结果 string
return_description string 可选,描述返回值的含义,帮助模型理解工具输出 "检索到的知识片段,每段以'资料n:'开头"

触发与示例

字段 类型 含义 示例
trigger_keywords list<string> 可选,用户问题时若命中这些词,可优先考虑调用此工具 ["近义词","造句","古诗","成语"]
examples list<object> 调用示例,供模型学习正确的调用格式。建议至少提供一个 见下方
examples.query string 模拟的用户提问 "请用'高兴'的近义词造句"
examples.parameters object 期望传入的参数键值对 {"query": "高兴 近义词"}
examples.expected_call string 期望模型输出的完整 JSON 调用指令 {"tool": "search_knowledge", "query": "高兴 近义词"}
examples.note string 可选,解释为什么这样调用,用于文档维护 "该例展示了模型如何提取核心查询"

执行与限制

字段 类型 含义 示例
timeout_seconds integer 可选,工具执行的超时时间(秒),超时后返回错误信息 10
max_concurrent integer 可选,允许同时调用的最大实例数,通常为 1 1
requires_confirmation boolean 可选,若为 true,在执行前系统会要求用户确认 false
tags list<string> 可选,用于给技能分类,方便管理 ["语文","知识库","查询"]

维护信息

字段 类型 含义 示例
author string 可选,技能作者 "张三"
version string 可选,技能版本号 "1.0.0"

四、手写第一个 Skill:语文知识库检索

我们以"小学语文知识库检索"为例,用本地 Qwen2-0.5B 模型 + Chroma 向量库实现一个可用的 Skill。

4.1 准备知识库(Chroma)

假设你已经按照之前的 RAG 教程,在 ./chroma_rag_db 中构建好了小学语文知识库。如果没有,请运行一次数据入库脚本。

4.2 定义 Skill 函数

python

python 复制代码
# skill_search.py
import chromadb
from sentence_transformers import SentenceTransformer

client = chromadb.PersistentClient(path="./chroma_rag_db")
collection = client.get_collection("primary_chinese_qa")
embedder = SentenceTransformer("shibing624/text2vec-base-chinese")

def search_knowledge(query: str, top_k: int = 2) -> str:
    """在小学语文知识库中检索相关片段"""
    q_emb = embedder.encode([query]).tolist()
    results = collection.query(query_embeddings=q_emb, n_results=top_k)
    chunks = results["documents"][0]
    if not chunks:
        return "未找到相关资料。"
    return "\n\n".join(f"资料{i+1}:{chunk}" for i, chunk in enumerate(chunks))

4.3 撰写工具描述(给模型看的说明书)

我们需要在系统提示中告诉模型:何时调用、如何调用、参数是什么。

python

ini 复制代码
TOOLS_DEFINITION = """
你可以使用以下工具来回答问题:

1. search_knowledge(query: str) -> str
   描述:在小学语文知识库中检索相关资料。
   调用格式:{"tool": "search_knowledge", "query": "简短查询关键词"}

规则:如果你需要调用工具,请只输出一行JSON,不要输出任何其他内容。
     如果不需要工具,直接正常回答。
"""

4.4 实现调用循环(Tool Use Loop)

加载本地微调模型,编写对话函数,自动解析 JSON 并执行工具。

python

python 复制代码
import json
import torch
from transformers import AutoTokenizer, AutoModelForCausalLM
from peft import PeftModel

# 加载模型(你的微调 Qwen2-0.5B)
model_path = "./Qwen2-0.5B-Instruct-Local"
lora_path = "./qwen-lora-finetuned"

tokenizer = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True)
base = AutoModelForCausalLM.from_pretrained(
    model_path, torch_dtype=torch.float16, trust_remote_code=True, device_map="cpu"
)
if lora_path:
    model = PeftModel.from_pretrained(base, lora_path)
    model = model.merge_and_unload()
else:
    model = base
model.eval()
if torch.backends.mps.is_available():
    model.to("mps")

def chat_with_skill(user_message, max_calls=3):
    messages = [
        {"role": "system", "content": TOOLS_DEFINITION},
        {"role": "user", "content": user_message}
    ]
    for _ in range(max_calls):
        # 1. 生成回复
        text = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
        if isinstance(text, list):
            text = text[0]
        inputs = tokenizer(text, return_tensors="pt").to(model.device)
        with torch.no_grad():
            outputs = model.generate(**inputs, max_new_tokens=256, temperature=0.7,
                                     do_sample=True, top_p=0.9,
                                     pad_token_id=tokenizer.eos_token_id)
        reply = tokenizer.decode(outputs[0][len(inputs.input_ids[0]):], skip_special_tokens=True)

        # 2. 尝试解析工具调用
        try:
            call = json.loads(reply)
            if call.get("tool") == "search_knowledge":
                print(f"🔧 调用知识库检索:{call['query']}")
                result = search_knowledge(call.get("query"))
                messages.append({"role": "assistant", "content": reply})
                messages.append({"role": "user", "content": f"工具返回结果:\n{result}"})
                continue   # 继续循环,让模型生成最终答案
        except (json.JSONDecodeError, KeyError):
            pass
        # 如果不是工具调用,就是最终答案
        return reply
    return "抱歉,工具调用次数过多。"

# 测试
while True:
    q = input("👤 你:")
    if q.lower() in ["exit", "quit"]:
        break
    print(f"🤖 模型:{chat_with_skill(q)}\n")

运行效果

text

复制代码
👤 你:请用'高兴'的近义词造句。
🔧 调用知识库检索:高兴 近义词
🤖 模型:高兴的近义词有快乐、喜悦、愉快。我可以造句:他快乐地跳了起来。

模型自动识别了需要检索,调用 Skill 后给出了基于资料的准确回答。

五、用 Markdown 文件管理 Skill(解耦与扩展)

当 Skill 增多时,将工具定义硬编码在代码中难以维护。我们可以用 Markdown 文件 + YAML 元数据 来管理每个技能的描述,实现"添加 Skill 只需写文档和函数"。

5.1 Skill Markdown 文件规范

为每个 Skill 创建一个 .md 文件,放在 skills/ 目录下。文件包含:

  • Front Matter (YAML) :技能名、参数、触发词、调用示例等结构化信息。
  • 正文:自然的技能描述(可选,可进一步丰富提示词)。

示例:skills/search_knowledge.md

markdown

yaml 复制代码
---
name: search_knowledge
description: 在小学语文知识库中检索相关资料
parameters:
  - name: query
    type: string
    description: 查询字符串,例如"高兴的近义词"
    required: true
  - name: top_k
    type: integer
    description: 返回结果数量,默认2
    required: false
    default: 2
return_type: string
examples:
  - query: "高兴的近义词"
    expected_call: {"tool": "search_knowledge", "query": "高兴的近义词"}
---

# search_knowledge

当用户问题涉及语文知识点、成语、古诗、近反义词等内容时,应调用此工具获取权威资料,然后基于资料回答。

示例:skills/get_current_date.md

markdown

yaml 复制代码
---
name: get_current_date
description: 获取当前的日期和时间
parameters: []
return_type: string
examples:
  - expected_call: {"tool": "get_current_date"}
---

# get_current_date

当用户询问当前时间、日期或星期时,调用此工具获取系统时间。

5.2 实现 SkillLoader 解析器

利用 python-frontmatter 库读取并解析 Markdown 文件,生成工具提示文本,并管理函数映射。

bash

复制代码
pip install python-frontmatter pyyaml

python

python 复制代码
# skill_loader.py
import os
import frontmatter

class SkillLoader:
    def __init__(self, skill_dir="skills"):
        self.skill_dir = skill_dir
        self.skills = {}          # 技能名 -> 元数据
        self.function_map = {}    # 技能名 -> 实际函数

    def load_skills(self):
        """扫描skill_dir下所有.md文件并解析"""
        if not os.path.exists(self.skill_dir):
            return
        for fname in os.listdir(self.skill_dir):
            if fname.endswith(".md"):
                with open(os.path.join(self.skill_dir, fname), "r", encoding="utf-8") as f:
                    post = frontmatter.load(f)
                    meta = post.metadata
                    name = meta.get("name")
                    if name:
                        self.skills[name] = {"meta": meta, "content": post.content}

    def register_function(self, skill_name, func):
        self.function_map[skill_name] = func

    def get_tools_prompt(self):
        """生成给LLM的工具说明"""
        lines = ["你可以使用以下工具:\n"]
        for i, (name, info) in enumerate(self.skills.items(), 1):
            meta = info["meta"]
            desc = meta.get("description", "")
            params = meta.get("parameters", [])
            param_str = ", ".join(f"{p['name']}:{p.get('type','string')}" for p in params)
            call_ex = meta.get("examples", [{}])[0].get("expected_call", "")
            lines.append(f"{i}. {name}")
            lines.append(f"   描述:{desc}")
            if params:
                lines.append(f"   参数:{param_str}")
            lines.append(f"   调用格式:{call_ex}\n")
        lines.append("规则:需要工具时只输出一行JSON,否则正常回答。")
        return "\n".join(lines)

    def execute_tool_call(self, tool_call):
        """执行工具调用并返回结果字符串"""
        name = tool_call.get("tool")
        func = self.function_map.get(name)
        if not func:
            return f"错误:未知工具 {name}"
        kwargs = {k: v for k, v in tool_call.items() if k != "tool"}
        try:
            return str(func(**kwargs))
        except Exception as e:
            return f"工具执行错误:{str(e)}"

5.3 集成到对话系统

python

perl 复制代码
# chat_with_skills_v2.py
from datetime import datetime
from skill_loader import SkillLoader
# 导入或定义你的 Skill 函数...

loader = SkillLoader("skills")
loader.load_skills()

# 注册 Skill 函数
loader.register_function("search_knowledge", search_knowledge)
loader.register_function("get_current_date", lambda: datetime.now().strftime("%Y年%m月%d日 %H:%M:%S"))

# 生成工具提示
TOOLS_PROMPT = loader.get_tools_prompt()

# 其余模型加载和循环部分与之前类似,只需将 TOOLS_DEFINITION 替换为 TOOLS_PROMPT
# 并在解析到 tool_call 时使用 loader.execute_tool_call(tool_call) 代替手动判断

5.4 添加新 Skill 步骤总结

  1. skills/ 目录新建 .md 文件,按规定格式填写元数据。
  2. 编写实际的 Python 函数。
  3. 在对话启动时调用 loader.register_function("技能名", 函数)
  4. 重启服务,模型自动获得新技能。

优势:非技术人员可以只修改 Markdown 文档来调整工具描述,不影响业务逻辑。


六、进阶:让 Skill 更智能

  • 多轮工具使用:一个任务可能需要先查时间、再查课表,循环逻辑天然支持。
  • 错误处理:工具执行可能失败,将错误信息返回给模型,让它尝试修正请求。
  • 参数校验 :可在 execute_tool_call 中加入类型转换和缺失默认值处理。
  • 并行调用:高级框架可一次调用多个工具,但目前手写循环已足够轻量。

七、总结

  • Skill 是 AI 应用的手和脚,让模型从"能说"变为"能做"。
  • 实现 Skill 只需要函数 + 描述 + 调用循环三个元素。
  • Markdown 文件管理 可以让系统更具扩展性和可维护性。
相关推荐
ComputerInBook1 小时前
数字图像处理(4版)——第 9 章——形态学图像处理(Rafael C.Gonzalez&Richard E. Woods)
图像处理·人工智能·计算机视觉·形态学·数学形态学
eastyuxiao2 小时前
如何用思维导图拆解项目范围
大数据·人工智能·流程图
机器之心2 小时前
DeepSeek版Claude Code登顶热榜:8700星,鲸鱼哥火了
人工智能·openai
易标AI2 小时前
标书智能体(五)——如何让弱模型也能稳定输出复杂json
人工智能·python·提示词·智能体·招投标
:mnong2 小时前
模具非标件报价-精密算盘智能体SOP
人工智能·cad
技术人生黄勇2 小时前
GitNexus 把代码库变成知识图谱|审核 AI 产出更清晰,改 Bug 更精准
人工智能·bug
俊哥V2 小时前
每日 AI 研究简报 · 2026-05-05
人工智能·ai
阿里云大数据AI技术2 小时前
Qwen3.6、Kimi-K2.6、Minimax-M2.7、GLM-5.1 来啦!PAI支持海量模型一键部署!
人工智能·llm
袁庭新2 小时前
2026年03月总结
人工智能·袁庭新·工作总结·月总结·openclaw