MoreRSS

site iconJoway修改

工程师,新加坡字节跳动,喜欢摄影、旅行。
请复制 RSS 到你的阅读器,或快速订阅到 :

Inoreader Feedly Follow Feedbin Local Reader

Joway的 RSS 预览

关于 AI Coding 的一些个人技巧

2026-09-19 08:00:00

之前写了一篇 《尽职编程:AI Coding 时代的个体产出差异的来源》 引发了大家很多讨论,但那篇文章只写了工作态度,没有太多可以指导工作实践的技巧。所以我打算再分享下一些更具实操性内容。

网上关于 AI Coding 最多的讨论都集中在工具和模型,但我认为这种讨论就如同过去人工编程讨论用什么 IDE 和语言一样,没有太大意义。我发现自己现在似乎从来不在乎用的工具和模型是什么,我用过 cursor,windsurf,devin,codex,claude code,没有太大的感觉他们有什么本质的差异。在今年 Opus 4.5 发布之后,我认为连模型的差异也越来越小了。我承认他们各自有各自的特点,但这就好比不同品牌的车一样,作为驾驶员,应该掌握的是开不同车用不同的开法,而不是专门限制自己非法拉利不开。我认为,在合理的使用方式下,用不同工具和各家最新一代的模型不会对自己的产出产生太大的质量差异,如果有,那么说明你并没有非常好的驾驭不同工具和模型的特点,全在任凭它们自我发挥,这样当然会导致你的产出有巨大的波动。举个例子,如果某个模型特别喜欢过度设计,最后你的产出过度设计了,你不能说因为我的模型喜欢过度设计所以我的产出过度设计了,你只要简单跟模型提一句不要过度设计,再喜欢过度设计的模型也会按照你的指令不过度设计。模型是模型,人是人,正确的 AI Coding 方式应该是产出质量跟操控者走而不是跟着工具和模型走。

下面是我自己总结的一些通用的 AI Coding 技巧,不代表一定是最佳实践,仅作为抛砖引玉。

质量远高于速度

我一直很想问网上一些 AI Coding 效率极高的大 V 的一句话是,「哥们,你有正经工作吗?」。就我的上班经验来看,如果你在一家中国大公司工作,你的犯大错的机会可能只有一次,如果你在一家外企,老外对人宽容一点,可能犯大错机会有三次,如果你在一家创业公司,可能因为用户量和犯错成本不大,犯大错机会会有四五六次。

我相信绝大部分人 AI Coding 的目的都是为了完成工作,毕竟 token 成本摆在那里,只要是一份正经工作,它对错误的容忍程度就不会高。如果一个人并行开三个以上窗口编程,每天烧大量 token 产出几千上万行代码,配置了各种自动化的 MCP,Skill,CLI,网上流行什么新潮思想就采用什么思想,我很难信任这样的人工作会不犯错,事实上我这两年已经见到不少这么做的人翻车了。但是这些翻车的人如果把自己的工作方法分享到网上,可能不仅没人会批评,反而会得到大量嘉奖,甚至认为这些人适应 AI 新工作方式适应的好。

我日常最大的工作并行读只有 3 了,再多我脑子已经忙不过来去检查每个 Agent 的工作成果了。即便如此,我也完全不觉得自己干活比别人慢,所以我很好奇什么牛逼的公司会需要更快的编程速度,以及他们的产品质量到底如何。

很少有公司会因为你干活稍微比别人慢一点而开除你,但是经常因为某个人搞出大事故而开除人。就一份工作而言,尽量不要搞到被开除肯定还是第一要务。

不要过度使用 Skill

之前有一阵子出现了 Skill 热,什么玩意都整一个 Skill,甚至认为 Skill 用的好就能大大提升产出质量。我对 Skill 使用有一条原则是,「只有模型和巴菲特都不知道的知识,才用 Skill」。比如巴菲特不知道 x.com 的 API ,巴菲特也不知道如何快速自动化调用浏览器,这些你装 Skill 会极大改进 AI 工作的效率。但是巴菲特很知道批判性思维,很知道实现一个东西之前得多问几遍问题,也知道如何正确提问,你不能装一个 「巴菲特 Skill」然后指望模型按照巴菲特的脑回路来运行,这种做法有几个缺点:

  1. 不是每个任务都需要按照巴菲特的思考来运行的,Overthinking 本来就已经是模型的一个天生缺点了,加上 Skill 会极大放大这个缺点
  2. Skill 装多了,你不知道什么是模型的内置思考,什么是 Skill 的引导,什么又是你自己的引导
  3. 大多数人装之前不会完整阅读完 Skill,而你不知道 Skill 会干嘛,你就很难预期每次会话模型会产生什么回复,这是在黑盒编程

我日常的 Skill 很简单,一个 Lark Skill & CLI 来帮助我利用 Lark 的能力解决办公的协同问题,一个 ego-browser Skill 帮我自动化浏览器任务,然后就没有任何其他 Skill 了。我每次和模型对话,都能清楚预期对面的反应,这样我就能驾驭我的模型了,我想要模型批判性思考的时候,它就批判性思考,我想要它别多想给我使劲干它就使劲干。我们是一个人,不是机器,况且连机器现在都能学习,如果你觉得某个人的思维方式很好,你应该学习这个人思想,然后操控模型,而不是去 github 找一个这个人的 Skill 装到你机器上。

少命令,多提问

假如你要实现一个需求是,用户注册后发一封邮件,有几种提示词:

  • 提示词 A: /register 接口下调用 resend 接口发一封注册邮件
  • 提示词 B: 注册后发一封邮件
  • 提示词 C: 我想要实现发注册邮件的功能,有哪些实现方法?

你可能会觉得,注册后发邮件这个功能多简单,怎么问不都能实现吗?都能实现没错,但是我敢说无论用不用AI大部分人都实现不好这个需求。因为你要考虑:

  1. resend 接口挂了咋办,注册的接口是返回成功还是失败?
  2. 如果返回失败,不代表用户注册也失败了吗,有必要这么严格吗?
  3. 不管返回什么,resend 失败得重试,我又要怎么去重试?总不能这个接口同步等待重试吧?

