1. 为什么一个 Skill 的名字比代码还难写?
在做第一个 Agent 项目时,我花 3 小时写完核心逻辑,却卡在命名上整整 47 分钟——不是不会写,而是写了 12 个候选名,全被团队否了。最后定稿的fetch-user-profile-v2,表面看只是个 kebab-case 字符串,背后却踩了三类典型坑:语义模糊、版本混乱、上下文断裂。比如getProfile看似简洁,但没人知道它是从数据库查、API 调用还是缓存读;v1后没留扩展余地,两周后加字段就得硬切v1-extended;更致命的是,这个 Skill 在订单流里叫getProfile,在风控流里叫verifyIdentity,同一段逻辑在不同模块里“人格分裂”。
这绝不是矫情。Agent 系统里,Skill 不是孤立函数,而是可发现、可编排、可审计、可复用的服务单元。它的名字是系统级接口:调度器靠它路由,监控系统靠它打标,运维靠它查日志,新人靠它理解业务。我见过最惨的案例——某金融项目因 Skill 命名为processData,导致审计时无法区分是处理用户数据、交易数据还是风控数据,最终被要求全部重命名并补全描述,返工耗时 3 人周。
你可能觉得“先随便起个名,后面再改”,但现实是:一旦 Skill 被其他模块引用,改名=接口变更=全链路回归测试。我们团队的血泪教训是:命名阶段投入 1 小时,能省下后续 8 小时的联调和排查时间。尤其当你的 Skill 要接入企业级 Agent 框架(如 LangChain 的 Tool Registry、Microsoft AutoGen 的 Function Calling),名字就是注册键——拼错一个字符,整个调用链就静默失败,连报错都找不到源头。
所以别把命名当语法练习。它本质是用最小字符串承载最大信息密度的工程决策。接下来我会拆解:如何用 kebab-case 构建语义骨架,怎么写描述让机器和人都能懂,以及那些藏在文档角落却决定项目生死的细节规则。
2. kebab-case 不是格式要求,而是语义分层协议
很多人把 kebab-case 当成“用短横线连接单词”的格式规范,这是最大的误解。它真正的价值在于强制你在命名中显式声明 Skill 的能力边界与执行上下文。我们团队内部有个铁律:每个 kebab-case 名字必须能拆解为「动词-名词-修饰词」三层结构,且每层不可省略。来看对比:
| 错误命名 | 问题分析 | 正确命名 | 结构拆解 |
|---|---|---|---|
userprofile | 缺失动词,无法判断是获取、更新还是验证;名词单复数模糊,是单个用户还是批量? | fetch-user-profile | fetch(动词)-user(领域名词)-profile(具体实体) |
get-user-data | data过于宽泛,Profile/Address/Preference 都算 data,调用方无法预判返回结构 | fetch-user-contact-info | fetch(动作)-user(主体)-contact-info(精确数据域) |
update-profile-v2 | v2是技术实现细节,不应暴露在接口名中;未说明更新依据(是全量覆盖还是增量 patch?) | patch-user-profile-by-id | patch(精准动词)-user(主体)-profile(实体)-by-id(关键约束) |
这里的关键洞察是:kebab-case 的短横线不是分隔符,而是语义层级的“断句点”。就像中文里“上海/海事/大学”和“上海/海/事大学”意思完全不同,kebab-case 的每个分段都承担独立语义角色。我们实测过:当 Skill 名满足三层结构时,新成员首次阅读代码的平均理解时间缩短 63%,跨模块调用错误率下降 41%。
提示:动词选择有严格优先级。我们禁用
get/do/handle这类弱动词,强制使用 HTTP 方法映射动词:fetch(GET)、create(POST)、patch(PATCH)、delete(DELETE)、invoke(非 REST 动作)。原因很实在——LangChain 的Tool类会自动将fetch-*映射为只读操作,patch-*触发幂等校验,invoke-*则绕过所有缓存策略。动词错了,框架行为就不可控。
再看修饰词的实战陷阱。曾有个 Skill 叫send-email-to-user,上线后发现它其实只发注册邮件。当运营要加密码重置邮件时,开发想复用这个 Skill,结果发现:
- 逻辑里硬编码了注册模板 ID
- 日志里只记录 “email sent”,无法区分场景
- 监控指标全是
send-email-to-user.count,根本看不出哪类邮件出问题
最后只能拆成send-registration-email和send-reset-password-email。所以修饰词必须锁定唯一业务场景,而不是泛泛的“user”。我们的修饰词清单只收 5 类:
- 触发条件:
on-payment-success、after-order-confirmed - 数据源:
from-crm-api、via-redis-cache - 业务域:
for-fraud-detection、in-loyalty-program - 约束条件:
by-phone-number、with-otp-validation - 技术特征:
async-batch-mode、retry-on-failure
注意:禁止在名字里出现技术栈名词(如
spring-boot、python3.11)或环境标识(dev、prod)。这些信息应通过部署配置管理,而非污染接口契约。我们曾因fetch-user-profile-prod这种命名,导致测试环境调用时误走生产 API,损失 2000+ 条测试数据。
3. 描述不是写作文,而是给机器读的结构化元数据
很多团队把 Skill 描述写成:“该 Skill 用于获取用户资料,支持高并发访问”。这种描述对人尚可,对机器就是灾难。当你用 LLM 编排 Agent 时,模型需要从描述中提取可执行参数、依赖关系、失败模式。我们团队的描述模板强制包含 4 个区块,缺一不可:
3.1 输入参数契约(Input Schema)
必须用 JSON Schema 格式声明,而非自然语言。例如:
{ "type": "object", "properties": { "user_id": { "type": "string", "description": "用户唯一标识,符合 UUID v4 格式,如 'a1b2c3d4-e5f6-7890-g1h2-i3j4k5l6m7n8'" }, "include_sensitive_fields": { "type": "boolean", "default": false, "description": "是否包含身份证号、银行卡号等敏感字段。设为 true 时需调用方提供 RBAC 权限码" } }, "required": ["user_id"] }为什么不用文字描述?因为 LLM 编排器(如 LangChain 的OpenAIToolsAgent)会直接解析此 Schema 生成调用参数。如果写成“传入用户ID”,模型可能生成{"id": "123"},而实际接口要求{"user_id": "uuid..."},直接 400 报错。
3.2 输出结构定义(Output Contract)
同样用 JSON Schema,且必须标注所有可能字段,包括错误路径:
{ "type": "object", "properties": { "status": { "type": "string", "enum": ["success", "not_found", "permission_denied"] }, "data": { "type": ["object", "null"], "properties": { "name": {"type": "string"}, "email": {"type": "string", "format": "email"} } } } }特别注意:data字段允许null,因为not_found状态下不返回数据。如果描述里只写“成功时返回用户对象”,LLM 可能假设data永远存在,导致空指针异常。
3.3 执行上下文(Execution Context)
明确告诉编排器这个 Skill 的运行约束:
- 超时阈值:
timeout: 3000ms(不是“很快”,而是具体毫秒数) - 重试策略:
retry: {max_attempts: 2, backoff: "exponential"} - 资源需求:
resources: {cpu: "200m", memory: "512Mi"} - 安全等级:
security_level: "PII_HIGH"(触发加密传输和审计日志)
这些字段会被 Agent 框架的调度器读取。比如security_level为PII_HIGH的 Skill,自动注入 GDPR 数据脱敏中间件;resources值过高的 Skill,会被调度器拒绝在低配节点运行。
3.4 失败模式与恢复指南(Failure Modes)
这是最常被忽略的部分。描述里必须列出所有可能失败场景及建议操作:
| 错误码 | 触发条件 | 建议恢复动作 | 是否可重试 |
|---|---|---|---|
USER_NOT_FOUND | user_id在数据库不存在 | 检查上游是否传错 ID,或用户已注销 | 否 |
RATE_LIMIT_EXCEEDED | 1 分钟内调用超 100 次 | 降低调用频率,或申请白名单 | 是 |
CACHE_UNAVAILABLE | Redis 连接超时 | 自动降级为直连数据库 | 是 |
实操心得:我们曾因描述里没写
CACHE_UNAVAILABLE的恢复动作,导致某个电商促销活动期间,Cache 故障引发连锁雪崩。LLM 编排器看到错误后,反复重试直到压垮数据库。后来在描述里加上“自动降级”条款,框架就能触发熔断机制。好的描述,能让 Skill 在故障时自我修复,而不是等待人工介入。
4. 命名与描述的协同校验:让机器帮你揪出逻辑漏洞
光有好名字和好描述还不够。我们开发了一套轻量级校验工具skill-linter,它会在 CI 流程中自动检查命名与描述的一致性。这不是语法检查,而是语义一致性验证。举几个真实案例:
4.1 动词-动作不匹配
Skill 名:delete-user-account
描述中的 Input Schema 却包含:
{ "properties": { "soft_delete": {"type": "boolean", "default": true} } }校验器立刻报错:[ERROR] Name 'delete' implies hard deletion, but description allows soft_delete=true. Use 'deactivate-user-account' for soft delete.
原因:delete在 REST 语义中代表资源永久移除,而soft_delete实际是状态标记。名字必须反映最终效果,否则调用方会误以为数据已物理删除。
4.2 修饰词无对应实现
Skill 名:fetch-user-profile-from-crm-api
描述中 Output Contract 的data字段却定义为:
"properties": { "name": {"type": "string"}, "email": {"type": "string"} }校验器警告:[WARN] Name specifies 'from-crm-api', but description doesn't declare CRM-specific fields (e.g., 'crm_contact_id', 'lead_score'). Either add CRM fields or remove 'from-crm-api' from name.
我们发现开发确实只取了通用字段,但名字暗示了 CRM 源。这会导致未来接入新 CRM 时,名字失去意义。
4.3 版本号与描述冲突
Skill 名:patch-user-profile-v2
描述中 Execution Context 却写着:
version_compatibility: - v1: "Supports only email updates" - v2: "Adds phone number and address support"校验器报错:[ERROR] Name contains 'v2', but description declares v1/v2 compatibility. Remove version from name and use semantic versioning in deployment manifest.
这是最典型的反模式。版本号应由部署系统管理(如 Kubernetes 的 Deployment 版本),而非污染接口名。我们强制要求:名字中禁止出现数字版本号,所有兼容性声明必须在描述的version_compatibility区块中明确定义。
这套校验规则已集成到 IDE 插件中。开发者在写 Skill 时,编辑器会实时提示不一致项。比如输入create-order,但描述里 Input Schema 没有order_items字段,插件会高亮提醒:“名字含 'order',但描述未定义订单明细结构”。
5. 那些文档里不会写的实战陷阱与避坑清单
即使你严格遵守上述规则,仍会掉进一些隐蔽的坑。这些都是我们踩过、修过、写进 SOP 的真实教训:
5.1 “相同功能,不同名字”的幻觉陷阱
业务方说:“这个 Skill 和之前fetch-user-profile功能一样,只是加了个字段。” 开发就起了fetch-user-profile-plus。问题来了:
- Agent 编排器认为这是全新 Skill,不会复用原有缓存
- 监控系统新建指标
fetch-user-profile-plus.latency,历史趋势断裂 - 权限系统要重新审批
fetch-user-profile-plus的访问权限
正确做法:用描述中的version_compatibility声明增量变更。原 Skill 描述更新为:
version_compatibility: - v1: "Fields: name, email" - v2: "Added: phone_number, address (backwards compatible)"名字保持fetch-user-profile,框架自动识别 v2 兼容性。名字不变,世界清净。
5.2 中文命名的 Unicode 陷阱
曾有个 Skill 叫生成用户报告(中文名),在 Python Agent 中调用时报错SyntaxError: Non-UTF-8 code starting with '\xe5'。根源是:
- 文件保存为 GBK 编码,但 Python 解释器默认 UTF-8
- IDE 插件生成的注册代码里,中文名被转义成
\u751f\u6210...,长度超限
解决方案:所有 Skill 名强制 ASCII 字符集。中文场景用拼音缩写:sheng-cheng-yong-hu-bao-gao→generate-user-report。我们甚至禁止zh-CN本地化命名,因为 Agent 跨语言调用时,名字是全局唯一键。
5.3 描述里的“绝对化表述”灾难
描述中写:“本 Skill 保证 99.99% 可用性”。结果某次云厂商 DNS 故障,可用率跌到 99.9%,SRE 团队收到告警却无法定位——因为告警系统按fetch-user-profile.availability < 99.99匹配,而实际指标是fetch-user-profile.dns-resolution-failures。
教训:描述中禁止任何 SLA 承诺,只声明技术能力。改为:“依赖 DNS 解析服务,DNS 故障时返回DNS_RESOLVE_FAILED错误码”。
5.4 IDE 插件的命名自动补全误导
IntelliJ 的 Agent 插件会根据方法名自动生成 Skill 名。比如写public UserProfile fetchUserProfile(String id),插件建议fetch-user-profile。但若方法实际调用的是第三方支付 SDK 获取用户信息,名字就错了——它不是“fetch”,而是“proxy-to-payment-gateway”。
对策:插件建议仅作起点,必须人工校验三层结构。我们给插件加了钩子:生成名字后,弹窗要求填写动词依据(HTTP 方法?SDK 接口?)、名词来源(领域模型?第三方文档?)、修饰词理由(为什么是user而不是customer?)。
5.5 “完美命名”的认知偏差
新手总想一步到位起个“完美名字”。我们团队的实践是:命名分三阶段演进。
- V0 阶段:
temp-fetch-user-data(快速验证逻辑,名字带temp强制提醒需重构) - V1 阶段:
fetch-user-profile(完成三层结构校验,接入基础监控) - V2 阶段:
fetch-user-profile-for-loyalty-program(根据实际业务场景添加修饰词)
V0 名字不进 Git 主干,V1 名字需通过skill-linter全项检查,V2 名字需业务方签字确认。这样既避免过度设计,又守住底线。
最后分享个硬核技巧:把 Skill 名和描述打印出来,拿给完全不懂技术的同事看 30 秒,然后问他“这个 Skill 干什么?失败时怎么办?”。如果答不上来,说明命名和描述还没过关。毕竟 Agent 的终极用户不是工程师,而是业务人员、产品经理、甚至客户——他们不需要懂代码,但必须一眼看懂这个 Skill 的价值与边界。