☰
从零构建AI工程能力:手写RAG系统与生产级落地实践
2026/9/30 8:49:36 网站建设 项目流程

1. 从零搭建AI工程能力:为什么我劝你别一上来就调包

这两年AI应用开发的门槛肉眼可见地在降低,随便拉个框架、调个API、拼几段提示词,一个能跑的Demo就出来了。但我自己带过几支团队、也帮不少朋友做过技术咨询之后,发现一个很普遍的现象:能跑通Demo的人很多,能真正把AI系统稳定交付到生产环境的人极少。这个差距,就是“AI工程能力”的差距,也是“ai-engineering-from-scratch”这个方向值得认真聊一聊的原因。

所谓从零构建AI工程能力,不是让你从零手写Transformer、从零训练一个大模型,而是指从工程视角出发,把模型、数据、服务、评估、监控这一整条链路自己动手搭一遍,理解每一层在干什么、为什么这么设计、出问题该往哪里查。它解决的核心问题是:当你手里的AI功能从“玩具”变成“产品”时,那些框架帮你隐藏掉的细节,恰恰是决定系统能不能扛住真实流量的关键。

这篇文章适合三类人看:一是刚入行、只会调API但想搞懂底层链路的AI应用开发者;二是有后端或数据工程背景、想转型做AI工程的同学;三是已经在做AI产品、但系统总在关键时刻掉链子的团队负责人。我会按照“整体设计思路—核心细节拆解—实操落地—问题排查”的顺序,把这条从零到一的路子讲透,中间穿插我自己踩过的坑和实测有效的做法。

2. 整体设计思路:为什么我坚持“先手写一遍再上框架”

2.1 从零构建的真正含义与边界

先把边界划清楚,避免有人误以为要从零造轮子。我这里说的“from scratch”,指的是不依赖高层封装框架,用最基础的库把AI工程的核心环节实现一遍。具体来说,模型推理可以用PyTorch或ONNX Runtime这种相对底层的运行时,服务层用FastAPI或Flask自己写,向量检索用NumPy或FAISS自己搭,评估和监控自己写脚本。至于模型训练本身,除非你的业务确实需要微调,否则用现成的开源权重就够了。

为什么强调“手写一遍”?因为框架的价值在于帮你省事,但省事的前提是你知道它在替你做什么。我见过太多人用LangChain搭了个RAG,结果检索效果差,完全不知道问题出在切分、嵌入还是召回排序上,因为整条链路被框架包成了一行chain.invoke()。当你自己手写过一遍,知道每一步的输入输出长什么样,再回去用框架,你就能精准地知道该在哪里插桩、在哪里替换组件。

这个边界还有一个现实考量:生产环境对可控性的要求远高于开发效率。框架版本一升级,行为可能就变了;某个依赖出了安全漏洞,你得能快速定位影响面。自己手写过的部分,你心里有底。

2.2 分层架构:把AI系统拆成能独立验证的模块

我习惯把AI工程系统拆成五层,每一层都能独立测试、独立替换。这个分层不是教科书上的标准答案,而是我在实际项目里反复调整后觉得最顺手的划分方式。

层级职责常用技术选型独立验证方式
数据层文档加载、清洗、切分自定义脚本 + 正则抽样检查切分边界
表示层文本向量化、索引构建sentence-transformers + FAISS用已知相似句验证召回
检索层相似度计算、重排序NumPy + 交叉编码器构造查询看Top-K结果
服务层API封装、并发处理FastAPI + Uvicorn压测接口延迟与吞吐
观测层日志、指标、评估自定义埋点 + 评估集回归测试对比基线

这么分的好处是,任何一层出问题,你都能快速定位到具体模块。比如用户反馈“答非所问”,你可以先看检索层返回的Top-K文档是否相关,如果相关那就是生成层的问题,如果不相关就往表示层和数据层查。这种定位效率,是黑盒框架给不了的。

2.3 技术选型背后的取舍逻辑

选型这件事,我的原则是优先选“行为可预测”的组件,而不是“功能最全”的组件。举个具体例子:向量检索我一开始用的是某个功能很丰富的向量数据库,支持各种过滤、混合检索,但后来发现它的相似度计算在不同版本间有细微差异,导致线上结果和离线评估对不上。后来我换成FAISS加自己写的元数据过滤,虽然功能少了,但相似度计算完全由我控制,离线线上一致性问题直接消失。

再比如服务层,很多人喜欢用现成的推理服务框架,但我更倾向于用FastAPI自己写。原因很简单:AI服务的瓶颈往往不在HTTP层,而在模型推理和检索的耗时上,自己写能精确控制批处理、缓存、超时的逻辑,出了问题也好排查。框架帮你省的那点代码量,在排查线上问题时往往要加倍还回去。

提示:选型时问自己一个问题——“如果这个组件出问题,我能在多长时间内定位到根因?”如果答案是“不知道”或“要翻源码”,那就要慎重。

