20 天 · 每天 2 小时 · 共 40 小时

从零理解
编程原理与 AI 原理

页面里所有的 Python 代码都能直接改、直接跑,用的是浏览器里的真 CPython(Pyodide),不用装任何东西。第一次点「运行」会下载运行时,之后就有缓存了。

先把预期钉死

40 小时能到哪、不能到哪

能达到达不到
理解计算机执行代码的完整链路;能独立写出 50–100 行解决真实问题的程序;能读懂报错并自己排查;理解神经网络和大语言模型的工作原理;能向别人讲清楚「ChatGPT 为什么会说话」。 成为能接活的程序员;手写神经网络训练代码;掌握软件工程实践。这些需要几百小时,不是四十小时。

40 小时的正确目标不是「学会」,而是建立正确的心智模型,并装好一台能自己走下去的引擎

每天 2 小时的固定节奏

时段做什么
前 40 分钟 — 读材料或看视频,只读当天那一小块,不许超前
中间 65 分钟 — 动手写代码,必须亲手敲,不许复制粘贴
最后 15 分钟 — 写学习日志三行

最后那 15 分钟看起来最像浪费,实际是整个计划里回报最高的部分。

日程

勾掉每一天。进度存在这台设备的浏览器里,关掉页面也还在。

阶段 01 · DAY 1–6

编程的本质

里程碑 1

能从零独立写出 30 行程序。写不出来说明前面某天虚了,回去补,不要往前赶。

阶段 02 · DAY 7–11

软件如何构造

里程碑 2

能写一个「从网上取数据 → 处理 → 存到本地」的脚本。这是绝大多数软件的骨架。

阶段 03 · DAY 12–17

AI 原理

里程碑 3

能用 5 分钟向一个完全不懂技术的人讲清楚「ChatGPT 为什么会说话」,并且对方听懂了。讲不清,就等于你自己没懂。

阶段 04 · DAY 18–20

合龙

阶段一 · 编程的本质 · 第 1 天,共 6 天

Day 1 · 第一行代码

今天只有一个目标:搞清楚你敲下的那串字符,是怎么让机器动起来的。

一、你写的不是「命令」,是一份文件

很多人以为编程是「对电脑下命令」。不是。你做的事情朴素得多:用文本编辑器写一个纯文本文件,存成 .py 结尾。这个文件本身什么也不会发生,和一张购物清单没区别。

让它动起来的是另一个程序,叫 Python 解释器(你装的那个 python.exe)。整条链是:

你写的 .py 文件  →  Python 解释器读它  →  一行行翻译成 CPU 能执行的操作  →  你看到结果

所以「装 Python」装的不是一门语言,是那个翻译官。语言只是一套约定好的写法。

二、解释 vs 编译(知道区别就够)

有的语言(C、Go)是编译型:先整份翻译成机器码,生成 .exe,之后直接运行。快,但改一行就要重新翻译一遍。

Python 是解释型:运行时一行行现翻。慢一些,但改完立刻能跑。学习阶段,这个即时反馈比速度值钱得多——这也是为什么入门几乎都选 Python。

三、计算机没有「理解」你

这是今天最该带走的一句:解释器不懂你在说什么,它只是在做严格的模式匹配。

你写 print,它查表发现这是个已知的名字,对应「把括号里的东西显示出来」这个动作。你写 prnt,它查不到,就报错——不是因为它看出你打错了,而是因为表里没有。

你后面遇到的所有报错,根子都在这里:你写的东西不在它的规则里。

动手一 · 第一行代码

下面这段直接点「运行」。然后把名字改成你自己的,再跑一次。

动手二 · 引号的分量

看清楚这四行的区别。一个引号,决定了它是「数字」还是「一串字」。

最后一行输出的是 1 + 1 这三个字符本身,不是 2。加了引号,它就不再是算式,而是一段文本。这个区别是明天的主题,今天先看见它。

动手三 · 故意搞砸一次

初学者最大的心理障碍是怕报错。今天就主动撞一次,把它变成熟人。

看那行 NameError: name 'prnt' is not defined——「没见过 prnt 这个名字」。它没有责备你,它在告诉你查表失败了。报错是线索,不是判决。Day 7 会专门讲怎么读它们。

写进日志的三个问题

  1. 用你自己的话说清楚:.py 文件和 Python 解释器,分别是什么、谁让谁动起来?
  2. print(1 + 1)print("1 + 1") 为什么输出不同?
  3. 把动手三里的 prnt 改回 print,但把引号删掉一个,看会报什么错。抄下来。

今天只记一句:计算机不理解你,它只是严格地按规则查表和执行。

明天预告:今天你让程序说了话,但它还没有「记性」。Day 2 给它记忆——变量。

阶段一 · 编程的本质 · 第 2 天,共 6 天

Day 2 · 变量与类型

昨天的程序说完就忘。今天给它记性,顺便解决新手第一个真正的坑:为什么 "18"18 不是一回事。

一、变量是标签,不是盒子

很多教材说「变量是一个盒子,把值装进去」。这个比喻在 Python 里会误导你。更准的说法是:

变量是贴在数据上的一张标签

age = 18,不是把 18 塞进名叫 age 的盒子,而是内存里有个 18,你给它贴了张写着 age 的标签。再写 age = 19,标签被撕下来贴到 19 上,原来那个 18 没人要了,被回收掉。

为什么这个区别重要?因为一个数据可以贴多张标签,后面处理列表时,这一点会让你少踩一个大坑。

二、类型:数据的「品种」

类型写法装什么
int18整数
float1.75小数
str"你好"文本,必须有引号
boolTrue / False真假,首字母大写

类型决定了这个数据能做什么。数字能相减,文本不能;文本能拼接,数字的「加」是求和不是拼接。

三、新手第一大坑

从键盘、从文件、从网络读进来的东西,默认全是字符串

用户输入年龄「18」,你拿到的是 "18" 这两个字符,不是数字 18。直接拿去做算术就会炸。必须先用 int() 转换。

这个坑你未来会掉进去无数次。今天先认识它。

动手一 · 看清每个东西的类型

动手二 · BMI 计算器

** 是乘方,height ** 2 就是身高的平方。

weightheight 改成你自己的数据跑一遍。注意 f"..." 这种写法(叫 f-string),花括号里的东西会被算出来填进去,:.1f 表示保留一位小数。这个写法你会用一辈子。

动手三 · 亲手撞那个坑

报错是 TypeError: can only concatenate str (not "int") to str——「字符串只能和字符串拼接」。Python 拒绝猜你想干嘛:是想拼成 "181",还是想算成 19?它不猜,它报错。

age = "18" 的引号去掉,再跑一次。然后把引号加回来,改成 int(age) + 1,再跑一次。两种修法都试。

写进日志的三个问题

  1. 为什么说变量是「标签」而不是「盒子」?
  2. 从用户、文件、网络拿到的数据,默认是什么类型?这为什么危险?
  3. int("18")str(18) 分别做了什么?试着让 int("abc") 报错,抄下错误信息。

今天只记一句:类型决定了一个数据能做什么;外面进来的东西默认都是字符串。

阶段一 · 编程的本质 · 第 3 天,共 6 天

Day 3 · 控制流

前两天程序都是从上到下走一条直线。今天给它分岔口回头路——有了这两样,它才算是在「思考」。

一、程序默认是一条直线

解释器读你的文件,从第一行到最后一行,一条条执行,不回头也不跳跃。控制流就是打破这条直线的两种手段:

就这两样。所有软件——操作系统、游戏、浏览器——控制流都只由这两种东西组成。

二、缩进不是排版,是语法

大多数语言用花括号划定范围,Python 用缩进。这意味着:

缩进错了,程序的意思就变了。它不是「不好看」,是「不对」。

统一用 4 个空格,不要用 Tab 和空格混着来——这是新手最常见的隐形 bug,肉眼看不出区别,程序死活不对。

三、for 和 while 怎么选

用在什么时候例子
for知道要转几圈,或者要把一堆东西挨个过一遍把名单里每个人打印一次
while不知道要转几圈,只知道什么条件下该停一直猜,直到猜中为止

