AI 写代码的时代,仍然值得学的 10 个基础

HN LLM Code Research 2026-09-28T11:35:13.333408

没人愿意再学编程了,我理解为什么。我每天都在用编码智能体(coding agent)做开发,2026 年 3 月以来我发布的代码几乎全是智能体写出来的。但我仍然在用的,是那些能让我指挥智能体、读懂它的产出、发现它哪里搞错了的知识。用 AI 学编程,学的多半就是这个。

下面这十个,是我依然会去学的。每一个我都会讲:它是什么;讲一个真实场景,智能体在那里出了问题,而只有懂基本功的人才会察觉;再给出一门免费的课程。

如果你是零基础,先读《编程入门》和我的新手指南,挑一个氛围编程工具(vibe coding tool),就像我在《IDE、CLI 还是氛围编程工具》里建议的那样,先做出点小东西。然后再回来搞清楚它是怎么运作的。

1. HTTP:请求与响应

HTTP 就是浏览器(或者任何客户端)和服务器之间的对话。客户端发一个请求:方法(比如 GET 或 POST)、一个 URL、若干请求头,可能还有请求体。服务器回一个状态码(比如 200 或 404)、响应头,通常还有响应体。这一来一回,就是前端和后端之间的那条线。

这些方法各有含义。GET 用来读取,而且绝不能改变任何东西——因为浏览器会预取链接,爬虫会跟踪链接,缓存会存下结果。要改动数据,得用 POST、PUT 或 DELETE。

如果你让智能体写一个「删除订阅者」的功能,可能拿到这样的代码:

app.get('/subscribers/:id/delete', async (req, res) => {
  await db.run('DELETE FROM subscribers WHERE id = ?', req.params.id)
  res.redirect('/subscribers')
})

你手动点一下,它确实能用。然后某个链接检查工具或者爬虫把页面上所有 URL 都访问了一遍,你的订阅者就全没了。这种情况你根本看不出有 bug——除非你知道 GET 必须是安全的。

状态码和响应头同样是证据。2026 年 6 月本站从 Netlify 迁到 Cloudflare Pages 时,curl -I https://flaviocopes.com 返回的响应头告诉我切换已经完成了。

免费的 HTTP 课程涵盖方法、状态码、响应头、缓存和 Cookie。

2. Git:提交、分支,以及怎么把活儿找回来

Git 存的是项目快照。每次提交就是一个快照,带一条说明信息和一个指向父提交的指针,而分支就是个指向某次提交的名字。因为每次提交都记得自己的父提交,你随时能拿回文件的任何一个历史版本。提交过的工作基本丢不了。

没提交的内容就是另一回事了。AI 代理跑 Git 命令时往往很有自信:用 git reset --hard 清理现场,用 git checkout -- . 丢掉它改坏的文件,用 git push --force 让远端和本地保持一致。每一条命令都会扔掉点东西。从分支上脱离的提交通常还能找回来,因为 reset 只是移动了分支指针,并没有删除任何数据:

git reflog
git reset --hard HEAD@{1}

但被 reset --hard 丢弃的未提交改动,就真没了。

我经常在同一个仓库里同时跑好几个代理。有一次其中一个随手跑了 git reset,另一个会话刚做的提交就从分支上消失了,过了几分钟我才把它们恢复回来。现在我的代理规则里写得很清楚:只暂存明确指定的路径,每次都新建提交,工作区有变动时绝不 reset、rebase 或 force-push。另外装了几个 Git hook,能在坏提交落地前把它拦下来。

免费的 Git 课程从第一次提交讲到分支、远端,以及怎么从错误中恢复。

3. SQL,以及数据是怎么装进表里的

关系型数据库把数据放在表里。每一行是一条记录,比如一个用户或一笔订单;每一列是一个属性。所谓“关系”,就是一张表里有一列存着另一张表某行的 id。SQL 就是用来向这些数据提问的语言:SELECT 读取,WHERE 过滤,JOIN 合并表,索引让查询更快。

AI 代理最经典的错误是 N+1 查询。让它查最近 20 笔订单加上客户名字,它会写出一个循环:

const orders = await db.all(
  'SELECT * FROM orders ORDER BY created_at DESC LIMIT 20'
)

for (const order of orders) {
  order.customer = await db.get(
    'SELECT name FROM customers WHERE id = ?',
    order.customer_id
  )
}

