本文作者:落秋枫,TRAE 开发者用户
概述
名词解释
- SKILL: 最初是 Claude AI 的一项功能,允许用户将复杂的、多步骤的流程(例如数据清洗、内容格式化、特定分析等)保存为可重复使用的自定义“技能”,用来提升在某些特定任务上的表现。更多理解欢迎阅读 一文读懂 Skills|从概念到实操的完整指南
-
OneService(后写作 OS): 由字节团队开发的数据服务化平台。帮助用户将各种主流数据源的 SQL 查询快速服务化,提供 API 创建、管理、运维和共享的全生命周期管理能力
-
PSM: 面向字节内部使用的公司服务唯一标识
项目背景
日常服务端开发的同学,「在列表页里新增数据指标」是一类很常见的需求,但在复杂的项目当中,这些指标的数据来源往往很分散,来自不同数据库。在过去,我们要拿到这些数据,通常会用两种比较 “笨重” 的方式:
-
一种是去调用已经封装好的各个 RPC 接口,接口所在的服务连接了各个数据库
-
另一种是直接在当前服务里连接到具体的数据库,去查原始的底层表
后来我们用上了 OS 这个工具,它可以把各种数据库查询快速包装成 API 服务,让我们跨数据源取数据的工作变得轻量化了不少
但新的问题又出现了:
如果新增的指标数量较多,且来自不同数据源,意味着需要新增多个 OS API,这些 API 的调用逻辑高度相似,只是 API ID、入参和出参略有不同
这种机械重复的开发,恰好暴露了我们团队在 OS API 调用上长期存在的痛点。
所面临的痛点
团队的多数 PSM 当中,针对 OS API 的调用,存在以下诸多痛点问题
-
新增效率低下: 新增需要重复编写高度相似的 API 调用代码,开发效率低且易出错
-
调用方式不统一: 不同的开发者,调用 OS 的 SDK,使用方法/参数命名/传参方式定义各不相同,后续维护困难
-
重复开发/接入 : 同一个 API ID 被不同人写了多份方法,逻辑分散、难以收敛
-
难以查找与复用 : 针对项目里已有的 API 调用,没有接口描述和出入参备注,重度依赖开发者的个人注释习惯
-
命名与文件组织混乱 : 调用方法命名随意、文件散落,导致同类能力难聚合、难形成约定俗成的目录结构
为什么选择 SKILL 功能?
一句话定义 SKILL:
是一种结构化的、可复用的能力单元,它告诉 Agent 在特定场景下,应该遵循怎样的流程(SOP)来完成一个具体任务
1. 提高增量开发效率:新增 OS API 调用,只需输入 Prompt,TRAE 一键搞定
- 针对已经导入 OS 的 PSM,SKILL 提供完整的 OS API 调用方法的生成能力
2. 降低接入成本:把“接入 OS API”从流程化
- 针对一个新的 PSM,对于 OS 的 SDK 引入、Client 初始化、封装 QueryWithParams/SqlQuery 等基础设施操作,SKILL 可以一键完成,并符合统一初始化规范
3. 避免重复与冲突:用 API ID 做统一索引
- OS 的唯一标识是 API ID,同一个 API 在代码里被多处各写一份非常常见,SKILL 强制先在 api_description.md 里按 API ID 查重,再决定复用还是新增,减少重复代码和逻辑漂移
4. 统一产出标准:方法命名、入参出参、SQL、日志、错误处理、注释风格一致 SKILL
业务里常见的混乱是:
- 有人用原生 SDK,有人用封装方法;有人返回 list,有人聚合成 map
- 命名随意、注释不规范导致后续使用/维护困难
SKILL 通过 examples.md 固化两种范式,让生成代码符合 OS 官方 SDK 调用规范
5. 提升可维护性与可追溯性:让“这个 API 干嘛的、在哪用、参数是什么”一眼可查
过去的业务场景中,想要了解某个 API 的相关信息:
- 强依赖注释是否规范
- 若注释较少,只能寻找 Owner 或通过 API ID 去 OS 平台查询
SKILL 可以通过 api_description.md 把 API 的相关信息沉淀下来,相当于轻量内部文档,后续排查和复用成本大幅降低
使用规范
为提高 AI 生成代码的准确性,SKILL 当中严格规定了生成代码的规范
使用当前 SKILL,我们需要对以下规范进行约定
1. OS 的依赖库: xxx/xxx/sqlclient
2. 初始化和生成文件形式: TRAE 使用 SKILL 生成代码会严格按照以下结构生成
Project Name
└── infra
└── one_service/
├── client.go
└── get_xxx_xxx.go
-
infra/one_service/client.go:存放初始化方法
InitOneService:初始化 OS 的 Client,提供 SDK 调用
SqlQuery: 封装原生 SDK 提供的SqlQuery方法,添加日志记录
QueryWithParam: 封装原生 SDK 提供的QueryWithParam方法,添加日志记录
-
infra/one_service/get_xxx_xxx.go: TRAE 使用 SKILL 生成的 OS API 调用文件都会存放在infra/one_service目录下,文件名会根据 API 名称 AI 自行拟定
开始实践
-
更新 TRAE 到版本 3.3.21 以上
-
设置--规则和技能--创建技能
- 上传 SKILL.md 文件或者包含 SKILL.md 和其他相关配置文件的压缩包
TRAE 目前针对SKILL功能支持两种一键上传的方式
- 直接上传SKILL.md文件
- 上传根目录下带有SKILL.md文件的压缩包,这个压缩包当中除了包含SKILL.md文件,还可能包含其他目录和配置文件
最终这个文件/压缩包都会被统一放在.trae/skills目录下
- 新建任务,输入 Prompt ,使用 SKILL
// 方式一:输入请求参数和返回参数,调用QueryWithParam
我需要生成一个OneService API调用方法
API ID:xxxxxxxxx
名称:查询离线广告消耗指标
请求参数
...
返回参数
...
-----------------------------------------------------------
// 方式二:输入SQL,调用SqlQuery
我需要生成一个OneService API调用方法
API ID:xxxxxxxxx
名称:查询离线广告消耗指标
SQL:...
成果展示
未初始化 OS
使用未初始化 OS 的 psm 进行测试
生成结果:初始化+接口描述+新API调用
-
infra 目录下,新增one_service目录
-
导入OS 依赖库,新增client.go初始化文件,infra/init.go当中调用 OS 初始化方法
-
infra/one_service目录下新增文件,其中包含所需 API 的调用方法
-
api.description.md当中新增接口描述
已初始化 OS
使用已初始化 OS 的 psm 进行测试
生成结果:跳过初始化,只生成接口描述+新 API 调用
-
infra/one_service目录下新增文件,其中包含所需 API 的调用方法
-
api.description.md当中新增接口描述
SKILL 设计思路
了解了具体的使用步骤之后,更关键的地方是
- 为什么这个 SKILL 要这么设计?
- 它的思路和组成结构是什么?
下面我们从设计思路、分层架构和具体SKILL文件编写几个方面,阐述这套SKILL的核心设计逻辑
设计思路
树状结构体
.TRAE/
└── SKILLs/
└── generateosmethod/
├── api_description.md
├── examples.md
└── SKILL.md
核心思路
把“生成 OS API 调用代码”这件事拆成三层:
- 规范/流程
- 可检索资产
- 可复制模板
从而保证生成结果一致、可复用、可治理
1. 用 SKILL.md 做“流程编排器”(强约束,防跑偏)
-
SKILL.md 定义了前置条件(工程里必须已引入 SDK、初始化 Client、提供 Query/SqlQuery 封装)
-
核心目的: 避免重复造轮子、避免生成与仓库现状不一致的调用方式(如果当前PSM已有 infra/oneservice/client.go 的封装形式,就应沿用)
2. 用 api_description.md 做“注册表/索引”(让生成具备记忆与可检索性)
api_description.md 不是给编译器看的,而是给“人+工具”看的:记录每个 API ID 对应的用途、SQL、入参/出参表格、以及生成到哪个 go 文件
它承担两个关键职责:
-
去重与复用入口 : 生成前先在这里按 API ID 搜索,避免仓库里已经存在同 ID 的方法还重复生成。
-
知识沉淀 : 后续任何人要查“某个 OS API 干什么、在哪个文件、参数是什么”,不用去 os 平台或者看源代码,直接看描述文件即可
3. 用 examples.md 做“代码模板库”(统一代码风格与调用姿势)
examples.md 给出两种标准范式:
- 方式一:用户提供的是“参数化查询”,用 QueryWithParams
- 方式二:用户提供的是“SQL 字符串”,用 SqlQuery
生成代码时对齐示例的导包、日志、返回聚合方式等,保证整个 repo 的 OS 调用“长得一样”,降低维护成本
4. 与业务仓库结构解耦:SKILL 只管“怎么生成”,代码落在 infra/oneservice
-
SKILL 目录里不放业务 go 代码,只放“规则与模板”;真正生成的调用方法落到仓库既有的 OS 基建目录(统一是 infra 目录下的 OS 相关目录 )
-
核心目的:让 SKILL 可以跨项目复用, 不同 PSM 只要 infra 侧初始化方式略有不同,按前置条件对齐即可复用同一套生成流程
SKILL.md
内容简介:SKILL 的流程以及规则设定
-
前置条件: 检查当前服务是否引入 OS 的 SDK 并初始化,若未引入,负责生成
-
规定: 设置当前 SKILL 的使用时机,以及生成规范
-
流程: 设置当前 SKILL 的执行链路,是渐进式披露的核心
---
name: GenerateOSMethod
description: 生成OnsService API调用方法
---
## 前置条件
#### 1.检查当前项目的infra目录中是否存在OneService的相关目录,并引入OneService的SDK并初始化好Client
#### 2.若当前项目未引入OneService的SDK或未初始化Client,请先引入并初始化好Client,按照以下步骤:
- 导入OneService的工具库,终端输入`go get xxx/xxx/sqlclient`
- 在infra目录下创建one_service目录,用于存放OneService的相关代码,包括Client的初始化代码
- 在one_service目录下创建client.go文件,用于初始化OneService的Client,代码示例如下:
```go
package one_service
import (
"xxx/xxx/sqlclient"
"xxx/xxx/logs"
"xxx/xxx/utils"
"context"
"sync"
)
var Client *sqlclient.SQLClient
var once sync.Once
func InitOneService() {
if Client != nil {
return
}
once.Do(func() {
// init client
var err error
Client, err = sqlclient.NewSqlClient()
if err != nil {
panic(err)
}
})
}
func SqlQuery(ctx context.Context, id string, sql string, res interface{}) error {
err := Client.SqlQuery(ctx, id, sql, res)
if err != nil {
logs.CtxError(ctx, "[SqlQuery] query sql failed.id:%s,sql:%s,err:%s", id, sql, err.Error())
return err
}
return nil
}
func QueryWithParam(ctx context.Context, id string, param map[string]interface{}, res interface{}) error {
err := Client.QueryWithParams(ctx, id, param, res)
if err != nil {
logs.CtxError(ctx, "[QueryWithParam] query sql failed.id:%s,param:%s,err:%s", id, utils.JsonMarshal(ctx, param), err.Error())
return err
}
return nil
}
```
- 将InitOneService方法添加到infra目录下的init.go文件中,确保Client在项目启动时就被初始化好
- 如果当前项目的infra目录下已经存在OneService的相关目录,并有了初始化好的Client,检查是否有SqlQuery或QueryWithParam方法,没有则新增
## 规定
#### 1.你只有在以下情况下才需要使用GenerateOSMethod SKILL,使用前先检查前置条件
- 用户提到需要使用/新增OnsService接口
- 用户提供的代码生成链路文档、技术方案文档当中提到需要使用/新增OnsService接口
#### 2.你必须严格按照SKILL.md文件中规定的【流程】进行操作,不能偏离流程,只能在流程规定的阶段生成代码
#### 3.生成的API描述,参考api_description.md文件中的示例,保存到api_description.md文件中
#### 4.新增一个go文件,存放到infra/one_service目录(或者已有的infra下的其他OneService相关目录)下,新增的代码(API ID,调用方法,SQL语句,请求参数,响应参数)都存放到新增的go文件中,参考examples.md文件中的示例
---
## 流程
### 1. 检查用户是否输入了需要调用的OnsService的API ID
检查用户输入内容和文档当中是否明确指出了需要调用的OnsService的API ID
如果没有明确列出,请让用户输入确认需要调用的OnsService的API ID
### 2. 遍历当前已有OnsService API调用方法
在api_description.md文件中遍历所有定义好的OnsService的API ID
通过API ID进行匹配,检查是否已存在调用当前OneService API的方法
如果用户输入多个API ID,遍历每个ID进行匹配,然后列出每个API ID的调用方法的存在情况
### 3. 检查用户是否确认调用/新增OnsService接口
上一步列出所有输入的API ID的调用方法的存在情况的列表,发送给用户,让用户确认
- 匹配到已有的OneService API调用方法,提示用户是否直接使用
- 如果没有匹配到已有的OneService API调用方法,提示用户确认是否新增
### 4. 收集OnsService API信息
- 如果用户确认直接使用,直接使用已有的调用方法
- 如果用户确认新增,提示用户输入新增的OneService API的相关信息,包括API ID、SQL语句、请求参数、响应参数等
### 5. 生成OnsService API相关代码
根据用户输入的接口信息,需要生成以下内容
- api_description.md文件中新增对应API ID的描述,参考api_description.md文件中的示例
- 根据用户的输入,结合examples.md文件里的示例,生成新的go文件,存放到infra/one_service目录下
- 最终的效果,应该是只在infra/one_service目录(或者已有的infra下的其他OneService相关目录)下新增一个go文件,在api_description.md文件中新增对应API
ID的描述
examples.md
内容简介:SKILL生成内容的模板
-
方式一: 调用 QueryWithParams 方法的生成示例模板
-
方式二: 调用 SqlQuery 方法的生成示例模板
---
name:examples
description:OnsService的API调用方法代码示例
---
方式一:用户输入的是请求参数,调用QueryWithParams方法查询示例数据
```go
package one_service
import (
"context"
"xxx/xxx/jsonx"
"xxx/xxx/logs"
)
const (
// 示例API ID
OneServiceAppId_Example = xxxxxxxx
)
type ExampleData struct {
ExampleID int64 `gorm:"column:example_id"` // 示例ID
}
// 查询示例数据
func GetExampleData(ctx context.Context, exampleIds []int64) (map[int64]*ExampleData, error) {
if len(exampleIds) == 0 {
return nil, nil
}
// OneService入参
params := map[string]interface{}{}
// 示例ID列表
params["example_ids"] = exampleIds
// OneService出参
var rpcResp []*ExampleData
// 调用 OneService SQL 查询数据
err := QueryWithParams(ctx, OneServiceAppId_Example, params, &rpcResp)
if err != nil {
logs.CtxError(ctx, "GetExampleData QueryWithParams err:%v", err)
return nil, err
}
logs.CtxInfo(ctx, "GetExampleData appID %v, req is %v, resp is %v", OneServiceAppId_Example, jsonx.ToString(params), jsonx.ToString(rpcResp))
result := make(map[int64]*ExampleData, len(rpcResp))
for _, item := range rpcResp {
if item == nil {
continue
}
// 以 example_id 为 key 聚合
result[item.ExampleID] = item
}
return result, nil
}
```
方式二:用户输入的是SQL语句,调用SqlQuery方法查询示例数据
```go
package one_service
import (
"context"
"fmt"
"strings"
"xxx/xxx/jsonx"
"xxx/xxx/logs"
"xxx/xxx/utils/conv"
)
const (
// 示例API ID
OneServiceAppId_Example = xxxxxxxx
)
type ExampleData struct {
ExampleID int64 `gorm:"column:example_id"` // 示例ID
}
const (
// 示例SQL
ExampleSql = "select example_id from example_table where example_id in (:example_ids)"
)
// 查询示例数据
func GetExampleData(ctx context.Context, exampleIds []int64) (map[int64]*ExampleData, error) {
if len(exampleIds) == 0 {
return nil, nil
}
// OneService出参
var rpcResp []*ExampleData
// 调用 OneService SQL 查询数据
err := SqlQuery(ctx, OneServiceAppId_Example, ExampleSql, &rpcResp)
if err != nil {
logs.CtxError(ctx, "GetExampleData SqlQuery err:%v", err)
return nil, err
}
logs.CtxInfo(ctx, "GetExampleData appID %v, req is %v, resp is %v",
OneServiceAppId_Example,
fmt.Sprintf(ExampleSql, strings.Join(conv.Int64sToStrs(exampleIds), ",")),
jsonx.ToString(rpcResp))
result := make(map[int64]*ExampleData, len(rpcResp))
for _, item := range rpcResp {
if item == nil {
continue
}
// 以 example_id 为 key 聚合
result[item.ExampleID] = item
}
return result, nil
}
```
api_description.md
内容简介:记录 OS API 的相关内容描述
- 方式一: 名称 + ID + 生成代码文件 + 请求/返回参数
- 方式二: 名称 + ID + 生成代码文件 + 请求SQL + 请求/返回参数
---
name:api_description
description:OnsService的API描述
---
## 接口列表
### API 名称(方式一示例,生成代码参考examples.md)
API ID:用户输入
生成代码文件:根据API名称生成对应的go文件
请求参数
| 参数名称 | 参数类型 | 是否必须 | 参数描述 |
|:--------|:-----------|:-----|:-------|
| xxx_ids | array[int] | 否 | xxid列表 |
| | | | |
返回参数
| 参数名称 | 参数类型 | 是否必须 | 参数描述 |
|:-------|:-----|:-----|:-----|
| xxx_id | int | 否 | xxid |
| | | | |
---
### API 名称(方式二示例,生成代码参考examples.md)
API ID:用户输入
生成代码文件:根据API名称生成对应的go文件
请求SQL:select ...
请求参数
| 参数名称 | 参数类型 | 是否必须 | 参数描述 |
|:--------|:-----------|:-----|:-------|
| xxx_ids | array[int] | 否 | xxid列表 |
| | | | |
返回参数
| 参数名称 | 参数类型 | 是否必须 | 参数描述 |
|:-------|:-----|:-----|:-----|
| xxx_id | int | 否 | xxid |
| | | | |
---
总结
SKILL的适用于 【稳定地】 执行 【多步骤】 的有 【固定流程】 的任务。
核心设计亮点: 渐进式披露,即通过分层加载 + 按需激活 + 零上下文执行实现 Token 效率与功能深度的平衡,大幅降低上下文压力并提升任务执行准确性
-
稳定性: 不掺杂过多特殊因素和特殊业务场景,90%情况都是较为统一的
-
多步骤: 相较于一步一步多轮对话,直接使用 SKILL 可以一步到位
-
固定性: Agent 会跟着 SKILL.md 当中定义的 steps 一步一步执行,相对于普通的多轮对话,AI 不会去扫盘和联想,一方面可以避免有庞大的上下文消耗,导致资源浪费,另一放面,减少大模型出现幻觉的概率,从而避免乱回复、乱生成代码的情况。
需要强调的是,文章当中的示例结合了公司内部的工具,相关配置大家肯定无法直接复用,本篇文章的核心如标题所示,是对 SKILL 设计思路的一次分享,希望大家可以从中获取灵感
凡是【稳定地】执行、包含【多步骤】、且具备【固定流程】的任务,都可以被抽象成清晰的 SOP,并通过 SKILL 固化下来,交给智能体去稳定、高效地执行
只要抓住“先梳理流程,再结构化抽象,再让工具去执行”这条主线,就能够在各类场景中持续沉淀出属于个人、部门、企业的自动化能力,而不仅仅局限于文中示例
AI 编程的未来会是什么样子,没有人能确切知道。但有一点是确定的:那些现在就开始认真学习、积极实践、深入理解的人,将最有能力塑造和适应这个未来
去实验,去失败,去学习。这个过程本身,就是价值所在