while 有个天生的危险:条件永远成立,就是死循环。写 while 时永远先问自己一句:什么情况下它会停?

动手一 · 分岔

score 的值多跑几次,把每个分支都走一遍。注意 elif 是「否则如果」,而且一旦有一个分支成立,后面的就不再检查了——所以顺序很重要,把 >= 90 放在 >= 60 后面,90 分就会被判成及格。试试看。

动手二 · 绕圈

动手三 · 让程序自己猜数字

经典的猜数字游戏。这里让程序来猜,而且用聪明的办法:每次猜区间正中间,一刀砍掉一半可能。

数一下用了几次。100 以内,最多 7 次必中。因为每猜一次范围减半:100 → 50 → 25 → 13 → 7 → 4 → 2 → 1。

secret 改成 1 或 100 这种极端值再试。然后把范围改成 1~1000,猜猜要几次?(答案:10 次。翻十倍,只多三次——这就是二分查找,你已经用上了一个真正的算法。)

写进日志的三个问题

  1. forwhile 分别在什么情况下用?
  2. 把动手一里的 >= 90 分支挪到 >= 60 后面,输入 95 分会怎样?为什么?
  3. 动手三里如果把 low = guess + 1+ 1 删掉,会发生什么?动手试一次(别怕,有死循环保护)。

今天只记一句:所有程序的流程,都只由「分岔」和「绕圈」两样东西组成。

阶段一 · 编程的本质 · 第 4 天,共 6 天

Day 4 · 函数

到今天为止你的代码都是一整坨。今天学会把它切开——这是从「能跑」到「能维护」的分水岭。

一、函数就是给一段代码起个名字

别把它想复杂。def 做的唯一一件事,是给几行代码贴个名字,以后喊名字就能用

好处有三个,按重要性排:

  1. 命名即解释。 看到 calculate_bmi(70, 1.75) 你立刻懂了;看到那三行公式,还得读一遍
  2. 改一处,处处生效。 公式错了只改函数里那一行,而不是满文件找
  3. 隔离。 函数内部的变量出不去,不会污染外面

二、参数和返回值:函数的两个口

参数是入口(给它什么),return出口(它还你什么)。

最常见的新手错误:print 当成 return

一个只 print 不 return 的函数,它的返回值是 None——「什么也没有」。拿去做算术就会炸。今天会亲眼看到。

三、调用栈:程序怎么知道回哪

A 调用 B,B 调用 C。C 干完了,怎么知道该回到 B 而不是回到 A?

因为解释器维护着一摞「待办便签」:每调用一个函数就往上压一张,记着「回来之后从这里继续」。函数结束就掀掉最上面一张,按上面写的地方回去。这摞便签叫调用栈

这不是冷知识——你以后看到的每一个报错,那一长串 File ... line ... 就是这摞便签的快照,从下往上就是完整的呼叫路径。Day 7 会真正用到它。

动手一 · 第一个函数

动手二 · print 和 return 的区别

两个函数看起来几乎一样,结果天差地别。

第二个报错了:unsupported operand type(s) for +: 'NoneType' and 'int'——「None 不能和数字相加」。因为那个函数只是把结果显示了出来,并没有把它交还给程序。

这个错误你以后会遇到很多次,每次都是同一个原因:忘了写 return。

动手三 · 把 Day 3 的猜数字重构成函数

同样的逻辑,切成三块。注意读起来是不是清楚多了。

这就是重构:行为一模一样,但结构变了。现在你想换一种猜法,只改 next_guess 一个函数;想改提示语,只碰 compare

写进日志的三个问题

  1. printreturn 的根本区别是什么?
  2. 一个没写 return 的函数,调用它得到什么?
  3. 动手三里,把 next_guess 改成「每次都猜 low」(线性查找),猜 73 要多少次?和二分法比一下。

今天只记一句:函数是给一段代码起名字;return 交还给程序,print 只给人看。

阶段一 · 编程的本质 · 第 5 天,共 6 天

Day 5 · 组织数据

到现在一个变量只装一个东西。要存一百个人的电话,总不能开一百个变量。今天学会成堆地装。

一、两种容器,按「怎么找」来分

长什么样怎么取东西
list
列表
["张三", "李四"]位置:names[0] 是第一个
dict
字典
{"张三": "138...", "李四": "139..."}名字:phones["张三"]

选哪个,只看一个问题:你是靠「第几个」找它,还是靠「叫什么」找它?

二、下标从 0 开始

names[0] 是第一个,names[1] 是第二个。别问为什么,记住就行——几乎所有编程语言都这样

推论:5 个元素的列表,最后一个是 names[4]。写 names[5] 会报 IndexError。这个「差一」错误(off-by-one)是编程史上最经典的 bug 类型,你会犯很多次。

好在 Python 给了个捷径:names[-1] 直接取最后一个,不用数。

三、字典查不到会炸

phones["王五"] 如果没这个人,直接 KeyError 崩掉。更安全的写法是 phones.get("王五", "查无此人")——查不到就返回你给的默认值,不崩。

处理外部数据时永远用 .get(),因为你没法保证对方给的东西里一定有那个键。

动手一 · 列表

动手二 · 字典

动手三 · 一个能用的通讯录

把今天学的和前四天的东西缝起来:字典存数据,函数封装操作,循环遍历,条件判断。

试着自己加一个功能:写一个 search_by_prefix(prefix),找出所有姓氏匹配的人。提示:字符串有个 .startswith() 方法。

写进日志的三个问题

  1. 什么情况下该用 list,什么情况下该用 dict?各举一个你生活里的例子。
  2. 5 个元素的列表,names[5] 会怎样?names[-1] 又是什么?
  3. d["x"]d.get("x") 在键不存在时表现有什么不同?哪个更适合处理外部数据?

今天只记一句:按位置找用 list,按名字找用 dict。

阶段一 · 编程的本质 · 第 6 天,共 6 天 · 整合日

Day 6 · 把五天缝起来

今天不学新东西。整合日的价值不在增加,在于把散的东西连成一条能走的路——这才是「会编程」和「学过编程」的区别。

先自检:这五样你都清楚吗

Day拿到了什么一句话自检
1执行链条.py 文件和解释器分别是什么?
2变量与类型为什么 "18" + 1 会报错?
3分岔与绕圈forwhile 怎么选?
4函数printreturn 差在哪?
5list 和 dict按位置找还是按名字找?

有答不上来的,今天先回去补那一天,别急着往下走。这不是浪费时间——里程碑 1 过不去,后面十四天会越走越吃力。

今天的任务

写一个 30 行左右的小工具,解决你自己的一个真实小麻烦

「你自己的」这四个字是重点。抄一个练习题的答案,和为自己解决一个问题,大脑投入程度完全不同。而且真实问题一定会撞上教程里没讲的细节,那才是学习真正发生的地方。

动手 · 一个范例:文本统计

先跑这个看看一个「完整的小工具」长什么样。它用上了这五天的全部东西。

读一遍代码,确认每一行你都知道在干嘛。有不确定的,回去翻对应那天。

然后写你自己的

不知道写什么?这几个都是真实需求,任选一个,或者想你自己的:

用下面这个空白区写。写不出来就回去翻前五天,翻书不算作弊,查资料是这行的日常

里程碑 1

能从零独立写出 30 行程序。「独立」的标准不是不查资料,而是:你知道自己要干什么,也知道该去查什么。做不到就回去补,不要往前赶。

写进日志

今天的日志比平时重要,多写几行:

  1. 你做的小工具是什么?解决了什么麻烦?
  2. 写的过程中卡在哪?怎么解决的?
  3. 回头看 Day 1 那行 print("你好,世界"),六天前的你和现在的你,差别在哪?

明天预告:Day 7 是全程最重要的一天,也是最反直觉的一天——专门学怎么读报错。今天写工具时你八成撞了不少红字,明天让它们从敌人变成线索。

阶段二 · 软件如何构造 · 第 1 天,共 5 天

Day 7 · 读懂报错