这就是 21 次查询。在你笔记本上只有 20 行数据时,感觉是瞬间完成的;但如果数据有 500 行,数据库又在另一台机器上,页面加载就得等好几秒。其实一条查询就够了:

SELECT orders.*, customers.name
FROM orders
JOIN customers ON customers.id = orders.customer_id
ORDER BY orders.created_at DESC
LIMIT 20

只有当你明白 JOIN 是用来干什么的,并且知道每次查询都是一次往返请求,才能看出这里藏着一个循环。

数据建模同样重要。让它给文章加个标签,AI 代理可能会把所有标签塞进一列里,用逗号分隔——这是让测试通过的最省事做法。它能跑,直到你需要「找出所有带某个标签的文章」时才露馅。

免费的 SQL 课程从「什么是表」讲起,一路讲到联表、索引,以及如何在应用程序里安全地查询。

即将上线 · 候补名单已开放

AI Workshop 与 Solo Lab:新一期招生

Solo Lab 于 10 月 26 日开课:用三周时间寻找客户,靠自己的软件养活自己。AI Workshop 于 11 月 30 日开课:用三周时间学会指挥编码代理,并审查它们写出的代码。加入候补名单,报名开放时我会发邮件通知你。

4. 终端

终端就是你敲命令、由 shell 运行程序的地方。一条命令由名称、参数和选项标志(flag)组成。程序会打印输出,并以退出码结束(0 表示成功)。管道(pipe)把一个程序的输出送进下一个程序。所有编码代理都活在这里——它们靠 shell 命令来安装、构建、测试和部署,所以终端就是你和它们交流的共同语言。

懂 shell,你才能读懂运行记录,识别出那一行危险命令。有些命令只是「看一看」(ls、cat、git status);有些则会改动机器(rm -rf、npm install -g,以及任何带 --clear 或 --force 的命令)。

2026 年 9 月,我让几个只读代理对照已安装的二进制文件,逐项检查一长串 Ghostty 配置。其中一个「测试」了 ghostty +ssh-cache --clear,结果把我 Mac 上的 SSH terminfo 缓存清空了。在长长的运行记录里,它看起来和其他每一行都没什么区别。只有当你清楚 --clear 的含义时,才会注意到它。我现在给审计代理的指令里明确写着:仓库之外的命令,一律不许改动机器状态。

免费的 Shell Commands 课程涵盖目录导航、权限、管道和进程。

5. 浏览器如何把 HTML 和 CSS 变成像素

浏览器读取 HTML,构建出一棵节点树,也就是 DOM。接着读取 CSS,当多条规则作用在同一个元素上时,由层叠(cascade)来裁决:先看重要性和来源,再看特异性(specificity),最后看书写顺序。每个元素都是一个盒子,由内容、内边距、边框和外边距组成。布局决定盒子放在哪里,绘制负责把它们画出来。

AI 写的 CSS 截图看着通常没问题,问题都藏在层叠里。

本站有 220 个浏览器工具,分批由多个 AI 并行生成。Tailwind v4 用 :where() 选择器来实现 space-y-* 间距,而 :where() 的特异性为零。有好几个 AI 在工具样式表里加了一条看似无害的 margin: 0 重置。任何正常选择器都能压过零特异性,于是这条重置赢了,表单字段之间的间距全部塌掉。就这样上线了七个工具。修复方法是删掉那条重置,而能定位到它,前提是你知道这场冲突是由特异性、而不是书写顺序决定的。

同样的知识还能识破 AI 用 !important 去“修”溢出问题,或者把一个按钮套进三层容器里做居中。

免费的 HTML 课程和 CSS 课程覆盖了 DOM、层叠、盒模型、Flexbox 和 Grid。

6. 异步 JavaScript 与事件循环

JavaScript 运行在单线程上。任何耗时的操作,比如网络请求、读文件、定时器,都交给运行时去处理,代码继续往下走。结果就绪后,回调进入队列,等调用栈清空,事件循环再执行它。await 只会暂停它所在的那个函数,直到对应的 promise 落定,不影响其它任何东西。更多内容见 JavaScript 事件循环。

漏写 await 是我最常遇到的异步 bug,而且代码照样能跑。

