ai-engineering-from-scratch 这个项目名,乍一看像是某个 GitHub 上的学习清单,点进去无非是资源链接的堆叠。但我在把整条学习路径完整走了一遍之后想说的是:从零开始做 AI 工程,真正难的不是“没有资料”,而是“每一处选择都没有标准答案”。模型选哪个、知识库切多碎、检索用稠密还是稀疏、Prompt 怎么写才稳定、评测到底该测什么——这些问题的答案,跟在教程里看到的 demo 完全是两码事。
这篇文章是我把 ai-engineering-from-scratch 这个项目从想法落地为完整可运行工程的全过程记录。它是一套面向 AI 应用开发的起步路径,覆盖概念理解、工具选型、RAG 应用搭建、评测优化和上线部署前的问题排查。目标很直接:让你不只是“跑通 demo”,而是有能力独立完成一个能被真实使用的 AI 功能。适合正在转 AI 开发的 Python 工程师,也适合已经写了几个测试项目、但总觉得缺一块拼图的同学。
1. ai-engineering-from-scratch到底要解决什么问题
先说结论:这个项目解决的不是“怎么学会调用大模型”,而是“怎么把大模型放进一个真实的应用里还能正常运转”。这个区别,很多人是在上线前才发现的,因为调用 API 太容易了,难的是它周围的那些工程问题。
1.1 多数人学AI工程为什么卡在中途
我接触过不少想转 AI 工程方向的人,也看过很多自学打卡帖。大家的学习曲线几乎一模一样:前两周非常亢奋,因为调用 API 太简单了,几行代码就能出结果;第三周开始迷茫,因为发现自己只会照着官方文档写 example,一遇到真实场景就不知道怎么处理;第五周基本放弃,因为模型质量、响应速度、成本、稳定性这些问题堆在一起,完全不知道从哪里下手。
我观察到的根源,是市面上绝大多数教程都把“AI 应用”简化成了“调用模型”。但实际上,一个能在生产环境里跑起来的 AI 应用,90% 的代码跟模型推理根本没直接关系。数据清洗要做,索引要建,缓存要设计,接口要封装,日志要记录,评测要走回归,降级方案要准备。这些工程问题,才是 AI 工程的核心。跑通 demo 只证明了模型能力存在,而工程化要解决的是“稳定、可控、可维护、成本可接受”这一整套完全不同的指标。
1.2 项目设计的四条主线
所以我在设计 ai-engineering-from-scratch 的时候,就没打算按“模型科普 → 调 API → 完事”这个顺序走。整个项目围绕四条主线展开。
第一条线是模型能力边界。先搞清楚大模型擅长什么、不擅长什么。数字计算、精确匹配、长文本精读,这些场景看着像 LLM 的强项,实际用起来各有各的坑。第二条线是应用架构。从最简单的单轮调用开始,逐步叠加多轮对话、外部知识库、工具调用,每次只引入一个复杂度,避免一步到位把自己绕晕。第三条线是工程化细节。评测、监控、部署、迭代,每个环节都写了对应的脚本和文档,而不是停留在口头建议。第四条线是成本与性能。Token 消耗、首字延迟、并发上限,这些在生产环境里比模型精度更致命,很多新手完全意识不到它们的存在。
这四条线对应到项目里,就是几个明确分出的模块:core存放模型封装,rag存放检索流程,eval存放评测脚本,apps存放完整的示例应用。每个模块都配了 README,写清楚为什么这样设计、踩过哪些坑,而不是只贴代码。我认为这才是“工程”和“脚本”的分水岭——工程需要理由,脚本只需要结果。
1.3 这条路径适合谁、不适合谁
说实话,这个项目的门槛比想象中高。我建议读者至少要有 Python 基础,能看懂函数、类、装饰器,知道 HTTP 请求是怎么回事,最好还写过一两个小工具。如果你满足这些条件,那这条路径会非常有用——它会帮你弥补从“会写代码”到“能做 AI 应用”之间的那个断层。我见过不少人基础语法刚学完就来看这类内容,结果模型的输出格式、回调参数、embedding 维度这些概念全在空转,最后只能放弃,不是看不懂,是缺了前置的地基。
反过来,如果你已经独立完成过两三个有真实用户的 AI 应用,那这个项目里的很多设计决策,你可能已经在实践中摸过了。这种情况下我不建议从头刷,直接挑评测和调优部分看,重点对比一下你之前是怎么做质量保障的,有没有遗漏环节。这个项目最核心的价值区间,就是中间那段:帮你建立一套完整的工程思维,而不是堆知识。
2. 基础准备:概念、工具和模型选型
这一章是真正动手前的准备。我踩过的最大一个坑,就是工具还没选明白就开始写代码,结果写了一半发现方案行不通,回头重来,浪费了整整一周。
2.1 先别急着调API,把四个概念吃透
开始写代码之前,我认为必须先弄清楚四个概念:Token、Embedding、上下文窗口、检索增强生成(RAG)。这四个词会出现在你接触到的所有资料里,但大部分人只是混了个眼熟就跳过,导致后面每个决策都做得稀里糊涂。
Token 是模型的计费单位,也是理解成本的第一把钥匙。中文场景下一个 Token 大约对应 0.5 到 1 个汉字,具体取决于分词器。很多人以为便宜,真实跑起来才发现,一个每天一万次请求的应用,如果 Prompt 写得啰嗦、日志里全是重复调用,光输入费用就能吃掉一个小团队的全部预算。我建议所有人在做任何模型接入之前,先写一个统计 Token 用量的中间件,这是成本控制的第一步。
Embedding 是把文本变成向量的方法,解决的是语义相似度计算的问题。为什么不用关键词匹配?因为用户问“车坏了怎么办”,你知识库里写的是“车辆故障处理流程”,关键词重叠很少,但语义是相关的。Embedding 用向量距离把这个相关性量化出来,这是 RAG 的地基,不理解它,后面做检索优化就完全无从下手。
上下文窗口决定了模型单次能处理多少内容,这是最容易被忽略的约束。你要处理的文档可能很长,但模型窗口有限,这就要求我们在上游做切分、检索和摘要,而不是把什么都往模型里塞。RAG 则是把外部知识接入模型的标准方案——模型自身的知识有截止时间,也不了解你的业务,通过检索把相关片段拼进 Prompt,让模型基于给定材料回答,是目前可控性最好的落地方式。
这四个概念不用背定义,但要能回答“为什么”。我建议动手跑一个最小实验:拿五篇自己的文档,分别用关键词匹配和 Embedding 检索做对比。你亲眼看到两种结果差异的那一刻,比读十篇文章都管用。
2.2 工具链选型:什么值得上手就用
工具选型这块,我的原则是“默认选生态最成熟的,除非有硬理由换”。具体到项目里:
- 模型访问:优先用 OpenAI 兼容接口,因为不管是国内的模型服务还是开源框架,大多都支持这个协议。把模型调用封装在一个类里,后面换模型只改配置,不动业务代码。
- 向量存储:先本地用 FAISS,数据量大了再考虑 Milvus 或 pgvector。项目早期不要引入分布式组件,能把流程跑通比什么都重要。
- 应用框架:LangChain 可以学,但不要迷信。它最大的价值是提供了丰富的组件概念,最大的问题是抽象太厚,出了问题不好排查。我在项目里更多是参考它的设计思路,自己写轻量封装。
- 评测工具:RAGAS 可以用来做检索质量评估,但真正的主评测集一定要自己构建,因为只有你自己最清楚业务里的“正确答案”长什么样。
工具不是越多越好。我的实际经验是:一个项目的技术栈每多一个组件,排查问题的难度就增加一个量级。宁可多写几行代码,也不要引入一个不知道工作机制的黑盒。我见过有人为了“看起来专业”,上了向量数据库、上了编排框架、上了监控平台,结果应用本身只有 500 行代码,排查问题要跨四个系统,纯粹是给自己挖坑。
2.3 模型选型的三个真实场景
模型选型是另一个让人纠结的点。我的经验是分场景看,别问“哪个模型最强”,要问“我这个场景需要模型做到什么”。
场景一:通用问答、文案生成、代码辅助。这种任务直接选通用大模型就行,能力强、迭代快,不需要额外工程。场景二:基于专属知识库的问答。这种任务光靠模型本身不够,要用 RAG,所以选型重点变成了“指令遵循好不好、中文理解强不强、输出是否稳定”。场景三:结构化信息抽取、分类、格式化输出。这种任务对推理能力要求不高,但要求输出严格可控,所以重点测试 JSON 输出的成功率。
我把选型整理成了一张表,方便对照:
| 任务类型 | 首选方案 | 备选方案 | 关键指标 |
|---|---|---|---|
| 通用问答 | 旗舰通用模型 | 中端通用模型 | 生成质量、速度 |
| 私有知识库问答 | 通用模型+RAG | 微调+检索 | 召回质量、引用准确率 |
| 信息抽取/分类 | 中端模型+强约束 | 小模型+微调 | 输出格式合规率 |
| 数据增强/标注 | 旗舰模型 | 中端模型 | 一致性、成本 |
这张表解决不了所有问题,但至少能在你犹豫的时候提供一个思考框架。我自己在项目里用的是一套双模型策略:简单任务走轻量模型,复杂任务走旗舰模型,中间用路由逻辑做分流。实测成本大概能省 40%,效果几乎没有差别。路由逻辑也不用做得多聪明,先用几个关键词规则过滤掉明显的简单任务,再让模型自己判断难度,两步就够了。
3. 从零搭一个RAG应用:完整实操
聊完选型,我直接用一个具体的例子来展示整个工程怎么落地。这是我个人很推荐的一种学习方式:别跟着别人的项目做二次复刻,找一份自己熟悉的资料,从零建一个问答系统,然后把过程中遇到的问题记录下来。这才是真正属于自己的 ai-engineering-from-scratch。
3.1 需求与架构:先写一份失败的样例
先说一个反常识的做法:动手写代码之前,先构造一个“失败的场景”。什么意思?就是你想清楚:如果我的系统上线了,用户在什么情况下会获得糟糕的体验?把这个场景写下来,你的架构思路会清晰很多。
比如我要做一个基于公司运维手册的问答机器人。用户最常见的需求是:“我的服务起不来了怎么办?”、“这个报错是什么意思?”、“如何回滚版本?”。这些问题的挑战在于,故障描述往往很口语化,甚至包含错别字,但系统里的报错信息是英文的,两者之间几乎没有字面重合。如果检索只靠关键词匹配,基本一搜一个空。
因此架构设计上要分三层。接入层负责接收用户问题,做改写和意图分类;检索层负责从知识库定位相关内容,要同时支持关键词和语义两种方式;生成层负责组织答案,并对引用来源做校验。每一层都要有独立的服务接口,便于单独测试和替换。我在项目里最开始偷懒,三层全写在同一个脚本里,线上出了问题根本定位不到是检索挂了还是生成挂了,后来才老老实实拆开。
3.2 数据准备和切分的细节
数据是整个系统质量的上限。模型选得差可以换,数据如果不干净,换什么模型都救不回来。这是我在这个项目里最大的体会。
我的数据源是一批 Markdown 格式的运维文档,一共 200 多篇。处理流程分四步:第一步统一格式,把零散的txt、docx转成统一的 Markdown,顺便去掉无效字符;第二步清洗,删除空行、表格错位、图片说明等干扰内容;第三步按标题层级做结构化切分,保留文档的大纲结构;第四步给每段文本生成元数据,包括来源路径、原文标题、章节序号。
切分是这里面最需要花心思的。切太碎,模型的上下文里缺失前后文,回答容易断章取义;切太长,检索召回时浪费 Token,而且多主题混在一起降低相关性。我的做法是先按 Markdown 的二级/三级标题切块,再检查每个块的长度,超过 800 字就继续往下拆。同时为每个块保留“父标题”信息,在生成时把标题一起拼进 Prompt,模型能获得更多上下文线索。
def split_document(doc_text, max_chars=800): # 先按标题层级切块 blocks = split_by_headings(doc_text) result = [] for block in blocks: if len(block) > max_chars: # 超长块继续按段落切分 result.extend(split_by_paragraph(block, max_chars)) else: result.append(block) return result这里有几个我后来才悟到的细节:Markdown 里的表格要单独处理,不要把表头和表行拆散,否则检索到的一半内容毫无意义;代码块的注释信息也是重要检索内容,不能粗暴删掉,我一开始为了“省空间”把所有注释都清了,结果很多操作类文档检索质量直线下降;如果文档里有多级编号,要把编号保留在文本里,否则模型回答时无法引用准确章节,用户会投诉“你给的指引根本找不到在哪一页”。
3.3 向量化与检索:参数怎么定
数据准备好之后,下一步是向量化。我用的 Embedding 模型是开源的 bge-m3,它在中文语义理解上的表现很不错,而且轻量、本地可跑。向量维度 1024,这个维度确实偏大,存储开销高一些,但检索质量比小维度模型好不少。如果你的机器资源紧张,可以换更轻量的模型,但要做好效果打折扣的心理准备。
索引结构我用的是 FAISS 的 IVF 索引。先在小规模数据上跑暴力检索得到真实分布,然后设置nlist为 100,nprobe为 10。这里有一个权衡:nprobe越大,召回率越高但延迟越高。实测 2 万条文档的情况下,参数组合带来的延迟差异在 20 毫秒以内,所以可以放心往高调一点,不用为了那几毫秒牺牲召回率。
但这里我要强调一个关键点:光靠向量检索是不够的。在运维场景里,报错码ER_PARSE_ERROR这种精确匹配,用 Embedding 检索往往不如直接关键词搜。我的做法是混合检索:向量检索负责语义召回,BM25 负责关键词召回,然后把两路结果做加权融合。融合公式很简单:final_score = 0.7 * vec_score + 0.3 * bm25_score。权重取决于业务,如果你的场景更依赖术语精确匹配,可以把 BM25 的权重调到 0.5。这个权重是可以用一组标注样本试出来的,别拍脑袋。
检索的返回数量也需要测试。我试过top_k从 3 到 10 的各个档位,最终定在 5。少于 5,有用的信息经常漏掉;多于 5,模型需要处理的上下文变长,回答质量没有提升,反而增加了 Token 成本。一个 5 和 10 的对比测试看起来性价比不高,但它决定了你每天的调用成本,值得花一下午把参数摸透。
3.4 生成环节:Prompt和上下文策略
检索做完了,生成层要把这些片段组织成答案。这个环节有三个坑,我一个个说。
第一个坑是害处最大的:把检索结果全部拼进 Prompt,不管相关度高低。这样做会稀释模型的注意力,甚至被不相关内容误导。比如用户问“如何回滚”,结果检索到了两个不相关模块的配置说明,模型会一本正经地把它们也组织进答案里。我的做法是把检索结果排好序,只保留相关性分数在前三的片段,每个片段前加一个[来源: 文档标题-章节]的标记。这样一来,模型在回答时能自然引用来源,用户也能自己核对。
第二个坑是 Prompt 写得过于宽松。很多人的 Prompt 只会写“请基于以下内容回答”,然后期望模型自己理解一切。我尝试过把 Prompt 结构化之后,效果提升非常明显。一个可以参考的模板:
你是运维知识库问答助手。请严格基于提供的资料回答用户问题。 要求: 1. 如果资料中有明确答案,直接回答,并标明来源。 2. 如果资料中没有答案,明确回复“该问题在现有资料中未找到答案”,不要自行编造。 3. 回答控制在300字以内,使用简短的分点列表。 资料片段: {retrieved_chunks} 用户问题:{user_query}第三个坑是温度参数。问答场景下我设temperature=0.2,既能保持一定的措辞多样性,又不会让模型自由发挥。做过对比测试,温度在 0.7 以上时,编造答案的概率明显上升,特别是涉及具体数字和步骤的时候。这种参数看起来不起眼,但就是这些不起眼的细节,决定了一个 AI 应用是“能用”还是“好用”。
4. 把应用做稳:评测、调试与成本控制
RAG 应用能跑通之后,真正的工程化工作才刚开始。这也是 ai-engineering-from-scratch 项目和普通教程项目最大的区别:教程结束于 demo 跑通,工程开始于 demo 跑通。
4.1 评测集怎么构建
没有评测,就没有优化。这是我一直在遵守的原则。但很多人在这一步走歪了:评测质量全靠“我觉得回答得还行”。主观感受只能排查明显问题,无法指导优化方向,因为你的记忆会美化结果,“这个版本好像好了点”在复盘时是最模糊的证据。
我的做法是把评测分成三个维度:检索质量、生成质量、端到端表现。检索质量用标准的信息检索指标——Recall@k和MRR,需要为每条测试问题标注它对应的标准答案片段。生成质量我用三个子评分:完整性(是否覆盖答案要点)、准确性(是否有幻觉内容)、引用合规性(是否引用了不存在的来源)。端到端表现则是真实用户视角的主观评分。
评测集怎么来?我的建议是三个来源叠加:从历史客服记录中抽样真实问题;让业务专家撰写高频问题;把文档中的关键知识点改写成问句。数量不用多,100 到 150 条就足够发现大部分问题。关键是每一条都要有标注答案,否则评测结果不可信。我最开始偷懒,直接从文档标题生成问题,结果评测集和线上真实分布完全脱节,测出来的高分毫无意义。
4.2 调优的优先级排序
当系统表现不达标时,怎么定位问题?我的经验是:先查检索,再查生成,最后查改写。这个顺序不能乱,因为生成层的输入就是检索层的输出,检索错了,生成再怎么调也是白搭。
检索环节的问题通常表现为“答案相关但不到位”——召回结果里没有用户想找的信息。这种情况下我会去看检索日志里召回片段的来源分布,如果正确的片段排在 10 名开外,说明切分或者 embedding 有问题。生成环节的问题通常表现为“材料里有但回答错了”——召回是对的,模型没答好。这时优先调整 Prompt 结构和上下文组织方式。改写问题则比较隐蔽,表现为“问法一变就答偏”,这是用户意图没有正确转换。
我在项目里做了一次完整的调优实验,记录每一版调整的效果。一个特别有用的操作是:每次只改一个变量。比如这轮只调top_k,那轮只调温度,每轮跑完评测再决定保留还是回滚。这种笨办法比同时改三个参数效率高得多,因为你能确定效果变化来自哪个环节。很多人的调优陷入死循环,就是同时改太多了,最后只能靠运气。
4.3 成本与延迟的量化方法
最后说成本和延迟。我在项目里做了一个简单的成本统计脚本,记录每次请求的输入 Token、输出 Token、链路耗时。运行两周后,汇总数据让我吓了一跳:看起来平淡无奇的配置,一个月消耗了大约 500 万 Token,其中检索生成的中间环节占了接近一半——因为每次调用都带上了全套检索结果,而其中不少是低相关片段,白白烧钱。
优化方向也随之清晰了。第一,把检索逻辑改为“先取 10 条候选,精排后只保留 3 条进 Prompt”,这一步直接砍掉了一半的输入 Token。第二,对重复的查询做缓存,一样的运维问题每天被问几十遍,完全可以命中缓存,我把缓存命中率做到了 40% 左右。第三,设置上下文预算,根据问题类别动态调整检索数量,简单问题取 2 条就够。这三招做下来,成本下降了 55%,首字延迟也明显改善。很多时候成本的坑不在模型贵,而在工程上给了模型太多不需要吃的内容。
5. 常见问题与避坑记录
5.1 高频问题速查表
我把这个项目开发过程中遇到的高频问题整理成了一张速查表。排查问题的时候照着对,能省很多时间:
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 回答内容是对的,但语言很怪 | Prompt 没给出语气规范 | 检查 Prompt 中是否有风格指令 |
| 答案经常漏要点 | 检索召回不足 | 调大 top_k 或优化切分 |
| 答案引用了不存在的来源 | 模型幻觉 | 降低温度,限制生成长度 |
| 中文长文档表现差 | 切分破坏了语义 | 检查切分是否按章节结构 |
| 同一问题每次答案都不同 | 温度过高或上下文顺序不稳定 | 统一温度,固定检索排序 |
| 大量 Token 花费在检索内容上 | 上下文塞入了低相关片段 | 开启精排,压缩上下文 |
这张表还有一个隐含的使用方法:每发现一个新问题,就补一行。一个月之后,你的速查表就是团队里最值钱的文档,因为它是从真实故障里长出来的。
5.2 我踩过的三个典型坑
第一个坑是最痛的:我用了一个看起来很聪明的切分策略——按语义相似度聚类再切分。理论上看很好,实际跑完发现检索效果反而不如简单的按标题切分。原因是聚类切分生成了很多“语义集中但结构混乱”的块,生成阶段丢失了章节上下文,模型经常答非所问。这个实验花了三天,结论就是:文档结构本身就是最好的切分依据,别为了“聪明”而绕路。
第二个坑是评测指标用错了。早期我用准确率衡量整个问答链路,结果准确率 95%,上线后用户反馈却很差。后来才发现,准确率只统计了“最终答案是否完整”,但没关注“检索是否召回了正确信息”。几条测试问题碰巧都能答对,但检索本身就是飘忽的,只是运气好答对了而已。后来改成对检索和生成分别评估,问题很快暴露出来,检索的Recall@5其实只有 60%,这才是体验差的真相。
第三个坑是 Embedding 模型的版本不一致。训练时的文本用 bge-m3 的 v1 版本向量化,上线时为了“升级”换成了 v2 版本,结果两套向量空间完全不兼容,检索质量暴跌。这给我上了一课:向量的版本兼容性不是小事,生产环境不能随便升级 embedding 模型。如果一定要换版本,所有向量必须重新生成、重新建索引,没有捷径。
5.3 给后来者的几条操作建议
最后给走这条路的朋友一些操作层面的建议。
第一,一定要从自己熟悉的领域选题,不要为了练手去搭一个完全陌生的知识库。检索系统的调试非常依赖你对内容的判断力——有没有检索对,你自己心里得先有数。如果你对文档内容一窍不通,出了错你根本分不清是检索的问题还是自己的理解问题。第二,把日志从第一天就设计好。请求 ID、检索来源、Token 用量、响应时间,这些字段后续排查问题会救命。临时补日志是最痛苦的,因为你不知道该补哪些。第三,每做完一个里程碑就写一份复盘文档,哪怕只有几百字。ai-engineering-from-scratch 项目走到后期,最有价值的就是那些记录里当时看起来不起眼的实验结论。
我个人的最大体会是,AI 工程里绝大多数问题都不是“知识不够”导致的,而是“不知道自己的系统在哪个环节出了问题”。评测和日志就是你的手电筒,没有它们,只能在一片黑里瞎摸。如果你正在走这条路,建议把评测、日志、复盘这三件事从第一天就做起来——它们不会让你的 demo 跑得更快,但会让你在 demo 之外走得更远。