提示词 A 的问题在于,你是一个「精确命令」,大部分模型真的会按照你的命令做,这样模型也不会深度思考,你也不会深度思考你的需求本身的复杂性,结果就是落下了一个隐患在代码里。

提示词 B 的问题在于,你是一个「模糊命令」,模型不一定会来给你方案选择,而是自己一股脑闷头实现了,它可能实现对,也可能实现错,不仅你和模型都没有深度思考,你如果不 review 代码认真点,你甚至都不一定知道它最后是用什么方法实现的,纯黑盒了。

而提示词 C ,因为是一个问句,而且我特地加了一个魔法词「哪些」,所以模型会深度思考不同方案,比如简单实现,或者发一个注册消息,让consumer来幂等消费这个事件直到成功。最关键的是,最后拍板的人是你,而不是模型。这才叫真正的驾驭模型。

控制设计而不是代码

如果我要实现一个几百行代码量以上的改动,我都会让 AI 先生成一份 Lark 的设计文档,为什么我要强调是 Lark 文档是因为它是富文本文档,有丰富的工具集。我之前公司只能用 Jira,那倒霉玩意生成的文档根本不可读。如果你公司不用 Lark,你也可以让它生成 HTML 文档。AI 会画一些架构图/流程图,能够比命令行窗口的文字更能帮助你理解它的设计。你理解了设计,才能提出比 AI 自己的思考更有价值的见解,从而优化设计或者简化设计。

另外,设计文档也能帮 review 你代码的能快速理解你的实现。至于代码本身,现在的模型能力很难出现设计文档是对的,代码是错的的情况,所以取决于你这个 PR 的重要性我觉得是否 review 已经没有那么重要了。

选择性向后兼容

现在很多模型有一个习惯是,喜欢写向后兼容的代码,这个代价就是为了兼容让代码变得很脏很复杂。模型并不知道你们生产环境的部署顺序,也不知道这个产品是否已经上线有真实用户,更不知道这个产品是否允许停机维护。但是你知道这些上下文,所以你需要根据实际情况做取舍,如果允许破坏兼容性,那就让模型写最优解的代码,而不要管是否兼容,这样对项目的长期维护有很大好处。

善用 AI Review

现在的代码生产速度,人力 Review 已经不太现实了,所以很多人都会用 AI Review 来替代人工 Review。在大部分情况下确实有很好的效果,但是,我 100% 反对那种一个 Agent 写代码,一个 Agent Review 产出意见,第一个 Agent 再根据 Review 意见写代码的 Loop 编程。

这里有一个关键问题是,如果 Review Agent 共享了 Coding Agent 的上下文,它 Review 的意见就不客观了,如果它不共享,它的上下文就不充分了。且不说还有一部分上下文存在于使用者自己的脑子里。让 AI 自己循环工作,最终产生的结果是不确定的,甚至可能偏离了你当初最早让 Agent 干的活的意图。我更倾向于,AI Review,人工检查 Review 意见,摘取部分让 Coding Agent 修改,再反复。虽然速度会慢,但正如上文所述,编程速度的问题早就已经解决了,现在的问题是质量,特别是如果质量提升,我们人类还得明确知道哪些质量提升。

保持勤奋

上面说了这么多,难道每一条在每一次对话里我自己都能做到吗?完全不能。我发现比模型更喜欢偷懒的是人。

大部分需求,AI 可以 10 分钟就完成,也可以和它反复对话十几轮花几个小时完成,目前的工作方式没有公司能来审核你的勤奋度,完全靠个人意识。如果你今天想早点下班,你就可以偷懒一点,让 AI 快速把活干完了。而就算你今天加班干到很晚,可能产出质量也就是从 80 分提升到 90 分,有这个必要吗?这可能会是未来会改变我们工作方式的很大一个话题,就目前这个过渡阶段,我们能做的只有时刻提醒自己,保持勤奋,至少在重要的事情上保持勤奋,这其实比上面所有奇技淫巧都重要。

尽职编程:AI Coding 时代的个体产出差异的来源

2026-08-29 08:00:00

这一年多的时间以来,AI Coding 已经从一个新潮的编程方式变成了默认方式,每个人都用着近乎相似的模型,相似的设备,相似的 Agent,理论上所有人产出的代码水平也应该相似,但如果你在一线工作过,你会发现实际情况恰恰相反,大家的代码产出水平方差反而比以往更大。这是一个相当有意思的话题,但是我却发现在互联网上鲜有人讨论。

为什么大家明明用着一样的编程方法,但是产出质量却会有如此大的差异?特别是当我反思自己每天跟 AI 的对话的时候,我觉得没有任何技术含量,我既没有什么魔法的提示词,也没有装什么花里胡哨的 Skills,纯粹就是把 AI 当作一个伙伴一起协作完成工作。我认为每个人的工作方式也都是如此,但是为什么还会存在个体差异?

在研究这个问题许久后,我认为我找到了一个很少有人讨论,但是在我看来是 AI Coding 核心要义的工作方法,我把它叫做「尽职编程」,用更口语化的表述就是「得靠谱」。

如果你和 HR,物业,政府机关等社会人员打交道多了,你就会知道在这个社会,要求这些行政人员帮你办成一件事有多难。但是我相信每个人的社交圈都会存在一种特能帮你办成事的能人,在古代,他们有另一种名字,叫做乡绅或者师爷,他们的工作就是两个字「办事」。需要注意的是,这类人自己往往并没有具体的专业知识,事情也多半不是他们自己执行的,但是他们就是有能力让别人高高兴兴把事情办成了。

