从零构建AI工程能力:Python、TypeScript、Rust三语言实战指南

发布时间:2026/10/3 19:12:33
从零构建AI工程能力:Python、TypeScript、Rust三语言实战指南 1. 从零搭建AI工程能力为什么我选择用三种语言同时开工很多人第一次看到ai-engineering-from-scratch这个项目名第一反应是又一个教你调API的教程仓库。我最初也是这么想的直到真正把仓库拉下来跑了一遍才发现它想做的事情完全不是教你用现成框架搭个Demo而是从最底层的工程视角把AI系统拆成可以独立理解、独立实现、独立验证的模块。这个区别很关键前者让你会用一个工具后者让你在工具失效、文档缺失、线上出问题时还能自己造轮子。这个项目最吸引我的地方是它同时用Python、TypeScript、Rust三种语言去实现同一套AI工程概念。这不是为了炫技而是因为三种语言在AI工程链路里承担的角色完全不同。Python是算法验证和数据处理的主战场生态最全numpy、pytorch、transformers这些库几乎垄断了研究和原型阶段TypeScript负责的是前端交互层和Node服务层尤其是现在大量AI应用需要做流式输出、WebSocket通信、浏览器端推理TS的类型系统能帮你在编译期就拦住一大批低级错误Rust则是性能和可靠性的兜底推理服务、tokenizer、向量检索这些对延迟和内存敏感的部分用Rust重写往往能带来数量级的提升。我自己的判断是一个真正合格的AI工程师不应该只会写model.fit()和openai.chat.completions.create()。你需要知道一次推理请求从HTTP层进来之后经过tokenizer、embedding、attention计算、采样、detokenize每一步的耗时和内存开销大概是多少你需要知道为什么流式输出要用SSE而不是普通轮询你需要知道向量数据库在做ANN检索时HNSW和IVF两种索引结构在召回率和延迟上的取舍。这些东西光看框架文档是学不到的必须自己从零实现一遍。这个项目适合谁我认为有三类人收益最大。第一类是有一定编程基础但没系统做过AI工程的后端或前端开发者想补齐AI这一环第二类是在做AI应用但总感觉知其然不知其所以然的工程师想搞清楚底层到底发生了什么第三类是想在面试或技术分享中讲清楚AI系统设计的人需要一套能自己跑起来、能改、能验证的代码作为支撑。如果你只是想快速调个API做个聊天机器人这个项目可能有点重但如果你想真正理解AI工程它值得你花时间。接下来我会按照我自己实际跑这个项目的顺序把环境搭建、三种语言的模块设计、核心实现细节、踩过的坑以及一些文档里不会写的经验完整地讲一遍。2. 环境准备三种语言工具链的安装与版本对齐2.1 Python环境别用系统自带的用pyenv或conda隔离Python这块我踩过的坑最多。很多人图省事直接用系统自带的Python或者从python官网下载一个安装包一路下一步结果后面装依赖时各种版本冲突。我的建议是永远不要在系统Python上装项目依赖用pyenv或者conda做环境隔离。如果你用pyenv安装流程大概是这样的# macOS/Linux curl https://pyenv.run | bash # 配置shell环境变量后 pyenv install 3.11.7 pyenv virtualenv 3.11.7 ai-eng pyenv activate ai-eng为什么选3.11而不是最新的3.12或3.13因为AI生态里很多库对Python版本的跟进有延迟3.11是目前兼容性最好的版本numpy、torch、transformers这些主流库都有成熟的wheel包不需要自己编译。我试过在3.12上装某些库结果因为C扩展没跟上pip直接开始从源码编译等了二十分钟还报错。Windows用户我建议直接用conda因为conda能同时管理Python版本和二进制依赖省去很多编译麻烦conda create -n ai-eng python3.11 conda activate ai-eng装完Python之后先升级pip再装基础依赖python -m pip install --upgrade pip pip install numpy scipy matplotlib jupyter这里有个细节numpy的版本要和你的Python版本、操作系统架构匹配。如果你在M1/M2的Mac上确保装的是arm64版本的numpy否则性能会差很多。可以用python -c import numpy; print(numpy.__version__)验证再用numpy.show_config()看底层BLAS库是不是Accelerate或OpenBLAS。2.2 TypeScript环境Node版本管理和tsconfig的关键配置TypeScript这块第一件事是装Node。我推荐用nvm管理Node版本因为不同项目对Node版本要求不一样全局装一个很容易出问题curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash nvm install 20 nvm use 20Node 20是目前LTS版本对ES模块和顶层await支持都很好。装完Node之后全局装TypeScript和ts-nodenpm install -g typescript ts-node然后是tsconfig.json的配置这里有几个关键点。项目里如果用到了现代模块系统module和moduleResolution要配对设置。我见过太多人在这两个选项上踩坑报出选项moduleResolutionnode10已弃用这类警告。正确的做法是{ compilerOptions: { target: ES2022, module: NodeNext, moduleResolution: NodeNext, strict: true, esModuleInterop: true, skipLibCheck: true, forceConsistentCasingInFileNames: true, outDir: ./dist, rootDir: ./src }, include: [src/**/*], exclude: [node_modules, dist] }strict: true这个选项我强烈建议打开虽然初期会报一堆类型错误但它能帮你养成写类型的好习惯。AI工程里数据流很复杂一个any传下去后面出问题你根本不知道在哪。skipLibCheck: true是为了跳过node_modules里第三方库的类型检查加快编译速度这个在实际项目里几乎是必开的。2.3 Rust环境镜像源配置和工具链选择Rust的安装相对简单用rustup一条命令搞定curl --proto https --tlsv1.2 -sSf https://sh.rustup.rs | sh但国内网络环境下cargo拉依赖会很慢需要配置镜像源。在~/.cargo/config.toml里加上[source.crates-io] replace-with ustc [source.ustc] registry sparsehttps://mirrors.ustc.edu.cn/crates.io-index/配置完之后cargo build的速度会有明显提升。工具链方面默认装的是stable但如果你要用一些nightly特性比如某些SIMD指令可以用rustup toolchain install nightly装一个nightly然后用rustup override set nightly在特定项目里切换。Rust的IDE支持我用的是VS Code加rust-analyzer插件。装完之后记得在设置里把rust-analyzer.checkOnSave打开这样每次保存都会跑一遍类型检查比等到cargo build才发现错误效率高很多。如果你用IntelliJ IDEA也有Rust插件但rust-analyzer的体验目前还是最好的。三种语言的环境都装好之后我建议先各自跑一个hello world验证一下别急着上项目。Python跑python -c print(ok)TypeScript跑ts-node -e console.log(ok)Rust跑cargo new hello cd hello cargo run。三个都通了再开始看项目代码。3. 项目结构拆解三种语言各自负责什么3.1 目录组织逻辑与模块边界这个项目的目录结构不是按语言简单分三个文件夹就完事而是按功能模块划分每个模块下再分语言实现。我理解这种设计的意图是让你在学一个概念时能同时看到三种语言的实现对比它们各自的表达方式和性能特征。大致的结构是这样的ai-engineering-from-scratch/ ├── 01-tokenization/ │ ├── python/ │ ├── typescript/ │ └── rust/ ├── 02-embeddings/ │ ├── python/ │ ├── typescript/ │ └── rust/ ├── 03-attention/ │ ├── python/ │ ├── typescript/ │ └── rust/ ├── 04-inference-server/ │ ├── python/ │ ├── typescript/ │ └── rust/ └── ...每个模块都是一个独立的、可运行的最小示例。这种一个概念三种实现的组织方式好处是你能直观感受到同样一个tokenizerPython写出来可能就几十行Rust写出来要考虑所有权和生命周期TypeScript写出来要处理类型定义。这种对比本身就是一种学习。3.2 Python模块算法验证和数据处理的主力Python模块在这个项目里承担的是参考实现的角色。它的代码通常最简洁、最接近数学公式因为Python的语法表达力强numpy又能把矩阵运算写得像数学一样直观。比如在attention模块里Python版本的实现可能就是这样几行import numpy as np def scaled_dot_product_attention(Q, K, V, maskNone): d_k Q.shape[-1] scores Q K.transpose(-2, -1) / np.sqrt(d_k) if mask is not None: scores np.where(mask, scores, -1e9) weights softmax(scores, axis-1) return weights V这段代码几乎就是论文里公式的直接翻译。它的价值在于可读性和可验证性你可以拿它当基准去验证TypeScript和Rust的实现结果是否一致。我在实际做的时候会先用Python生成一组随机输入和对应的输出存成JSON然后让另外两种语言的实现去读同样的输入对比输出差异。如果差异在1e-5以内就认为实现正确。Python模块的另一个作用是快速实验。你想改个参数、换个激活函数、试试不同的初始化方式在Python里改一行就能跑不用等编译。这种迭代速度在研究阶段是无价的。3.3 TypeScript模块前端交互和服务编排TypeScript模块在这个项目里的定位比较特殊。它不只是把Python代码翻译成TS而是承担了服务编排和前端交互的职责。很多AI应用的实际形态是前端页面收集用户输入通过HTTP或WebSocket发给后端后端调用模型推理再把结果流式返回给前端。这个链路里TypeScript出现在两端。在项目里TypeScript模块通常会实现一个轻量的HTTP服务用Node的http模块或者Express/Fastify把推理逻辑包成API。同时它会实现一个简单的客户端演示怎么处理流式响应。比如处理SSEServer-Sent Events的代码const response await fetch(/api/chat, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ prompt }) }); const reader response.body?.getReader(); const decoder new TextDecoder(); while (true) { const { done, value } await reader!.read(); if (done) break; const chunk decoder.decode(value, { stream: true }); // 解析SSE格式的chunk for (const line of chunk.split(\n)) { if (line.startsWith(data: )) { const data JSON.parse(line.slice(6)); process.stdout.write(data.token); } } }这段代码看起来简单但里面有几个坑TextDecoder的stream: true选项必须加否则多字节字符比如中文在chunk边界处会被截断成乱码SSE的chunk不一定按行对齐需要自己维护一个buffer。这些细节在文档里往往一笔带过但实际写的时候不注意就会出问题。3.4 Rust模块性能敏感路径的重写Rust模块是这个项目里最有工程味的部分。它通常用来实现那些对性能要求高的组件tokenizer、矩阵运算、向量检索、推理调度。Rust的优势在于零成本抽象和内存安全你可以写出接近C的性能同时不用担心内存泄漏和空指针。比如tokenizer的BPE实现Python版本可能用字典和循环跑一万条文本要几秒Rust版本用HashMap和迭代器同样的数据可能只要几十毫秒。这个差距在离线处理时无所谓但在线服务里就是能不能扛住并发的问题。Rust模块的代码通常会长一些因为要处理所有权、生命周期、错误类型。比如一个简单的矩阵乘法pub fn matmul(a: [f32], b: [f32], m: usize, k: usize, n: usize) - Vecf32 { let mut c vec![0.0f32; m * n]; for i in 0..m { for j in 0..n { let mut sum 0.0f32; for p in 0..k { sum a[i * k p] * b[p * n j]; } c[i * n j] sum; } } c }这段代码没有用任何外部库就是最朴素的三重循环。它的意义在于让你理解矩阵乘法的内存访问模式为什么按行优先存储时内层循环要遍历k而不是n因为这样能更好地利用CPU缓存。这种底层认知是调库调不出来的。4. 核心实现细节从tokenizer到推理服务的完整链路4.1 TokenizerBPE算法的三种实现对比Tokenizer是AI工程链路的第一环也是很多人容易忽略的一环。你可能觉得tokenizer就是个分词工具调一下就行但实际上tokenizer的实现质量直接影响模型的输入质量和推理速度。BPEByte Pair Encoding是目前主流的分词算法GPT系列、LLaMA系列都用它。核心思想是从字符级别开始不断合并出现频率最高的相邻字符对直到词表达到目标大小。训练过程大概是把所有文本拆成字符序列统计每个字符的频率统计所有相邻字符对的频率合并频率最高的字符对形成新的token重复2-3直到达到目标词表大小或没有可合并的对Python实现这个算法用collections.Counter和循环就能写出来代码大概一百行。但它的性能瓶颈很明显每次合并都要重新统计所有对时间复杂度是O(n²)级别。处理大规模语料时Python版本可能要跑几个小时。TypeScript实现BPE性能比Python好一些因为V8的JIT优化不错但依然受限于单线程。而且TS里没有原生的优先队列要实现高效的频率统计得自己写堆或者用排序模拟。Rust实现BPE可以用HashMap做频率统计用BinaryHeap做优先队列再配合多线程并行处理不同文本块性能能比Python快几十倍。但Rust的代码量也最大光是处理UTF-8字符边界就要写不少代码。我在实际对比三种实现时发现一个有意思的现象小规模数据下三种语言的差异不明显但数据量上去之后Rust的优势是指数级放大的。这提醒我们技术选型不能只看能不能跑通要看在目标数据规模下能不能跑得动。4.2 Embedding与向量检索从余弦相似度到HNSWEmbedding是把文本、图像等非结构化数据转成稠密向量的过程。在AI工程里embedding的质量决定了检索、推荐、聚类等下游任务的效果。这个项目里embedding模块会实现几个核心功能向量归一化、余弦相似度计算、以及一个简单的向量索引。余弦相似度的公式是similarity(A, B) (A · B) / (||A|| * ||B||)如果向量已经归一化模长为1那分母就是1相似度就退化成点积。所以实际工程里通常先把所有向量归一化然后用点积算相似度这样能省掉一次开方和除法。Python实现用numpy一行就能算def cosine_similarity(a, b): return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))但如果要做大规模检索比如一百万条向量里找最相似的十条暴力计算就是一百万次点积延迟可能到几百毫秒。这时候就需要ANNApproximate Nearest Neighbor索引。项目里会实现一个简化版的HNSWHierarchical Navigable Small World或者IVFInverted File Index。HNSW的核心思想是构建一个多层图结构上层稀疏、下层稠密检索时从上层开始快速定位到大致区域再逐层下沉精确查找。它的优点是召回率高、查询快缺点是构建索引慢、内存占用大。IVF则是先把向量聚类成若干簇检索时只查最近的几个簇优点是构建快、内存省缺点是召回率受聚类质量影响。我在实际项目里选型时的经验是数据量在十万级以下暴力检索就够了别过早优化十万到千万级用HNSW或IVF千万级以上考虑量化压缩或者分布式方案。这个项目里两种索引都会实现你可以自己跑benchmark对比。4.3 Attention机制从数学公式到高效实现Attention是Transformer的核心也是AI工程里计算量最大的部分。标准的scaled dot-product attention公式是Attention(Q, K, V) softmax(QK^T / sqrt(d_k)) VPython实现直接翻译公式就行但要注意数值稳定性。softmax在输入值很大时容易溢出标准做法是先减去最大值def softmax(x, axis-1): x_max np.max(x, axisaxis, keepdimsTrue) exp_x np.exp(x - x_max) return exp_x / np.sum(exp_x, axisaxis, keepdimsTrue)这个x - x_max的操作看起来多余但它是防止np.exp(1000)变成inf的关键。我在早期实现时忽略过这一步结果在长序列上直接出NaN排查了半天才发现是数值溢出。TypeScript实现attention如果没有WebGL或WASM加速纯JS跑矩阵乘法会很慢。项目里可能会用typed arrayFloat32Array来优化内存布局再配合循环展开提升性能。但说实话TS做attention计算不是它的强项它的价值更多在于演示和教学。Rust实现attention可以用ndarray库或者手写SIMD指令。手写SIMD的版本性能最好但代码可读性差而且不同CPU架构的指令集不一样要做运行时检测。项目里可能会提供一个基础版本和一个优化版本让你对比性能差异。这里有个经验attention的计算瓶颈通常在内存带宽而不是计算单元。因为QK^T会产生一个n×n的矩阵n是序列长度这个矩阵的读写量是O(n²)。当n很大时GPU的显存带宽会成为瓶颈。这也是为什么FlashAttention这类算法能提速它通过分块计算和重计算减少了显存读写。4.4 推理服务HTTP层、批处理和流式输出推理服务是把模型能力暴露给外部调用的最后一环。这个项目里推理服务模块会实现一个完整的HTTP服务包括请求解析、批处理调度、推理执行、流式返回。HTTP层用Python的话FastAPI是首选因为它原生支持async和SSE。用TypeScript的话Fastify或Express都行但要注意Node的单线程模型CPU密集的推理会阻塞事件循环需要用worker_threads或者子进程。用Rust的话axum是目前比较流行的选择配合tokio做异步运行时。批处理是提升吞吐量的关键。单个请求推理时GPU利用率可能只有10%因为大部分时间在等数据传输。把多个请求攒成一批一起推理GPU利用率能到80%以上。但批处理会引入延迟第一个请求要等后面的请求凑够一批才能执行。所以实际系统里要在吞吐量和延迟之间做权衡常见的策略是设置一个最大等待时间比如10ms超时就用当前攒到的请求执行。流式输出是现在AI应用的标配。用户不想等模型生成完整回复才看到结果而是希望token一个一个蹦出来。实现流式输出后端要用SSE或WebSocket前端要处理chunk边界。这里有个坑SSE的每个chunk不一定是一个完整的token可能一个token被拆到两个chunk里也可能一个chunk里有多个token。所以前端要维护一个buffer按SSE的\n\n分隔符切分事件再解析data:字段。5. 踩坑实录那些文档里不会写的经验5.1 Python依赖冲突numpy版本和torch的兼容问题我遇到最头疼的问题是numpy和torch的版本冲突。torch在安装时会锁定一个numpy版本范围如果你先装了最新版numpy再装torchpip可能会把numpy降级然后你之前写的代码因为numpy API变化跑不了。解决办法是先装torch再装其他依赖让pip自己解决版本约束。或者用pip install torch --no-deps跳过依赖检查然后手动装兼容的numpy版本。但后者风险大除非你很清楚自己在做什么。另一个坑是M1/M2 Mac上的架构问题。有些库只有x86版本在arm64上装会走Rosetta转译性能差很多。装之前用pip debug --verbose看支持的平台标签确保装的是arm64版本。5.2 TypeScript类型体操泛型约束和条件类型TypeScript的类型系统很强大但也很容易写过头。我在实现一个向量检索的泛型接口时写了这样的类型type VectorLikeT T extends number[] ? T : never; type DistanceFnT (a: VectorLikeT, b: VectorLikeT) number;结果编译器报了一堆错因为T在VectorLike里没有被正确约束。正确的写法应该是type VectorLike number[] | Float32Array; type DistanceFnT extends VectorLike (a: T, b: T) number;这个坑的教训是类型体操要适度能用简单类型解决的就别上条件类型。AI工程里数据流复杂类型定义太花哨反而会增加维护成本。5.3 Rust所有权在推理循环里避免不必要的cloneRust的所有权系统是它的优势也是初学者的噩梦。我在写一个批处理推理循环时一开始每个请求都clone一份输入数据结果性能比Python还差。后来发现大部分clone是不必要的用引用和切片就能解决。比如这个函数签名fn process_batch(inputs: VecVecf32) - VecVecf32 { inputs.iter().map(|x| x.clone()).collect() }这里的x.clone()完全可以避免改成返回引用或者用into_iter消费所有权fn process_batch(inputs: VecVecf32) - VecVecf32 { inputs.into_iter().map(|x| x).collect() }into_iter会把Vec的所有权转移进来不需要clone。这个改动在数据量大时能省掉大量内存分配。5.4 跨语言数据一致性浮点数精度和序列化格式三种语言实现同一套算法最大的挑战是保证结果一致。浮点数在不同语言、不同硬件上的精度可能不一样比如Python的float是双精度Rust的f32是单精度TypeScript的number是双精度。如果混用结果会有微小差异。我的做法是统一用f32做计算用JSON做序列化对比时允许1e-5的误差。JSON的浮点数表示虽然不完美但跨语言兼容性好。如果对精度要求极高可以用二进制格式比如MessagePack或Protobuf但调试起来麻烦。还有一个坑是字节序。不同CPU架构的字节序可能不同虽然现在主流都是小端如果直接传二进制数据要注意统一。JSON和文本格式没这个问题但性能差一些。6. 从项目到生产还需要补哪些工程能力6.1 可观测性日志、指标和追踪这个项目作为学习材料代码里基本没有可观测性。但真要把AI系统上生产日志、指标、追踪三件套一个都不能少。日志要结构化用JSON格式包含请求ID、用户ID、模型版本、输入长度、输出长度、耗时等字段。这样出问题时能快速定位是哪个请求、哪个环节出的问题。指标要覆盖QPS、延迟分布P50/P95/P99、错误率、GPU利用率、显存占用、队列长度。这些指标用Prometheus采集Grafana展示。延迟分布比平均延迟重要得多因为用户感知的是P99不是平均值。追踪要用OpenTelemetry把一次请求经过的每个组件网关、tokenizer、推理引擎、后处理都打上span这样能看到时间花在哪。我见过很多系统优化了半天结果发现瓶颈在日志写入或者序列化上。6.2 模型版本管理和灰度发布AI系统和传统软件最大的区别是模型是动态的。今天用v1模型明天可能换v2后天可能做A/B测试同时跑两个版本。所以模型版本管理是必须的。基本要求是每个模型有唯一版本号推理请求要记录用了哪个版本支持按流量比例灰度发布出问题能快速回滚。这些用配置中心或者特征开关就能实现但要在架构设计时就考虑进去别等上线了再补。6.3 成本控制token计费和资源调度AI推理是烧钱的尤其是大模型。token计费是基本操作记录每个请求的输入token数和输出token数按定价算成本。但更重要的是资源调度能不能把多个小请求合并成一批能不能在低峰期用更便宜的实例能不能对长请求做超时和截断我自己的经验是成本优化里收益最大的是批处理和缓存。批处理能把GPU利用率从20%提到70%直接省掉三分之二的机器。缓存则是对重复请求直接返回结果比如系统提示词、常见问题命中率能到30%以上。6.4 安全与合规输入过滤和输出审核AI系统面临的安全风险包括提示词注入、越狱、敏感信息泄露等。工程上要做的是输入过滤和输出审核。输入侧检测异常模式比如超长输入、特殊字符、已知的攻击模板输出侧做敏感词过滤和内容分类拦截不合规内容。这些机制要在推理链路的哪个环节做需要权衡。放在最前面能省算力但可能误杀放在最后面最准确但已经消耗了算力。实际系统里通常是多层过滤前面粗筛后面精筛。7. 我个人的学习路径建议如果你打算认真跟完这个项目我的建议是不要按目录顺序从头到尾看而是按数据流的顺序学。先搞懂一条文本从输入到输出经过哪些环节每个环节的核心算法是什么然后再逐个模块深入。具体来说我推荐的顺序是tokenizer → embedding → attention → 推理服务 → 向量检索。这个顺序符合数据流动的方向每一步都是下一步的输入学起来有连贯性。每种语言的学习重点也不一样。Python重点看算法实现和数值计算理解公式怎么变成代码TypeScript重点看服务编排和异步处理理解怎么把AI能力包装成APIRust重点看性能优化和内存管理理解怎么把慢代码变快。最后说一个我自己的体会这个项目最大的价值不是代码本身而是它逼着你去思考为什么。为什么tokenizer要用BPE而不是按词分为什么attention要除以sqrt(d_k)为什么推理服务要做批处理这些问题你在调库的时候永远不会想但自己实现一遍就全明白了。这种理解才是AI工程师和调包侠之间的真正差距。