
1. 为什么我要从零手搓一套AI工程学习路线第一次听到“ai-engineering-from-scratch”这个说法是在一个做后端的朋友群里。有人甩了个链接说现在AI工程师的岗位JD越来越离谱既要懂模型微调又要会写推理服务还得能搭RAG管道最好再顺手把前端界面也撸了。底下有人回了一句“那不就是全栈加炼丹”然后群里就沉默了。这个沉默很真实。市面上讲AI的教程要么是纯理论一上来就是反向传播公式糊脸要么是调包侠速成三行代码调用API跑通了但完全不知道背后发生了什么。真正能把“从零搭建”这件事讲清楚的资料少之又少。而“ai-engineering-from-scratch”这个标题恰好戳中了这个痛点——它想做的事情是把AI工程化落地所需的整条链路从最底层的环境配置到最上层的应用交付一层一层剥开给你看。我自己在这个方向上踩了差不多两年的坑。最开始用Python写脚本调模型后来发现服务化部署绕不开TypeScript做前端交互再后来遇到性能瓶颈又开始啃Rust写推理后端。这一路走下来最大的感受是AI工程不是单一语言能搞定的事它更像是一个多语言协作的流水线。Python负责实验和训练TypeScript负责界面和编排Rust负责高性能计算和底层优化。三者各司其职缺一不可。这篇文章适合谁看如果你已经会写一点Python但对怎么把模型变成真正能用的产品毫无头绪或者你是前端出身想搞清楚AI应用的后端到底在干什么又或者你是个纯新手想找一条不那么容易放弃的学习路径——那这篇内容应该能帮到你。我不会只给你一堆链接让你自己啃而是会把每个阶段该做什么、为什么这么做、容易在哪里翻车都掰开揉碎了讲。2. 整体学习路线的设计与选型逻辑2.1 为什么是Python、TypeScript、Rust这三件套先说说语言选型这件事。很多人会问AI工程为什么不能只用Python答案很简单——Python能跑模型但跑不快能写服务但扛不住高并发能做界面但做出来的东西没人愿意用。Python在AI领域的地位不用多说PyTorch、TensorFlow、JAX这些框架全是Python优先。数据清洗、特征工程、模型训练、实验管理这一整套流程用Python是最顺手的。但Python的GIL锁和解释执行特性决定了它不适合做高吞吐的在线推理服务。你可以用FastAPI搭个Demo但真到了生产环境延迟和并发一上来就露馅。TypeScript的价值在于它把前端的交互能力和后端的编排能力统一了。现在的AI应用早就不只是命令行工具了用户期望的是网页端对话、实时流式输出、可视化调试。这些用TypeScript配合React或者Vue来做开发效率和可维护性都远超Python的Gradio或Streamlit。而且TypeScript的类型系统能在编译期就帮你抓出很多低级错误这在多人协作的AI项目里特别重要。Rust则是解决性能天花板问题的终极武器。当你需要自己写推理引擎、做模型量化、优化内存布局、处理大规模并发请求的时候Rust的零成本抽象和内存安全特性就体现出来了。现在很多主流的推理框架比如Candle、Burn都是Rust写的。学会Rust你就能深入到模型执行的底层做真正意义上的性能优化。注意不要试图同时学这三门语言。正确的顺序是Python打底TypeScript做应用Rust做优化。每一步都建立在前一步的基础上跳步只会让你崩溃。2.2 从“能跑”到“能用”的四个阶段我把整个学习路线拆成了四个阶段每个阶段都有明确的产出目标。第一个阶段是环境搭建和基础能力建设目标是能在本地跑通一个完整的模型推理流程。第二个阶段是服务化封装把模型变成API能通过网络调用。第三个阶段是应用层开发做一个有界面的AI应用支持流式输出和多轮对话。第四个阶段是性能优化和工程化包括并发处理、缓存策略、监控告警、持续集成。这四个阶段不是线性的而是螺旋上升的。你在做应用层开发的时候可能会发现服务端的响应格式需要调整在做性能优化的时候可能会回头修改Python端的预处理逻辑。这种反复迭代的过程才是AI工程的真实状态。每个阶段我都会给出具体的工具链推荐和版本号因为AI领域的依赖管理是个大坑。Python的包版本冲突、Node.js的node_modules黑洞、Rust的crate兼容性每一个都能让你浪费一整天。我会把我验证过的组合写出来你直接抄作业就行。2.3 学习路径的避坑原则第一条原则不要一上来就搞大模型。很多人觉得AI工程就是调GPT-4结果连Transformer的基本结构都说不清楚。我的建议是从小模型开始比如TinyLlama或者Qwen的0.5B版本在本地跑通了再去碰大模型。小模型跑得快调试成本低能让你把注意力放在工程流程上而不是等API响应。第二条原则不要跳过环境配置。我见过太多人因为Python环境搞不定就放弃了。conda、venv、poetry、pipenv选一个用熟就行。我个人的习惯是用conda做环境隔离pip做包管理因为conda在处理CUDA相关的依赖时更省心。TypeScript这边用pnpm比npm快比yarn稳。Rust就用官方的rustup配合国内镜像源下载速度能快不少。第三条原则不要只写代码不写文档。AI项目的迭代速度极快今天能跑的代码下周可能就因为依赖更新挂了。养成写README和注释的习惯把每个模块的输入输出、依赖版本、已知问题都记下来。这不是浪费时间这是在给未来的自己省时间。3. 核心细节解析与实操要点3.1 Python环境搭建从零到跑通第一个推理脚本Python环境是整条链路的地基。我推荐用Miniconda而不是完整的Anaconda因为后者预装了大量用不到的包会让环境变得臃肿。下载Miniconda的安装包一路下一步就行注意勾选“Add to PATH”那个选项虽然安装程序会警告你不推荐但加了之后在终端里用起来方便很多。装完之后创建一个专门用于AI工程的环境conda create -n ai-eng python3.11 conda activate ai-eng为什么选3.11而不是最新的3.12或3.13因为很多AI框架的预编译轮子还没跟上最新版本。3.11是目前兼容性最好的选择PyTorch、TensorFlow、ONNX Runtime都有稳定的wheel包。接下来装核心依赖。这里有个技巧先装PyTorch再装其他。因为PyTorch的CUDA版本需要和你的显卡驱动匹配如果先装了其他包可能会把numpy的版本搞乱导致PyTorch安装失败。pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121cu121代表CUDA 12.1你需要根据自己显卡支持的CUDA版本调整。用nvidia-smi命令可以查看驱动支持的CUDA版本。如果用的是Mac的M系列芯片把index-url换成https://download.pytorch.org/whl/cpu就行MPS加速会自动启用。装完PyTorch之后验证一下import torch print(torch.__version__) print(torch.cuda.is_available()) print(torch.cuda.get_device_name(0) if torch.cuda.is_available() else CPU mode)如果cuda.is_available()返回False先别急着怀疑人生。检查一下显卡驱动版本然后确认安装的PyTorch是不是CUDA版本。用pip list | grep torch看看版本号后面有没有cu121这样的后缀。没有的话就是装成CPU版了卸载重装。实操心得国内下载PyTorch的wheel包经常超时可以加上清华源或者阿里源的镜像。但注意PyTorch的CUDA版本包不在PyPI上所以镜像源只对普通包有效。CUDA版本的PyTorch还是得从官方源下载建议挂个代理或者用离线安装包。3.2 TypeScript工程化配置避开那些弃用警告的坑TypeScript这两年的版本更新很快很多以前能用的配置项现在都标了弃用。比如baseUrl和moduleResolution: node10在TypeScript 7.0里就要被移除了。如果你现在写的tsconfig.json里还有这些配置编辑器会一直给你弹黄色警告虽然不影响运行但看着很烦。正确的做法是迁移到bundler模式。如果你用的是Vite或者Next.js它们已经帮你处理好了大部分配置。如果是手动配置参考这个tsconfig.json{ compilerOptions: { target: ES2022, module: ESNext, moduleResolution: bundler, strict: true, esModuleInterop: true, skipLibCheck: true, forceConsistentCasingInFileNames: true, resolveJsonModule: true, isolatedModules: true, noEmit: true, jsx: react-jsx, paths: { /*: [./src/*] } }, include: [src/**/*], exclude: [node_modules, dist] }moduleResolution: bundler是专门为现代打包工具设计的它支持package.json里的exports字段能正确处理条件导出。paths配置配合baseUrl的替代方案直接用相对路径的别名不需要再设置baseUrl。包管理器我推荐pnpm。安装很简单npm install -g pnpm然后在项目目录里pnpm init再pnpm add你需要的依赖。pnpm的硬链接机制能节省大量磁盘空间而且安装速度比npm快很多。对于AI应用来说你可能需要这些包aiVercel的AI SDK、openai、langchain/core、zod做输出格式校验、eventsource-parser处理流式响应。注意TypeScript的版本建议锁定在5.4以上但不要用beta版。5.4对using声明和NoInfer类型有更好的支持这些在写AI相关的类型定义时很有用。3.3 Rust开发环境从安装到跑通第一个推理DemoRust的安装比Python简单官方脚本一行搞定curl --proto https --tlsv1.2 -sSf https://sh.rustup.rs | sh但在国内下载crate的速度可能会让你想砸键盘。配置镜像源是必须的。在~/.cargo/config.toml里加上[source.crates-io] replace-with ustc [source.ustc] registry sparsehttps://mirrors.ustc.edu.cn/crates.io-index/中科大的镜像同步频率高速度也稳定。配置完之后cargo build的速度会有质的提升。Rust在AI工程里的切入点主要是推理侧。我建议从candle这个框架入手它比tch-rs更轻量纯Rust实现不依赖libtorch。创建一个新项目cargo new rust-inference-demo cd rust-inference-demo在Cargo.toml里加上依赖[dependencies] candle-core 0.4 candle-nn 0.4 candle-transformers 0.4 tokenizers 0.15 anyhow 1.0然后写一个最简单的模型加载和推理脚本。这里以加载一个HuggingFace上的小模型为例use candle_core::{Device, Tensor}; use candle_transformers::models::bert::{BertModel, Config}; use tokenizers::Tokenizer; fn main() - anyhow::Result() { let device Device::Cpu; let tokenizer Tokenizer::from_file(tokenizer.json)?; let config Config::default(); let model BertModel::load(model.safetensors, config, device)?; let encoding tokenizer.encode(Hello, AI engineering!, true)?; let input_ids Tensor::new(encoding.get_ids(), device)?.unsqueeze(0)?; let output model.forward(input_ids)?; println!(Output shape: {:?}, output.shape()); Ok(()) }这段代码跑通之后你就有了一个纯Rust的推理基础。接下来可以尝试量化、批处理、多线程并发这些优化手段。实操心得Rust的编译错误信息非常详细但有时候会一次性报几十个错误看着很吓人。我的习惯是从第一个错误开始改改完重新编译不要试图一次性解决所有问题。另外cargo clippy是个好东西能帮你发现很多代码风格和潜在性能问题建议每次提交前都跑一遍。4. 实操过程与核心环节实现4.1 用Python搭建一个完整的RAG管道RAG检索增强生成是当前AI应用最主流的架构之一。它的核心思路是先把文档切片、向量化、存入向量数据库然后在用户提问时检索最相关的片段拼接到提示词里送给大模型生成回答。先装依赖pip install langchain langchain-community chromadb sentence-transformers pypdf文档加载和切片from langchain_community.document_loaders import PyPDFLoader from langchain.text_splitter import RecursiveCharacterTextSplitter loader PyPDFLoader(technical_manual.pdf) pages loader.load() splitter RecursiveCharacterTextSplitter( chunk_size512, chunk_overlap64, separators[\n\n, \n, 。, , , , , , ] ) chunks splitter.split_documents(pages) print(fTotal chunks: {len(chunks)})chunk_size设为512是因为大多数embedding模型的最大输入长度是512个token。chunk_overlap设为64是为了保证切片之间的语义连续性避免一个完整的句子被切成两半。分隔符列表里把中文标点放在前面是因为中文文档的句子边界通常由这些标点界定。向量化和存储from langchain_community.embeddings import HuggingFaceEmbeddings from langchain_community.vectorstores import Chroma embedding HuggingFaceEmbeddings( model_nameBAAI/bge-small-zh-v1.5, model_kwargs{device: cuda}, encode_kwargs{normalize_embeddings: True} ) vectorstore Chroma.from_documents( documentschunks, embeddingembedding, persist_directory./chroma_db ) vectorstore.persist()BAAI/bge-small-zh-v1.5是一个中文效果不错的小型embedding模型只有24M参数在消费级显卡上跑得飞快。normalize_embeddingsTrue是为了让余弦相似度计算更准确。检索和生成from langchain_community.llms import Ollama retriever vectorstore.as_retriever(search_kwargs{k: 3}) llm Ollama(modelqwen2.5:7b, base_urlhttp://localhost:11434) def ask(question): docs retriever.invoke(question) context \n\n.join([d.page_content for d in docs]) prompt f基于以下上下文回答问题。如果上下文中没有相关信息直接说不知道。 上下文 {context} 问题{question} 回答 return llm.invoke(prompt) print(ask(这个设备的额定功率是多少))这里用Ollama在本地跑Qwen2.5的7B模型不需要联网数据不出本地。k3表示检索最相关的3个片段这个数字可以根据文档的粒度调整。如果文档切片比较细可以调到5如果切片比较大3就够了。注意RAG的效果很大程度上取决于切片质量。如果文档里有大量表格和图片纯文本切片会丢失很多信息。这种情况需要考虑用多模态embedding或者专门的表格解析工具。4.2 TypeScript实现流式对话界面流式输出是AI应用的基本要求。用户不想等模型生成完整回答再看到结果而是希望像打字机一样一个字一个字地显示。用TypeScript配合React和Vercel AI SDK实现起来很顺手。先装依赖pnpm add ai ai-sdk/openai react react-dom pnpm add -D types/react types/react-dom typescript vite vitejs/plugin-react核心的流式处理逻辑import { createOpenAI } from ai-sdk/openai; import { streamText } from ai; const openai createOpenAI({ baseURL: http://localhost:11434/v1, apiKey: ollama, }); export async function POST(req: Request) { const { messages } await req.json(); const result streamText({ model: openai(qwen2.5:7b), messages, temperature: 0.7, }); return result.toDataStreamResponse(); }前端组件import { useChat } from ai/react; export default function Chat() { const { messages, input, handleInputChange, handleSubmit, isLoading } useChat(); return ( div classNamechat-container {messages.map(m ( div key{m.id} className{message ${m.role}} {m.content} /div ))} form onSubmit{handleSubmit} input value{input} onChange{handleInputChange} placeholder输入你的问题... disabled{isLoading} / button typesubmit disabled{isLoading}发送/button /form /div ); }useChat这个hook帮你处理了消息历史管理、流式接收、加载状态这些琐事。toDataStreamResponse会把模型输出转换成SSE格式前端自动解析并逐字渲染。实操心得流式输出在开发环境下可能会被Vite的代理缓冲导致看起来不是逐字显示。解决办法是在Vite的配置里关掉代理的缓冲或者直接用Next.js的API路由它原生支持流式响应。4.3 Rust做推理性能优化量化和批处理当你把模型部署到生产环境第一个遇到的问题就是内存占用和推理速度。Rust在这方面有天然优势因为你可以精确控制内存分配和线程调度。量化是最直接的优化手段。以Candle为例把FP16的模型转成INT8use candle_core::quantized::ggml_file; use candle_core::Device; let device Device::Cpu; let model ggml_file::qtensor_from_ggml( ggml_file::Content::read_file(model-q8_0.gguf)?, device, )?;INT8量化能把模型体积缩小一半推理速度提升30%到50%精度损失通常在1%以内。对于大多数应用场景来说这个 trade-off 是值得的。批处理是另一个优化点。当多个请求同时到达时逐个处理会浪费大量计算资源。把请求攒成一批一次性送给模型能显著提高吞吐量use rayon::prelude::*; let inputs: VecVecu32 requests.iter().map(|r| r.token_ids.clone()).collect(); let batched Tensor::new(inputs.as_slice(), device)?; let outputs inputs.par_iter() .map(|ids| { let input Tensor::new(ids.as_slice(), device)?.unsqueeze(0)?; model.forward(input) }) .collect::ResultVec_, _()?;rayon是Rust的并行计算库par_iter()会自动把任务分配到多个线程上。配合批处理单机的推理吞吐量能提升3到5倍。注意批处理的大小不是越大越好。太大的batch会导致显存溢出反而降低吞吐量。需要根据你的硬件配置做压测找到最优的batch size。一般来说消费级显卡8G显存的batch size在4到8之间比较合适。5. 常见问题与排查技巧实录5.1 环境配置类问题速查问题现象可能原因排查步骤解决方案torch.cuda.is_available()返回FalsePyTorch装成了CPU版pip list | grep torch看版本号后缀卸载后从官方CUDA源重装pnpm install卡住不动默认源在国外pnpm config get registry切换到国内镜像源cargo build下载crate超时未配置镜像检查~/.cargo/config.toml添加中科大或清华源Python包版本冲突依赖树太复杂pip check查看冲突用conda新建干净环境重装TypeScript报baseUrl弃用TS版本≥5.0查看tsconfig.json改用pathsmoduleResolution: bundler5.2 模型推理类问题排查问题一推理结果乱码或者重复。这种情况通常是tokenizer和模型不匹配导致的。比如用Llama的tokenizer去解码Qwen的输出就会出现乱码。检查方法是打印tokenizer的vocab_size和模型的embedding层维度两者必须一致。另外生成参数里的repetition_penalty设得太低也会导致重复一般设在1.1到1.2之间比较合适。问题二显存溢出OOM。先看模型加载后的显存占用用nvidia-smi或者torch.cuda.memory_allocated()。如果加载完就快满了说明模型太大需要量化或者换小模型。如果加载后还有余量但推理时OOM那就是batch size或者序列长度的问题。把batch size降到1序列长度截断到512先跑通再逐步往上加。问题三流式输出断断续续。检查网络延迟和缓冲区设置。如果是本地部署可能是推理速度本身就跟不上流式输出的节奏。可以在服务端加一个小的缓冲队列攒够几个token再发。如果是通过API调用远程模型检查SSE连接的keep-alive设置有些代理会在60秒后自动断开。5.3 我踩过的三个大坑第一个坑在Python里用多线程跑推理。Python的GIL会让多线程变成伪并行不仅没有加速反而因为线程切换增加了开销。正确的做法是用多进程或者把推理逻辑放到Rust里用rayon做并行。第二个坑TypeScript的any类型滥用。AI应用的响应格式经常变很多人图省事就用any接住所有数据。结果就是运行时各种undefined错误。我的建议是用zod定义schema在API边界做运行时校验这样类型安全和运行时安全都有了。第三个坑Rust的unwrap()满天飞。在原型阶段用unwrap()快速验证没问题但到了生产环境任何一个unwrap()都可能让整个服务崩溃。用anyhow和thiserror做错误处理把错误往上抛在顶层统一处理。虽然写起来麻烦一点但稳定性提升是值得的。实操心得每次遇到问题先问自己三个问题——最近改了什么错误信息的第一行是什么最小复现路径是什么把这三个问题回答清楚90%的问题都能自己解决。剩下的10%再去搜或者问人。6. 从学习到落地的几个关键决策学完这套路线之后你手里会有三个能跑的东西一个Python的RAG管道、一个TypeScript的流式对话界面、一个Rust的量化推理服务。但怎么把它们串起来变成一个真正的产品这才是AI工程最考验人的地方。我的建议是先用Python做原型验证。把整个流程跑通确认业务逻辑没问题。然后逐步把性能敏感的部分迁移到Rust把交互部分用TypeScript重写。不要一开始就追求完美的架构先让东西跑起来再慢慢优化。另一个决策点是部署方式。如果只是内部使用用Docker Compose把三个服务串起来就够了。如果要对外提供服务需要考虑负载均衡、限流、监控这些。但那是另一个话题了先把单机跑通再说。最后说一个我自己的体会AI工程这个领域变化太快了。今天的最佳实践可能下个月就被新的工具替代。所以不要死守某一种技术栈保持学习的心态但也要有自己的一套核心方法论。我的方法论就是理解原理、动手验证、记录踩坑、持续迭代。这十六个字比任何具体的工具都重要。