
聊一个很多人踩过的坑学了一堆机器学习算法卷积、Transformer、注意力机制讲得头头是道但真要你做一个能给别人用的AI应用——比如给公司内部做一个人事政策问答机器人——就卡住了。数据不知道从哪来模型不知道怎么接就算接上了回答得乱七八糟也不敢上线。这就是典型的会算法但不会AI工程。我理解的ai-engineering-from-scratch不是从零推导数学公式也不是自己从头训练一个大模型而是指从零建立一套把大模型能力稳定落地成产品的工程能力。它跟你懂多少层Transformer没有必然关系反而跟数据清洗、系统设计、成本意识、评测方法这些东西强相关。这篇文章就想聊聊我走通这条路径时沉淀下来的技术框架、实操步骤和踩过的坑适合那些已经入门Python、想从跑通Notebook迈向交付可用AI应用的人。1. 先搞清楚AI工程到底在做什么1.1 它跟算法研究是两条路很多人误以为AI工程师就是训练模型的人。实际上今天的AI工程重心早就变了。基础模型由专门的团队去训练大部分业务的AI工程是在已有大模型能力之上做二次开发把模型接入业务数据用提示词把输出校准到可用水平再把它包成一个稳定、可控、可监控的服务。我自己的一个判断标准是如果你做的东西挂了用户感知到的是什么。算法研究员关心的是这个模型在新测试集上的F1是否提升了而AI工程师关心的是用户问了一个奇怪问题服务是否还能正常返回、是否还能控制成本和延迟。这是两种完全不同的思维方式前者追求最优后者追求稳态。1.2 为什么从零开始要刻意做减法网上关于AI工程的资料很多但绝大多数是工具全家桶式的LangChain、LlamaIndex、向量数据库、Agent框架恨不得全堆上去。我走过这段弯路结论很简单从零起步的时候工具越多你越是学了个寂寞。原因在于这些框架抽象层级太高如果你不理解它背后的逻辑出了问题根本不知道去哪查。我见过有人连向量检索的原理都没搞清就用LangChain搭了一个RAG结果检索结果一塌糊涂他只能重启服务碰运气。所以我的建议是从零开始先把最小链路用裸代码跑通再逐步引入框架。你亲手拼过一次脑里才有那张数据流向图后面用任何框架都是图省事而不是被框架牵着走。1.3 首先定义你的第一个里程碑没有目标的从零开始最后都会变成学了三个月啥都没做出来。我在带人时一定会让他们先定一个足够小的里程碑比如做一个基于本地文档的问答应用用户上传一份PDF就能问这份PDF里的问题并且回答要能注明出处。这个目标看起来普通但它涵盖了AI工程的全部核心要素数据解析、文本分块、向量化与检索、提示词拼装、大模型调用、输出格式约束、成本控制。做完这个你就不是在学AI而是在做AI工程了。2. 从零搭建AI工程的技术栈选型2.1 编程语言与Python生态的必要性目前做AI工程Python依然是最稳妥的选择不是因为它语法有多优雅而是因为整个AI生态的默认语言就是它。你随便翻一个大模型SDK文档示例代码基本都是Python开源的向量库、文档解析库、以及部署相关的工具链对Python的兼容性最好。但我要多说一句Python基础必须扎实到能看懂源码的程度。你不能只会调用至少要理解装饰器、上下文管理器、生成器、类型标注。原因是AI工程链条很长任何一个环节出错排查时都要深入库的源码。我印象很深的一次是某个embedding接口的报错信息非常隐晦最后顺着源码才发现是请求重试逻辑里没有处理网络断连的异常类型。基础不牢你连入口都找不到。2.2 模型接入API优先别急着本地部署群里经常有人问我要不要搞一张显卡跑开源模型我的回答一般是如果你不是专门做私有化部署交付的先别。从零开始做AI工程最高效的方式是直接使用成熟的大模型API服务五花八门的国内大模型平台都提供了兼容接口按量付费、免运维、自带推理优化。API优先的好处不只是省事它还能让你把有限的精力集中到AI工程真正的核心——数据处理和系统设计上。等到你的应用真的跑通、有了稳定的调用量再计算自部署和API的成本临界点。真到了那时候你也会发现最大的成本往往不是模型推理而是维护一个GPU集群的人力和精力。2.3 数据侧的选型向量库别盲目追求流行RAG是目前从零上手性价比最高的AI应用形态而向量数据库是避不开的组件。流行的选项很多FAISS、Chroma、Qdrant、Milvus、pgvector。新手容易犯的选择困难症是哪个最强大选哪个但我的原则是跟业务体量匹配。如果只是原型验证、几万条文档片段用Chroma或者直接上FAISS就够了它们轻量、嵌入快速、排查问题方便。如果数据量到了百万级、需要分布式扩容和精细化权限控制再上Milvus或Qdrant。如果你的团队本身就在用PostgreSQL那pgvector是最省心的选择少维护一套系统。工程上少一个组件本身就是一种优势运维成本和故障点都降低了。2.4 工程化工具链从第一天就要有的习惯我见过太多人的AI项目死在代码只能在自己电脑上跑这一步。从零开始就要建立三个习惯代码版本管理、环境隔离、依赖锁定。具体来说Git是最基本的不解释Python项目必须用虚拟环境要么venv要么conda依赖要用requirements.txt或pyproject.toml锁定版本尤其是AI项目对版本极其敏感——embedding模型换了版本向量维度或分布可能都变了旧索引很可能直接失效。还有一个容易被忽视的是数据版本管理对AI应用来说知识库数据就是代码的一部分每次调整数据都要能回溯别等出了线上事故才发现是半个月前的数据改动引起的。这套习惯建立起来之后你的项目才算真正有了工程的雏形。3. 实操从零手写一个最小可用的RAG问答服务3.1 先画数据流再写代码我不太建议一上来就写代码。先用最简单的方式把系统数据流画出来哪怕是纸笔也行。一个典型的RAG问答系统数据流长这样文档输入 - 文本清洗 - 分块(Chunking) - 向量化(Embedding) - 存入向量库 用户提问 - 向量化(相同模型) - 向量检索 - 拼装Prompt - 调用大模型 - 返回回答这个图是不是看着特别简单但绝大多数线上事故都出在这条链路的某个环节里。你在脑子把这张图刻下来后面做监控、做日志、做排查都是围绕它展开的。哪个环节慢了、哪个环节报错了、哪个环节数据不对了你第一反应就应该是定位到图上的位置而不是瞎试。3.2 核心链路代码骨架我以处理一份Markdown文档并回答用户提问为例写一个最简但结构完整的版本。这里我刻意不引入任何编排框架让你看清每一步在干什么。代码使用示例性的接口风格换成本地开源模型SDK也是一样的套路。from typing import List import os import hashlib # 假定环境变量里配置了API Key from openai import OpenAI client OpenAI(api_keyos.environ[MODEL_API_KEY], base_urlos.environ[MODEL_API_BASE]) EMBEDDING_MODEL your-embedding-model-name # 例如 text-embedding-3-small LLM_MODEL your-chat-model-name # 例如 gpt-4o-mini def load_document(path: str) - str: with open(path, r, encodingutf-8) as f: return f.read() def split_chunks(text: str, chunk_size: int 800, overlap: int 100) - List[str]: 按字符数简单切分带固定重叠。生产环境建议按标题结构切分。 chunks [] start 0 n len(text) while start n: end min(start chunk_size, n) chunks.append(text[start:end]) if end n: break start end - overlap return chunks def embed_texts(texts: List[str]) - List[List[float]]: # 注意真实实现需要处理batch大小限制和重试逻辑 resp client.embeddings.create(modelEMBEDDING_MODEL, inputtexts) return [d.embedding for d in resp.data] def build_index(doc_path: str, vector_store): doc load_document(doc_path) chunks split_chunks(doc) doc_id hashlib.md5(doc_path.encode()).hexdigest() for i, chunk in enumerate(chunks): emb embed_texts([chunk])[0] vector_store.add( idf{doc_id}:{i}, vectoremb, payload{text: chunk, source: doc_path, chunk_index: i} ) return len(chunks) def search(query: str, vector_store, top_k: int 4) - List[str]: q_emb embed_texts([query])[0] hits vector_store.search(q_emb, top_ktop_k) return [h.payload[text] for h in hits] def build_prompt(query: str, contexts: List[str]) - str: context_block \n\n---\n\n.join(contexts) return f请仅基于以下参考资料回答用户问题。 如果参考资料中没有相关信息请直接回答资料中未找到相关内容不要编造。 参考资料 {context_block} 用户问题{query} def ask(query: str, vector_store, llm_client): contexts search(query, vector_store) prompt build_prompt(query, contexts) resp llm_client.chat.completions.create( modelLLM_MODEL, messages[{role: user, content: prompt}], temperature0.2, streamFalse, ) return resp.choices[0].message.content这个代码骨架里最容易被忽视的点有两个。第一个是在Prompt里明确要求资料中没有就直说。这是对抗幻觉最便宜的手段不做的话模型很容易顺着用户的问题强行编一个答案。第二个是embedding的batch调用问题。真实场景中你可能有成千上万个chunk绝对不能一个循环里逐条调接口那太慢了必须按batch合并后一次请求。而且embedding接口通常有每分钟调用次数限制幂等重试和退避策略是必须的这块代码看起来不AI但它决定了你的数据处理流程能不能在合理时间内跑完。3.3 参数选择背后的工程考量很多人问我chunk_size到底设为多少合适我的回答是没有固定最优值它是数据形态和模型能力之间权衡的结果。chunk太小比如200个字符检索是精准了但上下文信息不完整大模型可能理解不了句子之间的指代关系chunk太大比如2000个字符单次检索返回的内容太泛而且可能切断语义边界还容易超出上下文窗口限制。我用800到1000字符、重叠100到150字符作为起步值后续再根据评测结果调。重叠区域的存在是为了避免一个语义完整的段落恰好被一刀切断。另外一个看似细节实则关键的参数是temperature。在RAG场景下我永远把它调低基本在0.1到0.3之间。原因很直白这是问答系统用户要的是稳定正确的答案不是富有创意的长篇大论。温度越高同一问题两次回答的差异就越大这对工程上做回归测试非常不利。3.4 从原型到勉强能上线的工程化改造上面的代码跑通只算原型离能交付还有一段距离。我每次在项目里至少要做四个改造。第一是增加缓存层。同样是人事政策里病假天数这种高频问题每次都重新embedding加重新调大模型既花钱又慢。用一个简单的键值缓存key是query的哈希value是回答结果命中率一上来成本至少降三分之一。第二是给接口加超时和重试。大模型API的响应延迟波动很大你不设置超时用户就会一直转圈不设置重试一次网络抖动就白屏。第三是加日志和链路追踪。用户问了一句今年年假还能休几天你得能从日志里追溯出检索了哪几个chunk、Prompt最终长什么样、大模型返回了什么。没有这套东西后面出了问题你连复现都没法复现。第四是安全兜底。输出的内容要做基本的敏感信息过滤Prompt里也要加系统级的安全约束别把希望全寄托在模型自觉上。# 缓存装饰器只是示意实际生产可换用Redis _CACHE {} def cached_answer(query: str): key hashlib.md5(query.encode()).hexdigest() if key in _CACHE: return _CACHE[key] def wrapper(): contexts search(query, vector_store) prompt build_prompt(query, contexts) answer call_llm(prompt) _CACHE[key] answer return answer return wrapper()4. 从零到一踩过的坑问题排查与实务技巧4.1 向量检索查不到的真相第一个高频问题用户的问题明明跟文档里的内容很相关但检索出来的是无关片段。大部分人第一反应是换更好的向量模型但我的经验是八成问题出在数据上。最常见的是分块切碎了语义比如连续两句属于同一条规定却因为长度限制被拆进了两个chunk检索时单看哪块都不完整。解决办法是设计父子块结构把小chunk用于检索拿到命中小chunk后将它所属的父块更大范围的章节一并丢给大模型。还有一个常见原因是用户问法的表述跟文档里的用词差异很大比如文档写薪资构成用户问工资怎么算的。这类问题靠向量模型硬扛效果有限更有效的做法是给索引里的关键文档块配置别名关键词或者在提问侧做一次改写把口语问题改成更接近书面语的关键词组合。4.2 上下文窗口与Token成本失控第二个高频问题回答质量确实上去了但Token消耗大得惊人账单看着肉疼。原因通常有两个一是你把所有检索结果一股脑全塞进Prompt二是chunk切得太大一次检索四个chunk每个2000词一轮问答烧掉八千词的上下文。工程上有对应的体检清单检索结果的条数是不是太多了top_k3到4通常够用每个chunk是不是太大了系统提示词里是不是堆了一堆长而无效的规则说明还有是否开启了流式输出给用户先看到第一个字的体验避免用户因等待时间过长反复重试重试也烧钱。最有效的降本办法是上文提到的缓存尤其对高频重复问题命中一次省一次全套链路费用。4.3 大模型幻觉怎么压制彻底消灭幻觉现在还做不到但可以把概率压到可接受范围除了Prompt里明确要求未找到请直说之外我更推荐一套组合拳。第一强制要求模型给出来源引用。我通常会在Prompt里要求模型在回答末尾列出来自第几份文档的哪个小节并附上该小节的原文短引用。第二设置一个简单的置信度校验环节让模型自己先评判检索到的上下文是否足够回答用户问题不够就不答这个步骤看起来土但确实能把强编概率压下来。第三也是工程师最容易忘记的建立评测集。你收集一批典型问题每条标注好期望答案和对应的文档片段每次改Prompt、换模型、调分块参数后都跑一遍这批问题肉眼扫一遍回答质量。没有评测集你就永远在靠感觉做优化。4.4 测试集应该覆盖什么评测集不要求大但要求有代表性。我会分成三类第一类是从实际用户日志里捞出来的高频问题这类最重要它代表真实需求第二类是有明确标准答案的事实型问题适合快速判断正确率第三类是边界和对抗性问题比如跟文档相关的反事实问题文档里是不是提到过XX不存在的内容专门用来检查模型会不会强行说有。这套评测集的价值会随着时间增长越滚越大它其实是你这个AI应用在数据侧的回归测试体系。4.5 原型环境与生产环境脱节最后这个坑特别隐蔽你在本地Notebook里跑得好好的一上生产就各种状况。差别在哪本地内存里存的索引重启就没了本地单个请求无所谓并发本地你用的测试文档干净整洁生产环境里的真实数据却充满格式混乱、扫描件和表格。工程解法是环境一致性用Docker把依赖和服务封装起来向量库的结果持久化到磁盘或云存储接口层用并发测试压一遍最好把真实数据里最脏的那部分样本提前暴露给系统。把本地能跑和生产能跑当成两个不同目标你会少很多深夜崩溃。5. 写在后面的话这条路我自己走了一遍最大的感受就是AI工程的门槛不在数学而在系统性思维。你面对的不是一个模型函数而是一条从用户问题到最终答案的完整链路链路里的任何一环都可能让你一夜回到解放前。从零开始的人不要贪多就做那一个问答机器人把它做到自己被自己说服可用的程度你会发现后面的一切都顺理成章了。等技术熟练以后自然可以往Agent、多模态方向扩展单向知识问答也只是起点而已。最后再分享一个小建议每次跑完一次完整的项目迭代把这次出问题的环节、原因、排查过程、最终方案记成笔记。这些笔记才是AI工程这条路上最值钱的资产比任何课程和框架都管用。