
“ai-engineering-from-scratch”这个标题字面意思是“从零开始学 AI 工程”。我最早是在 GitHub 上看到这个仓库名后来发现它更像一类学习路径的概括。很多人一上来就找现成的 RAG 模板、复制智能客服代码部署半天 Demo 能跑一换真实业务数据就崩。原因很简单AI 工程的复杂度根本不在“调用模型”而在模型之外那一整圈环节包括数据处理、检索链路、上下文组装、效果评估、服务化部署和持续迭代。这篇文章就用我自己的实操经历聊聊从零起步搭建一套可用、可评估、可迭代的 AI 应用系统到底要经过哪些关卡。适合想系统入门的开发者也适合已经做了几个 AI 项目但总觉得很碎片、没有章法的同学。我不写概念堆砌只写能落地的步骤、参数和避坑经验。1. 先想清楚什么是“从零开始做 AI 工程”1.1 AI 工程不是调接口而是一条完整链路我见过不少朋友把“AI 工程”理解成“调用大模型接口”。这个认知在小 Demo 里没问题但一旦进入真实场景问题就冒出来了知识库在哪、怎么切分、怎么检索、怎么控制模型不乱说、怎么判断新版本比旧版本好、怎么保证接口延迟稳定、上下文一长成本怎么控制。这些才是工程问题也是 ai-engineering 的核心。一句话概括AI 工程是把模型能力包装成可靠业务系统的过程。它包含数据采集与清洗、向量化与存储、检索排序、提示词组装、模型推理、结果评估、日志监控、成本治理这条完整链路。任何一个环节薄弱整体效果都会垮。这就是为什么很多团队直接拿 LangChain 拼一个 Demo 很容易可上线一两个月后就被各种边界问题折磨。1.2 为什么我建议从最小闭环起步刚开始做自己的 AI 项目时我也犯过错。第一次做知识库问答系统直接搬了一整套成熟框架界面还没写好先被框架里一堆抽象概念搞懵了。后来我换了个思路不依赖重型框架先用 Python 手写一个最小闭环把“文档进、答案出”的每个中间步骤都拆开看清楚。从零起步的真正价值不是造轮子而是逼你理解每一层在干什么。就拿最简单的检索增强生成RAG来说你只有亲手算过余弦相似度才会明白为什么切块大小影响效果只有手动组装过上下文才知道 token 是怎么被消耗的只有自己写过评估脚本才懂得为什么“看起来回答变好了”不一定是真的好。所以我的建议是第一版不要追求架构完整追求链路完整。先跑通一条能够记录输入输出、能够改参数重新运行、能够做对比评估的极简链路再逐步加复杂组件上去。2. 最小可用的技术栈与环境搭建2.1 我选型的原则先能用再优化技术选型是最容易纠结的地方。今天有人推荐向量数据库明天有人推荐编排框架后天又有人说要上微服务。我的原则很简单第一版能用最少工具跑通所有组件都能被替换。所谓能用指的是代码量少、调试直观、依赖简单。如果你对 Python 有一定基础那么 FastAPI、本地 embedding 模型和 NumPy 就够起步了。不需要一上来就上 Kubernetes也不需要配十几个中间件。先让链路完整再考虑性能、高可用和扩展性。我自己的第一版甚至没有用向量数据库只用了一个 JSON 文件加内存列表几千条知识片段检索照样能跑。后面数据量上来了再平滑迁移到专门的向量存储。这个迁移过程反而很顺畅因为我当时的代码把“向量计算”和“向量存储”拆开了替换存储不影响检索逻辑。2.2 最小技术栈清单与选型理由模块我的选择为什么这么选开发语言Python 3.10AI 生态最成熟调试速度快写脚本和写服务都方便服务框架FastAPI自带参数校验和接口文档写一个 AI 接口非常快向量化本地 embedding 模型如 BGE 系列或通用向量接口本地模型方便调试接口方式适合快速验证向量存储NumPy 数组 JSON 文件起步数据量小时足够用零运维成本任务编排手写顺序逻辑 / 简单函数管线避免框架屏蔽细节出现问题容易定位容器化Docker Compose保证环境一致后续迁移部署不踩坑这套组合最大的优势是任何一部分出问题你都能直接看到代码、改代码、重跑结果。框架的抽象越少排查问题的路径就越短。2.3 环境搭建与项目结构建议我建议从一开始就把项目结构按功能分目录别把所有代码塞进一个 main.py。一个最小可维护的结构大概是这样ai-engineering-from-scratch/ ├── data/ │ ├── raw/ # 原始文档 │ ├── processed/ # 清洗后的文本 │ └── chunks/ # 切分后的知识片段 ├── src/ │ ├── embed.py # 向量化 │ ├── store.py # 存储与检索 │ ├── prompt.py # 提示词组装 │ ├── evaluate.py # 效果评估 │ └── api.py # 服务接口 ├── scripts/ │ ├── ingest.py # 数据处理流程 │ └── test_search.py ├── eval_set/ │ └── qa_pairs.jsonl └── requirements.txt看起来简单但结构背后是清晰的边界数据管数据、向量管向量、提示词管提示词、评估管评估。这种边界感是 AI 工程里最重要的习惯。环境层面我的经验是开发环境用 venv 就够了项目级依赖用 requirements.txt 固定版本。部署环节用一个 Dockerfile 把环境和代码一起打包避免“在我机器上是好的”这种尴尬。模型文件如果使用本地模型注意挂载到容器外部目录否则每次重新构建都要重新下载。3. 核心环节实操拆解3.1 数据准备切块策略直接决定效果上限数据准备是 AI 工程里最不性感但最影响效果的部分。我做过一个企业知识库项目文档是几十份不同格式的培训材料有 PDF、Word 和网页导出件。PDF 直接提取文本经常出现乱序和断行尤其是有表格和页眉页脚的内容Word 文档里有些“修订批注”也被提取出来污染了知识库。我最后花了两天时间处理数据格式PDF 用解析库提取文本后做规则清理把页眉页脚、水印、无用符号去掉Word 先转成纯文本再按标题结构切分网页内容只保留正文区域。这些脏数据如果不前置处理后续检索和生成都会受到连带影响。切块策略是另一个重点。块太小单条信息不完整检索到了但回答缺上下文块太大噪声太多相似度被无关内容稀释。我常规的起步参数是块大小 500 到 800 字重叠 50 到 100 字。普通科普类文档用 600 字比较多技术手册类条目化明显可以压到 300 字左右。没有绝对标准必须拿真实检索结果去调。提示切块时尽量保持段落和标题的完整性不要在一句话中间硬切。简单的做法是优先按段落切段落太长再按句子边界补充切分。这一条看着不起眼实际很影响检索命中率。3.2 检索增强生成的极简实现不靠框架也能跑通很多人问我要不要直接用 LangChain。我不排斥框架但强烈建议先手写一遍核心检索逻辑哪怕只是几百行。因为只有手写你才能理解框架帮你做的事情是什么。我第一版 RAG 的检索部分甚至只有三个函数向量化、相似度计算、取 Top-K。embedding 我用的是本地模型把知识片段全部向量化之后存成 NumPy 矩阵。查询时把用户问题向量化然后计算余弦相似度。import numpy as np def cosine_similarity(vec_a, vec_b): dot np.dot(vec_a, vec_b) norm_a np.linalg.norm(vec_a) norm_b np.linalg.norm(vec_b) if norm_a 0 or norm_b 0: return 0.0 return dot / (norm_a * norm_b) def search(query_vector, chunk_vectors, top_k3): scores [cosine_similarity(query_vector, vec) for vec in chunk_vectors] top_indices np.argsort(scores)[-top_k:][::-1] return top_indices, [scores[i] for i in top_indices]这段代码本身很简单但跑通以后你会自然理解几个关键点向量维度是多少、全表遍历有什么瓶颈、Top-K 的 K 取多少合适、为什么同一问题换个写法检索结果会不同。这些都是直接用框架时很难感知的。在实际项目中我后来把检索从纯向量召回升级成了“向量召回 关键词召回 重排序”。原因是很多专业名词、缩写、产品编号在向量空间里未必能通过语义关联到而关键词检索在精确匹配方面更强。把两种结果合并再去掉重复已经能解决大部分召回不足的问题。还可以再接一个重排序模型用小模型对候选结果做二次打分进一步优化排序质量。3.3 提示词与上下文组装让模型稳定输出的关键很多人在提示词上疯狂调试其实真正的问题出在上下文组织上。我把上下文组装分成三层系统提示、参考材料、用户问题。系统提示负责设定角色和行为边界我常用的写法是明确告诉模型“只根据提供的参考材料回答如果材料中没有答案直接说明不知道”这类约束。参考材料是检索回来的知识片段需要按相关度排序拼接并在每段前面标注来源编号。用户问题保持原样传入除非需要改写后再检索。一个容易被忽略的细节参考材料里的噪声会直接干扰生成质量。如果检索结果中有明显不相关的片段不要硬塞进上下文。我在评估阶段发现加入两条无关片段后模型答错的概率明显上升。所以宁可只喂一条高质量片段也不要为了显得信息丰富而堆入大量无关内容。另一个细节是 token 消耗。每次请求都会把系统提示、参考材料、历史对话全部算进去。如果上下文越堆越长不只是成本上升模型反而会因为信息过载而“忽略”关键内容。我后来给上下文加了一个上限最多放 5 条片段每条最多 400 字。超出部分直接截断优先保留最相关的片段。这个简单策略在线开销和回答质量之间取得了不错的平衡。3.4 效果评估没有评价指标的 AI 工程都是玄学这一点是我最想强调的。如果每次改完提示词只靠人工看两三个例子判断“好像更好了”这个项目永远不会稳定。AI 工程必须引入评估集和量化指标。我建议准备一份黄金评估集格式非常简单每行是一条 JSON 记录包含输入问题、期望的知识点关键词、可选的标准答案。规模不用很大一百到两百条覆盖主要场景就够了。每次改动后在同一个评估集上跑一遍记录三个指标检索命中率、生成答案的相关性、答案正确率。检索命中率看的是 Top-K 结果里有没有出现正确片段相关性可以人工打分也可以用另一个更强的模型来打分正确率用于判断最终回答是否满足要求。我实际使用中字符串匹配这种简单方式只适合检测“是否包含某个关键词”真正的语义正确性必须靠人工或模型评测。# eval_set 示例 {question: 产品的试用期是多久, keywords: [30天, 免费试用], reference_chunk_id: chunk_0042}注意用模型评测另一个模型时要定义明确的评价标准和打分区间比如 1 到 5 分分别代表什么。同一个问题最好采样两到三次取平均否则随机性会让评估结果失真。这个环节还有一个额外好处评估集就是一个回归测试集。当你指向一个新的 prompt 优化方向发现整体分数下降时就能立刻回滚不用靠感觉。3.5 部署与服务化从脚本到可用的 API本地跑通脚本和对外提供稳定服务之间还差一层封装。我用 FastAPI 做一个极简接口把检索和生成串起来暴露成标准的 POST 接口。from fastapi import FastAPI, HTTPException from pydantic import BaseModel app FastAPI() class QueryRequest(BaseModel): query: str top_k: int 3 class QueryResponse(BaseModel): answer: str sources: list[str] app.post(/query, response_modelQueryResponse) def query_endpoint(request: QueryRequest): try: result run_pipeline(request.query, top_krequest.top_k) return QueryResponse(answerresult[answer], sourcesresult[sources]) except Exception as e: raise HTTPException(status_code500, detailstr(e))这里有一个非常实用的经验流式输出。如果大模型推理时间较长用户等待固定响应会很难受。改成 SSE 流式输出后首字延迟从几秒降到几百毫秒的体感能极大改善用户体验。虽然代码多了一些但绝对值得。并发控制也不能忽略。每个请求都要占用显存或带宽资源如果不对并发做限制一个热门查询就能把服务打挂。我的做法是给模型推理服务加一个请求队列控制最大并发数为 4 或 8超出部分排队处理。再加一层简单的缓存相同或相似问题直接返回历史答案效果非常明显。4. 实操过程中最常见的六个坑4.1 数据解析阶段格式与编码的隐形杀手第一个坑出现在数据解析阶段。PDF 表格内容提取出来经常是乱序文本指望它直接进入知识库等于埋雷。另一个坑是字符编码某些老系统导出的 Word 或 TXT 文件是 GBK 编码直接按 UTF-8 读取会出现一堆乱码。我的处理顺序是先统一转成 UTF-8再按文件类型分别解析最后做一轮正则清理把多余空白、页眉页脚、特殊符号全部过滤掉。如果知识库涉及用户上传的文档还要考虑文件名、路径里的中文编码问题。我遇到过 Linux 部署环境下文件名中文乱码导致读取失败的情况后来统一规范成 hash 文件名原文件名只存元数据彻底避免了这个坑。4.2 检索阶段向量检索命中低、结果不稳定检索阶段最常见的坑是向量化模型选型和数据领域不匹配。通用embedding模型对垂直领域术语的理解可能不够这时候要么换领域微调的 embedding 模型要么用混合检索补足。我做过一个法律文档问答发现纯向量检索对法条编号的匹配极不稳定改成“关键词完全匹配权重调高 向量语义召回”之后命中率明显提升。还有一个低级的坑向量没有做归一化就直接算余弦相似度。其实很多向量接口输出的向量默认不是单位向量手动归一化后再存入矩阵计算逻辑更稳也方便后续直接不用再除模长能省一点计算时间。4.3 生成阶段模型“太有主见”与“太听话”生成阶段的坑有两端。一端是模型太有主见参考材料里没有的内容它也自行补全然后在答案里出现编造信息。另一端是模型太听话参考材料里哪怕有明显的错别字或错误数据它也照抄。我常用的对策是在系统提示里加一句“根据参考材料回答材料未提及的信息请明确说明”同时在生成后加一轮简单的规则校验检查是否引用了参考材料中的关键句。如果你用的是开放对话模型还要注意模型可能把历史对话中的信息当成新知识。我会在每次请求时控制是否有历史记录如果没有历史上下文就明确告诉模型不要编造用户信息。4.4 服务化阶段并发上不去、响应太慢服务化阶段的问题往往不是模型本身而是架构。第一次上线时我把模型加载、向量检索、生成逻辑全部放在同一个进程里结果两个并发请求一进来CPU 和显存飙满接口大面积超时。后来拆成两部分入口服务和推理服务分离推理服务单独管理并发。入口服务负责检索、组装上下文再通过内部接口调用推理服务。这样任何一部分抖动都能单独定位和扩容。流式输出这里再提一次不用流式时用户看到的总响应时间等于完整生成时间用流式后用户感受到的等待时间大大缩短。配合前端打字机效果体验提升非常明显。4.5 成本与性能的平衡能用小模型不上大模型成本失控是最容易被忽视的工程问题。我见过一个项目每天调用量不大但每次都把长篇参考文档塞给大模型一个月的 token 费用高得离谱。优化思路有几个优先用小模型处理简单任务只有复杂推理才调用大模型缓存高频查询压缩上下文长度。我把这三板斧用上后成本降到了之前的五分之一效果基本没变。4.6 评估与回归的坑人工验证掩盖了真实波动最后一个坑是评估样本太少。只看三五个例子就宣布某个 prompt 更好这是自欺欺人。模型输出有随机性同一个 prompt 连续问两次都可能不同。我的建议是评估集至少五十条每次修改跑完整评估并且对关键结果做多次采样取多数答案。这不是浪费时间这是在给项目建立依赖的数据基线。5. 常见问题速查表与避坑清单现象常见原因排查与解法检索结果完全不相关切块过大、向量模型不匹配调整块大小启用混合检索可选换模型答案包含编造信息缺少约束提示、上下文不足强化系统提示要求“无据不答”增加片段来源同一个问题答案不稳定温度过高、未设置随机种子降低 temperature多次采样取多数接口响应太慢同步生成、并发未控制改流式输出拆分推理服务加缓存费用快速上涨上下文过长、高频调用未缓存截断上下文复用结果小模型分流知识点更新后不生效嵌入库未重建、缓存未清理重建向量索引设置缓存失效策略特定格式文档解析乱码编码问题、PDF表格乱序统一 UTF-8分类型解析规则清理这份速查表不是一次性生成的而是我在多个项目里不断积累的。遇到新问题第一反应应该是把它记录下来并归因到链路的具体环节而不是头痛医头地乱调参数。6. 最后的经验把系统拆到足够细才能走得足够远写到这里我最有感触的一点是从零开始做 AI 工程最难的不是学会某一个工具而是建立“拆解黑暗”的能力。你面对一个不能稳定输出的系统时如果只能看到“模型回答不对”那就无从下手而当你把链路拆成数据、切块、向量化、检索、排序、上下文、提示词、生成、评估九段每一段都能单独输入、单独输出、单独测指标那问题就变成查表定位剩下的只是耐心调优。我在实际项目中养成了一个习惯每次改动只改一个变量。改 prompt 就别动切块参数动了切块参数就不要同时换 embedding 模型。否则结果变好了不知道是哪一步起了作用变差了也不知道该回滚哪一层。这个习惯看起来保守但在 AI 系统的混沌和随机性面前它反而是最稳妥的推进方式。另外建议尽早把评估集建立起来哪怕只有二十条。我前面几个项目都是先写功能后补评估导致后期优化时没有基线很多改动没法确认是否真的有效。等你在 AI 工程这条路上走了一段你会发现自己最值钱的资产不是某一个模型或框架而是那套完整的实验流程和数据基线。这套方法后续还可以继续扩展接入用户反馈自动标记难例把难例吸收回评估集逐步构建更高质量的训练数据甚至做更细粒度的 prompt 自适应调整。但这一切的前提都一样基础链路清晰、评估指标可信、迭代过程可控。从零开始恰恰是建立这些前提最扎实的路。