全程最重要的一天。不是因为内容最难,而是因为它决定了你能不能自己走下去。看懂报错的人能一直学;看不懂的人,卡住一次就放弃了。

一、先扭转一个态度

新手看到红字的第一反应是慌:「我又错了。」

但报错其实是 Python 能给你的最友善的回应。它明确告诉你:哪一行、什么问题、什么类型。相比之下,真正可怕的是不报错但结果是错的——那种 bug 能藏几个月。

报错是线索,不是判决。一个会读报错的新手,战斗力超过一个只会写代码的新手。

二、报错的三层信息

随便一个报错,信息量都很密:

Traceback (most recent call last):
  File "test.py", line 8, in <module>
    result = level_one(data)
  File "test.py", line 5, in level_one
    return level_two(data)
KeyError: 'missing_key'

拆开看:

看哪里告诉你什么
最后一行什么错KeyError = 字典里没这个键。这是最重要的一行
倒数第二、三行哪里错。文件名、行号、出事的那行代码
中间那些 File怎么走到这的。就是 Day 4 说的调用栈

读报错的顺序是从下往上。先看最后一行知道「什么错」,再往上找第一个属于你自己代码的文件名——那通常就是元凶。

为什么从下往上?因为 Python 按调用顺序打印:最上面是最早的调用,最下面是真正炸掉的地方。而报错信息本身在最底下。

三、常见报错速查

报错意思通常是因为
NameError没见过这个名字拼错了,或者变量还没赋值就用
TypeError类型不对字符串和数字混用;忘了写 return(None 参与运算)
IndexError下标越界差一错误:5 个元素却访问 [5]
KeyError字典没这个键拼错键名,或数据里本来就没有 → 用 .get()
ZeroDivisionError除以零除数是变量,恰好算出来是 0
IndentationError缩进不对空格数不齐,或空格 Tab 混用
SyntaxError语法不通少了冒号、括号没配对、引号没闭合
AttributeError这东西没这个方法把类型搞混了,比如对数字用 .append()

把这张表存进你的 errors.md两周后你会发现,90% 的报错都在这八种里。

四、调试三板斧

  1. print 大法。 在可疑的地方前后各打一个 print,把变量的值和类型都打出来。看似笨,专业程序员每天都在用
  2. 二分法缩小范围。 不知道哪出错?注释掉后一半,还错吗?错 → 问题在前半;不错 → 在后半。这就是 Day 3 那个二分查找,现在用来找 bug
  3. 橡皮鸭。 对着一只橡皮鸭(或任何东西)把代码一行行讲一遍。讲到某一行时你会突然说「哎不对」——这不是玄学,是因为「读代码」和「解释代码」用的是不同的脑力

动手一 · 把八种报错撞一遍

下面每组前面都有 #一次去掉一组的注释,跑一次,读懂报错,再注释回去,换下一组。

每撞一个,就往你的 errors.md 里记一条:报错原文 + 一句话原因。

动手二 · 读一个多层的 traceback

这段有三层函数调用,报错会显示完整的呼叫路径。

练习:先看最后一行知道是什么错,再从上往下数,说出「谁调用了谁」。最后指出真正出问题的是哪一个函数。

动手三 · 修一个不报错的 bug

最危险的 bug 不报错。下面这段能正常跑完,但答案是错的。

别直接看代码找答案,用调试三板斧。在循环里加一行 print(s, total),跑一次,盯着 total 每一轮的变化——你会立刻看见它在干什么。

这个练习的目的不是修好这一行,是让你养成「不确定就打印出来看」的反射。

写进日志的三个问题

  1. 读 traceback 应该从哪一行开始看?为什么?
  2. TypeError 最常见的两个原因是什么?
  3. 你今天的 errors.md 里记了几条?(少于 5 条说明动手一没认真做)

今天只记一句:报错从下往上读——最后一行说什么错,往上找第一处自己的代码。

阶段二 · 软件如何构造 · 第 2 天,共 5 天

Day 8 · 文件

到今天为止,你的程序一关就失忆。Day 5 那个通讯录,跑完什么都不剩。今天给它长期记忆。

一、两种记忆

在哪特点
内存RAM快,但断电即失。你的变量全在这
硬盘SSD / HDD慢几个数量级,但关机也还在

「保存」这个动作,本质就是把内存里的东西写到硬盘上。所有软件的存档、配置、数据库,底下都是这一件事。

为什么不全用硬盘?因为慢得多——Day 10 会让你亲手测出这个差距。

二、三种打开方式

模式意思注意
"r"文件不存在会报 FileNotFoundError
"w"会清空原内容!文件不存在就新建
"a"追加接在原内容后面写,不清空

"w" 是新手最容易犯的破坏性错误。想追加却写了 "w",原来的数据一瞬间全没,而且没有任何提示、没有回收站

三、必须用 with

打开文件后必须关闭,否则内容可能还卡在缓冲区没真正落盘,或者文件被一直占用。手动 close() 很容易忘,而且中途报错就执行不到了。

with 保证无论如何都会关闭——正常跑完会关,中途炸了也会关。养成习惯:打开文件永远用 with,没有例外。

四、中文用户必踩的坑

在 Windows 上读写文件,永远显式写 encoding="utf-8"

不写的话,Windows 中文系统默认用 GBK 编码,而绝大多数文件是 UTF-8。结果:中文变成乱码,或者直接 UnicodeDecodeError。这是中文开发者遇到的第一个「玄学问题」,原因就这一个。

动手一 · 写和读

页面里的文件系统在浏览器内存里,刷新页面就清空——正好当沙盒,随便折腾。本机上跑的话,这些文件会真的出现在你的文件夹里。

动手二 · 看清 w 和 a 的区别

看明白了吗?"w" 那次,之前写的三行凭空消失了。记住这个感觉。

动手三 · 让 Day 5 的通讯录活下来

加两个函数:存盘和读盘。这样程序重启后数据还在。

注意存盘用的格式:每行一条,用逗号分隔。这其实就是 CSV,Excel 能直接打开。真实项目里会用 JSON(明天讲)或数据库,但原理完全一样:把内存里的结构变成文本,再从文本还原回来。

写进日志的三个问题

  1. "w""a" 的区别是什么?哪个更危险?
  2. 为什么必须用 with 而不是手动 close()?
  3. 不写 encoding="utf-8" 会发生什么?

今天只记一句:保存 = 把内存里的东西写成文本存到硬盘;"w" 会清空原文件。

阶段二 · 软件如何构造 · 第 3 天,共 5 天

Day 9 · 站在别人肩膀上

前八天你什么都自己写。今天学会一件让效率翻几十倍的事:用别人写好的代码。这是现代软件开发真正的样子。

一、import 到底做了什么

import math 的意思是:去找一个叫 math 的文件(或文件夹),把里面定义的东西拿过来用

没有魔法。math 就是别人写好的一个 .py,里面定义了 sqrtpi 这些名字。你 import 之后,就能用 math.sqrt() 调用它们。

那个点号是命名空间:math.sqrt 意思是「math 里面那个 sqrt」。这样即使你自己也定义了一个 sqrt,两者不会打架。

二、三种「别人的代码」

来源怎么拿到例子
标准库装 Python 时自带,直接 importmath random json datetime os
第三方库pip install 下载requests pandas flask
你自己的同目录下的 .py 文件import mytools

Python 的标准库大得出名,号称「自带电池」。动手写之前先查一下标准库有没有现成的,这个习惯能省下大量时间。

三、pip 和它解决的问题

pip install requests 做的事:去 PyPI(一个几十万个包的公共仓库)下载 requests,连同它依赖的其他包一起,装到你的 Python 里。

它解决的核心问题是依赖管理:A 依赖 B,B 依赖 C 的 2.0 版本,而 D 又要 C 的 1.0——这种缠绕人工处理会疯掉。

这一段在你本机做,页面里的 Python 装不了第三方包。

pip install requests        # 装
pip list                    # 看装了哪些
pip show requests           # 看某个包的详情
pip uninstall requests      # 卸

