ARTICLE DETAIL

资讯详情

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

从零搭建AI工程:RAG与向量检索的实战指南

从零搭建AI工程:RAG与向量检索的实战指南 1. 为什么我会从一个仓库名出发重新定义AI工程事情的开头很普通我在整理GitHub星标列表时又一次刷到了那个叫ai-engineering-from-scratch的仓库。仓库名直译过来就是从零开始学AI工程。很多项目叫类似名字多半是资料大礼包或者教程索引。但那天我多翻了几页发现它其实是一套非常硬核的自学路径从数据管道、模型训练、评估、上线部署到迭代监控全链路都有涉及而且只给思路不给答案。我突然意识到一个有点扎心的事实过去半年我自己在做的AI应用离AI工程还差得很远。我写过的很多东西只是Python脚本把它们拼在一起再加上一个API调用看起来像那么回事实际上既不可复现也不可维护更谈不上系统化。从那个仓库名出发我花了半年时间重新梳理自己的认知和做法一点点把调接口的脚本变成能交付、能监控、能迭代的工程系统。这篇文章就是那次重构过程的完整记录。先说清楚AI工程和AI程序之间的差别。一个AI程序是输入文本进模型得到输出一个AI工程则至少包含这么几个环节可重复的数据处理流程、清晰的模型/提示词版本管理、可量化的离线评估、稳定的线上服务、可观测的运行日志以及一套持续迭代的反馈闭环。这不是教条而是我在线上被真实问题虐过之后总结出来的。这篇文章适合谁一种是有一些Python基础、调过一两个大模型API想把自己从demo玩家推向工程质量的人另一种是已经在做AI应用、却总在重复踩坑——比如模型输出忽好忽坏、指标虚高、线上效果比评测差一大截——的人。我接下来会把我的完整思路、选型理由、踩坑过程和改造动作都摊开讲包括那些我在线上文档里找不到、只能靠Debug和日志一点一点试出来的经验。2. 搭建我的第一个端到端AI工程基线从问题定义到交付2.1 场景选择为什么选工单摘要检索问答而不是花哨的生成式应用我的习惯是先选一个业务价值明确、边界可控的场景来练手。如果连场景都定义不清楚后面所有环节都会变成自嗨。我最后选的是企业内部客服工单的智能处理每天有大量客户反馈以自然语言形式进到系统里人工一个个看既慢又不一致。这个场景拆开来有两块需求一是把长工单自动压缩成结构化摘要方便值班人员快速判断优先级二是对历史工单做语义检索当新问题进来时自动找出最相似的旧工单和解决办法。这两块都能直接提升效率而且效果好不好非常直观——摘要准不准检索返回的内容有没有用业务方一眼就能看出来。选这个场景还有一个考虑它对文本生成的要求不算苛刻但工程链条非常完整。它需要数据清洗和存储需要做向量化需要搭检索服务需要把生成模型接进服务和适配输出格式需要设计离线指标和线上人工抽评。一个场景能把AI工程的各个环节都串起来这才是练手的正确打开方式。2.2 数据环节没有干净数据时我从哪些地方下手很多教程默认你手上已经有一份规整的、带标签的数据集但现实是你手里的原始工单表格里满是重复项、乱码、格式混杂的HTML内容甚至还有个人信息明文。我花在这个环节的时间远比我预想的长。我的做法分四步去重按工单标题文本内容的SimHash值做近似去重很多同一故障被不同用户各报了一遍不去重会让检索质量严重失真。清洗用正则和简单规则清理掉HTML标签、多余的换行符和不可见字符。这里有个小技巧不要在清洗之前就做小写化因为产品名和报错信息经常区分大小写而且后面要用的编码模型自己有统一处理过早在清洗阶段做标准化反而会丢信息。脱敏所有手机号、邮箱、身份证号、地址用预编译的正则规则做掩码替换。这一步不是合规部门要求的是我自己加上的因为任何一次不慎把真实信息传给模型API都是在制造安全隐患。弱标注我没有任何既有的该工单属于哪个类别的标签于是先用一组关键词规则做了一个粗糙的预分类再抽了一部分让业务同事人工复核。规则不可能全对但能产出一个种子集后面无论是做分类模型还是做检索评估都离不开它。我把每一步的输入输出都固化成独立脚本原始数据始终保存一份只读副本。任何经过清洗、脱敏、预标注的数据集都带上版本号和生成时间。做到这个程度之后基础数据质量才算勉强过关。2.3 检索方案为什么我先用RAG而不是微调我真正进入模型策略决策时给团队写了三页对比用提示词硬写逻辑、走检索增强RAG、对模型做微调。最终选了RAG原因很实在。提示词硬写的问题在于判断新旧工单相似这类语义任务根本无法用规则覆盖硬写出来的提示词五十行止不住效果还非常不稳定。而微调对我来说成本太高——我需要先攒几千条高质量的人工标注数据训完还要维护多版模型效果回归起来非常麻烦。RAG的思路是先检索再生成新问题进来先从历史工单里召回最相近的几条作为参考材料再让模型基于这些材料给出摘要和建议。它最大的好处是知识可以随时替换——新工单入库后检索池更新即可不需要重新训练模型。选型定了之后我对比了向量数据库。当时摆在我面前的有三个主流选择方案优势劣势专业向量库如Milvus性能强、支持超大规模运维成本高需要多部署一个服务轻量向量库如Chroma起步快适合原型生产环境长期稳定性一般业务数据库扩展pgvector与已有数据同库事务一致性好单机向量规模上限低于专业库我选了pgvector因为我们的主数据本来就在PostgreSQL里工单清洗完后直接在同一张表旁边建一个向量列不需要跨系统同步数据备份和权限管理都走原有机制。对几十万量级的工单pgvector配合HNSW索引完全够用。Embedding模型和生成模型也是分开选的。Embedding负责把文本变成向量我用的是开源的中文向量模型生成模型则用了托管的API模型原因是初期不想扛GPU运维先把流程跑通再优化成本。这一步选择背后的核心逻辑是把让系统跑起来和让模型最大化解耦不要让自己一上来就陷入模型训练的地狱。2.4 服务工程FastAPI、任务队列和容器化检索和生成的逻辑在Jupyter里怎么写都行但真正往工程化走第一步是把它们变成服务。我按三个层级来拆API层用FastAPI写轻量接口负责接收请求、参数校验、调用内部逻辑、返回结构化结果。业务层包括检索器的构建与查询、提示词模板的组织、模型输出的解析与过滤。持久层包括工单原始数据、向量索引、请求日志和人工反馈记录。摘要生成这个动作比较慢用户点一个按钮要等十几秒。如果直接用同步接口网关超时会非常难看。我引入了一个最简单的任务队列请求来了先落库并返回处理中的状态标记后台worker去跑摘要跑完更新状态。前端轮询状态即可。落地形态其实就是一个docker-compose.yml里面跑Postgres带pgvector扩展、API服务、worker、一个轻量的任务队列。我附上当时起步时最简的部署结构services: db: image: pgvector/pgvector:pg16 environment: POSTGRES_DB: ticket_system volumes: - db_data:/var/lib/postgresql/data api: build: ./app ports: - 8000:8000 depends_on: - db worker: build: ./app command: python worker.py depends_on: - db这个阶段的目标不是性能极致而是所有环节都能在本地一键启动、一键复现。任何新人clone下来跑一条docker compose up就能得到整个环境这才是工程化的入场券。2.5 评估我怎么量化这个系统好不好评估是AI工程里最容易被糊弄过去也最容易翻车的一环。我见过太多项目上线前拿几个例子人工看两眼觉得差不多结果上线后各种翻车。我的做法是把评估拆成两块检索质量评估和生成质量评估。检索质量我用了召回率K和倒数排位MRR。做法是人工构造一组问题-正确工单测试对跑检索看正确工单有没有出现在前K个结果里以及排在第几位。这个指标非常客观不受模型风格影响。生成质量则复杂一些。摘要问题我对比的是信息忠实度和要点覆盖率。信息忠实度指模型生成的摘要里有没有编造原文没有的信息这是底线指标要点覆盖率看的是该提的关键信息如报错代码、产品版本、涉及金额有没有漏掉。我用了RAG评估里常用的忠实度维度作为参考但真正落地时还是结合人工抽检每周随机抽50条让业务方按三个等级打分。所有评估结果跑完都会存成一张带版本号的表模型换了、提示词改了、检索TopK变了都重新跑一遍评估。没有这个机制我后来几次改动根本不敢上线。3. 我把前三个月的时间花在哪了踩坑实录3.1 坑一没有评估就上线线上效果比评测差了一大截第一次上线检索功能时我在离线测试集上看到召回率5高达0.85觉得信心十足。但上线第二天就收到运营反馈搜索账号被锁定相关工单返回的结果里有大量无关内容。排查时我查了一下线上日志发现问题出在查询分布上我构造的离线测试集里大多是完整、通顺的问句而线上真实查询又短又碎比如锁登不上重置几个字就发出去了。短查询缺乏上下文向量检索会匹配到很多含义宽泛的结果。这是典型的离线指标失真——你的评测集没有反映真实使用场景。修复方案分两层。一是给查询做个预处理查询进入检索器之前先做拼写归一、常见缩写展开锁展开为账号被锁定同时把工单类型字段过滤条件加上比如能锁到登录问题类目就绝不跨类目检索。二是重建离线测试集把真实线上日志里的查询捞出来重新构造评估样本。我更正后的策略很朴素评估数据必须是线上数据分布的子集而不能是你脑海中设想的分布。3.2 坑二数据泄漏指标虚高导致误判这是另一个让我非常难堪的问题。有段时间生成摘要的信息忠实度突然涨到了0.9以上我一度以为是提示词优化起了奇效。后来检查数据版本时才发现测试集里的一部分工单它们的答案文本早就被编码器在预训练阶段学习过了。说得具体点我的测试集在构建时选用的历史工单是公开论坛和社区里非常常见的几类问题而底下的生成模型很可能已经在预训练数据里见过类似写法。这导致模型生成的摘要并不是在阅读你的参考材料而是在背它见过的场景。要验证这一点我换了一批完全没有公开内容特征的内部特有工单做测试指标立刻跌到0.72左右。这个问题的根源是我把测试集和训练集的时间边界没控制好。正确的做法是测试集必须来自一个时间段之后的新数据并且确保这批数据从未暴露给任何模型训练过。以时间分割代替随机分割才能防止消息从未来泄漏到过去。后来我的所有评估样本都加了时间戳约束只看某个截止日期之后的新工单。3.3 坑三上下文窗口截断长文档摘要像盲人摸象第三个坑来自模型的上下文窗口限制。工单文本最长能到几千字而我的生成模型单次输入限制在8K tokens左右。一开始我的做法简单粗暴从前面截取4K tokens塞给模型。毛病很快暴露了——很多工单的关键信息恰恰出现在中间和末尾。用户先描述了一堆背景到后半段才写出真正的报错代码和最后步骤产生的效果。截断之后模型看到的是一段不完整的前言摘要结果自然各种漏重点。修复方案是搞了一个滑动窗口 分层摘要的管道先把长工单按段落切块对每块用模型生成低层摘要再把各块摘要拼起来做一次高层摘要。这个过程用任务队列串成异步管道每块独立跑最后合并。虽然成本高了一些但摘要质量和完整性显著提升。我还加了一个很有效的补充把工单的结构性字段类型、产品名、时间戳、上报者角色先以结构化形式传给模型让模型在生成时始终有全局信息打底。这个方法相当于给模型一张骨架图即使正文被截断结构化字段也能兜底。出现的问题是提示词要写成先看元数据再看正文分段摘要这又对提示词工程的排错能力提出了更高要求。3.4 坑四只看GPU/API成本没看隐性运维成本我在选型时特别在意API的单次调用价格比来比去省了几厘钱。但真正跑起来之后发现最花钱的其实是另一件事重试和异常处理。有一次上游模型API不稳定连续几个请求超时我没做重试退避直接一路向上抛异常。调用方都是同样的逻辑失败就重试一次重试又失败就丢给另一条链路。结果就是每个请求实际被翻了几倍调用量账单瞬间炸了。后来我加了两道保险。第一道是客户端级重试只对5xx错误和网络超时做重试指数退避重试最多三次4xx参数类错误绝不重试。第二道是业务级兜底如果模型摘要实在生成不出来降级到用规则抽取首段关键句返回不让用户干等。这个兜底逻辑日常几乎用不到但偶尔一次就能省下一整夜的故障告警。成本监控也做了结构化处理每次模型调用都记录模型名、输入token数、输出token数、耗时、重试次数、成本估算。每天跑一个汇总脚本按接口维度看成本趋势。不看这些数字你根本意识不到一次没必要的重复调用积累下来是多大一笔钱。3.5 坑五提示词调整像玄学因为缺特征归因最后这个坑也是我见过最普遍的一个效果很好的提示词稍微加点背景说明效果反而变差本来没问题的逻辑加了几个示例之后开始出现幻觉。调整提示词调到我怀疑人生没有任何工具告诉我为什么。开始解决这个问题时我做了三件事第一提示词版本化。每个提示词是一个独立文件带版本号、变更时间、变更原因、评测指标变化。不让任何提示词裸奔在代码里。第二结构化日志。每次生成都记录完整上下文System Prompt、User Prompt、检索到的资料块或它们的ID、模型输出、使用模型版本、温度参数。没有这个日志你根本无从归因这次变差是因为提示词还是因为检索回来的资料变了。第三控制变量法。一次只改一个变量比如只加一个示例或者只把TopK从3改到5其他全部锁死然后跑同一套评测集看指标。我怎么强调这一点都不过分很多人一次改三个地方效果变了根本不知道是哪一步造成的。做完这些提示词调整从一个炼丹工程慢慢变成可对照、可验证的迭代过程。它依然有艺术成分但至少每一步都有据可查。4. 从能跑到好用工程化改造的关键动作4.1 可观测性日志里要能看到模型在想什么可观测性是我花了很大力气补的课。原本的日志只有请求路径、状态码和耗时出了问题只能知道某个接口挂了但完全不知道是检索没召回东西、模型吐了异常文本还是下游解析出错。改造后的日志分成了三层请求层记录链路ID、参数、耗时和最终状态检索层记录查询改写后的内容、召回的TopK结果ID和相似度分数生成层记录实际使用的提示词版本、模型版本、输入输出token数、原始输出文本、后处理是否触发。每一层日志都带同一个链路ID追踪问题时一条命令就能把所有环节串起来grep request_id8f3a2c... service.log结构化日志上线后的第一个收获是发现检索返回的相关度分数普遍在0.70左右徘徊但业务方反馈仍然不错。这说明绝对分数低不一定是问题问题要看排序的相对位置。如果没有分数日志我可能会跑去调Embedding模型浪费大量时间。我还把成本估算、API延迟、错误码分布都做成了指标用Prometheus采集Grafana画板。硬件资源占用反而在其次最关键是模型调用链路的所有关键环节都必须有数字。没有数字就没有发言权。4.2 语义缓存与重复请求过滤趁还没烧钱之前挡住公司内部使用场景有个特点很多查询是重复的。同一批新员工入职问的问题高度相似。同样一个故障报修话术可能被不同人提交成两张工单。如果每次都去调Embedding和生成模型成本翻倍还不可控。我实现了一个语义缓存层向量化后的查询先进缓存计算与已有缓存项的余弦相似度超过0.92就直接返回缓存里的结果。这个阈值不能太低否则语义相近但答案不同的问题会互相污染也不能太高否则缓存就失去意义我用一周的真实查询调出了这个值。缓存命中带来的收益非常直观每周报表里显示大概有25%的查询被缓存命中对应的模型调用成本和平均延迟都降了一个量级。另一个意外收获是当模型API暂时不可用时缓存能兜住大部分重复查询服务降级的时间变短了。4.3 版本化模型即代码提示词也要走Git很多AI项目的糟糕之处在于模型的输出变好了或者变差了但没有任何记录能告诉你哪一次改动造成了这个变化。家庭作坊式开发可以靠记忆工程化开发必须有版本管理。我的做法是提示词、评估脚本、测试集、配置参数全都进Git。提示词按场景/环节/版本号组织目录每次改动必须同时更新评测结果和变更说明。比如prompts/ summarize/ v1.md # 初始版本忠实度0.78 v2.md # 增加分块摘要策略忠实度0.85 v3.md # 增加结构化字段注入忠实度0.89 retrieve/ query_rewrite_v1.md模型本身的版本也要记录。哪怕用托管的API我在每个下游任务配置里都写清楚用的模型名、部署时间、上下文窗口配置。这样线上日志里每一条生成记录都能对应到模型版本和提示词版本。遇到昨天还好好的今天突然变了这种情况我能快速判断是上游API迭代还是自己的某个调整踩到了。4.4 安全护栏输入脱敏、输出过滤、人工闭环直接对真实业务数据做AI处理安全这关绕不开。我见过不少先上线再说的团队等出了事故再后悔代价大得多。我的最小安全护栏包含四道输入层脱敏任何发送到模型API之前所有个人敏感字段都会先掩码输出层过滤模型返回的文本要先过一遍敏感词表和正则黑名单命中就拒绝入库访问控制API层用Token鉴权不同角色拥有的读写权限不同审计日志每一次处理都记录操作人、数据来源、脱敏规则版本、处理结果。最后一道是人工闭环摘要和检索推荐结果默认是建议态而非最终态业务人员必须点确认或修改后才正式标记为已处理状态。系统有了一个人工的确认环节即使AI出了错也永远不会直接影响业务流程。这是我的经验之谈AI工程落地时永远留一道人可以兜底的出口。5. 如果再来一次我会这样分配精力5.1 先练带约束的编码再碰模型回看这半年我在模型和提示词身上花的时间太多在工程基础上花的太少。如果让我重来第一件事是先把工程基本功打牢会写单元测试、会用Docker打包、会配置CI、会看日志和指标监控。这些听起来无聊但AI工程里大部分翻车现场都是基础工程能力不足导致的。举个例子你辛辛苦苦调好的RAG系统上线后因为忘了做数据迁移备份一次误操作把向量索引全弄丢只能重新全量构建。这种事故跟AI一点关系都没有纯粹是工程纪律问题。所以我的建议是在没有接任何一个模型之前先用普通Web服务把部署、测试、监控这套流程走通。等接模型时你已经有了可靠的容器环境、自动化测试和可观测的日志体系要做的事只剩关注模型本身。5.2 用三个小项目建立肌肉记忆如果从零开始我建议做三个递进的小项目而不建议一上来就做大项目。每个项目服务一个明确目标第一个项目搭一个带向量检索的RAG问答服务数据只有几百条全部本地跑。目标是打通数据入库、做索引、检索、生成、输出这整条链路理解每个环节是什么角色。第二个项目给这个服务补齐评估三件套——离线评测集、核心指标脚本、每次改动的对照报告。目标是养成任何对系统有影响的变更都要有量化指标支撑的习惯。第三个项目加上缓存、重复请求过滤、结构化日志和人工确认闭环。目标是让系统具备最基本的生产可用性。完成这三个项目之后再去看企业里的真实场景比如客服工单、知识库问答、日志异常检测都会从容很多。因为这些场景的本质都是同一套架构范式数据源、向量索引、检索策略、生成组件、评估反馈、可观测性。5.3 时间和精力分配的建议如果按百分比来算我现在的建议是30%花在数据工程清洗、标注、版本、管道30%花在评估体系建设指标设计、评测集维护、回归测试20%花在工程化改造服务化、容器化、可观测性、缓存剩下20%才花在模型本身选型、提示词、微调评估。这个比例在很多人看来反直觉——大家总觉得AI工程的重心应该放在模型上但现实是模型和提示词的能力天花板大家都差不多真正拉开差距的是数据质量、评估严谨度和系统稳定性。我前三个月的比例正好反过来70%都在调整模型和提示词结果线上效果依然一言难尽。把重心挪到数据、评估和工程化之后反而是最省时间的。5.4 最后一句话从零开始最难的不是学不会而是不知道做到哪算完这是我自己最大的体会。网上的AI教程大多止步于调通一个接口或跑通一个Demo如果你照着这个标准去做很容易陷入一种错觉能运行等于完成。但真实需求从来不是能运行而是在数据不断变化、模型版本不断演进、访问量不停波动的条件下依然稳定输出合格结果。从零开始这几个字真正指的不是把代码从空目录写起来而是把整个系统的可靠性、可维护性、可迭代性从零建立起来。做到这一步你才算是真的入了AI工程的门。我自己还在这个门里持续精进但至少回头看走过的路已经非常清晰地知道当初哪里走了弯路哪里——如果再走一遍一定会直接跳过去。
返回列表