☰
AI工程从零起步:手把手构建RAG知识库系统
2026/10/3 11:05:54 网站建设 项目流程

1. 先搞清楚:AI工程到底在干什么

先说个扎心的事实:AI工程和普通软件工程,看起来都是写代码,但骨子里是完全不同的两套逻辑。你把一个普通的CRUD系统写得再花哨,它的行为也是确定性的——输入什么,输出什么,逻辑链条清清楚楚。但AI工程不一样,你面对的是一堆概率模型,同样的输入可能每次输出都不同,你调了一个参数,可能精准命中,也可能全盘崩坏。

很多人一提到“AI工程”,第一反应是训练模型。这个理解不算错,但放在2024年之后的语境下,有点过时了。现在大家说AI工程,更多指的是:怎么把已经存在的AI能力(大语言模型、向量检索、多模态模型等)真正落到一个能用的产品里,并且让它稳定、高效、可维护。

“ai-engineering-from-scratch”这个标题,我的理解是两层意思。第一层,是从零开始搭建一个AI应用系统,不依赖那些封装好的全家桶框架,而是把每个组件都理解透、自己拼起来。第二层,是从零开始建立一套AI工程思维——不是“我会调一个API”,而是“我知道这个系统每个环节为什么这么设计,出了问题能从底层排查”。

这篇文章我会按自己实际踩坑的路径来讲:先讲怎么设计一个AI应用的整体架构,再逐个拆解核心环节的实操细节,最后分享一份真实的问题排查手册。目标读者是那种已经会写Python、懂点基础机器学习概念,但面对“AI应用怎么落地”心里没底的人。


2. 从零开始的架构设计思路

2.1 别急着上框架,先把组件拆明白

我做这个项目时,第一件事不是找开源框架,而是先画了一张图:一个AI应用,本质上需要哪些基础设施组件。画完之后发现,其实就六块。

  • 模型接入层:怎么跟大模型通信,用哪个模型,怎么管理密钥和并发。
  • 数据管道层:知识库里的原始文档,怎么被读取、清洗、切分,变成模型能用的格式。
  • 检索层:用户的问题来了,怎么从知识库里找到最相关的片段。
  • 上下文组装层:把检索结果和系统提示词、历史对话拼成一段模型能理解的完整输入。
  • 生成与输出层:模型返回结果后,怎么解析、怎么格式化、怎么处理流式输出。
  • 评估与监控层:怎么判断回答好不好,怎么发现系统退化。

这一层拆完你会发现,市面上那些AI开发框架,本质就是把这六块帮你拼好了。框架当然好用,但如果你一开始就上框架,遇到问题就只能黑盒排查。“从零开始”的真正价值在于:你亲手把每个组件搭了一遍之后,再回头看框架,一眼就能看穿它在底层做了什么。

我当时的选择是:核心链路完全不依赖AI框架,只用Python基础库加少量周边工具。比如解析PDF用pypdf,做向量检索自己写一个简单的暴力检索(数据量小的时候完全够用),只有在需要规模扩展时才接真正的向量数据库。

提示:如果你的知识库只有几百个文档,十万字级别,完全没必要上Elasticsearch或者专门的向量数据库。自己写一个余弦相似度检索,性能完全够用,而且排查问题极其方便。

2.2 明确应用类型:不是所有AI项目都需要RAG

技术选型之前,必须先明确你做的是哪类AI应用。我在实际做项目时发现,大多数人(包括一开始的我)会把所有问题都往RAG上套,但RAG并不是万能钥匙。

我见过的AI应用大致可以分三类:

  • 纯对话型:模型直接回答,不依赖外部数据。适合通用问答、头脑风暴、写作辅助。这种项目最简单,核心工作集中在提示词和模型选型。
  • RAG检索增强型:模型的回答需要先从一个知识库里检索相关内容,再基于这些内容生成。适合客服问答、企业知识库、法律/医疗辅助等场景。
  • Agent代理型:模型需要调用外部工具(搜索、计算器、API接口),多轮决策后完成任务。适合自动化操作、数据分析、工作流编排。

“ai-engineering-from-scratch”这个项目,我选择的核心场景是RAG——因为它在工程上的挑战最全面:既涉及数据处理,又涉及检索算法,还涉及提示词工程和模型调优。把RAG完整做一遍,AI工程的地基就打牢了。