顺带一句虚拟环境:不同项目需要同一个包的不同版本,全装在一起会冲突。解决办法是给每个项目建一个独立的环境(python -m venv .venv)。今天知道有这回事就行,真正需要它是在你同时做两个项目的时候。

动手一 · 四个最常用的标准库

动手二 · json:程序之间的普通话

json 值得单独说。它是不同程序、不同语言之间交换数据的通用格式——明天调 API 拿回来的就是它。

注意 ensure_ascii=False。不写的话中文会变成 中文 这种转义,虽然不算错但没法看。中文用户记住这个参数。

再对比一下 Day 8 那个逗号分隔的存法:JSON 能存嵌套结构(字典里套列表,列表里套字典),而逗号分隔只能存平铺的表格。这就是为什么真实项目几乎都用 JSON。

动手三 · 用 json 重写通讯录的存盘

比 Day 8 那版短了一大截,而且现在每个人可以有多个字段(电话、邮箱、备注),想加就加,不用改存盘逻辑。

今天在本机做的事

  1. 打开终端,跑 pip install requests
  2. pip list,看看你的环境里已经有哪些包
  3. 写一个 .py 文件 import requests 然后 print(requests.__version__),确认装成功了

装好放着,明天要用它。

写进日志的三个问题

  1. import math 的时候,Python 实际做了什么?
  2. 标准库和第三方库的区别是什么?各举两个例子。
  3. JSON 比「逗号分隔」强在哪?

今天只记一句:动手写之前,先查一下有没有现成的。

阶段二 · 软件如何构造 · 第 4 天,共 5 天

Day 10 · 底下发生了什么

今天补一课硬件。不写新语法,但会解释你这九天遇到的所有「为什么」——为什么程序会卡、会崩、会吃内存。

一、四个角色

部件干什么打个比方
CPU执行运算,一次只做一件事(但快到每秒几十亿次)一个手速极快的工人
内存放当前正在用的数据。快,断电即失工人面前的工作台
硬盘长期存储。慢,但持久房间角落的仓库
操作系统调度谁用 CPU、分配内存、管理文件车间主任

你的程序运行时,叫一个进程。操作系统给每个进程分一块自己的内存,互相看不见——所以一个程序崩了不会带崩整台机器。

二、速度差异大到超出直觉

把 CPU 读一次内存的时间当作 1 秒来打比方:

操作按这个比例换算
CPU 内部运算眨一下眼
读内存1 秒
读 SSD几天
读机械硬盘几个月
发一个网络请求几年

这张表解释了性能优化的第一原则:能不碰硬盘就不碰,能不发网络请求就不发。

Day 8 那个「保存」为什么慢?因为它跨越了从工作台到仓库的距离。

三、为什么程序会崩

动手一 · 亲手量一次「不可变」的代价

下面两段代码结果完全一样,速度差很多。先猜哪个快,再运行。

为什么差这么多?因为Python 的字符串不可变——s = s + "x" 不是在原来那串后面加一个字,而是新建一个更长的字符串,把原来的整个复制过去。循环两万次,就复制了两万次,总共搬运的字符数是 1+2+3+…+20000。

列表可以原地追加,最后 join 一次搞定,只搬一趟。

这是你第一次因为理解内存而写出更快的代码。

动手二 · 数据结构的选择有多贵

在十万个元素里找东西,list 和 dict 的差距会让你吃惊。

list 只能从头挨个比,最坏要比十万次。dict 用哈希直接算出位置,一步到位,和它有多少元素几乎无关。

回头看 Day 5 那句「按位置找用 list,按名字找用 dict」——那不只是写法习惯,是性能上的数量级差距。

动手三 · 画一张图(不写代码)

合上电脑,拿纸笔画出:你双击一个程序图标之后,到它出现在屏幕上,中间发生了什么。

至少要出现这些角色:硬盘、内存、CPU、操作系统、进程。画完对着讲一遍。

讲不顺就回去重读前两节——这张图是你理解「计算机」这三个字的骨架

写进日志的三个问题

  1. 内存和硬盘的根本区别是什么?为什么不全用内存?
  2. 为什么 s = s + "x" 循环两万次会那么慢?
  3. 十万个元素里查找,list 和 dict 差多少倍?(填你实测的数)

今天只记一句:CPU 快、内存中、硬盘慢、网络最慢——差的是数量级,不是一点点。

阶段二 · 软件如何构造 · 第 5 天,共 5 天

Day 11 · 软件之间怎么对话

前十天你的程序活在自己的世界里。今天让它接上互联网——这是阶段二的收尾,也是 Day 17 调 AI 的前置课

一、API 就是插座

你想在程序里显示天气。自己造气象卫星?当然不。你去找一个气象机构提供的 API——一个专门给程序用的接口。

网页是给看的,API 是给程序看的。同一份数据,两种包装。

网页返回花花绿绿的 HTML,API 返回干干净净的 JSON(昨天刚学)。因为程序不需要好看,只需要好解析。

二、一次请求的全过程

  1. 你的程序发一个 HTTP 请求到某个网址
  2. 对方服务器收到,查数据库,组装结果
  3. 返回一个 HTTP 响应:状态码 + 数据
  4. 你的程序解析数据,用它做事

状态码是响应里最先该看的东西:

意思该怎么办
200成功正常解析
401 / 403没权限检查 API key
404地址不存在检查网址拼写
429请求太频繁等一会再试,别硬冲
500+对方服务器出问题不是你的错,过会儿重试

永远先检查状态码再解析数据。直接把响应当成功来解析,是新手最常见的网络代码 bug。

三、两个必须养成的习惯

动手一 · 解析一个 API 响应

页面里的 Python 发不了真实网络请求,但拿到数据之后的处理才是重点,那部分完全一样。这是一份真实格式的天气 API 返回:

动手二 · 处理嵌套和缺失

真实 API 返回的数据往往嵌套好几层,而且你要的字段随时可能不存在。这是实战中最花时间的部分。

注意那一串 .get() 的写法。处理外部数据时永远假设它可能残缺——服务器改版、字段改名、网络截断,都会让你的 [] 直接崩掉。

动手三 · 在本机发真实请求

这一段必须在你自己电脑上跑。新建 weather.py,把下面的代码抄进去(用昨天装的 requests):

import requests
import json

# 这是一个免费、不需要 key 的公开 API(北京的天气)
url = "https://api.open-meteo.com/v1/forecast"
params = {
    "latitude": 39.9,
    "longitude": 116.4,
    "current": "temperature_2m,wind_speed_10m",
}

response = requests.get(url, params=params, timeout=10)

print("状态码:", response.status_code)          # 先看这个!

if response.status_code == 200:
    data = response.json()                       # 自动解析成字典
    current = data.get("current", {})
    print("温度:", current.get("temperature_2m"), "°C")
    print("风速:", current.get("wind_speed_10m"), "km/h")

    # 存下来 —— 这就是 Day 8 + Day 9 + 今天的合体
    with open("weather.json", "w", encoding="utf-8") as f:
        json.dump(data, f, ensure_ascii=False, indent=2)
    print("已保存到 weather.json")
else:
    print("请求失败,检查网络或网址")

跑通之后,打开生成的 weather.json 看看。把经纬度改成你所在的城市再跑一次。

连不上的话,多半是代理问题。在代码最前面加两行:import os 然后 os.environ["HTTPS_PROXY"] = "http://127.0.0.1:7078"

里程碑 2

能写一个「从网上取数据 → 处理 → 存到本地」的脚本。这是绝大多数软件的骨架——你刚写的那三十行,和一个真实的数据采集程序,结构上没有区别。

写进日志的三个问题

  1. API 和网页的区别是什么?为什么 API 返回 JSON 而不是 HTML?
  2. 拿到响应后第一件事该做什么?为什么?
  3. 为什么 API key 绝对不能写进代码?

今天只记一句:先看状态码,再解析数据;外部数据永远假设它会残缺。

阶段二完成。明天进入 AI 原理——你会发现前十一天学的每一样东西,在那边都用得上。

阶段三 · AI 原理 · 第 1 天,共 6 天

