
AI工程化从零搭建一份完全自学路线与实战拆解AI工程ai-engineering这个词最近确实够火但火归火真正能说清楚AI工程师到底干什么的人少之又少。不少人以为AI工程就是调调API、套套LangChain或者跑通几个开源模型就算入门了。实际上AI工程的核心是把模型能力落地成稳定、可维护、可评估的系统它横跨数据工程、模型服务、检索架构、评测反馈和部署运维这套东西如果不成体系地学迟早会在真实项目里翻车。这篇文章想用ai-engineering-from-scratch这个思路完整拆解一条从零开始构建AI工程能力的路线图我会从一个真实可落地的项目——基于RAG加Agent的智能知识库问答系统——出发把规划、数据层、模型层、检索层、评估层、部署层的每个环节怎么想、怎么做、为什么这么做讲透。内容会比较长适合那些已经会写Python、跑过一些模型但总觉得AI工程很虚、无从下手的开发者如果你只是想看一份调库清单或者三天速成套路那这篇文章不太适合你因为你真正缺的不是代码片段而是工程判断力。1. 项目设计与思路拆解1.1 为什么从零而不是选框架我见过太多人一上来就直接上LangChain、LlamaIndex、Dify这类框架结果写完demo之后一遇到业务问题就完全懵住不知道数据是怎么切片的、不知道向量召回为什么不准、不知道Prompt为什么经常漏答。问题出在哪出在你把框架当成了黑盒框架帮你省掉了从零思考的过程但也同时剥夺了你在过程中建立心智模型的机会。from scratch的核心价值不是让你拒绝框架而是先学会手工搭建一条最精简的链路把每个环节的原理和瓶颈摸清楚之后再用框架提速时你一眼就能看出框架背后到底做了什么。就像学车一样手动挡开熟了再开自动挡没有难度但只开自动挡的人永远不知道换挡逻辑是什么。我建议的首个项目是AI知识库问答助手喂给它几份产品文档或内部手册它可以基于这些资料回答具体问题并附上引用来源再叠加一个轻量级Agent能力让它在必要时调用计算器、查天气或查数据库。这个项目麻雀虽小五脏俱全几乎覆盖AI工程的所有核心环节文本解析、切片、向量化、检索引擎、提示词管理、输出解析、工具调用、评测回归、部署上线。1.2 整体架构与模块边界整个系统的架构我画过很多次每次给团队讲我都会强调一句话不要把模型当成架构的中心把数据和业务场景当中心。具体来说这个项目的整体链路包含6个模块资料接入层负责把PDF、Word、Markdown、HTML等不同格式的文档统一解析成纯文本并保留元信息标题、页码、来源、更新日期。切片与向量化层把长文本切分成合理粒度的片段chunk用Embedding模型转成向量写入向量数据库。检索引擎层实现向量相似度检索、关键词检索、以及两者的混合检索Hybrid Search并做重排。生成层把检索到的片段和用户问题组合成Prompt交给LLM生成带引用的回答。Agent与工具层让LLM能够识别什么时候需要调用工具比如需要实时数据时去查天气API需要精确计算时调用Python解释器。评测与观测层对回答质量做离线评测、在线日志分析并持续优化。如果你的项目只有100个文档、10个用户没有必要上微服务架构设计要跟着体量走。项目初期我把所有模块放在一个FastAPI服务里数据存储只用SQLite加一个Qdrant向量库足够了。学到后期你自然能分辨哪些模块需要拆出去独立部署向量检索流量大了可以独立成一个检索服务文档解析耗时长了可以拆成异步worker评测服务则可以做成离线任务而不是在线API。架构不是越复杂越好而是在可维护和可演进之间找一个务实平衡点。2. 核心细节数据预处理与切片策略2.1 文本解析你以为的PDF转文本没你想的那么简单第一个坑往往就出现在解析文档上尤其是PDF。PDF本质上是一个排版描述语言它记录的是哪个字符画在哪个坐标而不是哪句话属于哪个段落。所以PDF转文本经常出现三种问题文字乱序多栏排版时文字块会按坐标顺序输出导致左右栏内容互相穿插段落断裂标题和正文、表格和说明被错误地拼接或切开公式乱码数学公式、化学式解析后基本不可读。我踩过的坑第一版解析代码直接用PyPDF2抽取文本结果把一个技术文档的URL链接和错误码说明切得稀碎检索出来的内容张冠李戴。后来换成了pypdf加pdfplumber组合方案先用pypdf快速抽取整体文本遇到文本抽不干净比如扫描件、表格混乱的文档再用pdfplumber按坐标区域精细提取。对于扫描版PDFOCR方案一般优先选PaddleOCR识别效果在中文场景下明显优于Tesseract但注意PaddleOCR对机器配置有要求CPU环境下速度偏慢。实操建议做一个解析落盘检查。解析完之后把纯文本文件打开看一遍跑一个文字密度检查如果某一页解析出的字符数异常少大概率是解析失败需要标记人工复核而不是默默跳过。哪怕你的文档总量很大这一步也千万不能省——脏数据进脏答案出这是检索增强生成系统里最隐蔽也最致命的缺陷。2.2 切片策略没有正确的chunk大小只有适配场景的chunk大小文本切片是RAG系统里最容易被低估的环节。很多教程直接告诉你固定切500个字符重叠100个但真实项目中这个参数直接决定你检索质量的天花板。切得太小比如200字符会导致语义不完整一句话被拦腰切断向量表示就很奇怪切得太大比如2000字符又会导致检索命中范围过大里面可能包含大量噪声信息直接影响生成质量。我推荐的分层策略是优先以文档结构为边界其次才考虑字符数。具体做法是先用文档的标题层级通过Markdown的#符号或PDF的书签结构把文档切分成章节块再把超过上限的章节块继续按段落语义切分。每个chunk保留一个小的上下文窗口用于生成侧拼接。实际参数上我一般从每个chunk 300-500个中文字符overlap 50-100字符起步然后再针对具体文档类型调整。这块一定得做实验对比不能拍脑袋。评估指标可以选一个召回命中率人为准备20道必须从某份文档中找到答案的问题分别用不同的切片方案去检索统计答案内容出现在Top-5 chunks里的比例。这个实验只需要半天时间但会给你非常多真实的体感。2.3 Embedding选型与向量库选择Embedding模型的选择会影响检索质量的基线。中文场景我用下来比较稳的有几类开源的bge-large-zh-v1.5、bge-m3以及各家的商业化Embedding API。如果你的项目以中文为主bge-m3在大多数任务上表现很不错而且它支持100种语言左右对混合中英文文档的场景非常友好。Embedding模型也有降级策略如果你的文档领域非常垂直比如法律文书、医疗术语开源通用模型往往不够好用一个实用方案是先用通用模型做检索再用一个小的分类器或者人工标注去校准重点术语的语义关系。说实话工程实践中很少一来就微调Embedding模型——因为你需要先攒够足够的query和doc对这个数据成本往往比你想的高得多。向量数据库的选择我的建议是中小项目直接用Qdrant或ChromaDB本地运行用户量上来了再评估Milvus如果是公有云部署可以直接用云上向量检索服务。尽量避免在这个阶段陷入对某个数据库的盲目信仰——你真正要评估的是API复杂度、过滤能力比如按文档ID过滤、增量更新能力以及是否支持混合检索。3. 实操过程从0到1实现一个混合检索问答服务3.1 数据管道实现下面给你一份我实际项目中用过的数据管道骨架代码做了简化但核心逻辑保留了# ingest.py - 文档摄取流水线 from pathlib import Path import re from typing import List, Dict from pypdf import PdfReader def extract_text_from_pdf(path: str) - List[Dict]: 抽取PDF文本返回每页的文字和页号 reader PdfReader(path) pages [] for idx, page in enumerate(reader.pages): text page.extract_text() or # 去除非可见字符保留基本标点 text re.sub(r[\x00-\x08\x0b\x0c\x0e-\x1f], , text) pages.append({page: idx 1, text: text}) return pages def smart_split_chunks(pages: List[Dict], chunk_size: int 400, overlap: int 60) - List[Dict]: 按页内语义段落切片超长段落到固定窗口 chunks [] for page in pages: text page[text] # 先按换行符和句号切段 segments re.split(r(?[。.\n]), text) buffer for seg in segments: if len(buffer) len(seg) chunk_size: if buffer: chunks.append({ text: buffer.strip(), page: page[page], chunk_id: fp{page[page]}-{len(chunks)} }) buffer seg else: buffer seg if buffer: chunks.append({text: buffer.strip(), page: page[page], chunk_id: fp{page[page]}-{len(chunks)}}) return chunks这段代码的关键是smart_split_chunks里用了正则(?[。.\n])来做句边界后切分这样能保证切片尽量落在完整语义单元之后而不是硬生生砍断一句话。实测下来这种基于中文标点的软边界切分比固定字符数切分在后续检索效果上稳定很多。切完的chunk要进向量库而进库前一步需要去重。我踩过的一个真实问题同一个文档被反复执行ingest向量库里出现大量完全相同的chunk导致检索结果里同一个答案重复出现两三次非常影响可信度。解决办法是在入库前对每个chunk算一个哈希值存到元数据里写入前先查一遍。3.2 混合检索的实现与参数选择混合检索这个词看起来高级本质上就是向量检索擅长语义相似召回关键词检索擅长精确匹配比如产品型号T-800这种字符串两者合并后通过重排Rerank把最相关的结果顶到最前面。向量检索我用Qdrant实现查询逻辑是# search.py - 混合检索 from qdrant_client import QdrantClient from qdrant_client.models import Filter, FieldCondition, MatchAny client QdrantClient(path./local_qdrant) def vector_search(query_vec: List[float], top_k: int 10, doc_ids: list None): query_filter None if doc_ids: query_filter Filter(must[FieldCondition(keydoc_id, matchMatchAny(anydoc_ids))]) hits client.search( collection_nameknowledge, query_vectorquery_vec, query_filterquery_filter, limittop_k ) return [{id: h.id, score: h.score, payload: h.payload} for h in hits]关键词检索部分不建议为了省事直接跳过。我一般用基础版BM25算法实现上有现成库rank_bm25只需要把全部chunk的文本输入进去建索引即可。查询时计算BM25分数然后和向量检索的分数做归一化加权合并最后的排序分数大致这样算def hybrid_score(v_score: float, b_score: float, weight_v: float 0.7) - float: # 分数归一化到0-1区间后融合 norm_v 1.0 / (1.0 v_score) # 距离分越小越好转换到越大越好 norm_b b_score / max_b_score # 除以最大的BM25分做近似归一化 return weight_v * norm_v (1 - weight_v) * norm_b这个融合公式里最关键的是归一化方式。向量库返回的分数类型每个库不一样有的返回余弦相似度越大越好有的返回距离越小越好你不能直接拿原始分数去加权必须先做scale。我在这个环节花了很长时间调试最终建议你直接测试两三种归一化方法选择让人工标注的正确结果排名最靠前的那一组。重排环节如果你有预算可以用专门的rerank模型比如bge-reranker它会为每个查询-文档对打一个相关度分数效果非常明显但要注意它是pairwise的线上实时重排的性能开销比较大。中小项目可以用手动规则重排比如优先展示文档来源级别更高的结果、优先展示与query有标题关键词匹配的结果。这种规则虽然土但可控性更强也不会拖垮延迟。3.3 生成层的Prompt构造与管理检索做得再好生成层拉胯一样白搭。我见过最典型的问题就是Prompt写得像废话请根据以下内容回答问题。内容... 问题...。这种Prompt在简单测试上够用但面对真实用户问题会频繁出现编造答案漏掉要点不给出处等情况。我建议的Prompt骨架包含四部分系统指令人设和行为边界信息检索到的参考片段按相关性排序标注来源用户问题原样保留不做改写输出格式要求最好是结构化的输出比如JSON实际用的Prompt类似下面这样SYSTEM_PROMPT 你是一个严谨的知识库问答助手。请基于参考资料中的内容回答用户问题。 要求 1. 如果参考资料中没有足够信息请直接回答当前资料中未找到相关内容不要自行编造。 2. 回答末尾用[来源:文件名-页码]的形式标注引用来源引用必须真实对应参考资料片段。 3. 如果用户问题需要在多个资料片段间做总结请分点陈述每点都标注来源。 build_prompt lambda query, chunks: f 【参考资料】 {.join(f[{i}] {chunk_text} for i, chunk_text in enumerate(chunks))} 【用户问题】 {query} 请按要求输出 .strip()这里有个容易被忽视的细节参考片段最好带序号[0]、[1]...而且Prompt里明确告诉模型标注来源时用序号这样回答里的引用能精确对应到具体的chunk。如果你不给序号模型很可能会自己脑补一个引用来源那就完全失去可信度了。3.4 Agent机制工具调用的落地方案Agent部分是这个项目的加分项。有了工具调用能力系统才能回答帮我算一下A方案比B方案成本高多少这种需要实时计算的问题或者今天上海天气适合办户外活动吗这种需要外部数据的场景。我的实现思路是不依赖复杂的ReAct Loop框架用最朴素的函数调用Function Calling协议。以OpenAI兼容接口为例你先定义工具schema让模型决定是否需要调用以及传什么参数tools [ { type: function, function: { name: calculate, description: 执行数学计算支持四则运算和函数, parameters: { type: object, properties: { expression: {type: string, description: 数学表达式例如 (12000 5000) * 0.85} }, required: [expression] } } } ]然后在对话中先让LLM决定动作解析出工具调用参数后执行对应函数把结果追加到上下文里再让LLM生成最终回答。整个过程代码量不多却非常有学习价值——你手动实现一遍之后再看LangChain的Agent模块会瞬间通透很多。关键注意点工具结果返回给模型时也要保持结构化的格式不要直接丢一个计算成功这种含糊信息要把具体结果值和可能的错误信息打包返回。另外工具调用一定要设置最大迭代次数比如最多允许3轮调用否则模型可能在一个问题上循环死转甚至反复调用同一个工具。4. 评测体系与模型选型4.1 为什么评测是AI工程的地基工程很多教程只讲怎么搭系统不讲怎么判断系统好不好。但在真实的工程环境里没有评测体系你的系统就是一块没法迭代的石头。你可能改了切片参数以为效果更好上线之后用户却反馈更差了没有评测基准你甚至不知道问题出在检索、Prompt还是模型。我搭评测体系分三层端到端问答评测准备50-200道真实业务问题标注标准答案或判断条件。每次改动后跑一遍看正确率和引用准确率检索质量评测单独测Top-N召回命中率判断问题出在检索还是生成在线对话日志分析上线后每天分析用户反馈、拒绝率、无效回答占比作为迭代依据。最简单的评测集格式大概长这样[ { id: 1, question: 退款申请后多久能到账, must_contain: [3个工作日, 原路退回], source_doc: 退款政策.pdf } ]跑评测时用must_contain做关键词命中判断虽然不够完美但它快速、可复现、不依赖裁判模型。等到评测集足够多、场景足够复杂时再用一个强模型当AI裁判给回答打分。这个循序渐进的方案适合大多数人。4.2 模型选型开源还是API关于选模型我提供一套务实的判断标准而不是哪个火用哪个数据隐私要求高、必须本地部署优先考虑Qwen系列或ChatGLM系列7B-14B量化后的模型在消费级显卡上能跑起来中文效果够用。需要最强推理/工具调用能力直接用商用模型API。尤其涉及复杂理解、长上下文总结时商用模型目前仍然明显领先开源。预算敏感且文档不是很复杂开源小模型加好一点的RAG管线效果完全够用真正卡脖子的一般是领域特殊而非模型智力。我在实践里体会最深的一点是先别纠结大模型选型先用你能最快拿到的模型把端到端链路跑通然后花时间建评测集。因为模型是可以随时替换的组件但评测集是资产是你能在后续迭代中比较哪个模型适合的唯一底气。5. 部署落地与常见问题排查5.1 从本地脚本到服务化部署在真实项目中部署不是跑一个FastAPI就完事你必须为上线考虑几个问题。并发与队列如果文档解析和向量化是耗时操作需要一个异步任务队列。我常用简单方案是fastapi-background加Redis队列任务状态存数据库前端轮询如果任务量再大再引入Celery或Arq。只要你提前把任务拆分好了迁移成本不高。缓存策略常见的query如果每次都重新检索和调用LLM成本和延迟都受不了。我给相似度大于0.95的query加了一层简单缓存命中后直接返回上次结果。这个策略对重复问题占比高的场景尤其管用能省一半以上的模型调用费用。配置管理API Key、数据库地址、模型端点一律走环境变量或配置中心绝对不硬编码在代码里。我见过不止一次因为硬编码Key导致的内网配置泄露事故这种问题一次都不能出。推荐的基础部署架构是FastAPIWeb层 Qdrant向量库 PostgreSQL元数据、用户数据、评测记录 Redis缓存和队列全部用Docker Compose编排。这套结构在单机扛住日活几百到一两千的问答场景没有问题再往上有需要再拆服务。5.2 真实项目踩坑记录我把自己在搭建过程中踩过的一线问题列成一个速查表方便你对照排查。症状可能原因排查思路与解法检索结果明显不相关切片粒度过大或过小、Embedding模型不匹配领域先固定一个query打印Top-10的chunk内容看看是否语义接近再微调chunk参数回答经常编造Prompt缺少不知道就直说约束、参考片段排序混乱强化Prompt的系统指令检查是否把低分chunk也塞进上下文加上来源标注要求前后两次回答不一致模型温度过高、检索结果不稳定降低temperature到0.2左右稳定检索排序避免无谓的Prompt随机性向量库里的数据反复重复管道缺少幂等控制给每个chunk加内容哈希写入前查询去重文档更新时先删旧doc_id再入库生成延迟高Prompt过长、模型过大、未走缓存压缩prompt里的参考片段数量限制最多取Top-4加query缓存量化模型或换更小的模型PDF解析结果乱序双栏排版、表格复杂用pdfplumber按坐标块提取对扫描件走OCR流程解析后做人工抽样检查5.3 成本与性能的平衡实操AI工程落地最大的拦路虎往往不是技术本身而是成本。我算过一笔账如果一天有2000个用户提问每个问题平均消耗大模型输出300个token用中档商用模型API月成本大约在一两千元左右如果加上向量化、rerank和缓存后的降耗实际还能再省30%-40%。一个实用的成本优化思路是分层路由简单问题比如产品功能介绍走小模型或预设答案库复杂问题比如多文档综合对比才走大模型。这个策略对用户体验影响极小但成本降幅明显。性能侧要区分耗时在检索还是生成。检索侧P95如果超过200ms优先查向量库索引和重排环节生成侧P95如果超过5秒主要瓶颈是模型速度和输出长度。你可以用流式输出大幅改善用户体感让用户先把内容读起来等待感知会低很多。6. 复盘与后续扩展6.1 持续迭代的闭环方法论这个项目从零搭建完成之后真正让系统和玩具Demo拉开差距的是你是否坚持做迭代闭环。我建议你对每次Prompt改动、切片策略调整、模型替换都打一个版本号在评测集上跑全量回归把分数变化记录成一张表格。不要相信感觉变聪明了这种直觉判断——没有数字佐证的优化在团队协作里站不住脚。我自己的做法是开发一个极简的评测脚本每天CI里跑一次。脚本输出三列数字召回命中率、引用准确率、回答规范率。这三项的变动趋势直接告诉你系统的健康度。一旦某次改动导致指标下滑立刻回滚不让疑似优化的代码停留在生产环境。6.2 下一步可以扩展的3个方向这个项目完成后如果你的目标是进阶到更专业的AI工程领域我从个人经验角度推荐这三个方向每个都能让你有全新的认知升级从RAG走向Agent系统让LLM自己规划任务、写代码、执行并验证结果。这个方向的技术栈会涉及更多可靠性和安全性的讨论比如如何做断点恢复、如何设计工具访问控制、如何防止Agent跑偏。从离线评测走向在线反馈闭环给你的系统接上用户反馈按钮把踩的数据回流成评测集形成数据飞轮。这也是一门非常深的数据工程学问。从通用模型走向领域专用攒一批领域敏感数据和标签对Embedding模型或LLM做微调。很多垂直行业法律、医疗、金融的实际问题微调模型带来的效果提升远超Prompt技巧。我在多轮实践中的体会是AI工程的瓶颈永远不是某个模型有多聪明而是你有多了解自己的数据和业务边界。数据在哪里、噪声有多大、用户真正想要什么这些才是工程上最难得的判断力。希望这套from scratch的路线图和实战细节能帮你在AI工程这条路上少踩几个深坑真正建立起自己的技术判断体系。