3. 核心细节拆解与实操要点

3.1 文档处理的“脏活累活”决定了系统上限

很多人以为RAG系统最核心的是模型,其实错了。一个RAG系统的质量上限,在文档解析和切分环节就已经决定了。你喂给切分器的文档是乱七八糟的扫描件PDF,后面无论用多牛的模型,检索出来的东西都是一坨。

文档处理我总结了一个流程:

  1. 格式归一化:把所有文档统一转成纯文本或Markdown。PDF用pypdf或pdfplumber提取文本,Word用python-docx,HTML用BeautifulSoup。这一步的目标不是好看,而是干净。

  2. 结构嗅探:花时间观察文档的结构规律。我发现大多数企业文档都有固定模板,比如“1. 背景”“2. 方案”“3. 预算”。这种结构信息比正文内容还宝贵,处理时要把标题层级保留下来,后续切分才能智能。

  3. 切分策略制定:这是最关键的一步。切分太粗,检索精度差;切分太细,上下文碎片化,模型理解不了。我的经验是:先按文档结构切,再按字符数兜底。具体来说,如果文档有明确的一级标题,就以一级标题为边界切块;如果文档是一篇没有结构的连续性文章,就按500-800个字符重叠100字符切。

切分还有一个很多人不知道的细节:要保留原始文本的上下文锚点。什么叫锚点?比如切出来的片段是从一个表格中间断开的,后面那半截如果不知道前面是什么,模型根本看不懂。我见过不少团队用“Markdown格式的文本块”,但丢失了表格结构信息。解决办法是:切分时如果遇到表格,千万别硬切,整块保留。

from pypdf import PdfReader def extract_text_from_pdf(path): reader = PdfReader(path) texts = [] for page in reader.pages: page_text = page.extract_text() if page_text: texts.append(page_text) return "\n".join(texts) # 实操心得:pypdf对扫描版PDF无能为力,那种必须先过OCR # OCR工具我试过Tesseract和国内几家云OCR,效果差距很大, # 如果预算允许,直接上云OCR,准确率高一个量级

3.2 嵌入模型选型:别只盯着效果,还要看成本

嵌入模型(Embedding Model)是RAG系统的另一个关键决策点。它的作用是把文本变成一串数字向量,让计算机能计算“相似度”。选型时我关注三个维度:维度大小、成本、语言适配度。

首先是维度大小。OpenAI的text-embedding-3-small有1536维,text-embedding-3-large有3072维。维度越高,理论上表达能力越强,但存储和计算成本也线性上升。我实测下来,在知识库规模不超过50万条的情况下,1536维和3072维的检索效果差距不到3个点,但成本差了4倍以上。没有特殊需求,用小维度的就够了。

其次是成本。这一步容易被忽视,但我的建议是:先算一笔账,再选模型。假设你有10万条文档片段,每条平均嵌入成本0.0001美元,一次全量嵌入就花10美元,看起来不多。但如果你用的是大维度模型,再叠加按量计费的高价API,成本会翻好几倍。开源模型(比如BGE系列)可以本地部署,零成本嵌入,但需要一块不错的GPU。

最后是语言适配度。中文场景下,直接用英文原生的嵌入模型经常效果打折,因为中文分词和语义表达跟英语差异很大。我测试过几款模型,结论是:中文场景优先考虑专为中文优化的模型,或至少在中文语料上微调过的版本。

实操建议:选嵌入模型时别只看榜单分数,拿你自己的数据实测。每个行业的术语分布完全不同,通用榜单上的SOTA(最优模型),到了你的垂直领域可能连前十都进不了。找个周末,跑一个简单的准确率对比测试,比看十篇评测文章都有用。

3.3 向量检索的“暴力美学”:自己手写一个也行的前提

很多人听到“向量检索”就觉得必须用Milvus、FAISS、Qdrant这类专业工具。其实,在小规模场景下,用纯Python写一个暴力检索完全可行,还省去了一大堆部署和运维的麻烦。

我的做法是这样:所有文档嵌入完成后,存成一个NumPy矩阵。查询时,把用户问题的嵌入向量和库里所有向量算一遍余弦相似度,取Top-K。