在我看来,AI Coding 需要的就是这种能力。这个时代由于编程的速度极大地提升,工作分配的颗粒度也大大加粗,上一个时代每个人分到的活可能只是写某个模块甚至是某个函数,你不需要关心最后大家工作内容合并做成的事情是什么。但是如今更多时候是要让你把某件事情做成。而做成一件事情可能分一百个子事情,AI 有能力做完每件子事情,但是 AI 没有能力决定要做哪一百件子事情,这个的上下文可能在老板与你的沟通中,也可能存在你历史积累的工作经验里,还可能是你生活的常识,甚至还可能要预测未来的发展。而 AI 完全没办法通过你聊天窗口那几句话知道这么多上下文信息。

我在上一家公司接触过一种我觉得最可怕的人,甚至我都一度想申请让公司禁止他写代码,那就是他会让 AI 来决定,做成某件事情,需要做哪一百件事情,然后让 AI 全完成了。这就引出了今天的主题,那就是「尽职编程」。在我看来,他最大的错误就是没有做到「尽职」。坦率讲,AI 在自主决策的时候,正确率其实也不低,但绝对没有达到 100% 的地步。你和 AI 说要写一个文章编辑功能,如果不提供其他信息,AI 会自己决策是实时保存还是点击按钮保存,编辑框是固定大小还是会随着内容变长。但是 AI 依然会漏掉一些细节,比如多人同时编辑会如何。因为你给他的提示词就几个字符,他只会把它当简单的需求完成,况且如果 AI 思考过多可能还有过度设计的嫌疑。这时候,非常依赖人在其中多思考,多反问 AI,更重要的是,虽然代码不是你写的,但是你要能够回答得出在技术评审的时候别人的提出 —— 这是我认为的达到了「尽职编程」的标准。

如果只是为了实现需求,我甚至感觉在 AI Coding 时代到来后,我每天只需要工作 2 小时。但我其他时间其实往往是花在了「理解 AI 的设计」上。我会通过让 AI 画架构图,设计文档的方式,来理解 AI 写出来的代码而不是阅读代码本身,我甚至有一个百试不爽的魔法提示词是让 AI 写一个「面向产品经理」的文档,这样甚至连枯燥的技术语言都不需要阅读就能理解 AI 的设计了,还能把这个文档丢给产品经理让其他人也能理解系统的实现。

但「尽职」是一个主观行为,且没有尽头。什么样的需求需要尽职到什么程度才够,这完全凭个人自己的理解。比如近期我写的一个用来评测 Agent 质量的系统, 我就没做太多「尽职」工作,因为他的代码并非核心代码,且极容易推翻重改。但如果你要写一个 Agent 的核心代码,尽职就没有止境了,它不仅仅在你提交代码之前要 review,每过一段时间,随着代码的持续演进,还需要反复对系统进行「回归尽职调查」,才能对系统有最新的认识。而这些工作内容,不会产出可看见的工作结果,做与不做,全凭个人,做了也不一定有立竿见影的效果,不见得每家公司都能容纳这种工作方式尤其是大公司那种不懂行的中层领导的团队里。

尽管如此,也还是有一些方法可以在 AI Coding 工作流里,注入一些客观工具,鼓励这种「尽职精神」在公司内开枝散叶。比如我最近在我们的主仓库加了一个 Codex 自动 Review 代码的插件。注意,这个不是网上那些浅显的看几行代码评价几个无关痛痒问题的那种插件。而是会把代码库 clone 下来,结合代码库看修改本身产生的所有副作用,然后再给出 review 意见。而它的 Agent 上下文,完全独立用户 AI Coding Agent 的上下文,所以它会评价的非常客观。如果你就是执意要在一个 Get 接口修改数据,Claude Code 会因为你的命令而偏袒让你这么干,Review 的 Agent 不知道你的固执,所以依然不会让你这么干。由于你每一次修改都会有 Review 意见产生,你就会被迫进行多轮修改,或者你要直说你不同意 Review 意见。而且 github 上能看得到你的「尽职」过程,review 你代码的人本质上也是在 review 你的尽职过程。这就把主观的决策变成了客观可见的努力。我的体验下来,这个 Review Agent 不知道是不是因为提示词的原因已经有点演化成 Review 喷子了,但是在这个每个人都被 AI 阿谀奉承的时代需要喷子。

当我发现了这种在同样的工具下,产生不同的个体差异的现象时,说实话我有点松了一口气,至少 AI 短期内还没有那么容易能替代我的工作。我们和 AI 的区别在于,我们想把事情干成,AI 只想让这个对话完成,而干成事情的能力,在历史的长河里一直是稀缺品,从来没有因为工具的进步而被取代过。

大公司与创业公司的 AI Coding 体验差异

2026-08-13 08:00:00

我在 AI Coding 能力达到生产级水平后,先后在一家世界500强大公司和一家创业公司任职,有意思的是,我个人对待 AI Coding 的态度和实践在这两家公司截然不同。

在大公司的时候,由于我们在负责一个直接和商品定价有关系的重要系统,且系统已经历经了十几年的持续迭代,所以我对我自己和同事提交的每一行代码都要求极高,不管这行代码是不是 AI 写的,写代码的人必须要看过且看懂了每一行代码在做什么。如果有一个家伙提交了一大片根本无法人工审查的代码,我会对这个家伙非常生气,并认为这是对这个系统和其他同事的不尊重。在这家公司,AI Coding 对整体工作效率的提升其实微乎其微,三分之一的时间在开会对齐对需求的理解,三分之一的时间在处理审批之类的各类杂活,剩下的三分之一时间也不在写代码而在验证代码,真正的写代码时间微乎其微。

然而后来我来到了一家创业公司,我自己变成了那个每天提交大量人力无法 review 的代码量改动的家伙,但于此同时我对改动的自信却不比之前在大公司谨小慎微的时候少。我会通过让 Agent 画流程图,写文档等多种方式帮助我去理解产出的代码,并以一目十行的速度大致检查下产出代码又没有特别让人意外的片段。

