1. 先搞明白openclaw的Skill到底是个什么机制
很多人第一次看到openclaw的Skill目录,会以为它和传统插件市场里的插件一样,下载装上就能用。实际上Skill在openclaw里承担的是"可复用的原子能力"角色,它不想做重型业务系统,而是把一个具体操作指令封装成一套让模型能理解、能调用、能执行的标准化流程。
我举个生活化的例子:openclaw本体像是一个聪明的助理,它懂很多事,但它不是万能的具体执行者。你给它装一个"food-order"技能,相当于给它配了一本完整的点餐操作手册——上面写清楚"用户说要吃辣的就优先川湘菜""下单前必须确认配送地址""优惠券要用完再算实付"这些规则。助理拿到这本手册,再结合当前对话上下文,才知道该怎么动手。
这里有个关键点:Skill的本质是"提示词模板+可执行代码+配置协议"三件套,而不是一个黑盒程序。openclaw运行时会先让大模型阅读你的SKILL.md说明文档,理解这个技能在什么时候适用、需要哪些参数、输出什么结果,然后再去调用你写好的脚本或API。所以很多基础好的同学完全可以自己手写Skill,并不需要什么特殊技能。
1.1 Skill在openclaw里的定位:它不是插件也不是普通API
我先做个对比帮助理解:
- 插件(Plugin):通常是对框架能力的扩展,比如给openclaw加一个数据库连接器,它是系统级别的存在,对所有会话生效。
- API接口:是一个纯函数式的数据接口,你传参数进去,它返回结果,本身不具备"理解什么时候该用我"的能力。
- Skill:位于这两者之间。它自带"应该如何被使用"的说明文档,让大模型可以动态判断当前业务请求是否匹配这个技能,同时内部封装了具体调用逻辑(脚本、API请求、本地命令等)。
所以你在openclaw里配置一个food-order技能,模型收到"帮我点一杯冰美式"这种诉求时,会根据技能描述自动联想到:这个请求应该走food-order流程,然后填充参数、执行调用、返回结果。这个过程不是硬编码的,而是模型在推理时动态决策的,编排的灵活性就在这里体现。
1.2 Skill和MCP有什么区别
这个话题几乎在每次openclaw相关的讨论里都会出现,值得专门拿出来说清楚。MCP(Model Context Protocol)是一套标准的协议,核心是让AI模型通过一种统一方式去访问外部工具和数据。你可以把它理解成"插座标准",什么设备插上去都能通电。
Skill则是openclaw自己的一套技能封装规范,更侧重于"模型怎么理解任务、怎么组织流程",内部的工具调用可以用MCP,也可以直接写HTTP请求或执行本地脚本。两者的关系不是替代,而是配合:
- 如果你的诉求是让openclaw统一接入大量外部服务,走MCP比较合适,接入效率高、生态标准。
- 如果某个业务场景需要精细的逻辑控制,比如点餐时的菜品规格校验、优惠计算、店铺营业状态判断,用Skill封装更灵活,因为你可以直接写Python代码控制流程,而不是受限在MCP的工具调用协议里。
老实说,MCP适合做"标准化连接",Skill适合做"定制化场景"。food-order技能里我建议核心逻辑用Skill来写,如果后续想接多个餐饮平台,再考虑通过MCP统一对接上游接口,这样架构层次最清晰。
1.3 food-order技能适合解决哪些场景
标题里加了"实用"两个字,说明这个技能应当落地到真正的餐食场景中,而不是演示demo。我在实际测试中发现,下面这几类需求非常适合用food-order技能承接:
- 按条件推荐店铺并下单:用户说"帮我点一份人均40以内的轻食",技能自动筛选附近符合条件的店铺,生成菜单候选并询问确认。
- 定时/预约点餐:用户不在电脑前,希望明天中午12点自动下单,技能可以结合定时任务到点触发。
- 多地址管理:家里、公司、健身房多个常用地址,用户直接说"送到公司",技能从配置中读取对应地址并完成下单。
- 多平台比价与优惠计算:同一家店在不同平台价格、起送费、配送费不一样,技能并行查询后把最优方案给用户选择。
普通对话机器人只能做到"告诉你哪家店好吃",但结合food-order技能,openclaw能真正做到"从对话到订单"的闭环。这也是为什么值得花时间把技能打磨精致。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. food-order技能的设计蓝图:从需求到拆解
动手写代码之前,先把方案想清楚。我见过太多人上来就写SKILL.md,写到一半发现动作拆分不彻底,结果模型不知道该先查店铺还是先看库存。本节我把food-order的设计链路完整拆开。
2.1 点餐流程的核心链路
一个标准的点餐闭环,按数据流向可以分为六个阶段:
- 意图识别:模型判断用户是在点餐,还是在问天气。
- 槽位收集:确认店铺偏好、菜品、规格(大杯/中杯、辣度、温度)、数量和配送地址。
- 商家履约能力校验:店铺是否营业、当前时段是否在配送范围、菜品是否在售。
- 订单试算:计算商品金额、打包费、配送费、优惠券抵扣和预计送达时间。
- 用户确认:把订单明细给用户过一遍,确认后才会真正提交,避免误下单。
- 下单并回执:提交订单、获取订单号、推送状态。
设计技能时,这六个阶段不能混在一个函数里,否则模型不好编排。我在实践中的做法是拆成shop_search、menu_query、cart_create、price_calculate、order_confirm、order_submit六个动作,每个动作对应一个可调用的函数。模型按流程一步步走,每一步输出都可以被用户确认或被上层系统拦截,安全性高很多。
2.2 技能输入输出的协议设计
一个Skill要被反复使用,输入输出必须标准化。我以food-order为例,给出一个最小可用的JSON协议设计:
用户输入侧(模型从对话中抽取):
json复制{
"action": "menu_query",
"params": {
"shop_id": "10023",
"category": "coffee",
"keyword": "冰美式"
}
}
订单提交侧(技能脚本返回给模型确认):
json复制{
"action": "order_confirm",
"data": {
"shop_name": "示例咖啡店",
"items": [
{"name": "冰美式", "spec": "大杯", "qty": 2, "price": 15.0}
],
"package_fee": 0.5,
"delivery_fee": 3.0,
"discount": -5.0,
"total": 28.5,
"estimated_minutes": 32
}
}
设计协议时有三个原则值得记住:
- 字段名要稳定,不要今天叫total明天叫amount,否则模型会混乱。
- 金额一律用数字类型,不要用带"元"的字符串,方便计算。
- 每个action一定要有明确的成功和失败返回结构,失败时带上错误码,例如CLOSED/BUSY/STOCK_OUT,便于模型向用户解释并给出备选方案。
2.3 为什么选择API对接而不是前端自动化
做点餐技能有两条技术路线:一条是直接对接商家开放平台或外卖平台的API,另一条是模拟浏览器操作(前端自动化)去点按钮。我强烈推荐前者。
API对接的好处显而易见:稳定、合规、响应快,数据也是结构化的,不会有小程序的UI改版导致脚本报废的问题。前端自动化的成本初看很低,但实际运行时你会遇到登录态过期、验证码、元素选择器失效、页面加载延迟各种问题,维护成本远超想象。
当然,API对接的前提是平台开放了接口。如果你的场景里没有现成的开放API,退一步的方案是走"开放接口聚合层",找第三方聚合服务,或者和商家协商开通企业订餐接口。实在不行再考虑半自动兜底方案,但主链路一定不要依赖UI自动化。
3. 手把手实现一个可用的food-order技能
设计层面盘清楚了,下面进入实操。我会用openclaw的标准Skill目录结构来搭建,并给出一份可运行的Python脚本示例。
3.1 技能目录结构与文件清单
在openclaw的skills目录下,为food-order技能新建一个独立文件夹,推荐结构如下:
code复制food-order/
├── SKILL.md
├── main.py
├── config.json
├── requirements.txt
└── sample_orders.json
各文件职责:
- SKILL.md:技能说明书,大模型靠它决定"什么时候调用、怎么调用"。
- main.py:核心执行逻辑,实现查询、计算、下单等动作。
- config.json:配置文件,存API地址、密钥、默认配送地址等。
- requirements.txt:Python依赖清单。
- sample_orders.json:测试用样例数据,离线开发时非常有用。
3.2 编写SKILL.md:让openclaw理解这个技能的说明书
SKILL.md是技能的"门面",它的质量直接决定模型调用技能的准确率。写这份文档时,要像给一个新同事写交接文档,不能省略关键信息。
我给出一个精简但完整的示例:
markdown复制# Food Order Skill
## 描述
当用户想要点外卖、订购咖啡、预订餐厅餐品,或询问附近可送餐的店铺时,使用本技能。
包含菜品查询、优惠计算、下单确认和订单提交能力。
## 适用场景
- 用户说:帮我点一杯冰美式
- 用户说:中午吃点什么,40元以内
- 用户说:看看那家店现在还能不能配送
## 不适用场景
- 用户只是想查询餐厅评价,不下单
- 用户问某道菜的做法
## 动作列表
| 动作 | 说明 | 关键参数 |
| --- | --- | --- |
| shop_search | 搜索附近店铺 | keyword, lat, lng |
| menu_query | 查询店铺菜单 | shop_id, keyword |
| price_calculate | 计算订单金额 | items, address_id, coupon_id |
| order_confirm | 展示订单并等待确认 | order_data |
| order_submit | 提交订单 | confirm_token |
## 调用示例
用户:帮我点一杯大杯冰美式
Assistant调用:menu_query(shop_id="10023", keyword="冰美式")
写SKILL.md的几个心得:
- 描述部分不要写成一句空话,要具体列出可能触发技能的典型用户表达,这对模型意图识别帮助极大。
- 动作表格要精炼,参数名和实际代码保持一致,否则模型会传错字段。
- 不适用场景也要写,能够有效减少误调用。
3.3 编写Python脚本:真正的点餐逻辑
下面是一份简化版main.py,实现了菜单查询和价格试算的核心逻辑。生产环境中你只需要替换内部的HTTP请求为真实对接的平台API即可。
python复制import json
import hashlib
import time
from typing import Dict, List
class FoodOrderSkill:
def __init__(self, config_path="config.json"):
with open(config_path, "r", encoding="utf-8") as f:
self.config = json.load(f)
self.session_token = self._generate_token()
def _generate_token(self) -> str:
raw = f"{self.config.get('api_key', '')}{int(time.time() // 300)}"
return hashlib.md5(raw.encode()).hexdigest()
def shop_search(self, keyword: str, lat: float = None, lng: float = None) -> Dict:
# 真实实现中,这里应调用外卖平台的店铺搜索接口
# 本示例返回离线样例数据,便于本地调试
with open("sample_orders.json", "r", encoding="utf-8") as f:
data = json.load(f)
results = []
for shop in data["shops"]:
if keyword.lower() in shop["name"].lower() or keyword in shop.get("tags", []):
results.append(shop)
return {"status": "success", "shops": results[:5]}
def menu_query(self, shop_id: str, keyword: str = None) -> Dict:
with open("sample_orders.json", "r", encoding="utf-8") as f:
data = json.load(f)
shop = None
for item in data["shops"]:
if str(item["id"]) == str(shop_id):
shop = item
break
if not shop:
return {"status": "error", "code": "SHOP_NOT_FOUND", "message": "店铺不存在"}
if not shop.get("is_open"):
return {"status": "error", "code": "CLOSED", "message": "店铺当前未营业"}
menu = shop["menu"]
if keyword:
menu = [m for m in menu if keyword.lower() in m["name"].lower()]
return {"status": "success", "shop_name": shop["name"], "menu": menu}
def price_calculate(self, items: List[Dict], address_id: int, coupon_id: str = None) -> Dict:
with open("sample_orders.json", "r", encoding="utf-8") as f:
data = json.load(f)
address = None
for addr in data.get("addresses", []):
if addr["id"] == address_id:
address = addr
break
if not address:
return {"status": "error", "code": "ADDRESS_NOT_FOUND", "message": "配送地址不存在"}
item_total = 0.0
for it in items:
item_total += it["price"] * it["qty"]
package_fee = 0.5
delivery_fee = 3.0 if item_total < 20 else 0.0
discount = 0.0
if coupon_id:
for cp in data.get("coupons", []):
if cp["id"] == coupon_id and item_total >= cp.get("threshold", 0):
discount = cp["amount"]
break
total = round(item_total + package_fee + delivery_fee - discount, 2)
return {
"status": "success",
"item_total": round(item_total, 2),
"package_fee": package_fee,
"delivery_fee": delivery_fee,
"discount": discount,
"total": total,
"eta_minutes": 32,
}
def order_confirm(self, order_data: Dict) -> Dict:
# 将订单明细格式化返回给模型,由模型推送用户确认
lines = []
for it in order_data["items"]:
spec = f"({it['spec']})" if it.get("spec") else ""
lines.append(f"- {it['name']}{spec} x{it['qty']} 小计{round(it['price'] * it['qty'], 2)}元")
message = "\n".join(lines)
total = order_data.get("total")
eta = order_data.get("eta_minutes")
return {
"status": "success",
"confirm_message": f"订单明细:\n{message}\n配送费{order_data.get('delivery_fee')}元,"
f"打包费{order_data.get('package_fee')}元,优惠{order_data.get('discount')}元\n"
f"合计{total}元,预计{eta}分钟送达。请确认是否下单?",
"confirm_token": hashlib.md5(f"{total}{self.session_token}{time.time()}".encode()).hexdigest(),
}
def order_submit(self, confirm_token: str) -> Dict:
# 生产环境应校验token并向支付/下单系统提交
order_id = "ORD" + str(int(time.time() * 1000))
return {"status": "success", "order_id": order_id, "message": "下单成功"}
def action_handler(action: str, params: Dict) -> Dict:
skill = FoodOrderSkill()
handlers = {
"shop_search": skill.shop_search,
"menu_query": skill.menu_query,
"price_calculate": skill.price_calculate,
"order_confirm": skill.order_confirm,
"order_submit": skill.order_submit,
}
func = handlers.get(action)
if not func:
return {"status": "error", "code": "ACTION_NOT_SUPPORTED", "message": f"不支持的action: {action}"}
return func(**params)
if __name__ == "__main__":
test_result = action_handler("menu_query", {"shop_id": "10023", "keyword": "冰美式"})
print(json.dumps(test_result, ensure_ascii=False, indent=2))
代码里的要点:
action_handler是openclaw调用脚本的统一入口,所有动作都从这里分发,保持对外交互协议一致。- 样例数据放在sample_orders.json,离线也能调试,等联调真实API时只改对应函数内部逻辑。
- 金额计算逻辑里,满20免配送费、优惠券门槛这两点很容易遗漏,我在测试时就被坑过,后面专门展开说。
3.4 本地测试与调试方法
技能写完先在本地跑通,不要急着接入openclaw。我的调试流程是:
- 直接执行
python main.py,确认基础逻辑正常,样例数据能查到店铺、能算出正确总价。 - 手动多测几个边界:店铺打烊、商品下架、优惠券门槛不满足、地址不存在。
- 在openclaw的配置里加载该技能,发起一次真实对话,观察模型是否精准调用了
shop_search和menu_query。 - 如果模型没有触发技能,回去看SKILL.md的描述和示例,往往是触发词写得太少或者意图描述不够清晰。
本地测试阶段多花半小时,能为后面联调省下大量时间。特别是金额、优惠这类和钱相关的逻辑,离线环境可以放心地反复改反复跑,不用怕误下单。
4. 实操中的踩坑记录与排查链路
这个环节是全文最值钱的部分,我挑了三个真实遇到的问题,把完整的排查过程写出来。每个坑都真实发生过,它们也是openclaw社区里讨论度比较高的问题。
4.1 问题一:点餐请求一直返回"商家不支持"
表现:用户点了"帮我点一份黄焖鸡米饭",openclaw调用了技能,但返回结果一直是"商家不支持当前菜品"。换了几家店都不行,看起来很诡异。
排查链路:
- 先确认是不是模型把参数传错了。我查看openclaw的会话日志,发现模型传给
menu_query的keyword是"黄焖鸡米饭",这没问题。 - 再检查脚本逻辑,发现
menu_query里对菜单做了精确匹配,但真实菜单数据中这家店的菜品名是"黄焖鸡米饭(微辣)",包含括号说明。模型传的关键词是完整名称,精确匹配等式两边对不上,自然返回空。 - 修复方案是把精确匹配改成包含匹配:
if keyword.lower() in m["name"].lower(),并且在搜索店铺时也把tags考虑进去。
这个坑的核心教训是:模型抽取关键词时通常会保留用户的完整表述,但店铺菜单是半结构化文本,菜品名有各种括号、别名,必须用包含匹配而不是全等匹配。
4.2 问题二:金额计算偏差,优惠券没有被正确使用
表现:用户有一张满30减8的优惠券,点了两杯咖啡共36元,按理说应该减8元,但技能算出来的结果没减。
排查链路:
- 先看
price_calculate里优惠券的匹配逻辑。代码里判断的是item_total >= cp.get("threshold", 0),看起来没问题。 - 再打印coupon_id,发现模型调用的优惠券ID是空字符串,而配置文件里的优惠券ID是"COUPON_30_8",对不上。
- 为什么模型没有传用户提到的优惠券ID?回去看SKILL.md,发现动作表格里
price_calculate说明是"计算订单金额",参数列只写了items、address_id、coupon_id,但没有明确提示"需要从用户对话中提取优惠券信息并映射成coupon_id"。 - 修复方案:SKILL.md加一行说明:"当用户提及‘满减券’‘优惠券’等信息时,应从配置表匹配对应的coupon_id后传入,若无匹配则置空。"
这个坑说明,技能的"槽位抽取"不仅靠模型,也靠说明书引导。模型不是不会用优惠券,而是你不知道它需要你把"用户口语化的优惠券描述"翻译成"系统里的券ID"这个规则写清楚。
4.3 问题三:openclaw切换模型后,技能prompt失效
表现:起初用的模型版本对SKILL.md理解得很好,技能调用很稳定。后来在openclaw后台切换了一个新模型,发现同样的对话,模型完全不再调用food-order技能,而是直接回答"我无法帮你点餐"。
排查链路:
- 第一反应是模型本身不支持工具调用。检查openclaw配置,确认新模型确实支持function calling。
- 再次查看SKILL.md,发现它开头第一行是"# Food Order Skill",但整体格式很长。某些模型对超长markdown的解析能力不同,可能没有抓住关键动作列表。
- 尝试精简SKILL.md:把动作表格放在靠前位置,删掉冗余的示例说明,把"何时调用"的描述压缩到前20行内。
- 精简后重新测试,模型又恢复调用了。
这个问题的深层原因是不同模型对上下文的注意力分布不同。有的模型喜欢看末尾的示例,有的更关注前几行。为了兼容性,SKILL.md的"核心调用说明"一定要在文档前20%的篇幅内交代完毕,不要把重要内容藏在很长的文字后面。
4.4 排查方法论归纳
综合以上三个问题,我总结了一套顺手的排查顺序:
- 第一查日志:openclaw会话日志会显示模型call了哪些action、传了什么参数,绝大多数问题在这一步就能定位。
- 第二查参数:确认技能内部是否真的收到了预期参数,关键字段有没有被模型误传或漏传。
- 第三查数据:检查样例数据或真实API返回的数据格式,确认是否存在字段名不匹配、编码问题、空值。
- 第四查说明书:如果逻辑本身没问题但模型不调用,返回去改SKILL.md,精简、前置、加示例。
这套顺序走下来,绝大多数技能问题都能在15分钟内定位,不用瞎猜。
5. 让food-order技能变得更实用的几个进阶方向
基础版本的food-order技能已经能完成从对话到订单的闭环,但在实际使用中,我发现要让它真正成为"常用技能"而不是"演示玩具",还需要在几个方向上做优化。
5.1 多商家聚合与动态选择
单店点位餐价值有限,用户真正需要的是"帮我从附近所有可送餐的店里选一个合适的"。进阶做法是:
- 在
shop_search阶段,不仅按关键词搜店,还按距离、评分、人均价格、预计送达时间做多条件排序。 - 返回结果时,给模型提供每个店铺的2-3个关键推荐理由,比如"评分最高""配送最快""预估总价最低",方便模型向用户解释。
- 用户确认店铺后,再加载菜单,避免一开始就把所有店的全部菜品灌给模型,浪费token。
这个阶段有一个很容易踩的优化误区:把搜索结果一股脑全返回给模型。实际上模型一次能消化的店铺数量有限,最好先过滤到3-5个候选,再让模型做对比推荐。
5.2 与日程/偏好记忆联动
点餐的时间和用户偏好强相关,比如"下午三点"通常意味着下午茶,"加班到晚上九点"可能想吃重口味。一个真正聪明的food-order技能,应该能结合openclaw的记忆系统做个性化推荐。
实现思路是:
- 在SKILL.md里增加一个"个性化上下文"段落,说明可以读取用户的历史点餐偏好。
- 在
shop_search之前,先调用一个load_user_profile动作,拉取用户历史订单分析和口味标签。 - 把用户偏好作为参数拼接进搜索请求,比如偏好"少油少盐"的用户,搜索结果里自动过滤炸鸡类店铺。
我测试下来,加了这个联动之后,用户对推荐结果的满意度高了很多,因为模型给出的不只是"附近有什么",而是"你喜欢的口味里现在有什么可选的"。
5.3 语音点餐与定时任务
openclaw本身支持多种交互入口,food-order技能完全可以接入语音输入和定时触发。语音场景里,用户经常会说出非常口语化的需求,比如"那家上次喝的咖啡店,给我来个大杯的",这时候技能要配合会话记忆,解析出店铺指代和去年的订单规格。
定时任务的场景更适合预约送餐。我做过一个简单实现:在订单确认后,把任务写入一个定时队列,到点调用order_submit。这里的坑在于,平台侧的营业时间可能会在深夜发生变化,所以提交前要二次校验店铺状态,不能拿用户确认时的状态当最终状态。
5.4 容错与人工兜底机制
无论技能做得再完善,线下环节总有失控的可能:商家突然接不了单、骑手不够、库存告急。设计技能时要默认这些情况会发生,并准备好兜底方案。
我的做法是:
- 技能内置重试机制,当
order_submit返回临时性错误(如BUSY、NETWORK_TIMEOUT)时,自动重试最多三次,间隔指数退避。 - 重试仍失败时,返回给模型一个明确的错误状态,并阻止模型擅自换店下单。这一步非常关键,避免AI在用户不知情的情况下改变订单内容。
- 订单提交后,技能要主动查询一次订单状态,确认是"已接单"还是"待支付",把真实状态回传给用户。
餐品、金额、地址任何一个环节出问题,用户都会直接受到影响,所以宁可保守,也不要让AI自作主张。这是我调试技能过程中最想强调的一个原则。
我的最终体会
把food-order做成一个稳定可用的openclaw技能,最耗时的不是写代码,而是调SKILL.md和模型之间的"默契"。模型能不能准确识别意图、正确填槽,全靠说明书怎么引导。开始之前,一定要先把你想支持的每一种点餐说法都整理成触发示例;上线之后,把每次调用成功或失败的对话都存下来,定期回去改说明书。这套方法不仅在点餐技能上有效,换成其他Skill同样适用。如果你也在做类似技能,欢迎按这个思路试一轮,我猜你也会遇到过几个我没有列出来的坑。