我自己就踩过这个坑,发生在本站处理课程购买的 Cloudflare Pages Function 里。调用邮件订阅接口时没有加 await。Worker 在响应发出后随时可能终止,于是那个 fetch 被取消,买家从没进过订阅名单。没有报错,没有日志。如果你知道,没有任何机制会强制运行时去等待一个没人 await 的 promise,那这就会是你第一个去排查的地方。

在 React 里,同样的道理表现为竞态(race),出现在每当 query 变化就拉取结果的 useEffect 中:

useEffect(() => {
  fetch(`/api/search?q=${query}`)
    .then(res => res.json())
    .then(setResults)
}, [query])

快速敲下「git」,三个请求就发出去了。谁最后返回谁说了算,哪怕它是「g」那次的结果。解决办法是加一段清理逻辑,把过期响应丢掉:

useEffect(() => {
  let ignore = false

fetch(`/api/search?q=${query}`)
    .then(res => res.json())
    .then(data => {
      if (!ignore) setResults(data)
    })

return () => {
    ignore = true
  }
}, [query])

只有当你清楚早先那几个请求还在路上,才会想到要加这段清理。

免费的 JavaScript 课程会讲到 Promise 和事件循环,React 课程会讲到 effect。

安全基础其实就是一串很短的问题:这个请求是谁发来的?他能做什么?浏览器靠什么记住他?页面允许加载哪些内容?密钥放在哪里?还有一条——任何输入在验证通过之前,都先当成恶意内容对待。

有两种「智能体」(AI agent)常犯的错误,反复出现。

第一种是把密钥留在代码仓库里。智能体调试一个失败的 API 调用时读了 .env,把密钥粘进临时脚本里「就测一下」,下一次 git add . 就把它提交了。密钥一旦进了 Git 历史,删掉那一行毫无用处,你只能去撤销(revoke)它。2026 年 9 月我把自己的软件迁移到公开的 GitHub 仓库时,对每个仓库的完整历史都跑了 gitleaks,发布前跑一遍,发布后再跑一遍。

第二种是 CSP(内容安全策略)把自己的脚本给拦了。你让智能体加一段内容安全策略,它写了 script-src 'self',结果页面自己的内联 <script> 不执行了,所有的 onclick 也一起失效。只要你去看控制台,它会告诉你原因。顺手的改法是加上 'unsafe-inline',那等于把防护整个关掉;正确的做法是用 nonce 或者哈希,这在《内容安全策略详解》里和其他安全响应头一起讲过。

如果智能体主动提出要自己写密码哈希,直接拒绝,让它用经过持续维护的库,或者用 passkeys。

免费的安全基础课程会讲到威胁建模、密钥管理,以及如何测试那些真正要紧的失败路径。

8. DNS 和一点网络知识

DNS 负责把名字翻译成地址。你输入 flaviocopes.com 时,解析器会去问权威域名服务器要那条记录,然后按记录里的 TTL(存活时间)把答案缓存一段时间。再往下,数据包在 IP 地址和端口之间流动,TCP 保证投递可靠。有一件事你必须记住:域名解析到处都有缓存,而且是分层缓存的。

智能体在这里最容易犯的错,是把「DNS 传播」当回事。你让它把网站搬到新服务器,它改了 A 记录,跑一下 dig,看到新 IP,就回报任务完成。可访客在旧 TTL 到期之前(往往是一整天)仍然访问旧服务器,因为他们本地的解析器缓存了旧答案。要是你顺手把旧服务器关掉,访客就会整整一天打不开网站。正确做法是:先把 TTL 调小,等旧 TTL 过期,再切换。

2026 年 Codex 帮我把邮件订阅服务器从一台 DigitalOcean 机器迁到另一台时,它在动公共 DNS 之前,先在测试机上强制把域名解析到新 IP,验证网站没问题。用 curl 实现这个技巧的写法:

curl --resolve flaviocopes.com:443:203.0.113.10 https://flaviocopes.com/

即便如此,切换完成后,我的浏览器里还缓存着旧 IP,而旧机器上的 Apache 刚被停掉,于是我看到「连接被拒绝」。浏览器也会缓存。最后智能体把旧服务器临时改成了指向新服务器的代理。

免费的 DNS 课程 讲了各种记录、TTL,以及如何安全地更换服务商;Networking Foundations 则覆盖 IP、TCP 和端口。

