1. 从零搭建AI工程能力:为什么我劝你别一上来就调包
这两年“AI工程”这个词被说得太多了,多到有点变味。打开任何一个技术社区,满屏都是“三行代码调用大模型”“十分钟搭建RAG”“零基础转行AI工程师”。我身边不少做后端、做前端的朋友看得心痒,觉得门槛也没那么高,于是买了几节网课,跟着敲了一遍LangChain的demo,就觉得自己算是入门了。结果真到了公司要落地一个稍微像样的AI功能,比如做一个能查内部文档、能带上下文记忆、还能控制成本的问答助手,立刻就卡住了——不知道向量库怎么选,不知道chunk怎么切,不知道token怎么算,更不知道线上延迟为什么忽高忽低。
这就是我想聊“ai-engineering-from-scratch”这个主题的原因。它不是让你从零开始手写一个Transformer,也不是让你去啃论文复现GPT,而是指从工程视角,把AI能力真正落地成可维护、可观测、可迭代的系统。说白了,模型是别人的,但工程是你自己的。你要解决的是数据怎么进、结果怎么出、成本怎么控、效果怎么评、线上怎么稳这一整套问题。适合看这篇内容的人,是那些已经会写代码、但对AI工程还没有系统认知的开发者,以及那些被demo骗过、想认真补齐工程链路的同学。
我自己走过一段弯路。最开始我也是“调包侠”,觉得把API接上就完事了。后来做一个客服知识库项目,用户问一句“上个月的退款政策改了吗”,系统要么答非所问,要么把三年前的旧文档翻出来。那一刻我才意识到,AI工程的核心根本不在模型调用那一行代码,而在它前后的所有脏活累活。下面我就把这套从零搭建的思路,按我实际踩过的顺序拆开讲。
2. 整体设计思路:先想清楚数据流,再谈模型选型
2.1 为什么“先跑通demo”是个陷阱
很多人做AI项目的习惯是:先找个最火的框架,把官方示例跑起来,看到输出正常,就以为架构定了。这个顺序其实是反的。Demo之所以能跑通,是因为它喂给你的数据是干净的、问题是标准的、场景是单一的。而真实项目里,数据是脏的、问题是发散的、场景是复合的。
我现在的做法是,动手写第一行代码之前,先在白纸上画一条数据流:原始数据从哪来 → 怎么清洗 → 怎么切分 → 怎么向量化 → 存到哪 → 用户问题怎么进来 → 怎么检索 → 怎么拼上下文 → 怎么调模型 → 结果怎么后处理 → 怎么返回给用户。这条链路上每一个箭头,都是一个可能出问题的工程点。你先把这条线画清楚,再去选具体用什么工具,就不会被框架牵着鼻子走。
提示:如果你画不出这条数据流,说明你还没想清楚要做什么,这时候写代码就是浪费时间。
2.2 分层架构:把“会变的”和“不变的”分开
AI工程系统最容易烂的地方,就是所有逻辑搅在一起。今天换个模型,明天换个向量库,后天改个切分策略,结果牵一发动全身。我的经验是分成四层:
- 数据层:负责原始文档的采集、清洗、切分、向量化、存储。这一层的变化频率中等,主要跟着数据源和切分策略走。
- 检索层:负责把用户问题转成查询,去向量库或关键词库里捞相关内容,做重排和过滤。这一层变化频率高,因为检索策略直接决定效果。
- 生成层:负责拼prompt、调模型、解析输出。这一层变化频率最高,模型迭代快,prompt也要反复调。
- 应用层:负责对外接口、鉴权、限流、日志、监控。这一层相对稳定,但一旦出问题就是线上事故。
分层的意义在于,当你发现效果不好时,能快速定位是哪一层的问题。是检索没捞到对的文档,还是捞到了但模型没用好?这两者的修法完全不同。我见过太多人一效果不好就换模型,结果换了三四个模型还是不行,因为根子在检索层。
2.3 成本与延迟的提前估算
这一条是很多新手完全忽略的。AI工程和传统后端最大的区别之一,就是每一次请求都有真金白银的成本,而且延迟不可控。你在设计阶段就要算一笔账。
假设你的知识库有5000个文档,平均每个文档2000字,切分后大概每个chunk 500字,那就是2万个chunk。每个chunk向量化一次,按常见的embedding模型算,大概要花几块钱到几十块钱不等,这是一次性成本。但用户每次提问,你都要把问题向量化,然后去2万个向量里做相似度检索,再取top 5拼进prompt。如果每个chunk 500字约等于700个token,top 5就是3500个token,加上系统prompt和用户问题,一次请求输入大概4000 token,输出按500 token算。按当前主流模型的定价,一次请求的成本大概在几分钱到一毛钱之间。听起来不多,但如果日活一千,一天就是几十到上百块,一个月就是几千块。
延迟方面,向量检索通常在几十毫秒,模型生成则取决于输出长度,几百毫秒到几秒都正常。你要在架构里预留超时和降级策略,不能让它无限等下去。
3. 核心细节解析:数据、检索、生成三件套
3.1 数据切分:chunk大小不是拍脑袋定的
切分是AI工程里最容易被低估的环节。很多人直接用框架默认的1000字符切分,重叠200字符,就完事了。但chunk大小直接决定了检索的粒度和生成的质量。
chunk太大,一个chunk里混了好几个主题,检索出来噪音多,模型容易被带偏;chunk太小,一个完整的意思被切碎,检索出来信息不全,模型答不完整。我的经验是,chunk大小要跟着你的文档类型走。技术文档、API说明这种结构清晰的,按段落或按标题层级切,每个chunk控制在300到500字;小说、访谈这种连续叙述的,按语义切分,chunk可以到800到1000字;表格和代码块要单独处理,不能硬切。
重叠部分的作用是防止关键信息正好落在切分边界上被切断。一般设成chunk大小的10%到20%就够了。我试过完全不重叠,结果有些问题的答案正好跨在两个chunk之间,检索时两边都只捞到一半,模型拼出来的答案就是残缺的。
还有一个细节是元数据。每个chunk除了文本内容,还要带上来源文档、章节标题、更新时间、权限标签这些信息。这些元数据在检索时可以拿来过滤,比如只搜某个部门有权限看的文档,或者只搜最近半年更新的内容。没有元数据,你的检索就是盲搜。
3.2 向量化与向量库选型:别只看benchmark
向量化就是把文本变成一串数字,让语义相近的文本在向量空间里距离也相近。这一步用的embedding模型,选型时不要只看排行榜上的分数,要看你的语言、你的领域、你的成本预算。
中文场景下,有些开源embedding模型在通用语料上表现不错,但在专业领域(比如医疗、法律、金融)就会掉点。这时候要么用领域数据微调一下,要么在检索层加一个关键词召回做互补。我一般会做混合检索:向量检索负责语义匹配,关键词检索(比如BM25)负责精确匹配,两路结果合并后重排。这样即使用户问了一个很生僻的专有名词,向量模型没见过,关键词也能兜住。
向量库的选型,小规模(几万到几十万向量)用FAISS或者Chroma就够了,本地跑,零运维。上了百万级,就要考虑Milvus、Qdrant这类专门的向量数据库,支持分布式和持久化。选的时候重点看三件事:索引类型是否支持你的数据规模、是否支持元数据过滤、是否支持增量更新。我踩过一个坑,早期用了一个不支持增量更新的库,每次加新文档都要全量重建索引,5000个文档重建一次要十几分钟,根本没法做实时更新。
3.3 Prompt拼装:把模型当人看,把上下文当 briefing
Prompt拼装看起来简单,就是把检索到的内容塞进去,但其实很讲究。我的原则是:把模型当成一个刚入职的聪明新人,你要给它清晰的briefing。
一个合格的prompt至少包含四部分:角色设定、任务说明、参考资料、输出格式。角色设定告诉它你是谁,比如“你是一个企业内部知识库助手”;任务说明告诉它要干什么,比如“根据下面的参考资料回答用户问题,如果资料里没有答案,就说不知道,不要编造”;参考资料就是检索到的chunk,要标上编号方便引用;输出格式规定它怎么答,比如“先给结论,再给依据,依据要标注来源编号”。
这里有个细节:参考资料要按相关度排序,最相关的放最前面。因为很多模型对上下文开头和结尾的内容注意力更强,中间容易忽略。如果你把最相关的放中间,效果可能反而差。我实测过,同样的top 5,按相关度降序排列比随机排列,答案准确率能差出十几个百分点。
注意:prompt里一定要明确禁止编造。我见过太多案例,检索没捞到相关内容,模型硬编了一个看起来很像的答案,用户信以为真,后果很严重。
4. 实操过程:从零搭一个可用的问答系统
4.1 环境准备与依赖安装
我假设你已经有一个Python环境,版本3.9以上。先建一个干净的虚拟环境,这一步别偷懒,AI工程的依赖冲突非常常见。
python -m venv ai-eng source ai-eng/bin/activate # Windows用 ai-eng\Scripts\activate pip install --upgrade pip然后装核心依赖。我这里列的是最小集合,具体版本你按实际情况调:
pip install openai # 或者你用的其他模型SDK pip install sentence-transformers # 本地embedding pip install faiss-cpu # 向量检索,有GPU可以装faiss-gpu pip install langchain # 编排框架,可选但省事 pip install pypdf # 解析PDF文档 pip install tiktoken # 算token数提示:如果你用的是在线embedding服务,就不需要sentence-transformers,但要注意网络延迟和费用。本地模型的好处是免费且数据不出域,坏处是占内存、速度慢一些。
4.2 文档加载与清洗
这一步的目标是把各种格式的文档(PDF、Word、Markdown、HTML)统一转成纯文本。我写了一个简单的加载器,按扩展名分发:
import os from pypdf import PdfReader def load_document(file_path): ext = os.path.splitext(file_path)[1].lower() if ext == '.pdf': reader = PdfReader(file_path) text = '\n'.join(page.extract_text() or '' for page in reader.pages) elif ext in ('.md', '.txt'): with open(file_path, 'r', encoding='utf-8') as f: text = f.read() else: raise ValueError(f'不支持的文件类型: {ext}') return text清洗主要是去页眉页脚、去多余空行、去乱码。PDF解析出来的文本经常带一堆换行和空格,我一般用正则把连续空白压成一个空格,但保留段落之间的双换行。这一步别过度清洗,把有用的格式信息也洗掉了。
4.3 切分与向量化
切分我用的是递归字符切分,优先按段落切,段落太长再按句子切,句子还长再按字符切。这样能尽量保持语义完整。
from langchain.text_splitter import RecursiveCharacterTextSplitter splitter = RecursiveCharacterTextSplitter( chunk_size=500, chunk_overlap=80, separators=['\n\n', '\n', '。', '!', '?', ' ', ''] ) chunks = splitter.split_text(text)向量化用sentence-transformers加载一个中文embedding模型:
from sentence_transformers import SentenceTransformer model = SentenceTransformer('BAAI/bge-small-zh-v1.5') embeddings = model.encode(chunks, normalize_embeddings=True)normalize_embeddings=True很重要,这样向量都是单位向量,后面用内积算相似度就等价于余弦相似度,省一步计算。
4.4 建索引与检索
用FAISS建一个内积索引:
import faiss import numpy as np dimension = embeddings.shape[1] index = faiss.IndexFlatIP(dimension) index.add(np.array(embeddings).astype('float32'))检索时把用户问题也向量化,然后搜top k:
def search(query, k=5): query_vec = model.encode([query], normalize_embeddings=True) scores, indices = index.search(np.array(query_vec).astype('float32'), k) return [(chunks[i], scores[0][j]) for j, i in enumerate(indices[0])]这里有个经验:top k不要设太大。k=5到8通常够了,设到20反而引入噪音,还增加token成本。我一般先用k=10召回,再用一个重排模型(比如cross-encoder)精排到top 5。重排模型比向量检索慢,但准很多,适合对准确率要求高的场景。
4.5 拼prompt与调用模型
def build_prompt(query, contexts): context_text = '\n\n'.join( f'[资料{i+1}] {ctx}' for i, (ctx, _) in enumerate(contexts) ) prompt = f'''你是一个企业内部知识库助手。请根据下面的参考资料回答用户问题。 如果参考资料中没有相关信息,请直接说"根据现有资料无法回答",不要编造。 参考资料: {context_text} 用户问题:{query} 请先给出结论,再给出依据,依据要标注资料编号。''' return prompt调用模型的部分,不同SDK写法不同,核心就是传prompt、设temperature(问答场景我一般设0.1到0.3,越低越稳定)、设max_tokens控制成本。
4.6 加一层缓存和日志
上线前一定要加缓存。很多用户会问重复或相似的问题,缓存能省大量成本。我用的是问题向量做key,相似度超过阈值就命中缓存。日志则要记录每次请求的问题、检索到的chunk id、模型输出、耗时、token消耗,这些数据是后续优化的基础。
5. 常见问题与排查技巧实录
5.1 检索不准的排查顺序
检索不准是最常见的问题,排查要按顺序来,别乱试。
| 排查项 | 检查方法 | 常见原因 |
|---|---|---|
| 切分是否合理 | 随机抽几个chunk看内容 | chunk太大混主题,太小丢信息 |
| embedding是否匹配 | 用几个已知相似句测相似度 | 模型不适合中文或领域 |
| 检索参数 | 调整top k和相似度阈值 | k太小漏召回,k太大引噪音 |
| 元数据过滤 | 检查过滤条件是否过严 | 把相关文档过滤掉了 |
| 查询改写 | 看用户原始问题是否太短 | 短问题语义信息不足 |
我遇到最多的是切分问题。有一次用户问“报销流程”,检索总是捞不到,后来发现报销流程的文档被切成了三段,每段都不完整,向量化后语义都偏了。改成按标题层级切,每段保持完整,问题就解决了。
5.2 模型答非所问的三种情况
第一种是检索没捞到对的资料,模型只能瞎编。这种情况要在prompt里强制它说不知道,同时优化检索。第二种是捞到了但模型没看懂,可能是prompt里的资料格式太乱,或者资料太多模型注意力分散。这时候要精简资料,只放最相关的。第三种是问题本身有歧义,模型理解偏了。这种情况可以在检索前加一步查询改写,把用户问题扩写成更明确的查询。
5.3 延迟忽高忽低的优化
延迟波动通常来自三个地方:embedding计算、向量检索、模型生成。embedding和检索一般很快,波动主要来自模型生成。优化手段包括:设max_tokens上限、用流式输出让用户先看到部分结果、对长回答做截断、高峰期降级到更小的模型。我实测过,把max_tokens从2000降到800,平均延迟能降一半以上,而大部分问答场景800 token足够。
注意:流式输出虽然体验好,但要做好前端拼接和错误处理,网络中断时要有兜底。
5.4 成本失控的预警信号
如果你发现账单涨得比预期快,先查三件事:一是top k是不是设太大了,二是prompt里是不是塞了太多无关资料,三是缓存命中率是不是太低。我见过一个案例,top k设了20,每次请求输入上万token,成本是正常情况的四倍。改成top 5加缓存后,成本直接降到原来的五分之一。
6. 我踩过的坑和几条实在建议
第一个坑是过早引入复杂框架。我一开始就用LangChain的全套抽象,结果出了问题根本不知道是哪一层的事,调试成本极高。后来我改成先用最朴素的代码把链路跑通,每个环节都自己控制,等稳定了再考虑用框架简化。框架是帮你省事的,不是帮你理解问题的。
第二个坑是忽略数据更新。知识库不是建一次就完事,文档会更新、会新增、会删除。我早期没做增量更新,每次都要全量重建,后来改成给每个chunk打时间戳,更新时只重建变化的文档,效率高了很多。
第三个坑是不做评估。效果好不好,不能靠感觉。我后来建了一个小测试集,五十个问题和标准答案,每次改动都跑一遍,看准确率是升是降。没有这个测试集,你根本不知道自己的改动是优化还是劣化。
最后分享一个实用技巧:把检索到的chunk和模型输出一起存下来。这样当用户反馈答案不对时,你能快速定位是检索的问题还是生成的问题。这个习惯帮我省了大量排查时间。
这套东西说起来不复杂,但每个环节都有细节。AI工程的门槛不在模型,而在这些工程细节的积累。你把这些脏活累活做扎实了,模型的能力才能真正发挥出来。