第三课:自由软件、协作构建与 AI Agent

核心问题

第一课你理解了网络——你知道了数字世界的地图。第二课你学会了编程——你知道了如何让电脑为你工作。

但还有一个更根本的问题:你怎么和他人一起建造东西?

你自己写了个脚本很好。但如果两个人同时改一份代码怎么办?如果一百个人一起做一个项目怎么办?你怎么知道你脚下的操作系统在干什么?还有那个最近到处都在说的「AI Agent」——它到底是什么?边界在哪?跟你会编程这件事有什么关系?

这一课回答这些问题。它分成上中下三篇:

  • 上篇:建造的基石——自由软件、Git、GitHub。你学会如何分享、如何协作。

  • 中篇:你脚下的操作系统——Linux 到底是什么,文件系统、权限、进程、容器化。你学会如何理解并控制你的电脑。

  • 下篇:AI Agent——从文本补全到工具调用,Agent 的本质和边界。你学会如何让 AI 成为你的放大器。


上篇:建造的基石

第一章:自由软件与开源运动

核心问题:为什么有人免费写软件给你用?

你打开电脑,浏览器是 Firefox(或者 Chrome),编程用的是 Python,编辑器可能是 VS Code。这些东西你一分钱没花。谁写的?为什么写?他们图什么?

答案不在技术里。答案在人心里。

什么是自由软件

「自由软件」(Free Software)里的 free 不是「免费」,是「自由」。Richard Stallman 在 1980 年代定义了四个自由:

  • 自由 0:为任何目的运行这个软件的自由。

  • 自由 1:研究这个软件怎么工作的自由,并修改它来满足你的需求。前提是你能看到源码。

  • 自由 2:复制和分享这个软件的自由——你可以帮到你的邻居。

  • 自由 3:改进这个软件并公开发布你的改进的自由——这样整个社区都受益。前提同样是你能看到源码。

四个自由的核心只有一句话:你拥有这个软件,而不是这个软件拥有你。

你用微信,腾讯可以决定什么时候改界面、什么时候加广告、什么内容不让发——你只是使用者,软件不属于你。你用自由软件,你可以看它的源码、改它的行为、把它分给你的朋友——你是主人。

自由软件 ≠ 免费软件

自由软件可以卖钱。Stallman 当年就是靠卖 GNU Emacs 的拷贝磁带赚钱的。自由软件的「自由」是指权利,不是指价格。

你可以把自由软件打包卖钱——但你不能阻止买你软件的人也去分享它。他付钱买的是你的服务、你的技术支持、你帮他省的时间。他如果回头把软件免费发出去,你也拦不住——因为自由 2 保护了他的分享权。

这个逻辑跟传统商业不一样。传统商业的逻辑是:我卖一个东西,你付钱,你不能复制。自由软件的逻辑是:我写了一个东西,我开放它,你能用、能改、能分享。我可能通过技术支持、定制开发、培训来赚钱。或者我就不赚钱——我做这件事本身就有意义。

开源许可证

「开源」(Open Source)跟「自由软件」基本是同一套东西的不同叫法。自由软件强调道德和权利,开源强调工程实践的好处——让更多人看代码、找 bug、贡献功能,软件质量会更高。

不管叫什么,核心机制是一样的:许可证(License)

你写了一段代码,默认情况下,根据版权法,别人不能随便用、不能随便改。如果你想让别人用、想让别人改,你需要一个许可证来告诉他们:你可以做什么,不可以做什么。

常见的开源许可证:

许可证

核心态度

约束

MIT

「拿去用,随便怎么用,别告我就行」

几乎无。只需保留版权声明。

Apache 2.0

「跟 MIT 差不多,但额外讲了专利的事」

保留版权声明,明确专利授权。

GPL v3

「你改了之后也必须开源」

传染性:基于 GPL 代码的衍生作品必须也用 GPL 发布。

LGPL

「库可以用,但如果改库本身要开源」

比 GPL 松——你用了 LGPL 的库,你自己的代码可以不开源。

BSD

「跟 MIT 差不多」

几乎无。

AGPL

「如果你在网络上提供服务,也要开源」

GPL 的加强版——堵了「我用 GPL 代码开个网站但不发布源码」的洞。

选择许可证 = 选择你和社区的关系。

你选 MIT,意思是:「我写的东西谁都可以拿去用,拿去卖钱也行,闭源也行,我不在乎。」你选 GPL,意思是:「我写的东西开放给大家,但谁要是基于我的东西继续开发,也必须继续开放——这是我和社区的契约。」你选 AGPL,意思是:「包括在网上提供服务也算『发布』——别想钻空子。」

没有哪个「最好」。它们只是不同的人在不同的情况下做的不同选择。Linux 内核是 GPL v2,Python 是 PSF 许可证(类似 MIT),Android 是 Apache 2.0,MongoDB 后来从 AGPL 改成了 SSPL(更强的限制)。

类比:Minecraft 社区

你玩过 Minecraft 的话,你已经参与过开源文化了——只是你可能没有意识到。

Minecraft 社区里:

  • 有人做 Mod(修改游戏),分享出来

  • 有人做整合包,把别人的 Mod 组合成一个新体验

  • 有人做材质包、光影包

  • 有人做建筑存档分享给大家参观

  • 有人做红石教程,有人做农场设计

这些人做的事情,和开源社区做的事情在结构上是一样的:

  • 你做了一个 Mod → 别人下载、使用、在评论区反馈

  • 你的 Mod 依赖了另一个 Mod 的 API → 你在别人的基础上建造

  • 有人把你做的 Mod 放进整合包里 → 你的作品被二次创作

  • 有人发现了 bug,在 GitHub 上提 issue → 你修复,大家受益

  • 有人 fork 了你的项目,加了新功能 → 社区分叉出新路线

唯一不同的是:Minecraft 社区分享的是游戏的模组和存档,开源社区分享的是代码。但驱动这两者的是同一种力量:被人看到、受到反馈和欣赏、改造世界也改造自己。

你在 CurseForge 上发布一个 Mod,下载量涨到 1000,底下有人留言说「太好用了谢谢你」——这种感觉,和你 pip install 了一个别人写的库、解决了你的问题、然后去 GitHub 上点了个 star——是同一种感觉。

PyPI:不只是 pip install

你在第二课用过 pip install。但你有没有想过——pip 从哪里下载这些包?那些包是谁放到那里的?

PyPI(Python Package Index) 就是 Python 的「应用商店」。跟苹果 App Store 不一样的是:PyPI 是开放的。任何人都可以注册账号,任何人可以上传包。没有审核,没有「不通过」,没有分成——你上传,别人就能下载。

上传一个你自己的包到 PyPI,流程非常简单:

# 1. 注册 PyPI 账号:https://pypi.org

# 2. 把你的项目整理好
# myproject/
# ├── myproject/
# │   ├── __init__.py
# │   └── core.py
# ├── README.md
# └── pyproject.toml    # 包的元数据

# 3. 安装打包工具
pip install build twine

# 4. 构建
python -m build

