news 2026/10/1 19:28:55

AI工程从零构建:完整路线图、最小闭环与踩坑实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
AI工程从零构建:完整路线图、最小闭环与踩坑实战

把 ai-engineering-from-scratch 当项目名的人,大概率不是想再装个环境跑通 demo 了事,而是想把这门技术栈从地基开始重新立一遍。这几年我前后面试过不少候选人,简历上写着“熟悉 AI 开发”,但一聊到数据怎么准备、模型怎么评估、服务怎么部署、日志丢了一晚上怎么排查,明显就露怯了。这就是“会用模型”和“会做 AI 工程”之间的差距。这篇文章想聊的正是后者:从零开始构建 AI 工程能力的完整思路、最小落地路径,以及我实际踩过的一些坑。适合正在转 AI 工程方向、或者已经能写训练脚本但没正经做过系统化项目的朋友。

1. 先把“AI 工程”这顶帽子戴正:它是工程,不只是炼丹

1.1 算法、研究与工程:三种角色的核心差异

很多人以为 AI 工程就是把训练好的模型包装成一个接口,其实这只是冰山一角。在我带团队做推荐系统和 NLP 服务的几年里,最大的感受是:算法研究员追求的是“指标再涨一个点”,工程师追求的是“系统能不能稳定跑 180 天”。两者目标不一样,做事方式就完全不一样。

拿一个生活类比来说,AI 算法像研发一道新菜:在厨房里试配方,失败了就倒掉重来,锅碗瓢盆乱点没关系。AI 工程则是把这道菜变成连锁中央厨房的标准作业流程:食材供应、切配标准、烹饪时间、出餐温度、冷链配送,每一环都要有量化标准。很多从算法转工程的人初期会极其难受,因为代码写一半发现数据有问题,第一反应不是去修数据管道,而是想着换模型——这在工程里是灾难。

AI 工程的核心范围其实就四大快:数据工程、训练/微调、推理优化、部署与监控。很多人以为最难的是训练模型,实际到了生产环境,数据管线和可观测性才是最耗费精力的事情。如果你准备从零搭建一套 AI 工程能力,第一课不是去追最新的大模型,而是建立“以稳定交付为目标”的系统思维。

1.2 from scratch 到底在“重造”什么

项目名叫 ai-engineering-from-scratch,这个 from scratch 不是让你从反向传播推导开始写神经网络,也不意味着所有基础设施都自己造轮子。在我理解里,核心含义是:不依赖一键脚本、不复制粘贴拼凑教程,而是亲手把整个项目链路走通,并为每一层选择给出理由。

市面上很多“从零构建大语言模型”类的项目,我看过不少,也见过有人专门拿这类路线图当学习大纲。直接体验是:这类项目很适合建立模型内部结构的认知,比如注意力机制、分词器、训练循环长什么样。但工程化能力不能只靠这些。你还需要能回答:数据谁清洗?特征存在哪?服务怎么发版?模型效果变差是谁的锅?日志凭什么能定位问题?这些问题的答案,是任何端到端教程无法塞进你脑子里的东西。

真正有意义的 from scratch,是亲自动手做一遍这样的闭环:收集原始数据 → 设计数据处理流水线 → 训练或微调一个可用模型 → 离线评估找短板 → 封装成服务 → 监控线上表现 → 根据反馈回炉数据。跑通这一圈,你才有资格说自己具备 AI 工程的地基。

2. 从零起步的完整路线图:先搭最小闭环,再追热点

2.1 五个必修基础:Python、数据、数学、工具链、版本管理

我面试的时候总有人问,是不是要先学完机器学习理论才能做工程?答案是不用。但有一些基础是逃不掉的,它们像工地上的脚手架,缺一个都得停工。

第一是Python 工程能力。不是能写 for 循环就行,而是要会写结构清晰、有类型标注、能跑单测的代码。至少要掌握venv或poetry做环境隔离,熟练使用pandas、numpy,读过requests的源码就更好了。第二个是数据处理基本功,SQL 是你最少需要能写 join、窗口函数、子查询的水平。很多模型上线后效果差,最后查下来都是数据拼接错了一个键。第三是必要数学直觉,线性代数知道矩阵乘法是什么、概率论知道条件概率和独立事件,微积分有个导数的概念,就足够起步。真正复杂的数学公式用到时再补,完全可以。