Day 12 · AI 是什么

今天搞懂「机器怎么会学习」。Day 13 拆开神经网络的零件,Day 14 看它怎么被训练出来,Day 15–16 一路走到 ChatGPT。今天是地基,别急着追大模型。

一次方向的翻转

传统编程你已经做了十一天了,它长这样:

输入  +  规则  →  输出

你写 if 分数 >= 60——规则是你想出来的,计算机只负责执行。机器学习把这个式子转了个方向:

输入  +  输出  →  规则

你给它一堆「身高和对应的体重」,它自己算出那条公式。你给它十万张标好「猫/狗」的照片,它自己找出区分的办法。整个 AI 领域,起点就是这一次翻转。

为什么非翻转不可

因为有些规则,人根本写不出来。试着用 if 写出「什么是猫」:有毛?有胡须?四条腿?——狗也全中。耳朵是尖的?折耳猫不是。你会发现你认得猫,但你说不清你凭什么认得。

这类知识叫默会知识:你会,但你讲不出规则。人脸、语音、语义、审美,全属于这一类。于是换个思路:我说不清规则,但我能给你一万个例子,你自己去找。

找规律靠的是「猜 + 纠错」

  1. 瞎猜一个规则,拿数据一验,算出错了多少
  2. 朝着错得更少的方向,把规则改一点点

重复几百万次,仅此而已。那个「错了多少」是一个具体的数字,今天你就会亲手算出它——它叫损失,是 Day 14 的主角。

三个词,现在分清

是什么
训练从数据里找规则的过程。慢、贵、只做一次
模型找出来的那个规则,本质上就是一堆数字
推理拿现成模型去用。快、便宜、每次调用都在做

动手一 · 传统编程:规则由你给

先跑一遍,看看这条老公式偏得有多离谱。

动手二 · 机器学习:规则由程序找

假设关系是 体重 = a × 身高 + b,但 ab 是多少谁也不知道。让程序把所有可能都试一遍,挑错得最少的那组。

程序没被告诉过任何公式,它只见过那五组数字。

跑完后回答(写进日志)

  1. 程序试了多少组 ab?
  2. total_error 里为什么要平方?把 ** 2 删掉再跑一次,看结果崩成什么样。
  3. data 里加一条 (200, 200) 这种明显不合理的数据,重跑。数据脏了,学出来的规则就是错的。

加分题:range(0, 101) 改成 range(0, 1001) 并把 a_step / 100 改成 / 1000。误差变小了吗?变慢了吗?——你刚体会到 AI 领域最核心的那对矛盾:精度和算力的交换

今天的原定产出

写一段一百字左右的「什么是机器学习」,不许出现「算法」「模型」「数据」「训练」「参数」。写给一个完全不懂电脑的人。

阶段三 · AI 原理 · 第 2 天,共 6 天

Day 13 · 神经元

昨天你写的 a * height + b,把 a 改名叫权重b 改名叫偏置,它就是一个神经元。你已经把神经网络最小的零件写出来了,只是没人告诉你它叫这个名字。

一、从一个输入到多个输入

总和 = w₁·x₁ + w₂·x₂ + w₃·x₃ + … + b

二、第二半:激活函数,以及为什么非有不可

假设不加激活,把一个神经元的输出直接喂给下一个:

第一层:h = w₁·x + b₁
第二层:y = w₂·h + b₂

代入化简:y = (w₂·w₁)·x + (w₂·b₁ + b₂)。括号里不过是两个新的常数

两层不加激活,结果还是一条直线,和一层完全等价。叠一百层也一样。没有激活函数,深度学习的「深」就是白搭。

名字做什么特点
ReLU负数变 0,正数不变简单粗暴,现代主流
Sigmoid把任何数压进 0 到 1像「概率」,老牌选手
阶跃大于 0 输出 1,否则 0像开关,好手算,今天用它

三、一个神经元的全貌

输入 → 乘权重 → 全部加起来 → 加偏置 → 过激活函数 → 输出

这就是全部。 从今天到 GPT,零件没变过,变的只是数量。

动手一 · 先用纸笔(15 分钟,不许开电脑)

输入 x₁=2, x₂=4,权重 w₁=0.5, w₂=-0.3,偏置 b=0.1,激活用 ReLU。在纸上算出输出,写下每一步。

算完了再跑下面这段对答案:

对不上就停下来查,别往下走。(答案是 0 ——注意 x₂ 的权重是负的,它在反对,而且 x₂ 比较大,一票把结果否掉了。)

动手二 · 一个神经元能做逻辑判断

同样的结构,只改了偏置,行为就完全不同。 这就是「学习」要调的东西。

动手三 · 一道做不出来的题

现在做 XOR:两个不一样时输出 1,一样时输出 0。

花 10 分钟,试着给单个神经元找一组 w₁、w₂、b 满足这四行。 用下面这个改权重试:

十分钟后你会失败。这不是你的问题——它无解。1969 年 Minsky 和 Papert 在书里证明了这一点,直接掐灭了当时的神经网络研究,业界叫它第一次「AI 寒冬」,冷了将近二十年。

动手四 · 两层就解决了

四行全对。你刚刚做了两件大事:

  1. 造出了你的第一个神经网络——第一层两个神经元,第二层一个
  2. 亲手验证了为什么需要「深度」。不是深了效果好一点,是浅的根本做不到

写进日志的三个问题

  1. 数一数你的 XOR 网络一共有多少个参数(权重 + 偏置)?
  2. NAND 的偏置从 1.5 改成 0.5,哪几行错了?为什么?
  3. 如果去掉激活函数,XOR 还能成立吗?为什么?

今天只记一句:没有激活函数,叠多少层都等于一层。

阶段三 · AI 原理 · 第 3 天,共 6 天

Day 14 · 训练

昨天每一组权重都是我直接给你的——凭什么是这几个数?我怎么知道的?今天补上这个洞。训练,就是自动找出这些数字的过程。

一、你已经训练过一次了

Day 12 那个双层循环就是一次完整的训练:一个衡量「错了多少」的损失函数,一堆待定的参数,一个搜索过程叫优化。今天要解决的只有一件事:那个搜索办法慢到不能用。

二、为什么暴力穷举必然死掉

Day 12 有 2 个参数,每个试 100 种,一万次,眨眼就完。那 10 个参数呢?100¹⁰ = 10²⁰ 次,用最快的超算也要跑上千年。

Day 13 那个 XOR 网络有 9 个参数——已经算不动了。GPT 那种几千亿参数的,穷举次数比宇宙里的原子还多。每多一个参数,搜索空间就乘一次。这叫维度灾难。

三、换个思路:蒙着眼睛下山

浓雾里要走到谷底。穷举 = 把每一平方米都走一遍,荒谬。聪明的办法:用脚感觉哪边是下坡,朝那个方向迈一步,再重新感觉。

机器学习
你所在的位置当前这组参数值
海拔高度损失(错得多少)
脚下的坡度梯度

关键在于:算坡度只看你脚下这一点,不需要走遍整座山。 这就是它比穷举快出天际的原因。

四、每步走多远:学习率

五、反向传播(今天只要直觉)

网络有很多层,输出错了,到底是哪一层哪个权重的锅? 反向传播就是回答这个的:从输出端的误差出发,一层一层往回分摊责任。像追查一次事故——从结果倒推,每个环节按它实际的影响力承担一份;影响大的,调整也大。

训练 = 反向传播算坡度 + 梯度下降迈步子,循环几百万次。

动手一 · 最小的梯度下降

先不碰神经网络。找 f(x) = (x-3)² 的最低点——你一眼知道答案是 3,正好用来验证。

盯着那两列数字往下看——x 一步步爬向 3。没人告诉过程序答案。

注意 x = x - lr * g 这一行。这就是整个深度学习的核心公式。 GPT 训练时跑的本质上也是它,只不过 x 换成了几千亿个数。

动手二 · 把学习率玩坏

把上面那段的 lr 依次改成 0.01 / 0.1 / 1.0 / 1.1,每次重跑。

