多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

AI工程从零到落地:模型部署、Prompt与Agent全链路实践指南

AI工程从零到落地:模型部署、Prompt与Agent全链路实践指南 一年前我把仓库名定为ai-engineering-from-scratch的时候心里其实没底。做后端出身模型只是调过 API所谓的“AI 工程”在我脑子里只是一个模糊的拼图有训练、有部署、有提示词、有 Agent但不知道它们怎么串成一条能落地的流水线。如今回头看这个“从零开始”的仓库已经积累了十几套可运行的代码覆盖了从数据准备到模型部署、再到多 Agent 协作的全链路。如果你正卡在“模型调用会写但不知道如何上线”、“训练脚本跑通但不知道怎样才算好”、“想做一个 AI Agent 却不知道从哪里下手”这几个阶段这篇内容应该能帮你少走一些弯路。这个项目解决的核心问题是把 AI 建模的零散技能点整理成一条工程实施路径。它适合三类人刚入门想系统建立 AI 工程认知的开发者、准备把个人项目从“能跑”提升到“能部署”的工程师以及想了解 Prompt 和 Agent 背后工程细节的产品或技术负责人。整篇内容会按照我从零开始的实际推进顺序展开尽量说清楚每个环节为什么这样做以及踩坑之后我调整成了什么方案。1. 先搞清楚AI工程到底解决什么问题1.1 算法工程师和AI工程师的边界很多人刚开始会把 AI 工程等同于训练模型一上来就抱着 Transformer 论文和损失函数啃。我的经验是如果目标是“把 AI 能力送上线”那算法只是其中的一段。算法工程师更关注模型结构、训练策略、效果指标优化AI 工程师则需要把模型放到真实系统里让它稳定、可控、可观测地运行。打个比方算法解决的是“模型能不能认出来”工程解决的是“模型能不能在每秒钟几十次请求的压力下还认得准”。于是这个项目里我做了一个刻意安排不追求自己从零写模型结构而是把重心放在“如何选择模型、如何训练、如何部署、如何围绕模型搭一套 Agent 工作流”上。这也解释了为什么仓库名强调“from scratch”——并不是说所有代码从零手写而是以工程师视角从零搭建整套体系。到今天再看这个选择让我在很短的时间内具备了交付完整 AI 项目的能力。1.2 从零开始的路线的核心顺序我把整个学习路线拆成六个递进阶段顺序是基础工具链、数据处理、模型训练与评估、服务化、Agent 工作流、性能优化。这个顺序最大的好处是每个阶段都能产出一个可运行、可检查的成果。比如基础工具链搞定后你能在本地跑通一个加载模型并输出的脚本数据处理阶段产出一个干净的训练集模型训练阶段产出一个可评估的 checkpoint。每一步都有明确“完成”的定义不会出现学了三个月却做不出东西的情况。更要提醒的是不要一上来就追求多先进的技术。AI 工程里最被低估的能力是“把简单方案做到稳定可用”。我见过很多项目死在过度设计上比如刚学会训练就想搞分布式刚接触 Agent 就想上多智能体编排。从零开始的正确姿态是先做窄、再拓宽。2. 搭好基础环境工具链选择是一门取舍课2.1 Python环境与依赖管理的实际选择环境搭建这一步看似简单实际坑最多。刚开始我用的是pip install全装到系统环境结果是不同项目间相互干扰一个项目要升级 PyTorch另一个项目直接跑不起来。后来换成了虚拟环境方案用conda管理大版本用项目内单独的requirements.txt锁定依赖。这里建议用conda create -n ai-eng python3.10建环境同时在项目根目录加一个.python-version文件来标记版本。为什么选 Python 3.10因为主流框架 PyTorch、ONNX Runtime 在 3.10 上的兼容性最好3.11 的部分算子存在缺失3.9 又太老。另一个容易被忽略的是 CUDA 环境。GPU 推理和训练需要匹配的 CUDA 版本我现在的稳妥做法是直接用 PyTorch 预编译的 wheel 包而不单独装全局 CUDA。安装时加--index-url https://download.pytorch.org/whl/cu121它能保证 torch 和 CUDA Runtime 版本一致。这样做的代价是包体积大但换来了零冲突的稳定性。2.2 不是只有GPU才能开始实践很多人没有 GPU 就放弃了。我的实践结论是2G 内存的 CPU 机器也可以完成 AI 工程的大部分入门实践只是模型选择受限。在 CPU 环境下建议从 100M 参数以下的小模型开始比如distilbert-base、MiniLM或用 4bit 量化的 7B 模型做推理。训练阶段如果要跑微调可以选择 CPU 也能承受的小模型或仅训练最后几层。实测下来用 CPU 训练一个文本分类模型几千条样本大概需要几十分钟推理一条结果则在几百毫秒级别完全能接受。我在仓库里专门放了一套环境和算力的自检脚本输出当前可用的设备、内存、推理时延。这个脚本的价值是让每次实验在开始前就知道自己的“预算”不要等训练跑到一半才意识到资源不够。前期把环境问题解决掉后面所有实操都会顺畅很多。3. 第一个从零实现的项目文本分类服务全流程3.1 数据采集与清洗的细节我选的第一个完整项目是“工单自动分类”。场景是客服系统里有一批用户反馈文本需要按售后、技术支持、退款、物流等类别自动标记。这个项目技术门槛适中又覆盖了 AI 工程的大部分步骤。数据这块我用了两种方式组合从公开数据集里抽了 3000 条相近样本又自己标注了 500 条业务真实数据。清洗时踩的最大的坑是——标签不平衡。售后类样本占了 60%物流类只有 5%。如果不处理模型会学会“无脑猜售后”来获取高准确率。处理办法有三个层次第一层对样本量少的类别做加权采样训练时让模型看到更多少数类样本第二层在损失函数里提高少数类的权重第三层评估时使用 F1-score 而不是准确率。最终效果是少数类别的召回率从 30% 提到 68%而整体准确率只降了不到 2 个百分点。这说明没有清洗和重采样模型表现再好也是“假好”。3.2 选模型的一整套评估逻辑选模型不是越强越好而是要匹配任务、数据量、推理预算。这个项目我对比了三个候选模型参数量推理时延CPU验证集F1结论MiniLM22M30ms0.82够用但长文本略弱distilbert-base66M50ms0.86均衡可作为默认选型bert-base-uncased110M80ms0.87效果上限高但成本也高评估逻辑上我做了三件事固定同一批验证集、多次跑取均值、额外检查错误样本。很多新手只盯着指标看忽略了错误样本本身的价值。后来我发现有相当一部分“预测错”是标注本身不统一比如“快递没收到”既可标成“物流”也可标成“售后”。这类边界问题不是模型问题而是定义问题。把标注口径重新调整后F1 又提升了 3 个点。3.3 训练脚本中真正影响结果的参数训练阶段有几个参数是我反复调整后收益最大的。第一是学习率BERT 系列用默认的 2e-5 起步过大的学习率会让预训练权重快速被破坏第二是 max length它看起来只是截断长度实际上决定了输入的信息量工单文本平均 80 个 token我最终设成 128第三是 batch size它影响梯度稳定性和训练时间我用的 GPU 内存不大所以用了 16配合梯度累积达到 64 的等效批大小。还要强调 Early Stopping 的作用。我一开始习惯固定训练 3 个 epoch结果在验证集上 Loss 会先降后升这就是过拟合的开始。后来改成按验证 Loss 是否连续两轮上升来决定停止。不要迷信“训练更多轮效果一定更好”在预训练模型时代微调通常很快收敛超过一定轮数只会记住噪声。训练结束后还要关注一个容易被忽略的产物分类器在置信度低样本上的表现。我导出所有置信度低于 0.5 的预测结果发现这些样本恰好是业务中需要人工复核的部分。这个发现帮我设计了一个路由机制高置信度走自动分类低置信度退回人工。这也是 AI 工程落地的常见打法——不是所有场景都要模型硬扛。3.4 用FastAPI把模型包成服务模型训练完成后我第一时间把它接成了 API 服务。这一步的价值是让模型真正变成一个可被系统调用的模块而不是只存在于 Jupyter Notebook 里。我用的是 FastAPI因为它在性能、类型校验、文档生成上都比 Flask 更适合 AI 服务。核心代码结构分三层模型加载层初始化时加载权重到全局变量避免每个请求都重新加载业务处理层负责文本预处理、后处理把原文转成模型能接受的输入格式API 层定义请求体、响应体和异常处理。在模型加载层有个很小的细节PyTorch 默认开启了梯度计算但推理阶段根本不需要所以加载后要执行model.eval()并包在torch.no_grad()里。否则每次推理都会多出无用的计算显存占用会明显上涨。响应设计上除了返回预测类别我还会返回置信度和各个类别的概率分布。这样做的好处是调用方可以基于概率做决策比如阈值过滤或者人工接管。服务启动后我用locust做了压力测试单实例 CPU 环境下 QPS 大约 25这已经能满足中小业务场景。整个从零封装过程走完我才敢说真正掌握了“部署一个模型”的完整含义。4. 理解Prompt与Agent让模型真正“干工程活”4.1 Prompt engineering不只是写提示词文本分类服务上线后我开始转向更接近“AI 应用”的方向——用大语言模型完成开放任务。这时才知道 Prompt Engineering 的重要性但它并不只是“把问题写清楚”。我记录过一个真实例子。最开始我让模型做“文案润色”提示词是“请润色以下文案。”输出质量很不稳定有时候会添加不必要的内容有时候甚至改变原意。后来我把提示词改成了系统角色加任务约束加输出格式的三段式效果立刻稳定很多你是负责电商详情页的资深文案你的任务是润色产品卖点文案。 规则 1. 不改变原文事实 2. 输出不超过原文字数 1.2 倍 3. 直接输出润色后的内容不要加任何解释。这次调整让我意识到Prompt Engineering 的核心不是“咒语”而是把任务目标、限制条件、输出格式变成可校验的约束。我在项目里为此写了一套模板 schema系统化维护所有业务提示词。这样每次需求变化都只需要改模板不需要改代码。4.2 用Function Calling实现可用的AI Agent有了稳定的 Prompt 后我开始研究 AI Agent。一开始做成纯对话式让模型自己决定下一步干什么。结果发现最大的问题是“幻觉式行动”——模型会声称自己调用了 SQL但其实根本没有执行。后来我采用了 Function Calling 这个更工程化的机制。它的核心逻辑不复杂把可调用的工具定义成一个 JSON Schema比如查询订单状态、创建售后单、获取物流轨迹。模型的任务是先根据用户意图选择工具并生成参数代码侧真正执行工具调用调用结果再反馈回模型由模型生成最终答复。我实作了一套最小可用的 Agent 流程用户输入查询我的订单快到了吗模型输出tool_namequery_order, args{user_id: 123}代码执行工具拿到物流信息拼接工具结果再请求模型组织语言返回最终回答这个过程中最重要的工程改进是“把工具的执行为真实可追踪的调用”。每次调用都写入日志包括输入参数、返回结果、耗时以及模型上下文。这样即使用户觉得回答不对也能快速定位是模型理解错了还是工具数据本身有问题。4.3 多AI协作工作流的工程化设计做单一 Agent 之后我继续尝试了多 Agent 协作。初衷是拆解复杂任务一个 Agent 负责理解用户需求一个 Agent 负责查数据一个 Agent 负责生成最终格式化内容。但这种“多 Agent”听起来高级实际工程复杂度成倍上升最关键的问题是上下文管理和“接力棒”设计。我采用的是一种相对可控的管线式模式而不是全部交给模型自己协调。整条流水线中每个节点都有独立的 Prompt 模板和输出校验器需求理解节点输出结构化意图工具调用节点根据意图参数调用 API结果生成节点把工具结果渲染为最终回复每一步的输出由上一环节的代码校验不合法就直接报错而不是传给下游模型继续“猜”。分布式 Agent 调研固然有意义但工程落地初期我强烈建议先用这种“流水线校验器”的模式。等到所有节点都稳定再考虑让模型动态编排否则你连“到底哪个环节出错”都难定位。这套工作流还有一个副产品所有节点的输入输出我都做了版本化保存。这让我可以重放任意一条历史请求用来优化特定节点的 Prompt。实测下来这种“数据驱动调 Prompt”的效果远比凭感觉反复试要显著。5. 模型部署与线上性能优化5.1 推理优化量化和批处理模型服务上线后第一件事就是面临性能压力。我本来的基线模型是 110M 参数的 Bert单次推理 80ms并发一高就排队。我做了两轮优化。第一轮是量化。把 PyTorch 模型转成 ONNX并启用动态量化模型大小从 400MB 降低到 110MB推理时延从 80ms 降到 32ms。量化损失很小F1 从 0.87 降到 0.85对于分类场景完全可接受。这里要强调量化不是所有场景都适用如果你的模型对数值精度非常敏感比如细粒度 NER需要先量化后充分评估再做决定。第二轮是批处理。模型推理和批大小有关单条请求走一次前向计算多条请求合并成一个 Batch 走一次前向计算吞吐量能成倍提高。我实现了一个简单的“动态攒批”模块请求先进入队列攒够 8 条或超过 50ms 窗口就触发一次推理。这个机制表面上增加了代码复杂度但同样的硬件条件下QPS 从 25 提高到 100 以上。5.2 不同部署方案的取舍部署方案我走过了三条路裸脚本、Docker 容器化、Kubernetes 编排。最开始裸脚本跑服务优点是简单缺点是环境迁移基本靠重装。后来把模型服务容器化写了一个 Dockerfile把模型权重打进镜像本地跑通后直接扔到一台云服务器上。容器化之后有两个收益环境一致性和水平扩展。一个镜像既可以在开发机跑又可以在生产机跑不再出现“我本地是好的”的情况。到了需要多副本和服务发现阶段可以用 Docker Compose 或者 Kubernetes。我用 Kubernetes 主要是为了按流量扩缩容。这里强烈建议关注 Pod 启动时长模型在容器启动后需要加载权重如果探测时间设置太短会把容器误判为不健康。我踩过这个坑后来把 Readiness Probe 改为“加载完权重后返回一个就绪标记”并在镜像里内置了一个/healthz接口专门汇报状态。对于大多数个人项目和中小团队我建议先别上复杂的编排。Docker Compose 单机部署 反向代理已经能覆盖 90% 的场景。等到请求量真正冲上来再用 Kubernetes避免为了“架构先进”而把交付周期拉长。6. 常见报错与排查记录6.1 训练阶段容易踩的坑这一节我整理几个真实遇到、且反复出现的报错和解决思路。第一个是 OOM显存溢出。刚开始我以为是模型太大后来加了监控发现是 DataLoader 加载了太多数据到显存。解决方式是缩小 batch size同时用pin_memoryTrue加快数据拷贝还把验证集的分批也做了同样处理。另外我给自己定了一个原则任何长训练任务启动前先跑一个 20 步的小批量实验确认显存水位正常再开完整训练。这个习惯省下的调试时间不计其数。第二个是训练 Loss 变成nan。排查顺序是检查学习率是否过高检查数据是否存在 INF检查是否有脏字符导致 tokenizer 输出异常。我的错误根因最后是两个样本的标签越界分类数只有 6但标注文件里出现了 7。这种低级错误如果没有加“数据校验”步骤会让人白调很久。第三是模型预测全部输出同一个类别。这往往是标签不平衡 损失函数没有处理导致的。我在前面章节已经提过重采样和类别权重这里是它在排查场景中的具体体现。6.2 API服务和模型加载阶段的坑服务上线后的报错类型完全不同。一个是模型加载慢。我一开始在请求处理函数里加载模型结果第一个请求要等几十秒非常影响体验。后来把所有模型初始化移到启动事件里相当于开服务时先加载好请求进来时直接用。如果是多进程部署还要注意每个 worker 进程都会各自加载一份模型内存 模型大小 * worker 数。控制 worker 数量或者使用 CPU 共享内存比无脑配置要好很多。第二是并发请求下结果错乱。这个坑很隐蔽我遇到过同一个进程里多个请求的预测结果串了。原因是 tokenizer 在处理文本时使用了一个共享状态对象在多线程并发下被污染。解决方法是确保每个请求创建独立的预处理实例或者给模型推理过程加上线程锁。FastAPI 默认是异步处理但模型前向计算是阻塞的需要自己管理线程池当时我用run_in_executor把推理放到线程池里执行同时加了一把锁保护 tokenizer才彻底解决。第三是服务上线的可观测性。如果没有日志和指标线上模型出了问题很难定位。我给服务加了三个最基本的观测点请求耗时、预测类别分布、以及置信度分布。置信度异常下降往往预示着输入数据分布变化这是模型需要重新训练的信号。7. 给从零开始的人几点建议7.1 项目驱动学习回头看这套从零开始的经验最重要的一条是不要按教程顺序学而要按项目需求学。我做文本分类前不知道什么是 ONNX Runtime但部署遇到性能瓶颈时自然就学了我做 Agent 前不知道 Function Calling 是什么但想让模型真正执行工具时就会主动查阅它的原理和落地方式。项目驱动学的核心是“带着问题找答案”而不是“先把知识都看完再动手”。问题天然地筛选了优先级能让你把有限的时间花在最关键的技术点上。我在仓库里也刻意保留了每个阶段启动时的 TODO 和报错记录而不是只放成功代码。因为对后来者来说看到一段从错误到正确的演化路径往往比看到一行完美代码更有价值。7.2 后续可以扩展的方向这个仓库已经走完了“从零到部署”的闭环接下来我会继续扩展几个方向一是把 Agent 工作流升级成可配置的图形化编排。现在节点还是代码写死下一步希望做到前端拖拽生成流程定义后端动态加载对应模块。二是增加评估驱动机制每改一次 Prompt 或模型都自动跑一遍标注样本集用指标防止回归。三是引入更完整的观测体系把 token 数、工具调用成功率、上下文长度都纳入监控。如果你也在做自己的ai-engineering-from-scratch我的建议很简单从一个小需求开始完整走一遍数据、训练、部署、迭代的闭环再根据反馈逐步拓展。工程能力是“趟过坑”趟出来的不是看文档看出来的。项目跑通是起点能在真实流量中稳定运行才是这条路的终点。
返回列表