资讯动态

从零开始AI工程:数据清洗、RAG选型到部署与监控的完整实践

发布时间:2026/9/28 13:15:02 来源:尧图企业网站定制
1. 为什么我把这个项目命名为 ai-engineering-from-scratch先说个身边经常看到的场景模型能力越来越强开源社区每天都有新模型发布各种Demo视频刷屏朋友圈但真正能把一个AI应用稳定跑进业务里的人依然不多。我在过去半年里被问得最多的一句话是“为什么我照着教程把代码跑通了真要用起来还是不行”。大多数情况下问题并不出在模型本身而是出在被教程跳过的那些环节上——数据怎么准备、怎么评测、怎么部署、怎么监控。这些环节拼在一起才叫AI工程。这也是我整理“ai-engineering-from-scratch”这套内容的初衷。我不打算从概念讲起。网上讲AI工程定义和趋势的文章已经够多了真正缺的是顺着一条可以落地的路径把每一步怎么选型、怎么写、怎么避坑讲清楚。本文就是以“从零开始做一个企业文档智能问答系统”为线索带你走完整条链路数据准备、模型选型、提示词工程化、服务部署、线上监控。你如果是一名开发工程师或者正在做AI相关项目的技术负责人这篇文章能帮你省掉不少“摸索完发现方向错了”的时间。项目核心关键词是“ai-engineering”整体思路是这样一句话AI工程不是写一段调用大模型接口的代码而是让这端代码在一个真实运行环境里稳定、可控、可维护地产生价值。所以本文会非常偏实践很多配置、代码片段、评测方法都是我实测后留下的你可以直接抄作业。我还想表达一个反直觉的结论从零开始做AI工程最大的成本往往不是模型推理费用也不是开发时间而是你对自己系统的失控感。当你不知道数据从哪里来、不知道模型为什么输出这个结果、不知道线上用户为什么会触发某个badcase时整个项目会变成一团迷雾。而工程化的过程就是把这团迷雾一层层拨开。下面每一章都在做这件事。2. 目录结构设计把整条工程链路拆成六个可执行的阶段在进入具体实现之前先看看这套内容对应的项目结构。我采用模块化思路每个阶段都能独立启动、独立验证彼此之间通过标准格式的数据和接口衔接这样在调试时可以快速定位问题出在哪一环。ai-engineering-from-scratch/ ├── data/ # 数据层存放原始资料、清洗脚本、生成结果 │ ├── raw_docs/ # 原始PDF/Word/网页文档 │ ├── cleaned/ # 清洗后的纯文本 │ └── qa_pairs/ # 用于评测的问题答案对 ├── pipeline/ # 数据处理管线 │ ├── extract.py # 从不同格式中抽取文本 │ ├── clean.py # 清洗、分段、去重 │ └── chunk.py # 文本切分与向量化 ├── models/ # 模型相关代码 │ ├── embedder.py # 向量化模型封装 │ ├── llm_client.py # 大模型接口封装 │ └── prompt_templates/ # 提示词模板目录 ├── eval/ # 评测模块 │ ├── build_qa_set.py # 构建评测集 │ ├── run_eval.py # 跑评测 │ └── metrics.py # 指标计算 └── serve/ # 服务部署 ├── api.py # FastAPI服务 ├── cache.py # 缓存层 └── monitor.py # 监控与日志这个结构对应了AI工程里最核心的六件事数据准备、模型封装、检索增强、评测体系、服务部署、监控反馈。我把它作为贯穿全篇的线索。下面每一章会深入一个阶段把其中隐蔵的坑和取舍逻辑说清楚。3. 数据层AI工程质量的上限从源头上就被决定了很多从零开始做AI工程的人会犯同一个错误拿到模型后先急着写提示词把数据问题扔到一边。结果模型输出经常“一本正经地胡说八道”或者在某个知识领域上的回答质量飘忽不定。我个人的经验非常明确数据准备和清洗的时间至少要占到项目总工期的一半以上。模型能力再强喂进去的是垃圾产出的也只能是高级垃圾。3.1 原始资料的采集格式你以为的“半结构化”其实是非结构化我接触的企业文档场景里原始资料形态五花八门扫描版PDF、Word合同、Excel报表、网页介绍、PPT方案。第一步工作不是“想清楚怎么解析”而是先把它们统一“降级”成纯文本。拿PDF举例。数字原生的PDF可以用pypdf抽取文本老式扫描件则需要OCR。最简单粗暴的判断方法新建一个文本文件把PDF里的文字复制进去如果乱码或缺字说明这个PDF需要OCR。我用过的OCR方案里开源的有PaddleOCR本地私有化效果好识别中文准确率比Tesseract高出一大截。虽然部署显得重但实测之后我认为是值得的。这个阶段有一个坑如果你不提前想清楚会非常痛苦文档编码和格式混乱问题。有些Word文档是GBK编码有些是UTF-8处理不好会出现大量乱码。清洗脚本里的第一行就应该把编码统一掉# pipeline/clean.py 摘录 def read_text_file(path: str) - str: for enc in [utf-8, gbk, gb18030, latin-1]: try: with open(path, r, encodingenc) as f: return f.read() except UnicodeDecodeError: continue raise ValueError(f无法识别文件编码: {path})这样虽然看起来笨但在真实数据面前笨办法往往才是靠得住的。注意不要把原始文档直接作为模型输入。大模型的上下文窗口虽然越来越长但文档里的页眉、页脚、目录、空行、重复声明都会干扰生成质量还白消耗Token。3.2 文本清洗的落地细节不只是去空格那么简单文本清洗的核心目标有三个去掉噪音、保留语义、降低切块的难度。我给清洗环节定了这么几条规则在实践中反复验证过删除页眉页脚规则上就是看每页开头和结尾重复出现的文字删除目录页尤其是页码指向的内容对检索系统帮助为零统一中英文标点和全角半角折叠连续空行为一个换行按章节标题保留结构信息用 \n\n\n 作为大段分隔标记清理无意义的换行符——很多PDF转出来的文本会在一行没写满时强行断行这会导致段落碎成一段一段检索时语义不连续。清洗后建议做一次抽样肉眼检查随机抽10段文本去读确认没有乱码、没有结构错乱再进入下一步。3.3 段落切分不能只看字符数要顺应文本的自然边界RAG方向的项目里切块方式是决定检索效果的第一因素。这里有一个常见的矛盾块太小上下文不完整召回信息碎片化块太大直接塞给模型做答案生成时又会混入大量不相关内容。我的做法是结构感知切块structure-aware chunking优先按标题和段落边界切分在段落过长时再按句子边界二次切分同时保留段落的来源信息比如“来自于XX文档的XX章节”供后续引用追溯用。如果文档有标题层级可以用正则把标题识别出来让每个切块自带一个“小标题”这样检索命中时模型更容易定位。切块后建议写一个统计脚本看一眼每个块的长度分布。如果大量块的字符数在几百到一千之间且分布在自然标题边界上那大概率是可用的状态。如果切出来的块全部集中在固定长度上很可能你的切块算法是“硬按数字截断”后续检索效果会打折扣。4. 模型选型与封装不追最强模型只追最稳的组合模型选型是所有环节里最容易被带节奏的。今天很多团队在立项时直接说“用当下最强的模型”但在真实落地中工程系统要的是平衡成本可预估、延迟可接受、可私有化部署或数据合规允许。我建议从三个约束出发做选型。4.1 三个约束成本、延迟、数据边界数据边界企业文档可能包含内部机密如果无法接受数据出网你要么用私有化部署的开源模型要么买云厂商的专属实例。这一步直接决定了后面所有方案。别在项目做到一半时发现数据合规不通过再回头换模型那种重构代价相当大。成本一个简单换算关系是“每千Token的价格乘以日均请求量”。我见过不少项目用大模型做文本分类和实体抽取杀鸡用牛刀成本一个月下来高得离谱。实际上很多结构化任务用小参数模型就能达到95%以上的效果。延迟如果应用面向用户在线交互要求1到2秒内的响应那需要控制模型的推理链路。开源模型如果部署在没有GPU的生产环境速度会很尴尬如果调用API链路尽量只保留一次大模型请求不要做“模型A判断要不要调用模型B然后B再调用C”的串联。4.2 我常用的一个组合分层设计我的惯用组合是“小模型做子任务大模型做生成”。具体来说文本向量化用开源的 embedding 模型比如bge-large-zh或m3e-base这种模型参数量不大跨机部署成本低中文语义效果够用路由或分类如果任务是判断“这个问题是否需要走知识库检索”我用一个较小的分类模型解决不走大模型判断省时省钱最终答案生成这里才上大模型把检索到的相关内容作为上下文生成最终答案。这样一个分层设计不仅省钱还让每个环节都更容易debug检索不对就查embedding模型分类不对就查路由模型只有答案质量问题时才需要去调整大模型的提示词和参数。4.3 封装接口时预留好扩展位模型封装模块不要写“死”建议把嵌入模型、生成模型都设计成可配置项。我的做法是写一个model_config.yaml把模型名称、API地址、Key、温度、最大Token数、超时时间都放进去运行时动态加载。这样以后模型升级或更换直接改配置不用动业务代码。# models/model_config.yaml 摘录 embedding: type: bge-large-zh dim: 1024 device: cuda:0 llm: provider: openai-compatible base_url: http://localhost:8000/v1 model: Qwen2.5-14B-Instruct temperature: 0.2 max_tokens: 1024 timeout: 30另外所有大模型调用必须做异常兜底和重试。网络抖动、上游服务不稳定、返回err这些都是线上常态不做兜底的话用户端表现就是偶发性的错误或空白页。5. 检索增强生成让“问什么答什么”变成“问到点子上”既然做的核心是文档问答系统RAG检索增强生成就是重头戏。很多人以为RAG就是把文档切块、向量化、建个向量库、查相似度然后塞给模型看完下面的内容你会发现真正的工程问题都在细节里。5.1 向量检索只是起点不是终点向量检索的缺点非常明显它对关键词敏感度低对同义改写敏感但在精确匹配场景上反而表现不佳。举个例子问一句“公司年假制度怎么规定的”如果知识库里原文写的是“休假管理办法”部分embedding模型可能会漏掉最相关的文档。所以我建议至少用两层召回第一层BM25关键词召回。用rank_bm25或者ES的bm25相关性算法把和问题有直接词汇重叠的段落捞回来第二层向量语义召回。把问题向量化召回语义相关的段落。两层召回各自返回TopK然后合并去重再按自定义的分数让重排器去挑最优的若干段。这样既保住了精确匹配的文档又不错过语义相关的上下文。5.2 “重排”是RAG最容易拉差距的地方你以为重排是把两层召回的结果直接合并给大模型那还不够。我给重排定的规则是把召回结果按“来源文档分布”做一次去重——同一个文档最多保留3段避免模型被同一份资料的相似表述反复灌输相关性打分用交叉注意力重排模型如bge-reranker-base它的效果比纯向量相似度更好因为它是把“问题段落”拼在一起做精细语义匹配设定一个相关性阈值低于阈值的段落直接丢弃。宁可返回“知识库中暂未找到相关信息”也不能让模型基于无关段落硬编。这一步做和不做线上效果天差地别。很多项目把RAG效果差归结为“模型不行”其实八成是召回和重排没调好。5.3 提示词模板里要把“检索证据”和“生成限制”写清楚生成层的提示词模板我调整过不下十个版本目前稳定使用的策略包含三个要素给模型限定“只能基于提供的上下文回答”并声明“如果上下文里没有相关信息请明确告知”把检索到段落按编号列出要求模型在回答时引用编号方便人工追溯明确要求答案不要重复上下文原文而是做概括和转述这样回答更自然也不会让用户看到一个“原文片段堆砌”。还有一个细节不要把检索到的所有段落全部填入Prompt。上下文塞得越多模型越容易在其中“挑”出一些不相关信息来编造。精挑细选3到5段有质量的上下文比堆10段效果要好得多。6. 评测机制没有评测的AI工程就像没有测试的软件工程这一章是对很多人的“点睛之笔”。我在与同行交流时发现他们做AI工程最迷茫的是“不知道线上效果算好还是算差”。没有量化指标就只能靠“感觉”。这种状态下后续优化根本无从谈起因为你不知道改动到底产生了什么影响。6.1 怎么快速构建一个小而有效的评测集最轻量的做法从业务对话记录里收集60到100个真实问题并人工写好标准答案。这些问题要覆盖简单检索类、推理类、对比类、略复杂问答类。不要用模型生成答案再喂给模型环环相套容易掩盖问题。评测集字段可以设计为{ question: 公司年假最长可以休多少天, expected_keywords: [20天, 累计工作年限], expected_source_doc: 休假管理办法.pdf, category: policy }跑评测时通过“答案里是否包含预期关键词”“是否召回预期来源文档”两个指标来判断基础效果。更精细的可以加“答案相关性”“忠实性”这类模型评价指标但那套体系比较复杂从入门角度先把关键词和召回率两项做扎实已经能发现绝大多数问题。6.2 三个关键指标能够具体操作的那种我最看重的三个指标分别是检索召回率RecallK正确答案对应的文档有没有出现在召回的TopK里。这个指标不对后面生成质量再高也救不回来答案忠实度Faithfulness生成的回答是否严格基于检索到的上下文有没有加入模型自己的幻觉内容。做法是把“上下文”和“生成的答案”再交给一枚评判模型打分端到端准确率Accuracy基于人工标注的标准答案做比对看整体回答正误率。我在跑评测时用到的套路是“先恶化后优化”故意把一个环节调到明显不好的状态比如关掉重排器确认后台指标能反映效果变化再做优化。这能验证评测体系是否灵敏不然你改动后指标纹丝不动评测就形同虚设。6.3 评测要能回归每改一次提示词、每换一次模型版本都用同一套评测集跑一遍完整回归。这个流程看起来耗时间但在项目迭代中非常值得。没有回归机制的话今天优化了A问题明天冒出来B问题项目状态会一直处于“修修补补”的混沌里。7. 服务设计与部署从脚本到生产环境还差了很多细节模型效果可以接受之后进入部署阶段。很多开发者在“脚本能跑”和“线上可用”之间摔得鼻青脸肿下面这些点是我实测之后认为最容易踩且后果严重的。7.1 用FastAPI搭一个最简单但完整的服务服务层我习惯用FastAPI异步支持和OpenAPI文档天然适合做AI应用。一个最小实现就两件事一个对外接口一个内部调用链路。# serve/api.py 摘录 from fastapi import FastAPI from pydantic import BaseModel app FastAPI(titledoc-qa-service) class QARequest(BaseModel): question: str user_id: str anonymous class QAResponse(BaseModel): answer: str sources: list[str] app.post(/v1/qa, response_modelQAResponse) async def answer_question(req: QARequest): # 1. 检索召回 contexts retrieve(req.question) # 2. 生成回答 answer generate(req.question, contexts) # 3. 返回答案与溯源 return QAResponse(answeranswer, sources[c.doc_name for c in contexts])要注意一个关键点接口传入的只应该是业务数据问题和用户ID不要在业务接口里去暴露“模型参数怎么设置”这类内部细节。控制参数放到配置模块统一管理这样接口才会稳定后续升级模型也不需要让调用方改代码。7.2 缓存层让重复问题少烧钱少等待真实业务里大量用户会问相近甚至完全相同的问题。没有缓存每次都去跑向量检索加大模型生成成本和时间都会失控。加一个简单的缓存层键为“标准化后的问题”值为“答案来源”。这里的学问在“标准化”。用户问“年假能休几天”和“公司年假是多少天”两者在缓存命中上会互相错过。我的做法是先用小模型做一次问题改写和归一化再取哈希做缓存键。虽然多了一次小模型调用但在高频重复场景下节省的生成成本显著大于一次改写成本。7.3 容器化部署时的内存与并发问题部署时最容易被忽视的是embedding模型的显存占用。bge-large-zh这类模型加载后大约占1.3GB显存但如果和生成模型同一张GPU又没有留足余量推理时会直接OOM。我的建议是embedding模型可以放CPU跑推理速度在批量场景下可以接受生成模型才用GPU。如果并发上来了CPU模式的embedding可能成为瓶颈再用独立的GPU实例单独部署embedding服务通过HTTP内部调用。上线前的检查清单大致是这样的服务日志有没有分级INFO/WARN/ERROR能不能按请求ID串起整条链路超时时间有没有设置上游慢请求会不会拖垮整个服务并发量往上压时显存和内存会不会爆掉容器健康检查有没有配好启动顺序是不是先加载模型再对外提供服务有没有做简单的限流防止恶意刷接口。8. 线上监控与持续迭代模型上线不是终点部署上线只是进入了最漫长的阶段持续维护和迭代。很多AI项目死在“上线后没人看护”这一步。模型不像传统软件那样是确定性的它的效果会受数据漂移、用户提问分布变化、上游模型版本更新等因素影响。8.1 日志里要记录哪些字段线上日志必须包含用户原始问题、标准化后的问题、检索到的TopK文档列表、模型最终回答、响应耗时、命中的缓存标讑、模型版本号。这些字段的作用有两个方面一是用来复盘badcase二是用来判断线上真实问题分布。有了完整日志评测集的更新就不再是拍脑袋了。每隔一两周从日志里抽一批新问题补进评测集重新跑评测观察效果的变化曲线你就能清楚地知道系统是越来越稳还是需要干预。8.2 最容易“翻车”的三类线上问题根据我的观察最容易出现的是这三类第一类检索召回漂移。新上传的文档格式特殊切块异常或者文档主题和原有文档高度相似导致检索到了错误来源。这类问题的发现靠“来源文档分布统计”如果某个文档被反复召回但用户反馈不断大概率这个文档的切块和metadata有问题。第二类模型幻觉集中在“信息缺失”场景。当知识库里没有答案时模型倾向于编造一个看起来合理的回答。解决方案是前面提过的相关性阈值加严以及提示词里强约束“没有信息就说没有”。评测集里必须包含一批“无答案”问题用来专门测试模型会不会瞎编。第三类长尾问题处理不当。用户问得又多又杂很多问题没有命中知识库。积累一段时间后应该对无命中问题做聚类分析如果某类问题持续高频说明该主题的知识资料缺失这时要补文档而不是试图通过提示词解决问题。8.3 持续的样本回流机制最理想的迭代闭环是线上问题日志 → 人工粗筛 → 补充到评测集 → 离线评测验证 → 优化检索或提示词 → 发布新版本 → 观察线上指标。这套机制跑通后AI工程就进入一个正向循环。我在实际维护中最深的体感是做一个AI应用并不难难的是一直让它维持在靠谱状态里。9. 回到标题从零开始的AI工程本质是在做“系统性”“ai-engineering-from-scratch”这个项目对我来说不只是写一套代码更像是在建立一种思考方式。每一层的选型、每一个参数、每一次评测都是在为目标服务让一个AI系统在真实环境里可以被理解、被控制、被改进。如果你现在正要从零开始一个AI项目我想给出的建议很简单不要先写代码先写清楚数据和评测方案。把数据从哪里来、怎么清洗、是否过期、评测集怎么建、怎么判断好坏这些问题回答完再动手开发你的工程进度反而会快得多。很多项目碰到的“调不通”“效果不稳定”根源都在前面这些环节欠了债。这个过程不会一帆风顺保留着一点好奇心和较真劲就好。每遇到一个奇怪的结果就去翻日志、找原因追得多了你会慢慢感觉到自己不再是“调API的人”而是真正在“做AI工程”了。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