9. 读代码和调试

读代码,就是跟着一个值走:从它进入系统的地方,一路跟到它离开的地方——路由、函数、查询、响应。调试则是同样这条路,只是手上多了证据:调用栈告诉你程序崩溃时走到了哪里,日志告诉你它看到了什么,git bisect 告诉你哪个提交改变了行为。这项技能的核心,是把「它不工作」变成一个有精确答案的问题。

智能体只能修它看得见的东西。它们很少意识到,问题根本不在你的代码里。

2026 年 9 月 3 日,本站点的 Cloudflare 构建连续失败三次,报错是缺少 astro/_internal/logger 导出,可站点里什么都没改。智能体的本能反应是反复改文件,直到报错消失。但堆栈指向的是 Astro 内部,问题于是变成「构建机器上发生了什么变化?」。这个仓库不提交 lockfile,所以每次构建都会安装 package.json 允许的最新 Astro,而 7.3.0 刚发布到 npm 大约三个小时,是坏的。把版本下限提到 ^7.3.1 就修好了。

要走到这一步,你得真的读懂堆栈,而不是把它贴给智能体说「修一下」,同时还得知道 semver 范围在安装时意味着什么。

TypeScript 也该算进这项基本功。类型是编译器会替你核对的文档;开启 strict(参见《理解 tsconfig.json》)后,智能体没法悄悄把 undefined 传进本该是字符串的地方。当它开始写 as any 或 // @ts-ignore,那一行就是你要盯住的地方。

免费的《实用调试》课程涵盖堆栈跟踪、日志、Web 请求链路,以及如何安全地调试生产环境。

10. 测试:一个好测试能证明什么

测试的做法是:搭好一个场景,跑代码,然后对结果中的某一项断言做检查。好的测试证明的是你在乎的事,比如「重复的 webhook 不会发出第二封邮件」。坏的测试证明的是「代码做了代码做的事」:mock 掉依赖,调用函数,断言 mock 被调用过。这两种测试都是绿的。

智能体会写出大量第二种测试;如果代码本身建立在错误假设上,测试会继承这个假设,然后顺利通过。

Paddle 是我卖课程用的支付服务商,每笔购买它会发两次各自签名的投递:一个是履约 webhook,一个是账号级告警。有一阵子,我的 /purchase 函数从任意一个 payload 里读产品 id,两种都处理,结果买家收到了两次欢迎邮件。一个只跑正常路径的测试会通过。而能抓住这个 bug 的测试——「账号告警的 payload 不应该发出任何东西」——需要你先知道存在两种 payload。

当智能体递给我一份测试文件时,我的规矩是:至少有一个用例,在我故意改坏代码时必须失败,否则它什么都证明不了。真正重要的测试还得跑在 CI 里,因为本地钩子是智能体唯一能绕过去的东西。

免费的《JavaScript 应用测试》课程从纯函数讲到 API、数据库、浏览器和 CI。

我现在怎么学这些东西

我不再抱着书学了。我在 agent 替我写的代码里学。

循环的关键在于用 AI 来理解代码。我把 agent 指向已有的代码,一次只问一个尖锐的问题。这个 webhook 来两次会怎样?如果这里返回 200,用户是不是就拿到权限了?每个回答我都要文件路径,假设和已验证的行为要分开。然后我自己验证:发请求,读 header,打开数据库里那一行。

我读文件,不读总结,因为报告里写「完成」不算证据。一次一个任务,在我自己的机器上,这样我能看到 agent 动了什么。更完整的故事在《我用 agentic AI 六个月学到的东西》里。

还有,先试一遍:读那个函数,猜它做什么,再拿你的猜测去对 agent 的答案。这样你才能培养出判断答案是不是胡扯的直觉。

我到现在也不确定手写代码还值不值得花时间(《AI 与编程的乐趣》)。理解代码是另一回事。现在软件的生产成本很低,做的人比以往任何时候都多,但它的价值从来不在于花了多少小时,而在于东西能不能用。

如果你想找个起点,就选 HTTP 和 Git。用任何 agent 的第一个小时你都会用到这两个。

想让我聊聊你的产品?你可以赞助本站。

免费电子书

想深入一点?订阅我的通讯,解锁我的免费下载库,里面有书、课程和软件。

相关职业文章:

查看原文