第三课:自由软件、协作构建与 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,意思是:「我写的东西谁都可以拿去用,拿去卖钱也行,闭源也行,我不在乎。」你选 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 做这几件事:
把暂存区的内容生成一个 tree 对象(文件快照)
创建一个 commit 对象,写上 tree 的哈希、父 commit 的哈希、作者、时间、message
把当前分支指针(比如
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 是一个命令——你可以把它改成别的:
一个实际场景:你做了 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 替你做了。
这就是自动化的精髓:把重复的、机械的、容易忘的事,写成脚本,让机器每次自动执行。
开源协作的完整流程
回顾一下,你现在知道了一整套流程:
你发现一个开源项目,想参与。
你 fork → clone 到本地。
你建一个分支,写代码,commit,push 到你的 fork。
你在 GitHub 上发一个 Pull Request。
原作者 review,你修改,直到通过。
你的代码被合并到主项目。
GitHub Actions 自动跑测试、自动部署、自动发版。
你不再是一个孤立的程序员了。你是全球开源协作网络中的一个节点。
一个具体的建议
这节课结束后,去 GitHub 上找一个你在用的 Python 库。看它的 issue 列表。找一个标着
good first issue或help wanted的,试着修。不一定修成功——但你会第一次体验到「参与开源」是什么感觉。
中篇:你脚下的操作系统
第四章:按下电源键之后
你按下电源键。
屏幕还是黑的。风扇开始转,主板上某个指示灯亮了。然后呢?在那一两秒钟里发生了什么?
我们跟着电流走一遍。
固件:电脑的胎教
电流接通。主板上的一小块芯片醒了过来——UEFI(统一可扩展固件接口),或者老一点的叫 BIOS。
UEFI 做的事跟新生儿出生时的条件反射差不多——最基本、最本能:
通电自检(POST,Power-On Self-Test):CPU 还在吗?内存能用吗?显卡没坏吧?键盘插了没?
找到启动设备:你设定过从哪个硬盘(或 U 盘)启动——UEFI 按你设定的顺序去找。
加载引导程序:在硬盘最开头的一个特殊区域里,UEFI 找到 bootloader(引导程序),把它加载到内存,然后把 CPU 的控制权交给它。
UEFI 退场。它的使命完成了——它只是一扇门。推开之后,真正的旅程才开始。
Bootloader:交接棒
最常见的 bootloader 叫 GRUB(GRand Unified Bootloader)。它的名字很唬人,但它做的事很简单:
显示启动菜单——如果你装了多个系统(比如 Windows + Linux),会让你选进哪个
把 Linux 内核 从硬盘加载到内存
把一些启动参数(比如「根文件系统在哪」「用不用安全模式」)传给内核
跳转到内核的入口——控制权从 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 有很多类型:
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 看到目录内容——同一件事,两种表达方式。
CLI 的优势:类比快捷键
你刚开始用电脑的时候,是不是用鼠标点来点去?后来你学会了 Ctrl+C、Ctrl+V、Alt+Tab——手指不离开键盘就能做事,突然比纯鼠标快了三倍。
CLI 就是终极的快捷键。 GUI 把操作暴露为按钮和菜单——你看见什么点什么,入门友好。但当你需要重复做一件事一百次的时候,GUI 就很痛苦了——你要点一百次鼠标。CLI 里你写一行命令(或者写一个脚本),回车,一百次操作瞬间完成。
这不是说 CLI 比 GUI 更好。是不同场景适合不同工具:
GUI 适合探索:你不知道有什么选项的时候,菜单和按钮让你发现功能。
CLI 适合重复和精确:你知道你要什么的时候,键盘比鼠标快一个数量级。
CLI 适合自动化:你第二章写的脚本——CLI 命令可以直接写进脚本,GUI 操作很难自动化(除非用专门的自动化工具)。
CLI 适合远程:SSH 进一台服务器,你只有一个终端。没有 GUI 可选。
程序员偏爱终端不是因为它酷——是因为日常工作中「重复」和「精确」的场景占了绝大多数。
没有按钮怎么办?Completion 几乎就是填表单
终端最劝退的地方是什么?你敲了一个 git 然后愣住——后面该接什么?commit?push?pull?参数怎么写?
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。你挂载之后,所有云端文件都在你的目录树里,你可以用 ls、cp、cat 操作——就像它们在本地一样。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 把它的内部状态暴露为一个你可以用 ls、cat、echo 操作的文件系统。你不需要特殊的 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,回车。在这不到一秒的时间里:
你的 shell(bash)解析你的命令——它发现你要运行
/usr/bin/python3,参数是my_script.pybash 调用
fork()——Linux 内核复制了一份 bash 自己,创造一个一模一样的子进程子进程调用
exec()——把自己内存里的代码替换成python3的代码。从这一刻起,它不再是 bash 了——它是 python3python3 读
my_script.py,一行行执行执行完毕,进程退出。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,约定的暗号」
进程可以怎么回应一个信号
信号到了目标进程手里,进程可以有以下几种反应:
不是所有信号都能被忽略或自定义。SIGKILL(9)和 SIGSTOP(19)永远不能被捕获、不能被忽略——内核强制执行。这是最后的手段:如果你程序死循环了、把所有信号都忽略了,SIGKILL 能确保你最终还是能干掉它。
完整信号速查
你在 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 里的进程看到独立的资源视图。
还有一个配套机制叫 cgroup(Control Group)——不隔离「看见什么」,而是限制「用多少」。你可以限制某个进程组最多用 50% CPU、2GB 内存、100MB/s 磁盘读写。
Docker:让这一切变得好用
Docker 没有发明容器。 namespace 和 cgroup 是 Linux 内核的功能,在 Docker 出现之前就存在了。Docker 做的是:
定义了标准镜像格式——你的应用 + 所有依赖 + 启动命令,打包成一个镜像文件
提供了简单的命令行——
docker run、docker build、docker 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 → 程序执行工具 → 结果追加到对话历史 → 回到模型
模型可以在一次对话中多次调用工具。比如你说「帮我查一下北京和上海的天气,然后比较哪边更适合周末出游」,模型可能:
调用
get_weather("北京")→ 拿到数据调用
get_weather("上海")→ 拿到数据比较两边数据,生成推荐
它自己规划了调用顺序。你没有告诉它「先查北京再查上海」——它从你的话里推断出来了。
工具的类型
Agent 可以使用的工具不限于查天气。实际上,任何可以通过编程接口执行的操作都可以变成工具:
当 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 自己决定的,每一步都是你第二课学的工具(ls、cat、pip、git)——只是现在它们在 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——你可以组合它们。工具在手,遇到问题你不会说「我做不到」,你会说「让我想想怎么做」。