3. 核心细节解析:每个环节的坑与关键参数

3.1 文档切分:最容易被低估的环节

文档切分看起来简单,实际上是我见过翻车最多的环节。很多人直接按固定字符数切,结果把一句话切成两半,检索出来的片段语义不完整,生成质量自然差。

我的做法是按语义边界切分,同时保留重叠。具体来说,优先按段落切,段落太长再按句子切,句子还长才按字符切。重叠部分保留10%到20%,确保跨边界的语义不会丢失。这里有个参数需要计算:假设你的嵌入模型最大输入是512个token,中文大概1个token对应1.5到2个字符,那么单段控制在300到400字比较稳妥,留出余量给重叠部分。

def split_by_semantic(text, max_len=400, overlap=60): paragraphs = text.split("\n\n") chunks = [] for para in paragraphs: if len(para) <= max_len: chunks.append(para) else: sentences = re.split(r"(?<=[。!?])", para) current = "" for sent in sentences: if len(current) + len(sent) <= max_len: current += sent else: chunks.append(current) current = current[-overlap:] + sent if current: chunks.append(current) return chunks

这段代码的关键在于current[-overlap:],它保证了相邻块之间有重叠。实测下来,这个简单的策略比固定长度切分的检索命中率能高出15%以上。

3.2 向量化与索引:模型选择与归一化

嵌入模型的选择直接决定检索质量。我的经验是:中文场景优先选在中文语料上训练过的模型,不要盲目追大。一个几百MB的模型,在特定领域上往往比几个GB的通用大模型效果更好,而且推理速度快、显存占用低。

向量化之后一定要做归一化,也就是把向量除以它的L2范数。这样内积就等于余弦相似度,后续计算统一用内积就行,省去每次算余弦的除法。这个细节很多人忽略,导致相似度分数不可比。

import numpy as np def normalize(vectors): norms = np.linalg.norm(vectors, axis=1, keepdims=True) return vectors / np.clip(norms, 1e-10, None)

索引构建用FAISS的话,数据量在百万级以内,IndexFlatIP(内积索引)就够了,它是精确检索,没有近似误差。数据量再大再考虑IVF这类近似索引,但要接受召回率下降的代价。

3.3 检索与重排序:两阶段召回的价值

单靠向量检索,Top-K里经常混入语义相似但实际不相关的结果。我的做法是两阶段:向量召回Top-50,再用交叉编码器重排取Top-5。交叉编码器把查询和文档拼在一起过一遍模型,精度比向量内积高很多,缺点是慢,所以只用在少量候选上。

这个“召回宽、重排精”的思路,是检索系统的经典范式。参数上,召回数量我一般设成最终需要的5到10倍,重排后再截断。实测下来,加了重排之后,最终喂给生成模型的上下文相关性明显提升,答非所问的比例能降一半左右。

3.4 服务层设计:并发、缓存与超时

服务层最容易出问题的是并发和超时。AI推理是计算密集型,如果每个请求都同步跑模型,并发一上来就排队。我的做法是用线程池或异步队列把推理和HTTP处理解耦,同时给每个环节设超时。

缓存也很关键。相同的查询在短时间内重复出现是常态,我用查询的哈希做键,缓存检索结果和最终答案,命中率在真实场景里能到20%到30%,直接省下这部分算力。超时设置上,检索给200毫秒,重排给500毫秒,生成给5秒,任何一环超时就降级返回,保证接口整体可用。

注意:缓存要设过期时间,否则知识更新后用户拿到的还是旧答案。我一般设10到30分钟,具体看业务对时效性的要求。

4. 实操落地:从空目录到可运行系统的完整流程

4.1 环境准备与依赖安装

先把环境搭起来。我习惯用conda建独立环境,避免依赖冲突。核心依赖就几个:PyTorch或ONNX Runtime做推理,sentence-transformers做嵌入,FAISS做检索,FastAPI做服务。

conda create -n ai-eng python=3.10 -y conda activate ai-eng pip install torch --index-url https://download.pytorch.org/whl/cpu pip install sentence-transformers faiss-cpu fastapi uvicorn numpy

这里用CPU版本的torch和faiss,是因为大部分中小规模场景CPU足够,而且部署简单。如果QPS要求高再换GPU版本,接口代码不用改。

4.2 数据管道搭建与索引构建

数据管道分三步:加载、切分、向量化。加载支持txt和markdown就行,PDF和Word用对应库转成文本再进管道。切分用上面说的语义切分。向量化批量做,一次过几百条,比逐条快很多。

from sentence_transformers import SentenceTransformer import faiss model = SentenceTransformer("your-chinese-model") chunks = load_and_split("docs/") vectors = model.encode(chunks, batch_size=64, normalize_embeddings=True) index = faiss.IndexFlatIP(vectors.shape[1]) index.add(vectors.astype("float32")) faiss.write_index(index, "index.faiss")