在大公司,个人对公司成长发挥的作用微乎其微,可以说基本为零。甚至个人对团队的贡献,有时候都是微不足道的。很多团队的存在意义,是维持这个系统的/正常运转,而不是创新。相反,如果你因为创新而引发了事故,那么你对团队的伤害往往远大于贡献。特别是现在 AI 放大了个人的能力,也放大了破坏力,一个不靠谱的人,现在要捅出超大篓子的可能性远大于以往,基于此,最适合团队正常发展的策略就是谨慎谨慎再谨慎,不然我很怀疑你作为团队负责人每天看着大伙儿的编程速度大跃迁能否晚上睡得着觉。

而在创业公司,每个人摊上的是大公司几个团队的活,如果不充分利用 AI 的 Coding 速度,根本不可能干完日常的工作。况且创业公司要么没赚钱要么赚的钱不多,而且创业公司的价值在于未来能赚多少钱而不是这个季度赚了多少钱,在这种情况下,走得快比走路姿势稳不稳重要的多。这也是我觉得现在在创业公司的编程体验远超大公司的一点。对于一份工作来说,我们当然要保证工作的完成质量,但因为大公司的代码量之大和审批流程之复杂,保证质量这件工作往往相当之无聊。如果环境允许你充分发挥自己的主观能动性,在质量与速度之间,发挥自己的审美和判断力来做取舍,这种工作的创造感其实远高于传统人工编码的时代,但也对实操人的素质提出了更高的要求。

LLM 训练与推理的基本理解

2026-05-17 08:00:00

学习一个技术最好的方式就是能够写一片文章把这个技术的原理解释清楚,本文记录了我在阅读 《Build a Large Language Model (From Scratch)》一书以及和 Claude Code 对话过程中的笔记,仅供参考。

术语解释

向量点积

定义:向量点积为标量

a = (a1, a2, a3)
b = (b1, b2, b3)

a · b = a1*b1 + a2*b2 + a3*b3

几何意义:

a · b = |a| |b| cos(theta)

其中 theta 是两个向量的夹角。

用途:衡量向量相似度

矩阵转置

符号表示:Kᵀ

定义:

A =
1 2 3
4 5 6

Aᵀ =
1 4
2 5
3 6

线性层

假设某个词的向量是 2 个数字:x = [2, 5]。我要 3 个输出,就准备 3 个配方(权重和偏置都是训练学出来的数字):

配方1: 1×x₁ + 0×x₂ + 0
配方2: 0.5×x₁ + 2×x₂ + 1
配方3: −1×x₁ + 1×x₂ + 0

代入 x = [2, 5]:

输出₁ = 1×2 + 0×5 + 0 = 2
输出₂ = 0.5×2 + 2×5 + 1 = 1 + 10 + 1 = 12
输出₃ = −1×2 + 1×5 + 0 = 3

结果:[2, 5] → [2, 12, 3]。 一个线性层的全部工作。

用途:对向量升维或者降维。

Softmax

softmax 把"一排任意分数"变成"一个概率分布":每个值都在 0~1 之间,且全部加起来 = 1。

 e^(xᵢ)
softmax(xᵢ) = ─────────────
 Σ e^(xⱼ)

LayerNorm

LayerNorm 是 Transformer Block 的常见操作。向量在几十层里被反复加加乘乘(注意力、MLP、残差……),里面的数字很容易漂移——有的飙得很大、有的接近 0、整组忽大忽小。这会让训练不稳定(梯度爆炸/消失、loss 乱跳、训不动)。

LayerNorm 的作用:每过一个子层前,把这组数字"重新校准"回一个稳定、统一的范围。

算法:对一个 token 的数字(n_embd 个),做三步:

  1. 去均值:算出这组数的平均值,每个数减掉它 → 平均值变成 0(居中)
  2. 除标准差:算出这组数的标准差,每个数除以它 → 波动幅度变成 1(统一尺度)(为防止除 0,分母会加一个极小的 ε)。 做完①②,向量被"标准化"成:均值≈0,波动≈1
  3. 乘可学习的缩放 γ、加可学习的偏移 β:让模型自己学要不要、以及怎么把尺度调回去——不强制死守0/1,而是"先归一化,再让模型自己微调到最合适"。

同一个 LayerNorm 会对不同 token 都套用一遍。

文本编码

Tokenize

与常规的分词不同的是,对于例如空格之类的特殊字符,需要取决于特殊场景决定是否过滤。比如对 Python 代码,空格有独特语意,不应该被剔除。

"This is a example." => ["This", "is", "a", "example", "."]

Token ID

训练集中的每个词都需要有一个 Token ID 来数值化。有多种不同方法可以来数值化训练集词汇。

简单统计

遍历训练集分词后的词表,排序后,给每个词设置一个 token id。

一般还需要在不相关的文本之间插入一个 <|endoftext|> 标记。这有助于让 LLM 理解文本间是无关的。

- "This" => 40134
- "is" => 2052
- "a" => 133
- "example" => 389
- "." => 12
...
- "<|endoftext|>" => 50000

Byte pair encoding (BPE)

简单基于训练集分词结果建立词表有一个缺点是无法处理例如 “xasdasd” 这类未知词汇。BPE 编码方法可以解决这个问题。

假设语料库只有:

see 出现 3 次
set 出现 1 次

基础的字符集合为:[’e’, ’s’, ’t’],所以初始词表为:

"e" → 1
"s" → 2
"t" → 3

第 1 轮:把所有挨着的字母对,数一数(要乘以这个词出现的次数):

┌────────────┬────────────────────────────┬───────────┐
│ 挨着的一对 │ 在哪出现 │ 总次数 │
├────────────┼────────────────────────────┼───────────┤
│ s e │ see 里有(×3)、set 里有(×1) │ 4 ✅ 最多 │
├────────────┼────────────────────────────┼───────────┤
│ e e │ see 里有(×3) │ 3 │
├────────────┼────────────────────────────┼───────────┤
│ e t │ set 里有(×1) │ 1 │
└────────────┴────────────────────────────┴───────────┘

