看得见的大模型

跑模型之前
先把机器打通

从 Linux 目录到 Python 容器,再到 MySQL 和 Docker。模型下面那一层:环境、代码、数据、接口。

每一节带一个可动手的演示。另有水墨本

一套环境的旅程 点开大站,再进子章节
00

从这里开始

模型页讲「一个 token 怎么走完网络」,数学页讲「那些符号是什么意思」。这一页讲第三件事:你坐在电脑前,怎么把环境、代码、数据和接口串成一条能跑的线。

真正要带走的是服务器上天天用的那一套:目录怎么长、命令怎么拼、Python 怎么装数据、MySQL 怎么查、FastAPI 怎么对外、Docker 怎么把「我电脑能跑」变成「哪都能跑」。

和模型页怎么接 训练脚本是 Python,张量是 NumPy / PyTorch,语料和业务数据进 MySQL 或文件,推理服务用 FastAPI,部署用 Docker。Linux 和 Shell 是这条链的地板。地板不熟,上面的注意力公式写得再漂亮也跑不起来。

先认路。Linux 里没有盘符,只有一棵从 / 长出来的树。

01

Linux:一切皆文件

Windows 用 C:、D: 把世界切开。Linux 用一棵树:根叫 /,往下全是路径。硬盘、进程、网卡,最后都能在某个文件名下面找到。

真实服务器你摸不到机箱,日常都是 SSH 连上去。连上之后面对的就是这棵树:没有 C: D:,只有从 / 长出来的路径。

点开一棵目录树

每个目录只记一件事:它住的是「程序」「配置」还是「会变的数据」。

打个比方 /usr 是已经装好的家具,/opt 是你后来搬进来的大家电,/etc 是墙上的开关面板,/var 是会不断写的流水账,/home 才是你的书桌。模型权重、conda 环境,优先放 /opt 或自己家目录,别塞进 /usr

软件从哪来

Ubuntu 用 APT。先 sudo apt update 刷新目录,再 sudo apt install 包名。国内把源换成清华 / 阿里,下载才不会卡死。这和后面 conda、Docker 换镜像源是同一件事:默认源在海外,先换近的。

sudo apt update
sudo apt install -y ssh net-tools vim
02

命令与权限:和机器说话

命令不是魔法单词,是 /usr/bin 里的小程序。权限则是「谁能碰这个文件」。把这两件事摸熟,后面的脚本、Docker、SSH 都只是在复用它们。

命令干什么记得的那一招
pwd / ls / cd我在哪、有什么、去哪cd - 回到上一处,ls -alh 看隐藏文件和权限
mkdir / touch / cp / mv / rm增、改名、删rm -rf 没有回收站。复制目录要 cp -r
cat / tail / less看内容日志用 tail -f,大文件别 cat
> >> |重定向、管道> 覆盖,>> 追加,| 把左边的输出变成右边的输入
find / grep找文件、找字ps -ef | grep python 是日常动作
tar / gzip打包压缩tar -zcvf a.tar.gz dir 打包,-zxvf 解开
ps / top / kill进程kill -9 PID 是强迫停止,先试试不带 -9
chmod / chown权限、主人目录要有 x 才能 cd 进去,这和文件的 x 不是一回事

chmod 的九个开关

从左到右三组:属主、属组、其他人。每组 r=4、w=2、x=1,加起来就是 7、5、4 这些数字。

属主 u

属组 g

其他人 o


    
管道是 Unix 的灵魂 ps -ef | grep uvicorn | awk '{print $2}' | xargs kill 读作:列出进程 → 滤出 uvicorn → 取出 PID → 交给 kill。每个程序只做一件小事,用 | 接起来。Shell 章节会把这件事写成脚本。

命令敲多了就要写成文件。那就是 Shell。

03

Shell:把命令变成程序

Ubuntu 默认的解析器是 bash。脚本第一行 #!/bin/bash 是在指定「用谁来读我」。变量默认全是字符串,等号两边不能有空格。

两种跑法

  • bash script.shsh script.sh:解析器帮你跑,脚本本身不用 +x
  • ./script.sh:脚本自己作为程序启动,必须 chmod +x
  • 子脚本里要看见某个变量,父 Shell 得先 export

特殊变量,面试爱问

  • $0 $1 $2 脚本名和参数,$# 参数个数
  • $* 把所有参数看成一串;$@ 把每个参数分开。加了双引号区别才明显
  • $? 上一条命令的退出码,0 是成功