import numpy as np def cosine_similarity(query_vector, doc_vectors): query_norm = np.linalg.norm(query_vector) doc_norms = np.linalg.norm(doc_vectors, axis=1) return (doc_vectors @ query_vector) / (doc_norms * query_norm) def top_k_search(query_vector, doc_vectors, k=5): scores = cosine_similarity(query_vector, doc_vectors) top_indices = np.argsort(scores)[-k:][::-1] return top_indices, scores[top_indices]

这段代码在100万向量以内都能跑,只是慢一点。但实际上,个人项目和企业内部工具的知识库基本在10万条级别,每次查询也就几十毫秒,体感毫无压力。

更深一层说,向量检索的瓶颈从来不在“算相似度”,而在于“过滤”。真实场景里,用户的知识库往往需要按权限、部门、时间范围等条件过滤,这时候纯向量检索就不够用了。我的做法是:先做标量过滤(比如时间范围、文档类型),再做向量排序。这样既控制了相关性,又满足了业务规则。

3.4 上下文组装:决定回答质量的是“你怎么把东西喂给模型”

检索到了相关内容之后,接下来这一步是很多人最容易忽略、也最影响最终效果的:怎么把检索到的片段组织成提示词。我见过太多人把检索结果一股脑倒给模型,结果模型被无关信息带偏,回答质量差得离谱。

上下文组装的核心原则有四个:

  • 精简:相关性低的片段坚决不要。我早期习惯Top-5全给,后来发现Top-2的信息有时比Top-5全给效果好得多,因为噪声少了。
  • 结构化:用清晰的格式区分“系统指令”和“参考文档”。我常用的格式是:先给系统提示词,明确说明“以下为参考资料,请严格基于资料回答,不要编造”,然后把检索结果用分隔线隔开。
  • 优先级:相关片段按相关性排序,最相关的放在离问题最近的位置。大语言模型对中间位置的注意力衰减严重,开头和结尾的信息最容易记住。
  • 引用溯源:每一段参考内容都带上文档ID和原文位置,并要求模型回答时标注来源。这不仅能提升可信度,还能方便用户反查原文档。
system_prompt = """你是企业知识库助手。请严格基于下方提供的参考资料回答问题。 如果参考资料中找不到答案,请直接说“资料库中没有相关信息”,不要推测或编造。 回答需要包含引用来源编号,格式为[来源编号]。""" context_text = "" for idx, doc in enumerate(top_chunks): source_tag = f"[{idx + 1}]" context_text += f"{source_tag} {doc['content']}\n---\n" user_query = f"用户问题:{question}" final_prompt = f"{system_prompt}\n\n参考资料:\n{context_text}\n\n{user_query}"

这个Prompt结构看着简单,但每个句子都有讲究。“不要编造”这句话就非常重要——没有这句话的时候,模型会在资料缺失时脑补出完全错误的信息。加了这个约束之后,幻觉率直线下降。


4. 实操过程:从零搭建一个可用的RAG系统

4.1 项目初始化与数据准备

我拿到的“原料”是一批企业内部的技术文档,大概300份,7300多个文档片段。内容包括产品说明书、排错指南、常见问题问答。

第一步是把这些文档统一转为文本,统一编码格式和换行符。这步看着简单,实际操作时我踩了个坑:部分文档是GBK编码的,直接用UTF-8读会乱码。解决方式是读文件时先检测编码。

第二步是切分。按照前面说的方法,我先把每篇文档按标题拆成小节,再对过长的小节做字符切分。切分后的数据我存成了JSONL格式,每条记录包含:文档ID、小节标题、正文内容、字符数。这个结构在被检索时非常方便。

4.2 嵌入与存储:选择开源本地模型还是云API?

这一步我做了两个方案的实测对比。

方案一是用云API做嵌入。效果不错,但当时算了一笔账:7300个片段,按每个片段300个token算,共219万token,一次全量嵌入的成本不到5美元,很便宜。但问题是后续每次更新知识库,如果增量不多也要调API,长期下来虽然不多但也是笔持续支出。另外还有一个隐患:数据安全。企业文档往往包含内部敏感信息,把全部内容发到外部API,很多公司这关就过不了。

方案二是本地部署开源嵌入模型。我用的是BGE-small-zh-v1.5,模型体积不到500MB,CPU也能跑,每个片段嵌入大概需要0.5秒,7300条就是一小时左右。成本为零,而且数据不出服务器。嵌入质量我用了一个30条问题的评测集做对比,BGE在中文场景下和云API的差距非常小,甚至在某些垂直术语上略胜一筹。