s e 出现 4 次最多 → 把 s e 粘成一个新块,叫它 se。

文字变成:

see → [se] e ×3
set → [se] t ×1

第 2 轮:再数一次

┌────────────┬───────────┐
│ 挨着的一对 │ 总次数 │
├────────────┼───────────┤
│ se e │ 3 ✅ 最多 │
├────────────┼───────────┤
│ se t │ 1 │
└────────────┴───────────┘

文字变成:

see → [see] ×3 ← 整个词变成 1 个块了!
set → [se] t ×1 ← 2 个块

于是我们有了一张完整的对照表(词表 vocab):

┌──────────┬────────────┐
│ token id │ 代表的文字 │
├──────────┼────────────┤
│ 1 │ e │
├──────────┼────────────┤
│ 2 │ s │
├──────────┼────────────┤
│ 3 │ t │
├──────────┼────────────┤
│ 4 │ se │
├──────────┼────────────┤
│ 5 │ see │
└──────────┴────────────┘

这个词表能够 encode 未知的单词,比如 “tee” 对应的是 [3,1,1] 。

Token Embedding

有了 Token ID 后,还需要把一个 Token ID 转成一个向量,因为向量间有距离,距离便可以表示两个词间词意的相似度。比如 ‘dog’ 和 ‘cat’ 虽然字符差别很大但是距离就很近,但是 ‘cat’ 和 ‘car’ 虽然字符相似但是距离很远。这是 Token ID 所做不到的。

一开始词表都是随机数,预训练时,这个词表也会被作为参数训练。

┌──────────┬────────────┐
│ token id │ 向量 │
├──────────┼────────────┤
│ 1 │ vector_A │
├──────────┼────────────┤
│ 2 │ vector_B │
├──────────┼────────────┤
│ 3 │ vector_C │
├──────────┼────────────┤
│ ... │ ... │
└──────────┴────────────┘

Positional Embedding

上面训练的向量没有位置相关的信息,然而词的意思和位置有直接的关系。

例如两句话:

狗 咬 人
人 咬 狗

在词向量嵌入后得到:

"狗"→[0.8, 0.9] "咬"→[0.1,-0.9] "人"→[0.5, 0.3]

两次话词嵌入得到的向量相同,只是顺序不同。而 Transformer 是并行处理每一个向量,所以会丢失位置信息。所以位置信息也必须嵌入到词向量中去。

建立一个位置向量表,注意,这个位置表一开始数字随机,也是作为参数被训练出来的:

┌─────────────┬────────────┐
│ 位置 │ 位置向量 │
├─────────────┼────────────┤
│ 第 1 个位置 │ [0.0, 0.1] │
├─────────────┼────────────┤
│ 第 2 个位置 │ [0.0, 0.5] │
├─────────────┼────────────┤
│ 第 3 个位置 │ [0.0, 0.9] │
└─────────────┴────────────┘

位置向量表的行数等于最大上下文长度。

新向量 = 词向量 + 它所在位置的向量

句子 A:「狗 咬 人」

  • 狗(第1位): [0.8, 0.9] + [0.0, 0.1] = [0.8, 1.0]
  • 咬(第2位): [0.1,-0.9] + [0.0, 0.5] = [0.1,-0.4]
  • 人(第3位): [0.5, 0.3] + [0.0, 0.9] = [0.5, 1.2]

句子 B:「人 咬 狗」

  • 人(第1位): [0.5, 0.3] + [0.0, 0.1] = [0.5, 0.4]
  • 咬(第2位): [0.1,-0.9] + [0.0, 0.5] = [0.1,-0.4]
  • 狗(第3位): [0.8, 0.9] + [0.0, 0.9] = [0.8, 1.8]

前向推理

自注意力

上一部分,我们最终得到了每个词自己的意思和位置信息,但是不知道上下文信息。自注意力机制就是为了解决上下文的问题。

例如:

"我吃了一个苹果" → 这里苹果是水果
"我买了一台苹果" → 这里苹果是手机/公司

为了解决这个问题,我们需要引入 Q(Query)/K(Key)/V(Value) 三个向量,每个词的向量(n_embd 维),分别乘以三个权重矩阵 Wq、Wk、Wv(所有 token 用的是同一套矩阵,形状为 n_embd × n_embd ),就得到它自己的 Q、K、V (形状 n_embd × n_embd)。Wq、Wk、Wv 一开始随机,也是作为参数被训练的。

处理一个词时,做这 5 步:

  1. 算相关度分数:用我的 Q 去和每个词的 K 做点积(点积大 = 方向接近 = 相关)。

    分数(我, 某词) = 我的Q · 某词的K

  2. 缩放:除以 √d(d 是向量长度)。 目的:向量越长,点积数值越大,会让后面 Softmax 变得极端(几乎只剩一个词 100%)。除一下把数值压回合理范围,训练更稳。

  3. 加因果掩码:把"后面的词"的分数设成 −∞。 因为生成时后面的词还没出现,不能偷看未来。设成 −∞,下一步 Softmax 后它的占比就是 0。

  4. Softmax:把这一行分数变成加起来 = 1 的占比(注意力权重)。

  5. 按占比加权求和各词的 V:得到这个词最终吸收到的信息。

整个过程就是这一个著名公式:

Attention(Q, K, V) = softmax( Q·Kᵀ / √d ) · V
 └─ 谁多重要(占比) ───┘ └─取走对应内容─┘

Kᵀ 为矩阵转置

KV Cache

在 LLM 推理的过程中,当仅新增了 Token 时,前面 Token 所有的 K/V 以及对应的 Attention 输出都不需要重新算。原因是因为因果掩码的存在:第 i 个 token 只能看 ≤ i 的位置。后面新增第 j 个 token(j > i),第 i 个 token 根本看不到它,所以第 i 个词的注意力结果不会因为后面追加了新词而改变。