第四是命令行和云资源管理,bash 脚本、进程管理、GPU 显存查看、日志流式追踪这些都得熟练。我见过不少人在本地 IDE 里跑得动,一上 Linux 服务器就寸步难行,这就谈不上工程化。第五是Git 和实验追踪。模型训练和普通软件开发的差异在于“可复现性”,训练脚本、数据版本、超参数、模型权重四者必须绑定保存。我自己常用 DVC 做数据版本管理,配合 Git commit 写清楚改动原因,这样三个月后模型效果回退,还能定位到是哪一天哪次提交引入的问题。

2.2 工具选型:框架、训练、推理、部署怎么选

工具选型最容易犯的毛病是“哪个流行选哪个”。从零起步阶段,我的原则是:用上手成本最低、社区最活跃、调试最直观的工具,先跑通再考虑规模化。

表格是踩坑参考:

环节我常用的起手方案什么时候升级
模型训练PyTorch + Hugging Face Transformers需要分布式训练时换 DeepSpeed 或 Megatron
数据存储SQLite / Parquet 文件超过百万级样本再引入数仓或向量数据库
向量检索Chroma / FAISS数据量千万级、要求高并发时再上专门服务
服务框架FastAPI需要类型安全的 RPC 时切 gRPC
监控Prometheus + Grafana + structlog规模化后补充链路追踪(OpenTelemetry)
自动调参Optuna特征多、实验量大时引入

这个选型的核心逻辑是:小项目用大工具,只会让从零到一的过程变得无比痛苦。我第一次做RAG项目时,一开始就上了 Kubernetes,结果整整一周都在折腾容器网络,业务一行没写。后来学会先写单机 FastAPI,效果验证没问题后再容器化,效率高了不是一点半点。

2.3 第一版 MVP 怎么定:给自己一个可验收的目标

from scratch 最容易失败的原因是目标太大。你可以花两个月看视频,但输入一个烂 query 之后,连一个能回答“查不到”的系统都跑不出来,那就一直在纸上谈兵。

我建议第一个项目定这种目标:用 1000 条内部文档,做一个本地知识库问答系统,回答命中率在测试集上达到 80%,单次请求延迟不超过 5 秒。听起来朴素,但它强制你覆盖 AI 工程全部关键环节:数据加载、文本切分、向量化、检索策略、生成或规则回答、API 封装、评估脚本、日志监控。先不追求大模型生成,用检索 + 模板回答就能验证链路。后续再逐步替换其中每一环,就变成了持续迭代的工程。

这类 MVP 有一个额外的好处:能拿给非技术同事试,得到反馈。任何没有用户的 AI 系统都是自嗨。我见过太多团队把模型刷分刷得很高,但落到实际业务一问三不知。最小闭环的意义,就是尽早把“系统行为”暴露给真实场景,逼你关注数据分布、边界案例和失败模式。这些才是工程经验真正沉淀的地方。

3. 手把手:从零搭一个可运行的小型 AI 工程

3.1 项目结构设计与“为什么这样拆”

先定项目结构。一个看起来“正规”但不过度设计的目录:

kb_qa/ ├── data/ │ ├── raw/ # 原始文档 │ └── processed/ # 清洗切分后的段落 ├── src/ │ ├── ingest.py # 文档加载与切分 │ ├── embed.py # 向量化与入库 │ ├── retriever.py # 检索模块 │ ├── api.py # FastAPI 服务 │ └── config.py # 参数配置 ├── tests/ │ ├── test_retriever.py │ └── test_api.py ├── scripts/ │ └── run_eval.py # 离线评估 ├── requirements.txt └── README.md

这个拆法的逻辑是:data、src、tests、scripts四个目录把“数据、代码、验证、实验”四个关注点分开。很多人把所有 .py 文件堆在根目录,一开始没问题,等项目变大,依赖关系就开始失控。把ingest.py和embed.py分开,是因为这两个环节重跑的成本完全不一样:导入文档可能只需一次,而向量化在模型换版本后需要重新执行。合在一个文件里,会导致你为了改一个切分参数,被迫把整个入库流程重跑一遍。