1.1 那次看清楚——数字会左右横跳,而且越跳越远。这就是「飞出去」。没有公式能算出最佳学习率,只能试。

动手三 · 回到 Day 12,把暴力搜索换掉

先做一件真实工程里必做的事:把身高缩放到 -1 到 1 之间。原始数值 150~190 太大,梯度会跟着爆掉。

Day 12 暴力穷举Day 14 梯度下降
计算次数12,221500
精度卡在 0.01 的格子上想多准有多准
加到 10 个参数宇宙热寂也算不完一样 500 步

这就是为什么现代 AI 能存在。 不是硬件变快那么简单,是有了一个不随参数数量爆炸的搜索办法。

写进日志的三个问题

  1. x = x - lr * g,为什么是梯度不是加?(想想站在 3 左边时 g 是多少)
  2. 动手三里如果不做特征缩放,直接用原始身高,会发生什么?动手试一次。
  3. 用一句话说清损失函数和梯度的关系。

阶段过半:你已经掌握了神经网络的全部零件。从这里到 ChatGPT,不再有新的基本原理,只有规模和结构上的变化。

阶段三 · AI 原理 · 第 4 天,共 6 天

Day 15 · 从数字到语言

前三天处理的输入全是数字。可语言不是数字——「小猫」这两个字,要怎么喂进一个只会做乘法和加法的神经元?答案分三层:先切开,再编号,最后让它们互相看见。

一、切开:token

第三种叫 BPE,规则蠢得出奇:统计哪两个相邻片段一起出现得最多,就把它俩合并。重复几万次。

顺带解释一个你早该好奇的现象:模型数不清「strawberry 里有几个 r」——因为它看到的根本不是字母,而是被切好的两三个块。字母级别的信息,在切开那一刻就丢了。

二、编号:embedding

每个 token 对应一串数字(几百到几千个)。关键在于它怎么来的:不是人写的,是训练出来的——就是 Day 14 那套梯度下降。调完之后会自动出现一个惊人的性质:

意思相近的词,数字也相近。
国王 − 男人 + 女人 ≈ 女王

意思变成了空间里的位置,语义变成了几何。 注意:那几千个数字每一维代表什么,没有人知道。不是保密,是真的没人知道。

三、看见彼此:注意力

还差最后一块。两个问题:「我吃苹果」和「苹果发布会」是同一个 token,意思却完全不同;「小猫追鱼,因为饿了」——「它」是谁?

让每个 token 去打量句子里所有其他 token,算出「我该重点看谁」,然后按这个权重,把别人的信息揉进自己的表示里。

「它」打量一圈,发现和「小猫」最像,于是给「小猫」很高的权重、给「追」几乎为零。词义随上下文而变,自动实现。

四、为什么「注意力」是那次突破

在它之前,主流是从左到右一个词一个词地读(RNN),两个致命伤:距离一远就忘;没法并行——必须读完第 3 个才能读第 4 个,GPU 有一万个核也只能干等。

注意力两刀砍掉:任意两个位置一步直连;所有位置可以同时算。第二点才是引爆点——能并行,才能上万张 GPU 一起训;能一起训,才可能有几千亿参数的模型。

注意力内部那些 Q、K、V 本质仍是权重矩阵,仍然靠梯度下降训练出来。Day 13 和 14 之后,你没有再遇到过新的基本原理。

动手一 · 让程序自己发现「词」

程序自己发现了「情商」和「血压」是一个整体——没人告诉过它中文有词语这回事,它只是数了数谁和谁总一起出现。真实模型对着几万亿字跑同样的过程几万轮,就得到了词表。

动手二 · 意思变成距离

猫和狗最近,和苹果最远。「意思」被换算成了距离。

动手三 · 手算一次注意力

句子:小猫 追 鱼,因为它饿了。「它」要去打量前面三个词:

看那三根柱子:「它」把七成注意力给了「小猫」,给「追」的几乎为零。模型没有语法规则,没有指代消解算法——它只是算了几个点积,做了一次加权平均,代词就找到了主语。

写进日志的三个问题

  1. 动手一里,如果 corpus 换成八个完全不重复的词,BPE 会合并出什么?为什么?
  2. 动手三把 * 5 改成 * 1 再改成 * 20,三根柱子怎么变?太大或太小分别有什么坏处?
  3. 为什么大模型数不清「strawberry 里有几个 r」?

阶段三 · AI 原理 · 第 5 天,共 6 天

Day 16 · 它为什么会说话

昨天结束时你手上有一台能理解上下文的机器,但它离「会聊天」还差得远。

大模型只会做一件事——猜下一个 token 是什么。对话、写代码、翻译、解数学题,全都是这一件事的副产品。

一、第一道工序:预训练

训练目标简单到近乎荒谬:遮住一段文字的后半截,让模型猜下一个 token,猜错了就用 Day 14 那套梯度下降调参数。

为什么这个笨办法能成?因为它不需要人工标注——文本本身就自带答案。互联网上有多少字,就有多少道免费的练习题。而想把这道题做准,模型被逼着学会记事实、学会指代、学会算术、学会写代码。

没有人专门教过它算术。 只是为了把下一个字猜得更准,它不得不顺带学会。这种现象叫涌现——能力不是设计出来的,是压出来的。

「知识截止日期」就是从这来的。 训练数据是某个时刻的世界快照,那之后发生的事,模型的权重里根本没有。

二、可是预训练完的模型不能用

因为它只会续写。你问「中国的首都是哪里?」,它可能回你「中国的最大城市是哪里?中国的人口有多少?」——在互联网上,一个问题后面确实经常跟着更多问题。它猜得非常准,只是猜的不是你要的东西。

三、第二道工序:微调

人工写一批「问题 + 好答案」的范例,拿这些继续训练。量不大(几万到几十万条)但质量高。练完之后模型学会了:看到问题,该给答案,而不是续写更多问题。

四、第三道工序:人类反馈

「什么是好答案」这件事写规则写不出来,但人一看就知道——又是默会知识。于是:生成好几个答案 → 请人排序 → 拿排序训练一个打分模型 → 让主模型去追求高分。这就是强化学习

工序干什么成本产出
预训练猜下一个词天价博学但不会聊天的续写机
微调学会答题格式中等会回答问题的助手
人类反馈学会什么算好中等有分寸的助手

五、幻觉到底从哪来

训练目标从头到尾都是「像真的」,从来不是「是真的」。

翻遍三道工序,没有任何一步的损失函数里写着「事实正确」。对一台猜下一个词的机器来说,「爱因斯坦生于 1879 年」和「生于 1872 年」流畅度完全一样。三个原因叠加:

  1. 知识是弥散的,不是存起来的。 模型里没有一张可查的事实表,知识被抹在几千亿个权重里。它没法「查一下自己知不知道」,只能生成
  2. 训练数据里几乎没有「我不知道」。 人写文章很少写「这我不清楚」,所以这个句式概率天然就低
  3. 人类反馈可能帮了倒忙。 打分的人往往更喜欢自信、完整的回答

结果就是那个最危险的特性:它编造的时候,和它正确的时候,语气一模一样。 现代模型校准好了很多,但这个根子拔不掉。这不是 bug,是这个训练范式的必然产物。

动手一 · 亲手做一台续写机

用最原始的办法实现「猜下一个字」:统计每个字后面都跟过什么字,然后按频率随机抽。

多跑几次,盯着那些句子看一会儿。 这就是幻觉最纯粹的形态:每一步的下一个字都合法、都符合统计规律,连起来却毫无事实可言。这台机器根本没有「真假」这个概念——而 GPT 和它是同一类东西,只是把「看前 1 个字」换成了「看前几十万个 token」,把统计表换成了几千亿个权重。

动手二 · 让它看得更远

明显通顺了。记住这个对比——从「看 1 个字」到「看 2 个字」,通顺度就跳一大截。这正是过去几十年语言模型一路往上爬的方向:让模型看得更远、看得更准。昨天那个注意力机制,就是这条路上的终极答案。

动手三 · 找出它必然答错的三类问题

不要靠瞎试,用今天学的原理推导出来,再去验证(打开任何一个聊天 AI 即可)。

