做了 8 年前端,简历改漂亮后面一个挂一个:差的不是前端,是这 3 块底子
今年四月中,一个做了 8 年前端的同学来找我。他被裁了,简历投了两周,约面反馈还不错,但连着挂了四家。
他把简历发我看。项目写得很满,React 组件库、可视化大屏、低代码搭建平台,每个都有量化结果。按前端岗的标准,这份简历没问题。
问题出在他转的方向上。他面的是 AI 应用开发岗,面试官问的却是另外一批问题。
「你这个 Agent,工具调用超时了怎么办?」 「模型返回的结果,谁来判断能不能直接写库?」 「你说首屏加载快了 40%,那 40% 是怎么测出来的?」
他一个都答不上来。原因很直接:他过去八年积累的经验里,没有这三件事。
我们后来做的事很笨:没有继续改简历,也没有再投,而是花了一个多月补底子。六月初他拿到第一个 offer,之后再没断过。
这篇文章我把那三块底子拆开讲。前端同学转 AI 时,最容易漏掉的就是这三块后端侧的东西。
一、第一块:数据链路,知道一次请求从哪进到哪出
前端同学对「请求」的理解,通常停在 fetch 发出去、拿到 JSON 渲染页面这一层。中间那段是黑盒,平时也不需要打开。
AI 应用开发的岗位要求正好相反。面试官想知道的是黑盒里面长什么样。
一个问答请求进到系统,大致要过这么几跳:
| 环节 | 干什么 | 这一跳最容易出的问题 |
|---|---|---|
| 入口鉴权 | 确认是谁在调、有没有配额 | 把用户身份交给模型判断 |
| 意图识别 | 判断这次该走哪个分支 | 识别错了没有兜底路径 |
| 检索(RAG) | 把相关知识捞出来 | 召回为空时直接把空上下文喂给模型 |
| 工具调用 | 查库、调外部接口 | 超时不设上限,一次抖动拖垮整条链路 |
| 生成 | 模型产出结果 | 流式输出中断后前端白屏 |
| 落库与审计 | 存下来、留痕 | 只有结果没有过程,出问题查不回去 |
这张表看着基础,但面试官追问的永远是某一跳里面的细节。比如「工具调用超时了怎么办」,标准答案不是「加重试」,而是三件事分开说:
- 重试按错误类型分:网络抖动可以重试,参数错误重试一百次也没用;
- 重试有上限且带抖动:固定 3 次、间隔按指数退避加随机量,避免同时打爆下游;
- 重试都失败要有兜底:给一个业务上能用的降级结果,而不是把错误抛给用户。
这三句说出来,面试官就知道你真的在生产上接过这种活。
二、第二块:工程边界,知道哪些事绝对不能交给模型
这是前端同学最容易踩的一块。前端写惯了「用户点什么就发生什么」,转到 AI 之后很容易顺势写成「模型说什么就发生什么」。
我给学员定的边界只有一句话:模型负责产出意图和参数,判断和执行必须在业务代码里。
具体到代码,是这样分层的:
go
package agent
import (
"context"
"errors"
"fmt"
"time"
)
// ToolSpec 是工具登记信息。模型只能调用登记过的工具,
// 能不能执行由 Allow 决定,而不是由模型说了算。
type ToolSpec struct {
Name string
Timeout time.Duration
Allow func(userID string) bool
Run func(ctx context.Context, args map[string]any) (any, error)
}
// reservedArgs 是模型传参里不允许出现的字段名。
// 身份和权限只由服务端填,模型给的一律拒绝。
var reservedArgs = map[string]bool{
"userID": true, "user_id": true, "role": true, "permission": true,
}
type Router struct {
tools map[string]ToolSpec
}
// NewRouter 在登记阶段就把不完整的工具挡掉,
// 免得运行到一半才发现 Allow 或 Run 是 nil。
func NewRouter(specs ...ToolSpec) (*Router, error) {
r := &Router{tools: make(map[string]ToolSpec, len(specs))}
for _, s := range specs {
if s.Name == "" || s.Allow == nil || s.Run == nil {
return nil, fmt.Errorf("incomplete tool spec: %q", s.Name)
}
r.tools[s.Name] = s
}
return r, nil
}
// Execute 的 userID 由服务端会话提供,不从模型入参里取。
func (r *Router) Execute(ctx context.Context, userID, name string, args map[string]any) (any, error) {
spec, ok := r.tools[name]
if !ok {
return nil, fmt.Errorf("tool not registered: %s", name)
}
if !spec.Allow(userID) {
return nil, errors.New("permission denied")
}
for k := range args {
if reservedArgs[k] {
return nil, fmt.Errorf("reserved arg from model: %s", k)
}
}
timeout := spec.Timeout
if timeout <= 0 {
timeout = 3 * time.Second
}
cctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
type result struct {
v any
err error
}
ch := make(chan result, 1)
go func() {
v, err := spec.Run(cctx, args)
ch <- result{v, err}
}()
select {
case res := <-ch:
return res.v, res.err
case <-cctx.Done():
// 走到这里只代表上层放弃等待,工具那个 goroutine 还在跑。
// 工具内部必须自己 select ctx.Done() 才能真正退出。
return nil, fmt.Errorf("tool %s timeout after %s", name, timeout)
}
}
这段代码里藏了四个边界,面试时都能单独拎出来讲:
- 白名单:没登记的工具名,模型说了也不执行;
- 权限在服务侧判定 :
userID由服务端会话传入,模型如果在args里塞userID、role这类字段,直接报错拒绝; - 超时按工具单独设:不共用全局超时,慢工具才不会因为快工具的阈值被误杀;
- 超时要自己兜 :
context.WithTimeout只把截止时间传下去,取消是协作式的。工具内部不select ctx.Done(),超时就拦不住,所以这里在Execute里用select等结果和等cctx.Done()二选一,超时就返回错误,上层的重试和兜底才有东西可接。
上面四个分支我都实跑过:漏填 Allow 的工具在 NewRouter 阶段就被拒;无权用户返回 permission denied;模型塞 userID 返回 reserved arg from model: userID;一个 50 毫秒超时、内部睡 200 毫秒的工具,实测 50 毫秒返回 timeout 错误而不是傻等到底。
有些同学会问:模型返回的结果能不能直接写库?我的答案一直是不能。写库、删数据、算金额、判定权限,这四类一律走业务代码,模型最多给个建议值,然后被校验。
这位同学当时的问题就在这。他的项目里,模型输出一段 JSON,前端拿到就直接渲染、直接提交。功能跑得通,但面试官一问「如果模型被人诱导输出了越权字段呢」,他就接不住了。
三、第三块:数字溯源,简历上每个数字都说得清怎么来的
第三块最容易被忽略,也最致命。
他的简历里写着「首屏加载优化 40%」。面试官问了一句「这 40% 是怎么测的」,他答的是「优化前后各点几次,感觉快了」。
问题不在数字真假,在他没有测量口径。
我让他做的一件事是:把简历上每一个数字,都补上三个字段。
| 数字 | 怎么测的 | 测了多少次 | 在什么条件下测的 |
|---|---|---|---|
| 首屏加载 40% | Lighthouse 跑分 + 真实用户埋点 P75 | 30 次取中位数 | 4G 网络、中端机型、冷启动 |
| 召回准确率 0.82 | 200 条标注集上跑评测 | 3 轮取平均 | 固定切片版本、固定 embedding 模型 |
| 单次成本 0.03 元 | 服务端埋点累加 token 后折算 | 连续 7 天全量 | 含重试,不含人工介入的场景 |
上表是补完口径之后的写法示例,不是该学员的实测值,填的时候请换成自己的数。
补完之后他自己的感受是:有三个数字他不敢写了,因为测不出来。这三个最后从简历里删掉了。
剩下来的数字反而好讲了。面试官追问的时候,他能正面回答口径,而不是绕。
四、为什么是「补」而不是「改」
这位同学一开始想要的是「再投一波」。他想不通:简历明明改好了,约面反馈也不错,为什么就是不过。
我跟他说的是另外一句话:
着急我能理解,但标准不能降低。面试可以密集一些,能过关就过关;达不到内部标准,就讲清楚哪里需要补、为什么没过关,没过关的阶段继续学。
这不是劝他慢下来,是告诉他顺序错了。简历解决的是「有没有面试」,底子解决的是「能不能通过」。前一个已经解决了,再改一百遍也没用。
我们后来定的节奏是:面试不停,白天照常面,晚上把当天被问住的题拆成一块底子补上。这一个多月里他补的就是上面这三块,没有多,也没有少。
六月初的第一个 offer 是在杭州。之后又面了几家,最后去了北京。具体薪资我不写,只能说比他被裁前的水平高出一大截。
五、前端同学转 AI,真正能复用的是什么
写到这里要说一句公平话:前端八年的经验不是废的。
前面讲的三块里,第一块数据链路,前端同学理解起来其实比纯后端更快,因为你们天天在和「请求」打交道,只是以前不用管黑盒里面。第三块数字溯源,做过性能优化的前端本来就有埋点习惯,差的是把「我感觉快了」换成「P75 是多少」。
真正缺的只有第二块,工程边界。这一块前端几乎没有训练场景,因为浏览器里不存在「别人给我的指令要不要执行」这个问题。服务器端有。
所以如果你也是前端转 AI,我的建议是:数据链路补一遍,数字溯源养成习惯,工程边界从头学。第三块花的时间会比你想象的长。
你现在在做 AI 项目的时候,有没有哪一处是「模型说什么就执行什么」的?评论区说说,我挑典型的单独写一篇。