模拟面试结束之后,一位学员把这段时间的学习记录打包发给我,问我一句:这样算不算补够了。
我点开看了一遍。里面有课程笔记、有三个项目的代码、有几十篇收藏的文章,还有一份改到第七版的简历。东西不少,但他这个问题我答不了。
不是我不想答,是这个问题本身没有验收标准。
「补够了没有」听起来像在等一个时刻,等到某一天有人告诉他可以投了。但底子这件事没有终点线,它只有信号。你学完一门课、跑通一个项目、把简历改到第七版,这三件事都只证明你做了动作,不证明动作落到了地方。
这位学员的情况在我这儿算典型:6 年 Go,地产 SaaS,普通本科,裸辞出来转 AI。裸辞头两个月,招聘平台上打招呼的不算少,真正约到面的没几家,面完的多半是回去等通知。他自己把原因归到「零 AI 经验、岗位不匹配」上,这两个月也一直在往这个方向堆东西。
我听完他复述的那三分钟面试,判断是另一回事:缺的不是 AI 经验这个标签,是标签下面那三块底子没有可验收的进度。
上一篇写的是这三块底子怎么补、按什么顺序补。这篇换到另一头:补到什么程度算数。答案不是再给一张更长的清单,而是每一块各认一个信号,十分钟之内自己就能验完。
一、为什么「我学完了」不算验收
自学转 AI 的人手边最常见的三种进度条是这样的:课程过了一遍,项目本地跑通了,简历排版改好了。
这三句话有个共同点,描述的都是你自己这侧的动作,不是对面那侧的判断。面试官不在乎你看没看完,他在乎的是你把这话说完之后,还能不能接住他的下一句。
所以我跟他定的原则只有一条:每块底子只认一个信号,而且这个信号不能由自己给自己打分。
为什么强调只认一个?因为信号一多,人就会挑对自己有利的那个。三个信号里过了两个,绝大多数人的结论是差不多了,唯独最严格的那个标准被悄悄丢掉。
下面三块按验收难度从低到高排。每一块我都先拿他当时验不过的样子开刀,因为不看验不过的样子,信号就只是一句正确的废话。
二、底子一 · 业务匹配度:信号是「让一个不懂你行业的人,说出它对应 AI 项目的哪个难点」
他第一次讲自己最拿得出手的那段经历时,讲的是自己负责某核心交易链路的后端开发,行业术语密集,讲了好几分钟也没进入正题。
我让他把同样一段讲给一个做 AI 方向、完全不熟地产的朋友听。讲完对方挺客气,但接不住,大意是听着挺厉害、说不出这跟他做的事情有什么关系。
这个反应就是没通过的样子。它说明句子上的业务名词没有被剥掉,剥掉之后剩下的东西还不够硬,硬到让一个外行自己认出来。
剥词这件事有三步:把业务名词剥掉只留约束,把约束翻译成目标岗位的行话,再补一句这个约束在 AI 项目里会怎么坑人。这三步上一篇详细写过,这里不重复。本篇关心的是它有没有做成,而验收只看一个动作:
把剥完的那句话讲给一个不熟你行业的人,两分钟讲完。对方如果自己说出「这不就是某某场景里最难的那部分吗」,翻译就算过了。
他按那三步重写之后,同一段经历变成「一条多环节、有状态、不能重跑、出错要回滚的链路」。再讲一遍,那个朋友的反应变了,大意是自己接过话说这不就是做 Agent 编排时最容易出事的地方。
信号也就清楚了,对照如下:
| 维度 | 真信号 | 假信号 |
|---|---|---|
| 判断依据 | 外行听完能自己说出对应关系 | 简历里高频词的数量 |
| 谁来判断 | 听你讲的人 | 你自己 |
| 讲完耗时 | 两分钟以内 | 越长越说明没剥干净 |
| 不通过的样子 | 对方接不住,只能说听着挺厉害 | 词都加上了,但没人追问 |
表里第二行是关键。凡是能由自己给自己打分的验收,基本都不算验收。他前面那两个月的毛病有两面:对外归错了因,对内又用最简单的那把尺子给自己判卷,这两件事是连着的:一直用自己这边做了什么来判卷,才会把原因归到外面去。
三、底子二 · 项目深度:信号是「同一个问题往下追三层,第三层不空」
这一块最花时间,也是他真正缺的那一块。
先定义层。第一层,你做了什么。第二层,你是怎么做的。第三层,你这么定的依据是什么。第四层,出了问题你怎么定位。
验收线划在第三层。 第四层不是不重要,它是加分项。但多数人卡不住第四层,是因为第三层本来就是空的,不是因为第四层太难。所以先别往上够。
他当时的问答机器人,第一层讲得很顺,接模型、套检索、做增强问答。第二层也能讲,失败加重试。追到第三层,问他那几个数是怎么定的,就空了。
不是他不会写重试,是他定的时候没有依据,参数是照着教程填的。第三层空,说明这个项目里他没有真正拍过一次板。
想让这件事可保存,就得把三层答案写成文件,而不是记在脑子里。下面是一份示例格式,不是他的真实文件,取的是同类型的多轮问答项目:
project: 多轮问答(示例)
layers:
L1_what: 检索增强的问答,支持基于上一轮答案继续追问
L2_how: 每轮的检索结果和引用来源随轮次落库,追问时把召回范围收缩到上一轮命中过的文档
L3_why: 收缩范围而不是每轮全库重召回,是从追问的真实分布倒推的,多数追问是在同一批文档里再往下问一句;代价是首轮漏召会传导到后面几轮,所以首轮召回单独放宽了 top_k
L4_locate: 每轮带 query_id 和命中文档 id 列表,能回放整段对话(加分项,不在验收线内)
这个文件的价值不在写,在于写的时候你会发现第三层填不出来。而填不出来,就是面试时被问出来。
所以验收动作是:找一个你没提前背过的问题,让别人或者自己对着录音往下追三层。第三层不空,这块才算过了。
四、底子三 · 表达口径:信号是「每个数字扛得住三句追问」
这一块最容易被跳过,因为它不涉及技术本身,看起来只是措辞。
他简历上写着一句响应速度明显提升。我接着问他这个提升是怎么测出来的,他答不上来,最后落到一个很含糊的说法上,大意是自己用着感觉快了。
这一句,把前面所有准备的分都扣掉了。
我给他定的规则是:简历上每一个数字,都要连着对自己问三句,三句都得有答案。
第一句,这个数是在哪种口径下跑出来的。本地单机自有的资源、测试环境的固定配置、线上灰度的真实流量,三者结论差得很远。
第二句,喂进去的是什么数据、一共多少条。几十条挑出来的样本和几千条真实分布,压出来的根本不是一个东西。
第三句,跟谁比。没有参照系的提升无法归因,连基线都说不清的数,不该出现在简历上。
三句里有任何一句答不上来,这个数字就删掉。
这三句是可以脚本化的。下面这个小程序干的就是这件事,它读一份「数字 + 口径」的清单,把缺项的那几行连同文件真实行号一起挑出来。它不判断你的数字对不对,只判断这一行有没有写完整。
// check_numbers.go · 简历数字口径自检
// 用法:go run check_numbers.go numbers.tsv
// numbers.tsv 每行五列,用 Tab 分隔:指标名 / 数值 / 环境 / 样本量 / 对照组
// 缺数值、环境、样本量或对照组的行会被列出来,行号按文件真实行号计。
package main
import (
"bufio"
"fmt"
"os"
"strings"
)
func main() {
path := "numbers.tsv"
if len(os.Args) > 1 {
path = os.Args[1]
}
f, err := os.Open(path)
if err != nil {
fmt.Println("打开失败:", err)
os.Exit(1)
}
defer f.Close()
sc := bufio.NewScanner(f)
sc.Buffer(make([]byte, 0, 64*1024), 1024*1024)
line, bad, flat := 0, 0, 0
for sc.Scan() {
line++
raw := sc.Text()
trimmed := strings.TrimSpace(raw)
if trimmed == "" || strings.HasPrefix(trimmed, "#") {
continue
}
cols := strings.Split(raw, "\t")
if len(cols) == 1 {
// 整行只有一列,说明分隔符已经不是 Tab,这一行没法检查,直接跳过而不是误报
flat++
continue
}
for len(cols) < 5 {
cols = append(cols, "")
}
var missing []string
if strings.TrimSpace(cols[1]) == "" {
missing = append(missing, "数值")
}
if strings.TrimSpace(cols[2]) == "" {
missing = append(missing, "环境")
}
if strings.TrimSpace(cols[3]) == "" {
missing = append(missing, "样本量")
}
if strings.TrimSpace(cols[4]) == "" {
missing = append(missing, "对照组")
}
if len(missing) == 0 {
continue
}
bad++
fmt.Printf("第 %d 行「%s」缺:%s\n", line, strings.TrimSpace(cols[0]), strings.Join(missing, "、"))
}
if err := sc.Err(); err != nil {
fmt.Println("读取失败:", err)
os.Exit(1)
}
if flat > 0 {
if bad > 0 {
fmt.Println()
}
fmt.Printf("有 %d 行只解析出 1 列,未参与检查。分隔符请用 Tab 而不是空格。\n", flat)
}
if bad == 0 && flat == 0 {
fmt.Println("全部数字口径完整,可以进简历。")
return
}
if bad > 0 {
fmt.Printf("共 %d 个数字口径不全,补完再投,或者从简历里删掉。\n", bad)
}
}
配套的输入文件长这样。这是一份示例,里面的数字都是编的,只为演示格式:
向量入库耗时 P95 180ms 线上灰度 近 7 天入库文档 3200 篇 优化前同口径 P95 310ms
首字延迟 P95 1.2s 测试环境 压测 500 并发持续 10 分钟 同配置未开流式 P95 2.4s
准确率 提升 30% 线上
QPS 3 倍 本地单机 100 条手挑样本
跑一遍,第 3 行和第 4 行会被点出来:第 3 行缺样本量和对照组,第 4 行只缺对照组。
脚本本身不值钱,值钱的是它把「我感觉提升了」变成了一张填不填得出来的表。填不出来的那一行,就是面试官会问倒你的那一行。
三句问下来,简历上的数字通常得砍掉一大批,但留下来的每一个都经得起追。
五、一张可保存的自检表
三块底子,每块一个信号,连同多久验一次列在下面。这张表可以单独存下来。
| 底子 | 只认的那个信号 | 不通过长什么样 | 多久验一次 |
|---|---|---|---|
| 业务匹配度 | 外行听完能自己说出它对应 AI 项目的哪个难点 | 对方接不住,只说听着挺厉害 | 每换一个目标岗位验一次 |
| 项目深度 | 同一个问题追三层,第三层不空 | 追到第三层开始讲道理不讲依据 | 每个项目收尾时验一次 |
| 表达口径 | 每个数字扛得住三句追问 | 落在「感觉快了」这类含糊说法上 | 每改一版简历验一次 |
第二列那三行,是整篇唯一需要记住的东西。
六、为什么验收信号比补的动作更重要
补底子是一个没有结束条件的动作。课永远看不完,项目永远能再加一层,简历永远能再改一版。只盯着动作的人会长期停在「还没准备好」的状态里,而真正的问题是没人告诉他差的那一块在哪。
换成信号之后,事情的性质变了。三个信号都能在十分钟之内验完,都不需要别人给你打分,也不需要你先学完什么。验不过,你知道差在哪一块;验过了,你可以放心往下走。
他后来花了两个月按这三块补。我让他做的事其实很少,主要是每周把那三个信号各验一遍,把验不过的那一项记下来,下一周从那一项开始。他一开始追到第三层就空了,后来能扛住三层还有余量。
两个月之后,他同时拿到了几个 offer。薪资我不写具体数字,只说一句:最后入职的那份,比他最初给自己定的预期高出一截。
这三块底子里,你觉得自己最有可能验不过的是哪一块?评论区说说你的判断依据。