# 5. 上传
twine upload dist/*

输入 PyPI 用户名和密码(或者用 API token),你的包就在互联网上了。现在全世界的人都可以 pip install your-package 来用你的代码。

这件事的意义远超技术本身。 你写了一个工具——比如一个帮你自动整理下载文件夹的小程序,或者一个把 Markdown 转成 PPT 的脚本——你把它放到 PyPI 上,别人就能搜到、用到、感谢你。你可能收到 issue 说「能不能加一个功能?」你可能收到 pull request 说「我帮你加了」。你从一个消费者,变成了一个发布者

这就是从第二课到第三课的跨越:第二课你学会了写代码给自己用。现在你学会了把代码发出去给别人用

劳动价值的另一种可能

你从小被教育:好好学习 → 找好工作 → 用劳动换工资。这是劳动价值的一条路。但它不是唯一的路。

开源社区提供了另一种可能:「我写了一个工具,有人用上了,有人感谢我,有人在此基础上做出了更好的东西。」

这不是传统的「劳动换取工资」。你没有跟任何一个用户签劳动合同。你没有老板给你发工资。但你创造了价值——你的代码帮别人省了时间、解决了问题、甚至启发了新的想法。

而你自己也得到了东西:

  • 你的代码被成千上万的人看到——你的作品有了观众。

  • 有人给你提建议、报 bug、贡献代码——你有了协作者。

  • 你的 GitHub profile 上有一排绿色的贡献图——你在找工作的时候,这就是你最好的简历。

  • 你被人需要,被人看到,被人欣赏——这恰恰是人类最深层的需求之一。

被需要、被看到、被欣赏——这不只是「感觉好」而已。 它是人类心理的刚需。你上了一天课回到家,有人跟你说「今天你做的那个东西帮了我大忙」——这种满足感比工资单更持久。工资花了就没了。被人感谢的记忆是你做下一件事情的动力。

MIT 的 Eric S. Raymond 写过一本著名的文章叫《大教堂与集市》,里面有一句话:「足够多的眼睛,能让一切 bug 都浮出水面。」(Given enough eyeballs, all bugs are shallow.)

这句话说的是代码审查。但它也说了另一件事:分享不是「损失」,是「放大」。 你把代码藏起来,它就是一段代码。你把它放出去,它可能变成一百个人的起点。


第二章:Git——协作的第一课

核心问题:两个人怎么同时改一份代码?

你现在会写 Python 脚本了。你自己写自己的,一个文件从头写到尾——没问题。

但如果两个人同时改同一个文件呢?

你和你弟弟都想给同一个 Python 脚本加新功能。你在他睡觉的时候改了第 50 行,他在你上课的时候改了第 80 行。第二天你们俩的修改怎么合并在一起?

一个最原始的办法:把文件发来发去。

main.py          # 原始版本
main_v2.py       # 你改的
main_v2_hkl.py   # 你弟在你改的基础上又改了
main_最终.py     # 到底哪个是最终版???
main_真的最终.py  # 不要这样

你见过这种情况。你写论文的时候,一个 论文.docx 能生出二十个版本。你发邮件的时候写着「论文(最终版)(3)」——这已经是失控了。

这个问题的本质是:如何管理共享状态的变更历史。

Google Docs 是怎么做的

在讲 Git 之前,先看看 Google Docs 是怎么解决这个问题的。

Google Docs 是实时的、中心化的。所有人同时编辑同一个在线文档,你打一个字,别人立刻能看到。Google 的服务器持有「真相」——所有人在同一个人家里开会。

优点:简单。没有版本冲突,因为只有一个版本。
缺点:你必须在线。你断网了就不能编辑。而且你只有一份最终版本——你想回到三天前的版本?Google Docs 有版本历史,但你不能很方便地「在这一版上拉出一个试验性的分支」。

代码协作不能靠 Google Docs

代码不是文章。文章你看一眼就知道有没有问题。代码你改了第 50 行,第 200 行可能就崩了——而且你可能一周以后才发现。所以你需要:

  • 离线工作:你在火车上写代码,没网也能 commit。到了有网的地方再同步。

  • 试验性分支:「老板让我试试新算法,但我不知道会不会搞坏现有的。让我拉一个新分支,搞坏了就扔掉,搞好了再合并回去。」

  • 完整的历史:不是「最新版本」,而是「从第一天到现在的每一步变更,每一步谁做了什么、为什么做」。

  • 回退能力:如果六天前的改动导致了一个 bug,你能精确地退回六天前那一个变更,而不影响其他好的变更。

这些需求指向一个答案:Git。

Git 的数据模型:DAG

Git 的本质是什么?不是「记录每个文件每次改了什么行」——那是 diff。

Git 的本质是:把整个项目的每一次提交组织成一个 DAG(有向无环图,Directed Acyclic Graph)。

每个 commit 是图里的一个节点。每个节点指向它的父节点(一个或多个——merge commit 有两个父节点)。节点之间有父子关系、无环(不可能出现 A→B→C→A),整个仓库的历史就是这个图。

A --- B --- C --- D (main)
       \
        E --- F (feature)

节点 A 是根——第一次提交。A→B→C→D 是 main 分支的历史。从 B 分叉出 E→F 是另一个分支的历史。D 的父节点是 C。F 的父节点是 E。E 的父节点是 B。

这不是比喻——.git 目录里真的存的就是这个图。 每个 commit 是一个文件(或 packfile 里的一条记录),里面写着:

  • 这个 commit 的 SHA-1 哈希(比如 a1b2c3d...)——这就是节点的名字

  • 父 commit 的哈希(一个或两个)

  • 作者、时间、commit message

  • 一个指向tree 对象的哈希——tree 对象描述了这个 commit 所有文件的内容

Git 在底层是一个内容寻址的文件系统。你给它内容,它返回一个哈希。你用哈希来取内容。commit 之间通过哈希引用形成图结构。这整个设计极其干净——你理解了这个 DAG,你就理解了 Git。

三个区域:工作区、暂存区、仓库

Git 把你的操作分成三个空间:

工作区 (Working Directory)      ← 你肉眼看到的文件,你在 IDE 里编辑的地方
    ↓ git add
暂存区 (Staging Area / Index)   ← 你挑选的「下次提交要包含的改动」
    ↓ git commit
仓库 (Repository / .git)        ← 永久存储的 DAG,一次提交一个节点

这和你平时用的「保存文件」完全不同。你改了文件——改动先只在工作区。你 git add——改动进入暂存区,但还没成为历史。你 git commit——暂存区的内容变成 DAG 里一个新的节点,被永久记录。

为什么需要暂存区? 因为真实场景里你经常改了好几个地方,但只想把其中一部分做成一次提交。你改了 a.py 的新功能,同时顺手改了一个 bug 在 b.py 里。你不希望这两个不相关的东西混在一次 commit 里——以后找历史的人会困惑。你 git add b.py,commit "fix: 修复 b.py 的一个 bug",然后 git add a.py,commit "feat: 新功能"。暂存区给了你精确控制每次提交内容的自由。

仓库是怎么存下来的

你在文件夹里 git init,Git 创建 .git/ 目录。这个目录就是你的本地仓库——整个 DAG 的所有节点、所有分支指针、所有标签,全在这个目录里。把它拷走,仓库就完整迁移。

你的 DAG 存在本地。你队友的 DAG 存在他的本地。GitHub 上的 DAG 是第三个副本。三个仓库对等——没有谁比谁「更真」。这是 Git 的分布式本质。

你可以给本地仓库指定一个或多个远程仓库(remote)

git remote add origin https://github.com/你的账号/项目名.git
# origin 是远程仓库的别名——约定俗成的名字,但你可以叫它任何东西
git remote -v   # 查看所有远程仓库

现在你的仓库有了两颗树:本地的,和远程(origin)的。你 push,是把本地的新节点上传到远程。你 fetch,是把远程的新节点下载到本地。你 pull,是 fetch 之后自动 merge。

你的本地 DAG                    origin 的 DAG
A---B---C---D                   A---B---C
            ↑                        ↑
          main                    origin/main

远程分支在本地有一个镜像引用:origin/main 是 origin 上 main 分支在你本地的上一次 fetch 的快照。它不是实时的——只有在你 fetch/pull 时才更新。所以 git status 能看到「你的 main 领先 origin/main 几个 commit」——对比的就是这两个引用。

Commit:DAG 上的一个节点

当你 git commit,Git 做这几件事:

  1. 把暂存区的内容生成一个 tree 对象(文件快照)

  2. 创建一个 commit 对象,写上 tree 的哈希、父 commit 的哈希、作者、时间、message

  3. 把当前分支指针(比如 main)移到这个新 commit

提交前:          提交后:
A---B---C         A---B---C---D
        ↑                    ↑
       main                 main

main 不是一个节点——它是一个指针,指向 DAG 上最新的那个节点。分支本质上就是一个可以移动的指针。

提交信息非常重要。三个月后你 git log,看到的是你当初写的一行行 message。如果上面全是「改了一下」「又改了」——你会想穿越回去打自己。好的 commit message 解释做了什么为什么这么做

# 不好的
修改了 calc 函数
修复 bug

# 好的
calc: 修复除零时崩溃的问题,现在返回错误信息
user_auth: 支持 email 或用户名登录,不再只认用户名

分支:DAG 上的指针

分支就是指向某个 commit 的可移动指针。创建分支就是创建一个新指针,指向当前 commit。

git branch feature   # 在当前 commit 上创建一个叫 feature 的指针(还不切换过去)
git switch feature   # 把 HEAD 指向 feature(HEAD 是你「当前在哪」的指针)
# 或者一步到位:
git switch -c feature  # 创建并切换

现在你提交新代码:

A---B---C (main)
         \
          D (feature, HEAD)

如果你切回 main 再提交:

A---B---C---E (main, HEAD)
         \
          D (feature)

DAG 分叉了。两个分支各走各的路。你在 feature 上怎么折腾都不会影响 main——因为 main 指针还停在 C,它的历史只有 A→B→C。

merge 和 rebase:两条并一条

当你 feature 做完了,想把改动合回 main。你有两种方式。

merge:保留真实历史

git switch main
git merge feature

如果 main 和 feature 没有分叉(feature 直接基于 main 的最新 commit),Git 做 fast-forward:直接把 main 指针移到 feature 的位置。线性的,干净的。

如果分叉了(像上面的例子),Git 创建一个新的 merge commit,它有两个父节点:

A---B---C---E---M (main)
         \     /
          D---F (feature)

M 是一个合并节点——它的父节点是 E 和 F。Git 把两边的改动合并在一起。如果有冲突(两边改了同一个文件的同一行),Git 停下来让你手动解决。

merge 的好处是历史真实——哪天分叉、哪天合并、谁做了什么,全部保留在 DAG 里。代价是 DAG 会变「毛线团」——分支多了,历史图很难读。

rebase:重写历史让 DAG 保持线性

git switch feature
git rebase main

rebase 做的事:把 feature 分支上独有的 commit(D 和 F),「摘下来」,放到 main 的最新 commit(E)后面重新「长」一遍。

rebase 前:                    rebase 后:
A---B---C---E (main)           A---B---C---E (main)
         \                                 \
          D---F (feature)                    D'---F' (feature)

D' 和 F' 是新的 commit——内容和 D、F 一样,但父节点变了,哈希也变了。原来的 D 和 F 还在 .git 里(暂时),但不再被任何分支引用。

然后切回 main,merge(这次是 fast-forward):

git switch main
git merge feature   # fast-forward,线性历史

最终 DAG 是一条直线:A→B→C→E→D'→F'。

rebase 的好处是历史干净——一条直线,容易读、容易 bisect(二分查找 bug 是哪个 commit 引入的)。代价是历史被重写了——如果有人已经基于你的 D 和 F 做了开发,rebase 会让他们疯掉(因为 D 和 F 被 D' 和 F' 替代了,哈希全变了)。

黄金法则:不要 rebase 已经被 push 到共享仓库的 commit。 rebase 只在你自己的、还没推出去的分支上做。一旦推出去,别人可能已经基于你的 commit 开发了——你 rebase 就是在拆他们的地基。

cherry-pick:摘一颗樱桃

有时候你不需要整个分支——你只想要某一个 commit

你队友在 feature 分支上做了三个 commit:D 修了一个 bug,E 加了新功能 A,F 加了新功能 B。你在 main 上只需要 D 那个 bug 修复,不想要 E 和 F。

git switch main
git cherry-pick D

cherry-pick 把 D 这个 commit 的改动单独应用到 main 上,生成一个新的 commit D'。D' 的内容和 D 一样,但父节点是 main 的当前 commit,哈希不同。

cherry-pick 前:               cherry-pick 后:
A---B---C (main)               A---B---C---D' (main)
         \                                 \
          D---E---F (feature)               D---E---F (feature)

cherry-pick 在「我只要这个,不要别的」场景下极其有用。它的本质就是:把 DAG 上一个节点的 diff(它和它父节点的差异)应用到另一个节点上。

commit --amend:修改最近的提交

你刚 commit 完,发现 message 写错了字,或者忘了 add 一个文件。你不想为了这点小修创建第二个 commit。

git add 忘掉的文件
git commit --amend

amend 做的事:用当前暂存区的内容,替换上一次 commit。 旧的 commit 被废弃(还在 .git 里但不再被引用),新的 commit 有新的哈希。

amend 前:    amend 后:
A---B---C     A---B---C'

跟 rebase 一样——如果 C 已经被 push 了,不要 amend。因为 amend 改了 C 的哈希,C' 和 C 是两个不同的节点——你 push 的时候会冲突。

rebase -i:交互式历史编辑

rebase -i 是 rebase 的交互模式,也是 Git 里最强大的历史编排工具。它让你随心所欲地编辑一串 commit

git rebase -i HEAD~4   # 编辑最近 4 个 commit

Git 打开编辑器,列出这 4 个 commit:

pick a1b2c3d feat: 加了功能 A
pick b2c3d4e fix: 修了一个小 bug
pick c3d4e5f WIP 写到一半先存一下
pick d4e5f6g feat: 加了功能 B

每一行前面的 pick 是一个命令——你可以把它改成别的:

命令

效果

pick

保留这个 commit,原样

reword

保留内容,但改 commit message

edit

保留,但停下来让你修改内容(改完 git commit --amend,然后 git rebase --continue

squash

把这个 commit 合并到上一个 commit,message 也合并

fixup

把这个 commit 合并到上一个,但丢弃这个 commit 的 message

drop

删掉这个 commit 和他的改动

换行顺序

调整 commit 在历史中的顺序

一个实际场景:你做了 4 个 commit,但推之前你想把它们整理干净:

pick a1b2c3d feat: 加了功能 A
fixup b2c3d4e fix: 修了一个小 bug          ← 合并到上面,不留 message
drop c3d4e5f WIP 写到一半先存一下          ← 删掉
pick d4e5f6g feat: 加了功能 B

保存退出后,Git 按你的指令重新构建历史。最终只有两个 commit:

A---B (feat: 加了功能 A,附带修 bug) ---C (feat: 加了功能 B)

这就叫「整理提交历史」(squash and cleanup)。推出去之前把你的草稿痕迹清理干净,让 PR reviewer 看到的是一个清晰的、逻辑连贯的 commit 序列。

tag:给节点贴标签

分支指针会随着新 commit 自动移动。但有时候你想永久性地标记某个节点——「v1.0 发布版就是它」「论文提交截止日的那个版本就是它」。

git tag v1.0                # 在当前 commit 上打轻量标签
git tag -a v1.0 -m "第一个正式版"  # 带注释的标签(推荐)
git tag                     # 列出所有标签
git push origin v1.0        # 把标签推到远程
git push origin --tags      # 推所有标签

tag 也是指针,但它不移动。你之后继续 commit,tag 永远指向你打标签时那个节点。GitHub 的 Release 功能就是基于 tag 的——你给某个 commit 打上 v1.0 标签,然后 GitHub Release 自动打包源码。

你的 Git 工具箱

走到这里,你掌握了完整的 Git 操作工具箱:

工作区 ←→ 暂存区 ←→ 本地 DAG ←→ 远程 DAG

节点级操作:commit, commit --amend, cherry-pick, tag
分支级操作:branch, switch, merge, rebase, rebase -i
远程操作:clone, fetch, pull, push, remote
查看操作:log, status, diff, show

所有这些操作的本质,就是在一个 DAG 上增删改查节点、移动指针。你不再需要恐惧 Git——它就是一棵你随时可以查看、随时可以操作的图。

Git 不是 GitHub

很多人第一次接触 Git 就是通过 GitHub,于是以为这两个是一回事。不是。

类比:

  • Git:你电脑上的文档编辑器的「修订模式」——它记录每一次改动,管理一个 DAG。

  • GitHub:Google Drive——它让你的文档在网上有一个副本,别人能看到、能评论、能协作。

没有 GitHub,Git 依然完整可用——你可以在自己电脑上 commit、branch、merge、rebase、cherry-pick、看完整的 DAG 历史。只是你的代码只在你的电脑上,别人看不到。

GitHub 做的事情是:

  • 提供了一个远程仓库,让你的 Git 仓库在互联网上有一个备份/分享点

  • 提供了一个社交层:别人可以看你的代码、提 issue、发 pull request

  • 提供了一些附加功能:GitHub Actions(自动测试/部署)、GitHub Pages(免费静态网站)、Discussions(论坛)

Git 是工具。GitHub 是平台。 类似的平台还有 GitLab、Gitee、Bitbucket——它们都是 Git 仓库的托管平台,底层用的都是同一个 Git。


第三章:GitHub——不只是存代码

核心问题:怎么和陌生人一起做项目?

Git 解决了「两个人的修改怎么合并」的问题。但还有一个更深的问题:怎么和你不认识的人一起做项目?

你发现了一个开源项目,想给它加个功能。你不认识这个项目的作者。你不能直接往他的仓库 push——他没有给你权限。你怎么贡献你的代码?

这就是 GitHub 的协作模型要解决的。

Fork:把别人的项目变成你自己的副本

第一步:Fork

你在 GitHub 上看到一个项目,你觉得有个地方可以改进。你点击右上角的 Fork 按钮。GitHub 做了这件事:把这个项目的仓库完整克隆一份,放进你自己的 GitHub 账号下。

现在,原作者/项目名 变成了 你的账号/项目名。你有这个仓库的完全控制权——你可以随便 push、建分支、删东西。因为这是你自己的副本。

Clone、Branch、Commit、Push

第二步:把你 fork 的仓库 clone 到本地。

git clone https://github.com/你的账号/项目名.git
cd 项目名

第三步:建一个分支,写你的改动。

git switch -c add-new-feature
# 改代码……
git add .
git commit -m "add: 新功能,做XXX"
git push origin add-new-feature

Pull Request:请求原作者合并你的改动

第四步:Pull Request(简称 PR)。

你回到 GitHub 网页上,点击 New Pull Request。你选的对比是:你的仓库的 add-new-feature 分支 → 原作者仓库的 main 分支

PR 不是一个技术概念——它是一个社交请求。它的意思是:

「嘿,我在你的项目上做了一些改进。你看一下,如果觉得 OK,合并进去。」

原作者会看到你的代码改动。他可以在 PR 的页面里逐行评论:「这一行为什么这么写?」「这里可能有个边界情况你没考虑到。」「这个变量名不够清晰。」

这就是 Code Review(代码审查)。它不只是找 bug——它是知识传递。原作者看你的代码,告诉你项目的约定(「我们这个项目日志都用 logging 模块,不用 print」),告诉你你没注意到的边角情况,向你解释为什么某个地方当初设计成那个样子。

你根据反馈修改,再 push,PR 自动更新。直到原作者觉得 OK,点击 Merge。你的代码就合并进了原项目。

你的名字出现在原项目的 Contributors 列表里。 你给一个你欣赏的开源项目贡献了代码。这件事在你简历上的分量,比「计算机二级证书」重得多。

Issue:不只是 bug 报告

当你在 GitHub 上发现一个项目有问题——可能是 bug,可能是一个你希望有的功能——你不需要先 fork 然后写代码再发 PR。你可以先开一个 Issue

Issue 是一张讨论帖。你描述你遇到的问题:「Python 3.12 下运行 import xxx 报错」。附带你的 Python 版本、操作系统、完整的报错信息。

原作者可能回复:「确实是 bug,我周末修。」也可能回复:「这不是 bug,你需要先配置环境变量。」也可能说:「好建议,但暂时没时间做——你愿意试着写 PR 吗?」

Issue 是你和项目作者之间的第一次接触。 良好的 issue 报告是一门艺术:

  • 说清楚你做了什么、期望看到什么、实际看到了什么

  • 附带环境信息(Python 版本、操作系统等)

  • 附带最小复现步骤——让作者能在自己电脑上重现你的问题

一个糟糕的 issue:「这软件不行啊,报错了。」作者没法帮你,因为他不知道你在什么情况下碰到了什么错误。

GitHub Actions:自动化一切

你还记得第二课的核心观点吗?——「让电脑为你工作」。GitHub Actions 就是这个想法在项目协作层面的延伸。

Actions 让你在仓库里定义一个自动化流程:当某个事件发生(有人 push 了代码、有人发了 PR、有人开了 issue),自动执行一段脚本。

最常见的用法:

  • 自动测试:有人发 PR → 自动跑测试 → 测试没过就标红,不让合并

  • 自动部署:你 push 到 main → 自动把网站部署到服务器上

  • 自动发布:你打一个 tag → 自动构建并发布到 PyPI

Actions 的配置文件叫 workflow,放在 .github/workflows/ 目录下。一个典型的 Python 自动测试 workflow 长这样:

name: Test

on: [push, pull_request]        # 什么时候触发

jobs:
  test:
    runs-on: ubuntu-latest       # 在什么环境上跑
    steps:
      - uses: actions/checkout@v4   # 第一步:把代码 checkout 到环境里
      - uses: actions/setup-python@v5  # 第二步:装 Python
        with:
          python-version: "3.12"
      - run: pip install -e ".[dev]"  # 第三步:装依赖
      - run: pytest                    # 第四步:跑测试

这个文件一旦存在,从此以后,每次有人 push 或发 PR,GitHub 都会自动跑一遍测试。测试没过,PR 就标红。项目维护者不需要在自己电脑上拉下来手动跑测试——GitHub 替你做了。

这就是自动化的精髓:把重复的、机械的、容易忘的事,写成脚本,让机器每次自动执行。

开源协作的完整流程

回顾一下,你现在知道了一整套流程:

  1. 你发现一个开源项目,想参与。

  2. 你 fork → clone 到本地。

  3. 你建一个分支,写代码,commit,push 到你的 fork。

  4. 你在 GitHub 上发一个 Pull Request。

  5. 原作者 review,你修改,直到通过。

  6. 你的代码被合并到主项目。

  7. GitHub Actions 自动跑测试、自动部署、自动发版。

你不再是一个孤立的程序员了。你是全球开源协作网络中的一个节点。

一个具体的建议

这节课结束后,去 GitHub 上找一个你在用的 Python 库。看它的 issue 列表。找一个标着 good first issue 或 help wanted 的,试着修。不一定修成功——但你会第一次体验到「参与开源」是什么感觉。


中篇:你脚下的操作系统

第四章:按下电源键之后

你按下电源键。

屏幕还是黑的。风扇开始转,主板上某个指示灯亮了。然后呢?在那一两秒钟里发生了什么?

我们跟着电流走一遍。

固件:电脑的胎教

电流接通。主板上的一小块芯片醒了过来——UEFI(统一可扩展固件接口),或者老一点的叫 BIOS。

UEFI 做的事跟新生儿出生时的条件反射差不多——最基本、最本能:

  1. 通电自检(POST,Power-On Self-Test):CPU 还在吗?内存能用吗?显卡没坏吧?键盘插了没?

  2. 找到启动设备:你设定过从哪个硬盘(或 U 盘)启动——UEFI 按你设定的顺序去找。

  3. 加载引导程序:在硬盘最开头的一个特殊区域里,UEFI 找到 bootloader(引导程序),把它加载到内存,然后把 CPU 的控制权交给它。

UEFI 退场。它的使命完成了——它只是一扇门。推开之后,真正的旅程才开始。

Bootloader:交接棒

最常见的 bootloader 叫 GRUB(GRand Unified Bootloader)。它的名字很唬人,但它做的事很简单:

  1. 显示启动菜单——如果你装了多个系统(比如 Windows + Linux),会让你选进哪个

  2. 把 Linux 内核 从硬盘加载到内存

  3. 把一些启动参数(比如「根文件系统在哪」「用不用安全模式」)传给内核

  4. 跳转到内核的入口——控制权从 bootloader 交给内核

这一步你可以亲手干预。开机时狂按 e 键(在 GRUB 菜单上),你会看到传给内核的启动参数。你甚至可以改它们——比如加一个 single 参数进入单用户模式(root 权限,不需要密码——这是紧急修复系统的方法)。整台电脑的启动流程对你没有秘密。

内核:我只做一件事

Linux 内核拿到控制权后,做了一系列硬件初始化:

  • 设置 CPU、初始化内存管理

  • 探测并初始化所有硬件设备(硬盘、网卡、USB 控制器、显卡……)

  • 挂载根文件系统(你安装系统时分给 / 的那个分区)

  • 启动第一个用户空间进程

这最后一步特别重要。内核启动完第一个用户空间进程之后,自己就退居后台了。它变成了一个服务提供者——应用程序通过「系统调用」来请求内核的服务(读文件、发网络包、分配内存),但决策权在用户空间。

内核管理硬件,用户空间管理一切其他事情。 分工极其清晰。

PID 1:一切进程的祖先

内核启动的第一个用户空间进程,名字叫 init,PID 永远是 1

你可以验证:

ps -p 1
# PID TTY  TIME CMD
#   1 ?   00:00:03 systemd

系统里跑的每一个进程——你的终端、你的浏览器、你的 Python 脚本——追溯它们的「父亲」,最终都会到 PID 1。PID 1 是所有用户空间进程的祖先。它永远不会退出——如果 PID 1 挂了,内核会直接 panic(系统崩溃),因为一个没有 init 的系统没有意义。

在今天的绝大多数 Linux 发行版里,这个 PID 1 就是 systemd


第五章:systemd——系统的大管家

systemd 不是什么神秘的东西

systemd 是一个程序。它在 /usr/lib/systemd/systemd(或者 /lib/systemd/systemd)。它被内核启动,PID 是 1,然后它开始干活。

它干的事,你用人话描述就是:

「好了,内核已经把硬件准备好了,根文件系统挂上了。现在我要把整个系统装配起来——把剩下的文件系统挂上、把网络配好、把该跑的服务跑起来、把登录界面显示出来。」

systemd 怎么知道该做什么

systemd 不会自己猜。它读配置文件。这些文件叫 unit,放在 /etc/systemd/system/ 和 /usr/lib/systemd/system/ 下。

Unit 有很多类型:

Unit 类型

后缀

干什么

Service

.service

一个后台服务(nginx、sshd、数据库)

Mount

.mount

挂载一个文件系统

Timer

.timer

定时触发一个 service

Socket

.socket

监听一个端口或 socket

Target

.target

一组 unit 的集合(相当于「运行级别」)

Device

.device

一个硬件设备

systemd 读取这些 unit,分析它们之间的依赖关系,然后并行启动所有不互相依赖的服务。

这就是为什么 systemd 比老的 SysV init 快那么多。老 init 是串行的——A 启动了才启 B,B 启动了才启 C。systemd 是并行的——网络服务、日志服务、蓝牙服务,只要它们不互相依赖,就同时启动。

systemd 还做了更聪明的事

socket 激活:systemd 可以替一个还没启动的服务监听端口。当第一个网络请求到达时,systemd 才启动那个服务。对你来说服务一直「可用」,但对系统来说它还没跑——省了开机时间和内存。就好像餐厅门口的迎宾员——客人来了才通知厨房开火。

自动重启:服务莫名其妙挂了?systemd 可以自动重启它。你只需在 service 文件里写:

Restart=on-failure
RestartSec=5s

意思是:挂了就重启,每次重试间隔 5 秒。

统一管理:不管什么服务——nginx、数据库、你自己写的 Python 脚本——全用同一套命令管理:

systemctl status nginx       # 状态
sudo systemctl start nginx   # 启动
sudo systemctl stop nginx    # 停止
sudo systemctl enable nginx  # 设为开机自启
sudo systemctl disable nginx # 取消开机自启

把你自己的 Python 脚本变成系统服务

你在第二课写过 server.py——一个监听 8888 端口的远程命令服务器。现在你把它注册为系统服务。

创建 /etc/systemd/system/my-server.service

[Unit]
Description=我的远程命令服务器
After=network.target

[Service]
Type=simple
User=hkl
WorkingDirectory=/home/hkl/my-server
ExecStart=/usr/bin/python3 server.py
Restart=on-failure

[Install]
WantedBy=multi-user.target

然后:

sudo systemctl daemon-reload
sudo systemctl enable --now my-server

从此以后:开机自动启动。挂了自动重启。日志记在 journal 里(journalctl -u my-server)。你用 systemctl stop my-server 来停,systemctl start my-server 来起。

你的 Python 脚本变成了一个一等公民——和 nginx、数据库一样受 systemd 管理,不再是你在终端里手动 python3 server.py & 的临时玩具了。

定时任务:at 和 timer

systemd 做完开机启动之后,还在持续运转。你告诉它「以后某个时候要做某事」——它记得住。

一次性at

at 3:00 PM
# 交互输入要执行的命令
python3 /home/hkl/backup.py
# Ctrl+D 结束

atq          # 查看待执行的任务
atrm 3       # 删除 3 号任务

周期性:systemd timer

创建一个 timer 文件 /etc/systemd/system/backup.timer

[Timer]
OnCalendar=daily
Persistent=true

[Install]
WantedBy=timers.target

配合你写好的 backup.service,启用它:

sudo systemctl enable --now backup.timer

从此每天自动备份。如果关机错过了,开机后补跑(Persistent=true)。你不需要记住这件事。

systemd、at、timer 这些工具的核心精神跟第二课的 Python 自动化一脉相承:让机器记住该做什么、什么时候做——你只需关心做了什么和出问题了怎么办。


第六章:终端使用——让你的操作健步如飞

systemd 启动完毕,登录界面出现,你输入密码,桌面或者黑底白字的终端提示符等着你。

如果你从没用过终端,那个闪烁的光标可能有点吓人——没有图标、没有按钮、没有右键菜单。你不知道能做什么。

这一章就是让你知道能做什么。而且等你学会了,你会发现它比图形界面快得多。

Shell 是什么

你打开终端,里面跑着的那个程序叫 Shell。最常见的叫 bash(Bourne-Again SHell),有些系统默认用 zsh,有些用 fish。它们做的事情一样:显示一个提示符,等你输入命令,执行,显示结果,再等你输入下一条。

把 Shell 想象成 Windows 上的文件资源管理器(Explorer)——只不过 Explorer 是图形界面,Shell 是文字界面。Explorer 里你双击文件夹进去,看到里面的文件;Shell 里你敲 ls 看到目录内容——同一件事,两种表达方式。

你想做的事

Explorer(GUI)

Shell(CLI)

看当前目录有什么

窗口里显示着

ls

进入某个文件夹

双击

cd 文件夹名

新建文件夹

右键 → 新建 → 文件夹

mkdir 文件夹名

复制文件

Ctrl+C, Ctrl+V

cp 源文件 目标

删除文件

Delete 键

rm 文件名

搜索文件

右上角搜索框

find . -name "*.py" 或 grep -r "关键词" .

CLI 的优势:类比快捷键

你刚开始用电脑的时候,是不是用鼠标点来点去?后来你学会了 Ctrl+C、Ctrl+V、Alt+Tab——手指不离开键盘就能做事,突然比纯鼠标快了三倍。

CLI 就是终极的快捷键。 GUI 把操作暴露为按钮和菜单——你看见什么点什么,入门友好。但当你需要重复做一件事一百次的时候,GUI 就很痛苦了——你要点一百次鼠标。CLI 里你写一行命令(或者写一个脚本),回车,一百次操作瞬间完成。

这不是说 CLI 比 GUI 更好。是不同场景适合不同工具

  • GUI 适合探索:你不知道有什么选项的时候,菜单和按钮让你发现功能。

  • CLI 适合重复精确:你知道你要什么的时候,键盘比鼠标快一个数量级。

  • CLI 适合自动化:你第二章写的脚本——CLI 命令可以直接写进脚本,GUI 操作很难自动化(除非用专门的自动化工具)。

  • CLI 适合远程:SSH 进一台服务器,你只有一个终端。没有 GUI 可选。

程序员偏爱终端不是因为它酷——是因为日常工作中「重复」和「精确」的场景占了绝大多数。

没有按钮怎么办?Completion 几乎就是填表单

终端最劝退的地方是什么?你敲了一个 git 然后愣住——后面该接什么?commitpushpull?参数怎么写?

Shell Completion(自动补全)就是答案。 敲一下 Tab 键:

git <Tab>
# 弹出所有 git 子命令:
# add       commit    push      status
# branch    log       rebase    switch
# checkout  merge     remote    tag

git co<Tab>
# 自动补全为 git commit(如果有歧义就再按一次 Tab 列出候选项)

git commit -<Tab>
# 弹出所有选项:
# --amend      --message    --all
# --author     --no-edit    --quiet

装了好的 completion 之后,使用终端的体验就变了——从「我需要记住所有命令」变成了「我大概知道打头几个字母,Tab 帮我想起来」。

这几乎就是填表单的体验。只不过表单是 GUI 的导航方式——下拉菜单、复选框、单选按钮——completion 是 CLI 的导航方式。你不需要记住所有选项,你只需要知道怎么让 Shell 帮你找。

Linux 上大多数发行版默认给 bash 装了基础的 completion。更完整的是 zsh 的 zsh-autosuggestions(灰字提示,按 → 采纳)和 fish 的内置补全。如果你用的是 Ubuntu,可以装 bash-completion

sudo apt install bash-completion

然后重新登录(或 source /etc/bash_completion),你会发现 apt install <Tab> 能列出所有可安装的包名。

历史:不要重新敲已经敲过的命令

终端会记住你敲过的每一条命令。这个记忆叫 history

history          # 列出所有历史命令,每条前面有序号
history 20       # 最近 20 条

Ctrl+R 是最常用的:反向搜索历史。按 Ctrl+R,开始打字,Shell 模糊匹配你敲过的命令。再按 Ctrl+R 跳到更早的匹配。找到后按 Enter 执行,或按 Esc 先编辑再执行。

比 Ctrl+R 更快的几个技巧:

!!                # 重复上一条命令(比如 sudo !!——上条命令用 sudo 再跑一遍)
!$                # 上一条命令的最后一个参数
!*                # 上一条命令的所有参数(不含命令本身)
!git              # 重复最近一条以 git 开头的命令
!123              # 重复 history 里序号 123 的命令
^old^new          # 把上一条命令里的 old 换成 new 然后执行

实际场景:

mkdir -p /home/hkl/projects/summerclass/class/实用计算机
cd !$             # cd 到上一条命令的最后一个参数(那个很长的路径)

python3 test_script.py
# 报错:Permission denied
sudo !!            # 用 sudo 重新跑 python3 test_script.py

git diff --cached
# 看完了,想 commit
git commit -m "fix: ..."  # 哎呀打错字了
^fix^add                   # 把 fix 换成 add,重新执行

你很少需要重新敲一遍完整命令。Ctrl+R 和 ! 让你几乎可以只用键盘的前几次操作就定位到任何一条历史命令。

&& 和 ||:根据成败决定下一步

你经常需要串行执行命令:先做 A,A 成功了再做 B,失败了做 C。这就是 && 和 ||

# && 表示「前一条成功(退出码为 0)才执行下一条」
git add . && git commit -m "update" && git push
# 如果 add 失败了,commit 不会执行。如果 commit 失败了,push 不会执行。

# || 表示「前一条失败(退出码非 0)才执行下一条」
python3 script.py || echo "脚本执行失败了!"
# script.py 正常退出 → 不 echo。script.py 报错退出 → echo 提醒你。

# 组合使用
git push || (echo "Push 失败,可能没网。" && exit 1)

和 &&/|| 配合的还有一个概念:退出码。每个程序退出时返回一个整数——0 表示成功,非 0 表示失败。$? 变量保存了上一条命令的退出码:

python3 script.py
echo $?    # 0 表示成功,1 表示失败,2 表示用法错误……

$():把命令的输出当参数

# 把 date 的输出嵌进 echo
echo "现在是 $(date)"

# 把 find 找到的文件列表传给 rm(危险,谨慎使用!)
rm $(find . -name "*.pyc")

# 把 which 找到的路径传给 ls
ls -la $(which python3)

(命令) 的意思是:先执行括号里的命令,把它的输出当作参数嵌进来。 老式写法是用反引号 ——不要学了,用 (命令) 的意思是:先执行括号里的命令,把它的输出当作参数嵌进来。 老式写法是用反引号 `——不要学了,用 ()

一个更实用的例子:

# 找到所有 .log 文件,打包
tar -czf logs.tar.gz $(find . -name "*.log")

# 杀掉所有 python3 进程(谨慎!)
kill -9 $(pgrep python3)

{}:批量生成和分组

Bash 的 {}(花括号)有两个主要用途。

用途一:批量生成字符串(Brace Expansion)。

echo file{1,2,3}.txt
# file1.txt file2.txt file3.txt

echo {a,b,c}{1,2}
# a1 a2 b1 b2 c1 c2

mkdir -p project/{src,tests,docs,assets}
# 一次性创建 project/src, project/tests, project/docs, project/assets

mv file.txt{,.bak}
# 展开为 mv file.txt file.txt.bak(快速备份)

用途二:在管道中组合多个命令(Command Grouping)。

# 把一组命令的输出整体重定向
{ echo "Header"; cat data.txt; echo "Footer"; } > output.txt

# 配合 &&
mkdir new_project && { cd new_project; git init; echo "README" > README.md; }

{} 批量生成在文件管理场景里极其好用——你不需要手动敲五遍 mkdir

一个小工具箱

除了前面讲的,几个日常高频操作:

cd -              # 回到上一个目录(在 A 和 B 两个目录间反复横跳的神器)
Alt+. 或 Esc+.    # 粘贴上一条命令的最后一个参数(不用 !$ 的话)
Ctrl+L            # 清屏(比敲 clear 快)
Ctrl+U            # 删除光标之前的所有内容
Ctrl+K            # 删除光标之后的所有内容
Ctrl+W            # 删除光标之前的一个单词
Ctrl+A            # 跳到行首
Ctrl+E            # 跳到行尾

这些快捷键叫 Readline 快捷键(因为 bash 底层用 readline 库处理输入)。Emacs 用户会觉得很熟悉——它们就是从 Emacs 来的。在任何默认配置的 bash 终端里都有效。

GUI 和 CLI 没有孰优孰劣

最后说一句收尾的话。

你可能会觉得「程序员都用终端,好酷」或者反过来觉得「都什么年代了还用黑窗口」。

两种看法都不对。GUI 和 CLI 是两种交互范式,各有各的适用场景:

  • 第一次配置系统、浏览不熟悉的目录结构 → GUI 更直观

  • 每天重复执行的构建、测试、部署 → CLI 更快

  • 修改一张图片的某个区域 → GUI 无可替代

  • 把一千张图片从 PNG 转成 WebP → CLI 一行搞定

  • 挑一部电影看 → GUI 浏览封面很舒服

  • 在服务器上查日志、重启服务 → CLI 是唯一选项(服务器可能根本没装桌面环境)

成熟的计算机使用者不会站队 GUI 还是 CLI。他两种都会,碰到合适的场景用合适的工具。

你现在学了 Shell 的基础、completion 的填表式体验、history 的免重敲大法、&&/|| 的条件串联、$() 的输出内嵌、{} 的批量生成。你不是在学「怎么用终端」——你是在学怎么让终端为你加速。

第七章:文件系统——当目录树成形

systemd 在启动过程中做了一件关键的事:挂载所有的文件系统。

内核启动时只挂载了根文件系统(/)。/home 可能在另一个分区,/boot 可能在另一个分区,U 盘在 /dev/sdb 上等着——systemd 根据 /etc/fstab(文件系统表,File System Table)里的配置,把它们一个个挂到目录树的正确位置。

现在,让我们看看这棵完整的树。

根 /:一切的起点

Linux 没有 C 盘 D 盘。整个系统只有一棵目录树,根是 /

/
├── bin/          # 最基本的命令(ls, cp, cat...)
├── boot/         # 启动文件(内核镜像、initrd)
├── dev/          # 设备文件(硬盘、U盘、终端、声卡...)
├── etc/          # 配置文件(系统级和软件级)
├── home/         # 用户的家目录
│   ├── hkl/      # 你的地盘
│   └── brother/  # 你弟的地盘
├── proc/         # 内核在内存中生成的虚拟文件(进程、系统信息)
├── sys/          # 内核在内存中生成的虚拟文件(硬件、驱动信息)
├── tmp/          # 临时文件(重启后清空)
├── usr/          # 用户程序和数据(大部分软件装在这)
│   ├── bin/      # 更多的命令
│   └── lib/      # 库文件
├── var/          # 可变数据(日志、缓存、数据库)
├── mnt/          # 手动挂载点
└── media/        # 自动挂载点(U盘、光盘)

这棵树上的每一片叶子——不管它来自哪个硬盘、哪个设备、甚至是不是真实存在的——都整齐地挂在同一个命名空间下。

一切皆文件

Linux 有一个招牌设计哲学:一切皆文件。

不是隐喻。是真的一切都是文件。

  • 你的硬盘 /dev/sda ——是一个文件。你 dd if=/dev/sda of=backup.img,可以把整块硬盘的内容复制出来。

  • 你的终端窗口 /dev/pts/0 ——是一个文件。你 echo "hello" > /dev/pts/0,文字就出现在那个终端窗口里。

  • 一个正在运行的进程的信息 /proc/1234/status ——是一个文件。

  • CPU 信息 /proc/cpuinfo ——是一个文件。

  • 甚至内存 /dev/mem ——也是一个文件(虽然读它需要 root 且通常不这么做)。

这个设计的威力在哪? 在统一性。你学会四个操作就够了:open()read()write()close()。不管是硬盘上的文本、U 盘上的分区、进程的信息、网络连接——操作都是同一套。

这个「一切皆文件」的哲学来自 UNIX——Linux 的精神祖先。第二课说过「冯诺依曼架构下所有计算归结于:从输入读数据、处理、写到输出」。UNIX 把「输入」和「输出」统一成了「文件」——核心抽象和计算机的物理结构是吻合的。

挂载:把设备嫁接到树上

你插了一个 U 盘。它不像 Windows 那样弹出一个「可移动磁盘 E:」。它在 /dev/ 下出现(比如 /dev/sdb——第二块 SCSI/SATA 磁盘),然后系统(或你手动)把它挂载到目录树上的某个位置。

sudo mount /dev/sdb1 /mnt/usb
ls /mnt/usb       # 看到 U 盘里的文件
# 读写 /mnt/usb/ 就是在读写 U 盘
sudo umount /mnt/usb  # 用完了,卸载

挂载的本质是:把一个文件系统(不管来自硬盘、U盘、还是内核的内存)嫁接到目录树的某个节点上。 挂载之前,/mnt/usb 只是一个空目录。挂载之后,它就是 U 盘的入口。所有程序对 /mnt/usb 的操作,内核自动翻译成对 U 盘的操作——程序完全不知道也不需知道。

这就是透明的分层——跟你第一课学的网络分层一样:上层不用关心里层的实现细节。

挂载不是必须的:设备只是设备

这里有一个容易被忽略的关键点:挂载是可选的,不是必须的。

你插了一个 U 盘,Linux 在 /dev/sdb 下给你一个设备文件。它就是这块 U 盘——一个块设备的原始接口。你不需要挂载它就可以操作它:

# 不挂载,直接格式化
sudo mkfs.ext4 /dev/sdb1

# 不挂载,直接整盘备份
dd if=/dev/sdb of=usb_backup.img bs=4M status=progress

# 不挂载,直接写镜像到 U 盘
dd if=ubuntu.iso of=/dev/sdb bs=4M status=progress

设备就是设备。它像一块远程的硬盘存储——它就躺在那里,你有一个接口(设备文件)可以编辑它。把它变成一个有层次的文件系统(挂载)是你常用的事情,但不是必须的事情。你可能只是想格式化了它就走人。

挂载做的事情是:把一个线性的、无结构的块设备,解读成有目录、有文件、有权限的层次结构。 这个结构不是 U 盘自己提供的——U 盘只是一堆字节。是文件系统驱动(ext4/NTFS/FAT32)赋予了这些字节以「文件夹」「文件」「权限」的含义。然后内核把这个解读成果,嫁接到你的目录树上。

这意味着什么?意味着你看到的层次结构不是本地的东西。 /mnt/usb/照片/暑假.jpg 这个路径,看起来和 /home/hkl/照片/暑假.jpg 没有区别——但实际上,前者的数据在 U 盘上,后者的数据在硬盘上。文件系统的抽象让它们看起来一样。

数据的另一端可以是任何东西

挂载的本质就是:给一个能读写字节的东西,套上一层文件系统的解读,然后挂到你的目录树上。

「能读写字节的东西」不一定是硬盘。它可以是:

  • 一个 U 盘(/dev/sdb

  • 一个硬盘分区(/dev/sda1

  • 一个网络存储(NFS、Samba)

  • 一个云盘(Google Drive、OneDrive——通过 rclone

  • 一个加密卷(LUKS)

  • 一个压缩包(archivemount 把 .tar.gz 挂载成文件系统)

  • 一个远程 SSH 服务器(sshfs——通过 SSH 把远程目录挂到本地)

  • 一个单片机通过串口提供的文件系统

rclone 是一个典范:它把几十种云存储(Google Drive、Dropbox、OneDrive、Amazon S3、SFTP、WebDAV……)统一成一个 rclone mount。你挂载之后,所有云端文件都在你的目录树里,你可以用 lscpcat 操作——就像它们在本地一样。rclone 在后台把 open() / read() / write() 翻译成对应云盘的 HTTP API 调用。

# 把 Google Drive 挂载到 ~/gdrive
rclone mount gdrive: ~/gdrive --daemon
ls ~/gdrive          # 看到你 Google Drive 里的所有文件
cp ~/gdrive/文档/*.pdf ~/本地备份/   # 像本地文件一样复制

FUSE(Filesystem in USErspace)是让这些成为可能的技术。传统上,文件系统驱动必须写在内核里——因为文件系统是内核负责的。FUSE 开了一道门:文件系统驱动可以在用户空间跑,通过 FUSE 接口跟内核对话。 这意味着任何程序员都可以用一种自己舒服的语言(Python、Go、Rust……)写一个文件系统,而不用碰内核代码。

rclone、sshfs、gocryptfs(加密文件系统)、s3fs(把 S3 存储桶挂载为文件系统)——它们全是 FUSE 文件系统。它们做的事情本质上一样:把某种「能读写」的东西,翻译成文件系统操作,让一切看起来像文件。

回到冯诺依曼:输入,输出,一切皆文件

你还记得第二课讲的冯诺依曼架构吗?

「所有计算归结于:从输入读数据、处理、写到输出。」

UNIX 把这个模型里的「输入」和「输出」统一成了文件。 你的键盘是文件,你的显示器是文件,你的硬盘是文件,你的网卡也是文件。所有外设——不管你插的是一个 U 盘、一个鼠标、一个温度传感器还是远程 FTP 服务器——在 /dev 下都是一个文件。

理论上,最纯粹的抽象不是文件,是流。 流就是「一序列字节从源头流向目的地」——没有寻址、没有随机访问、甚至没有「文件」这个概念。「文件」是在流的基础上加了三个能力:

  • 随机访问:你可以跳到文件的第 1000 个字节去读,不需要从头开始——这是 lseek()

  • 状态:文件有名字、有大小、有权限、有修改时间——流没有这些。

  • 持久:文件在你不读它的时候还存在——流一断就没了。

对于大多数场景,文件的抽象已经足够好。但有些时候不够——比如高性能场景下,传统的 open() → read() → write() → close() 路径有瓶颈(每个调用都是一次系统调用、一次上下文切换)。这时候就有了 io_uring:Linux 的新型异步 I/O 接口——它让你把一批 I/O 请求预先提交给内核,内核批量处理完再通知你。不需要为每次读写做系统调用。

你现在不需要学 io_uring。但知道它的存在很重要:它说明**「一切皆文件」不是终点。** 它是 UNIX 在 1970 年代选择的抽象,极其成功,撑了五十年。但当硬件性能增长到一定程度,抽象层的开销变得不可忽视的时候,人们就会在它的基础上再开一道侧门。技术没有终极方案——只有某个时代最适合的方案。

/proc 和 /sys:操作系统活在内存里

你 ls /proc——看到一堆数字目录和文件。但这些文件不存在于硬盘上

/proc 和 /sys 是内核在内存里生成的虚拟文件系统。你 cat /proc/cpuinfo——内核现场读取 CPU 的信息,格式化成文本,返回给你。你 cat /proc/meminfo——同理。你 cat /proc/1234/status——内核去查 PID 1234 这个进程的当前状态,返回给你。

更厉害的是 /sys——它不只是可读,还是可写的

# 调整屏幕亮度
echo 50 > /sys/class/backlight/intel_backlight/brightness

# 让键盘灯闪烁
echo 1 > /sys/class/leds/input3::capslock/brightness

你通过操作文件,来操作内核参数和硬件状态。

这件事意味着什么? 操作系统本身对你是透明的。Linux 把它的内部状态暴露为一个你可以用 lscatecho 操作的文件系统。你不需要特殊的 API、不需要专门的调试工具。会读文件写文件,就会探查和操控系统。

而这恰好让你认识到:操作系统是活在内存里的。 /proc/sys/dev 都不是硬盘上的静态数据——它们是内核在内存中维护的数据结构,开机时动态装配成文件系统的样子。你看到的文件系统不只是一棵硬盘上的静态目录树——它是操作系统在内存中实时构造出来的一个视图。

硬盘给了你持久化——数据不因关机而丢失。但操作系统「活」的地方是内存。


第八章:用户与权限——谁可以做什么

系统不只是你一个人在用

systemd 启动的最后一步,是启动登录管理器。你看到登录界面,输入用户名和密码——回车。你的桌面出现了。

但你有没有想过:Linux 凭什么知道哪些文件你能看、哪些不能?你在终端里敲 rm -rf /,它为什么(应该)拒绝?

因为 Linux 是一个多用户系统。一台电脑可以有很多用户:你、你弟、一个叫 www-data 的专门跑网站的用户、一个叫 mysql 的专门跑数据库的用户——它们同时存在,各有各的权限。

rwx:每个文件的三组权限

ls -l calc.py
# -rw-r--r-- 1 hkl hkl 1234 Jun 20 10:00 calc.py
#  ↑      ↑      ↑
#  权限   所有者  组

权限字段拆开看:

-         rw-       r--       r--
类型      所有者     组        其他人
(-文件    rw-可读写  r--只读   r--只读
 d目录    不可执行   不可执行   不可执行)

三组权限,每组三个位:

  • r(read,读):可以看内容

  • w(write,写):可以修改内容

  • x(execute,执行):如果是个程序,可以运行它

三组对应三类人:

  • 所有者(user):文件的拥有者——通常是创建文件的用户

  • 组(group):文件所属的用户组

  • 其他人(other):既不是所有者也不在组里的所有用户

在这个例子里:hkl 可以读写 calc.py;跟 hkl 同组的用户可以读;其他所有人也都可以读。没有人可以执行它(因为 .py 不是二进制程序——真正执行的是 python3)。

root:超级管理员

有一个特殊用户叫 root,UID(用户 ID)永远是 0。root 可以无视所有权限限制——读任何文件、写任何文件、杀死任何进程。Linux 的权限系统在 root 面前形同虚设——因为 root 就是设置权限的那个人。

你永远不应该以 root 登录。 日常使用中你是普通用户。需要做管理操作时,用 sudo 临时提权:

sudo apt update            # 用 root 权限更新软件包列表
sudo systemctl restart nginx  # 用 root 权限重启服务

sudo 的意义不只是「防止你手滑删系统」。它更重要的是记录:每一条 sudo 命令都会被记录。如果系统出了问题,你可以回看谁在什么时候用 root 权限做了什么。

为什么权限如此重要

回到物业管理的那个比喻:你家漏水不能淹楼下。

在多用户系统里,权限就是隔水层。你的程序崩了、你误删了文件——影响范围只到你的家目录为止。www-data 用户的程序崩了——最多影响网站,不会碰到你的文件。mysql 用户的数据库挂了——不会影响系统正常运转。

这个「隔离」思想会一路延伸到容器化——那是第九章的主题。


第九章:进程与信号——什么东西在跑,怎么控制它

你的终端里发生了什么

你打开终端,敲下 python3 my_script.py,回车。在这不到一秒的时间里:

  1. 你的 shell(bash)解析你的命令——它发现你要运行 /usr/bin/python3,参数是 my_script.py

  2. bash 调用 fork() ——Linux 内核复制了一份 bash 自己,创造一个一模一样的子进程

  3. 子进程调用 exec() ——把自己内存里的代码替换成 python3 的代码。从这一刻起,它不再是 bash 了——它是 python3

  4. python3 读 my_script.py,一行行执行

  5. 执行完毕,进程退出。bash 重新显示提示符,等你下一条命令

fork + exec 这个两步创建进程的模式是 UNIX 最经典的设计。为什么分两步?因为 fork 之后、exec 之前,shell 可以做手脚。比如重定向

python3 my_script.py > output.txt 2>&1

bash 在 fork 之后、exec 之前,把子进程的标准输出(stdout)指向 output.txt,把标准错误(stderr)指向和 stdout 一样的地方。然后 python3 执行——它完全不知道自己的输出被重定向了。它正常 print("hello"),内容自动写进了 output.txt

这就是分层的力量:程序不需要知道它在跟谁说话。 它只管往 stdout 写。至于 stdout 连接的是终端窗口、文件、还是网络 socket——那是 shell 和内核决定的。

信号:进程间的轻声耳语

进程跑起来了。它在后台占着 CPU、吃着内存、可能还打开了文件。但你怎么控制它?你不能把手伸进 CPU 里把它拎出来——你需要一种进程间通信的方式。最简单、最古老的方式,就是信号

信号是一个进程发给另一个进程的一个整数。接收方看到这个整数,根据预设的规则做出反应。就像你在人群中拍了朋友一下肩膀——他回头,看你想说什么。不同的拍法(轻拍 vs 重拍)和不同的场景(餐厅 vs 考场)含义不同。

发信号的基本命令是 kill——名字有误导性,它真正的含义是「send a signal」(发送信号),不一定是杀掉。只是大多数信号的默认行为是终止进程。

kill 1234           # 发送 SIGTERM(默认)——「请你自己关掉」
kill -9 1234        # 发送 SIGKILL——「立刻死,不由分说」
kill -STOP 1234     # 发送 SIGSTOP——「暂停,不许动了」
kill -CONT 1234     # 发送 SIGCONT——「好了你继续」
kill -HUP 1234      # 发送 SIGHUP——「你爸终端关了」or「重载你的配置」
kill -USR1 1234     # 发送 SIGUSR1——「用户自定义信号 1,约定的暗号」

进程可以怎么回应一个信号

信号到了目标进程手里,进程可以有以下几种反应:

回应方式

含义

Term(终止)

进程退出。这是大多数信号的默认行为

Ign(忽略)

进程当没收到。信号被丢弃

Core(终止并 core dump)

进程退出,并把内存镜像存为 core 文件供调试

Stop(暂停)

进程被挂起,直到收到 SIGCONT

Cont(继续)

被 Stop 的进程恢复运行

自定义处理函数

进程可以安装「信号处理器」——收到某个信号时执行一段代码

不是所有信号都能被忽略或自定义。SIGKILL(9)和 SIGSTOP(19)永远不能被捕获、不能被忽略——内核强制执行。这是最后的手段:如果你程序死循环了、把所有信号都忽略了,SIGKILL 能确保你最终还是能干掉它。

完整信号速查

信号

编号

默认行为

含义

触发方式

SIGHUP

1

Term

挂断(终端关闭)。守护进程常拿它当「重载配置」的暗号

关终端 / kill -HUP

SIGINT

2

Term

中断。你按 Ctrl+C 就是这个

键盘 / kill -INT

SIGQUIT

3

Core

退出并 core dump。比 SIGINT 更狠——「不光退,还要留证据」

Ctrl+\ / kill -QUIT

SIGKILL

9

Term

强制杀死。不可捕获、不可忽略

kill -9

SIGTERM

15

Term

终止。默认的 kill。「请你自己关掉」——优雅退出的标准方式

kill / kill -TERM

SIGSTOP

19

Stop

暂停。不可捕获、不可忽略

kill -STOP

SIGCONT

18

Cont

继续。恢复被 Stop 的进程

kill -CONT

SIGUSR1

10

Term

用户自定义 1。约定暗号——比如「轮转日志文件」

kill -USR1

SIGUSR2

12

Term

用户自定义 2。另一个暗号

kill -USR2

SIGPIPE

13

Term

管道破裂——你往一个没人读的管道写数据

自动触发

SIGALRM

14

Term

闹钟——alarm() 系统调用设定的时间到了

自动触发

SIGCHLD

17

Ign

子进程状态改变(退出、暂停)。父进程靠它知道孩子死了

自动触发

你在 Python 里怎么处理信号

你在跑一个长时间的程序,按了 Ctrl+C——Python 进程收到 SIGINT,抛出 KeyboardInterrupt

import signal
import time

# 方式一:try/except 捕获 KeyboardInterrupt(SIGINT 的 Python 翻译)
try:
    while True:
        time.sleep(1)
except KeyboardInterrupt:
    print("收到 SIGINT,正在保存数据……")
    save_data()
    print("已保存,再见!")

# 方式二:注册自定义信号处理器
def handle_usr1(signum, frame):
    print(f"收到信号 {signum},执行自定义逻辑……")
    reload_config()

signal.signal(signal.SIGUSR1, handle_usr1)

# 现在在另一个终端里执行:
# kill -USR1 <这个进程的PID>
# 这个进程就会调用 handle_usr1

SIGTERM vs SIGKILL:先礼后兵

这是运维里最重要的信号惯例。

SIGTERM 是「礼貌的请求」。你 docker stop 一个容器、你 systemctl stop 一个服务——系统先发 SIGTERM。进程收到后,可以做优雅退出:关闭数据库连接、写入缓冲区、保存未完成的进度、通知其他服务「我要下线了」。默认的等待时间是 90 秒(systemd)或 10 秒(docker)。

SIGKILL 是「最后的裁决」。如果进程过了等待时间还没退出(有可能代码有 bug 卡住了,有可能忽略了 SIGTERM)——系统发 SIGKILL。内核直接回收进程的所有资源,不给任何反应时间。不会有任何清理代码执行。缓冲区的数据可能丢失。

所以你在写服务程序时,务必正确处理 SIGTERM。不要让你的程序被 SIGKILL 干掉——那意味着你的退出逻辑有 bug。

import signal
import sys

def graceful_shutdown(signum, frame):
    print("收到终止信号,开始优雅退出……")
    close_db_connections()
    save_checkpoint()
    print("退出完成。")
    sys.exit(0)

signal.signal(signal.SIGTERM, graceful_shutdown)

信号的哲学

信号的设计极其 UNIX:简单、不啰嗦、可组合。 发送方只需要发出一个整数编号——接收方想怎么处理是它自己的事。发送方不需要知道接收方是谁、是什么语言写的、在做什么、状态如何。

信号不做任何复杂的协商。它不说「请向我汇报你的当前状态,我来决定你是否应该终止」——它只说「SIGTERM」。这是一种对等的、去中心化的、单向的通信方式。跟 TCP 的握手、HTTP 的请求-响应形成鲜明对比——那是在应用层解决复杂通信问题。信号在操作系统层提供最简通信。

而你需要记住的操作其实就几个:

Ctrl+C              → 中断当前前台进程(SIGINT)
Ctrl+Z              → 暂停当前前台进程(SIGSTOP)
kill <pid>          → 优雅终止(SIGTERM)
kill -9 <pid>       → 强制杀死(SIGKILL)
kill -STOP <pid>    → 暂停进程
kill -CONT <pid>    → 恢复进程

后台任务和 job control

你跑 python3 long_running.py——终端被占了,光标不闪了,不能敲别的命令。怎么办?

python3 long_running.py &   # 末尾加 & = 放到后台
# [1] 12345                 # shell 告诉你 job 号和 PID

# 你可以继续敲命令。程序跑完后 shell 通知你:
# [1]+  Done    python3 long_running.py

如果程序已经在跑了,按 Ctrl+Z(发送 SIGSTOP)暂停它,再决定让它后台继续还是回到前台:

python3 long_running.py
# 按 Ctrl+Z
# [1]+  Stopped    python3 long_running.py

bg %1        # 后台继续
fg %1        # 拉回前台
jobs         # 查看所有后台 job

需要程序在你登出后继续跑?nohup(让进程忽略 SIGHUP——这样你关终端它也不会被终止),或者用 tmux/screen 创建可断线重连的终端会话。


第十章:容器化——隔离的极致

权限还不够

第八章说了权限可以隔离不同用户。但权限系统的隔离是粗粒度的——只能控制「谁能不能读哪个文件」。

真正的隔离需要更多维度:

  • 你能用的 CPU 不能超过 50%——你的程序跑飞了不能拖慢全系统

  • 你能用的内存不能超过 2GB——你的程序内存泄漏不能把别人的进程挤死

  • 你看到的文件系统是你专属的——你的 /usr/bin/python 是 3.12,别人看到的是 3.11,互不冲突

  • 你看到的 8080 端口是你自己的——和别人容器里的 8080 不冲突

这就是容器化要解决的问题。

从 chroot 到 namespace

最早的文件系统隔离工具叫 chroot——把某个进程的根目录 / 改成你指定的目录。这个进程从此看不到那个目录以外的文件。

chroot /tmp/jail /bin/bash
# 这个 bash 把 /tmp/jail 当成 /
# 就算它跑 rm -rf /,毁掉的也只是一个空 jail

但 chroot 只隔离了文件系统。内存还是共享的,CPU 还是共享的,网络还是共享的。

Linux 内核后来引入了 namespace 机制——把各种全局资源「命名空间化」,让不同 namespace 里的进程看到独立的资源视图。

namespace 类型

隔离什么

效果

Mount

文件系统挂载点

每个容器有自己的根文件系统

PID

进程 ID

容器里 PID 1 就是容器里那个进程,宿主机上它是 PID 12345

Network

网络设备、IP、端口

容器有自己的网卡、独立的端口空间

User

用户和组 ID

容器里的 root 不是宿主机 root——安全隔离的关键

UTS

主机名和域名

容器可以叫 my-container,宿主机叫 my-server

还有一个配套机制叫 cgroup(Control Group)——不隔离「看见什么」,而是限制「用多少」。你可以限制某个进程组最多用 50% CPU、2GB 内存、100MB/s 磁盘读写。

Docker:让这一切变得好用

Docker 没有发明容器。 namespace 和 cgroup 是 Linux 内核的功能,在 Docker 出现之前就存在了。Docker 做的是:

  • 定义了标准镜像格式——你的应用 + 所有依赖 + 启动命令,打包成一个镜像文件

  • 提供了简单的命令行——docker rundocker builddocker compose

  • 提供了镜像仓库(Docker Hub)——你可以像 pip install 一样 docker pull 别人的镜像

  • 提供了网络和存储的集成——容器之间怎么通信?数据怎么持久化?

就像第二课里 Python 的虚拟环境让每个项目有独立的包,Docker 让每个应用有独立的整个操作系统环境。但比虚拟环境强得多:虚拟环境只隔离 Python 包,Docker 隔离文件系统、进程树、网络栈、用户——全部维度。

为什么容器化是前面所有知识的总和

容器化不是凭空出现的新技术。它是 Linux 多年积累的每一样能力在「隔离」这个主题下的收束:

  • 文件系统(第七章):每个容器有自己的根文件系统——mount namespace 让这成为可能

  • 权限(第八章):容器里的 root 不是宿主机 root——user namespace 是关键

  • 进程与信号(第九章):docker stop 的本质是向容器主进程发 SIGTERM。docker kill 发 SIGKILL。你在容器外看到的是普通进程,只不过它活在自己独立的 PID namespace 里

  • 透明挂载(第七章):每个容器里 /proc/sys 是各自独立的——内核为每个 mount namespace 生成独立的视图

一个好用的工具背后,有一整套底层基础设施在支撑。 你用 docker run 的时候,可能觉得你在用「一个新工具」——实际上你动用的是 Linux 内核几十年工程积累的隔离能力。文件系统、权限、进程、信号、挂载——全都在容器化的场景里重新登场。

而你现在理解了其中的每一块。

下篇:AI Agent

第十一章:从填词说起

核心问题:大语言模型到底是什么?

2026 年,你到处能听到「AI」「大模型」「Agent」。但让我们回到最基础的问题:当你跟 ChatGPT 说话的时候,底层在发生什么?

最诚实的回答:在预测下一个 token。

token 是大模型理解文本的基本单位。英文里一个 token 约等于 3/4 个单词(或一个标点符号),中文里一个 token 约等于 1-2 个汉字。模型做的事极其简单:给你一段文本,它预测这段文本最合理的下一个 token 是什么。然后把它加进去,再预测下一个。如此循环。

输入:  "法国的首都是"
预测下一个: "巴黎"
输入:  "法国的首都是巴黎"
预测下一个: "。"
输入:  "法国的首都是巴黎。"
预测下一个: (特殊 token:<结束>)——停下来

这就是一个自回归语言模型的完整工作流程。没有理解、没有思考、没有意识——只有「给定前文,下一个 token 的统计分布是什么?」

这怎么就能聊天了?

如果你只是训练模型「预测互联网上所有文本的下一个 token」,你会得到一个很会接话的模型——但它不知道自己在跟你聊天。它以为自己在接续一段网上的文章。

为了让模型知道「你是在问我问题」,研究员做了一件事:对话模板

在把你说的话发给模型之前,系统会在外面包一层格式:

<|system|> 你是一个有帮助的助手。回答简洁准确。<|end|>
<|user|> 法国的首都是什么?<|end|>
<|assistant|>

模型看到这个模板,它理解了模式:前面是 system prompt(定义角色),接着是 user 提问,现在轮到 assistant 回答。它的「预测下一个 token」机制产生出「巴黎。」——和你在网上看到的 FAQ 页面的下一个词恰好是统计上最合理的。

然后对话继续:

<|assistant|> 巴黎。<|end|>
<|user|> 那里有什么好吃的?<|end|>
<|assistant|>

模型继续预测。它知道你问的「那里」指巴黎——因为上文里有。这不是因为它「记得」——是因为整个对话历史每次都被完整地发送给模型

这怎么就能写代码了?

同样的原理。训练数据里有海量的代码——GitHub 上的开源项目、StackOverflow 的问答、技术博客的教程。模型在预测下一个 token 时,学到的不只是自然语言的模式,还有代码的模式。

当你问「用 Python 写一个斐波那契函数」,模型在训练数据里见过无数类似的问题和答案。它预测出的下一个 token 恰好组成了正确的代码。它不是「会编程」——它是「见过太多次这个问题的答案,所以能复现」。

边界在哪?

理解了模型的工作方式,你就能理解它的边界:

它能做的

  • 生成文本:写文章、翻译、总结、改写——因为它就是训练来预测文本的。

  • 生成代码:写函数、解释代码、转换语言——因为训练数据包含了海量代码。

  • 回答问题:回答事实性问题、解释概念——因为训练数据包含了百科全书、论文、教程。

它不能做的(或者说,做不好的):

  • 真正理解:它没有对世界的内部模型。它不知道「水是湿的」意味着什么——它只知道这两个词经常一起出现。

  • 推理:它不自洽地进行逻辑推理。它更像一个直觉很快但容易犯低级错误的学生——解答复杂的数学题可能看起来很对,但仔细检查中间步骤,你会发现跳跃和错误。

  • 知道不知道:模型不会说「我不知道」。它被训练成预测下一个 token——所以当它碰到不知道的事,它会编造一个统计上合理的回答。这就是「幻觉」(hallucination)——模型说的内容看起来合理但事实错误。

  • 行动:它不能点鼠标、不能发邮件、不能执行代码。它只能生成文本。你让它「帮我发邮件」——它只能写一封邮件的文本,发不出去。

  • 记住:你跟它聊了一小时,你以为它记得开头说的所有内容。实际上,每次你发新消息,系统会把整个对话历史重新发给模型。模型没有持久的记忆——它只是每次都在处理一个越来越长的文本。对话历史有长度上限(上下文窗口)——超过上限,最早的内容会被丢弃。

关键认知:模型看到的世界只是文本。它对现实世界一无所知。你告诉它「我有一杯水放在桌上」,它不知道水是什么、桌子是什么——它只知道这些词在训练数据中怎么出现。它回答的所有内容,都基于文字与文字的统计关系,而不是基于对现实世界的体验。

但你可能会想——既然边界这么明显,为什么这么多人在用?因为文本能力本身已经覆盖了大量有价值的任务。写邮件、做翻译、总结论文、写初稿代码——这些本来就是纯文本的领域。模型的边界,恰好框住了一大块实用的领地。


第十二章:工具调用——当 AI 学会使用工具

核心问题:模型只能生成文本,怎么让它做事情?

第十章说了:模型只能生成文本。它不能发邮件、不能查天气、不能执行代码。那你能怎么办?

答案是:给模型提供工具,让它生成调用工具的文本,然后由程序来执行。

这个模式叫 Tool Calling(工具调用)或 Function Calling(函数调用)。它是把大语言模型从「只会聊天的程序」变成「能行动的 Agent」的关键一步。

一个具体例子

假设你想让 AI 帮你查天气。模型本身不知道今天的天气——它的训练数据截止到去年。而且就算截止到今天,它也不能实时获取天气数据。

但你可以给它一个工具:

# 工具定义:告诉模型「有这个函数可以用」
tools = [
    {
        "name": "get_weather",
        "description": "获取指定城市的当前天气",
        "parameters": {
            "type": "object",
            "properties": {
                "city": {
                    "type": "string",
                    "description": "城市名称,用中文"
                }
            },
            "required": ["city"]
        }
    }
]

然后你跟 AI 说:「北京今天天气怎么样?」

模型看到你的问题,也看到可用的工具列表。它「决定」:这个问题需要调用 get_weather 工具,参数是 {"city": "北京"}。但它不自己执行——它只是生成了一段结构化的文本

{
    "tool_call": {
        "name": "get_weather",
        "arguments": {"city": "北京"}
    }
}

你的程序收到这个 JSON,自己去调用真正的天气 API(比如和风天气、OpenWeather),拿到实际数据,然后把结果喂回给模型:

工具返回:北京当前晴,气温 25°C,湿度 40%。

模型看到这个结果,生成最终回复:「北京现在是晴天,气温 25°C,湿度 40%,天气不错!」

这和第二课的编程有什么关系?

你注意到了吗?工具调用的格式——函数名 + 参数 + 返回值——和你第二课学的 Python 函数调用是同一件事:

# Python 版本
def get_weather(city: str) -> str:
    # 调用天气 API
    return "北京当前晴,气温 25°C"

result = get_weather("北京")

你写代码的时候,是你决定调用哪个函数。AI Agent 的场景下,是模型决定调用哪个函数。但底层的机制是一样的:函数签名(名字 + 参数类型)→ 调用 → 返回值

这就是 Agent 和你学的编程语言之间的桥梁。你不是在学两件不同的事——你是在同一个概念框架里:定义能力为函数,让模型决定何时调用、用什么参数调用。

工具调用的循环

实际的 Agent 运行过程是一个循环:

用户输入
    ↓
模型看到 [system prompt] + [对话历史] + [可用工具列表]
    ↓
模型输出 text 或 tool_call
    ↓           ↓
text → 显示给用户    tool_call → 程序执行工具 → 结果追加到对话历史 → 回到模型

模型可以在一次对话中多次调用工具。比如你说「帮我查一下北京和上海的天气,然后比较哪边更适合周末出游」,模型可能:

  1. 调用 get_weather("北京") → 拿到数据

  2. 调用 get_weather("上海") → 拿到数据

  3. 比较两边数据,生成推荐

它自己规划了调用顺序。你没有告诉它「先查北京再查上海」——它从你的话里推断出来了。

工具的类型

Agent 可以使用的工具不限于查天气。实际上,任何可以通过编程接口执行的操作都可以变成工具:

工具类型

例子

网络请求

查天气、查股价、搜索网页、调用 REST API

文件操作

读文件、写文件、列目录、搜索文件内容

执行代码

运行 Python 脚本、执行 shell 命令

数据库

查询 SQL、写入数据

系统操作

查看进程、发送通知、修改配置

浏览器操作

打开网页、点击按钮、截图、填写表单

Git 操作

查看 git log、创建 branch、提交代码

当 Agent 有了执行 shell 命令的能力,它就跟你之前在终端里敲命令一样了——只是现在,它可以自己决定什么时候敲什么命令。

这很危险,也很强大。 你给 Agent 什么工具,它就有什么能力。如果工具列表里有 execute_shell_command,而且没有限制——Agent 理论上可以 rm -rf /。所以工具调用的设计原则是:只给完成当前任务所需的最少工具;对有破坏性的工具要加确认步骤。

从聊天到 Agent 的转变

当一个语言模型被接入工具调用循环,它在工程上就不再叫「聊天机器人」了——它叫 Agent

Agent = 大语言模型 + 工具 + 执行循环。

  • 大语言模型:大脑——理解任务、规划步骤、生成回复。

  • 工具:手和脚——执行实际操作、和外部世界交互。

  • 执行循环:神经系统——接收模型的决定、执行工具、把结果喂回模型,直到任务完成。

你会发现这个结构和你之前学的控制论循环一模一样:感知(读对话历史和工具返回)→ 决策(模型生成下一步)→ 执行(程序运行工具)→ 再感知。第十章提到的控制论,在 Agent 架构里完整落地了。


第十三章:上下文管理、Skill 与边界

System Prompt:Agent 的性格和规则

你在第十章看到了对话模板里的 <|system|> 部分。这个部分叫 System Prompt——它定义了 Agent 的角色、能力范围、行为规则。

System Prompt 极其重要。同样一个模型,配不同的 system prompt,表现可以天差地别:

# Prompt A:有帮助的助手
"你是一个有帮助的助手。以清晰简洁的方式回答问题。"

# Prompt B:编程专家
"你是一个资深的 Python 开发者。回答问题时给出可运行的代码示例,并解释为什么这样写。"

# Prompt C:严格的老师
"你是一个严格的数学老师。不直接给答案,而是引导学生自己思考。如果学生的解答有错,指出错误但不给正确答案。"

三个 Agent 拿到同一个问题「怎么用 Python 排序一个列表?」——回答方式会完全不同。A 会直接给出 list.sort() 的用法。B 会给出代码示例 + 讲 Timsort 算法。C 会说「你先想想,排序需要比较元素对吧?你打算怎么比较?」

System Prompt 就是你在第二课学的「函数签名」在 Agent 层的对应——它告诉调用者(以及模型自己)「我是谁、我能做什么、我会怎么做」。

上下文窗口:Agent 的记忆

第十章提到,每次模型生成回复时,看到的不是你的最新一句话——而是整个对话历史 + system prompt + 可用工具列表 + 工具调用结果

这些全部塞在一起,叫上下文(Context)。上下文能装多少东西,取决于模型的上下文窗口(Context Window)——用 token 数来度量。

  • 早期的 ChatGPT(GPT-3.5)上下文窗口是 4096 token——约 3000 个英文单词

  • 现在的主流模型(2026 年)可以到 128K、200K 甚至 1M token——能装下一整本《三体》

  • 但上下文越大,推理越慢、成本越高,而且模型对上下文中间部分的信息注意力会下降(「迷失在中间」问题)

上下文窗口就是 Agent 的「工作记忆」。窗口里的内容它都能「看到」。窗口之外的内容——就跟没发生过一样。

Skill:让 Agent 拥有长期能力

你想让 Agent 会做某件特定的事——比如「用我们公司的代码风格写 Python」。你可以把代码风格指南写在 system prompt 里——但如果每个对话都要写一遍,很蠢。而且 system prompt 会越来越长,占用宝贵的上下文窗口。

Skill(技能) 解决了这个问题:把一段指令(可能是很长的文档)存为独立的技能文件。当用户的问题匹配某个技能的触发条件时,系统才把那段指令注入上下文。

比喻:

  • System Prompt:你钱包里随身带着的身份证——你是谁。

  • Skill:你书架上的工具书——平时不在身上,但需要用的时候抽出来翻开。

当你问「帮我生成一张图片」——Agent 检测到关键词,加载「ai-image-generation」这个 skill 的指令。指令里告诉它该调用哪个模型、用什么参数、图片放在哪。任务完成后,skill 指令可以从上下文里移除,腾出空间。

Skill 让 Agent 可以在不占满上下文窗口的前提下拥有大量专业能力——就像一个医生可以随时翻看专业手册,而不需要把整本手册背下来。

边界在哪(续)

有了工具调用和 skill,Agent 能做更多事了——但边界并没有消失。第十章说的那些限制依然成立:

  • 幻觉:有了工具,Agent 可以查实时信息了——这是一大进步。但如果工具返回了不准确的数据,Agent 无法判断——它只能信任工具的返回。

  • 推理:Agent 可以调用 Python 解释器来做精确计算了——这解决了「不会算数」的问题。但复杂的多步推理依然容易出错——模型可能在中途「走神」。

  • 自主性:Agent 能在工具调用的循环中做多步操作了。但它没有真正的「意图」——它只是根据 system prompt 和上下文来预测下一个合理的 token。如果 system prompt 写「你是桌宠,卖萌就好」,它就不会主动去帮你查资料。

  • 安全:Agent 能执行 shell 命令了——这给了它强大的行动力,也给了它搞破坏的能力。你必须为破坏性操作加护栏——确认提示、黑名单命令、运行在受限环境(容器)里。

核心心态(再说一遍,因为它太重要了):Agent 是工具,不是替代品。它擅长搜索、总结、生成初稿、自动化重复任务。它不擅长创造性决策、价值判断、真正理解你的需求。你仍然是决策者,Agent 是你的执行力放大器。


第十四章:Linux 生态与 Agent 的天然契合

为什么 Agent 在 Linux 上最强

回顾这篇和第二课你学过的:

  • Linux:一切皆文件,一切操作可视作对文件的读写

  • 命令行:每一个工具只做一件事,但可以组合

  • 脚本:Bash / Python 可以自动化一切

  • 包管理:apt / pip 一键装任何软件

  • systemd:把任何程序封装为服务

  • 容器:隔离、可复现

这些东西加在一起,构成了一个自然适合 Agent 生存的生态

为什么?

因为 Agent 的操作方式就是「读文本输入 → 决定调用什么工具 → 把结果作为新文本输入继续」。 而 Linux 的命令行本身就是这个模型:你读命令输出(ls 的结果、git status 的结果),你决定下一个命令,你键入命令,看到新的输出——循环。

Agent 和 Linux 命令行的思维模型完全同构。当 Agent 有了执行 shell 命令的能力,它就像一个有超强阅读和分析能力的、永不疲倦的系统管理员。

# Agent 可以这样工作:
# 用户:「帮我把这个项目的依赖全部更新到最新版」
# Agent 的思考过程:
#   1. 看看项目是什么语言 → ls 看看有没有 pyproject.toml
#   2. 读一下 pyproject.toml → cat pyproject.toml  
#   3. 查出每个依赖的最新版本 → pip index versions <package>
#   4. 更新文件 → 编辑 pyproject.toml
#   5. 测试一下 → python -m pytest
#   6. 如果测试通过 → git add + git commit + git push

每一步都是 Agent 自己决定的,每一步都是你第二课学的工具(lscatpipgit)——只是现在它们在 Agent 的指挥下自动运行了。

Windows 和 macOS 呢?

Windows 的 GUI 传统意味着很多系统操作没有命令行入口——或者有,但写法完全不同。macOS 底子是 UNIX(Darwin),命令行体验接近 Linux——但很多系统层面的东西被苹果锁住了。

Linux 的开放性——源码可看、系统可改、一切都有命令行接口——让它成为 Agent 最理想的栖息地。你可以在 Linux 上给 Agent 几乎无限的工具,而不用担心「这个操作有没有命令行方法」。

实际案例:你现在用的这个工具(opencode)就是一个 Agent。它在 Linux 终端里运行,通过 shell 命令执行你的指令、读写文件、搜索代码、运行测试。它的每一条能力——读文件、写文件、搜索代码、执行命令、管理 Git——对应着底层的 shell 工具和系统调用。它没有「黑魔法」——它就是第二课的 Python 自动化 + 第三课的 Linux 命令行 + 一个大语言模型的大脑。


扩展篇:更广阔的数字世界

以下内容不要求你现在就掌握——它们是一扇扇窗户,让你知道数字世界还长什么样。看了,知道了,以后某天你碰到相关的东西,不会觉得是从天而降的陌生词汇。


第十五章:Web3、DAO 与智能合约

核心问题:如果互联网没有中心服务器,会怎样?

你现在用的几乎所有网络服务——微信、淘宝、Bilibili、百度——都有一个共同特点:中心化。它们的数据存在公司的服务器上,它们的服务由公司控制。公司可以决定什么时候改规则、什么时候删你的数据、什么时候关停服务。

Web3 的愿景是:用密码学和去中心化网络来替代中心化服务器。 让用户自己掌控数据,让规则由代码执行而非公司决定。

区块链:一个谁都可以验证的账本

区块链是 Web3 底层最核心的技术。它的本质很简单:一个分布式的、不可篡改的账本。

传统的账本:银行有一个数据库,记录「张三给李四转了 100 元」。只有银行能改这个数据库。你信银行,是因为银行有法律约束。

区块链的账本:成千上万台电脑(叫「节点」)各自持有一份同样的账本。每当你新增一条记录(叫「交易」),所有节点要达成共识——多数同意之后,这条记录才被写入。写入后,任何人都不能改。因为改一条记录需要同时改掉成千上万个节点上的数据——这在算力上不现实。

去中心化的要点:没有一个人、一家公司、一个政府控制这个账本。它是被数学和密码学——而非法律和信任——保护的。

智能合约:自动执行的协议

如果区块链只是一个不可篡改的账本,那它跟电子表格没有本质区别——只能记录「谁付了多少钱」。

智能合约让它有了程序逻辑。你在区块链上部署一段代码,这段代码一旦部署就不能修改,它的执行结果由全网共识保证。任何人都可以调用它、查看它的代码。

想象一个自动售货机:

  • 你投 5 块钱 → 机器自动出一瓶水

  • 不需要店员、不需要信任——规则是机器硬件保证的

智能合约就是数字世界的自动售货机。代码规则公开、执行自动、结果不可逆。

经典例子:

  • 众筹:智能合约收款。如果 30 天内达到目标金额,钱转给发起人。如果没达到,钱自动退回——没有中间人可以卷款跑路。

  • 域名系统:ENS(Ethereum Name Service)是一个运行在区块链上的域名系统,不受任何政府或组织控制。你注册 hkl.eth,只要私钥在你手里,没人能没收。

  • 去中心化交易所:Uniswap 是一个自动做市商——你可以用它交易代币,没有「交易所」这个中间人。

DAO:没有老板的组织

DAO(Decentralized Autonomous Organization,去中心化自治组织)是智能合约的延伸应用:一个组织的规则写在智能合约里,由代币持有者投票决定一切,没有 CEO、没有董事会。

比如,一群人觉得「我们应该一起资助开源项目」。他们在区块链上创建一个 DAO,设定规则:

  • 谁持有 DAO 代币,谁就有投票权

  • 发起提案需要持有一定数量代币

  • 投票通过 → 智能合约自动执行(比如自动转账给开源项目的维护者)

  • 所有资金流水公开可查,每一笔钱花了多少、花在哪

现实中的 DAO 例子:ConstitutionDAO——2021 年,一群人在网上集资试图购买美国宪法原件。几天内筹了 4700 万美元。虽然最后没买到,但这件事展示了「完全由陌生人组成的组织可以多快、多大范围地协调行动」。

Web3 的争议和局限

Web3 目前远非完美:

  • 效率:区块链的共识机制(每笔交易要全网确认)比中心化数据库慢 1000 倍以上,成本高得多。

  • 用户体验:你丢了私钥?钱就永远没了。没有客服、没有申诉渠道。这在哲学上是「你的钱你负责」,但在实际使用中是灾难。

  • 投机:Web3 目前最大的应用场景不是去中心化应用,而是炒币和 NFT。技术愿景和实际用途之间有巨大鸿沟。

  • 监管:政府如何监管一个不由任何人控制的组织?这是一个开放的法律问题。

不管你对 Web3 的立场是什么——狂热信徒还是嗤之以鼻——理解这套思想是有价值的。它挑战了你对「信任」「组织」「货币」的默认假设。它让你意识到:你一直以来用的「免费服务」(微信、抖音)——其实不免费。你付的是数据和注意力。Web3 说:也许还有另外一种方式。


第十六章:点对点加密

核心问题:怎么让服务器也看不到你的聊天内容?

你用微信发消息。你的消息经过微信的服务器。微信的服务器能看到你的内容——事实上,在中国,法律要求它必须能看到。你跟朋友说「今晚吃火锅」,腾讯的服务器记录了这句话。

端到端加密(End-to-End Encryption,E2EE) 的目标是:消息在发送端加密,只有接收端能解密。中间的任何服务器——包括提供聊天服务的公司——都看不到明文。

Signal 协议的基本思想

Signal 是目前公认最安全的即时通讯协议,被 WhatsApp、Signal App、Google Messages 等采用。它的核心设计目标就是 E2EE。

Signal 协议背后的数学不算简单,但直觉你可以理解:

第一步:Diffie-Hellman 密钥交换。

这跟你第一课学的 TLS 中的密钥交换是同一种数学。Alice 和 Bob 各自生成一对公私钥。他们通过网络交换公钥(Eve 可以看到公钥),然后用自己的私钥 + 对方的公钥各自算出一个相同的共享密钥。Eve 看到了两个公钥,但她算不出共享密钥——因为从公钥逆推私钥在数学上不可行(离散对数问题)。

第二步:Double Ratchet(双棘轮)。

有了共享密钥之后,Signal 不是一直用同一个密钥。它用了一种叫「棘轮」的机制——每一条消息都换一个新密钥,而且新密钥是从旧密钥单向推导出来的

  • 旧密钥可以算出新密钥,但新密钥不能反推旧密钥。这是「前向安全性」(Forward Secrecy)——就算某个密钥泄露了,历史消息也无法被解密。

  • 每一条消息都掺入新的随机数,所以密钥不断更新。这是「后向安全性」——就算某个密钥泄露了,未来的消息(在新的随机数掺入之后)也是安全的。

结合这两点:窃听者就算拿到了某一时刻的密钥,也只能解密那一条消息,看不了解密的也看不到未来的。

密钥的验证:安全码

你怎么知道和你聊天的真的是 Bob,而不是中间人 Eve 在冒充 Bob?

Signal 的做法:你和 Bob 各自在 App 里看到一个「安全码」——是一个 60 位数字的哈希。如果你们在现实里见面,互相扫一下对方 App 上的安全码,系统就会验证你们用的是不是同一对密钥。如果对不上——说明有人在中间冒充。

这跟你第一课学的 TLS 证书链是同一个问题:「我怎么知道我在跟谁说话?」TLS 靠 CA(证书机构)来背书。Signal 靠你亲自验证。

去中心化 vs 联邦式

Signal 是中心化的——Signal 基金会运营服务器。虽然服务器看不到你的消息内容,但它知道谁在什么时候跟谁通信(元数据)。

Matrix 是去中心化的替代方案——我们下一章讲。


第十七章:IRC、Matrix 与 Webhook

IRC:古老但还活着的聊天协议

IRC(Internet Relay Chat)诞生于 1988 年——那时候你还没出生,甚至你爸妈可能还在上学。但它今天仍然被大量技术社区使用:Linux 发行版的开发者、开源项目的维护者、黑客社区的成员。

IRC 极简。没有头像、没有表情包、没有已读回执、没有消息同步。你连上一个 IRC 服务器(比如 Libera.Chat),加入一个频道(#python),你的消息被服务器转发给频道里的所有人。你断开——消息就收不到了。

这个极简背后是 IRC 持久的生命力:它没有中心化的控制。 任何人都可以搭一个 IRC 服务器。频道不由公司拥有——由频道的操作员(拥有 +o 标志的用户)管理。

技术社区的很多重大决策和日常讨论,不是在 Slack 或 Discord 上发生的——是在 IRC 上。Linux 内核的讨论、Python 开发的协调、Debian 的维护——这些项目至今仍在用 IRC。

Matrix:去中心化的现代 IM

IRC 没有消息历史(除非你用 bouncer 中转)、没有端到端加密、不能传文件。

Matrix 是一个现代化的替代方案,保持了 IRC 的去中心化精神,但加入了现代功能:

  • 联邦式:跟 Email 一样——你在 matrix.org 上注册,你朋友在 private-server.com 上注册,你们可以互发消息。没有中心服务器。

  • 端到端加密:默认开启。

  • 消息同步:你的所有设备都能看到完整历史。

  • 桥接:Matrix 可以桥接 IRC、Telegram、Discord、Slack——让你在一个客户端里收发跨平台的消息。

Matrix 在开源社区里越来越流行。很多项目从 IRC 迁移到了 Matrix,同时保留了通向 IRC 的桥。最终目标是:即时通讯应该像 Email 一样——你可以选择你的服务商,不同服务商之间可以互通。

Webhook:让 IM 融入自动化

Webhook 是一个简单的 HTTP 回调:当某件事发生的时候,向一个预设的 URL 发送 HTTP 请求(通常是 POST)。接收方收到请求后,可以做出反应。

在即时通讯中,Webhook 最常见的用途是机器人通知

  • 你的 GitHub 仓库有人发了 PR → GitHub Webhook 发送通知到你的 Matrix/IRC 频道

  • 你的服务器 CPU 超过 90% → 监控系统通过 Webhook 发告警到你的群聊

  • 每天早上 8 点 → 你的 cron job 调用天气 API,通过 Webhook 把当天天气发到你的频道

  • 你的 CI 跑完了 → 构建结果通过 Webhook 通知你

Webhook 让你可以把任何事情变成通知。你不需要频繁打开各种后台检查——你设定好规则,让信息来找你。这正是第二课自动化思想的延伸——不只是让电脑替你做事,还让电脑在做成之后主动告诉你。

这些技术和你有什么关系

IRC 让你知道技术社区的日常沟通方式。Matrix 让你看到去中心化通讯的可能性。Webhook 让你理解怎么让系统主动触达你。

你未来加入技术社区的时候,不管加入的是哪个项目、用的是什么工具,底层的模式都是这三个中的一个:实时聊天(IRC/Matrix) + 代码协作(GitHub) + 自动化通知(Webhook)。你不用现在就成为专家——你只需要知道有这些东西,以后碰到的时候,方向是对的。


第十八章:加入技术社区

核心问题:技术不是一个人在学

你现在在学编程、学 Linux、学网络。你在自己电脑上折腾,跟课本对话。

但技术的真实形态不是这样的。技术是一群人对着一堆问题各自提出解法,然后互相看、互相改、互相踩在肩膀上往上走。你的学习路径可以孤独,但如果你一直孤独下去,天花板会很低。

从 lurker 开始

Lurker(潜水者)是技术社区里一个尊贵的身份——意思是你只看不说。这不是贬义词。

你加入一个项目的 Matrix/IRC 频道或者 GitHub Discussions。你不发言,你看别人在聊什么——他们在讨论什么 bug、什么新特性、什么架构选择。你看多了,就对项目的节奏和风格有感觉了。你甚至不需要学完所有内容才能潜水——你现在就能去 #python 的 IRC 频道或 Matrix 房间,看别人在讨论什么。哪怕你看不懂一半——你能看懂另一半,就是学习。

提 Issue:第一次贡献

当你在做一个项目的时候碰到了问题——不是「这个怎么用」的问题(那应该去查文档或问搜索引擎),而是「这个场景下好像有 bug」「这个功能按理应该支持但没有」——你就有了提 issue 的资格。

一个好的 issue 报告(前面第三章讲过)本身就是对社区的贡献。它帮维护者发现了一个他们不知道的问题。你不需要会修——你能精准地描述问题,就已经帮了大忙。

修 bug、加功能:第一次 PR

在你提 issue 之后,如果问题看起来不复杂,你可以试着修。就算你不会修,你可以看别人怎么修的。你去翻项目的 git log,看类似的 bug 以前是怎么被修掉的,模仿那个模式。

你的第一个 PR 可能只是改了文档里的一个拼写错误。没关系。你的目的不是一次贡献多——你的目的是完成第一次完整的协作流程:fork → branch → commit → PR → review → merge。走通一次,以后就知道了。

聪明的提问

在技术社区混,你免不了要提问。提问的质量直接决定了你会不会被帮助。

一个经典的指南是 Eric S. Raymond 的《How To Ask Questions The Smart Way》。这篇文章的核心思想是:

  • 先自己查:搜索引擎、项目文档、已有的 issue——大多数问题已经被回答过了。如果你连搜都没搜就提问,别人会认为你不尊重他们的时间。

  • 说清楚上下文:你用的是什么系统、什么版本、什么环境、你做了什么、期望看到什么、实际看到了什么——附上完整的报错信息。

  • 不要问「有人能帮我吗」:直接说你碰到的问题。问「有没有人用过 XXX」是没有意义的——用过的人自然会出现。

  • 不要催:开源项目的维护者大多是志愿者。他们没有义务帮你。尊重他们的时间。

  • 你解决了就回来告诉大家怎么解决的:这样下一个搜到这个问题的人不会重走你的路。

你不用背下来——记住一句话就够了:向别人提问之前,先穷尽自己能做的所有尝试。 如果你做完了所有尝试还是解决不了——你的问题值得被回答,因为你已经走完了别人能指的路,剩下的路需要别人带。

技术社区不只是一个地方

技术社区——不管是 GitHub 上的项目、IRC 上的频道、Matrix 上的房间、线下的黑客松——它们不止是你「获取帮助」的地方。它们是你成为同行的通道

你在社区里回答别人的问题,你就在巩固自己的理解。你 review 别人的代码,你就在锻炼自己的判断力。你跟别人争论技术方案,你就在学习如何表达和权衡。这些能力——代码之外的,沟通、协作、判断的能力——恰好是在学校几乎学不到、在社区里天然习得的。


尾篇:三课统一与展望

你学了三课。

  • 第一课:你理解了网络。从物理层的电信号到 TLS 的证书链,你知道了一个网页从服务器到你屏幕中间走过的每一步。你知道了数字世界的地图。

  • 第二课:你学会了自动化与编程。从 Turtle 小海龟到面向对象到 Socket 服务器,你学会了如何把「我想让电脑做什么」翻译成它听得懂的句子。你知道了如何让电脑为你工作。

  • 第三课(这一课):你学会了协作构建。从开源许可证到 Git 分支到 GitHub PR,你知道了怎么和他人一起建造。从 Linux 的文件系统到进程信号到容器化,你知道了你脚下的操作系统在做什么。从语言模型的 token 预测到 Agent 的工具调用,你知道了 AI 是什么、能做什么、不能做什么。

三课走到这里,有一句话可以说了:

你不再是数字世界的消费者,你是创造者。

你不是只能打开别人写好的软件。你能写自己的。

你不是只能接受别人设计的规则。你能理解规则、修改规则、创造规则。

你不是只能被算法推荐牵着走。你能分辨什么是工具、什么是陷阱。

你不是只能独自折腾。你能加入社区、能在别人的基础上建造、能被看见。

你学这三课不是为了考试。是为了你上大学之后——当别人还在为「怎么装 Python」发愁的时候,你已经在写自动化脚本、在 GitHub 上发 PR、在用 Linux 的容器部署自己的服务、在用 Agent 帮你搜索和学习。

每一条你在课上记住的事,都是你未来的积累。

与 DLC 的钩子

这些能力会跟你的大学生活直接发生关系:

  • Git/GitHub → 你加入任何技术社团、参与任何项目,你跟别人协作的起点。

  • Linux → 你配服务器、跑实验、部署项目——Linux 是服务器世界的通用语言。

  • Python 自动化 → 你的作业、你的研究、你的日常——任何重复劳动都能脚本化。

  • Agent → 你的学习助理、你的代码审查者、你的文档生成器。

  • 开源社区 → 你的简历,你的社交圈,你的成长飞轮。

→ DLC:大学生存指南——带着这些能力去大学,你就不是「来混文凭的」。


想要传递的感觉

  • 分享不是「损失」,是「放大」——你在开源社区的贡献会被无数人看到、使用、改进。你上传到 PyPI 的一个小工具,可能帮到一个你不认识的、在地球另一端的人。

  • 劳动的意义不只在工资——被人需要、被人欣赏、改造世界也改造自己。你写的代码被人 star 了、你提的 PR 被 merge 了——这些是工资单给不了的东西。

  • 理解底层,才能不被上层绑架——你理解网络分层,就不会被「连不上网」吓到。你理解操作系统,就不会觉得系统是黑盒子。你理解 Agent 的原理,就不会把它当成魔法或者威胁。

  • 你不需要成为专家——你需要的是地图。知道什么地方有什么东西,碰到了知道去哪里找。这三课就是你的地图。

  • Agent 是放大器,不是替代品——你就是那个决策者。它帮你更快、更广、更深——但往哪个方向去,是你决定。

  • 「没有银弹」——没有一招解决所有问题的工具。但你学会的这些东西——网络、编程、Linux、Git、Agent——你可以组合它们。工具在手,遇到问题你不会说「我做不到」,你会说「让我想想怎么做」。