Go 后端转 AI:3 块底子怎么验收?每块只认 1 个信号

17 阅读11分钟

模拟面试结束之后,一位学员把这段时间的学习记录打包发给我,问我一句:这样算不算补够了。

我点开看了一遍。里面有课程笔记、有三个项目的代码、有几十篇收藏的文章,还有一份改到第七版的简历。东西不少,但他这个问题我答不了。

不是我不想答,是这个问题本身没有验收标准。

「补够了没有」听起来像在等一个时刻,等到某一天有人告诉他可以投了。但底子这件事没有终点线,它只有信号。你学完一门课、跑通一个项目、把简历改到第七版,这三件事都只证明你做了动作,不证明动作落到了地方。

这位学员的情况在我这儿算典型: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。薪资我不写具体数字,只说一句:最后入职的那份,比他最初给自己定的预期高出一截。

这三块底子里,你觉得自己最有可能验不过的是哪一块?评论区说说你的判断依据。