我另外加了config.py,所有可调参数集中管理。切分块大小、重叠窗口、Top-K 检索数量、模型名称、端口号,全部写在配置里而不是散落在代码各处。这是工程习惯,不是炫技。AI 项目里参数特别多,如果不集中管理,三个月后没人记得当时的chunk_size=512是怎么试出来的。

3.2 数据准备:清洗、切分与文本向量化

假设你已经把 1000 条 FAQ 或文档放进了data/raw/,下面这段代码演示加载和切分。

# src/ingest.py from pathlib import Path from typing import List, Dict def load_text_files(raw_dir: Path) -> List[Dict[str, str]]: docs = [] for path in sorted(raw_dir.glob("*.txt")): text = path.read_text(encoding="utf-8") # 注意编码 docs.append({"source": path.name, "text": text}) return docs def chunk_text(doc: Dict[str, str], chunk_size: int = 512, overlap: int = 128) -> List[Dict[str, str]]: """将长文本切成固定长度的小块,块与块之间保留overlap避免语义断裂。""" text = doc["text"] chunks = [] start = 0 while start < len(text): end = start + chunk_size chunks.append({ "source": doc["source"], "text": text[start:end], "chunk_id": len(chunks), }) start += chunk_size - overlap return chunks

这里必须多说两句为什么切分要设重叠。文本不像数据库里的结构化字段,语义是有跳变的。如果硬切,很可能一句话被从中间截断,前半段进了第 3 块,后半段进了第 4 块,结果是两个块都缺乏完整表达,检索时谁都召回不到。设 overlap=128,等于给关键句子留出冗余空间,牺牲一点存储换召回率,值。我第一次做的时候忘了加重叠,测试集命中率只有 60%,加上重叠直接跳到 78%,这就是工程细节决定效果上限的典型例子。

向量化这一步,我选的是一个多语言的轻量级 embedding 模型,比如sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2。选它不是因为效果最强,而是体积小、CPU 也能跑、对中文还算友好。启动项目时先别追求最强模型,能稳定产出向量并跑通链路,比什么都重要。

# src/embed.py from sentence_transformers import SentenceTransformer def get_embedding_model(model_name: str = "paraphrase-multilingual-MiniLM-L12-v2"): return SentenceTransformer(model_name)

3.3 检索模块:向量检索 + 少量规则兜底

现在进入检索部分。为什么先不讨论“生成模型”而是先做检索?因为 RAG 类系统的质量上限,很大程度由召回决定。你后面接入任何生成模型,喂给它的检索结果不对,生成再多东西也是胡说。工程上有一个经验法则:先让检索满足指标,再优化生成。

# src/retriever.py import chromadb from .embed import get_embedding_model client = chromadb.PersistentClient(path="./data/vector_store") collection = client.get_or_create_collection("kb") def index_documents(chunks: list): model = get_embedding_model() ids = [f"{c['source']}#{c['chunk_id']}" for c in chunks] texts = [c["text"] for c in chunks] embeddings = model.encode(texts).tolist() collection.add( ids=ids, documents=texts, embeddings=embeddings, metadatas=[{"source": c["source"]} for c in chunks] ) def search(query: str, top_k: int = 3): model = get_embedding_model() qvec = model.encode([query]).tolist() res = collection.query(query_embeddings=qvec, n_results=top_k) return res["documents"][0]

代码本身很简单,但选择 Chroma 的原因值得说。这是嵌入式向量库,不需要单独起服务,数据存在本地目录,非常适合从零起步。等你有上千万向量、多个客户端并发访问,再迁移到独立的向量数据库也不迟。一开始就接入庞大的分布式组件,只会让“充电两小时通话五分钟”。

另外我给检索加了一个事后发现很有用的兜底逻辑:如果向量检索返回结果的相似度分数都低于阈值(比如 0.35),就返回“知识库中没有找到可靠答案”。这比硬给一个不相关内容要负责任得多。在企业内部使用场景里,用户最讨厌的不是“我不知道”,而是 AI 一本正经地瞎说。这种阈值兜底是成本最低的可靠性保障。

3.4 把系统包成 API:FastAPI 与工程化封装