类型原理怎么问
知识截止之后训练快照之后的世界不在权重里问一件你确知发生在最近几个月的事
字符级操作token 化那一刻字符信息就没了数某个长词里某个字母出现几次,或把词反过来拼
极冷门的具体事实训练数据里出现太少,没编码进权重,但它照样流畅生成问某本冷门书第几页写了什么。重点看它编造时的语气有多笃定

这 20 分钟的真正目的不是抓 AI 的错,而是让你亲眼确认:它错的时候,你从语气上看不出来。 这个体感,比今天所有理论都重要。

写进日志的三个问题

  1. 为什么预训练不需要人工标注数据?这一点为什么至关重要?
  2. 只做完预训练、跳过后面两道工序的模型,你问它问题会得到什么?
  3. 为什么「训练目标是像真的,而不是是真的」必然导致幻觉?

你现在应该能完成里程碑 3 了——用 5 分钟给一个完全不懂技术的人讲清楚 ChatGPT 为什么会说话。今天找个人试一遍,讲不顺的地方就是你的漏洞。

阶段三 · AI 原理 · 第 6 天,共 6 天

Day 17 · 亲手调一次

前五天全是原理。今天把 Day 15、16 的每个概念对应到真实参数上——调完这一次,大模型对你就不再是黑箱。

一、最反直觉的一件事:API 没有记忆

API 是无状态的。它不记得你上一句说了什么。

你在聊天软件里感受到的「它记得我们聊过什么」,完全是客户端的功劳:每次发消息,程序都把之前的全部对话重新打包发过去一遍。模型每次都是第一次见你,只不过这次收到的材料里包含了历史。

三个直接后果:

二、messages 的结构

请求的核心是一个列表,每项有 rolecontent:

role是谁说的
system给模型的设定:身份、语气、规则。放在 messages 的第一条
user你说的
assistant模型之前说的——这就是「记忆」的载体

把模型的回复用 assistant 角色塞回历史,下一轮它就「记得」了。整个多轮对话的秘密,就这一句话。

三、几个必须懂的参数

参数作用
model用哪个模型。能力和价格差别很大
messages完整对话历史,第一条通常是 system
max_tokens输出的上限。给小了会被截断在半句话中间
temperature随机程度。0 更稳定保守,1 更发散。写代码用低的,写文案用高的

token 不是字。 Day 15 讲过,中文大致一个字一到两个 token,英文一个词往往就是一个。计费按 token 走,输入和输出分别计价,输出通常贵好几倍

四、API key 的安全(重来一遍,因为真的会出事)

API key 永远不写进代码。放环境变量,并确认 .env.gitignore 里。

泄露的后果很实在:别人拿你的 key 花你的钱。GitHub 上有专门扫 key 的爬虫,提交后几分钟就能被捞走——而且删掉那次提交也没用,历史记录里还在。

动手一 · 亲眼看见「无记忆」

下面用一个假模型演示。它只能看到你传给它的 messages,没有任何别的记忆——真实 API 就是这样。

看清楚了吗?同一个模型,同一个问题,区别只在于你有没有把历史一起发过去。

动手二 · 写一个对话管理器

把维护历史这件事封装起来。这段逻辑,你在任何一个 AI 应用里都会重写一遍。

注意最后那个统计:第五轮发出去的内容,是第一轮的好几倍。这就是长对话变贵变慢的原因,也是为什么真实产品需要「压缩历史」。

动手三 · 在本机真调一次

这一段必须在你自己电脑上做——页面里跑不了,也不该把 key 放进网页。

先选一家

用国内能直连的模型,不用代理、支持支付宝/微信充值。关键是:下面这四家全都兼容同一套接口格式(业界叫「OpenAI 兼容」),所以代码只有两行不同:

厂商base_urlmodel 填什么特点
DeepSeekhttps://api.deepseek.comdeepseek-chat便宜,文档清楚。推荐从这家开始
智谱 GLMhttps://open.bigmodel.cn/api/paas/v4/glm-4-flash有免费额度,想零成本先试就用它
Kimihttps://api.moonshot.cn/v1moonshot-v1-8k长文本处理是强项
腾讯混元https://api.hunyuan.cloud.tencent.com/v1hunyuan-turbo接在腾讯云体系里

模型名和价格会变,以各家官方文档为准。注册一般是手机号 + 实名,几分钟。

「兼容 OpenAI 格式」是个重要概念:接口成了事实标准之后,换模型的成本从「重写代码」降到了「改两行配置」。这也是为什么下面这段代码你学一次就能用在所有家上。

1. 装 SDK 并配置 key

注意:装的是 openai 这个包,但连的是国产模型——因为它们说的是同一套「话」。

pip install openai

# PowerShell,只在当前窗口有效
$env:LLM_API_KEY = "你申请到的key"

2. 新建 chat.py

import os
from openai import OpenAI

# ↓↓↓ 换厂商只需要改这两行 ↓↓↓
BASE_URL = "https://api.deepseek.com"
MODEL    = "deepseek-chat"

client = OpenAI(
    api_key=os.environ["LLM_API_KEY"],   # 从环境变量读,绝不写在代码里
    base_url=BASE_URL,
)

# 历史由你维护。第一条是 system,给模型定人设
messages = [
    {"role": "system", "content": "你是一个耐心的编程老师,回答简短,多用比喻。"}
]

print("开始聊天,输入 quit 退出\n")

while True:
    text = input("你:")
    if text.strip() in ("quit", "exit", ""):
        break

    messages.append({"role": "user", "content": text})

    response = client.chat.completions.create(
        model=MODEL,
        max_tokens=1024,
        messages=messages,          # 每次都发完整历史
    )

    reply = response.choices[0].message.content
    print("AI:", reply, "\n")

    # 关键的一步:把回复塞回历史,下一轮它才「记得」
    messages.append({"role": "assistant", "content": reply})

    usage = response.usage
    print(f"  [本轮 输入 {usage.prompt_tokens} tokens,"
          f"输出 {usage.completion_tokens} tokens]\n")

连不上的话,先确认没开代理——国内的 API 走代理反而可能失败。如果你按 Day 11 设过 HTTPS_PROXY,这里要先取消:$env:HTTPS_PROXY = ""

3. 跑起来,然后做三个实验

  1. 先说「我叫某某」,再问「我叫什么」。然后把 messages.append({"role": "assistant"...}) 那行注释掉,重来一次——它会失忆
  2. 盯着每轮的 token 数,看它怎么随对话增长
  3. messages 里第一条 system 的内容,比如改成「你只能用五个字回答」,感受系统提示词的威力
  4. 换一家试试——只改 BASE_URLMODEL 两行,别的一个字不动。跑通之后你会真正理解「接口标准」的价值
阶段三完成

从 Day 12 的「什么是机器学习」到今天亲手调通 API,你已经走完了从原理到实践的整条链。现在你看到的每一个 AI 产品,底下都是你今天写的这三十行。

写进日志的三个问题

  1. API 是无状态的,那多轮对话的「记忆」是谁维护的?
  2. 为什么对话越长越贵?
  3. 把模型回复塞回历史时,role 应该填什么?不塞会怎样?

今天只记一句:模型没有记忆,「对话」是你每次把完整历史重新发过去。

阶段四 · 合龙 · 第 1 天,共 3 天

Day 18 · 项目:读懂自己的日志

接下来两天做一个真东西:一个读你这 20 天学习日志、然后出题考你的助手。今天先把「读」这一半做完。

为什么是这个项目

它不是随便选的。它同时用到:

来自用在哪
Day 5 数据结构把日志组织成列表套字典
Day 7 调试解析真实文本一定会出意外
Day 8 文件log.md
Day 9 模块json 存中间结果
Day 17 API明天让 AI 出题

更重要的是:它的产出直接服务于你自己的持续学习。这就是那个正反馈闭环本身。

一、先想清楚再动手