最终我选了本地部署。如果你没有数据安全方面的担忧,选云API省心省力,但如果你做的是企业内部工具,本地嵌入模型几乎是必然选择。

4.3 检索:写一个带标量过滤的混合检索

我的检索模块除了向量相似度,还加了一道工序:关键词过滤。做法是:用简单的倒排索引先筛掉明显不相关的文档,再在剩下的交集里做向量排序。这里有个很实用的技巧——先用BM25关键词检索获取候选集,再用向量相似度重新排序,两者互补,效果比单用任何一种都好。

def hybrid_search(query, top_k=5, source_filter=None): # Step 1: BM25粗筛,召回100个候选 candidates = bm25_search(query, top_n=100) if source_filter: candidates = [c for c in candidates if c["source"] == source_filter] # Step 2: 对候选集做向量精排 candidate_ids = [c["chunk_id"] for c in candidates] candidate_vectors = vector_store[candidate_ids] query_vector = embed_model.encode(query) indices, scores = top_k_search(query_vector, candidate_vectors, k=top_k) return [candidates[i] for i in indices]

这个“粗筛+精排”的模式,在工程上是一个非常经典且稳定的方案。它解决了纯向量检索的两个痛点:一是速度——标量过滤(比如只查某个部门的文档)能在检索前就把搜索空间缩小,减少无意义的计算;二是准确率——向量相似度对同义改写很敏感,而BM25对精确关键词更敏感,两者结合能覆盖更多情况。

4.4 流式输出与前端体验

我见过很多从零起步的AI项目,把时间全花在后端,前端直接用了个裸对话框——输入问题,转圈三秒,啪一下出整段回答。体验倒是没大问题,但专业产品里这种模式几乎绝迹了。

流式输出(Streaming)是AI应用的基本功之一。原理很简单:模型不是一次性返回整段文本,而是一个token一个token地返回,前端收到就立即渲染。这样用户看到的是一个正在一个字一个字打字的效果,等待感完全消失。

后端实现上我用了FastAPI的StreamingResponse,配合SSE协议(Server-Sent Events)。核心逻辑就是拿到模型返回的流式对象后,一段段地转发给前端。

from fastapi import FastAPI from fastapi.responses import StreamingResponse app = FastAPI() def llm_generator(prompt): response = llm_client.chat.completions.create( model="gpt-4o-mini", messages=[{"role": "user", "content": prompt}], stream=True ) for chunk in response: if chunk.choices[0].delta.content: yield chunk.choices[0].delta.content @app.post("/chat") async def chat(request: dict): prompt = request["prompt"] return StreamingResponse( llm_generator(prompt), media_type="text/event-stream", headers={"Cache-Control": "no-cache"} )

别小看这个“打字机效应”,它对用户体验的提升是决定性的。我做过一次内部测评,同一个系统,流式版本的用户满意度评分比非流式版本高出40%。人就是带宽有限的动物,等三秒出全文和等零点三秒开始出字,心理感受天差地别。


5. 性能实测与成本分析:数字不会说谎

5.1 检索质量:用什么指标衡量才算靠谱

很多人做RAG系统,评估效果全靠“感觉”。这不对。我这次用了一个比较轻量但有效的评测方法:准备了30个有标准答案的问题,分别记录系统在三个指标上的表现。

  • 命中率(Recall):正确答案是否出现在检索结果中。
  • 回答正确率(Accuracy):模型生成的回答是否准确。
  • 幻觉率(Hallucination Rate):回答中是否出现了资料库里没有的信息。

实测下来,混用BM25+向量检索的命中率比纯向量检索高约12个百分点。这个差异在“产品名词精确匹配”类问题上尤其明显——比如用户搜“内存不足报错”,关键词“内存不足”能直接命中,但向量检索可能把它翻译成“存储异常排查”,反而丢了原文术语。

模型选择上我也做了对比。同样一个RAG系统,用GPT-4o-mini和用开源模型时回答正确率差距明显。但GPT-4o-mini的价格也更贵,每次问答平均消耗5000 token,成本分别是:开源模型约0元,GPT-4o-mini约0.003美元,GPT-4o约0.06美元。对企业内部工具来说,准确率差5个点可能无所谓,但成本差20倍,选型就得掂量掂量了。