检索闭环跑通后,下一步是包成服务。我用 FastAPI,几乎不用犹豫,因为它自带请求校验、交互式文档、异步支持,五个小时就能写完整套。

# src/api.py from fastapi import FastAPI from pydantic import BaseModel, Field from .retriever import search import time import logging app = FastAPI(title="KB-QA", version="0.1.0") logger = logging.getLogger("kb_qa") class Query(BaseModel): text: str = Field(min_length=1, max_length=200) class Answer(BaseModel): answer: str sources: list[str] @app.post("/answer", response_model=Answer) def answer(query: Query): start = time.perf_counter() hits = search(query.text, top_k=3) if not hits: return Answer(answer="知识库中没有找到相关答案。", sources=[]) context = "\n---\n".join(hits) answer_text = f"根据检索到的资料,最相关的内容如下:\n{context}\n\n请结合资料自行判断。" elapsed = time.perf_counter() - start logger.info("query_answered", query=query.text, latency_ms=round(elapsed * 1000, 2), hits=len(hits)) return Answer(answer=answer_text, sources=hits)

这个接口故意先不做大模型生成,而是把检索结果直接返回给用户。原因有两点:第一,验证链路时少一个变量,出了问题你只需要查检索;第二,很多企业内部知识问答场景里,给出原文出处比自由生成更受信任。等后续你要接入生成模型,只需在answering环节替换实现,接口签名不变,外部调用方毫无感知。这就是“模块可替换”的工程价值。

我通常还会加一个/health接口给部署探活用,返回当前版本号和最近一次索引时间。这看起来不像用户功能,但在真出问题时能救命。线上服务挂了,监控系统第一件事就是探活,如果不是 HTTP 200,再去看进程和日志。

3.5 测试、日志与评估:工程化的三条生命线

很多 AI 项目的测试只做接口连通性,忽视了“检索质量回归”。我强烈建议每个项目至少保留一组 golden questions,也就是带着正确预期答案的测试问题集。每次改切分参数、换 embedding 模型、调阈值,都跑一遍测试,看有多少 query 能召回到正确来源。这是把“AI 质量”像普通软件测试一样管起来的最简单办法。

# scripts/run_eval.py import json from kb_qa.src.retriever import search with open("tests/golden_set.json", encoding="utf-8") as f: golden = json.load(f) hit = 0 for item in golden: results = search(item["query"], top_k=5) if item["expected_source"] in results: hit += 1 print(f"Top-5 hit rate: {hit / len(golden):.1%}")

黄金问题集不需要追求数量,先精挑 30 条覆盖核心场景就够。我的经验是,这 30 条问题一开始就能暴露你系统中绝大部分低级错误,比如编码问题、停用词干扰、文档缺失。RUN 一次基准确认通过后,把它固化到 CI 流程里。今后每一次“看起来不大”的改动,都先跑一遍这批测试,你会发现“我觉得不影响”是最贵的五个字。

日志方面我用structlog,它不是打印一串字符串,而是输出结构化 JSON。这样 Grafana 或 ELK 消费起来非常方便。别小看这一层,生产环境排障靠的往往不是复现 bug,而是翻日志里的关联字段。我会把query、latency_ms、hits、model_version都打进去,出问题时一条条筛,几分钟就能定位到是检索落后还是模型版本异常。

4. 踩坑录:新手做 AI 工程最常见的坑与排查方法

4.1 环境与依赖:怎么快速定位“装不上”的问题

环境问题占了新手踩坑的大头。直接给一张速查表:

问题典型现象常见原因排查命令
CUDA 不可用torch.cuda.is_available()返回 False驱动与 PyTorch 版本不匹配nvidia-smi,对照官网的 torch 版本表
依赖冲突安装包时提示依赖任意版本失败requirements 未锁版本pip freeze > requirements.lock
模型下载失败程序卡在下载进度条或超时网络源不稳定改用国内镜像源或提前手动下载到本地缓存目录
Python 版本不对语法兼容或包无法安装使用了系统默认 Python用pyenv或uv管理多版本
内存不足进程被 OOM Killer 杀掉embedding 一次性加载过多分批 encode,或用生成器逐批处理

