Python技术迷

我用 SKILL.md 做了一个简历生成器

同一份简历数据,第一次生成像后端,第二次生成像产品,第三次连“负责核心模块”这种废话都冒出来了。

我第一眼就不太信模型。

简历生成器这东西,最怕的不是生成慢,也不是样式丑,而是它太会“补”。候选人只写了“维护订单接口”,它能给你扩成“主导订单中台架构升级”。看起来顺眼,投出去就是坑。

所以我后来没把重点放在 prompt 上,而是单独写了一个 SKILL.md。

这个文件不是简历模板,更像一份现场规矩。哪些词不能用,哪些经历必须保留原事实,项目描述按什么顺序写,技术栈不能乱扩,成果没数字就别硬编数字,都写进去。

大概长这样:

# Resume Skill

## 生成原则
- 不编造项目、公司、学校、时间
- 没有量化指标时,不主动补百分比、QPS、DAU
- 项目经历优先按:背景 -> 动作 -> 结果 写
- 技术栈只允许来自 user_profile.skills
- 禁止使用:精通、主导、赋能、闭环、抓手、深度参与

## Python 岗位偏好
- 强调脚本自动化、数据清洗、接口开发、日志分析
- 少写空泛协作,多写处理过什么脏数据、什么异常、什么边界

一开始我是把这些规则直接塞进 prompt。

后来发现不行。

prompt 一长,业务数据一多,模型就会挑着执行。尤其是禁用词、事实约束这种东西,很容易被后面的简历内容冲掉。所以我把 SKILL.md 当成一个固定输入,每次生成前先解析,再拼成更硬的任务描述。

代码没搞复杂,就一个读取器:

from pathlib import Path

classSkillBook:
def__init__(self, skill_file: str):
        self.raw = Path(skill_file).read_text(encoding="utf-8")
        self.rules = self._pick_rules()

def_pick_rules(self):
        blocks = {}
        current = None

for line in self.raw.splitlines():
            line = line.strip()
ifnot line:
continue

if line.startswith("## "):
                current = line.replace("## ", "").strip()
                blocks[current] = []
continue

if current:
                blocks[current].append(line.lstrip("- ").strip())

return blocks

defget(self, name: str):
return self.rules.get(name, [])

这里我没用什么 Markdown 解析库。不是不能用,是没必要。

SKILL.md 这类文件最好别写得太花,越像配置越好。标题一层,规则一行一条,后面排查问题也方便。

简历原始数据我用 JSON 放着,结构也尽量土一点:

profile = {
"name": "陈某",
"target": "Python 后端开发",
"skills": ["Python", "FastAPI", "MySQL", "Redis", "Pandas", "Linux"],
"projects": [
        {
"name": "销售数据清洗平台",
"role": "后端开发",
"facts": [
"接入 Excel、CSV 两类文件",
"处理字段缺失、日期格式混乱、金额带中文单位的问题",
"提供导入任务查询接口",
"失败记录可下载"
            ]
        }
    ]
}

这地方我踩过一次坑。

不要把原始经历写成大段自然语言。大段文字一进去,模型就会顺着原文润色,润着润着就开始“显得更厉害”。最好拆成事实点,生成时只允许围绕 facts 改写。

拼 prompt 的时候,我会把规则放在前面,用户事实放在后面,再单独加一段硬约束。

defbuild_resume_prompt(profile: dict, skill: SkillBook) -> str:
    base_rules = "\n".join(f"- {x}"for x in skill.get("生成原则"))
    job_rules = "\n".join(f"- {x}"for x in skill.get("Python 岗位偏好"))

returnf"""
你是一个简历生成器,只能基于给定事实写简历。

通用规则:
{base_rules}

岗位规则:
{job_rules}

候选人数据:
{profile}

输出要求:
1. 使用中文
2. 不新增公司、项目、时间、指标
3. 不使用禁用词
4. 项目经历每条控制在 2 行以内
5. 输出 Markdown 简历
"""

.strip()

