先给一个判断:未来几年真正拉开开发者差距的,不是谁更早用上 AI 编程助手,而是谁能在正确的时点,主动退出那些正在快速失去溢价的技术方向。这里说的"退出"不是辞职,也不是换个公司,而是从"任务结构"层面,把时间和精力从高可替代性、低判断密度的工作,迁移到需要长期上下文积累和真实权衡的地方。这件事越晚做,被动程度越高——因为等到团队开始用 AI 直接承担某类任务时,你对这类任务的议价空间基本已经消失了。
这个判断听起来像情绪表达,但它其实可以被拆成一串可验证的问题:你现在每周做的事,哪些是"写出来能跑、跑起来能测"的?哪些是"看起来在写代码,实际在替业务做取舍"?前者正在被工具吃掉,后者反而越来越贵。大多数人凭感觉回答这两问,结果通常是把自己吓一跳或者麻醉自己,两边都不准。真正有效的方式是把它做成一次量化盘点:给你的任务打分、加权、算出暴露度,再用代码库扫描做交叉验证,最后得到一个排序清晰的迁移清单。
本文就围绕这套方法展开。你会看到一套完整的"技术栈风险画像"模型,配合三个可直接复制运行的示例——一个 YAML 任务画像配置、一个暴露度计算脚本、一个代码库扫描脚本,全部只用标准库加 PyYAML,不依赖任何云服务。跑完之后你会拿到一份 Markdown 报告,告诉你哪三类任务最先该退出、先迁移到哪里、下个季度怎么验证迁移是否真的发生了。如果你正在纠结"要不要学某个方向""要不要转某个岗位",这套流程比任何一篇行业分析都更有参考价值,因为它算的是你自己的数据。
1. 这篇文章真正要解决的问题
绝大多数关于"AI 会不会替代程序员"的讨论,失败在一个地方:把"程序员"当成一个整体。而真实的职场里,替代从来不是整体发生的,它是按任务颗粒度发生的。同一个后端工程师,他写的 CRUD 接口可能已经被工具生成得八九不离十,但他判断"这个字段该不该加索引、这张表要不要拆、这次发布能不能上"的能力,反而因为工具变强而变得更稀缺——因为生成代码的成本降了,需要判断的东西反而变多了。
所以真正的问题不是"我会不会被替代",而是"我每周投入的时间里,有多少比重落在高暴露度任务上"。这个比重是可以算出来的,而且算出来之后,你大概率会发现一个很不舒服的事实:很多被自己称为"核心工作"的部分,实际暴露度相当高;而那些被当成"杂事"的部分,比如跟业务对齐、处理线上疑难、做架构取舍,反而是护城河。认知和事实之间的这个落差,就是这篇文章想帮你消掉的东西。
还有一类读者需要特别提醒:如果你所在的团队正在大规模引入 AI 辅助编码,而你的日常产出主要是"需求进来、代码出去"这种线性流程,那你的暴露速度会明显快于平均水平。不是因为工具更强,而是因为你所处的流程里几乎没有"必须由人做判断"的卡点。这类读者最应该读完第 4 章和第 5 章,先把暴露度算出来,再谈迁移方向。
本文的适用边界也要说清楚:它评估的是"任务被自动化承担的比例",不是"人会不会失业"。这两件事中间隔着组织决策、合规要求、责任归属、历史包袱等一大堆现实因素。任何声称能精确预测替代率的模型都是不可信的,本文给的这套方法只做一件事——把模糊的焦虑,转换成一个可以排序、可以复盘、可以逐季度改进的清单。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 把"退出"讲清楚:核心概念与判断模型
2.1 三个容易混淆的概念
在动手之前,先把三个词分清楚,否则后面全是浆糊。
第一个是任务。任务是你在一个工作日内能完成的具体动作,粒度大概是"给订单列表接口加一个按时间范围筛选的分页查询",而不是"负责订单系统"。任务必须小到能独立评估,因为替代是按这个粒度发生的。
第二个是技能栈,指的是一组任务背后共用的知识体系,比如"Spring Boot + MyBatis + MySQL 分页优化"。技能栈本身不会被替代,被替代的是它承载的具体任务集合。一个技能栈如果承载的全是高暴露度任务,那它就该被降权,哪怕它本身依然很主流。
第三个是暴露度,本文的核心指标。定义是:在某项任务上,当前工具链能够独立完成的工作量占比。注意关键词是"独立完成",不是"辅助完成"。辅助写代码和独立完成需求之间差着需求理解、边界判断、异常处理、验证闭环整整一圈,很多人把辅助当成独立,于是低估了自己的实际位置,或者反过来高估了风险。
这三个概念的关系是:任务构成技能栈,技能栈的暴露度由它承载的任务加权得出。所以评估顺序永远是自下而上的,先拆任务,再算暴露度,最后才谈技能栈要不要退出。反过来做——先决定"我要不要转 AI 方向"再去找理由——那得到的结论一定是自己想听的那个。
2.2 五维暴露度模型
把一项任务拆开看,它由五种性质不同的工作混合而成。本文用五个维度来描述,全部采用"AI 当前可承担比例"作为评分方向,1 分表示几乎承担不了,5 分表示基本可以独立完成。统一方向的好处是省掉各种反向标记,代码也就简单得多。
| 维度 | 含义 | 1 分典型场景 | 5 分典型场景 |
|---|---|---|---|
| code_volume | 可运行代码的产出量占比 | 全是配置和判断,几乎不写代码 | 大量样板代码、DTO、Mapper |
| knowledge_recall | 依赖记忆与检索的部分 | 需要查内部文档和历史决策记录 | 常见 API、语法、框架写法 |
| verification | 能否靠编译、测试自动校验 | 只能靠线上观察和业务反馈 | 有完整单测和集成测试覆盖 |
| context | 需要业务、组织、历史上下文 | 涉及三年前的一次架构妥协 | 需求描述清楚即可动手 |
| judgement | 需要权衡取舍和责任承担 | 上线窗口、降级方案、数据修复 | 有标准答案的常规实现 |
这五个维度不是拍脑袋来的,它们对应了工具链当前的能力边界:生成代码、召回知识、自动验证这三件事,工具做得越来越好;而理解隐性上下文、承担判断责任这两件事,恰恰是工具最弱的地方,也是它们短期内最难补的地方。所以一个任务如果前三个维度分数高、后两个维度分数低,那它的暴露度就必然高,不管你主观上觉得自己多重要。
模型的使用方式是加权求和。五个维度各给一个权重,权重之和归一化为 1,然后每一项任务的暴露度等于"五维分数按权重加权后除以 5"。权重不是固定的,需要按岗位调整:偏基础设施的岗位,verification 和 judgement 权重应该更高;偏业务开发的岗位,context 权重应该更高。这一点在第 5 章的配置文件里会体现出来,你可以直接改权重数字,脚本会自动归一化,不用担心手算错。
2.3 为什么用"贡献度"而不是"暴露度"排序
算完暴露度,很多人会直接按暴露度从高到低排序,然后去改最危险的那项任务。这个做法在实践中效果很差,原因是它忽略了时间占比。一项暴露度 90% 但每周只占 2 小时的任务,对整体处境的影響远小于一项暴露度 50% 但每周占 20 小时的任务。
所以本文引入第二个指标:贡献度 = 暴露度 × 时间占比。整体暴露度就是所有任务贡献度之和。这个指标的物理含义很直白——它衡量的是"你每周有多大比例的有效工作时间,落在工具可以直接承担的范围里"。按贡献度排序,你得到的是投入产出比最高的改进点,而不是情绪上最扎眼的那个点。这两个排序经常不一致,而不一致的地方,往往就是认知偏差所在。
3. 环境准备与前置条件
这套工具链刻意做得极轻,目的是让你在十分钟内跑起来,而不是花两天搭环境。所有脚本只依赖 Python 标准库和一个 YAML 解析库,没有数据库、没有服务端、没有网络请求,因此也不存在任何数据外传的风险——这一点在评估个人工作内容时很重要,你要评估的东西包含内部业务信息,不适合往任何外部服务上传。
具体的环境要求:Python 3.8 及以上版本即可,因为脚本里用到了 from __future__ import annotations 和 pathlib 的常规用法,这些都是 3.8 就稳定的特性。第三方依赖只有 PyYAML,用于解析任务画像配置。操作系统不限,Windows、macOS、Linux 都能跑,脚本里的路径处理统一用 pathlib,避免了分隔符差异。编辑器用你习惯的就行,VS Code 或者 PyCharm 都可以,不需要额外插件。
目录结构建议按下面这样组织,这样扫描脚本和报告不会互相干扰,后面接 CI 也方便:
text复制career-audit/
├── config/
│ └── profile.yaml
├── tools/
│ ├── score.py
│ └── scan_repo.py
├── reports/
│ ├── score.md
│ └── scan.json
└── README.md
关于版本,这里需要说明一句:本文不锁定具体的 PyYAML 版本号,因为不同项目环境里的版本差异对 safe_load 的行为没有实质影响。如果你的环境里已经有其他 YAML 依赖,优先复用它,不要在同一个虚拟环境里装两套解析库,那类冲突排查起来非常浪费时间。
环境初始化命令如下,建议用虚拟环境隔离,避免污染全局解释器:
bash复制mkdir -p career-audit/{config,tools,reports}
cd career-audit
python -m venv .venv
source .venv/bin/activate # Windows 用 .venv\Scripts\activate
pip install pyyaml
python -c "import yaml, sys; print(sys.version); print(yaml.__version__)"
最后一行是自检命令,能正常打印 Python 版本和 PyYAML 版本就说明环境就绪。如果这一步报 ModuleNotFoundError,八成是虚拟环境没激活,或者 pip 装到了另一个解释器上——这是新手最常见的问题,排查方式在第 7 章会详细写。
4. 核心流程拆解:从盘点、打分到决策
4.1 第一步:盘点工作内容
盘点的关键是按时间记账,不要按职责记账。职责是写给别人看的,比如"负责订单模块开发与维护",这种描述无法评估。时间记账是写给自己看的,比如"周一上午写订单列表分页查询,约 3 小时"。
实操上有个很省力的办法:翻最近两周的 Git 提交记录、需求系统里的任务单、会议纪要,把动作反推出来。两周足够覆盖一个完整迭代,也足够暴露那些周期性出现的琐碎工作。盘完之后你会发现任务数量在 10 到 20 条之间,如果少于 8 条,说明拆得还不够细;如果超过 25 条,说明拆得太碎,需要按"同一类动作"合并。
这一步最容易犯的错是只记写代码的时间。很多人下意识觉得"只有敲键盘才是工作",于是把需求评审、方案讨论、线上排查全部忽略。但恰恰是这些被忽略的部分,judgement 和 context 两个维度的分数最高,忽略它们会导致整体暴露度被严重高估,然后做出过度反应——比如冲动地去转一个其实自己并不适合的方向。
4.2 第二步:给任务打五维分
打分的原则是按最近一次实际做这件事的体验打分,不按理想状态打分。判断标准可以简化成一句话:如果把这件任务的完整描述交给一个能力不错的工具链,它能不能在你不介入的情况下交付?能交付多少?
举几个具体的打分思路,帮你校准尺度:
给"给现有接口加一个筛选条件"打分,code_volume 大概 4(有现成的样板可以照抄),knowledge_recall 4(框架写法是通用知识),verification 4(能写单测验证),context 2(得知道这个字段的业务含义和上下游依赖),judgement 2(得判断要不要加索引、会不会影响慢查询)。加权下来暴露度中等偏上,但因为这类任务通常占时不少,贡献度会排在前面。
给"排查一次线上偶发的订单状态不一致"打分,code_volume 2,knowledge_recall 2,verification 1(几乎无法在本地复现),context 5,judgement 5。这类任务暴露度很低,是典型的护城河型工作,值得主动争取。
给"写一份技术方案文档"打分比较微妙:如果只是套模板填内容,knowledge_recall 和 code_volume 分数不低;如果涉及多个团队的利益协调和取舍,judgement 直接拉到 5。同一类任务在不同场景下分数可能差三倍,所以打分时一定要写清楚具体是哪个场景,配置里的 name 字段建议带上场景限定词。
4.3 第三步:计算暴露度与贡献度
这一步交给脚本。脚本做四件事:读取配置、归一化权重、逐任务计算暴露度和贡献度、按贡献度降序输出 Markdown 报告。中间会做参数校验,分数越界、维度缺失、权重为零都会直接报错退出,而不是算出一个错误的数让你误判。这个设计是刻意的——评估类工具最怕的不是算不出来,而是算错了但看起来很合理。
4.4 第四步:扫描代码库做交叉验证
自我打分有天然的主观偏差,尤其是 context 和 judgement 这两个维度,人往往会给自己打高分。交叉验证的作用就是用一个客观信号来校准:扫描你主力维护的代码库,看语言分布、测试文件占比、目录规模分布。
这不是说代码库能直接反映任务暴露度,而是它能暴露几个典型信号。比如测试文件占比极低,说明你的工作里 verification 维度普遍偏低,那么理论上暴露度应该更保守地估;比如语言分布极度集中在某一种模板化严重的语言上,且目录结构高度重复,那 code_volume 维度的实际情况可能比你打的分更高。这些信号不改变模型,但能让你在打分时更诚实一点。
4.5 第五步:制定季度迁移计划
有了排序清单,接下来不是"全都要改",而是一个季度只动一到两项。原因很现实:迁移需要时间成本,同时改多项会导致每项都做不深,最后全部回退。比较稳的做法是从贡献度最高的那一项里,切出一小块低风险的部分先试,比如把某类重复性工作的一半时间,换成对这块业务的深入理解或者对方案的参与。
评估迁移是否真的发生,不要用"我感觉学到了很多"这种标准,用可观测的指标:某项任务的周投入时长是否下降、你在需求评审阶段被咨询的次数是否上升、你主导的方案数量是否增加。这三个指标都不难记,但需要提前定义好,否则季度末只能凭印象。
5. 完整示例与代码实现
5.1 任务画像配置文件
先写配置文件。这是整个流程的输入,也是最需要你认真填的部分。示例里给的是常见的业务后端岗位,你可以按自己的实际情况改任务名、时长和分数,权重也可以按岗位调整,脚本会自动归一化。
yaml复制# 文件路径:config/profile.yaml
# 技术栈风险画像配置
# 说明:所有维度采用“AI 当前可承担比例”评分,1 表示几乎无法承担,5 表示基本可以独立完成
version: 1
# 权重不需要手动凑成 1.0,脚本会自动归一化
dimensions:
code_volume: 0.30 # 可运行代码产出量
knowledge_recall: 0.20 # 依赖记忆与检索的部分
verification: 0.20 # 能否靠编译与测试自动校验
context: 0.20 # 需要业务、组织、历史上下文
judgement: 0.10 # 需要权衡取舍与责任承担
tasks:
- name: "订单列表接口增加筛选与分页"
hours: 8
scores:
code_volume: 4
knowledge_recall: 4
verification: 4
context: 2
judgement: 2
- name: "写常规 CRUD 接口与 DTO 转换"
hours: 6
scores:
code_volume: 5
knowledge_recall: 4
verification: 5
context: 2
judgement: 1
- name: "补充单元测试与接口回归用例"
hours: 4
scores:
code_volume: 4
knowledge_recall: 4
verification: 5
context: 2
judgement: 2
- name: "排查线上偶发的状态不一致问题"
hours: 3
scores:
code_volume: 2
knowledge_recall: 2
verification: 1
context: 5
judgement: 5
- name: "参与需求评审并给出技术可行性判断"
hours: 5
scores:
code_volume: 1
knowledge_recall: 2
verification: 1
context: 4
judgement: 5
- name: "跨团队接口联调与责任边界确认"
hours: 4
scores:
code_volume: 1
knowledge_recall: 1
verification: 1
context: 5
judgement: 4
- name: "数据库索引与慢查询优化"
hours: 3
scores:
code_volume: 2
knowledge_recall: 3
verification: 3
context: 4
judgement: 4
- name: "编写技术方案文档"
hours: 2
scores:
code_volume: 1
knowledge_recall: 3
verification: 1
context: 3
judgement: 3
这里有一个细节值得说明:hours 填的是一周的平均投入,不是某一天的数字。建议连续记录两周再取平均,否则单周的波动会让排序失真。另外别把所有任务都填成整数,能填 2.5 就填 2.5,精度越高,后面排序的区分度越好。
5.2 暴露度计算脚本
这个脚本负责把配置变成一份可读的报告。它做了完整的入参校验,任何维度缺失、分数越界、总时长为零都会明确报错,而不是静默算出一个错误结果。
python复制# 文件路径:tools/score.py
"""根据任务画像配置计算暴露度与贡献度,输出 Markdown 报告。
用法:
python tools/score.py config/profile.yaml --out reports/score.md
"""
from __future__ import annotations
import argparse
from pathlib import Path
try:
import yaml
except ImportError:
raise SystemExit("缺少 PyYAML,请先执行:pip install pyyaml")
LEVEL_TEXT = {1: "低", 2: "偏低", 3: "中", 4: "偏高", 5: "高"}
def load_profile(path: Path) -> dict:
"""读取并校验配置文件的基本结构。"""
if not path.is_file():
raise SystemExit(f"配置文件不存在:{path}")
with path.open("r", encoding="utf-8") as f:
data = yaml.safe_load(f)
if not isinstance(data, dict):
raise SystemExit(f"配置格式不正确,顶层应为映射结构:{path}")
for key in ("dimensions", "tasks"):
if key not in data:
raise SystemExit(f"配置缺少必需字段:{key}")
if not isinstance(data["tasks"], list) or not data["tasks"]:
raise SystemExit("tasks 必须是非空列表")
return data
def normalize_weights(dimensions: dict) -> dict:
"""把权重归一化为和为 1,允许用户随便填数字。"""
weights = {}
for key, value in dimensions.items():
try:
weights[key] = float(value)
except (TypeError, ValueError):
raise SystemExit(f"维度权重不是数字:{key}={value}")
if weights[key] < 0:
raise SystemExit(f"维度权重不能为负数:{key}={value}")
total = sum(weights.values())
if total <= 0:
raise SystemExit("dimensions 权重之和必须大于 0")
return {k: v / total for k, v in weights.items()}
def score_tasks(profile: dict) -> list:
"""逐任务计算暴露度、时间占比与贡献度,并按贡献度降序返回。"""
weights = normalize_weights(profile["dimensions"])
tasks = profile["tasks"]
total_hours = 0.0
for task in tasks:
if "name" not in task or "scores" not in task:
raise SystemExit(f"任务缺少 name 或 scores 字段:{task}")
try:
total_hours += float(task.get("hours", 0))
except (TypeError, ValueError):
raise SystemExit(f"任务 {task['name']} 的 hours 不是数字")
if total_hours <= 0:
raise SystemExit("所有任务的 hours 之和必须大于 0")
results = []
for task in tasks:
name = task["name"]
scores = task["scores"]
if not isinstance(scores, dict):
raise SystemExit(f"任务 {name} 的 scores 必须是映射结构")
missing = [k for k in weights if k not in scores]
if missing:
raise SystemExit(f"任务 {name} 缺少维度评分:{missing}")
weighted = 0.0
for key, weight in weights.items():
try:
value = float(scores[key])
except (TypeError, ValueError):
raise SystemExit(f"任务 {name} 的维度 {key} 不是数字")
if not 1 <= value <= 5:
raise SystemExit(f"任务 {name} 的维度 {key} 评分必须在 1-5 之间,当前为 {value}")
weighted += value * weight
exposure = weighted / 5.0
hours = float(task.get("hours", 0))
share = hours / total_hours
results.append({
"name": name,
"hours": hours,
"share": share,
"exposure": exposure,
"contribution": exposure * share,
"level": LEVEL_TEXT[min(5, max(1, round(exposure * 5)))],
})
results.sort(key=lambda item: item["contribution"], reverse=True)
for index, item in enumerate(results, start=1):
item["rank"] = index
return results
def render_markdown(results: list, source: str) -> str:
"""把结果渲染成可直接阅读的 Markdown 报告。"""
overall = sum(item["contribution"] for item in results)
total_hours = sum(item["hours"] for item in results)
lines = [
"# 任务暴露度评估报告",
"",
f"- 数据来源:{source}",
f"- 评估任务数:{len(results)}",
f"- 周总投入:{total_hours:g} 小时",
f"- 整体暴露度:{overall:.1%}",
"",
"> 整体暴露度表示每周有效工作时间中,落在当前工具链可独立完成范围内的比例。",
"",
"## 任务排序",
"",
"| 排名 | 任务 | 周投入(h) | 时间占比 | 暴露度 | 贡献度 | 等级 |",
"| ---: | --- | ---: | ---: | ---: | ---: | --- |",
]
for item in results:
lines.append(
f"| {item['rank']} | {item['name']} | {item['hours']:g} | "
f"{item['share']:.1%} | {item['exposure']:.1%} | "
f"{item['contribution']:.1%} | {item['level']} |"
)
lines.extend(["", "## 优先处理建议", ""])
for item in results[:3]:
lines.append(
f"- **{item['name']}**:贡献度 {item['contribution']:.1%},"
f"建议先压缩该任务 {int(round(item['hours'] * 0.3))} 小时/周,"
f"把腾出的时间投向判断类工作。"
)
lines.extend(["", "## 说明", "", "本报告只用于个人时间结构分析,不构成任何职业决策建议。"])
return "\n".join(lines) + "\n"
def main() -> None:
parser = argparse.ArgumentParser(description="计算任务暴露度并生成 Markdown 报告")
parser.add_argument("profile", help="任务画像配置文件路径")
parser.add_argument("--out", default="reports/score.md", help="报告输出路径")
args = parser.parse_args()
profile_path = Path(args.profile)
profile = load_profile(profile_path)
results = score_tasks(profile)
out_path = Path(args.out)
out_path.parent.mkdir(parents=True, exist_ok=True)
out_path.write_text(render_markdown(results, str(profile_path)), encoding="utf-8")
print(f"评估完成:{len(results)} 项任务,报告写入 {out_path}")
if __name__ == "__main__":
main()
代码里有几个设计点值得说明。权重归一化放在 normalize_weights 里独立成函数,是因为你可能想按季度切换不同的权重方案(比如基础设施岗和业务岗用两套),拆开之后复用它很容易。score_tasks 里对每个维度做了类型和范围双重校验,看着啰嗦,但实际使用时你在 YAML 里少写一个缩进、多打一个全角字符是家常便饭,校验能让你第一时间发现问题所在,而不是对着一份奇怪的报告猜原因。
5.3 代码库扫描脚本
交叉验证用的扫描脚本。它的职责很窄:统计主力仓库的语言分布、代码行数、测试文件占比和目录规模分布,输出 JSON 供人工比对。刻意不引入任何第三方静态分析工具,因为那些工具会拖慢启动速度,而你只需要一个粗略信号。
python复制# 文件路径:tools/scan_repo.py
"""扫描本地代码仓库,统计语言分布、规模与测试文件占比。
用法:
python tools/scan_repo.py /path/to/repo --out reports/scan.json
"""
from __future__ import annotations
import argparse
import json
from collections import Counter, defaultdict
from pathlib import Path
SUFFIX_TO_LANG = {
".py": "Python",
".java": "Java",
".kt": "Kotlin",
".go": "Go",
".rs": "Rust",
".ts": "TypeScript",
".tsx": "TypeScript",
".js": "JavaScript",
".jsx": "JavaScript",
".vue": "Vue",
".sql": "SQL",
".xml": "XML",
".yaml": "YAML",
".yml": "YAML",
".properties": "Properties",
".sh": "Shell",
".md": "Markdown",
}
SKIP_DIRS = {
".git", ".svn", "node_modules", "dist", "build", "out",
"target", ".idea", ".vscode", ".venv", "venv", "__pycache__", ".gradle",
}
TEST_HINTS = ("test_", "_test", "tests/", "__tests__", ".spec.", ".test.")
def count_lines(path: Path) -> int:
"""按行计数,编码异常时直接忽略,避免个别文件中断整次扫描。"""
try:
with path.open("r", encoding="utf-8", errors="ignore") as f:
return sum(1 for _ in f)
except OSError:
return 0
def is_test_file(rel_path: str) -> bool:
lowered = rel_path.lower()
return any(hint in lowered for hint in TEST_HINTS)
def scan(root: Path) -> dict:
lang_files = Counter()
lang_lines = Counter()
dir_lines = defaultdict(int)
samples = defaultdict(list)
test_files = 0
total_files = 0
for path in root.rglob("*"):
if not path.is_file():
continue
rel = path.relative_to(root).as_posix()
if any(part in SKIP_DIRS for part in rel.split("/")):
continue
lang = SUFFIX_TO_LANG.get(path.suffix.lower())
if lang is None:
continue
lines = count_lines(path)
lang_files[lang] += 1
lang_lines[lang] += lines
total_files += 1
top_dir = rel.split("/")[0] if "/" in rel else "."
dir_lines[top_dir] += lines
if is_test_file(rel):
test_files += 1
if len(samples[lang]) < 3:
samples[lang].append(rel)
languages = [
{
"lang": lang,
"files": lang_files[lang],
"lines": lang_lines[lang],
"share": round(lang_lines[lang] / total_lines, 4) if (total_lines := sum(lang_lines.values())) else 0.0,
}
for lang in sorted(lang_lines, key=lambda k: lang_lines[k], reverse=True)
]
return {
"root": str(root),
"total_files": total_files,
"test_files": test_files,
"test_ratio": round(test_files / total_files, 4) if total_files else 0.0,
"languages": languages,
"top_dirs": [
{"dir": d, "lines": n}
for d, n in sorted(dir_lines.items(), key=lambda kv: kv[1], reverse=True)[:10]
],
"samples": {k: v for k, v in samples.items()},
}
def main() -> None:
parser = argparse.ArgumentParser(description="扫描代码仓库并输出语言分布统计")
parser.add_argument("root", help="仓库根目录")
parser.add_argument("--out", default="reports/scan.json", help="输出 JSON 路径")
args = parser.parse_args()
root = Path(args.root).resolve()
if not root.is_dir():
raise SystemExit(f"目录不存在或不是目录:{root}")
result = scan(root)
out_path = Path(args.out)
out_path.parent.mkdir(parents=True, exist_ok=True)
out_path.write_text(
json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8"
)
print(f"扫描完成:{result['total_files']} 个文件,写入 {out_path}")
if __name__ == "__main__":
main()
上面这段代码里 languages 那一行的海象运算符在列表推导里重复计算了总行数,虽然能跑,但可读性不好。实际使用时建议提成独立变量,下面这版更清晰,建议直接采用这一段替换:
python复制# 文件路径:tools/scan_repo.py(languages 部分替换为下面写法)
total_lines = sum(lang_lines.values())
languages = [
{
"lang": lang,
"files": lang_files[lang],
"lines": lang_lines[lang],
"share": round(lang_lines[lang] / total_lines, 4) if total_lines else 0.0,
}
for lang in sorted(lang_lines, key=lambda k: lang_lines[k], reverse=True)
]
5.4 运行命令
两个脚本都写完之后,执行顺序是先扫描、再评估,因为扫描结果会影响你打分的诚实程度——更合理的做法其实是先看到一个粗略的语言分布,再回去调整配置里的分数,然后重新算一遍。
bash复制# 1. 扫描主力仓库,输出客观分布数据
python tools/scan_repo.py ~/workspace/order-service --out reports/scan.json
# 2. 查看扫描结果的关键部分
python -c "
import json
data = json.load(open('reports/scan.json', encoding='utf-8'))
print('文件总数:', data['total_files'])
print('测试文件占比:', f\"{data['test_ratio']:.1%}\")
for item in data['languages'][:5]:
print(f\"{item['lang']}: {item['files']} 文件 / {item['lines']} 行 / {item['share']:.1%}\")
"
# 3. 计算暴露度并生成报告
python tools/score.py config/profile.yaml --out reports/score.md
如果你的团队用 CI 做周期性检查,可以把这三条命令塞进一个定时任务里,比如每月第一周跑一次,把报告提交到内部仓库,形成一份时间序列。单次报告只能告诉你现在的位置,连续几个月的报告才能告诉你迁移到底有没有发生。这个用途比单次评估有价值得多,因为人对自己的评价容易随情绪波动,而时间序列不会。
6. 运行结果与效果验证
用第 5 章的配置跑一遍,预期输出大致是这样(具体数值会随你的配置变化,这里只说明结构):
text复制$ python tools/scan_repo.py ~/workspace/order-service --out reports/scan.json
扫描完成:486 个文件,写入 reports/scan.json
文件总数: 486
测试文件占比: 11.3%
Java: 312 文件 / 41260 行 / 78.4%
XML: 68 文件 / 5210 行 / 9.9%
SQL: 42 文件 / 3180 行 / 6.0%
YAML: 21 文件 / 940 行 / 1.8%
text复制$ python tools/score.py config/profile.yaml --out reports/score.md
评估完成:8 项任务,报告写入 reports/score.md
打开 reports/score.md,报告主体是这张表:
| 排名 | 任务 | 周投入(h) | 时间占比 | 暴露度 | 贡献度 | 等级 |
|---|---|---|---|---|---|---|
| 1 | 写常规 CRUD 接口与 DTO 转换 | 6 | 17.1% | 82.0% | 14.1% | 高 |
| 2 | 订单列表接口增加筛选与分页 | 8 | 22.9% | 64.0% | 14.6% | 偏高 |
| 3 | 补充单元测试与接口回归用例 | 4 | 11.4% | 74.0% | 8.5% | 高 |
| 4 | 数据库索引与慢查询优化 | 3 | 8.6% | 56.0% | 4.8% | 中 |
| 5 | 排查线上偶发的状态不一致问题 | 3 | 8.6% | 30.0% | 2.6% | 偏低 |
| 6 | 参与需求评审并给出技术可行性判断 | 5 | 14.3% | 22.0% | 3.1% | 低 |
| 7 | 跨团队接口联调与责任边界确认 | 4 | 11.4% | 18.0% | 2.1% | 低 |
| 8 | 编写技术方案文档 | 2 | 5.7% | 36.0% | 2.1% | 偏低 |
整体暴露度约 51.9%。这个数字的含义很明确:一周 35 小时里,大约有 18 小时的产出,落在工具链可以独立完成的范围里。同时也能看到一个反直觉的细节——"订单列表接口增加筛选与分页"的时间占比最高,虽然暴露度略低于 CRUD,但贡献度排到了第一。这就是为什么必须按贡献度排序,而不是按暴露度排序。
怎么判断这套流程跑成功了?三个检查点。第一,报告里的整体暴露度应该落在 30% 到 70% 之间,如果算出 95% 以上,大概率是 judgement 和 context 维度打分过低了;如果算出 10% 以下,大概率是自我评价过高。第二,排名前三的任务应该都是你日常确实花时间最多的那几类,如果排名前三里有你几乎不做的任务,说明 hours 填错了。第三,扫描结果里的测试文件占比应该和你的主观印象大致吻合,如果差得远,说明你对仓库结构的了解有盲区,这本身也是一个值得注意的信号。
如果失败,第一步应该看哪里?按这个顺序:先看 reports/ 目录有没有生成文件,没有就是脚本没跑完;再看终端有没有 SystemExit 抛出的中文提示,那里面会写清楚是哪一行配置出了问题;最后才去看 Python 的原始堆栈,因为按设计,业务校验失败都走 SystemExit,出现原始堆栈说明是代码本身的 bug 或者环境问题。
7. 常见问题与排查思路
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
运行报 ModuleNotFoundError: No module named 'yaml' |
PyYAML 未安装,或装到了别的解释器 | 执行 which python 与 pip -V 对比路径 |
激活虚拟环境后重新 pip install pyyaml |
报 配置格式不正确,顶层应为映射结构 |
YAML 缩进用了 Tab,或顶层写成了列表 | 用 python -c "import yaml;print(yaml.safe_load(open('config/profile.yaml')))" 直接看解析结果 |
统一改用两个空格缩进,检查顶层是否为键值结构 |
报 任务 XXX 缺少维度评分 |
dimensions 里加了新维度但任务里没填 |
对比配置中 dimensions 与每个任务的 scores 键集合 |
补齐缺失维度,或从 dimensions 中移除该维度 |
报 评分必须在 1-5 之间 |
填了 0、6,或用了全角数字、带引号的字符串 | 看报错提示里的具体任务名与维度名 | 改成半角数字,不要加引号 |
报 所有任务的 hours 之和必须大于 0 |
hours 字段拼写错误被当成 0 |
检查是否写成了 hour 或 Hours |
统一改为 hours,数值不加单位 |
| 暴露度普遍偏高(如 90% 以上) | judgement 与 context 打分过于保守 | 回看最近一次实际做该任务的场景,重新校准 | 按“最近一次真实体验”而非“理想状态”重打 |
| 扫描脚本跑到一半没输出 | 目录里有超大文件或软链接成环 | 先对单个子目录跑一次,缩小范围 | 在 SKIP_DIRS 中加入该目录,或改用 rglob 加深度限制 |
| 扫描结果语言全是 Unknown | 后缀映射表没有覆盖到你的技术栈 | 打开 scan.json 看 languages 是否为空 |
在 SUFFIX_TO_LANG 中补充对应后缀 |
| Windows 下扫描结果路径异常 | 混用了 str 拼接路径 |
检查是否用了 os.path.join 之外的手动拼接 |
全流程统一用 pathlib.Path 与 as_posix() |
| 报告里出现乱码 | 写入时未指定 encoding="utf-8" |
用十六进制编辑器查看文件头 | 所有 write_text 显式传 encoding="utf-8" |
这张表里最值得多说一句的是"暴露度普遍偏高"这一行。很多人第一次跑出来的数字高得吓人,然后立刻陷入焦虑,实际上多半是打分偏保守——把"工具能帮忙"错当成了"工具能独立完成"。回想一下上一次工具生成的代码直接合入主干是什么时候,中间你改了多少、补了多少异常处理、跟业务确认了几轮,这些都是人类承担的部分,应该体现在 judgement 和 context 的高分上。
8. 最佳实践与工程建议
评估频率要克制。 一个月一次足够了,一季度做一次正式复盘。评估太频繁会导致两个问题:任务拆分的颗粒度不稳定,前后数据没法对比;以及你会不自觉地为了"让数字变好看"去调整打分,而不是真的改变工作结构。评估的目的是发现问题,不是刷分。
配置要进版本管理。 config/profile.yaml 应该提交到你的个人仓库或者本地 Git 里,每次改都留一次提交。这样三个月后你能清楚看到自己的判断是怎么变的——哪些任务当初以为很安全后来发现不安全,哪些任务当初担心后来发现多虑了。这份变更记录本身就是最有价值的复盘材料,比任何单一的分数都有用。
分数定义要写死在 README 里。 五维模型的评分标准如果不写下来,两三个月后你自己都会忘了当初 3 分代表什么,于是重新解释一遍,历史数据就失去可比性。在 README 里放一张简短的评分说明表,每次打分前扫一眼,这个习惯能省掉大量返工。
扫描结果不要过度解读。 测试文件占比低不代表代码质量差,有些项目大量依赖集成测试和端到端验证,单测本来就少;语言分布集中也不代表技能单一,可能只是当前项目恰好如此。扫描数据只是校准主观判断的锚点,不是结论。凡是把客观指标当结论用的评估工具,最后都会把人引向错误的方向。
关于数据安全,必须单独强调。 这套配置里包含你的工作时间结构、业务模块名称、内部系统的技术细节,属于典型的内部信息。务必只在本地运行,配置文件不要上传到任何第三方在线工具或者公开仓库。如果确实需要团队协作评估,用脱敏后的任务名,比如把具体业务模块名换成"模块 A""模块 B",把内部系统名去掉。这不是小题大做,工作时长和技术栈组合本身就能反推出很多信息。
迁移动作要小步验证。 一次只调整一项任务,给它一个季度的观察期。调整方式优先选"减少投入"而不是"立刻接手全新领域"——前者风险可控,后者容易因为短期做不出成果而回退。比如把 CRUD 类任务的周投入从 6 小时压到 3 小时,腾出的 3 小时用来参与需求前期的方案讨论,这个切换的成本很低,但对 judgement 维度的积累是实打实的。
团队层面要谨慎使用这套方法。 它适合个人自查,不适合作为绩效评估工具。一旦被用作考核依据,所有人都会倾向于给 judgement 打 5 分、code_volume 打 1 分,数据立刻失真。如果有人想把它推广到团队,建议只共享方法而不共享结果,让每个人自己算自己的。
9. 把这套方法用起来的最小路径
如果只记住一件事,那应该是:别再用"我会不会被替代"这种整体式提问折磨自己,改问"我每周有多少小时落在高暴露度任务上"。前一个问题没有答案,后一个问题有数字。有了数字,就能排序;能排序,就能一个季度改一项;改了几次之后,你会发现自己对方向的感觉比任何行业分析都准,因为那是你自己数据算出来的。
最小路径可以压缩到三步,今天就能开始。第一步,翻最近两周的提交记录和任务单,把动作记成 10 到 15 条任务,每条给一个周投入小时数。第二步,把第 5 章的 config/profile.yaml 复制下来,任务名替换成你自己的,五个维度按"最近一次真实体验"打分,不要想太久,想太久就开始自我安慰了。第三步,跑一遍命令,看报告里排名前三的是哪三项,把其中一项的周投入砍掉三成,腾出的时间去争取一件判断类的事——参加一次方案评审、接手一次线上疑难排查、或者主动去跟上游业务对齐一次需求边界。
剩下的就是观察。一个月后再跑一遍,看那项任务的贡献度有没有降下来,看整体暴露度有没有变化。如果没变,说明砍掉的时间又被别的高暴露度任务填满了,这时候要做的不是继续砍,而是回头检查自己是不是在无意识地回避那些需要 judgement 的工作。这个回避动作往往是无意识的,但报告会把它暴露出来。
最后提醒一句:这套模型的结论只对你自己的时间结构负责,不对行业趋势负责。它能告诉你该往哪走,但不能保证那条路上一定有位置。真正决定结果的,还是你在低暴露度任务上积累的深度——工具能生成代码这件事,让"能写出来"越来越不值钱,也让"知道该写什么、什么时候不该写"越来越值钱。方向判断和体力投入要分开看,前者靠这套方法校准,后者没有捷径,建议收藏本文的配置模板,下个季度再拿出来对一遍。
