2025年如果你打开招聘软件搜“AI工程师”,会看到一个薪资很高、岗位很多,但能力要求写得含糊不清的职位。AI工程(ai-engineering)到底是做什么的?是熟练调大模型API的“接口工人”,还是精通PyTorch的算法专家,又或是能训练自己模型的炼丹师?我带了几个从零转行的同事,也翻过大量学习计划,越来越强烈的感受是:大多数人不是学不会,而是被各种课程和路线图带偏了方向。
今天这篇东西,我想认真聊聊一条我认为最靠谱的“AI工程从零开始(ai-engineering-from-scratch)”路径。它不要求你数学博士毕业,也不满足于把你训练成一个只会拼请求的API搬运工。它的目标,是让你能把一个LLM应用从Demo一路推到真能上线的状态。不管你是后端、前端、测试还是算法出身,只要会点编程,愿意花几个月时间,这条路基本都能走通。
1. 从零开始学AI工程,到底在学什么
1.1 先画边界:AI工程不是调API,也不只是机器学习课
AI工程这几年被包装得神神秘秘,我接触下来觉得它其实是个交叉地带。往左看是传统软件工程,往右看是机器学习,底下垫着大模型推理和数据处理。真正的AI工程,是把大模型技术模块化、产品化、生产化的一整套方法论。
说得直白一点,AI工程解决的是这样几类问题:怎么把用户的自然语言问题变成高质量的大模型输入,怎么在回答前塞进私有知识库里的相关资料,怎么让模型稳定地调用外部工具去完成多步任务,怎么把你调通的接口压到能扛住线上的流量,以及怎么判断这套系统到底变好还是变坏。
很多人以为学会LangChain就算入门AI工程,学会调参数就算精通LLM应用,这是天大的误解。LangChain只是个抽象层的工具箱,它把一些常见套路封装成了链式调用,但底层的数据流和状态管理仍然是你自己的工程问题。同样,仅仅会用OpenAI的API发几条请求,也不叫AI工程,那叫“API消费者的基本功”。
“from scratch”这个前缀之所以重要,就是因为它强调了要回到本质去理解。你不一定要能从零手写Transformer,但你得清楚token是怎么切出来的、上下文窗口意味着什么、为什么回答会瞎编、为什么某些prompt技巧有效而某些纯粹是玄学。这些底层认知,决定了你遇到线上故障时是只能重启大法,还是能一眼定位到问题源头。
1.2 最常见的三种错误学习姿势
我见过太多自学者的失败案例,归纳下来就是三种固定的错误姿势。
第一种是“一头扎进数学底洞”。买了一堆深度学习理论书籍,从反向传播开始推导,MNIST手写识别刷了三遍,却始终不敢碰大模型API。这些人往往在数学里迷失了方向,学了大半年偏导数,最后连一个能跑的聊天机器人demo都没做过。做AI工程,你的目标不是发表论文,而是交付系统。Transformer里面的矩阵推导可以作为拓展阅读,但绝对不该是入门第一站。
第二种是“只会调接口,不懂原理”。这种人熟练应用各种大模型API,会用Prompt模板,能接上LangChain,但问一句“为什么temperature设为0.7比0.2更合适”就卡壳了。他们写的应用一旦遇到意外输入就崩,因为完全依赖模型临场发挥,没有结构化兜底。不懂原理调出来的参数,换个模型、换个场景大概率要重新碰运气。
第三种是“看教程很多,做项目为零”。收藏了上百个仓库,GitHub星标了一圈,但自己一行端到端的业务代码都没写过。看别人做RAG觉得简单,轮到自己处理脏PDF时就发现全是细节。学习AI工程的最短路径,永远是带着一个真实项目往前走,教程只是你的地图,路还是得自己走。
2. 核心知识体系拆解:先把地基打结实
2.1 模型原理入门:token、上下文窗口、温度和注意力
要做AI工程,不啃论文也能懂的核心概念,其实用生活类比就能讲清楚。
tokenizer可以理解成“切菜工”,它把一段文本切成模型能处理的最小单元。英文里一个token大概是一个单词的一部分,中文里一个token大概是一个字到一个词。切法直接决定了你花多少钱、能传多少内容。很多人调接口时发现超长文本被截断,就是因为没注意到token不是按“字符”算的,是按“切出来的块”算的。一个中文字符在大多数模型里可能占0.6到1个token,算上下文预算时千万别按字符数去估。
上下文窗口就是“桌子大小”。模型一次能“看见”多少文本,取决于它的context window。这就像一张桌子,你摆得下多少资料,模型就能参考多少资料。超出部分要么被丢弃,要么需要你自己决定怎么取舍。做RAG系统时,这个参数直接决定你能塞进去多少检索结果。
temperature和top_p,控制的是模型采样的“随机性”。想象一下猜谜游戏,temperature高就像每轮都瞎猜新答案,擅长发散;temperature低就像把已有答案从头背一遍,稳定但死板。写代码任务、结构化输出任务,我基本都设成0到0.3;头脑风暴、创意写作,才敢把温度往高了调。注意,不同模型的temperature标定口径有细微差异,同一个数值在不同模型上表现可能差很多,最好自己用小批量样本实测一下。
注意力机制,简单理解就是模型在读文章时动态给每个词分配“注意力权重”。哪些词对理解当前句话更重要,就给更高的权重。Transformer革命性地取消了对文本顺序的强先验依赖,直接让所有位置同时计算注意力,这使得模型能在长文本里跨距离捕捉依赖关系。工程上理解到这个程度其实就够了,剩下的可以交给框架。
很多人会问,搞清这些原理有什么用?我举个亲身经历。之前排查一个线上问题,发现某些用户问同样的问题,同一个模型答案质量时好时坏。后来定位到是prompt里拼了太长的历史记录,把模型真正的上下文窗口撑爆了,早期对话被静默截断。如果你不懂上下文窗口的概念,遇到这种问题只会一头雾水。
2.2 应用层四件套:Prompt设计、RAG、Agent与评估
应用层四大件,是目前AI工程最核心的工作内容。
Prompt设计是入门第一步,但也是最容易被神化的一步。结构化模板是有用的,例如给模型固定System Role、明确输出格式、给一两个few-shot示例,这能显著提高输出稳定性。思维链这种技巧也确实有效,因为LLM在逐步思考时更容易保持逻辑一致性。但我要提醒的是,Prompt是上限最低的优化手段。很多时候你花一周调出来的prompt,不如直接换一个更强的模型或者引入检索来得效果显著。所以别陷入“prompt炼丹”的泥潭,应该把它当成工程栈里的一个普通组件。
RAG是检索增强生成,也是现在落地最多的一类应用。它的核心思想很朴素:模型不知道你私有的数据,那我们就先把相关资料找出来,塞进prompt里让它看着回答。这样做一是缓解幻觉,二是让知识可以实时更新,三是解决了私有数据怎么给模型用的问题。一个完整的RAG链路会涉及文档解析、切片、向量化、检索、重排、生成多个环节,每个环节都有工程决策要做。我在后面的实操章节会详细展开。
Agent是今年最热的方向之一。一个AI Agent本质上就是一个“计划加执行”循环:模型先理解用户目标,把它拆成子任务,然后调用外部工具去执行,拿到结果后不断调整计划直到完成。这里面最重要的是工具调用的可靠性,以及多轮状态的管理。很多初级开发者喜欢堆一堆工具给Agent,但根本没有定义清楚工具之间的依赖和异常处理,结果Agent在错误路径上越走越远。工具越少越精,Agent才越稳。
评估是四大件里最容易被忽略却又最重要的一环。没有评估,就无法量化你的Prompt修改是变好还是变坏,RAG检索优化是否有效,Agent流程调整是否更可靠。线上评估可以用人工抽检,开发期最好的方式是把评测用例沉淀成数据集,用LLM-as-judge或者传统指标来批量打分。到这里就理解了为什么很多AI工程师越往后越忙——他们在搭建自己的评估体系,而不是在调东调西。
2.3 部署与性能:从notebook到生产环境的必经之路
模型调得再漂亮,部署不了等于零。AI工程的部署有两个层面。
第一层是接口服务化。如果你用云端大模型API,你要做的服务化很简单,加个FastAPI或者Flask套壳转发就行。但要注意并发控制、超时处理、错误重试和限流逻辑,这反而是最容易出问题的点。比如线上用户多了以后,API偶尔返回超时,你的客户端没有重试机制的话,用户看到的就全是报错。另外还需要考虑成本控制,同样的请求次数,配合语义缓存能省下不少钱。
第二层是开源模型的自部署。如果你选择用开源模型(比如Qwen系列、Llama系列),那就要考虑推理引擎和显存管理。推理引擎选型通常考虑vLLM,它对高吞吐、长上下文的支持比较好,还集成了PagedAttention等显存优化手段。本地部署还要考虑量化,把模型从FP16压到INT4或者INT8,显存占用能少一半以上,推理速度往往还更快,只是精度会略有下降。
性能优化方向也很有讲究。流式输出(streaming)是交互体验的刚需,能显著降低用户等待焦虑。批处理(batching)可以提升吞吐,把多个请求拼成一批一起推理,GPU利用率会高很多。KV Cache命中就是在缓存模型过过的历史token,对多轮对话场景帮助很大,很多引擎对这个量做了优化。
3. 手把手实操:从零搭一套RAG知识库问答系统
3.1 项目定义与整体架构
纯聊概念没有用,我来带你把一套RAG知识库问答从零搭起来。这个项目是最典型、最落地、也最适合作为“from scratch”里程碑的练习。
场景定义:团队内部有一堆Word和PDF格式的文档,产品经理和技术团队想做个内部问答助手,员工自然语言提问,系统到文档库里找答案并给出回答,还要标注引用来源。
整体架构分为两条链路。入库链路:文档上传之后,先做格式解析,把文本切成适当大小的块,然后逐个embedding成向量,存入向量数据库,同时保留原文块的索引信息。查询链路:用户提问后,先把问题embedding成向量,去向量库做相似度检索,同时用BM25做一遍关键词检索,两份结果用RRF融合,再到重排模型把最终最相关的几条挑出来,拼进prompt模板,送给大模型生成回答,最后把引用来源一并返回前端。
技术选型我建议按下面的思路定下来,不需要一上来就上重型组件。
| 组件 | 推荐选项 | 理由 |
|---|---|---|
| 向量数据库 | 起步用FAISS,后续转pgvector或Qdrant | FAISS内存索引最简单,适合学习和原型验证;生产要持久化和多租户再迁移 |
| Embedding模型 | 开源BGE-M3或bge-large-zh | 中文效果好、免费、可控,且能离线推理 |
| LLM | 先接云端API做通闭环,再换vLLM自部署开源模型 | 先降低试错成本,后面按预算和隐私要求切换 |
| 检索策略 | 向量检索 + BM25 + RRF融合 | 单用向量会漏掉精确关键词匹配,融合显著提升召回率 |
| 重排 | 用bge-reranker系列 | 轻量模型,二次筛选非常有效 |
| 后端服务 | FastAPI | 异步支持好,生态成熟,和ML框架配合也顺 |
选型逻辑多说一句。很多人一开始就想上重型分布式的向量数据库,这其实是过度设计。你做原型验证时,几千个文档、几万条chunk,FAISS跑在内存里性能完全够用。等真正需要多个用户、并发写入、权限隔离的时候再迁移,迁移成本可控,因为你代码里抽象了检索接口。
3.2 文档切分与向量化
切分是整个RAG系统中最容易被低估的环节。切得太小,语义不完整,检索到的片段常常说不清前因后果;切得太大,噪声太多,超过模型上下文预算,而且向量表达会被稀释。我的默认起步值是chunk_size=500个token左右,overlap=50个token。这个数值不是拍脑袋,它大致对应两三个自然段落的长度,既能保留局部语义,又能通过overlap让段间承接不丢上下文。
来看一个实际切分代码,用LangChain的话很简单,但我更建议先手写一个理解原理:
def split_text(text: str, chunk_size: int = 500, overlap: int = 50): # 先用简单标点把文本切成语义块 import re sentences = re.split(r'(?<=[。!?;])', text) chunks, current = [], [] current_len = 0 for sent in sentences: sent = sent.strip() if not sent: continue current.append(sent) current_len += len(sent) if current_len >= chunk_size: chunk_text = "".join(current) # 计算overlap尾部 tail = chunk_text[-overlap:] if overlap > 0 else "" chunks.append(chunk_text) current = [tail] if tail else [] current_len = len(tail) if current: chunks.append("".join(current)) return chunks这段代码是个简化版本,生产环境中你还会需要处理代码块、表格、列表等特殊结构。比如Markdown表格被切开之后检索到一半表格,模型根本看不懂。经验是先用文档结构识别(标题层级、表格区域)做预分区,再在区内切块,不要让chunk跨过表格边界。
向量化部分用sentence-transformers可以直接跑:
from sentence_transformers import SentenceTransformer model = SentenceTransformer("BAAI/bge-m3") chunks = split_text(raw_text) embeddings = model.encode(chunks, normalize_embeddings=True) # 写入FAISS索引 import faiss import numpy as np dim = embeddings.shape[1] index = faiss.IndexFlatIP(dim) index.add(embeddings.astype("float32"))选择BGE-M3主要是因为它在中文长文本上的表现稳定,支持8192长度的输入,而且多语言向量空间设计合理。注意我encode时开了normalize_embeddings,这样用内积(IndexFlatIP)等价于余弦相似度,检索结果更稳定。把这个步骤记下来,这是很多教程不会提醒的细节。
3.3 混合检索与重排为何能显著提升效果
向量检索擅长“语义相似”匹配,比如用户问“报销流程”,文档里写“差旅费报销规范”,语义上能被召回。但它有个短板,如果query里的精确关键词(比如某个系统编号、项目代号)没有出现在文档里,向量检索可能会把真正含这个编号的文档排在后面。BM25恰恰擅长关键词精确匹配。所以业界普遍做法是混合检索:两路召回,再融合。
融合算法里最常用的是RRF(Reciprocal Rank Fusion)。它的思想很直观:不去比两路结果的绝对得分,因为两路分数分布完全不同,没法直接比较;而是按排名折算出得分。排在第1位的给最大权重,排名越靠后权重越小,最终把两路的排名加权合并。
def rrf_fusion(vector_ids, bm25_ids, k=60): score = {} for rank, doc_id in enumerate(vector_ids): score[doc_id] = score.get(doc_id, 0) + 1 / (k + rank + 1) for rank, doc_id in enumerate(bm25_ids): score[doc_id] = score.get(doc_id, 0) + 1 / (k + rank + 1) return sorted(score.keys(), key=lambda x: score[x], reverse=True)注意RRF里的k是平滑因子,通常取60,数值小会让排名靠前的文档获得更大优势。强烈建议不要改这个参数,除非你有足够的实验数据支撑。实测下来,用RRF融合后的Top5命中率比单用向量检索高20到30个百分点,这个提升基本是白捡的。
重排环节,我们再上一个reranker。重排和向量检索思路不同,它不是把问题和文档分别编码再算相似度,而是把问题和候选文档拼在一起,交给一个轻量级模型直接判定相关性。这个交叉编码方式精度高得多,但速度慢,所以只能对小批次(比如Top50)做精排。实践中我的管道是:混合召回50条,重排取前5条进prompt。实测中重排后首条答案正确率提升非常明显,强烈建议把reranker加入RAG管道。
3.4 生成环节与全链路串联
检索得到候选上下文后,生成环节不要简单粗暴拼接。首先要处理的是“上下文溢出”。把5条候选全塞进prompt可能超过模型窗口限制,我的做法是先粗算token,超过阈值就按rerank排名的权重截断,保前几条。其次要写清楚prompt的系统指令,明确要求模型只能基于提供的资料回答,不知道说不知道。
一个实际可用的prompt模板:
你是一名企业内部知识库问答助手。 请严格根据下面提供的资料回答问题。 如果资料中没有足够的信息,请直接回复“未找到相关信息”,不要尝试编造。 回答时请分点陈述关键依据,并在末尾列出使用的资料编号,格式为[1][2]。 资料: {context_with_sources} 问题: {question}标注引用来源是RAG工程里很重要的一环。我通常会给每一条chunk一个全局编号,生成阶段让模型输出引用编号,后端再根据编号去反查原文文件路径和页码。用户看到回答时能点击跳转到原始文档对应章节,一下子就解决了“AI回答能不能信”的信任问题。
最终把整条链路串起来,核心流程大概是:
# 查询链路(简化版) def answer_query(query: str): # 1. 混合召回 q_emb = embed_model.encode(query, normalize_embeddings=True) vector_hits = index.search(q_emb, 50) bm25_hits = bm25.search(query, 50) fused = rrf_fusion(vector_hits, bm25_hits) # 2. 重排精排 candidates = [all_chunks[i] for i in fused[:50]] rerank_scores = reranker.rank(query, candidates) top5 = [candidates[i] for i, _ in rerank_scores[:5]] # 3. 拼装生成 context_with_sources = format_context(top5) prompt = prompt_template.format(context=context_with_sources, question=query) answer = llm_client.chat(prompt) return answer, top5_sources到这里,一个最小可用的RAG问答系统就跑通了。我建议第一批数据不要用网上爬来的杂乱文章,就用自己的工作文档,几篇就好,把全链路调通,再慢慢扩到几百篇。先把一条路走通,再追求规模化。
4. 工程落地中的典型问题与避坑经验
4.1 环境与依赖的坑:版本地狱
做AI工程,环境问题可能是第一个劝退你的东西。PyTorch、CUDA、transformers、tokenizers这几个库版本相互依赖,经常出现安装torch了但CUDA不可用、transformers版本太老不支持某模型架构、tokenizers和transformers不匹配导致乱码之类的问题。
我的建议是:第一,所有实验环境用虚拟环境隔离,不要图省事直接pip install到全局。第二,锁定版本号,不要用latest。你会发现上月还能跑的项目,今天重新装就莫名其妙报错了,九成原因是某个传递依赖偷偷升了级。第三,有条件就上Docker,把环境固化到镜像里,这样任何机器上都能复现。特别是部署到服务器时,镜像会把很多“在我电脑上是好的”问题直接消灭掉。
另一个值得提的坑是GPU驱动和CUDA版本对不上。很多时候你明明有显卡,torch.cuda.is_available()却返回False,往往就是PyTorch版本编译时用的CUDA版本和驱动不匹配。解决办法是不要自己装CUDA开发套件,直接用PyTorch官方提供的对应CUDA版本轮子,例如torch的cu121/cu118等预编译包,它自带运行库,省去一大堆麻烦。
4.2 数据与检索方向的坑
RAG系统的效果上限很大程度取决于数据质量,但数据质量偏又是新手最容易踩坑的地方。
第一个坑是没清理就入库。我见过有人直接把扫描版PDF读出来的乱码文档丢进向量库,检索出来一堆“口口口”无意义内容,模型看着这些垃圾资料认真“答题”,结果自然惨不忍睹。文档入库前一定要做清洗:检测乱码、去除页眉页脚、把PDF转换后的文字先人工抽几页看看质量。宁可少入库,也不要污染向量库。脏数据一旦进了索引,检索端怎么优化都白搭。
第二个坑是切分导致的结构破坏。代码块被拦腰切开,表格被拆成上下两半,Markdown的标题层级错乱,这些都会让RAG召回的上下文不可读。我自己的做法是:对代码类文档,用代码结构感知切分器;对表格,整张表作为一个chunk,哪怕大一点也不要切碎;对带标题的文章,在标题层级上做预分区。结构化文档其实比纯文本更适合rule-based处理,值得多花时间。
第三个坑是embedding模型的语言和领域不匹配。你用英文模型处理中文文档,效果会明显下滑;用通用模型处理专业领域术语多的文档,召回也会失准。当前中文开源模型里BGE系列是一个稳妥的选择,领域特殊的话可以先用通用模型跑一版基线,再在领域数据上用对比学习微调。这个进阶操作没那么玄,本质是让你embedding模型看懂你领域里的黑话。
4.3 性能与稳定性方向的坑
线上系统的要求与demo完全不同,这里有三个高频故障。
第一个是内存OOM。embedding模型加载到显存后占几个GB,API服务里如果每一路请求都加载一次模型,并发一高直接爆显存。正确做法是模型常驻内存,做成单例,请求只做推理不加载。向量索引也存在类似问题,FAISS索引加载到内存后注意规模增长,最好定期重建,或者干脆切到支持磁盘映射的向量库。
第二个是流式输出与普通接口的兼容问题。很多前端框架默认是普通JSON响应,你改造为SSE流式后如果没同步更新前端解析逻辑,页面就会白屏或者收到一堆格式错误。另一方面,后端微服务之间如果用了普通HTTP,流式输出也不太好透传,需要网关支持SSE或者改用WebSocket。开发RAG服务时,我习惯把“是否流式”做成一个开关参数,给前端页面走流式,给内部接口走非流式,两边互不干扰。
第三个是成本方面的坑。大模型API按token计费,Prompt越长越贵。同一个问题被不同用户反复问,每次都跑一遍完整RAG链路,成本其实很高。语义缓存可以很有效地解决这个问题:把问题和答案缓存起来,新问题来了先算和已有问题的语义相似度,超过阈值直接命中历史答案。实测在问答类场景里,语义缓存能省30%到50%的token花费,很值得加进生产链路。
在做AI工程这条路上走了这么久,我一直向身边新人强调一个顺序:先用已有的大模型API把业务闭环跑通,再慢慢替换成自部署模型;先用最简单的向量检索把Demo搭起来,再逐步加入混合检索和重排。不是所有项目一开始就需要最复杂的方案,从简单出发才能让你快速发现问题、找到真正的瓶颈,而不是被花里胡哨的架构拖住了脚步。这套RAG系统就是最好的例子——它没有用到任何高深的技术,但每一步都是工程决策。以后你还会碰到的多模态、Agent、微调,本质上都是在同样的工程思路上,迭代和扩展能力。