我不太喜欢一上来就让它生成 Word。

先生成 Markdown,肉眼能看,程序也好检查。Word 是最后一步。否则样式、内容、结构混在一起,出了问题你都不知道该怪谁。

生成完还不能直接保存,得过一遍校验。

尤其是禁用词和技术栈。

FORBIDDEN = {"精通", "主导", "赋能", "闭环", "抓手", "深度参与"}

defcheck_resume(text: str, profile: dict):
    errors = []

for word in FORBIDDEN:
if word in text:
            errors.append(f"出现禁用词:{word}")

    allowed_skills = set(profile["skills"])
    suspicious = ["Go", "Kubernetes", "Spark", "Vue", "Docker"]

for item in suspicious:
if item in text and item notin allowed_skills:
            errors.append(f"疑似新增技术栈:{item}")

if"提升"in text and"%"in text:
        errors.append("出现疑似编造量化结果")

return errors

这个校验很粗,但够用。

简历生成不是论文纠错,不需要一开始就搞一套复杂 NLP。先把最容易翻车的地方拦住:乱加技术栈、乱写指标、乱用大词。

有一次原始数据里只写了“用 Pandas 处理 Excel”,生成结果里冒出来一句:

基于 Spark 完成百万级数据清洗任务,显著提升处理效率。

这句看起来挺漂亮,但候选人压根没写 Spark,也没写百万级。

这种简历投出去,面试官追问两句就露馅。

所以后面我又加了一个重试逻辑。校验不过,就把错误塞回去,让它按错误修。

defgenerate_with_retry(client, profile, skill, max_round=2):
    prompt = build_resume_prompt(profile, skill)

    last_text = ""
for i in range(max_round + 1):
        text = client.generate(prompt)
        errors = check_resume(text, profile)

ifnot errors:
return text

        last_text = text
        prompt = f"""
下面这份简历违反了规则,请只修正违规内容,不要新增事实。

违规点:
{errors}

原简历:
{text}
"""

.strip()

raise ValueError(f"简历仍未通过校验:{check_resume(last_text, profile)}")

这里有个小判断。

重试次数别太多。超过两轮还过不了,通常不是模型问题,是你的输入数据太乱,或者 SKILL.md 规则互相打架。

比如一边要求“突出结果”,一边又不给任何结果事实。那模型肯定会忍不住补。

最后一步才是渲染。

我这里没上复杂模板,只把 Markdown 分段写进 docx。真正投递用的简历,样式越克制越好。花里胡哨的图标、进度条、技能评分,我一般都删。

from docx import Document

defexport_docx(markdown_text: str, output: str):
    doc = Document()

for line in markdown_text.splitlines():
        line = line.strip()
ifnot line:
continue

if line.startswith("# "):
            doc.add_heading(line[2:], level=1)
elif line.startswith("## "):
            doc.add_heading(line[3:], level=2)
elif line.startswith("- "):
            doc.add_paragraph(line[2:], style="List Bullet")
else:
            doc.add_paragraph(line)

    doc.save(output)

做完之后,整个流程就清楚了:

resume.json
   ↓
SKILL.md 规则读取
   ↓
生成 Markdown
   ↓
规则校验
   ↓
失败重试
   ↓
导出 docx

这个生成器最值钱的地方,不是“自动生成简历”。

自动生成很容易。

真正有用的是它不会乱写,或者至少乱写了能被拦下来。

SKILL.md 在这里像一个老审核员,脾气不太好,看到“精通”“主导”“提升 80%”这种没证据的词就直接打回。简历可以写得好看,但不能写得像另一个人。

后面我准备再加两个检查。

一个是时间线检查,比如工作经历不能重叠得太离谱。

一个是岗位匹配检查,比如投 Python 后端,就少写“熟练使用 Office”,多写接口、脚本、SQL、日志处理。

简历生成器做到最后,拼的不是模型多聪明,而是规则够不够硬。

模型负责把话写顺。

SKILL.md 负责把它按住。