这条“从零构建AI工程”的路,很多人问过我怎么走。市面上关于AI的内容,大部分是两类:一类是教你调API的“速成教程”,另一类是讲模型原理的“学术论文”。两者之间其实横亘着一条很宽的、真正决定项目能否落地的鸿沟——工程化。我自己的实践路径,就是围绕“ai-engineering-from-scratch”这个项目展开的,说白了就是不着调参训练,也不止于调接口,而是把提示工程、检索增强、评估体系和部署优化这条链路,自己从地基开始一层层亲手搭起来。这篇文章不是复刻某节课的大纲,就是把我趟过的路、踩过的坑、最后留下来的代码骨架,按实操顺序给你捋一遍。
如果你正在做AI应用开发,或者准备把LLM能力接入自己的业务系统,但总觉得“按教程做出来是能跑,换个场景就不知道怎么办”,那这篇文章应该能帮你补上中间那段最关键的工程思维。我会把为什么这么设计、每个环节的取舍逻辑、以及实测下来真正能用的配置都给你,而不是只丢一堆概念让你自己猜。
1. 项目整体设计与“from scratch”的边界界定
先明确一下“from scratch”这个词在我这个项目里的定义。它不是让你从反向传播开始手写一个Transformer,也不是让你用pip装几个包然后调通一个聊天接口就完事。我理解的“AI Engineering from Scratch”,指的是不依赖任何封装好的、开箱即用的“AI应用框架”,把所有LLM应用的核心环节——数据处理、检索链路、提示构造、评估验证、服务封装——都用最基础的组件自己搭一遍。
1.1 核心需求解析:这个项目到底要解决什么问题
我给自己定的目标是:不用LangChain这类重量级框架的“高级抽象”,只用各模块最核心的底层库(比如用OpenAI SDK直接调模型、用SentenceTransformers自己做向量化、用向量数据库原生API自己写检索逻辑),从零拼出一个可以应对“私有知识库问答”场景的完整系统。
为什么非要自己造轮子?我用LangChain搭过好几个Demo,很快,真的很快,但后面遇到问题就傻眼了。有人可能会问,那你搭完这个“from scratch”项目,最后跟你直接用LangChain做出来的东西,功能上有什么区别?功能上区别不大,但你对系统的掌控力是完全不同的。我自己面临这些问题:检索结果不对,你不确定是Embedding模型选错了,还是Chunk切分的策略有问题,还是向量索引的相似度算法选得不对——如果全用封装好的框架,你只能看到最终结果,中间每一层都是一个黑盒。自己从零搭一遍,等于给每个黑盒开了天窗。
这个项目最适合的受众,是那种“已经会用Prompt调模型,也跑通过几行SDK,但面对真实业务数据时总觉得心里没底”的开发者。它不适合压根没写过代码的人,也不适合只想最快速度出Demo交差的人——后者直接用现成框架就好,效率最高。
1.2 技术选型与方案设计考量
整个项目的技术栈,我是在综合了国内网络环境的可得性、成本、以及可替换性之后才定下来的。核心选型如下:
- 基座模型:用兼容OpenAI接口格式的国产模型服务(比如智谱GLM、通义千问等),这样代码里写的是统一的SDK调用方式,万一哪家服务出问题,换另一家只需要改一个base_url和api_key变量。
- Embedding模型:第一版用OpenAI的text-embedding-3-small做基准对比,后面切换到开源的BGE-M3或者M3E这类中英双语Embedding模型,方便在本地私有化部署。这一步的思考是,Embedding选择直接决定后续检索的上限,后面我会展开讲。
- 向量数据库:先用FAISS做本地原型验证,因为它轻量、不需要起一个独立服务,适合开发调试。等到数据量超过几十万条、需要并发读写时,再迁移到Milvus或者Qdrant这样的独立向量数据库服务。
- 工程语言:Python,这个不用纠结理由,AI工程领域Python的资源密度是其他语言没法比的。
这个选型背后有个核心逻辑:每一项都有“Plan B”。我对开源社区的项目有个原则——绝不把自己绑死在任何单一厂商的私有协议上。ChatGPT全家桶好用,但万一业务方要求数据不出内网,你怎么办?所以从项目一开始,我就在接口层和数据层做了抽象,真正做到了“随时可以换”。
2. 数据处理与知识库构建全流程
从零构建AI应用,第一步不是写Prompt,也不是调模型,而是搞定数据。这句话我反复跟人说,因为绝大多数人把顺序搞反了。LLM应用的本质,是在模型先验知识和你的私有数据之间搭一座桥——桥搭不好,模型再聪明也没用。
2.1 数据清洗与格式标准化处理
我拿到的第一批测试数据,是从公司内部Wiki导出的几百篇技术文档,格式那叫一个乱:有Markdown,有PDF扫描件,有Word文档里嵌的表格,甚至还有几个PPT。格式化是整个流程里最脏最累的活,我把它拆成三步来完成。
第一步是统一文本抽取。PDF用PyMuPDF(fitz)把每一页的内容抽出来,表格结构用camelot处理;Word文档用python-docx直接读段落和表格;Markdown相对简单,但要小心代码块和特殊字符。第二步是清理“噪声内容”——页眉页脚、重复的导航栏文本、文档里夹带的Base64图片编码,这些都是检索时最容易干扰语义的垃圾信息,必须用正则批量干干净。第三步是我自己踩坑才加的:统一编码格式。有几个老文档是GBK编码的,不转成UTF-8的话,后面向量化的结果会出一堆“乱码向量”,检索效果被拖下去一大截确实不划算。
2.2 Chunk切分策略与参数调优
数据清理完,接下来是分块(Chunking)。这一步的决策会直接影响后续检索的精度,我自己的第一版做得比较粗暴——直接按每512个字一段切死,结果检索出来的片段经常前言不搭后语。
我后来整理出的组合策略是“结构感知切分 + 滑动窗口重叠”。具体来说:
- 优先按Markdown的标题层级(#、##、###)作为天然切分边界,把每个标题下的内容视为一个语义块;
- 如果某个标题下的内容过长(超过800字),再按段落边界二次切分;
- 每个Chunk之间保留30~50个字符的重叠区,保证跨Chunk的上下文不丢。
这里我提一个关键参数——chunk_size和overlap的比例大概控制在15:1到20:1之间。我刚开始把重叠设成100字符,结果一个知识点被重复塞进好几个Chunk,检索去重工作直接翻倍;后来调成30-50字符,效果好很多。这个数值没有绝对的黄金标准,跟你数据的平均段落长度强相关,用我上面给的初始值跑一遍,再结合检索测试结果微调就行。
做完分块之后,我给每个Chunk做了一套“元数据标记”:来源文档编号、所属章节路径、文档更新时间。这套元数据在后面做过滤检索和结果展示的时候帮了大忙——用户问“去年上线的那个功能怎么配置”,我可以直接用时间元数据过滤掉过时文档,这是纯向量检索做不到的。
3. 检索增强生成(RAG)链路实现
数据准备好了,接下来就是RAG的核心链路。我对RAG的理解,通俗点讲就是:用户问一个问题,大模型没见过你的内部文档,所以你需要先在一堆企业文档里找出跟问题最相关的几句话,然后把这几句话连同问题一起喂给大模型,让它“看着材料回答”。整个RAG链路的核心竞争力,就藏在这“找出相关几句话”的环节里。
3.1 Embedding选型与向量化实现
Embedding是整个检索链路的地基,它的作用就是把一句话或一段话转换成一串高维向量——语义越接近的内容,在向量空间里的距离越近。这段话解释起来很学术,但实际操作里就是一件事:选对模型,才能让“让检索到的东西真正相关”这件事成为可能。
我自己做了几轮对比实验,发现不同Embedding模型在中文技术文档上的差异大得吓人。我量化成一个直观的对比结果如下:
| 模型 | 维度 | 中文语义理解 | 检索Top-5命中率 | 备注 |
|---|---|---|---|---|
| text-embedding-3-small | 1536 | 良 | 78% | 需要联网API调用 |
| text-embedding-3-large | 3072 | 优 | 84% | 成本高、维度大、延迟高 |
| BGE-M3 | 1024 | 优 | 87% | 开源、支持本地部署、多语言 |
| M3E-base | 768 | 良 | 75% | 轻量、速度快 |
最终我选了BGE-M3做主力,原因很简单:检索效果最好且能本地部署。不过Embedding模型选完不是一劳永逸,不同领域文档有不同表达习惯,选好模型后一定要用你自己的文档跑一批检索验证,让测试结果来拍板,用模型跑一批检索看效果再定。
向量化流程本身不复杂,核心代码如下(基于FlagEmbedding库):
from FlagEmbedding import BGEM3FlagModel model = BGEM3FlagModel('BAAI/bge-m3', use_fp16=True) chunks = load_all_chunks() # 加载前面处理好的文档分片 embeddings = model.encode( chunks, batch_size=32, max_length=1024, return_dense=True, return_sparse=True ) # 保存向量供后续构建索引使用 save_vectors(embeddings['dense_vecs'])这里有个细节要注意:BGE-M3同时支持稠密向量和稀疏向量,这个概念下节会展开讲。另外跑向量化的时候我习惯用use_fp16=True开半精度,能用更少显存跑更大批量,而且对结果精度几乎没有可感知的影响。
3.2 混合检索与重排序机制
做AI工程跟做家常菜有点像,最开始用单一检索方式出来的结果往往不够“有滋有味”,纯粹靠向量相似度做召回,经常出现结果“沾边但不精准”的问题。比如用户问“数据库连接超时怎么办”,向量检索只找语义相近的句子,结果召回了“数据库连接被拒”——方向对,但没命中要害;如果想精确匹配某个报错码或接口名,向量又不如关键词靠谱。
这个问题我在第一版就撞上了,逼着我上了第二套检索方案:混合检索(Hybrid Search),就是同时用“语义相似度”和“关键词匹配”两条腿走路。具体实现是:
- 向量检索通道(稠密检索):用前面算好的Dense Vector做余弦相似度召回,负责理解语义。
- 关键词检索通道(稀疏检索):用BGE-M3算出的稀疏向量(Sparse Vector)作为补充,它本质上暗含了BGE论文里结合BM25的思路,能精准命中包含特定术语和编号的Chunk。
- 两路结果合并,用Reciprocal Rank Fusion(RRF)做分数融合,把两边的排名综合出一个最终分数。
- 再做一层“重排序”(Rerank),用一个Cross-Encoder模型对候选人逐对打分,把最相关的几个排到最前面。
写到这里你可能觉得链路好长,每个环节都挺费时间——但你真把每一步做扎实了,检索效果是从60分到90分的跨越,这层功夫省不了。
Rerank这一步,我用的是bge-reranker-base,模型体积不大但效果立竿见影。处理流程就是一次query跟10个候选Chunk拼成对,让模型输出一个相关度分数,然后按分数重新排序。Top-5命中率从混合检索的87%又能往上涨不少。
3.3 提示词动态构造与注入策略
检索把材料捞上来以后,第二个核心环节是“怎么把材料和大模型结合起来”。很多人的第一反应是直接把所有检索结果一股脑拼进Prompt——这其实是个大坑:检索出来的10个Chunk里往往有两三个是不太相关的,模型会被这些干扰项带偏,顺着不相关的上下文信口开河。我的策略是重排序后只取Top-3到Top-5个Chunk,并在Prompt里明确告诉模型“只用以下资料回答,不要掺入你的先验知识”。
动态构造的Prompt模板长这样:
你是一个企业知识库助手。请严格按照【参考资料】中的内容回答用户问题。 规则: 1. 如果参考资料中找不到明确答案,直接回复"资料库中未找到相关信息",禁止编造。 2. 回答时需要引用对应参考资料编号,格式如[1][2]。 3. 不要回答与参考资料无关的内容。 【参考资料】 [1](第1个Chunk内容) [2](第2个Chunk内容) ... 【用户问题】 (这里的用户问题原样插入)这套Prompt在一次测试里把一个最容易触发幻觉的问题“你们平台支持导出哪些格式”的回答准确率从62%拉到了95%以上。核心在于规则设计:给了模型一个“不知道就直说”的出口,模型沿着“只能说知道的东西”这个安全线作答,幻觉比例自然就掉下来了。
4. 评估体系与生产化部署
链路搭通了、演示也能跑了,别高兴太早,第二个深水区是“你怎么证明这个东西效果是好的”。凭感觉看几条回答就拍板,这是Demo思维;真正要做AI工程,你需要一套能源源不断给出反馈的评估体系。
4.1 离在线评估矩阵与指标解读
我参照RAGAS的评估思想,设计了一套自己的评估体系,用三大核心指标来衡量链路质量:
- 忠实度(Faithfulness):检查回答是否严格基于检索到的上下文,有没有“自由发挥”。这个指标我通过在Prompt里要求模型给自己“引用打分”来近似计算,简单说就是看回答里的关键论断能不能在检索到的原文里找到出处。
- 答案相关性(Answer Relevance):回答是否真正针对用户的问题,而不是自我发挥写了一堆废话。用一个较小的模型给“(问题,回答)”的组合打分,我实测下来跟人工判断的相关性在80%以上。
- 上下文召回率(Context Recall):给定标准答案,看看检索到的Chunk里包含多少能支撑答案的内容。这个指标是检索环节的核心体检表。
评估数据集的建设,我采用的思路是“先人工后增强”:人工挑出50个有代表性的用户问题,逐条手工写好标准答案和参考依据,再让大模型基于文档库仿写200个同风格问题,跑批量评估。每一次链路调整之后,就用这套数据集回归一遍,拿分数对比来确认到底哪次优化是正向的。
4.2 服务部署、并发优化与成本控制
评估没问题之后才进入部署环节。我采用的是FastAPI做服务封装,拆成/retrieve(只检索)和/chat(完整RAG问答)两个接口。为什么要拆两个?因为实际业务里有些场景只需要检索不需要生成,比如搜索引擎式问答;拆开之后两个接口各自的负载特征更清晰,后续按需做资源扩缩容也更方便。
并发层和成本层有几个关键优化,实测下来效果很明显:
- Embedding结果缓存:问答型场景里,用户问题经常是相似的(比如“怎么导出Excel”和“如何导出Excel表格”),如果每次都重新向量化,纯属浪费。我用Redis做了一层Embedding缓存,把文本hash和向量存起来,命中率实测在25%左右,直接省了四分之一的向量化成本和时间。
- 语义缓存:再进一步,如果新问题跟某条历史问题在语义上足够接近(余弦相似度>0.95),那连检索和LLM调用都省了,直接把缓存里的历史回答返回。这个策略能用Redis加一个简单的向量索引实现。
- Prompt缓存:对完全相同的Prompt,模型服务商侧一般有API层面的自动缓存计费优惠。即便用的第三方服务不支持,也可以自己按Prompt哈希做结果缓存。
部署时的详细参数我也总结了,拿来就能参考:
| 项目 | 推荐配置 | 说明 |
|---|---|---|
| 并发上限 | 与模型API限流对齐,预留20% buffer | 避免大量请求排队超时 |
| 超时设置 | 检索3s、生成30s、整体45s | 超时直接失败快速返回,别让用户干等 |
| 记忆/上下文窗口 | 4轮对话摘要 + 当前问题 | 历史太长会冲淡检索结果占比 |
| 连续对话 | 只携带“问题改写后的检索结果” | 不携带原文历史,省Token |
4.3 上线后的监控告警与异常发现
部署只是开始,上线后你必须盯着它。我搭的监控体系分三层——最底层是基础设施监控,CPU/内存/GC这些指标写个脚本定时检测就行,用现成Prometheus+Grafana最省心。中间层是AI链路专属监控,比如“检索耗时p95”“向量化耗时”“Rerank耗时”这些跟生成模型强相关的指标。最高层是业务质量监控,这里有一个我强烈推荐必须加的指标:“检索失败率”,也就是Top-3检索结果的平均相似度低于某个阈值的比例。这个指标一旦显著上升,几乎可以断定知识库里的数据或者Embedding链路出了问题——它经常比用户投诉早上好几个小时发出警报。
我上线后第一次真实事故,就是靠这个指标救回来的。数据更新进库时格式出了兼容性问题,新增的几百个文档向量化出来全是不合理的向量,检索质量直线下降。用户那边感知还不明显的时候,监控面板上的“检索失败率”已经跳红了——我赶在投诉产生之前就把数据回滚修复了。
5. 踩坑实录与排查方法速查
最后这部分,我整理一下整个项目从零搭建过程中遇到过的典型问题。这些问题我基本都在网上翻了大量资料、踩过坑之后才总结出来的,直接给你按“症状—原因—解法”列个速查表,希望能帮你少走几小时弯路。
| 症状 | 根因分析 | 解决措施 |
|---|---|---|
| 检索结果看似相关但答非所问 | Chunk切分把完整语义切断,导致上下文不完整 | 换用结构感知切分,增加重叠区;或者调整Rerank阈值 |
| 相似问题结果有显著差异 | 没开随机种子或温度参数过高 | 生成时固定temperature=0.2以下,top_p设为0.9 |
| 回答总说“资料中没有”但文档里有 | 检索召回率不够,正确答案没进入Top-K | 增大检索候选数,混合检索中给关键词通道更高权重 |
| 某个领域的文档几乎不被检索到 | Embedding模型对该领域术语不敏感 | 换垂直领域Embedding模型,或微调专用Embedding |
| 系统响应极慢,吞吐上不去 | 向量化或Embedding调用占了大量时间 | 加缓存;检索和生成异步化;用FP16推理加速 |
| 数据更新之后效果反而变差 | 新旧数据向量语义冲突或脏数据污染 | 新增数据先跑一轮质量校验(相似度分布检查),再手动触发向量索引重建 |
有了这套速查表,排查问题基本能按图索骥,不用每次从头疑神疑鬼。
再单独说一个我在“AI工程from scratch”过程中最有价值的体会:这条路线真正的收获,不是省了哪个框架的License费,而是你把AI应用的每一个环节都“亲手摸过一遍”之后,对系统的直觉灵敏度完全不一样了。你不再需要靠猜来定位问题——看到现象就能大概率判断是哪一层出的问题,这个能力上的跃迁,远比你照着教程搭出的那一堆“会跑的Demo”值钱得多。
最后再分享一个小技巧:把整个“从零实现”的工程过程写成技术笔记,每个环节的对比实验数据、踩坑过程、最终参数决策都记录下来。这份笔记既是你的个人知识库,也能成为面试和对外分享时最有说服力的“实战资产”。如果你也打算从零构建一次AI工程,我强烈建议你也记下属于自己的那份“踩坑录”。