引号决定「展开还是原样」

单引号平权、双引号展开变量、不打引号还会把 * 当成通配符。


    

判断和循环

条件写成 [ $a -gt 10 ],方括号内侧必须有空格。-eq -ne -lt -le -gt -ge 比整数;-e -f -d -r -w -x 问文件。if / case / for / while 的关键字都要收尾:fiesacdone

#!/bin/bash
if [ $# -eq 0 ]; then
  echo '请携带年龄'
elif [ $1 -lt 18 ]; then
  echo '未成年人'
else
  echo '成年人'
fi

S=$[(2+3)*4]          # 算术用 $[] 或 $(())
sum=0
for ((i=1;i<=100;i++)); do sum=$[$sum+$i]; done
文本三件套 cut -d: -f1 按分隔符切列;awk -F: '{print $1}' 能算、能过滤,NR 是行号、NF 是列数;正则里 ^ 行首、$ 行尾、. 任意一字、* 前一字重复。日志和 /etc/passwd 都靠它们拆。

机器听懂命令了。下面用 Python 描述数据。

04

Python:先分清「盒子」

语法规则(缩进、注释、标识符)一天就能混个眼熟。真正决定你会不会写的,是四只盒子:list、tuple、dict、set,以及它们可不可变。

四只盒子

有序还是无序、能不能改、用下标还是用键。大模型课后面的 tokenizer、batch、配置文件,全是这四只盒子的组合。

可变 / 不可变

  • 不可变:int float str tuple frozenset。看起来像「改了」,其实是绑到新对象上
  • 可变:list dict set。函数里如果改了传入的 list,外面能看见
  • 面试只爱问一件事:list 可变、不能当字典的键;tuple 不可变、能被哈希。里面嵌了 list,那个 list 仍可变

切片、enumerate、zip

  • 切片 a[1:5:2],倒序 a[::-1]。切片是新对象,但里面的可变元素仍是同一份
  • enumerate 同时拿下标,zip 并行遍历
  • is 问是不是同一个对象,== 问值相不相等。小整数和短字符串有 intern,别被 is 骗到

三个推导式,外加一个圆括号

Python 原生只认四种写法。方括号立刻造出整份 list;花括号看有没有冒号,决定是 set 还是 dict;圆括号不造容器——它返回生成器,next() 一次才算一个。面试常把后两个问成一对:列表推导式吃内存,生成器表达式吃时间。

同一句话,四个括号

数据源都是 range(5),表达式都是 x * 2。换括号,得到的东西完全不是一类。


      

匿名函数:就一行

lambda 参数: 表达式。没有名字、不能写语句,复杂逻辑别塞。真正出场的地方是「函数当参数」:sorted(..., key=)filtermap。能写成推导式的,优先推导式——更像人话。

和 def 差在哪

  • def 有名字、有语句块、能 return 多次
  • lambda 是表达式,只能有一个返回值,常被随手塞进参数里
  • 两者都是函数对象,都可以当闭包用。工厂函数返回 lambda n: n * k,捕获的还是那个 k

跟谁走

  • 排序:sorted(rows, key=lambda r: r['age'])
  • 过滤 / 映射:能写推导式就别用 filter + map
  • 面试更常问闭包和装饰器。lambda 只是闭包的一种瘦写法
students = [{'name': '张三', 'age': 36}, {'name': '李四', 'age': 14}]
sorted(students, key=lambda s: s['age'])
[s['name'] for s in students if s['age'] > 18]   # 比 filter+map 更清楚

流程控制就这些

顺序、分支、循环。分支用 if / elif / else,3.10 以后还有 match-case。循环里 continue 跳过本轮,break 离开,for-else / while-else 的 else 在「没被 break」时才走。输出优先 f-string:f'{name} 的 loss={loss:.4f}'

05

闭包、三器、三程

高频面试把 Python 进阶压成三叠:闭包把状态揣走;三器分别解决遍历、内存和监控;三程决定 CPU 密集还是 I/O 密集该派谁上场。

参数怎么传

位置参数、默认值、关键字、*args 收成元组、**kwargs 收成字典。默认值只求值一次,所以不要写 def f(xs=[])。不可变对象当参数,函数里重新赋值不影响外面;可变对象当参数,.append 会影响外面。

def train(model, data, lr=1e-4, *hooks, **opt):
    ...
train(net, ds, 3e-4, log, wandb, device='cuda')

闭包:外层走了,变量还在

内部函数引用了外部函数的局部变量,并且外部函数把内部函数 return 出去——这三件齐了就是闭包。外层结束本该销毁的变量,被内层「捕获」留在内存里。装饰器、函数工厂、藏一点私有状态,全靠它。

make_mul(n) 把 n 揣走

外层函数已经返回,按理说 n 该销毁。点下面的倍数,看内层函数还拿得到它。

三条缺一不可

  • 有函数嵌套:内层定义在外层里面
  • 内层引用了外层的变量(不是全局变量)
  • 外层把内层的引用 return 出去

改它、扔它

  • 内层默认只能读外层变量。要改,得写 nonlocal n,否则 Python 会当成「正在定义局部变量」报错
  • 闭包会把大对象一直握在手里,不用了把引用设成 None
  • 作用域查找顺着 LEGB:Local → Enclosing → Global → Built-in

三器:迭代器、生成器、装饰器

一句话带走:迭代器解决遍历,生成器解决内存,装饰器解决监控。循环的本质是先 iter() 拿到迭代器,再不断 next(),取完抛 StopIteration 结束。

next() 一次吐一个 token

列表本身不是迭代器,iter(tokens) 才是。惰性、单向、取完不能自动重来——和模型一次吐一个 token 同一个形状。

可迭代 vs 迭代器

  • 可迭代:实现 __iter__。list、str、dict、文件对象都是
  • 迭代器:再实现 __next__。一次性、只能往前、取完作废
  • 超大文件、百万行日志,用迭代器逐条读,避免一次性装进内存

生成器是特殊的迭代器

  • 函数体里有 yield,调用它不执行,只返回生成器
  • 每次 next(),代码才跑到下一个 yield
  • LangGraph 的 stream()、SSE 边生成边推 token,都是生成器在干活
def batched(xs, n):
    buf = []
    for x in xs:
        buf.append(x)
        if len(buf) == n:
            yield buf
            buf = []
    if buf: yield buf

装饰器:不改原函数,外包一层

@log 就是 chat = log(chat)。点开关,看出栈顺序:先进后出。计时、日志、重试、鉴权都是这一招。

装饰器本质是闭包:外层收下原函数,内层在调用前后加活,再把原函数的返回值递出去。functools.wraps 用来把原函数的名字抄回来,不然堆栈里全是 wrapper

类只要三件事

封装 · 继承 · 多态

  • _name 是约定别碰,__name 会被改写成 _类名__name
  • 多继承靠 MRO(C3),super() 按这张表走,不要手搓父类名
  • Python 的多态常常是鸭子类型:有 .predict() 就能当模型用

方法三种

  • 实例方法:第一个参数 self
  • 类方法:@classmethod,第一个参数 cls,工厂函数常用
  • 静态方法:@staticmethod,和类相关但不用到实例

三程:进程、线程、协程

同步:点完菜站在柜台等,期间什么也不干。异步:拿号回去刷手机,叫到再取。进程是操作系统分资源的独立房间;线程是房间里的几双手,共享家具;协程是自己排队,遇到 await 就让位,不劳操作系统。

三间工位

CPU 密集看谁能真并行;I/O 密集看谁切换更便宜。GIL 是那把只能递来递去的钥匙。

适合不适合典型任务
线程I/O 密集、要共享内存、几十到几百个任务CPU 密集(GIL)、成千上万连接读文件、调接口、后台轮询
协程高并发 I/O、连接数多、单任务计算量小CPU 密集(会堵住事件循环)、大量同步库HTTP、WebSocket、SSE、模型流式输出
多进程CPU 密集、要绕开 GIL、要强隔离纯等 I/O、要频繁共享数据(IPC 贵)图像处理、批量特征、模型并行
GIL 不是 bug CPython 同一时刻只有一个线程在跑 Python 字节码。等网络、等磁盘时线程会放开这把锁,所以 I/O 密集多线程仍有用;算矩阵时锁不放,多线程跑不满多核。CPU 密集请用多进程,或把活交给 NumPy / GPU。Agent 里 async 定义协程,await 让出执行权,asyncio.gather 把检索、调模型、调工具叠在一起等。

Python 会写了。模型要的不是列表,是一块整齐的数字。

06

NumPy 与 Pandas:张量和表格

list 能装东西,但装一百万个数就慢,还不能广播。NumPy 的 ndarray 是「同一类型、矩形、连续内存」的多维数组。Pandas 在它外面加了标签,变成 Series 和 DataFrame。

ndarray 记住这些

  • shape dtype ndim:形状、元素类型、维数
  • 切片是视图(改切片会改原数组),花式索引 / 布尔索引是副本
  • *np.multiply 是逐元素乘;矩阵乘用 @np.dot
  • 造数组:array zeros ones full arange linspace randn

和模型页的关系

  • 词向量是 (vocab, d) 的矩阵,一批 token 是 (B, T, d)
  • 注意力分数 Q @ K.T 就是这里的矩阵乘
  • 方差、标准差是归一化的原料,数学页 P2 有公式

广播:形状不够,就用 1 去补

两个数组要做逐元素运算,从右边对齐。某一维是 1 就「拉长」。两边都不是 1 且不相等,就报错。

数组 A
数组 B

Series 和 DataFrame

Series 是带索引的一列;DataFrame 是带行索引、列标签的一张表。loc 按标签取,iloc 按下标取。两列相加按索引对齐,对不上的格子填 NaN——和广播是亲戚,只不过对齐的是标签不是位置。

df.loc['aa':'cc', ['id', 'name']]   # 标签,含末尾
df.iloc[0:3, 2]                    # 位置,不含末尾
df.groupby('dept')['salary'].mean()
pd.merge(left, right, on='id', how='left')
pd.concat([a, b], axis=0)          # 0 往下接,1 往右接
方差和标准差 方差是「每个点和均值差的平方,再平均」;标准差是它开根号,单位和原数据一样。NumPy 默认除以 n(总体),Pandas 默认除以 n-1(样本)。数据越散,方差越大,后面 LayerNorm 要除掉的就是这个尺度。
格式特点什么时候用
CSV / TSV纯文本、通用交换、小表
JSON嵌套、跨语言接口、配置
Parquet / Feather列式、快稍大的分析数据
PKLPython 对象临时缓存,别当交换格式
XLSX给人看汇报,不进训练流水线

表格能算了。要长期存、多人查,就进数据库。

07

MySQL:把表问清楚

库是文件夹,表是带类型的 Excel,行是一条记录,列是字段。SQL 不是编程语言,是「描述你想要什么」的语言。分类记五个字母就够:DDL 建结构,DML 改数据,DQL 查询,DCL 管权限,TCL 管事务。

SELECT 真正的执行顺序

手写顺序是 SELECT … FROM … WHERE … GROUP BY … HAVING … ORDER BY … LIMIT。引擎心里的顺序是:先 FROM / JOIN 拿表,WHERE 滤行,GROUP BY 分组,HAVING 滤组,SELECT 算列,ORDER BY 排序,LIMIT 切开。所以 HAVING 里可以用聚合,WHERE 里不行;别名在 WHERE 里也还看不见。

JOIN:两张表怎么对上

员工表有一条没有部门的王五,部门表有一个没有员工的财务。三种 JOIN 对这两个人的态度完全不同。

NULL 是特殊公民

  • 任何和 NULL 的算术、比较,结果都是 NULL
  • 判断用 IS NULL / IS NOT NULL,不要写 = NULL
  • 实发工资这类计算,先 IFNULL(bonus, 0)

约束和事务

  • 主键、唯一、非空、外键、检查:把「脏数据」挡在门外
  • 事务 ACID:要么一起成功,要么一起回滚
  • START TRANSACTION; … COMMIT; 失败就 ROLLBACK
SELECT d.dname, AVG(e.salary) AS avg_sal
FROM t_employee e
JOIN t_department d ON e.did = d.id
WHERE e.salary IS NOT NULL
GROUP BY d.dname
HAVING AVG(e.salary) > 10000
ORDER BY avg_sal DESC
LIMIT 5;
子查询和 7 大子句 子查询出现在 WHERE(找「高于平均薪资」的人)、FROM(当临时表)、SELECT(每行再查一次,小心性能)。关联查询能写 JOIN 就别套三层相关子查询。索引加在 WHERE / JOIN 用到的列上,加太多会拖慢写入。
08

FastAPI:把函数变成接口

浏览器打过来的是 HTTP。FastAPI 做的事:按路径找到函数,把参数从 URL 或 JSON 里拆出来,校验类型,再把返回值变成 JSON。底下用 Uvicorn 跑,调度靠协程。

进程、线程、协程的对比在 05 三程。FastAPI 选协程,因为 Web 几乎全是等网络。下面只看 await 让不让路。

await 让不让路

任务 A 睡 1 秒,任务 B 睡 2 秒。串行写成两个 await 上下排;并发要先 create_taskgather

任务 A 1s
待命
任务 B 2s
待命

路径、查询、请求体

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

class Item(BaseModel):
    name: str
    price: float

@app.get('/items/{item_id}')
async def read_item(item_id: int, q: str | None = None):
    return {'id': item_id, 'q': q}

@app.post('/items')
async def create_item(item: Item):
    return item

/items/{item_id} 里的是路径参数,?q= 是查询参数,POST JSON 用 Pydantic 模型当请求体。类型写在函数签名里,FastAPI 负责转换和 422。模块一多,用 APIRouter 拆到 routers/user.pyinclude_router

SQLAlchemy 三件套 Engine 是连接入口,Session 是一次对话(add / commit / rollback / close),Model 是类和表的映射。FastAPI 里用依赖注入拿 Session:请求开始打开,结束关闭。关联关系四种:一对一、一对多、多对一、多对多(中间表)。懒加载默认 select,容易 N+1,列表接口改 selectin
09

Docker:把环境装进箱子

没有容器的时候,开发机和服务器永远差那么一点:Python 版本、系统库、环境变量。「在我电脑上能跑」就是这样来的。Docker 的目标是一次打包,到处跑。

虚拟机

  • 连客操作系统一起虚拟化
  • 隔离更彻底,启动以分钟计
  • 整台操作系统一起虚拟化

容器

  • 共享宿主机内核,只装箱应用和依赖
  • 启动以秒计,密度高
  • 隔离没虚拟机那么硬,但日常够用

镜像、容器、数据卷

点每一层。镜像是安装包,容器是跑起来的进程,卷是专门存数据的盘。

③ 数据卷 Volume

mysql-data → /var/lib/mysql

② 容器 Container

可写层 + 正在跑的 mysqld

① 镜像 Image

mysql:8.0 只读模板

两份文件就够上线

# Dockerfile  自定义镜像的菜谱
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

# docker-compose.yml  多容器一起起
services:
  db:
    image: mysql:8.0
    environment: { MYSQL_ROOT_PASSWORD: secret }
    volumes: [mysql-data:/var/lib/mysql]
  api:
    build: .
    ports: ["8000:8000"]
    depends_on: [db]
volumes:
  mysql-data:

docker compose up -d 会先起 MySQL,再起 API。镜像源换成国内的,pull 才不会超时。Windows 上 Docker Desktop 依赖 WSL2,需要先打开 CPU 虚拟化。

回头看一遍,从头到尾只有一条线。

10

回头看:一套环境的一以贯之

先有一台 Linux。目录和权限让你在这台机器上走动。Shell 把走动写成脚本。Python 描述逻辑,NumPy / Pandas 描述数字和表,MySQL 把表存住,FastAPI 把函数暴露出去,Docker 把这一切封进箱子。

一句话带走 大模型课的地板不是「会装 Ubuntu」,而是这条恒定主干:机器 → 命令 → 语言 → 数组/表 → 库 → 接口 → 容器。模型页的注意力、数学页的梯度,都站在这条主干上面。

三个能带走的心智模型

  • 一切皆文件。进程、权限、配置、日志,最后都是路径。找不到东西就 lsfindcat
  • 对齐再运算。NumPy 按形状广播,Pandas 按索引对齐,SQL 按 JOIN 键对齐。对不齐的格子不是 0,是「空」。
  • I/O 让出,CPU 独占。等网络用协程;算矩阵用 NumPy / GPU / 多进程。GIL 不是 bug,是在提醒你别用纯 Python 线程砸计算。

面试里常被问到的

  • list / tuple,闭包三条件,迭代器 / 生成器 / 装饰器,进程线程协程和 GIL
  • lociloc,广播规则,INNER 和 LEFT JOIN,事务 ACID
  • 镜像和容器的区别,为什么容器删了数据会丢,为什么要数据卷

读完之后

你已经可以做三件事

  1. 在一台 Ubuntu 上站住。知道文件在哪、权限怎么改、进程怎么查,并把常用动作写成 Shell。
  2. 把数据从文件送到接口。NumPy 管数组,Pandas 管表,MySQL 管持久化,FastAPI 管 HTTP。
  3. 用箱子把环境带走。Dockerfile 描述依赖,compose 同时拉起数据库和服务。然后可以回到 模型页 看注意力,或去 数学页 把符号捡回来。