-- 引题: 在 AI 应用开发中,"Skill"(技能)是让大语言模型(LLM)突破纯文本限制、学会调用外部工具完成特定任务的核心机制。本文将从通俗理解出发,结合可运行的 Python 代码,带你一步步实现并管理自己的 Skill。
一、什么是 Skill?通俗理解
1.1 为什么大模型需要"技能"?
大模型就像一个知识渊博但"手不能提、脚不能走"的学者:它能聊古诗、解释成语,但没法查数据库、不知道当前时间、也不会发邮件。为了让模型动起来,我们给它配备一套"外挂工具"------这就是 Skill。
Skill = 一个 Python 函数 + 一份给模型的说明书
当用户问题需要工具时,模型不直接回答,而是输出特殊的调用指令(如 JSON)。应用程序解析指令、执行函数,再将结果返回给模型,最终生成完整答案。
1.2 生活化类比
想象你有一个私人助手,他不会查天气,但你可以这样做:
- 在助手旁边贴一张字条:"如果需要查天气,请对我说
{tool: "天气", city: "北京"}" - 你作为"系统",听到这句话后就自己去打开天气 APP,然后把结果念给助手听。
- 助手再根据结果组织语言告诉你:"今天北京晴,23°C。"
这个字条就是工具描述 ,你执行的查询动作就是Skill 函数。
二、Skill 的核心原理
Skill 系统的运行包含四个步骤:
text
用户提问 → 模型判断是否需要工具 → 输出调用指令(JSON)
↓
系统解析指令,执行 Python 函数
↓
将执行结果拼回对话历史
↓
模型基于结果生成最终答案
这称为 Tool Use 或 ReAct 模式。许多框架(如 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 |
是否必填,true 或 false |
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 步骤总结
- 在
skills/目录新建.md文件,按规定格式填写元数据。 - 编写实际的 Python 函数。
- 在对话启动时调用
loader.register_function("技能名", 函数)。 - 重启服务,模型自动获得新技能。
优势:非技术人员可以只修改 Markdown 文档来调整工具描述,不影响业务逻辑。
六、进阶:让 Skill 更智能
- 多轮工具使用:一个任务可能需要先查时间、再查课表,循环逻辑天然支持。
- 错误处理:工具执行可能失败,将错误信息返回给模型,让它尝试修正请求。
- 参数校验 :可在
execute_tool_call中加入类型转换和缺失默认值处理。 - 并行调用:高级框架可一次调用多个工具,但目前手写循环已足够轻量。
七、总结
- Skill 是 AI 应用的手和脚,让模型从"能说"变为"能做"。
- 实现 Skill 只需要函数 + 描述 + 调用循环三个元素。
- 用 Markdown 文件管理 可以让系统更具扩展性和可维护性。