
我为什么要花半年时间做一个叫ai-engineering-from-scratch的项目如果你刷到这篇文章大概率和我半年前的状态差不多手里几个大模型API调得飞起RAG流程能跑通提示词模板攒了一堆但心里总觉得不踏实——模型为什么答错训练数据长什么样那个token到底是怎么切出来的Agent转几圈就失控到底是框架的锅还是我对机理理解不够我当时决定不装了。我给自己立了一个flag不再满足于“能调用”而是从头开始把AI工程这条路重新走一遍项目名就叫ai-engineering-from-scratch。这篇博客就是我的复盘。先说清楚我理解的“从零开始”是什么不是从线性代数课本啃起也不是从最新论文复现起而是把AI工程拆成我可掌控的最小闭环——我能理解最小模型的训练过程能做数据清洗和实验管理能把自己写的提示词当作正经代码来版本化能搭出带评估和观测的Agent工作流。到项目后期我甚至尝试了从零开始构建推理模型的最小验证。这篇文章适合所有想从“会用API”走向“能独立负责一个AI系统”的人也欢迎已经入行但觉得知识体系零散的工程师来对照。下面直接进入项目复盘按我实际推进的路径来写每个部分都包含能直接落地的细节和踩过的坑。1. 先把“AI工程”这个词拆开我要从零学会的到底是什么这个项目立项当天我在白板上写了一个问题AI工程师和调API的人差别到底在哪我当时给自己的答案有三层基本成了整个项目的导航图。1.1 三层能力理解模型、构建系统、建立工程纪律第一层是理解模型。不用理解到能推导transformer全部公式的程度但至少要知道token怎么切、注意力在干什么、训练和推理有什么不同、模型的能力边界由什么决定。这一层没打通后面所有的调优都是玄学。第二层是构建系统。单个模型调用只是系统里的一个组件。真实的AI项目还要考虑数据怎么进、上下文怎么组织、工具怎么调用、结果怎么评估、失败怎么兜底、在线怎么观测。这一层考验的是软件工程能力不是模型知识。第三层是工程纪律。我把这理解为prompt要版本化、实验要可复现、评测集要随bug增长、日志要能回溯。项目做得越久越发现模型能力决定系统的上限工程纪律决定下限。三层能力不是按顺序学完一关再进下一关的更像三条线并行推进。我每个阶段都会给自己出一个小项目把三层能力都裹进去做一遍。1.2 我给自己划定的三条边界线这个项目最大的风险不是内容太多而是方向漂移。我给自己定了三条边界实测很有用不追新算法先追运行机制。很多新东西跑起来本质还是那几件事数据处理、目标函数、解码策略、评估循环。最新论文可以等它沉淀三个月再回头看先把该懂的都抠明白。核心环节必须亲手实现一遍。分词器、注意力、训练循环、评测脚本这些环节不允许全部用现成库。自己实现一遍才知道别人封装的每一层都替你扛了什么坑。默认在本地资源跑通小规模实验特殊情况再上云。我只有一张桌面级显卡如果什么事都要靠大规模算力才能验证这个项目根本进行不下去。所以我会刻意选择“问题足够小但机制完整”的实验来做把结论迁移到大模型场景时保留一份谨慎。这三条边界让我的学习半径清晰了很多。接下来是我投入时间最多的第一部分从0到1构建一个最小模型。2. 从零构建最小LLMtoken和注意力这两个根基必须抠干净2.1 一个小型语言模型远比你想象的更容易跑起来我参考了《Build a Large Language Model from Scratch》这本书的思路但并不打算复刻GPT-2级别的大模型——我只想做一个参数规模在1500万到1亿之间、能在单卡上完成训练和推理的小型语言模型。我最终定下的配置如下词表大小约3万左右的中英混合BPE词表嵌入维度512注意力头数8transformer层数8上下文长度1024训练数据约5亿tokens 的混合语料公共数据集加我自己攒的领域文档这套配置跑一次完整的预训练在我那张显卡上大概需要两到三天。时间看起来有点久但当你盯着训练loss一格一格往下降并且亲自把推理代码写出来验证自己模型的输出时那种“我真把它造出来了”的感觉比调几百次API都值。核心训练循环其实就是这样的骨架for epoch in range(num_epochs): for batch in dataloader: logits, loss model(batch) optimizer.zero_grad() loss.backward() optimizer.step() if epoch % 5 0: print(fepoch {epoch}, loss {loss.item():.4f}) sample_text model.generate(max_new_tokens50) print(sample_text)就这么简单对训练循环就是这么简单。真正复杂和磨人的在数据和token处理这两个环节上。2.2 token与分词器大多数应用项目的盲区很多项目方讨论“模型能力不够”最后排查下来其实是分词器的问题。tokenization是整个模型理解文本的入口但它在工程里最容易被忽略。我用手写实现的方式做了一遍BPEByte Pair Encoding字节对编码。核心思想其实不复杂先把文本拆成单字符序列然后不断统计相邻两个token出现的频率把频率最高的pair合并成一个新token反复迭代直到词表达到目标大小。举个很生活化的例子假设你有一句话我喜欢打篮球一开始被拆成我 喜 欢 打 篮 球。如果语料里篮球经常一起出现BPE就会把(篮, 球)合并成篮球这个token。再往后打 篮球出现频率也高它们也可能被合并成打篮球。这样做的直接好处是模型可以用更少的token位置表达更多信息训练效率更高坏处是如果你的语料不够干净BPE会把错别字、奇怪符号也合并进去污染整个词表。实操中有两个经验值得分享中文场景不要直接用英文语料训练出来的分词器。中文字符密度高一个词可能被切成三个没意义的碎片。我自己用中英混合语料重新训练了一个词表效果立刻不一样。保存分词器时务必把原始训练状态一并记录。包括语料hash、merges数量、训练数据来源。否则三个月后模型迭代你根本不知道自己当时的token分布是基于什么数据得到的。2.3 注意力机制没有公式推导也能懂一家点餐的餐厅我从零实现注意力机制之前看了不下十篇教程最后发现最好用的类比是一家嘈杂的餐厅。QQuery是顾客手里的菜单需求“我想吃宫保鸡丁”KKey是厨房展示的菜牌“本店有宫保鸡丁”VValue是厨师端出来的菜本身注意力计算做的就是一件事让Q去匹配所有K算出“这道菜应不应该上桌”的权重再按权重把对应的V组合起来。在模型的语境里每个token都会同时扮演Q、K、V三种角色——它在看你的时候也在被你看最后输出的是所有token信息加权融合后的表示。因果注意力的区别只是加了一个“不能偷看未来”的规则第 i 个token计算注意力时只能看前 i 个token不允许看到后面的内容。这就是为什么语言模型能一个token接一个token地生成。def causal_attention(q, k, v, mask): scores q k.transpose(-2, -1) / (k.shape[-1] ** 0.5) scores scores.masked_fill(mask 0, float(-inf)) weights torch.softmax(scores, dim-1) output weights v return output这段代码只有五行但它是整个transformer大厦的地基。我手动实现过一次之后再去理解KV缓存、多头注意力和各种推理加速技巧全都变得顺理成章。2.4 “推理模型”不是玄学从预测下一词到组织推理链条项目进行到第四个月时各种开源推理模型开始密集出现。这时候热搜词里“build a reasoning model from scratch”也频繁冒出来我顺势给自己的项目加了一个挑战能不能用一个小模型验证“推理能力”是怎么被训练出来的。这里要先区分一个基础概念普通语言模型的目标是预测下一个token而推理模型的目标是生成一条完整、可验证的思考链条然后再给出答案。从工程角度看推理能力的本质不是模型结构发生了革命性改变而是训练方式变了。我实践下来的简化版路径是这样的先在推理数据集上做有监督微调。数据集里的每一条都包含“问题 - 推理步骤 - 最终答案”让模型学会“先思考后回答”的格式。再引入可验证奖励。我写了一个非常简单的评分函数只判断最终答案格式是否合法、结果是否正确正确就给加分不对就不给。用强化学习策略调整模型输出分布让模型自己倾向于生成“更容易获得正确奖励”的推理路径。我刻意控制这个实验的规模参数量只有2亿左右奖励函数也极其简化。但结果让我非常满意——模型在解题类任务上的准确率明显高于不做推理微调的对照组而且它会自己生成“让我分步思考”这类结构化的中间文本。这项实验给我的核心启发是推理能力的构建功劳主要在数据组织与训练策略上不在什么神秘结构上。后续再看到任何“推理神器”的宣传我心里就有底了先问它的训练数据怎么来奖励函数怎么定义。3. 数据与训练工程从“能训练”到“玩得转实验”如果说模型结构决定了算法的上限那数据质量就决定了实际效果的下限。这个项目做到一半时我把重心从“把模型跑起来”转移到了“把数据管明白”因为模型的每次能力提升几乎都能追溯到一次数据策略的调整。3.1 决定成败的数据配比与清洗预训练阶段我用了三类来源完全不同的数据配比如下数据来源占比作用清洗重点公开网页语料70%学习通用语言结构和世界知识去重、去噪、语言识别代码与结构化文档20%提升逻辑性和码能力过滤低质量代码领域论文与教材10%注入深度领域表达格式清洗、图表文本过滤清洗流程我踩过几个很实际的坑。第一是去重必须用MinHash这类近似去重方法哪怕只做最粗的一版都能显著降低后期“模型复读同一段话”的问题。第二是污染检测尤其要检查训练数据和测试集之间有没有重叠——我之前一次评测分数虚高查了半天发现是训练集里有一篇测试题目的原文差点被那个假指标骗过去。第三是用困惑度做质量过滤在通用模型上计算每条候选文本的困惑度分数异常高的大概率是乱码或非自然语言直接扔掉。这一套流程走完之后我对AI图片生成原理里那些“数据筛选”的讨论也更容易理解了。扩散模型和语言模型在数据基建上其实共享同一套逻辑数据进去之前不把关生成结果里什么奇奇怪怪的东西都可能出来。3.2 实验台账把训练当作研究课题来管理我做过最后悔的一件事是前几轮训练没有认真记账。超参数改了一版数据又换了一种最后模型效果提升了却完全说不清楚是哪一个改动起了作用。之后我给项目建了一个专门的实验台账每个实验至少记录以下字段实验编号与目标假设训练数据来源、版本号、shuffle随机种子模型配置层数、维度、头数、上下文长度超参数学习率、batch size、warmup步数、梯度裁剪关键曲线截图训练loss、验证loss、learning rate变化推理效果样例与失败案例我用wandb做曲线追踪同时把台账直接以Markdown文件放进代码仓库。这样一来每次迭代都有迹可循后续写模型卡或者复盘报告时也能直接引用。3.3 没有大规模算力时的三条跑法这是读者问得最多的一个问题我没那么多卡能做这些实验吗我的答案是能但你要学会选实验。具体有三条经验用“最小可复现体”验证机制。先不说训练完整模型你先拿一层attention、一万条数据跑通整个链路确认代码和流程正确再放大规模。卡不够就用“逐步续训”。不需要一次把5亿tokens全灌进去你可以分阶段训练每个阶段只学一部分数据分布中间记录checkpoint。这样随时可以停止回头看阶段的曲线。云资源也要花在刀刃上。日常迭代一律本地跑只有确认需要做大模型基线对比时才租卡。造出来的时间成本比省的钱更重要。算力有限的人犯的最大错误不是实验做得小而是为了弥补算力差距去“抄别人的全套配置”最后连自己实验里的变量都控制不住。4. 从模型到产品提示工程、上下文工程与AI工作流的落地训练完自己的小模型后我并没有立刻把它推到生产环境而是回头审视了我日常用的那些大模型API调用方式。一个清晰的结论冒了出来应用侧的AI工程其实不是“写提示词”而是“用工程方法管理模型的不确定性”。4.1 提示工程是软件工程不是文案写作很多教程把提示工程说得像文案课会写几个技巧句式就算会了。但我在项目里把提示词当成代码来管理之后整个系统的可靠性立刻提高了一截。我总结了三个必须做到的纪律提示词必须版本化。每次修改都要走和改代码一样的评审流程提交信息里写清楚“这次改动为了修复什么case”。提示词模板和业务逻辑分离。模板文件里只放指令框架具体变量在运行时填充这样既方便维护也方便测试。每个提示词要配一组回归样本。你修改提示词后必须能快速验证它在过去三个月里的所有失败案例上有没有变好或变坏。我经常看到某团队把prompt直接写在代码里出bug后整个服务回滚都很麻烦。这就像把SQL散落在业务代码中的味道一样短期爽长期痛。4.2 上下文工程把最合适的信息放进模型的视野模型一次能看的token有限而真实业务中需要的信息量往往远超这个上限。所以上下文工程的核心任务就是如何在有限的上下文窗口里放入对当前任务最有价值的信息。我搭过一版完整的RAG检索增强生成系统流程不复杂文档切块 → 向量化入库 → 用户提问 → 检索Top-K → 重排 → 拼入提示词 → 模型生成。中间最容易被低估的一步是重排。向量检索召回的前几十条里真正相关可能只有三四条不重排直接全部塞进上下文模型很容易被无关信息干扰甚至开始引用错误内容。我加了一个简单的重排模型之后回答的引用准确率明显上升。一个好的上下文工程还要处理“该忽略什么”。有些信息放在上下文里不叫支持叫噪音。如果你发现模型“越给资料越答非所问”先别急着换模型去检查上下文里的噪声比例。4.3 一条真实的排查链路客服问答机器人为什么越答越离谱这是我实际经手过的一个改造案例特别适合用来理解上下文工程的价值。原系统的表现是用户问“外地来上海工作社保怎么转移”机器人答了一堆“建议咨询当地社保局”之外还把“公积金提取条件”张冠李戴到了社保政策上。表面看是模型知识不足但我排查后发现根因有三层检索模块召回了一批高度相似但并非针对该问题的文档片段重排环节缺失。上下文里塞了过多文档内容真正的关键条款被淹没。提示词没有强制要求“只能基于上下文回答”模型自由发挥的空间太大。我当时的修改方案是加入重排环节只保留高相关度的文档片段在提示词里明确写入“回答必须引用上下文中出现的文档编号找不到依据时直接回答不知道”在检测到检索分数低于阈值时从“模型直接答”切换为“给出资料线索并转人工”。改了这三处之后同样的模型回答准确率肉眼可见地提升。这件事再次印证了我前面那句话提升AI产品效果很多时候不靠换更强的模型而靠把系统各个环节的约束补上。4.4 AI工作流把一次性调用串成稳定流水线除了单点调用我也尝试把多个环节串成一个AI工作流。比如我这篇文章所在的项目里有一个“技术文档周报生成”的小系统流程是用模型对一周内的技术资料做粗筛提取候选主题用规则脚本剔除内部消息、重复内容用模型对每条候选内容生成摘要最后再由人工审核把关后发布这种简单链条几乎不需要复杂的编排框架我用几十行Python脚本就能串起来。只有当流程里出现条件分支、循环回溯、多Agent协作时再考虑引入专门的编排工具。很多团队一上来就上重型框架最后反而被框架自身的复杂度拖垮。需要注意的是AI工作流里的每一步都要有“失败出口”。模型摘要抽风了怎么办脚本过滤误伤了好内容怎么办设计流程时先问自己“出错了会不会砸到用户”再考虑“怎么让它更智能”。5. 让模型动起来AI代理循环的搭建与踩坑完成了“模型上下文工作流”这套组合后我开始挑战更有意思的部分让模型拥有工具自主完成多步任务也就是广义上的AI Agent。5.1 代理的本质不是一个模型而是一个“循环”很多人理解Agent时把它想象成一个更聪明的模型。其实代理的本质是一个控制系统循环模型负责思考和决策外围的工程代码负责行动和反馈。一个最小可用的Agent循环大概是这样的控制器通常是LLM接收目标并决定下一步做什么工具层提供可执行能力比如查数据库、发请求、执行代码、读文件记忆模块记录中间结果与历史状态评估器判断任务是否完成若未完成则继续循环。这个循环结构对应到代码上比单次调用多了两件东西一是“决定下一步”的动作类型二是把环境反馈继续塞回下一次调用的机制。最简单的写法就是这个骨架messages [{role: system, content: system_prompt}] while not task_done: response llm.chat(messages [state]) action parse_action(response) result execute_tool(action) messages.append({role: tool, content: result}) task_done evaluator.check(state, result)如果你能理解这个循环你就能理解为什么Agent会“失控”——因为循环一旦没有一个可靠的终止条件它会一直尝试、一直生成、一直调用工具。5.2 工具调用的工程细节约束比聪明更重要工具调用是Agent项目里工程含量最高的地方而这部分业界现在常提“harness engineering”这个词意思就是给AI套上缰绳让它安全、规范地使用工具。我在项目里踩过的坑主要集中在三块结构化输出不稳定。早期让模型直接输出JSON格式结果偶尔多一个逗号整个解析就崩。后来改用了约束解码或者函数调用协议让模型在预定义的schema里填参数崩溃率大幅下降。工具定义不清晰。工具描述写得太含糊模型就会用错参数。后来我把每个工具的描述都改成“输入是什么、输出是什么、什么情况下用、什么情况下别用”的四段式模型判断准确率明显提高。缺少超时和幂等设计。工具执行要是卡住整个循环就卡死。真实的工程化做法是每个工具调用都要有超时上限并且工具本身最好幂等同一输入重复执行不会造成副作用。这些都是非模型因素但它们决定了一个Agent系统能不能真正上线。5.3 防止代理失控的三道防线我对自己搭的Agent实验有一套固定的安全配置写在这里供参考第一道防线权限最小化。Agent能调用的工具权限一律按“最小够用”原则设计绝不给整个系统的超级权限。它只能访问它该访问的目录、库、接口。第二道防线单任务步数预算。我会在循环里设置最大步数到点强制停止并返回当前中间结果。没有步数上限的Agent要么烧完你的token预算要么陷入死循环。第三道防线人工确认点。涉及对外发送请求、写入数据、执行不可逆操作等动作必须经过人工确认。不要迷信“全自动”尤其在早期阶段。做完这些防御之后我才敢让Agent去处理一些边界清晰、风险可控的子任务比如整理数据、批量生成初稿、做代码审查辅助。我的建议是先把它做成一个可靠的“提效工兵”再谈“全自主”别一上来就追求科幻电影里的效果。6. AI测试开发与质量保障让系统可靠的那80%工作量一个AI系统从“demo能跑”到“生产可用”中间的差距基本全在测试和质量保障上。这个项目的后半程我基本在啃这块硬骨头。6.1 AI系统为什么不能只用单元测试衡量传统软件工程的单元测试依赖精确断言输入f函数输出必须是y。但AI系统的输出天然有分布性——同一个问题模型今天和明天的回答可能措辞不同意思却一样。你要是拿传统断言去测永远会在“错别字导致测试失败”这种事情上浪费时间。我给自己定义了三层AI测试格式层测试输出必须是合法JSON、必须包含指定字段、长度不能超限。这种测试完全自动也是最基本的守门员。规则层测试输出必须通过一些硬性规则比如不得包含敏感词、不得引用不存在的文档编号、不得给出“我可以帮你非法访问”这类应答。语义层测试需要利用另一套模型或人工评价器来判分看回答是否与参考答案语义一致、关键点是否齐全、逻辑是否顺畅。6.2 回归评测集是AI工程的第一纪律整个项目中对我帮助最大的习惯是建立了一个持续增长的回归评测集。规则很简单每次线上出了问题修复之后把这个问题作为一个新样本永久加入评测集。三个月后我再改任何prompt或模型配置只需要跑一遍回归集立刻知道有没有把旧问题改坏。我给每个评测样本设计了评分卡字段差不多是这样样本ID输入问题期望要点允许的边界当前评分关联修复记录QA-001社保转移的条件必须提到缴费年限和流程措辞可以不同要点不能缺失0.95修复#17增加检索重排这个习惯的威力是复利式的——最开始只有二十条样本看不出什么到项目后期积累了上百条每次改动模型或提示词回归集都能在五分钟内告诉我“这次改动安全还是危险”。做AI测试开发最忌讳的是“靠眼缘评估”随便拿几个问题问一遍感觉差不多了就上线。这种感觉评估在demo阶段没问题但要支撑持续迭代的产线没有回归集基本等于裸奔。6.3 从日志里长出可观测性测试覆盖的是“已知的问题”可观测性覆盖的是“未来的意外”。我在项目里建立了一套最朴素的日志与追踪机制每次模型调用都记录时间戳与请求ID输入提示词模板版本号模型输出与耗时token消耗与费用估算检索命中的文档ID和相关性分数用户反馈如果有日志我直接以JSON行格式写入文件简单、不依赖重型框架后续要做分析也很方便。有了这些日志之后我每周都会做一次失败样本review——把用户反馈差、检索置信度低、耗时长、token异常多的调用捞出来一条条过。很多系统优化的线索其实就藏在日志的角落。6.4 上线后的质量阀门最后说一下上线和迭代策略。我的做法是重要变更先灰度小流量观察若干天比较新旧版本的评测集分数和线上日志数据再决定全量还是回滚。灰度期间必须有“一键回滚”的机制别把模型更新做成一次不可逆的赌局。此外AI系统最好永远有一个“规则兜底层”。模型答不上来的时候转人工、给默认话术、抛提示都比硬着头皮生成一段可能出错的回答要安全。一个敢于说“不知道”的系统比一个永远自信但偶尔闯祸的系统可靠得多。7. 复盘我的从零路线图以及三条给后来者的真心建议文章写到这里项目还没有结束——ai-engineering-from-scratch目前依然在持续更新。但复盘阶段的结论已经足够清楚我想把它作为最后的干货分享出来。7.1 六个月四条路线我实际走通的路径大致可以压缩为四个阶段分享给同样准备从零开始的朋友做参考阶段一约3周理解token与注意力。手写一个最小分词器和注意力层务必自己实现不要只调库。阶段二约6周训练一个小语言模型。用数据清洗、分层训练、实验台账把一个小模型从零训出来体验完整的训练循环。阶段三约6周研究应用层工程。做提示词版本化、RAG系统、AI工作流串联把“不确定性管理”刻进DNA。阶段四持续Agent与质量保障。搭一个最小Agent循环建立回归评测集和日志观测最后把它打磨成可安全上线的系统。这四条路线不是串行的中间有大量来回穿插。但每个阶段都必须有一个“能跑的东西交出来”而不是只积累笔记。7.2 三条后入者建议第一把可复现样本当资产。你的评测集、你的失败case、你的实验台账是参与AI工程竞争最有价值的护城河。第二先做一个“不智能但可靠”的系统再追求智能。如果你的AI系统十次里有三次答非所问那它不是“智能”而是“不稳定”。先把边界管住把输出约束好再谈更强的能力。第三少刷论文目录多看自己的日志。最新的模型技巧永远追不完但你线上系统的失败模式是有限的。把这些失败模式一个个消灭掉你的系统就已经超过大多数停留在“demo阶段”的团队了。7.3 我还在每天更新的原因现在回头看ai-engineering-from-scratch这个项目名既是我半年学习路径的总结也是我日常工作的底层方法论。直到今天我仍然会定期把工作里遇到的失败案例丢回到那个最小模型上做对照实验也会用回归评测集给生产环境的每次提示词变更打分。这个习惯一旦建立就很难放弃——因为它让我对AI系统的可靠性有了真正的掌控感。如果你也想走这条路别急着买课也别急着租卡。先打开编辑器写一个最简单的分词器训一个最小的模型跑通一个最基础的Agent循环。当这些最小闭环一个一个在你手底下成型时你对AI工程的理解就已经不再是收藏夹里的文章和朋友圈里的截图了。