最近刷到一堆 agent 框架都在讲 Skills / Tools / Plugins,我顺手点开看了下,发现大家讲的其实是同一件事:把“能力”做成可调用的模块。
学术上 1999 年的强化学习就把它叫“temporally extended actions/Options”(技能的一个祖先),LLM 这波是在 2022–2023 年被 ReAct、Toolformer、函数/工具调用这条线重新产品化了(见文末入口)。
Skills当前显然是炙手可热的研究对象, 是 agent 从“会聊”变成“能交付”的分水岭,它把不确定的生成,改造成可验证的执行。
Skills 解决的是“把自然语言意图变成可执行动作”的落地链路——复现路径更短(同一套 Skill 接口),成本更可算(每次调用可计费/可限流),上手更像搭积木(组合而不是重训)。
在强化学习里,技能常被形式化成 Options:你可以把它理解成一个小型策略(policy),加一个“什么时候能启动”(initiation set) 和 “什么时候结束”(termination) 的包装。1999 那篇论文的贡献是把“宏动作/技能”放进更通用的 SMDP 框架里,让分层决策有数学定义(Sutton/Precup/Singh 1999,见文末)。
这条线的直觉很产品:把长任务拆成可复用的子能力,复用带来更快学习/更稳执行。
LLM 里 Skills 更像“带 schema 的 API”。模型负责选哪个技能、填什么参数;系统负责执行并回结果。Function/Tool Calling 把这件事标准化成“模型输出一个函数名+JSON 参数”,并强调可以用 JSON Schema 严格约束参数形状(OpenAI 文档见文末)。
这里的关键不是“能不能调用”,而是“参数能不能被系统无歧义解析”。
一种思路是 ReAct:让模型在“思考/行动/观察”之间交替,行动就是技能调用,观察就是技能返回(ReAct 论文见文末)。它的好处是轨迹天然可读,出了错也能定位在某一步。
另一种思路是 SDK 把循环封装掉,比如 Semantic Kernel 把函数调用循环和 planner 打包,开发者重点写技能描述和边界(Semantic Kernel 文档入口见文末)。
不管哪条路,本质都是:从“一次性回答”变成“多轮控制回路”。
最早像“专家系统/脚本函数库”,后来变成“可发现的插件市场”,再到今天更像“企业内部能力中心”:每个技能都要有权限、配额、审计、版本、回滚。
Toolformer 这类工作还在推一件事:技能调用不只是规则路由,也可以用数据让模型学“何时该调用工具、调用哪个工具”(Toolformer 论文见文末)。这会让技能从“人为配置”走向“半自动增长”。
因为它把不确定的 token 生成,换成确定的系统调用。比如“算税后价”这种事,用模型算=高波动;用技能算=可复现、可测、可缓存。
代价是系统复杂度上升:你要维护技能的版本兼容、错误语义、超时重试,还要防模型乱填参数。速度和稳定性是用工程治理换来的。
粒度太细:编排步数暴涨,token 成本上升、失败点变多;粒度太粗:复用差、权限难拆、可观测性变差。
一个实用的定法是按“可验证边界”切:能被单测断言的、能被审计的、能被授权的,就值得单独做成技能。
剩下的“写文案/总结/改写”留给模型或许会更划算。
它像移动互联网的“能力开放平台”:登录、支付、地图都做成 SDK/接口,业务只管编排。
但它又不像传统 SDK:技能的“调用者”不是人写死的 if/else,而是模型在运行时做选择,所以你必须把不确定性前移到“接口设计+约束+观测”上。
也有点像机器人里的“技能树/行为树”,区别是 LLM 让编排更灵活,但也更需要护栏。
下面是一个极简“技能注册 + 路由 + 执行”的 Python 例子,不依赖第三方库。你可以把它当作 Skills 的最小骨架:技能=函数;路由=根据意图选技能;返回=结构化 dict。
# python 3.10+
import json
from typing import Callable, Any
Skill = Callable[[dict], dict]
def skill_calc_vat(args: dict) -> dict:
amount = float(args["amount"])
rate = float(args.get("rate", 0.13))
return {"ok": True, "vat": round(amount * rate, 2), "currency": args.get("currency", "CNY")}
def skill_lookup_user(args: dict) -> dict:
user_id = str(args["user_id"])
fake_db = {"42": {"name": "Ada", "tier": "pro"}}
user = fake_db.get(user_id)
return {"ok": bool(user), "user": user}
SKILLS: dict[str, Skill] = {
"calc_vat": skill_calc_vat,
"lookup_user": skill_lookup_user,
}
def route(intent: str) -> tuple[str, dict]:
intent = intent.lower()
if "vat" in intent or "增值税" in intent:
return "calc_vat", {"amount": 1000, "rate": 0.13, "currency": "CNY"}
if "user" in intent or "用户" in intent:
return "lookup_user", {"user_id": "42"}
return "lookup_user", {"user_id": "0"}
if __name__ == "__main__":
tool, args = route("帮我算一下 1000 的增值税")
result = SKILLS[tool](args)
print(json.dumps({"tool": tool, "args": args, "result": result}, ensure_ascii=False, indent=2))运行方式:
python skill_demo.py或许大家可以把每条轨迹的 tool calls 抽出来,固定同一套断言,跑回放对比(同输入、同技能版本)。