从零开始搭一条AI工程项目线,远比想象中复杂。年初我们团队要做一个企业内部文档智能问答的项目,仓库里没有任何AI相关的基础设施,甚至连GPU机器都是临时借的。整个项目从立项到上线小范围试用,我踩过的坑、推翻掉的方案、以及最终沉淀下来的工程套路,凑齐了这篇文章。如果你也正打算从零开始搭ai-engineering能力,希望这篇复盘能帮你少走几段弯路。文章会围绕一个具体项目展开:从问题定义、数据管线、模型实验评估,到上线部署和线上回归,完整走一遍。
1. 项目背景与第一阶段:把问题定义清楚再动手
1.1 我们到底要解决什么问题
项目最初的需求很模糊:"做一个智能问答助手,让员工查制度、查流程更快。"这种描述几乎等于没说。我入职后第一周做的事情,不是选模型,而是把问题拆成了三个层面:
- 用户场景:员工在什么场景下会提问?比如"年假怎么算""报销流程需要几个审批节点"。
- 回答形式:是直接返回一段文字,还是给出相关文档片段让用户自己判断?
- 数据范围:知识库包含哪些文档?更新频率如何?有没有权限隔离要求?
这个拆解过程直接影响了后续所有技术选型。比如"回答形式"如果要求给出文档出处,那么纯靠大模型背诵知识是不够的,必须有检索增强环节;如果数据范围包含敏感部门文档,那么权限过滤就必须进入系统设计,而不是事后补。
我的强烈建议是:动手写代码之前,先把这些问题整理成一页纸的需求文档。哪怕团队只有你一个人,也值得写。因为AI工程最贵的时间不是训练模型,而是在错误的方向上反复试错。需求文档不用严谨到PRD级别,但必须能回答"用户怎么用、答错了会怎样、数据从哪来"这三件事。
1.2 从零做技术选型的判断逻辑
技术选型阶段,我给自己定了四个约束:
- 不开源的技术不碰,避免被厂商绑定。
- 社区活跃度优先于文档漂亮程度,因为踩坑时只能靠社区。
- 允许先用一套简单但能跑通的方案,再逐步替换组件。
- 每一步都考虑能不能回滚到上一版。
基于这几个约束,我挑了主链路:向量数据库用PostgreSQL的pgvector扩展,没有单独引入Milvus;模型部分先走API调用,把流程跑通后再决定要不要私有化部署;编排框架直接用Python写,不引LangChain之类的大型框架。这个选择看起来"不够AI-native",但在当时团队没有专职AI运维的情况下,它最大程度降低了排查链路跨度。事实证明,很多问题都出在管道衔接上,组件越少越容易定位。
踩坑提示:不要一上来就搭一套"完整"的AI平台。平台化是业务验证之后的事情,早期最合适的形态是一个可以被快速丢弃或重写的脚本级管线。
2. 数据管线的搭建:真正的工程量集中在数据
2.1 文档采集与清洗:问题比想象中琐碎
我们的知识库文档散落在多个系统里,有Word、PDF、Excel、PPT,还有少量直接写在Wiki里的页面。第一步是写一个采集脚本,把这些内容统一抓下来,转成纯文本。这一步我踩了三个实打实的坑:
第一个坑是PDF解析质量。很多表格型制度文档用常规解析库抽出来之后,行列关系完全乱了,比如"审批节点"和"审批时限"被拆成两段孤立文本。后来我换成了基于布局分析的解析方式,对表格结构做感知,再针对特殊模板单写规则。第二个坑是图片型PDF,扫描件必须先做OCR,这导致整体处理时间涨了三倍。第三个坑是重复文档和版本问题,同一个制度文件在共享目录里存在"最终版""最终版V2""绝对最终版"三个版本,如果没有去重和版本识别,知识库会被污染得很严重。
清洗阶段,我按规则做了几件事:把连续空白符压缩、统一换行符,把全角符号转半角,把页眉页脚切掉,把"第X页共Y页"之类的噪声行去掉。这些处理看起来基础,但直接影响后续切分质量。你切出来的文本块如果带着页眉噪声,检索向量里就会混入高频无意义特征,轻则浪费token,重则检索结果漂移。
2.2 切分策略:改过三次才算稳定
文本切分是决定检索质量的核心环节,我前后调了三轮。
第一轮用固定字符数硬切,每512个字符一个块。结果很糟糕,很多句子被腰斩,检索到的片段读不通。第二轮改成按段落切,超过上限再折半。段落切分保留了语义完整性,但遇到超长列表类文档时,一个段落可能有几千字,继续切分会把编号列表腰斩。第三轮,我索性自己做了一个切分器:
- 以标题层级和段落边界为主边界。
- 单个块的上限设为800字,超过则按句子边界切。
- 切完后对相邻块做10%~15%字符重叠,避免检索时刚好遗漏边界内容。
- 每个块保留来源文档ID、标题路径、页码,方便溯源。
def split_document(text, max_chars=800, overlap_ratio=0.1): blocks = [] # 先按标题和段落边界拆成粗块 rough_blocks = split_by_headings_and_paragraphs(text) for block in rough_blocks: if len(block) <= max_chars: blocks.append(block) else: sub_blocks = split_by_sentence(block, max_chars) for i, sub in enumerate(sub_blocks): if i > 0: sub = merge(blocks[-1][-int(max_chars * overlap_ratio):], sub) blocks.append(sub) return blocks这个切分器的代码本身不复杂,但它是我整个项目里改动最频繁的模块。每次线上检索效果有问题,回头查切分都能发现新边界情况。经验是:切分规则尽量数据驱动,把一段异常文本喂进去观察结果,比在纸上设计完美算法更有效。此外我给每个文本块算了一个"质量分",低于阈值的块(比如全是表格碎片)会标记为低优先级,不进主检索索引。这个策略在后来的效果回归中帮了不少忙。
3. 模型实验与评估:决定成败的是评测方法
3.1 先跑一个笨的baseline
团队里有人一开始就想微调开源模型,我按住了这个冲动。理由是:在数据管线和评测集都没准备好的时候动手微调,结果好坏都没有参照系。我选择先做一个基于检索增强的baseline:从预置知识库里检索TopK文本块,拼进提示词,让模型生成回答。这一步用了商用模型API,回答质量不稳定,但胜在能快速串联整个链路。
baseline的价值不在于效果好坏,而在于它给了你一个可比较的"地板":后续任何优化策略,都必须打过这个地板。如果哪天真要微调,也必须先确认检索管线本身没有短板。我们当时测下来,baseline在简单制度问答上粗略正确率大约有75%,但在多跳问题(比如"申请A补贴需要满足B条件吗")上掉到不到50%。这些问题成为后续优化的重点。
3.2 评测集:不建评测集,效果就是玄学
我见过很多项目上线时凭感觉说"效果不错",结果用户一用就崩。为了让效果可量化,我建了一套三层评测体系:
- 第一层是单轮问答集,每个问题配标准答案和文档出处,用于跑离线自动评测。
- 第二层是带干扰项的问答集,问题里故意混入无关条件,看模型会不会被带偏。
- 第三层是真实用户会话回放集,从灰度日志里捞真实问题,手动标注好标准答案。
离线自动评测我用了一个很简单的打分逻辑:先判断回答里是否包含关键实体,再判断标准答案中的关键句子是否被召回,最后人工抽检百分之二十。之所以不把整段语义相似度作为唯一指标,是因为在实际场景中,用户更关心的是"数字、日期、流程节点"这些硬信息有没有答对。语义相似度高但关键数字错,等于完全错误。
指标上我盯四个:检索召回率(Recall@K)、生成答案的事实一致性、端到端正确率、平均响应延迟。这些指标会进每周一次的效果回归,任何技术改动都不能只看一两天的表现。
3.3 从纯RAG到混合方案的一次转折
跑了三周之后,发现纯RAG方案有一个硬伤:知识库里的制度更新之后,检索到的旧版本内容会有误导。我们当时在知识库文档里加了版本字段,但检索的是文本块向量,版本信息只是元数据,并不直接参与相关性排序。于是结果经常是旧版本制度排在前面,新版本排在后面。
解决思路不是在提示词里写"请优先使用新版本",而是改检索逻辑:如果同一个文档标题下有多个版本,只索引最新版本;同时在检索阶段增加一个硬性过滤条件,把已归档的文档排除掉。这个操作本身不花哨,但它逼着我把数据管线里增加了"版本号提取"和"索引重建任务"。
进一步地,我引入了混合检索:向量召回负责语义相关性,关键词召回负责精确匹配制度和术语编号。比如用户问"报销上限5000"时,关键词"报销上限"精确命中远比向量相似度靠谱。混合检索融合后,TopK召回率从原来的0.78提升到了0.86,端到端正确率从56%升到了67%。这个收益在当时比换更贵的模型大得多。
4. 部署、监控与回归:上线才真的开始
4.1 推理服务设计:别只盯着模型性能
上线之前,我们面临一个选择:继续用API还是私有化部署一个小模型。对比之后,我选择了后者。原因有三个:数据不能出域、用户请求量有波峰、长期算成本更可控。但私有化的代价是自己扛运维。我选了支持量化的模型,用半精度推理,单卡能撑住大约30并发,再配合排队机制把请求削峰填谷。
推理服务部署成三个独立服务,互不拖累:
- 检索服务:只负责向量召回和关键词召回,返回TopK文本块。
- 生成服务:调用本地模型,输入提示词文本,输出回答。
- 路由服务:负责权限校验、请求转发、超时控制。
# 部署时用到的关键启动参数示例 python -m vllm.entrypoints.openai.api_server \ --model ./model_dir \ --served-model-name local-qa-model \ --dtype bfloat16 \ --max-model-len 4096 \ --gpu-memory-utilization 0.8 \ --port 8001这里踩过一个真实教训:单独压测检索服务和生成服务时都正常,一联调就超时。原因是检索服务在大并发下偶尔跑到300毫秒,生成服务首次请求因为显存预热的冷启动高达8秒,路由服务设置的超时时间是10秒,勉强能过。但在高峰期,检索偶尔会跑到400到500毫秒,加上排队,整体就崩了。定位到问题后,我把路由超时拆了两级:第一级等待调度,第二级等待生成首字。同时给生成服务加了空闲预热,把冷启动时间压到2秒以内。这种"单服务正常、联调崩"的问题,最能体现AI工程化和单纯搞模型的区别。
4.2 线上可观测性:没有日志就是盲人摸象
第一版部署上线之后,我几乎每天都会被用户反馈吓一跳。有人说"回答不完整",有人说"怎么答非所问",最麻烦的是这些反馈很难复现。后来我硬性规定,所有线上请求必须记录三层日志:
- 输入层:原始问题、用户身份、请求时间。
- 中间层:检索到的文本块ID、相关性得分、排序位置。
- 输出层:生成的回答、各阶段耗时、是否触发超时。
有了这些日志,我就能做归因分析。比如"回答不完整"的案例,查日志发现是检索到的文本块太少,只有两个块,生成时信息不足。于是我把TopK从3调到6,情况立刻改善。"答非所问"的案例则大多是权限过滤把关键文档滤掉了,检索返回的是替代文档。这时候问题出在权限规则配置,而不是模型本身。
我还会定期做线上效果抽样回归:每周从日志里随机抽200条问答,人工判断是否正确,并将结果拆到各知识分类下。长期积累下来,我大致知道哪个分类的错误率偏高,再去反向优化切分和索引。这个过程很费人力,但没有捷径。
4.3 效果回归:让每一次迭代都可度量
上线后我定了一个规矩:任何改动,包括改提示词、换模型权重、调检索参数,都必须先跑一遍离线评测集,并记录前后的指标变化。评测集跑完还要在包含真实流量回放的测试集上过一遍,防止过拟合到小样本。
一次典型的回归流程是:
- 写清楚改动意图和影响范围。
- 导出当前线上配置作为基准版本。
- 在离线评测集上跑基准版本和候选版本,对比四个核心指标。
- 用线上日志回放50条真实请求,人工观察输出质量。
- 评估风险后灰度发布,先放5%流量,再逐步放量到100%。
我在这个流程里吃过亏。有一回只改了切分器的重叠比例,离线指标略有上升,线上却在某些超长文档场景下检索结果变乱。原因是我没有在回放集里加入"超长文档问答"这一类典型样本。从那之后,评测集里专门加了一个"长文档"子集,每次都单独看它的指标。AI工程的严谨性,说到底就是这些细节积累出来的。
5. 从零到一线复盘:真正的门槛其实在工程环节
5.1 我重新理解的AI工程概念
这个项目做下来,我对"AI工程"这个标签的理解有了很大变化。AI工程不是训练一个模型,而是把模型放进真实业务环境并稳定运转的一整套能力。它包含数据管理、评测体系、部署方案、监控告警、迭代流程,以及团队协作规范。模型本身只占了整条链路的一小部分。
回头看,最耗时间的三个环节分别是:数据清洗与切分、离线评测集的建设、线上效果归因。这三件事都不需要"高深算法",但它们决定了项目的天花板。如果你也在做一个AI项目,建议先在纸面上把这三个环节的人力和流程排出来,不要全部注意力都放在模型选择上。
5.2 给零基础起步者的最后几点建议
- 第一条:不要等数据完美了再开工。先拿10%的数据把链路跑通,你会更早发现真正的问题。
- 第二条:评测集从项目第一天开始攒。做了第一版问答后,立刻手动记录失败案例,这些内容以后都会成为评测集的一部分。
- 第三条:保留每个阶段的实验记录。我之前用表格记录每次改动的参数、指标、结论,三个月后回看,这张表格比代码注释更有价值。
- 第四条:警惕任何"玄学调优"。如果一项改动解释不通原理就生效了,很可能只是在小样本上的一次偶然波动,要复测确认。
- 第五条:控制技术栈数量。每多一个组件,排查链路就多一个黑暗角落。能用数据库扩展解决的,就别单独引一个服务。
最后分享一个我的个人习惯:每次做完一个阶段的迭代,我会把"当时的判断依据"和"后来实际发生了什么"写进一个单独文档。这些内容写的时候很费劲,但等到下一个新项目开始时,它就是你最可靠的参考手册。从零起步做AI工程没有想象中那么光鲜,大部分时间都是在和数据、日志、不一致的结果较劲。可也正是这些较劲的过程,让我真正理解了什么是工程,什么是炼金。