我最想提醒的一点是永远不要用系统自带的 Python 裸装环境。开发机和服务器环境不一致,后面会害死你。开个新项目第一件事就是建虚拟环境,养成肌肉记忆。另外装 PyTorch 一定要去官网生成适配你 CUDA 版本的安装命令,不要直接pip install torch,默认版本往往不是为你的 GPU 准备的。

4.2 数据与检索:为什么效果差却不报错

比环境错误更麻烦的是“代码不报错,效果很烂”。这种问题通常出现在数据准备环节。

第一个坑是文本编码。Windows 下记事本生成的文件经常是 GBK,Linux 下默认 UTF-8,直接读取就乱码。这属于上线前最该被 golden set 拦住的问题,但很多人因为没做测试,直到用户截图吐槽才发现。第二个坑是清洗不足。文档里如果有大量网页版式、表格符号、换行符,向量会把“装饰噪音”也编码进去,检索时经常被表象相似但语义无关的内容带偏。我的做法是入库前统一走一遍清洗函数:去 HTML 标签、压缩多余空白、统一中文标点。

第三个坑是切分粒度问题。我实验过 chunk size 从 256 到 1024 的变化,结论是:过小会把句子拦腰斩断,过大又会引入冗余噪音。512 字符 + 128 重叠是很多人调出来的经验值,但它不是普适真理。如果你做的是代码文档库,可能需要 150 行左右作为一块;做客服 FAQ,可能一句话就是一块。块粒度应该由内容结构决定,而不是由数字洁癖决定。

4.3 性能与部署:单机够用就不上分布式

部署问题里,我看到最多的错误是“过早分布式”。一个日请求量几百次的内部工具,硬要上 Kubernetes + 独立向量数据库,结果运维成本比开发成本还高。单机部署完全能扛的规模,就别给自己找事。先用systemd或supervisor挂守护进程,配合一个 Load Balancer 就足够。

遇到性能不够,优先排查的不是换更大 GPU,而是缓存和批量。检索结果如果对相同 query 有重复请求,加个简单的 LRU Cache 能把命中率提升一大截。embedding 计算也可以做成批量预计算,而不是每次请求实时算。有一次我排查一个“响应慢”的问题,最后发现是每次请求都重新加载了一遍 embedding 模型,加上磁盘读取,直接就干掉了 2 秒。把模型实例放到全局初始化一次后,响应时间立刻降到了 300 毫秒。这种性能问题光看代码很难发现,必须看日志里的耗时分布。

5. 从一个小项目扩展成真工程

5.1 评估先行:没有黄金测试集,一切都是“感觉”

我见过很多团队上线 AI 功能,上线理由是“模型效果看起来不错”。“看起来不错”是这个领域最危险的四个字。要负责任地说效果,必须有一批离线测试集和一批上线后的统计指标。

离线测试集对应的是“系统能不能做对”:用预先标注好的 query 与正确出处,评估检索命中率、答案可用性。在线指标对应的是“用户买不买账”:用户反馈按钮、问答采纳率、日志里的“复制/点赞”事件。两边组合使用才完整,缺少其一都只是在自我感觉良好。

我在实际项目里的做法是:每个迭代周期先跑一遍 golden set,看指标的波动曲线;如果某次改动让命中率掉了 5 个点,即使线上测试还没出问题,也先不合并代码。把“AI 质量回归”当作像软件工程的单元测试一样看待,你会少扛很多事故。你会发现自己是跟一个规范的、可持续的机制在合作,而不是在赌运气。

5.2 规模化要补的课:并发、缓存与可观测性

当系统真正有真实用户时,你要关注三件事:并发放大、缓存命中、全链路可观测。

并发放大指的是连接池、超时、重试这些后端家常问题,在 AI 服务里更容易被忽略。你的模型推理可能在毫秒级,但数据库链接、下游 API 调用的超时设置不对,用户侧就会感到随机卡顿。我的习惯是在网关层统一设置超时和重试策略,不把网络不稳的问题下发给业务代码。缓存方面,最简单的做法是给检索结果加语义缓存:对完全相同的 query 直接返回上次结果,相似 query 用 embedding 距离判断是否命中缓存。实测很多内部知识库平台能靠这一招把算力成本砍掉一半。可观测性则是从第一天就养成的习惯,给每次请求打上 trace id,日志、错误、耗时全部关联。没有这个,越往后排障越痛苦。