每个 Transformer Block 各有一份独立的缓存(因为每层 Wk/Wv 不同,K/V也不同)。

多头自注意力

单个注意力,做一次"打分→softmax→加权混合",只能捕捉一种关系。但一句话里,词之间的关系是好几种同时存在的。例如:“小明 把 书 给 了 她 , 因为 它 很 有趣”

要真正理解它,模型得同时关注好几条线:

  • 语法关系:“给"要关注 谁给(小明)、给什么(书)、给谁(她)
  • 指代关系:“它"指的是"书”;“她"指某个人
  • 话题关系:“有趣"和"书"是相关的话题词

一个注意力头忙不过来——它做一次加权混合,只能照顾到其中一种。这就是瓶颈。

多头注意力把每个词的向量切成 h 段,每一段交给一个"头”,每个头独立、并行地做一遍完整的 Q/K/V 注意力,最后把 h 个头的结果拼回来,再融合一下。

举例说明:

假设 n_embd = 8(每个词 8 个数字),n_head = 4(4 个头)。则每个头分到 8 / 4 = 2 个数字(这叫 head_size)。

第1步:把每个词的 8 维向量,切成 4 段、每段 2 维

词向量: [ a₁ a₂ | b₁ b₂ | c₁ c₂ | d₁ d₂ ]
 头1 头2 头3 头4

