一提到“skills”这个词,很多人第一反应是简历上那一排“精通XX”“熟悉XX”的标签。但我最近手搓的这个项目,给的完全是另一个答案:一个极简的个人技能追踪与成长管理工具,用命令行就能维护自己的技能树,记录每次刻意练习的时长、输出物和熟练度变化,然后自动生成复盘报告。这篇文章就围绕这个项目的完整落地过程来写,从需求拆解、数据模型设计,到实际编码、踩坑修复,再到最终沉淀出的个人使用工作流,一次性讲清楚。
如果你是那种喜欢用数据驱动自己成长的人,或者正在学习某个新领域但总觉得“学了就忘、练了没数”,又或者想在工作之外维护一套属于自己的能力账本,那这篇文章里的思路和代码,基本可以帮你省掉至少两个星期的试错时间。我自己从搭建到稳定使用花了三个版本迭代,最后沉淀下来的方案,足够简单,也足够抗造。
1. 项目缘起与整体设计思路
1.1 为什么不做成笔记而是做成“记账工具”
先说一个反直觉的观察:我过去也用过各种笔记软件、打卡App来管理学习进度,但几乎所有方案都有同一个致命问题——它们记录的是“内容”,而不是“变化”。
笔记擅长回答“我今天学了什么”,但它回答不了“我此刻的熟练度到底是多少”“上次练Python爬虫已经是三周前,该不该重新激活”。说白了,笔记是一种记忆外设,不是一个度量系统。而“skills”这个项目的核心主张是:把技能学习当成一笔笔‘进度存款’来管理,每次投入都要入账,每次长时间不用就要计息——只不过这个‘利息’是遗忘曲线。
所以项目定位从一开始就不是“另一个知识管理工具”,而是“技能资产的个人资产负债表”。它解决的真实需求是:让人清楚地知道自己在哪项技能上投入了多少、当前处于什么水位、哪些技能正在折旧归零。
1.2 核心功能拆解与设计取舍
这个项目最终收敛为四个核心命令:添加技能(add)、注入练习记录(log)、查看当前状态(tree/status)、输出复盘报告(review)。我故意砍掉了日历视图、社交分享、成就系统这类锦上添花的东西,理由是这些功能会诱导你把时间花在“装饰数据”而不是“积累数据”上。
其中一个关键取舍是:不做自动计时器。为什么?因为真实场景里面,技能练习往往发生在零散的时段里——通勤时听了一节音频课、午休时读了十几页书、晚上敲了一个小时代码。这些场景下,让用户手动点“开始计时”再“结束计时”的负担太重,最后大概率变成“忘了记录”。所以我采用“事后记录+时长估算”模式:你只需要在练完后花十秒钟填一个数字,比如“今天练了40分钟爬虫,产出是写了一个豆瓣Top250采集脚本”。数据不追求秒级精准,但能持续不断,本质上是在跟人性懒惰作对抗。
1.3 技术选型:为什么选了Python+SQLite+Click
技术栈上我没有引入任何重型依赖。主体语言用Python 3.10+,命令行框架用Click,数据存储用一个SQLite单文件数据库。这个组合的考量其实很朴素。
第一,单文件数据库。整个技能台账就是一个文件,备份等于复制文件,同步等于丢网盘,迁移等于拷到新机器。对于个人工具来说,这种“土办法”反而是最高级的可靠性。
第二,Click而非Argparse。Argparse写简单的单命令脚本够用,但一旦涉及多级子命令、自动补全、交互式确认,Click的文档和社区成熟度会帮你省下一堆“自己造轮子”的时间。
第三,用Python而不是Node或Go。纯属个人维护效率考虑——大部分会想复制这个项目的人,Python环境基本是现成的,拆开就能改。你要想换成Go也没问题,核心架构是通用的,下面我会把设计逻辑讲透,语言只是外壳。
2. 数据模型与核心细节解析
2.1 技能分类与标签体系:扁平比树形更抗用
在设计数据库结构的时候,我第一版做的是树形分类,比如“编程 -> 后端 -> Python -> 爬虫”,五层嵌套看上去很丰满,但实际用了两周就发现问题:维护成本太高,而且层级一旦超过三层,人的归类决策就会犹豫——“爬虫算后端还是算数据工程?”每归类一次都要做一次内心斗争,大脑会罢工。
后来我把分类结构砍成两层:技能主题(domain)+ 自由标签(tags)。每个技能记录必须属于一个domain(比如“编程后端”“语言学习”“写作”),但同时可以打任意多个tags(比如“效率工具”“面试必备”)。扁平结构的好处是两个:其一,创建的摩擦成本低,你不需要在“归属哪个父类”上纠结;其二,后期统计维度足够灵活,想按domain看全景就按domain聚合,想按tag看某个跨领域主题就按tag过滤。
下面这张表是实际表结构的设计,字段不算多,但每一条都是经过反复实战迭代后才保留下来的:
| 字段 | 类型 | 说明 |
|---|---|---|
| skill_id | TEXT | 技能唯一ID,主键,用kebab-case命名,如“python-spider” |
| name | TEXT | 技能显示名称 |
| domain | TEXT | 所属主题域,如“编程” |
| level | INTEGER | 熟练度等级,0-100整数 |
| total_hours | REAL | 累计投入小时数 |
| last_practiced | DATE | 最近一次练习日期 |
| streak_days | INTEGER | 连续练习天数 |
| status | TEXT | active / fading / dormant 三态 |
| tags | TEXT | 逗号分隔的标签列表 |
这里有三个字段我想特别说明一下设计意图。
level不是拍脑袋填出来的数,而是由“累计时长+练习频次+输出物数量”三个因子加权后取整得到。这样设计的好处是,你无法靠“感觉今天状态不错”来虚报等级,任何一次level的上涨都必须有对应的练习记录来支撑。
status字段是整套系统的灵魂。active表示正在常练,fading表示超过7天没练但还不至于忘记,dormant表示超过21天没练,已经进入“冷启动区”。每次录入练习记录时,程序会自动把该技能的status刷新为active;反之,每次打开status视图时,系统会按当前日期和last_practiced自动降级。这个机制让“哪些技能被冷落了”始终一目了然,不至于到了复盘的时候才发现自己在某个技能上已经荒废了两个月。
2.2 熟练度计算模型:不追求科学精准,只追求反馈及时
关于熟练度怎么算,我最早想的很复杂——想引入间隔重复算法的变体,搞什么遗忘曲线拟合。但后来想了想,项目初心是“低摩擦记录”,不是“心理学实验”,所以最终用的模型非常简单,但短期内效果出奇地好。
基础规则是这样:每次练习记录提交后,系统会计算一个增量,公式为:
increment = base_gain * (1 - level/120) + output_bonus其中base_gain按练习时长分档:少于15分钟不计时,15-45分钟算2点,45-90分钟算3点,超过90分钟算5点(但每天同一技能最多累计10点)。output_bonus是产出物的奖励分,每提交一个输出物(比如一篇总结、一个Demo、一篇文章草稿)额外加1点,上限5点。
为什么要除以120而不是除以100?这是个很微妙的设计。如果除以100,那么当level接近100时增量几乎为零,用户会感到“练满了”从而失去动力。除以120可以让增量曲线在80分附近仍有可感知的涨幅,同时又不会让满分过于廉价。说白了,这是给“高阶玩家”留的心理空间。
衰减规则则是:如果某技能超过7天未练,每天自动扣减0.5 + level*0.002点,封顶扣到该技能当前分数的60%为止。这样既模拟了遗忘,又不会让你多年的积累一夜清零。
2.3 与其他方案横向对比:为什么不用现成App
市面上其实有不少技能打卡类的App,也有类似LeetCode刷题热力图、语言学习App里的连续打卡统计。但我要强调,命令行工具和它们有一个本质区别:数据完全的本地所有权和可编程性。
App往往把数据存在别人服务器上,导出格式稀奇古怪,如果你想做“过去三个月里Python相关技能投入了多久”这种自定义分析,你得祈祷开发团队给了这个维度。而“skills”项目的所有数据都躺在一个.db文件里,你可以随时用SQL查询、用Python写脚本做任意维度的分析,甚至一小时写个教程把数据渲染成网页。对于喜欢折腾的开发者来说,这是App给不了的自由度。
下表是我在立项时做的对比,供你也做一次需求检查:
| 对比维度 | skills(本工具) | 通用打卡App | 笔记软件+表格 |
|---|---|---|---|
| 数据所有权 | 全部本地 | 服务商 | 本地但无结构 |
| 自定义统计 | 任意SQL | 受限于功能 | 需要手动维护 |
| 输入摩擦 | 可接受的10秒/次 | 较低 | 较高 |
| 自动化程度 | 中高(状态自动降级) | 低 | 无 |
| 长期演进 | 可编程 | 依赖厂商路线 | 依赖模板 |
3. 实操过程:从零搭建到日常落地
3.1 环境准备与依赖安装
如果你想把代码跑起来,环境准备大概五分钟就能完成。我假设你已经装好了Python 3.10以上版本,并且有pip可用。实际执行时只需要安装一个第三方库:
pip install clickSQLite是Python内置的,不需要额外安装。全部代码结构我做成了扁平化单模块,个人工具这样最省心:
skills/ ├── skills.py # 主程序,所有命令入口 ├── schema.sql # 初始化建表语句 └── skills.db # 运行后自动生成的数据文件首次运行前执行一次初始化,就会建好数据表:
python skills.py init这个命令做的事情很简单:用schema.sql里的CREATE TABLE语句创建出上一节说的那张技能表,并建立一个独立的practice_log表用于存储每一次练习记录明细。注意我把练习明细单独建表而不是直接更新总表数值,是因为后期做分析和报表时,明细数据是不可或缺的。
3.2 核心命令实现细节
先看添加技能。这里的要点是ID采用kebab-case,原因是实际输入时下划线需要按Shift键,而横杠直接按就能敲出来,摩擦更小。
python skills.py add python-spider --name "Python爬虫" --domain "编程" --tags "效率工具,数据采集"命令内部的处理逻辑上,值得关注的是它做了一个自动去重校验:如果同名skill_id已存在,会直接拒绝执行,避免误操作把已有进度清零。
接下来是核心的log命令,也就是每次练习完后的“入账”操作:
python skills.py log python-spider --minutes 40 --note "完成豆瓣Top250采集,解析JSON存储" --output 1执行时脚本会读取该技能当前level,然后按前面说过的增量公式计算新的熟练度,把total_hours加上对应小时数(40分钟约等于0.67小时),把streak_days逻辑变成:如果昨天也练过则加1,否则重置为1。这一连串操作都用SQLite的事务包裹,确保任何一个环节失败都不会产生脏数据。
tree命令用于查看当前全貌,输出时按domain分组,每行显示技能的当前level和状态图标化文本(文字标识ACTIVE/FADING/DORMANT),水平进度条用简单的等于号和减号拼接而成。一切以文本终端友好为主,不为格式美化增加额外依赖。
3.3 状态降级与报告生成:让数据自动替你操心
外界环境一变,数据不说谎。状态自动降级的逻辑写在status命令和review命令里,每次调用这两个命令时,系统会先执行一次_check_rotation()函数,把所有技能根据last_practiced和今天的日期重新计算新status。
举个例子:如果某项技能最后练习日期是今天,那它肯定是active;如果最后练习日期是10天前,就降为fading;超过21天,就会变成dormant。但这个降级不是简单的if-else,还耦合进了熟练度扣减逻辑,每次检查时一次性把所有罚款算完并落库。这样设计的好处是,老旧的进度不会在几个星期后才被发现,而是每次打开终端就会被提醒。
review命令则是生成一段markdown格式的复盘报告,我专门给它写了按周和按月两种粒度。周报重点看“本周新增练习多少次、每个技能投入时长、连续天数变化”;月报重点对比四个星期之间的趋势,并筛选出从上个月以来掉分最多的技能,按降序排列。输出可以直接重定向到文件,再丢进个人Wiki或者博客草稿箱:
python skills.py review --period weekly --output report.md3.4 时间投入的经验参考
用这个工具管理个人技能成长已经超过半年,我手头产生了一套比较稳定的大致参考数据:
| 技能领域 | 每周投入 | 三个月后level参考 |
|---|---|---|
| 后端开发 | 5-7小时 | 45-60 |
| 英语听说 | 3-4小时 | 30-40 |
| 写作输出 | 2-3小时 | 25-35 |
| 数据分析 | 4-6小时 | 40-55 |
数值因人而异,关键不是绝对数字,而是趋势线是否向上。用这套数据做复盘的时候,我很清楚地看到哪几项投入一直上不去,原因通常不是“没时间”,而是“优先级不清”——这个工具至少把优先级问题变成了一堆白纸黑字的数字,逼着你去面对它。
4. 常见问题与排查技巧实录
4.1 多设备同步时数据冲突怎么办
因为SQLite是单文件,如果通过网盘同步,在多台电脑上同时写入可能会遇到文件冲突。我的解决办法很简单:只在每天结束时同步一次,平时不把数据库放在实时同步目录,而是放本地的项目文件夹。同步前手动停掉另一台机器上的命令进程,或者干脆只在主力机上更新数据。
如果你确实需要多人协作或者双机同时高频写入,可以考虑把数据库换成PostgreSQL,但那就违背了“个人轻量工具”的初衷。对于99%的个人使用场景,单文件反而是优势——你不需要部署任何服务端。
4.2 熟练度数值虚高,怎么校准
有一次我发现某个技能的level已经到了82,但实际让我现场手写一个小功能时却卡住了。这说明模型对短期刷时长产生了虚高奖励。我的解法是:在review命令里增加一个“自评校准”模式,每个月月底把所有active技能的当前自信程度(1-5分)录入一次,然后用这个分数去约束下个月的level上限,即下个月的level不得超过“上月置信分×25”。效果立竿见影——你没法再用零碎的打卡来制造虚假的成长感。
4.3 冷技能被永久遗忘前,如何抢救
其实整个status机制都能看成一个“防遗忘预算系统”。但我额外给dormant技能加了一个“激活预案”:当review发现某个技能进入dormant时,会顺带打印一句提示,比如“python-spider已沉睡了42天,再练一次只需20分钟即可恢复active”。这个提醒会把“重新捡起来”的心理门槛拆到一个很小的动作,亲测管用——我靠这个机制把一个差点彻底放下的乐器技能又拉了回来。
4.4 一个隐蔽的坑:记了时间但忘了记产出物
使用初期我发现,total_hours一直涨,但level涨得却很少,排查半天发现原因是output_bonus经常为零。原因是每次练习完我经常忘了输入--output 1,或者说压根没产生实在的输出物。后来我改变了自己的实践习惯:没产出物的练习不再记入时长,强迫自己每次练习至少有一个可交付的小产物——哪怕只是20行总结、一个最小Demo、或者一份错题笔记。规则越硬,水平曲线越健康。
下面整理了一份快速排障速查表,都是实测过的:
| 症状 | 可能原因 | 处理方式 |
|---|---|---|
| level长时间不动 | 单次练习时长刚好卡在15分钟以下 | 调整分档阈值,或提高单次练习质量 |
| streak_days一直为1 | 上次练习与昨天的间隔超过24小时 | 复盘时确认是否真的连续练习 |
| 报告里出现重复技能条目 | 数据库中有历史遗留的重复id | 用SQL去重后merge两条记录 |
| 数据文件在网盘同步后损坏 | 并发写入导致数据库锁/崩溃 | 备份文件后用sqlite3 .recover恢复 |
| review报告的日期区间不对 | 终端时区未设置 | 在环境变量中固定TZ=Asia/Shanghai |
5. 从命令行工具到个人管理方法论
5.1 使用节奏:高频记录,低频复盘
体验下来最舒适的使用节奏是:每天都记录,但不是实时记录。我习惯在每天晚上十分钟内,把当天的三段练习轨迹统一录入。这个节奏的好处是:白天的投入不会被“记录行为”打断,晚上录入有仪式感,它会像“给自己的账本结账”一样,让人对自己一天做了什么有个交代。
复盘则固定为每周日晚和每月一号。周六晚上的数据通常会比较难看,但那个难看恰恰是有用的——它会在新一周开始前给你一次调整计划的依据。
5.2 一些真实的使用实证
两个月前我给一个技能打上了“面试必备”的tag,每周固定投入三个小时,配合每两周输出一篇技术笔记的规则,现在这个技能已经从35分涨到61分,而且在某次临时被要求内部讲课时,我不需要额外准备就直接能讲出脉络。可以确定地说,任何技能只要坚持“规律投入+创造成品”这两个原则,数据增长只是水到渠成的事情。
还有一次我连续两周没有碰写作技能,系统把它标成了fading,第三周我打开review的时候看到一行警告“craft-writing正在降温,预计两周后跌回dormant”,这才重新把写作捡起来。那行警告文字救了我一整个月的写作手感。
5.3 这个项目的后续扩展空间
如果是你的话,有几个扩展方向很做:可以把review生成的markdown自动化推到个人博客或笔记仓库;可以把所有练习明细用Pandas做长周期趋势分析,画成周热度折线;也可以在每次status降级时通过系统通知推送提醒(目前是终端内打印,没有走推送)。不过个人的建议是:先用最朴素的形态跑起来,至少跑一个月,感受到数据带来的反馈之后,再决定要不要加功能。
我自己在这个项目上最大的收益不是把Python写得更溜,而是打通了“计划-练习-记录-复盘-修正”的完整闭环,它现在已经替代了我之前百分之八十的手工表格和打卡类App。如果你也想为某个技能建立一条清晰的成长曲线,照这个思路搭一个私有工具,会是性价比很高的选择。