ARTICLE DETAIL

资讯详情

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

从零搭建AI工程:企业知识库问答系统全链路实践

从零搭建AI工程:企业知识库问答系统全链路实践 「ai-engineering」这两年被喊得很响但真要把一条AI工程链路从零搭起来和在Notebook里跑通一个模型完全是两种体验。我把这个仓库起名 ai-engineering-from-scratch记录的正是过去大半年从零搭建一个端到端AI工程项目的全过程——一个面向企业内部知识库的文档智能问答系统。整个链路覆盖文档解析、数据清洗、向量化、模型微调、推理服务部署、线上监控和持续迭代每一步我都踩过坑也把坑填了。这篇文章不聊概念只讲系统怎么设计、工具怎么选、参数怎么定、线上问题怎么排查。目标读者是那些已经能跑通demo、正想把AI能力推到生产环境的工程师这份总结能帮你少走至少两个月的弯路。1. 为什么叫 from scratchAI工程不是把模型跑通就完事1.1 AI工程到底在解决什么问题先说我自己的转变。最早我做的所谓AI项目就是在Notebook里加载一个开源模型跑个示例输出结果还不错就往群聊里丢一张截图。但这套东西一旦要交付成系统马上会撞上几个原始问题数据从哪来、数据质量谁来保证、模型结果怎么评价、吞吐和延迟能不能满足、模型在线上悄悄变差了怎么发现。这些问题没有一个是“换个更大模型”能解决的。我对AI工程的定义就三条数据可信、模型可复现、系统可观测。数据可信意味着每一份进入系统的东西来源、清洗记录、版本都可查模型可复现意味着给同样的数据和代码任何时候重新跑得到的结果一致系统可观测意味着线上任何一个异常要么有指标暴露要么有日志能查。这个知识库问答项目本质上就是我用来把这三件事完整走一遍的载体把2000多份格式乱七八糟的PDF和Word弄成干净的结构化语料再做模型微调、部署推理服务、在线上持续观测。跑完这一圈我才觉得自己算是做AI工程的而不是只会烧显卡的。做之前我还担心这套体系是不是只有大团队才配拥有做到一半就明白了所谓的工程化核心不是平台和基建堆得多高而是把每一条链路都管理到可控状态。就算只有一个人只要能管住数据版本、实验记录、服务日志这三个点你也能把一个AI项目做成可持续迭代的工程。1.2 AI系统与传统软件工程的分界线为什么AI工程总是让人头疼因为它和传统软件工程有本质差异。传统软件里输入输出是确定的传两个数进去返回它们的和你可以写单元测试做断言。但AI系统的输出本质是概率性的同一个query今天答得不错明天换一段上下文可能就跑偏。这意味着你不能只用“功能正确”来验收产品得用“指标分布”来验收比如引用准确率、无效回答率、延迟分布这些才是AI系统的“单测”。更要命的是数据漂移。我上线后观察过一段时间用户提问的主题会随着业务活动变化而迁移你训练时精心准备的向量库和prompt三个月后可能就不是最优解了。传统系统的代码不更新就一直稳定AI系统不更新就会悄悄变傻。所以你必须把验证链条贯穿全程训练前验证数据分布上线后验证线上指标评审时验证业务效果。把模型当作整个系统里的一个组件来管理而不是把模型当成项目本身。“from scratch”还有另一层意思这套工程体系是可以从零组装的不需要等公司给你搭好训练平台、推理平台、监控平台。我这次只靠一台带GPU的服务器、一套Docker Compose、一堆开源工具就搭出了全链路。先自己把轮子造明白以后上云上K8s也只是平移方案而不是推倒重来。2. 从零搭AI工程的技术选型与架构设计2.1 选型原则与工具栈清单我的环境约束很现实一个人主导、初期零预算、后期要能迁到云上。基于这三条选型原则就四个字够用优先。我不会为了炫技一开始就上K8s也不会花几周自研分布式训练平台先把单机GPU、Docker、开源推理框架这条链路跑熟比啥都强。下面是我实际用到的工具栈环节工具选择选它的理由深度学习框架PyTorch HuggingFace Transformers生态最全资料最多排查问题成本低训练加速DeepSpeed用Zero-2就能在单卡上训LoRA不需要豪华硬件文档解析PaddleOCR unstructured覆盖扫描件、复杂表格和常见办公文档向量检索pgvector Milvus前期用pgvector省事数据量大了再切Milvus实验追踪MLflow参数、指标、模型注册一条龙开源自部署数据版本DVC数据集版本和代码commit绑定可回溯推理服务vLLMPagedAttention对吞吐提升非常明显API层FastAPI写起来快自带OpenAPI文档好调试缓存Redis语义缓存能减轻GPU压力性价比极高容器化Docker Compose单机编排足够避免一上来就啃K8s这张表不劝你照抄但选型原则可以抄优先选社区活跃、接口规范、文档全的工具。AI工程链条长任何一环选了个没人用、没文档的库后面出问题时你会非常痛苦。我中途换过一次解析库因为老库对扫描版PDF支持太差换库加适配就花了两天。选型时多花两小时调研后面能省两天。2.2 全链路管线从原始文档到问答答案整个系统的数据流我建议按下面这条链路来设计数据接入定时扫描文件服务器新文档自动入库。文档解析PDF、Word、扫描件转成半结构化文本。内容清洗去页眉页脚、合并表格碎片、处理OCR错字。分块与向量化按语义切成chunk用嵌入模型转成向量。向量存储连同元数据一起写入向量库支持相似度检索。召回用户query先向量召回同时配合BM25做混合召回。重排召回结果过一遍重排模型保留最相关的片段。生成把相关片段拼进prompt由LLM生成带引用的答案。评估离线评估集跑指标问题样本进badcase池。上线服务部署、监控、反馈回流。这条链路每个环节都独立这是刻意设计的。独立意味着每一层都可以单独测试、单独替换也意味着每个环节都能单独排查。比如我把向量库从pgvector换成Milvus时只改动存储适配层解析和重排完全没动。分层的另一个好处是你可以精确描述一个问题出在哪一段而不是整个系统黑盒。项目里最常见的问题描述就是“答案质量不行”但经过分层排查后八成问题会落在召回不对或分块破坏了语义真正模型本身出错的反而是少数。3. 数据管线与特征处理AI工程里最要命的地基3.1 文档解析与清洗的工程化处理数据这一步我实际花的时间比训练和部署加起来都多。2000多份文档里有排版干净的电子PDF有模糊的扫描件有带复杂表格的报表。最开始我简单粗暴全丢给通用解析库结果一半文档的段落顺序是乱的表格被拆得七零八落。这次经验让我把解析方案彻底重做最终落地成下面这套电子PDF优先用unstructured按版面解析保留标题层级和段落边界。扫描件走PaddleOCR识别文字并附上坐标再根据坐标判断阅读顺序。Word和PPT先转成中间格式再解析避免针对每种格式写解析器。统一清洗规则去掉重复空行、页眉页脚、常见噪音字符把断行按段落重新拼接。解析后的每份文档统一成一个JSON结构包含source_id、page、block_type、text、metadata。这个结构是整个数据层的“接口契约”非常重要。只要这个结构稳定解析器内部无论怎么换版本下游的清洗、分块、向量化都不受影响。我在解析环节遇到的另一个麻烦是扫描件OCR结果经常乱序正文和表格混在一起。后来我强制OCR输出带坐标再用简单的排序算法按坐标重组文本块顺序问题就基本消失了。3.2 分块策略与数据版本管理分块是我踩得最狠的坑之一。最开始按固定512字符切简单粗暴结果很多语义完整的段落被拦腰截断检索时选出来的片段前言不搭后语。后来改成“结构化优先 长度兜底”能按标题和自然段落切的就按语义块切一个语义块太长再按256 tokens的窗口切overlap设置32 tokens保证上下文衔接。每个块都带上metadata内容包括但不限于来源文件名、章节路径、页码、块类型正文、表格、标题。这些metadata在检索阶段价值巨大。比如用户问“2024年营收报表里的同比增长”我可以把向量检索范围先用metadata过滤到指定报表文档再做相似度计算召回质量立刻提升一个档次。选块大小也要结合业务块太大检索粒度粗上下文里塞满无关信息块太小语义不完整召回再准也生成不了好答案。256 tokens这个值对我这个中文业务场景比较合适但你要根据自己语料情况多试几次。数据版本管理我用的DVC。每个数据集目录生成一个校验文件代码仓库里存的是指向对应数据版本的指针。这样训练时用的哪份数据、哪个commit的代码全部可以回溯。没有这套机制你很难解释“上周模型还行这周重训一个怎么就变差了”。我还遇到过向量库和数据集版本对不上的问题文档更新后忘了重灌向量库线上检索到的还是旧内容。现在我把数据版本号写进向量数据的标签里每次检索都能校验版本一致性这个问题才算根治。4. 模型训练与实验管理让结果可复现4.1 实验跟踪没有记录就等于没做过训练阶段最大的敌人不是显存不够而是记不清。我早期经常出现这种情况一个实验跑完感觉不错但回想起来用的是哪份数据、哪个参数、哪个底座模型已经完全模糊。等想复现时只能靠玄学。所以后来我强制自己在MLflow里记录每一条实验信息数据版本的哈希、代码commit号、超参数lr、batch、epoch、seq_len、LoRA秩、评估集和评估指标、模型产物和输入输出签名。这相当于给每个实验写病历。模型训练本身有随机性没有完整记录你看到的任何一次跑分都可能是孤例。我给自己的硬性要求是每个实验都用同一套固定评估集评估集200条覆盖高频业务问题每个epoch在评估集上完整跑一次指标不只盯着训练loss。只看训练loss是自欺欺人验证集指标才能告诉你模型到底是不是在泛化。评估集也不是一成不变的。每轮badcase review后我会把新发现的高频问题沉淀成用例补进评估集。半年下来评估集从最初的80条长到了400多条。指标可能会因为数据集变难而下降但这恰恰让你对模型的真实水平心里有数而不是看着一个虚假的漂亮分数沾沾自喜。4.2 训练工程化关键设置下面是我实际用的训练配置单张A100 80G训练7B模型做LoRA微调DeepSpeed Zero-2不开Offload保证训练速度。bf16混合精度优先选bf16而不是fp16。bf16动态范围更大省去了fp16的loss scaling处理训练更稳。单卡batch size设为4梯度累积8步等效batch 32。显存不够但想模拟大batch时梯度累积是常用手段。学习率2e-4warmup 200步随后线性衰减。warmup能稳住训练初期的梯度波动这个配置下loss下降明显更顺滑。序列长度2048LoRA秩16作用在query和value矩阵上。数据集做shuffle固定随机种子保证可复现。几个关键参数为什么这么定学习率2e-4是LoRA微调的常见起点秩16对多数指令任务已经够用再大容易过拟合。seq_len 2048是为了对齐文档问答的长上下文场景。这些都没有绝对标准但我的原则是每次只改一个变量改完看评估集指标你才能准确判断到底是哪个改动产生了效果。还有一条经验评估集指标只能筛掉明显更差的模型真正的胜负在线上。两版模型离线跑分差不多时不要纠结先上线小流量对比看badcase分布再决定。离线评估做得再精细也模拟不了线上真实用户千奇百怪的提问方式。5. 模型上线从Notebook到生产环境的最后一公里5.1 推理服务封装与显存管理模型在Notebook里能回答问题是远远不够的用户要的是一个稳定、快速、可调用的API。我用FastAPI把推理封装成三个接口健康检查、检索、对话。模型只在服务启动时加载一次这是最基础的常识但实际中经常有人不设全局变量、每个请求都重新加载模型显存瞬间爆炸。代码示意LLM None def load_model(): global LLM LLM LLMEngine.from_pretrained(...) app.on_event(startup) def startup(): load_model() app.post(/chat) async def chat(req: ChatRequest): if LLM is None: raise HTTPException(503, model not ready) result await run_in_executor(LLM.generate, req.messages) return result显存管理也是大坑。embedding模型、重排模型、LLM推理引擎挤在同一块GPU上不加限制很容易OOM。我后来给每个服务单独设置环境变量限定它在哪张卡上运行再用Docker Compose分配资源services: llm: image: llm-service:0.3.1 environment: - CUDA_VISIBLE_DEVICES0 deploy: resources: reservations: memory: 32G这样至少保证各个模型不会互相踩踏排查问题时也清楚知道谁占用了哪块卡。还有一个小细节服务启动后要主动warmup一次让模型把显存申请好否则第一个请求会慢得离谱甚至直接超时。5.2 推理性能优化量化、缓存与并发上线第一周我就发现单个用户请求还好并发一上来延迟和显存双双告急。优化路径我按性价比排了序换vLLM。PagedAttention对显存的管理比手动写batch生成强太多吞吐直接上了一个台阶。同一条机器上每秒生成token数至少翻了一倍。量化。把7B模型压到INT8在文档问答场景实测质量大约掉1到2个点但显存占用和生成速度的收益很值。短期可接受后续评测集更完善了再考虑回退。语义缓存。query向量相似度超过0.92时直接返回缓存结果。业务问答里很多用户反复问类似的问题这套缓存能拦掉三分之一以上的重复请求。控制并发。FastAPI里用信号量限制同时生成的请求数多余的请求排队等待而不是同时冲进来把GPU打爆。还有一个容易被忽略的点预处理和LLM生成不要抢资源。PDF解析和OCR是CPU密集型任务虽然不占显存但线程一多会和GPU服务抢内存最后导致整机不稳定。我把预处理改成异步任务队列高峰期宁可让解析任务积压也不影响在线问答的稳定性。这条经验是在一次线上事故里用十几分钟排查出来的教训不轻。6. 线上监控与持续迭代模型上线只是开始6.1 监控什么指标才不会慌没有监控的AI系统出问题时连排查方向都没有。我重点盯这几类指标维度指标我的阈值 / 预警服务健康P95延迟、QPS、错误率P95延迟超过3秒报警资源GPU显存使用、显存增长趋势显存持续增长说明有泄漏或缓存堆积生成质量输出长度、无效回答率、引用率无效回答率超过5%查上下文数据漂移query嵌入分布与基线差异连续一周偏离基线则报警数据漂移这块我线上定期保存用户的query embedding每周末和训练时刻的分布做对比。分布变化大说明业务方向变了该补充语料、调整prompt了。告警设置也有讲究阈值别太灵敏否则狼来了喊多了就没人理了。我一开始把延迟阈值设成1秒结果每天报警几十条很快就麻木了。后来改成P95大于3秒才预警才真正起到作用。日志必须是结构化的每条请求记录query、检索到的文档ID、生成的答案、延迟、得分和trace_id。排查badcase时这些日志就是破案线索。有一次用户反馈答案引用了假来源最后就是靠trace_id把整个请求链路翻出来发现是召回阶段某个文档的metadata被污染才锁定根因。6.2 反馈闭环怎么让系统越用越准我设计了反馈闭环让系统不会因为上线就停止进化用户在答案下方点“有帮助 / 没帮助”结果实时回流。没帮助的回答自动进入badcase池附带当时的上下文和引用来源。每周做一次badcase review把高频问题沉淀成新的评估集用例。发现是检索问题就调召回和重排发现是生成问题就调prompt两者都调不动了才考虑微调。模型更新节奏上我建议小步快跑。先改数据再改prompt最后改权重。因为改数据和prompt的周期短、风险低微调放在最后而且微调后必须回归整个评估集防止旧能力被遗忘。这套节奏让我上线后的两轮迭代都稳住了没有出现一次更新后效果反而变差的情况。7. 常见问题与排查技巧实录7.1 训练阶段的坑训练loss不降很多人第一反应是调模型结构、调学习率。我的第一反应永远是看数据。有一次loss怎么都压不下去把训练集随机抽了50条肉眼过了一遍发现三成样本的答案来自错误文档。数据错了模型再强也只能学到噪声。这个习惯帮我避免过好几次无用功。显存OOM时我优先调低batch size再用梯度累积补等效batch而不是硬换小模型。梯度累积带来的训练速度损失比重新调整套超参数要可控得多。loss震荡得像心电图时先检查warmup和学习率常见原因是lr偏大、warmup过短。如果eval loss比train loss高一大截那就是过拟合优先减小LoRA的秩或者增加数据量而不是盲目加正则化。还有一个不怎么起眼但容易坑人的点训练数据顺序会影响收敛曲线每次shuffle都用固定随机种子否则同样的配置跑两遍可能是两条不同的曲线排查起来极其痛苦。7.2 部署与运行时的坑部署现场我遇到过不少问题整理成一个快查表症状可能原因处理方式服务启动即崩显存被其他进程占满排查CUDA_VISIBLE_DEVICES和服务资源分配首个请求特别慢模型懒加载没有warmup启动时跑一次空请求预热偶发延迟陡增CPU预处理线程抢资源把解析任务下沉到独立队列日志乱且无从查非结构化日志统一JSON格式强制记录trace_id日志里的trace_id真的很重要它贯穿一条请求的解析、检索、生成全流程。没有trace_id你在多服务架构里定位一轮请求就像在图书馆找一本没有书号的书。这是我上线后感触最深的一条现在任何新服务我都会把trace_id作为第一优先级接上。7.3 数据层面的坑数据层的问题通常不是“跑不通”而是“结果歪”。分块时把关键句子切断导致检索到的片段前言不搭后语解决方法是结构化优先分块加overlap。表格被转成纯文本数值和结构信息丢失我后来对表格单独做结构保留转成markdown表格再入库检索质量立刻上来了。OCR错字导致关键术语检索不到我建了一个同义词词典检索query和索引文本同步做同义词扩展命中率明显提升。这些坑不会让系统崩溃但会悄悄拉低用户对系统的信任。我现在的习惯是任何一次badcase review先问是不是数据问题再问是不是模型问题。按这个顺序排查能省下大量时间。毕竟改一条数据规则可能只要两小时重训一次模型可能要两天。如果让我把这个项目重做一遍顺序会完全不同第一周先把评估集做出来第二周再碰解析和清洗第三周才让模型参与测试。很多人一开始就把全部精力放在模型上结果后面发现数据不可靠、评估不标准前面的模型实验全部白做。AI工程这条链上模型只是算式的最后一个乘数数据、评估、监控才是决定最终结果能有多高的底数。先把地基打牢再追求模型的fancy工程上从来不亏。
返回列表