拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

AI工程化从零搭建:RAG+Agent知识库问答实战路线图

AI工程化从零搭建:RAG+Agent知识库问答实战路线图

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按坐标区域精细提取。对于扫描版PDF,OCR方案一般优先选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": f"p{page['page']}-{len(chunks)}" }) buffer = seg else: buffer += seg if buffer: chunks.append({"text": buffer.strip(), "page": page["page"], "chunk_id": f"p{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(key="doc_id", match=MatchAny(any=doc_ids))]) hits = client.search( collection_name="knowledge", query_vector=query_vec, query_filter=query_filter, limit=top_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导致的内网配置泄露事故,这种问题一次都不能出。

推荐的基础部署架构是:FastAPI(Web层) + 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工程这条路上少踩几个深坑,真正建立起自己的技术判断体系。

返回列表