第2步:每个头,在自己那 2 维上,独立做完整的注意力(各自算各自的 Q·K → 各自的 softmax 占比 → 各自加权混合 V)

  • 头1(比如学到"语法”):算出它自己的一套注意力占比和结果
  • 头2(比如学到"指代”):算出另一套完全不同的占比和结果
  • 头3、头4:各管各的关系

→ 每个头都吐出一个 2 维的结果。

第3步:把 4 个头的结果拼回成 8 维

[ 头1结果(2) | 头2结果(2) | 头3结果(2) | 头4结果(2) ] → 8 维

第4步:再过一个线性层(输出投影)把这 8 维"融合"一下,让各头信息互通,得到这个词最终的注意力输出。输出投影是一个可学习的线性层(一个权重矩阵 Wo 做矩阵乘法,Wo 是训练学出来的参数)。它的作用:让输出的每一个数字,都能用上"所有头"的信息——把各头的结果重新打散、加权、混合成一个连贯的整体向量。输出投影就来决定每个头各占多少权重。

残差连接

注意力算出来的结果,不是直接替换原向量,而是加回到原向量上:

新向量 = 原向量 + 注意力的输出

把注意力的产出,看成是"根据上下文,给这个词的一点修改建议/补充信息"。

举例来说:

苹果的原向量: [0.8, 0.9]
多头注意力算出来的结果: [0.1, -0.4]
 ───────────── ← 对应位置相加
残差相加后的新向量: [0.9, 0.5]

MLP (前馈网络)

MLP = 两个线性层,中间夹一个 GELU。注意力是"词之间交流";MLP 是"每个词,自己单独,再深加工一遍"。它内部就三步:升维 → GELU → 降维。

  1. 初始向量

    某个词(注意力+残差之后)的向量,2 个数字:

    x = [2, 1]
    
  2. 第一步:升维线性层(一组线性层,2 个输入 → 4 个输出)

    4 个配方(权重+偏置,都是训练学来的):

    配方h₁: 1×x₁ + 0×x₂ + 0
    配方h₂: 0×x₁ + 3×x₂ + (−1)
    配方h₃: −2×x₁ + 1×x₂ + 0
    配方h₄: 1×x₁ + 1×x₂ + (−5)
    

    代入 x = [2, 1]:

    h₁ = 1×2 + 0×1 + 0 = 2
    h₂ = 0×2 + 3×1 − 1 = 2
    h₃ = −2×2 + 1×1 + 0 = −3
    h₄ = 1×2 + 1×1 − 5 = −2
    

    升维结果:[2, 2, −3, −2](2 个数字 → 变 4 个,每个是输入的一种加权组合)

  3. 第二步:GELU(对每个数字单独套,软开关)

    正数基本保留,负数压向 0。对照表:

    ┌──────┬─────────┐
    │ 输入 │ GELU 后 │
    ├──────┼─────────┤
    │ 2 │ ≈ 2.0 │
    ├──────┼─────────┤
    │ −2 │ ≈ 0.0 │
    ├──────┼─────────┤
    │ −3 │ ≈ 0.0 │
    └──────┴─────────┘
    

    逐个套 [2, 2, −3, −2]:

    [2, 2, −3, −2] → [2.0, 2.0, 0.0, 0.0]
    

    → h₃、h₄ 是负的,被 GELU “关掉"了(变 ~0);h₁、h₂ 是正的,“放行”。这就是"软开关”,也是MLP 里唯一带非线性、让模型能学复杂规律的地方(没它,两个线性层会塌缩成一个)。

  4. 第三步:降维线性层(另一组"配方",4 个输入 → 回到 2 个输出)

    2 个配方:

    配方y₁: 1×g₁ + 1×g₂ + 1×g₃ + 1×g₄ + 0
    配方y₂: 2×g₁ + (−1)×g₂ + 0×g₃ + 0×g₄ + 0
    

    代入 g = [2.0, 2.0, 0.0, 0.0]:

    y₁ = 1×2.0 + 1×2.0 + 1×0 + 1×0 = 4.0
    y₂ = 2×2.0 + (−1)×2.0 + 0 + 0 = 2.0
    

    MLP 的最终输出:[4.0, 2.0]

    整条路: [2,1] → 升维 [2,2,−3,−2] → GELU [2,2,0,0] → 降维 [4.0, 2.0]。

    (然后这个 [4.0, 2.0] 会被残差加回 MLP 的输入 [2,1] → [6.0, 3.0])

Transformer Block 完整循环

输入 x
 ├──────────────┐ (残差①)
 ▼ │
LayerNorm │
 ▼ │
多头自注意力 │
 ▼ │
(+)◄─────────────┘ x = x + 注意力(LN(x))
 ├──────────────┐ (残差②)
 ▼ │
LayerNorm │
 ▼ │
MLP │
 ▼ │
(+)◄─────────────┘ x = x + MLP(LN(x))
输出(形状同输入,可堆下一个 Block)

预测下一个词

上面过完所有 Block,最后一个词的最终向量 = [1.2, -0.4]。开始预测下一个词:

  1. 最后做一次 LayerNorm(数字校准)

    把 [1.2, -0.4] 再规整一下数字范围(和之前一样,只是"校准",不改变意义)。假设校准后 ≈ [1.0, -0.5]。

  2. 输出头 – 把向量变成"每个词的分数"

    用一组线性层,把这 2 个数字,算成词表里每一个词的分数。

    假设词表只有 4 个词:

    人 : 3.1
    猫 : 2.0
    苹果 : 0.4
    跑 : -1.5
    
  3. Softmax——分数变概率

    人 : 68%
    猫 : 23%
    苹果 : 7%
    跑 : 2%
    

    按概率抽一个(大概率抽到"人")→ 这就是模型预测的下一个词 → 解码成文字。

PS:为什么这里时按概率抽取而不是选择概率最高的词?

因为基于不可解释的原因,实践中发现如果永远选概率最高的词,得到的结果平淡无奇,而有时选取概率低的词会产生奇妙的结果。这也是 LLM API 里 temperature 参数的作用,如果 temperature 为 0 ,则永远会选择概率最高的词。

模型训练

Loss 函数

loss = − log( 模型给"正确答案"的那个概率 )

┌─────────────────────┬──────────────────────┐
│ 模型给正确词的概率 │ loss = −log(概率) │
├─────────────────────┼──────────────────────┤
│ 1.0(完全自信且对) │ 0 ← 完美,零惩罚 │
├─────────────────────┼──────────────────────┤
│ 0.5 │ ≈ 0.69 │
├─────────────────────┼──────────────────────┤
│ 0.1 │ ≈ 2.3 │
├─────────────────────┼──────────────────────┤
│ 0.01(基本没押中) │ ≈ 4.6 ← 惩罚很大 │
└─────────────────────┴──────────────────────┘

反向传播

我们手上有一个 loss 数字(比如 2.30),我们想让它变小。模型有几百万个参数(各种"配方"的权重)。问题是:每一个参数,到底该调大还是调小?

核心思想:把 loss 想成一座山

想象 loss 是一座山的高度,你站在山上,目标是往山下走(loss 变小)。

对某一个参数,关键就问一句话:

▎ “我把这个参数动一点点,loss 是变高还是变低?变多少?”

这个"动一点点会怎样"的量,就叫 梯度(gradient)。

反向传播就是一个数学技巧(链式法则):只需从 loss 出发,沿着网络反着扫一遍(输出层 → 往前一层 → 再往前……一直回到输入),就能一次性把所有参数的梯度全部算出来,不用一个个试。

梯度下降

反向传播跑完,每个参数都有了梯度(坡度:哪边是上坡、多陡)。

梯度下降公式:

新参数 = 老参数 − 学习率 × 梯度

梯度永远和参数同形状,如果参数是矩阵,梯度也会是矩阵。更新参数时,也是所有参数一同更新。

训练流程图

┌─────────────────────────────┐
│ 拿一小批数据(题目) │
└──────────────┬──────────────┘
① 模型做题(预测下一个词)
② 对答案,打一个"错误分"(loss)
③ 找出每个参数对这次错误的"责任"(梯度)
④ 按责任,每个参数各调一点点(更新)
错误分降一点,模型变好一点
└──── 再拿下一批,重复很多很多次 ───┐
┌──────────────────────────────────┘
练够了 → 得到训练好的模型(一堆调好的数字)

强化训练主要在这个过程中修改了第二步,不再是对答案,而是用奖励函数评分。

模型运行

推理流程

模型文件分为三个部分:

  1. 模型结构 (config.json)
  2. 模型本体(参数)
  3. 分词器 (tokenizer.json)

要运行模型,本质上就是按照模型结构搭建出模型的前向流程,然后自回归跑前向循环,直到遇到结束符。

输入「我 爱」
 → 前向#1(过完全部N层)→ 预测出「北」 ← 跑了一整遍
拼成「我 爱 北」
 → 前向#2(又过完全部N层)→ 预测出「京」 ← 又跑了一整遍
拼成「我 爱 北 京」
 → 前向#3 → 预测出「。」
 → 前向#4 → 预测出 <结束符> → 停止

昨日的世界不再重来

2026-03-30 08:00:00

最近在做一个思想实验,如果你面前有一个按钮,按下就可以让 AI 从世界上消失,你是否会按下。身边很多朋友的答案都是会毫不犹豫的按下,即便有一些人还是那种最拥抱 AI 的人。

拥抱 AI 和厌恶 AI 可以同时在一个人身上发生,我自己就是这样的人。我厌恶 AI,是因为对昨日世界的怀念,我拥抱 AI,是因为今日世界无法回退,我没有不拥抱的选择。

现在的 AI 虽然可以代替我完成很多事情,但是在事情完成后,带给我的并不是成就感而是一种无尽的空虚。有了一个 idea,找一个趁手的模型,用几句提示词让 AI 吭哧吭哧干活,再做几轮调整,最后几个小时项目完成了,剩下一段贤者时间,你不知道这个项目数据库的 schema,也不确定架构是否能支撑多少的用户规模,未来有新的需求,你只能对着这个黑盒许愿,在许愿前完全没有关于需求可行性的任何概念。当 AI 给你完成了修改后,你不知道会不会损坏已有的用户的数据,只能按照 AI 的要求上线。一通操作下来,身体很累,精神很空虚,干了一天的活,又好像什么也没干。伫立在这个黑盒面前,反反复复操作几个月,不知道自己究竟学到了什么。AI 有了更多的数据,一天天变得更聪明,而我们自己呢?

在昨日的世界,虽然我们工作很累,但是工作结束会有一种智力的满足。当其他人来咨询某个需求是否可行,我们可以给出斩钉截铁的答案。当需求可行的时候,我们知道需要修改哪些地方让它可行,当需求不可行的时候,我们知道因为哪些原因所以它不可行。我们面对的是一个充满确定性的世界,我们确定昨天的工作,确定明天的工作,确定自己的职业生涯,确定未来能够有一份收入养家糊口生儿育女。今日的世界一切都不确定,不确定昨天做了什么,不确定明天会做什么,不确定这每天光聊天的工作能否有职业生涯,不确定未来什么样的人才可能凌驾于 AI 之上能有一份糊口的工作。

今日的世界互联网的整体审美也在逐渐倒退。互联网到处充斥着用 AI 生成的机械感十足的图片,也没人在乎是否是真的符合自己脑海中想象的模样,只要和提示词差不多能用即可。各种「高效率人士」用 AI 创作着各种垃圾的文章,宣扬自己最高效的工作流,攀比着各自的 token 消耗量和同时并行启动的 Agent 数量。每天都把一些简单的步骤封装出各种下一个月就会消失的名词概念,并以自己掌握的名词数量而沾沾自喜。

我时常在刷新推特时间线的时候,感受到自己是在一群推土机中漫步,页面滚动,一颗颗大树被连根拔起,群众在欢呼,木匠们做出一件件精美的家具。而我只想回到自己的瓦尔登湖旁边,但是小木屋也早已被掀翻,地上留着一张告示,顺之者昌逆之者亡。

写在 AI Coding 奇点之后

2026-02-26 08:00:00

说来惭愧,我已经好几个月没有完全手写代码了,甚至像是把某个变量改一个名字这种活我都宁愿输入更多字符告诉 AI 来做而不是自己直接改了,因为我知道,哪怕是变量重命名这种简单的活,AI 也能做的比我仔细,它会检查所有用到这个变量的代码、相关的注释以及单元测试,然后一并都修改了,而我自己其实历史上就犯过不少忘记修改关联地方的错误。

过去一两年的时间,AI Coding 引来了飞速的发展,就我的个人体验而言,大概经历了三个阶段。AI Coding 1.0 时代,AI 只是简单给你提供代码补全,代码总体还是人在写。而在 AI Coding 2.0 时代,人更像是在指导 AI 写代码,一次 prompt 只能高质量地写一百来行代码,你需要仔细检查代码是否符合你的需求。而最近两个月的时间,AI Coding 3.0 来到了一个奇点时刻,你甚至都不需要精心设计你的 prompt,只要简单描述下需求,AI 自己会根据代码上下文,以极高质量生成成千上万行代码,而人无论是体力还是智力已经无法 review AI 的代码,只能粗浅把握一个编程方向。在两个月之前,我或许还会认为过去的编程工作还能继续苟活一两年的时间,而现在我认为此刻所有传统的编程工作已经在事实上被淘汰,我们这些传统程序员之所以还没有被开除的原因仅仅只是公司自己在制度流程上还没有完全适应 AI 作为员工的方式,而这个适应过程可能会持续几年之久。

在完全不手写代码的这几个月里,我时常在 AI reasoning 的间隙问自己到底在做什么。我似乎做着完全没有技术含量的『传话』工作,但与此同时,如果真要把我淘汰直接让 PM 和 Agent 对话,似乎也很难实现他们的需求,而且出事的风险极高。这意味着我依然在提供自己的价值,只是这个价值无法再被很好的「可视化」。我之所以比 PM 能让 AI 更好地干活是因为我具备一些专有的知识,这些知识的产出可能仅仅只是一个 Yes 或 No,但这不影响他们的价值。我之所以会觉得浑身难受的原因还是之前的日子过得太苦,觉得必须一定要自己手动干很多活才能体现自己的价值,没有真正把自己从一个「劳动者」换位到「管理者」。一个好的管理者肯定不是办公室里最忙碌的那一个人,那些做出对公司最有价值决定的管理者最终的产出也无非就是一个 Yes 或 No。能力强的员工往往希望老板管理的越少越好,AI Agent 也是如此。

我们已经近乎实现了 Coding 的自动化,但是在一个公司里,Coding 只是整个公司工作的很小一部分,甚至都只是程序员工作的一小部分。我们现在拥有了一个无限智能的机器,但是整个公司运行制度都是面向人类设计的,Agent 需要发挥能力必须建立一个正向的 Loop,现在的 Loop 仅仅只建立在 Agent 内部,公司的每个环节与 Agent 并没有建立起这个 Loop,甚至连 Input/Output 渠道都没有建立。未来 Agent 会进一步渗透进整个公司一切流程的方方面面,不仅改变软件工程师的工作内容,也会改变产品经理,HR等一切职能的工作内容。我觉得这些传统工作岗位的消失会成为必然事件,但并不意味着不会创造新的工作岗位。最可能发生的事情是工作岗位从过去的不断细化的趋势开始变得不断泛化。现在你还在那区分前后端工程师属实是一个笑话,各种软件工种现在都可以统称为工程师岗位,如果一个后端工程师现在还写不好前端需求只能说明你不是一个合格的工程师。而所有工程师的工作会变成编排管理成百上千 Agent 的编码任务,解决 Agent 运行过程中遇到的无法自我解决的问题,帮助公司完善流程让 Agent 干活干得更舒心。

在现在这个时刻,已经不需要怀疑这是一次工业革命级别的巨变,很高兴也很惶恐可以经历这场巨变。即便工作被淘汰了,至少也是被更高级的文明所淘汰,保持学习,保持好奇,毕竟是一个青壮年劳动力,世界总会出现新的工作留给我们这些人糊口的。Keep Calm and Carry On !