normalize_embeddings=True这个参数很关键,它让模型直接输出归一化向量,省得自己再算一遍。索引存盘后,服务启动时直接加载,不用每次重建。

4.3 检索服务与生成接口实现

检索服务封装成一个类,对外暴露search(query, top_k)方法。内部先编码查询,再检索,再重排。生成接口接收查询,调检索拿到上下文,拼进提示词,调生成模型,返回答案。

@app.post("/ask") async def ask(query: str): docs = retriever.search(query, top_k=5) context = "\n\n".join(docs) prompt = f"基于以下资料回答问题:\n{context}\n\n问题:{query}" answer = generator.generate(prompt) return {"answer": answer, "sources": docs}

提示词里明确要求“基于以下资料”,能有效减少模型胡编。返回里带上来源文档,方便用户核对,也方便你排查问题。

4.4 观测埋点与评估集构建

观测这块,我在每个环节都埋了耗时和结果的日志。检索记录Top-K的分数分布,生成记录输入输出长度和耗时。这些日志平时看着没用,出问题时就是救命稻草。

评估集我建议从真实用户查询里采样构建,至少准备100条,每条标注期望的答案要点。每次改动代码后跑一遍评估集,看指标有没有退化。指标不用太复杂,检索看命中率,生成看人工评分或关键词覆盖,够用了。

评估指标计算方式目标值
检索命中率Top-5含正确文档的比例>85%
答案相关性人工1-5分均值>4.0
P95延迟95%请求的响应时间<3秒
缓存命中率缓存命中数/总请求数>20%

这套评估跑下来大概几分钟,但能帮你挡住大部分“改一处坏三处”的回归问题。

5. 常见问题与排查技巧实录

5.1 检索结果不相关怎么查

这是最高频的问题。排查顺序是:先看查询编码是否正常,再看索引里的向量是否和查询在同一空间,最后看重排是否把好结果排掉了。我遇到过一次,原因是重建索引时用了新模型,但查询还在用旧模型编码,两个空间对不上,检索结果全是乱的。模型和索引必须版本绑定,这是血泪教训。

5.2 生成答案胡编乱造怎么治

模型胡编通常有两个原因:一是上下文里确实没有答案,模型只能编;二是提示词没约束好。前者要在检索层解决,提高召回质量;后者在提示词里明确“如果资料中没有相关信息,请回答不知道”。我实测下来,加上这句约束后,胡编比例能降不少。另外,把温度参数调低,也能减少随机发挥。

5.3 服务延迟忽高忽低怎么优化

延迟抖动大,一般是并发上来后资源争抢导致的。先看是不是每次请求都重新加载了模型,这是新手常犯的错,模型要在服务启动时加载一次,全局复用。再看是不是没有批处理,多个请求可以攒一批一起推理。最后看缓存有没有生效。这三招下去,延迟通常能稳定下来。

5.4 常见问题速查表

现象可能原因排查动作
检索全不相关模型与索引版本不一致核对编码模型版本
答案胡编上下文缺失或提示词无约束检查召回、加约束语句
延迟抖动模型重复加载或无批处理全局加载模型、加批处理
内存持续增长缓存无上限或日志未轮转设缓存上限、日志轮转
结果离线线上不一致相似度计算方式不同统一用归一化内积

5.5 我踩过的几个坑

第一个坑是切分时把表格切碎了,导致表格数据检索出来是残缺的,后来我专门给表格加了保护逻辑,整表不切。第二个坑是缓存键只用了查询文本,没考虑用户权限,导致A用户看到了B用户的答案,后来在键里加了用户标识。第三个坑是评估集一直没更新,用的还是半年前的查询,结果线上新出现的问法完全没覆盖,评估指标虚高。这些坑都不难避免,但没人提醒的话,大概率要自己撞一遍。

6. 后续扩展方向与个人体会

这套从零搭起来的系统,往上扩展的空间其实很大。检索层可以加混合检索,把关键词召回和向量召回融合;生成层可以加流式输出,改善用户体验;观测层可以接入更细的链路追踪,定位长尾问题。但我的建议是先把当前这套跑稳、跑透,再考虑加东西。很多团队一上来就堆功能,结果每个环节都是半吊子,出了问题谁都说不清。

我自己最大的体会是,AI工程和传统后端工程的差别,不在于用了多少新框架,而在于对不确定性的管理。模型输出不确定、检索结果不确定、用户问法不确定,工程手段的价值就是把这些不确定性圈在可控范围内。你手写过一遍,就知道每个环节的边界在哪、余量有多少,这种掌控感是调包调不出来的。

最后分享一个小技巧:每次改动上线前,除了跑评估集,我都会手动构造几个刁钻的查询,比如问一个资料里完全没有的问题、问一个需要跨多篇文档综合的问题,看系统怎么反应。这几个查询不写进评估集,专门用来抓那些指标覆盖不到的边界情况。实测下来,这招帮我提前发现过好几次线上事故。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询