5.2 端到端延迟:每个环节各花了多少时间

我对系统的端到端延迟做了拆解,发现一个反直觉的结论:嵌入和检索加起来只占了150毫秒,但模型生成占了整整3秒。这意味着,如果你的系统慢,瓶颈不在检索,而在生成环节。

生成环节能优化的手段很多:改用流式输出让体感时间缩短;把模型的max_tokens上限调低;在任务简单时切换到更小更快的基础模型。我实测了用不同模型生成的延迟数据,如下表:

模型平均生成500字耗时质量评分性价比评价
GPT-4o4.8秒9.5/10贵,适合复杂推理
GPT-4o-mini3.1秒8.0/10性价比之王
Qwen2.5-7B3.4秒7.0/10可本地部署,省成本
一个更小的3B模型1.9秒5.5/10快但质量不够

做AI应用的最优策略其实是“按需分配”:简单的检索问答走小模型,需要复杂推理的才把请求路由到大模型。很多系统根本没做这个路由,所有请求都走最强模型,结果就是钱花了不少,用户也没觉得多智能。

注意:max_tokens设置不要太大。我见过有人把max_tokens设成4096,但实际大部分回答才300字,白白浪费了生成时间。设成1024或512,延迟能降一半还不影响体验。


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

6.1 为什么检索结果准确,但回答还是错?

这个问题我排查了很多次,最后发现八成出在上下文组装环节。最典型的错误有两种:一是上下文太长,模型注意力涣散,抓不住关键信息;二是多个参考文档内容相互矛盾,模型不知道该信哪个。

解决办法是给系统提示词加上“冲突处理规则”:“如果参考资料中存在相互矛盾的说法,请分别列出所有观点,并注明各自的来源编号。”加上这句话之后,模型遇到矛盾时不再自由发挥,而是诚实地摆出分歧,这反而对用户更有用。

另一个隐蔽的坑是:参考文档的顺序。大语言模型对提示词前部内容的注意力和遵循度通常更高。如果把最关键的参考资料放在中间,模型很容易忽略。我的做法是:把与问题最相关的文档放在参考资料列表的第一位,不相关但有趣的文档坚决不要放进来。

6.2 检索结果不相关,问题出在哪?

如果检索环节就翻车,后面全白搭。我遇到过两次比较大的翻车场景。

第一次是发现某几分钟内检索结果特别差,查了半天才反应过来:当时正在批量更新知识库,新增的文档还没完成嵌入就被检索系统看到了。后来我加了“索引状态标记”,文档只有完成嵌入才置为可检索状态。

第二次是用户问题里用了大量口语化表达,比如“我们那个系统总是登不上去”,嵌入模型对“登不上去”这样的说法可能在向量空间里离“认证失败排查手册”很远。解决方案有两个:一是做一个“问题改写”前置模块,把口语化问题转成更规范的技术查询;二是在混合检索模块里把问题分词后做同义词扩展。我最后采用的是问题改写方案,用一个小模型负责改写用户问题,效果显著。

6.3 项目上线后如何持续维护?

从零搭系统只是第一步,AI工程的本职是让系统持续稳定地跑下去。我用了一套极其简单但有效的监控方案:

  • 每次检索记录下Top-K片段的得分和命中文档ID,输出到日志。
  • 每周人工抽检30条问答日志,标记是否有幻觉。
  • 每次模型升级或知识库更新前,先跑一遍那30条的标准评测集做回归测试。

这套流程很土,但真的有用。我见过很多团队上了复杂的监控大盘,反而没人认真看数据。不如把精力花在“标准评测集”的维护上——这个集合的质量,决定了你对系统的把控能力。


最后分享几个我个人在实操中的体会。第一个是:AI工程的难点不在模型,而在数据。你花在清洗文档、设计切分策略、建评测集上的时间,回报率远高于反复调提示词。第二个是:别在一开始就追求完美架构。先用最土的方案跑通第一版,再用评估结果驱动迭代,这是最靠谱的路线。第三个建议:如果你正在做一个RAG相关的AI项目,务必先把“标准评测集”建出来——哪怕只有20个问题。没有评测,你就永远不知道系统到底好不好;有了评测,你每次改动都有了判断依据。

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

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

立即咨询