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_id=8f3a2c..." 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工程的门。我自己还在这个门里持续精进,但至少回头看走过的路,已经非常清晰地知道当初哪里走了弯路,哪里——如果再走一遍,一定会直接跳过去。