最近把自己折腾过的一个小项目重新整理了一遍,名字就叫skills。起因很朴素:我有段时间觉得自己什么都在学,但真到要写简历、做团队技能盘点的时候,反而一句话都说不出来。技能点分散在简历、笔记、聊天记录、各个项目的 README 里,没有一个地方能说清楚“我目前到底会什么、熟练到什么程度、最近还在不在用”。于是我又犯了程序员的老毛病——不想着去记笔记,先写了个工具来管这件事。这篇文章就是完整记录这个叫skills的命令行工具的设计思路、核心实现、开发过程中踩过的坑,以及它最终如何融入我的日常工作流。如果你也在琢磨怎么系统化管理自己的技术栈,或者想给自己写一个类似的小工具,这里面的设计取舍和踩坑过程应该能帮你省不少时间。
1. 为什么我会给自己写一个叫 skills 的工具
1.1 痛点是“技能数据长在简历里,而不是长在系统里”
先说说最初的场景。去年我做年中复盘的时候,需要把过去半年的技术产出整理成一份文档。我打开简历、翻 Git 提交记录、翻笔记软件、翻团队 Wiki,折腾了两个多小时,最后产出的技能列表还是靠记忆硬凑的。更难受的是,我发现简历上写的“精通 XX”,和我内心真实的熟练度根本不是一回事——有些技术我半年没碰了,只是因为三年前用过,就一直挂在简历上;有些技术我每天都在用,但我从没认真想过它在我的技能体系里到底处于什么位置。
这其实暴露了一个问题:技能的“数据”一直被存放在各种不合适的载体里。简历是给别人看的,它有美化倾向,不适合做真实记录;笔记是给自己看的,但它只有描述性文字,没有结构化的状态管理;聊天记录就更不用说了,根本称不上数据。所以我的第一个想法是:我需要一个专门的地方,用结构化的方式管理“我会什么”。我管它叫skills,它首先是一个数据文件,其次才是一个命令行工具。
1.2 为什么不用现成的技能矩阵平台
在动手之前,我当然先看了一圈现成的方案。Notion 和飞书多维表格可以建技能库,在线技能矩阵 SaaS 工具也不少,但试下来都有一个共同问题:它们把“技能的记录”和“技能的使用场景”隔离得太远。我在终端里写代码、敲命令,技能数据却躺在网页里,这个距离感会让维护频率直线下降。工具再好,如果每次更新要打开浏览器、找到对应的表格、手动改单元格,那它很快就会被遗忘。
另外,很多在线平台对数据的所有权、导入导出的格式都有限制。我是把自己几年积累的技术栈信息放进去,如果哪天平台调整收费策略或者关闭服务,迁移成本会很高。所以我很明确地定了几个原则:本地存储优先,纯文本格式优先,命令行交互优先。只有做到这三条,技能数据才能真正长在我的工作环境里,而不是某个第三方平台上。
1.3 项目定位:写给自己的“技能账本”
定下原则之后,skills的定位就非常清晰了。它不是简历生成器,也不是团队 HR 系统,而是一个个人技能账本。它记录三个最核心的问题:我学过什么、我现在用到什么程度、我最近还在不在用。
为了实现这个定位,工具必须足够轻。我没有做 Web 界面,没有做数据库,甚至没有做服务端,就是一个跑在终端里的 CLI。数据文件默认放在~/.config/skills/skills.yaml,整体是一个 YAML 文件,任何人打开都能看懂,也能手动修改。这样做的好处非常明显:数据可读、可备份、可进 Git 仓库,即使哪天这个工具不再维护,我的技能数据也永远是我的,随便拿个文本编辑器就能接着记。
2. 数据层设计:技能树的数据结构才是核心决策
2.1 为什么选 YAML 而不是 SQLite 或 JSON
很多朋友听说我要写技能管理工具,第一反应是“用 SQLite 存啊,查询方便”。我理解这个思路,但实际做下来发现 SQLite 在这个场景下是过度设计。技能数据的特点是:总量小(一个人撑死几百条技能记录)、结构固定、访问模式简单。它不需要复杂的关联查询,不需要并发写入,更不需要事务回滚。这种情况下,一个结构良好的 YAML 文件比数据库更合适——因为它直接满足了“人可读”这个需求。
在 YAML 和 JSON 之间,我最终选了 YAML。JSON 当然更通用,但 YAML 对“人肉维护”友好太多:可以写注释,多行字符串不痛苦,嵌套结构也不用写一堆花括号。技能数据里不可避免地要写备注、写证据链接,YAML 的体验是 JSON 完全比不了的。代价是解析时要小心一些边界情况,比如缩进问题、特殊字符转义,这个后面踩坑部分会详细说。
2.2 分类、层级与字段设计:照着“目录树”的思路来
技能数据天然是树形的,这也是我把核心功能叫做技能树(skill tree)的原因。最高层是领域(category),比如后端开发、前端开发、运维与 DevOps、软技能;领域下面是具体的技能(skill);技能再往下,是它关联的证据(evidence),比如参与过的项目、写过的文章、维护过的开源仓库。
每个技能条目我设计了这几个字段:
categories: - name: backend display: 后端开发 skills: - name: golang level: advanced status: active since: 2022-03 last_used: 2025-06-10 evidence: - "交易系统核心服务重构" - "开源项目 x 的主要维护者" tags: [language, backend]这里每个字段都有它的用途:level表示熟练度等级,status表示当前状态(active/stagnant/archived),since记录从什么时候开始用,last_used记录最近一次使用的时间,evidence是支撑这个熟练度判断的实际产出,tags用于跨领域的检索。这套设计是我试错了好几版之后才稳定下来的,它既不过度复杂,又能覆盖我 90% 的查询需求。
2.3 一个反直觉的决策:熟练度不用 1-10 分,而是用阶段枚举
最早的一版设计里,我用的是数字评分,1 到 10 打分。用了一段时间发现很别扭:今天心情好给 Golang 打 9 分,过两天觉得自己也就 7 分的水平,数字会因为主观状态剧烈波动。更重要的是,数字没有语义,9 分和 8 分到底差在哪,我自己都说不清楚。
后来我换成了 Dreyfus 模型那种阶段式思路,简单化成四级:basic(入门能用)、intermediate(独立完成常规任务)、advanced(解决过复杂问题、能带人)、expert(有体系化的方法论和公认产出)。这四级之间是质的差异,不是量的差异,判断起来没那么纠结。我只需要问自己一个问题:这个技能我能不能独立搞定复杂问题,并给出让别人信服的方案?能,就是 advanced;不能,再高也只能算 intermediate。用枚举之后,每次更新熟练度都变成了一次理性的自我评估,而不是随手打个分。
3. 核心功能拆解:从 add 到 tree 的实现细节
3.1 命令体系设计:子命令优先,配置零负担
skills的命令设计没有走单命令多参数的老路,而是直接用了 Cobra 的子命令体系。原因很简单:技能管理这个场景天然就是多动词的,添加、删除、查询、统计、导出,都是不同的动作,硬塞进一个主命令的 flag 里会让命令行变得难记难用。
实际的核心命令只有六个:
| 命令 | 用途 | 示例 |
|---|---|---|
skills init | 初始化配置文件和示例结构 | skills init |
skills add | 添加技能条目 | skills add golang --category backend --level advanced |
skills rm | 删除技能条目 | skills rm vue --category frontend |
skills set | 更新字段(熟练度、状态等) | skills set golang --level intermediate |
skills query | 按维度查询 | skills query --category backend --level advanced |
skills tree | 输出完整技能树 | skills tree --status active |
skills export | 导出为 Markdown 或 JSON | skills export --format markdown |
skills review | 提示最近未使用的 active 技能 | skills review --days 30 |
另外还有skills stats做简单的分布统计,比如每个领域下 active 技能的数量。这套命令看起来多,但实际使用时心智负担很低,因为它们就是技能管理的日常动作,没有一个是凑数的。
3.2 熟练度演化:update不是手动打分,而是记证据
skills set在最早期就是个简单的字段修改命令,但用久了发现一个问题:我很容易在改熟练度的时候自我欺骗,把 intermediate 改到 advanced,却说不出来为什么。所以后来我加了一个隐性的规则——如果你要把某个技能升级到advanced或expert,必须同时添加一条 evidence。命令设计成:
skills set golang --level advanced --evidence "主导了交易系统重构,线上日志量下降 40%"如果--evidence为空,工具会拒绝执行升级。这个机制的本质是强迫自己为“熟练度判断”提供依据。它不复杂,十几行代码就能实现,但它对数据质量的影响极大。从这之后,我技能树里的 advanced 以上的条目,每一个都有对应的实际产出支撑,不再有虚高的数据。
skills review则是反向的约束。它扫描所有status: active的技能,如果某个技能的last_used距今超过设定天数(默认 30 天),就列出来问你要不要标记为stagnant。这个功能防止了“我以为我还在用,其实已经半年没碰”的情况。每隔一段时间跑一次 review,技能树就能自动筛选出值得投入精力保持的技能,以及应该诚实面对“已经生疏了”的技能。
3.3 tree 渲染:在终端里画一棵可读的技能树
skills tree是使用频率最高、也是最有成就感的命令。它的输出长这样:
技能树 ├── 后端开发 │ ├── golang (advanced, active) │ ├── mysql (intermediate, active) │ └── redis (basic, stagnant) ├── 前端开发 │ ├── vue (intermediate, stagnant) │ └── typescript (basic, active) └── 运维与 DevOps ├── docker (intermediate, active) └── kubernetes (basic, active)这个渲染逻辑比想象中要麻烦一点。终端宽度有限,技能名、领域名和括号里的状态信息混在一起时,很容易出现折行和错位。我没有用现成的树形打印库,而是自己处理了树节点的前缀逻辑:每一层用├──和└──表示分支,缩进用两个空格,叶子节点统一用 tabwriter 这类工具做列对齐。为了区分熟练度,我给不同 level 加了颜色:advanced 用绿色、intermediate 用黄色、basic 用灰色,stagnant 的条目会在末尾加一个标记。第一次跑出完整技能树的时候,我盯着屏幕看了好一会儿,那一刻才真正觉得——这就是我想要的管理面板。
4. 开发过程中踩过的三个坑:从代码细节到数据结构
4.1 Cobra 子命令与普通 flag 解析的“隐式冲突”
第一个坑出在命令解析上。Cobra 虽然好用,但它的 flag 解析规则和标准库 flag 有一处容易忽略的差异:Cobra 的本地 flag 默认只在当前子命令生效,持久 flag 才会传给子命令。我在早期版本里把--category同时用在了add、query和set上,一开始直接把PersistentFlags()加到了根命令上,结果导致所有子命令都必须显式带上这个 flag,不传就报错。
排查了半天才意识到问题是“定义位置”而不是“代码逻辑”。最后我把全局需要的 flag(比如--config指定数据文件路径)留在了根命令的PersistentFlags(),而--category这样的业务 flag 分散到各个子命令独立定义,再用一个initFlags()函数做统一注册。这样既避免了子命令之间的 flag 污染,也保持了每个命令的提示信息干净。这个教训用一句话总结就是:用 Cobra 时,flag 的“作用域”一开始就要想清楚,不要图省事全挂到根命令上。
4.2 终端中英文混排的对齐问题
第二个坑非常典型:中文技能名在 tree 渲染时对不齐。最开始实现 tabwriter 对齐时,我以为只要按字符数算宽度就够了,结果中英文混排的缩进完全乱了。原因在于终端显示的“列宽”是由字节宽度决定的——中文是全角字符,占两个英文宽度,而 Go 的字符串长度是按字节算的,一个中文 UTF-8 字符占 3 个字节。直接用len()计算宽度,所有含中文的技能名都会被算短,后面项目的对齐就全线崩坏。
解决方案是用github.com/mattn/go-runewidth这个库,它专门计算字符串的终端显示宽度。每个字符用rune遍历,遇到全角字符宽度按 2 算,半角按 1 算,然后再拿这个宽度去做 padding。修改之后,技能树才算真正对齐了。这个坑不好查,因为它不报错,只是输出看起来乱。如果你以后做任何终端 UI 相关的工具,遇到中文对齐问题,请第一时间想到 runewidth,不要自己造轮子去数字节。
4.3 YAML 序列化的字段丢失与默认值陷阱
第三个坑藏在 YAML 的序列化行为里。我的技能数据结构中,evidence和tags在正常有值的时候没有问题,但一旦某个技能的tags为空,序列化出来就缺了对应的键。这里涉及 YAML 库(我用的gopkg.in/yaml.v3)的一个内部细节:空切片在序列化时可能被输出为null而不是[],如果字段没有显式设置omitempty之类的 tag,就会把空切片序列化成tags: null。更隐蔽的是,null在 YAML 里既可以是空值也可以是空映射,如果后续解析时类型推断不一致,就会产生很难察觉的兼容性问题。
我的处理方式是在加载数据后做一个 normalize 步骤:遍历所有技能条目,把所有为nil的切片字段统一初始化为空切片,再写回结构体。同时数据结构里每个字段都显式声明 YAML tag,不依赖默认行为。这确保同一个数据文件无论是手工编辑还是工具生成,格式都是幂等一致的。数据处理类的工具,建议你也在加载后做一次“字段归一化”,这比在序列化和反序列化的无数边界里调试要省心得多。
5. 完整使用流程:从零开始搭建自己的技能管理
5.1 初始化与首次录入
假设你现在拿到这个工具,第一步自然是skills init,它会在配置目录下生成一个带示例的skills.yaml。我的建议是不要直接改示例里的数据,而是打开这个文件,先定义自己的领域分类。领域不宜太细,我现在的分类是backend、frontend、devops、mobility、soft五个,再细就变成知识点而不是领域了,反而失去树形结构的意义。
录入具体技能时,我有个实用原则:先记“正在用”的,不要试图把历史上所有用过的东西都补进去。第一次录入的目标是让技能树能真实反映当前状态,而不是做一个完整的历史档案。把当前工作中的主力语言、框架、工具链记下来,每项填上大概的level和status,这个初始版本控制在半个小时以内完成,后续再慢慢补。
5.2 日常维护:把更新技能变成一种轻量习惯
工具真正发挥价值,靠的是持续维护。我的维护节奏是跟随事件驱动的:每次完成一个比较有代表性的任务,就顺手更新对应技能的last_used;每次意识到某个技能长时间没用了,就通过skills review把它标记为stagnant;每隔一次大版本迭代,我会跑一次skills tree,整体看一遍自己的技能分布。
这里有一个很重要的使用心得:skills的数据准确性,比功能丰富度重要得多。哪怕你只是每两个月更新一次last_used,也比每天打开工具但数据都是几个月前要好。它本质上是一个自我审视的工具,你越诚实,它反馈给你的信息越有价值。我自己把skills review加到了每周五的复盘流程里,大约每次 10 分钟,成了类似 Git 提交那样的例行动作。
5.3 导出到简历:从技能树直接生成可读片段
当初设计导出功能时,我考虑的是如何把技能树“用出去”。skills export --format markdown会把技能树转成一段可以直接贴进简历或个人网站的 Markdown 片段:
### 后端开发 - Golang:高级,持续使用。主导交易系统重构,有开源维护经历。 - MySQL:中级,持续使用。 - Redis:初级,较少使用。这个功能虽然简单,但非常实用。以前每次更新简历都要重写一遍技能描述,现在只需改好skills.yaml,导出、粘贴,中间省掉了大量重复劳动。它还帮我发现了一个简历的老问题:原来我写的“精通”大多数其实只是 intermediate 级别,导出功能从源头上杜绝了自我标榜。
6. 从个人工具到团队技能地图:可行的扩展思路
6.1 多机器同步:把数据文件交给 Git 管理
skills的数据是纯文本 YAML,这给它带来了一个天然优势——可以用 Git 做同步。我把~/.config/skills/skills.yaml放到了一个私有仓库里,办公电脑和家用电脑之间通过 Git 同步。因为数据结构稳定,合并冲突的概率很低,真冲突了手动改一下 YAML 也很直观。这个方案比自建同步服务简单一百倍,也足够可靠。
数据进 Git 之后还能解锁一个隐藏功能:历史回溯。用git log可以查看某个技能是什么时候加入的、熟练度从什么时候开始变化。这等于给技能成长记录了带时间线的版本历史,比 excel 表格里的“最后修改时间”丰富得多。
6.2 团队共享时的格式约定与规范化
我后来把skills的数据结构开放给了团队里几个人试用了。这个时候发现,个人工具要想在团队里跑起来,最容易出问题的地方不是代码,而是“格式约定”。不同人记录的技能名写法不统一:有人写golang,有人写go,有人写Go;分类也五花八门,有人的web领域里包含了后端、前端、UI 设计,整体查起来就很混乱。
解决办法不是无限兼容,而是做一层规范化约束。我给数据文件加了 schema 校验逻辑:技能名统一小写连字符风格,领域名必须取自预定义的列表,熟练度必须是四个枚举值之一。不合法的条目会在读取时报出错误并指出具体位置。实践证明,工具强制性约束比文档约定有用得多,人的惰性太强了,可写可不写的规范最终一定不会被遵守。
6.3 Web 展示与自动提醒:下一步可以怎么玩
最后聊几个我还没做、但已经想清楚方向的扩展。第一个是读入 JSON 或 YAML 后渲染成 Web 技能地图,大概就是一个静态页面,把技能树按领域和熟练度做成可视化的网格,方便分享给同事或者放进个人作品集里。第二个是用 CI 定时跑skills review,把长期未使用的高熟练度技能以提醒消息的形式发送到即时通讯群,让团队成员能感知各自技能的“保质期”。第三个是给level: expert的技能做关联学习路径,比如技能 A 是技能 B 的前置依赖,当 B 标记为 stagnant 时,自动提示是否需要复习 A。
这些扩展的共同点,都是基于那个结构化的skills.yaml去生成新的信息。数据结构定好了,玩法可以有很多;反过来如果一开始就拘泥于特定功能,反而很难做出通用性。这也是我对这个项目最满意的一点:它最核心的产出不是代码,而是一个干净、诚实、可持续维护的数据模型。
最后再分享一点个人感受。写完skills之后,我最大的变化不是“简历更好写了”这类表面的便利,而是我开始更主动地审视自己的技能状态:哪些东西在悄悄生锈,哪些投入正在产生复利,哪些领域其实只是虚张声势的熟悉。工具本身很小,几百行代码而已,但它帮我把一个模糊的自我认知,变成了一个可以定期审视、修正、迭代的数据结构。如果你也想做类似的事情,我的建议是别急着写代码,先把你想回答的问题列出来,然后把数据结构设计好——数据模型想明白了,工具只是把它实现出来而已。