5.3 给未来的自己留三件事

最后分享一点我自己的坚持。每做完一个阶段,我都会在 README 里写下三件事:当前系统已知的短板、下一次迭代最想验证的假设、上次踩坑中沉淀出的检查清单。这些内容不是给外人看的,而是给三个月后忘记细节的自己看的。AI 工程迭代快,代码可能三个月不碰,逻辑就会生疏。有这份文档,你能快速恢复上下文,而不是从零读代码。

从零开始做一个 AI 工程项目的意义,不是最终交付的那一瞬间,而是你在每个卡住的地方被迫查资料、做实验、做取舍的过程。我个人的体会是,它能把你从“跑通一个教程”的舒适区里赶出来,真正去面对数据、评估、部署这些脏活。先把一个 1000 条文档的问答系统完整闭环做出来,比收藏十个“从零构建大模型”的资源更有用。如果看完这篇,你想动手试试,就从读取第一批原始文档、写第一行清洗代码开始。跑起来,然后改进。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/1 19:28:37

Unity切割模型实战:从Mesh切割到凸包封口与性能优化

简介&#xff1a;这份Unity切割模型案例面向游戏引擎初学者与希望掌握物理交互的开发者&#xff0c;围绕“模型切割”这一常见需求&#xff0c;提供可运行的实践项目。案例重点讲解碰撞检测、鼠标左键蓄力与右键触发切割的交互逻辑&#xff0c;以及通过修改Mesh顶点与索引数据实…

作者头像 李华
网站建设 2026/10/1 19:28:21

MySQL事务与索引实战:从原理到排障的完整指南

1. 把事务和索引拆开看&#xff1a;它们到底在解决什么问题先讲个我在实际项目中遇到的场景。去年帮朋友排查一个电商后台的订单接口&#xff0c;用户下单后页面一直转圈&#xff0c;数据库CPU直接飙到100%。查了半天&#xff0c;发现是两个程序员写代码时对同一张订单表做了不…

作者头像 李华
网站建设 2026/10/1 19:28:17

从零入门AI工程:环境搭建、训练部署与监控的完整实战路线

如果你也打算从零开始搞AI工程&#xff0c;我先劝你想清楚一件事&#xff1a;AI工程和你平时看的算法教程、Kaggle比赛完全是两码事。比赛只要一个精度数字&#xff0c;工程要的是稳定、可复现、能维护、能上线的一套体系。我把自己从只写过几个玩具模型&#xff0c;到能正经跑…

作者头像 李华
网站建设 2026/10/1 19:28:12

本地AI硬件选购指南:显存容量与模型匹配的底层逻辑

1. 本地AI硬件选购的底层逻辑&#xff1a;为什么显存是第一道门槛1.1 显存、算力与模型参数的真实关系很多人第一次接触本地AI部署&#xff0c;脑子里想的都是“我买张最强的卡就行了”。但实际折腾过几轮之后你会发现&#xff0c;本地AI硬件选购这件事&#xff0c;显存容量比纯…

作者头像 李华
网站建设 2026/10/1 19:27:50

ECharts visualMap 视觉映射实战:连续型、分段型与地图着色

1. visualMap到底在干什么&#xff1a;先破一个最常见的误解刚接触 ECharts 的人&#xff0c;十有八九会把visualMap当成"图例"来用&#xff0c;配置完发现颜色没变、数据全是一个色&#xff0c;然后开始怀疑人生。这个组件在官方文档里的定位是视觉映射组件&#xf…

作者头像 李华
网站建设 2026/10/1 19:27:28

Windows 10 定时开关机:任务计划程序与 BIOS RTC 闹钟

给一台 Windows 10 机器配上定时开关机&#xff0c;我见过太多人第一步就走错方向——先去应用商店搜一个"自动关机助手"装上&#xff0c;用两天发现只能关机、不能开机&#xff0c;卸掉之后又开始怀疑是不是系统版本不对。其实原因一点都不复杂&#xff1a;电脑一旦…

作者头像 李华