这是今天最该练的习惯——写代码之前,先在纸上把三件事写下来:

  1. 输入是什么?(一个 Markdown 文件,格式是我自己定的)
  2. 输出是什么?(一个列表,每项是一天的记录)
  3. 中间要经过哪几步?(读文件 → 按天切开 → 每天里再分「懂了/卡住/明天」)

新手最容易犯的错是打开编辑器就开始敲。想清楚这三件事,能省掉一大半返工。

二、约定日志格式

解析的前提是格式固定。用这个(你前面几天的日志如果不长这样,现在整理一下):

## Day 1
懂了:.py 是文本文件,解释器才是让它动起来的东西
卡在:分不清 print 和引号的关系
明天:变量

## Day 2
懂了:变量是标签不是盒子
卡在:TypeError 看不懂
明天:控制流

## Day N 开头,下面三行分别以「懂了:」「卡在:」「明天:」开头。简单、好写、好解析——设计数据格式时,这三条永远比「功能强大」重要。

动手一 · 写解析器

这里用一段内置的示例日志。跑通之后,把 log_text 换成你自己的。

动手二 · 从里面挖出信息

解析完只是拿到了结构化数据,真正有用的是从中发现规律

看那个「卡住的地方」汇总——那就是你的知识地图上的坑。如果某个词反复出现,说明那不是一次偶然的卡壳,是真的没学透。

动手三 · 在本机跑通

新建 study_helper.py,把动手一二的代码合起来,改成从真实文件读:

with open("log.md", "r", encoding="utf-8") as f:
    log_text = f.read()

entries = parse_log(log_text)
# ... 后面接动手二的统计

FileNotFoundError 就说明你的 log.md 不在同一个文件夹——这是 Day 7 的练习,自己排查。

今天的检验

能把你自己真实的日志读进来并正确拆分,今天就算过。拆不对多半是格式不统一——这也是真实数据处理的日常:大部分时间花在清洗上,不是算法上。

阶段四 · 合龙 · 第 2 天,共 3 天

Day 19 · 项目:让 AI 考你

昨天读懂了日志,今天接上大脑。做完这一天,你就有了一个真正属于自己的工具。

一、提示词是这个项目的核心

代码只有几十行,效果好坏几乎全取决于提示词。这是 AI 应用和传统软件最大的区别:逻辑的一部分不在代码里,在你写给模型的那段话里。

三条经验,今天会一一验证:

二、先在纸上设计提示词

动手写代码前,先把要发给模型的那段话想好:

你是一个严格但友善的助教。下面是学生某一天的学习笔记。

请出 3 道复习题,要求:
- 针对他「卡住」的地方重点提问
- 不要直接给答案
- 每题一行,用「1. 2. 3.」编号
- 只输出题目,不要多余的话

笔记:
{这里填当天的日志}

注意最后两条——「每题一行,编号」和「只输出题目」是给程序看的。没有这两条,模型可能返回一大段散文,你的代码就没法解析。

动手一 · 先用假模型跑通流程

真调 API 之前,先用假的把整个流程走通。这样调试不花钱、不受网络影响——这是专业做法。

动手二 · 换成真模型(本机)

流程跑通了,把 fake_ai 换成真的。整个程序只有这一个函数要改,其余一个字不动——这就是把代码切成函数的回报。

用 Day 17 选好的那家(下面以 DeepSeek 为例,换厂商就改 BASE_URLMODEL)。

import os
from openai import OpenAI

BASE_URL = "https://api.deepseek.com"
MODEL    = "deepseek-chat"

client = OpenAI(api_key=os.environ["LLM_API_KEY"], base_url=BASE_URL)


def ask_ai(prompt):
    """把 fake_ai 换成它,函数签名完全一样"""
    response = client.chat.completions.create(
        model=MODEL,
        max_tokens=1024,
        messages=[
            {"role": "system", "content": "你是一个严格但友善的助教,只输出题目,不要寒暄。"},
            {"role": "user", "content": prompt},
        ],
    )
    return response.choices[0].message.content


def make_questions(entry):
    return ask_ai(build_prompt(entry))      # build_prompt 沿用动手一的


# 用 Day 18 解析出来的 entries
for e in entries:
    print(f"\n=== {e['day']} ===")
    print(make_questions(e))

注意 ask_ai入参和返回值,和假的那个一模一样:给一段文字,还一段文字。所以调用它的代码完全不用改。

这个「先用假的跑通流程,再换成真的」的做法,在真实开发里叫 mock。它让你调试提示词逻辑时不花钱、不等网络、结果可复现。做 AI 应用的人天天在用。

三、然后开始调提示词

第一版出来的题多半平庸。接下来的时间全花在改提示词上,每改一次跑一次,对比效果:

这个「改一点 → 跑一次 → 看效果」的循环,就是 AI 应用开发的日常。它和 Day 14 的梯度下降是同一个形状:小步调整,朝效果更好的方向走。

今天的检验

用你自己真实的日志跑一次,拿到的题目确实问到了你心虚的地方。真的坐下来答一遍——这才是这个工具的意义。

可以加的功能(有余力再做)

阶段四 · 合龙 · 第 3 天,共 3 天 · 最后一天

Day 20 · 装好引擎

最后一天不学新东西。今天做两件事:看清自己走了多远,以及确保这台机器在明天之后还能自己转下去

一、先回头看

打开你的 log.md,读第一天那三行。

二十天前,你不知道 .py 文件和解释器的区别。现在你能解释注意力机制为什么是那次突破。这个跨度值得你停下来认真感受一下——不是自我表扬,是因为下一次卡住的时候,这个记忆是你继续下去的燃料。

动手一 · 用程序分析你自己的学习

用 Day 18 的解析器,统计这 20 天。

log_text 换成你真实的日志跑一次。「卡住关键词」那一栏尤其值得看——反复出现的,就是你要在接下来一个月重点补的。

二、四个已经装好的引擎零件

这 20 天真正的产出不是知识,是这四样东西。它们在明天之后还继续工作。

零件为什么它能持续
代码是客观裁判跑通了就是跑通了。不需要老师,不存在「我以为我懂了」
报错是线索Day 7 之后,卡住不再等于放弃,而是等于「该查什么」
日志有落差每天三行,二十天后回看的落差就是燃料
难度可校准太难就拆小,太简单就加码。永远待在「有点费劲但能做出来」的区间

三、接下来一个月怎么走

最重要的一条:不要再找下一个「20 天课程」。

入门阶段需要有人给你排路线,因为你不知道该学什么。这个阶段已经过去了。再找教程,是在回避「自己决定学什么」这件事——而那才是真正的门槛。

正确的下一步只有一个形状:选一个你真的想要的东西,然后把它做出来。

如果你想…就去做
把编程学扎实挑一个你每天在手动做的事,写程序自动化掉。读《流畅的 Python》
往 AI 应用走把 Day 19 那个助手做成能用的产品。读《这就是 ChatGPT》,跟李沐的《动手学深度学习》
往底层走《深入理解计算机系统》(CSAPP)。硬,但是分水岭
还不确定把 Day 6 那个小工具扩展成有界面的。方向会在做的过程中浮现

动手二 · 写给三个月后的自己

今天的最后一件事,也是唯一的作业。新建 roadmap.md,写四段:

  1. 这 20 天,最大的三个认知转变是什么?(不是「学会了什么」,是「原来我以为…,现在明白…」)
  2. 我最心虚的是哪一块?(看统计出来的「卡住关键词」)
  3. 接下来一个月,我要做出来的那个东西是什么?(具体到能验收:「做一个能自动整理我下载文件夹的工具」,而不是「继续学 Python」)
  4. 我怎么知道自己没有停下?(定一个可检验的标准:比如「每周至少提交三次代码」)

写完存好。三个月后打开它。

20 天完成

你现在能读懂代码、能自己排查报错、理解计算机怎么运行、理解大模型为什么会说话,并且有一套能自己转下去的学习机制。剩下的路没有捷径,但你已经知道怎么走了。

自由练习

练习台

空白的 Python 环境,随便写。和上面所有代码块一样,跑的是浏览器里的真 CPython——标准库都在,mathrandomjsondatetime 都能用。

正在加载 Python 运行时…