
早几年我还在老老实实调模型、跑实验真正让我对AI工程这四个字产生敬畏的是一次给内部业务部门交付问答助手的经历——模型在测试集上表现不错可一上真实数据就各种答非所问接口偶尔超时日志里全是乱码新来的同事照着README配置环境折腾了两天赋不出同一个结果。那一刻我才意识到从模型到系统中间隔着一整条工程化的河。这篇东西就是基于我自己的项目经验重新梳理的一份AI工程从零开始的路线图。它不完全讲算法也不只讲框架而是把问题定义、环境搭建、数据处理、模型选型、部署上线和后续监控这条链路完整走一遍。适合正准备把AI功能产品化的开发者、团队里负责技术落地的同学也适合那些模型调通了但不知道怎么做成稳定服务的人。1. 开工前的三件事问题定义、技术选型与预算公式很多项目翻车不是死在模型上而是死在开工前没想清楚我们要做的到底是什么。所以我习惯把AI工程的前置工作分成三件事把业务问题翻译成技术问题、把技术栈收敛到最小可运行闭环、把算力和成本算成一张能对账的表格。1.1 先把做模型和做系统分开我见过太多团队把做一个AI系统等同于训练一个模型这是两码事。如果你的需求是识别图片里的表格结构那是模型问题但如果需求是每天自动处理一万张图片、把结果写入数据库、失败自动重试、中间件挂了能恢复那是系统问题。AI工程的核心矛盾往往在后者。判断问题属性的方式很简单问自己三句话——现有模型能力是否可以直接满足需求如果行这是工程集成问题。是否需要对模型做针对性调整如果行这是微调或训练问题。两者都需要但都不确定那你要先做概念验证PoC不要一上来就搭平台。我在项目里会把可行性验证和工程落地分成两个阶段。PoC阶段允许代码写得乱、流程手动、不保证性能但一旦进入工程阶段代码规范、可观测性、数据版本化都要立刻建立起来。两个阶段混在一起往往会导致你一边调参一边补架构最后两边都做不好。1.2 技术栈选型的底层逻辑最小可运行闭环我看过很多团队的技术选型文档列了十几个组件比全家桶还全。但AI工程从零开始选型原则应该是能少一个组件就少一个让每个技术决策都能回答它解决了什么不可绕开的问题。以典型的RAG问答系统为例一套最小闭环至少需要以下组件环节最小必要组件可延后组件开发环境Python 3.10、uv/condaDocker、K8s模型管理HuggingFace Hub或本地模型目录MLflow、模型注册中心推理运行时PyTorch或ONNX RuntimeTriton、TensorRT向量存储FAISS或ChromaMilvus、Qdrant服务暴露FastAPI网关、服务网格监控日志文件、结构化日志Prometheus、Grafana建议第一版只保留左列。等服务真的有人用了、流量起来了再考虑右列的规模化组件。起步就上K8s加一堆中间件的架构维护成本足以拖垮一个三人小组。1.3 算力预算公式与成本预期算力预算是系统工程里最容易被低估的部分。我习惯用一个相对朴素的公式估算单次推理成本 峰值显存 /批量大小 × 单次批处理延迟举个例子一个7B参数量的量化模型约需6GB显存在L4 GPU上跑batch size为16的生成任务单批延迟约2秒那它的吞吐大概是每秒处理8条请求。如果业务量是每秒50条你就需要大概7张卡再留30%冗余就是9张卡。这个估算很粗糙但足以让你在采购和部署前心里有数。除了推理成本别忘了数据清洗和向量化也是吃资源的环节尤其是embedding大批量文本时很多人只算模型推理的钱漏掉了索引构建和重试消耗。2. 环境与依赖管理第一天就决定你未来是否想删库跑路从零开始做AI工程第一个真正意义上的坑不是模型跑不起来而是环境管不住。Python版本、CUDA版本、torch版本、第三方库版本任何一个对不上复现就是一句空话。我在这个环节踩过最深的坑是拿别人的requirements.txt在本地跑结果torch版本与自己机器的CUDA不兼容整个编译过程花了六个小时最后依然失败。2.1 Python虚拟环境uv、conda怎么选Python虚拟环境我最初用venv后来换成了conda再后来换成了uv。conda解决的是Python解释器版本和部分二进制依赖问题在需要不同Python版本之间切换时很舒服但conda的解析速度慢环境大了以后装包特别熬人。uv是目前我用下来最顺手的工具快而且依赖解析准确。我的建议是团队统一用conda管理Python版本项目内部用uv或pip管理包依赖。具体到项目我一般这样初始化# 创建基础环境Python 3.11 conda create -n ai-eng python3.11 -y conda activate ai-eng # 在项目内用uv初始化虚拟环境 uv venv .venv source .venv/bin/activate # 安装核心依赖 uv pip install torch --index-url https://download.pytorch.org/whl/cu121 uv pip install transformers datasets fastapi uvicorn chromadb为什么需要两层环境因为conda负责Python解释器级别的隔离uv负责包级别的可复现。只靠conda常出现换台机器就装不上的问题只靠venvPython解释器版本又控制不住。分层管理是我踩了多次坑后沉淀下来的固定动作。2.2 Docker镜像里最容易被忽略的坑CUDA版本与依赖缓存等到要把服务部署到服务器Docker基本是绕不开的。但AI项目的Docker镜像比普通Web服务更容易翻车主要原因是CUDA生态复杂。我踩过一个典型问题基础镜像用pytorch/pytorch:2.2.0-cuda12.1-cudnn8-runtime本地跑没问题部署到目标机器上却提示找不到libcudnn。后来查了半天是宿主机驱动版本和容器内CUDA版本不匹配。建议的做法是在Dockerfile里显式依赖nvidia的官方基础镜像并锁定tag不要用latest。同时注意CUDA小版本差异宿主机驱动版本用nvidia-smi确认镜像内部版本用nvcc --version确认两边的CUDA主版本必须大于等于容器内所需版本。还有一点AI依赖动辄几个GB不加缓存策略的话每次CI构建都是灾难。我会在Dockerfile里把依赖锁定文件单独COPY先安装依赖再COPY源码利用Docker层缓存避免每次改代码都重新下载torch。FROM pytorch/pytorch:2.2.0-cuda12.1-cudnn8-runtime WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . CMD [uvicorn, app.main:app, --host, 0.0.0.0, --port, 8000]2.3 依赖锁定requirements.txt里藏着魔鬼看到很多人直接把torch2.0写进依赖文件这是给自己埋雷。不同torch版本之间的行为差异你根本无法预料比如torch.cuda.is_available()的结果都可能因为编译配置不同而有差异。我们要做的是生成精确锁定的依赖文件# 使用pip-compile或uv lock生成锁定版本 uv pip freeze requirements-lock.txt锁定之后任何时候复现环境都应以这个文件为准。我一般会在项目里同时维护两个文件requirements.in手工维护的直接依赖记录版本范围requirements-lock.txt自动生成的完整依赖树精确到每个子包版本升级依赖时先改.in文件再重新生成锁文件绝不在锁定文件里手工改版本号。3. 数据管线的真实分量清洗、切分、评估集、版本化如果说模型是房子的主体数据就是地基。做AI工程一段时间后你会发现一天里真正花在模型上的时间可能只有20%其余时间全在和数据打交道。数据处理的脏活累活没有人替你省掉而且是决定项目上限的关键。3.1 数据从哪来私有知识库与公开语料的取舍企业内部项目绝大多数数据来自私有知识库比如产品手册、维修记录、客服聊天记录、工单系统导出的CSV。这些数据的共同问题是格式混乱、字段冗余、含有大量无关信息。以工单数据为例一次导出可能包含几十列字段但真正和模型训练/检索相关的可能只有标题、描述、处理结果三列。我的处理流程通常是字段裁剪只保留需要建模或索引的字段。清洗去重、删除空文档、处理编码问题GBK/UTF-8转换、乱码过滤。标准化时间格式统一、日期时区统一、数字单位统一。敏感信息脱敏手机号、身份证号、地址等字段在进入系统前必须脱敏。这份工作看起来简单实际执行时你会发现所谓数据质量好的团队凤毛麟角。哪怕数据量不大脏数据对后续RAG检索的影响也极其致命——垃圾进垃圾出。3.2 切分策略chunking的粒度直接决定RAG效果我在做RAG系统时最影响检索质量的因素不是模型而是文本切分。切得太碎每个chunk语义不完整切得太大两个不相关的话题被塞进同一个向量里检索出来精度很差。我通常采用两层切分策略文档类型首选切分方式兜底切分方式结构化表格按行组表头前缀固定长度技术手册按标题层级Markdown/HTML结构段落级切分工单/日志按完整记录切分固定长度重叠政策法规按条款编号切分字数上限重叠固定长度切分时建议token数控制在300到500重叠50到80个token这样可以避免关键句被拦腰截断。更高级的做法是让chunk携带上下文路径例如每个chunk前面拼接文档标题和一级小标题检索时这些前缀能显著提升相关性。3.3 嵌入模型选择与向量库对比嵌入模型embedding model是RAG的基石。我最初直接用某个通用中文embedding模型效果平平后来换成了针对中文优化的bge系列检索命中率明显提升。选择嵌入模型时不要只看MTEB榜单分数要拿自己的业务query去测召回效果。榜单分数是通用语料上的平均表现你的数据分布不同排名可能完全变化。向量库的选择主要看数据规模和部署位置。单机几十万条向量FAISS完全够用如果要求实时增删改查并且分布式扩展再考虑Milvus或Qdrant。Chroma的好处是零配置、开发体验好适合原型验证。不要一上来就搭分布式向量库开发阶段完全是自找麻烦。3.4 评估集的构造没有评估就没有优化没有评估集你的所有优化都是自我安慰。做RAG类系统我至少会构造三类评估数据单轮问答集50到100条真实业务问题每条标注正确答案和对应文档段落。多轮对话集模拟真实用户连续追问用来测上下文理解和session管理。边界样本集包括模糊问题、无答案问题、对抗性问题用来测系统拒绝回答的能力。我会把这些评估集单独放在eval目录下作为代码仓库的一部分来版本管理。每次修改prompt或调整切分策略都跑一遍评估脚本记录检索命中率和生成准确率两个指标。没有这一步你根本不知道改动到底是变好了还是变糟了。4. 模型能力与编排不训练也能定制的几种路径很多刚入门的同学容易陷入凡事都要微调的执念。实际上工程化的第一原则是尽量不动模型权重用更轻量的方式达成业务目标。能用prompt解决的就不要去训练能靠检索增强的就不要去微调。成本低一个数量级而且可维护性高得多。4.1 为什么大多数场景不需要从头训练从头训练大模型的投入是千万级起步的绝大多数业务场景根本走不到那一步。哪怕是微调也要慎重。微调的本质是在一个通用模型的能力之上把它的行为朝你的数据分布对齐。它的代价不仅是算力还包括维护多个模型版本、数据更新后要重新微调、线上效果不可控等一连串工程问题。我见过一个做合同信息抽取的项目最初他们尝试对开源模型做全参微调花了大几万块算力效果提升并不明显后来改用指令微调结构化输出解析只用几千条标注数据在LoRA上做轻量适应抽取准确率反而提升明显迭代成本更低。这说明先想清楚瓶颈在模型能力还是任务适配再来决定要不要动权重。4.2 Prompt模板与输出约束让模型按格式说话我强烈建议在代码中把prompt模板作为独立模块管理而不是散落在各个业务函数里。一套成熟的prompt模板应该包含这几个部分系统指令、业务上下文、示例few-shot、输出格式约束、兜底规则。举个实际的例子PROMPT_TEMPLATE 你是{assistant_name}一个负责{task_description}的AI助手。 请基于以下资料回答用户问题 {context} 用户问题 {question} 要求 1. 只回答与资料相关的内容 2. 如果资料中找不到答案请明确回复无法从现有资料中找到答案 3. 输出使用以下JSON格式 {json_schema} 结构化的输出约束非常关键它把模型输出变成可以被程序直接解析的数据结构。我在实际项目中会要求模型输出JSON然后做一次validation格式不对就重试一次带上错误提示。这比让模型自由输出再靠正则解析要稳定得多。4.3 检索增强RAG的工程实现骨架RAG看起来简单查向量库、拼prompt、调用大模型。但工程实现时有一堆细节。我提供一个简化的骨架from openai import OpenAI from chromadb import Documents, EmbeddingFunction, Embeddings class QueryPipeline: def __init__(self, collection, llm_client, embedder, top_k5): self.collection collection self.llm llm_client self.embedder embedder self.top_k top_k def retrieve(self, question: str) - str: query_vec self.embedder.embed(question) results self.collection.query( query_embeddingsquery_vec, n_resultsself.top_k, ) return self._format_context(results) def generate(self, question: str, context: str) - str: prompt PROMPT_TEMPLATE.format( assistant_name知识助手, task_description回答业务知识问题, contextcontext, questionquestion, json_schema{...}, ) resp self.llm.chat.completions.create( modelqwen2.5-7b-instruct, messages[{role: user, content: prompt}], temperature0.1, max_tokens512, ) return resp.choices[0].message.content这里有个容易被忽略的细节distance和top_k的选择。FAISS默认的L2距离适合欧式空间但通常用余弦相似度检索文本向量效果更好。向量库的索引类型也要根据数据量选小于1万条用暴力检索1万到100万用IVF大于百万再考虑HNSW或分区。4.4 什么时候才需要微调看清成本拐点微调的一个实用判断标准是当检索和prompt都优化到一定程度业务指标仍然不达标并且你可以明确指出模型缺少某类知识或模型行为模式与预期不符时才考虑微调。而且微调的第一选择是LoRA这类参数高效微调方法不是全参微调。全参微调的隐患不只是算力贵还有灾难性遗忘——模型可能学会新任务的同时忘掉旧能力。LoRA相当于在模型权重旁边挂了一条低秩可训练旁路原始权重不动因此遗忘风险小得多多个LoRA还可以同时挂在一个基础模型上切换使用工程上非常灵活。5. 部署与上线从跑通到扛住生产流量模型在Notebook里跑通只是万里长征第一步。部署环节考验的是系统意识服务化、推理优化、监控、安全和稳定性。这一块把它当成工程问题来解决不把它当算法问题来看。5.1 模型服务化的两种形态同步API与异步任务落地AI能力时第一件事是决定交互形态。同步API适用于实时的单轮或短交互场景。比如在线问答、内容审核用户发来请求、等待返回。实现方式常用FastAPI封装一个/predict接口背后挂进程内加载好的模型。异步任务适用于耗时长、批量大的场景。比如离线文档批量向量化、长文档审核需要任务队列Celery、Arq结果存储前端轮询或回调。同步服务最怕模型推理阻塞所有worker所以推理进程和工作进程要分离。我现在习惯的架构是FastAPI只做请求校验和转发模型推理放在专门的推理服务里通过内部HTTP或gRPC通信。这样模型扩容和API扩容互不影响避免了改一行接口代码就要重启模型进程的尴尬。5.2 推理加速三板斧量化、批处理、缓存推理加速是上线前必做的优化技术手段很多但最实用的三板斧是量化、批处理和缓存。第一量化。把模型从FP16压到INT8或INT4显存占用直接降一半甚至更多。工程上我优先用bitsandbytes做4bit量化或把模型转成ONNX Runtime再用动态量化。量化后的精度损失在大多数业务场景下可以接受但要在评估集上实测确认。第二批处理。大模型推理是batch越快单位成本越低。所以推理服务最好支持动态batching把并发的请求攒在一起凑够一个batch再喂给模型。在FastAPI里可以用Semaphore控制并发配合缓存队列实现简单动态批处理。import asyncio class BatchInferenceEngine: def __init__(self, model, max_batch8, timeout0.05): self.model model self.queue asyncio.Queue() self.batch_size max_batch self.timeout timeout async def predict(self, prompt: str): loop asyncio.get_event_loop() fut loop.create_future() await self.queue.put((prompt, fut)) return await fut第三缓存。对重复出现的query直接缓存结果尤其是知识库问答高频重复问题往往占30%以上。缓存键可以设为query规范化后的hash命中后直接返回大幅降低模型压力。5.3 监控体系系统指标与效果指标要分开看上线初期最容易犯的错是把监控等同于看CPU和内存。对AI服务而言你要监控两类指标缺一不可。系统指标请求量、延迟P50/P95/P99、错误率、GPU利用率、显存占用。这类指标用PrometheusGrafana就能收关键是P99延迟反映真实体验不要只盯平均值。效果指标检索命中率、生成准确率、用户反馈率、无答案率、拒答率。这些指标不能靠基础设施监控拿到需要业务打点。我会在每个请求的响应里加一个trace_id把问题、检索结果、生成结果、耗时整条链路的日志串起来定期抽样做人工标注评估系统质量在真实流量下的表现。5.4 安全与合规边界做AI工程安全不是上线前才补的模块而是从设计阶段就要考虑的约束。内容层面要做好输入输出双向过滤用户输入不能带提示注入模型输出不能包含不当内容。权限层面RAG系统尤其要防止越权访问——你用一套向量库存了整个企业的文档那检索范围就得按用户角色做隔离不能让普通员工查到涉密资料。日志层面查询记录和模型输出都要留存方便事后审计。合规这块务必要提前问清楚模型部署在哪个区域、数据是否能出境、用户隐私条款怎么写。不要赌不要等到监管找上门再补。技术上的隔离和审计日志现在不留将来就要花十倍的力气去补。6. 实测复盘一次从零到上线的完整案例前面讲了方法论这一节我把最近的一个实际项目完整复盘一遍。这个项目是给一家制造企业做产品知识库问答助手从零搭建经历了从原型到上线的全过程很多问题和前面提到的坑一一对应。6.1 项目背景与验收标准甲方有数百份产品手册和维修案例分布在Word、PDF和Excel表格里。业务诉求是让售后人员用自然语言查询某型号设备出现某故障怎么处理。验收标准有三条常见问题检索命中率不低于80%首轮回答平均耗时不超过8秒支持至少20个并发用户同时使用。这三个标准定得很清晰比效果好一点这种模糊表述强得多。我建议所有项目在开工前都做这一步把验收指标量化哪怕暂时估不准也要先定一个基线后续再修正。6.2 关键决策与代价这个项目里做了几次关键决策现在回头看基本都对但代价也都清晰可见。第一次决策用RAG而不是微调。当时有人建议微调一个售后领域的问答模型但几万块算力投下去售后案例还在不断更新模型根本追不上资料的迭代速度。用RAG每次资料更新只需重新索引对应文档成本低、实时性强。代价是检索质量高度依赖数据清洗和切分前期数据工程投入很大这部分工作占了项目总工时的40%左右。第二次决策用开源模型部署在客户内网不用云端API。因为涉及产品手册和维修案例客户明确要求数据不出内网所以最终选择了Qwen2.5-7B-Instruct量化后部署在一台单卡A10上。代价是开源模型的能力上限比头部云端API弱一些需要更精细的prompt和检索侧优化来弥补。第三次决策异步批量做文档向量化同步API做问答。原始文档几百份向量化一次需要几十分钟用异步任务队列跑得很稳。问答侧用同步接口配合缓存把高频问题响应压到了2秒以内。6.3 上线后第一周发生的三件事故第一件是乱码问题。客户提供的一部分PDF是用特殊字体生成的提取出文本后很多字符变成乱码检索结果自然一塌糊涂。后来我在文档处理流程里加了文本质量校验乱码比例超过阈值就直接把该页丢弃不进入向量库。第二件是并发打满导致超时。上线第二天售后团队二十多个人同时使用单卡推理服务瞬间被打满P95延迟飙到15秒。当时紧急做了三件事启用动态batching、把最大token数从512降到256、在API层加了限流。处理后P95降到6秒左右再后来加了缓存高峰期的表现才稳定下来。第三件是多轮对话的方向跑偏。用户第一轮问A型号故障怎么修第二轮问那B型号呢系统把它当成独立问题处理返回了完全无关的内容。原因是会话没有携带上下文摘要。修复方案是增加对话历史摘要注入每轮生成前先把前几轮的问答压缩成一段摘要放进prompt的context里。这个改动让多轮场景的效果明显改善。6.4 可复用的检查清单项目收尾后我沉淀了一张Checklist每次做类似的AI工程都会过一遍问题定义是否量化成可验收指标。技术选型是否保持最小可用闭环。数据清洗是否覆盖编码、格式、敏感信息。切分和embedding是否在业务数据上实测过。评估集是否覆盖正常、边界、无答案三类问题。推理服务是否做了量化、批处理、缓存。监控是否同时覆盖系统指标和效果指标。权限隔离、输入输出过滤、日志审计是否到位。最后说点我的实际体会。做AI工程这些年我自己最大的改变是不再沉迷于把模型效果刷高一点而是更在意这套系统能不能在真实环境里稳定地解决问题。从零开始搭AI工程体系很像是在织一张网——模型、数据、服务、监控、安全每一根线都不能太松。你不需要一开始就把网织得很大但要保证每个节点都是稳的。先把最小闭环跑通再慢慢扩这个过程虽然枯燥但它是唯一能让你在AI这条路上持续走远的办法。