ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

AI工程化从零搭建:从能跑到可靠的工程实践指南

AI工程化从零搭建:从能跑到可靠的工程实践指南 1. 从零搭建AI工程能力这个项目到底在解决什么问题第一次看到 ai-engineering-from-scratch 这个标题的时候我脑子里蹦出来的第一个念头是又是一个教人调包的教程仓库但翻了一圈之后发现它想做的事情比跑通一个demo要硬核得多——它试图回答一个很具体的问题当一个AI功能从实验室里的notebook走向真实产品时中间那段没人愿意讲的脏活累活到底该怎么干。说白了市面上讲模型原理的资料多如牛毛讲Transformer架构、讲注意力机制、讲微调技巧的内容一抓一大把。但真正到了工程落地环节你会发现卡住你的从来不是这个模型原理是什么而是这个推理服务怎么扛住并发、向量检索的延迟怎么从800ms压到80ms、提示词版本怎么管理、评测集怎么保证不污染、上线之后效果漂移了怎么发现。这些东西学术论文不写框架文档不讲只能靠踩坑踩出来。ai-engineering-from-scratch这个项目的价值就在于它把AI工程当成一门独立的工程学科来对待而不是机器学习的一个附属品。它假设你已经有基本的编程能力但对怎么把AI能力做成一个可靠系统这件事还没有系统认知。适合的人群很明确从后端转过来做AI应用的工程师、带团队做AI产品的技术负责人、以及那些已经能跑通demo但一到生产环境就抓瞎的开发者。我自己的背景是后端服务开发两年前开始接触AI应用落地踩过的坑能写一本书。所以看这个项目的时候很多地方会有共鸣——它讲的不是理论上应该怎样而是实际工程中你必须怎样。接下来我会把这个项目的核心思路拆开结合我自己的实操经验把每个环节的关键点讲透。2. 核心思路拆解为什么AI工程需要一套独立的方法论2.1 传统软件工程和AI工程的根本差异很多人刚转过来做AI应用的时候习惯性地用传统后端开发的思维去套结果处处碰壁。我自己就经历过这个阶段。传统软件工程的核心假设是输入确定逻辑确定输出确定。你写一个订单服务给定订单ID查数据库返回订单详情这条链路是确定的、可测试的、可复现的。AI工程打破了这个假设。同样的输入模型可能给出不同的输出同样的提示词换个模型版本效果可能天差地别同样的评测集今天跑出来85分明天可能就变成78分而你什么都没改。这种不确定性是AI工程的本质特征也是它需要独立方法论的根本原因。ai-engineering-from-scratch这个项目在开篇就强调了这一点你不能用单元测试的思路去测AI系统因为AI系统的正确性是一个概率分布不是一个布尔值。这听起来像废话但真正理解并接受这一点需要你在工程实践上做出一系列根本性的调整。举个例子。传统后端服务上线你关心的是QPS、P99延迟、错误率。AI服务上线这些指标当然也要关心但更关键的是输出质量有没有退化。而输出质量这个东西没有一个简单的数值指标能完全刻画。你需要建评测集、定评测标准、做人工抽检、监控线上badcase。这一整套东西传统软件工程里是没有的。2.2 分层解耦把AI系统拆成可独立演进的模块这个项目提出的一个核心架构思路是分层解耦。它把AI系统拆成几个相对独立的层层级职责变化频率关键考量模型层提供基础推理能力低季度级选型、成本、延迟提示词层控制模型行为高天级版本管理、A/B测试编排层串联多个模型调用中周级错误处理、重试、降级应用层面向用户的业务逻辑中周级用户体验、业务规则评测层衡量系统效果中周级评测集质量、指标设计为什么要这样分因为每一层的变化频率和变化原因完全不同。模型层可能半年才换一次但提示词层可能每天都在调。如果你把提示词硬编码在业务逻辑里每次调提示词都要改代码、走发布流程效率极低。反过来如果你把业务逻辑和模型调用混在一起换一个模型就要重写整个应用那更是灾难。我自己的做法是提示词单独存成模板文件用版本号管理支持热加载。编排层用配置文件定义流程而不是硬编码在代码里。这样调提示词不需要发版换模型只需要改配置。这个思路和ai-engineering-from-scratch里讲的分层原则是一致的。2.3 从能跑到可靠工程化的三个台阶这个项目把AI工程化分成三个台阶我觉得总结得很到位第一个台阶是能跑通。你写了个脚本调了API拿到了结果看起来还行。这个阶段大部分人都能到网上教程也最多。第二个台阶是能稳定跑。你的服务能扛住并发出错能重试超时能降级日志能追溯成本可控。这个阶段就筛掉一大半人了因为需要真正的工程能力。第三个台阶是能持续变好。你有评测体系能发现效果退化能快速迭代提示词和模型能基于数据做决策而不是拍脑袋。这个阶段是区分做了个AI功能和做了个AI产品的分水岭。ai-engineering-from-scratch这个项目的野心就是带你走完这三个台阶。它不是只教你调API而是教你建一套可持续演进的AI工程体系。3. 核心模块实操从环境搭建到第一个可用服务3.1 环境准备与依赖管理这个项目对环境的要求不算高但有几个坑我提前说一下。Python版本建议3.10以上因为很多AI相关的库已经不再支持3.8了。依赖管理我强烈建议用uv或者poetry不要用裸pip。原因很简单AI项目的依赖树非常深torch、transformers、fastapi、pydantic这些库之间的版本兼容性很微妙裸pip很容易搞出依赖冲突。# 用uv创建虚拟环境并安装依赖 uv venv .venv --python 3.11 source .venv/bin/activate uv pip install -r requirements.txt注意如果你要用GPU推理CUDA版本和PyTorch版本的对应关系一定要查官方矩阵不要凭感觉装。我见过太多人在这上面浪费一整天。环境变量管理也是个大坑。API Key、模型端点、数据库连接串这些东西绝对不要硬编码在代码里。用.env文件加python-dotenv或者用pydantic-settings做类型安全的配置管理。这个项目推荐的是后者我实测下来确实更稳因为配置项有类型校验写错了启动就报错不会等到运行时才发现。3.2 第一个推理服务的搭建项目里第一个实操环节是搭一个最简的推理服务。别小看这个环节里面有很多工程决策点。首先是同步还是异步。如果你用FastAPI默认是异步的。但很多模型推理库是同步阻塞的。如果你在异步函数里直接调同步推理会把事件循环堵死并发能力直接归零。解决方案有两种一是用run_in_executor把同步调用扔到线程池二是用支持异步的推理框架。我一般选前者因为改动最小兼容性最好。import asyncio from concurrent.futures import ThreadPoolExecutor from fastapi import FastAPI app FastAPI() executor ThreadPoolExecutor(max_workers4) def sync_inference(prompt: str) - str: # 这里是同步的模型调用 return model.generate(prompt) app.post(/generate) async def generate(prompt: str): loop asyncio.get_event_loop() result await loop.run_in_executor(executor, sync_inference, prompt) return {result: result}线程池的大小怎么定不是越大越好。如果你的推理是GPU密集型的线程数超过GPU并发能力反而会增加排队延迟。我的经验是线程数 GPU数量 × 每GPU并发数对于大多数7B-13B模型每GPU并发数设2-4比较合适。这个值需要压测来确定不能拍脑袋。其次是超时和重试。模型推理有时候会卡住尤其是显存不够触发swap的时候。你必须设超时否则请求会一直挂着把连接池占满。超时时间设多少看你的P99延迟一般设P99的2-3倍。重试策略要小心因为推理不是幂等的——同样的输入可能产生不同输出重试可能导致用户看到的结果不一致。我的做法是只对明确的错误码重试超时不重试直接返回降级结果。3.3 提示词管理与版本控制这是ai-engineering-from-scratch里我最有共鸣的一个模块。提示词管理看起来简单实际上是个大坑。我早期做项目的时候提示词直接写在代码里用f-string拼接。结果就是调提示词要改代码、要发版、要回归测试效率极低。更糟糕的是线上出问题了想回滚提示词得回滚整个服务影响面太大。这个项目推荐的方案是提示词模板化 版本管理。具体做法提示词存在独立的模板文件里用Jinja2或类似引擎渲染每个提示词有版本号支持灰度发布提示词变更记录在案能追溯哪个版本效果最好支持A/B测试线上分流对比from jinja2 import Template import hashlib PROMPT_TEMPLATES { summarize_v1: 请总结以下内容{{ content }}, summarize_v2: 你是一个专业编辑。请用3句话总结以下内容保留关键数据{{ content }}, } def get_prompt(version: str, content: str) - str: template Template(PROMPT_TEMPLATES[version]) return template.render(contentcontent) def route_version(user_id: str) - str: # 基于用户ID做稳定分流同一用户始终走同一版本 hash_val int(hashlib.md5(user_id.encode()).hexdigest(), 16) return summarize_v2 if hash_val % 100 20 else summarize_v1提示分流一定要用稳定哈希不要用随机数。否则同一个用户刷新页面看到不同版本体验会很割裂数据也没法分析。3.4 评测体系的搭建没有评测体系的AI工程就是盲人摸象。这个项目花了很多篇幅讲评测我觉得这是它最有价值的部分之一。评测的核心是评测集。评测集怎么建我的经验是分三层第一层是黄金集。50-100条人工精心标注的样本覆盖核心场景和边界情况。这一层是底线每次变更都必须跑分数掉了就不能上线。第二层是回归集。从线上badcase里积累的样本每次发现新问题就加进去。这一层是防止修了一个bug引入两个新bug。第三层是随机抽样集。从线上真实流量里随机抽用来发现你不知道自己不知道的问题。这一层是防止评测集过拟合。评测指标怎么定不要只看准确率。对于生成类任务我一般用这几个维度指标衡量什么怎么算相关性回答是否切题人工打分或模型打分完整性是否覆盖关键点关键点召回率忠实度是否有幻觉与参考资料的一致性格式合规是否符合输出格式要求规则校验延迟响应速度P50/P99模型打分LLM-as-Judge是个好工具但不要全信。我的做法是模型打分做初筛人工抽检做校准。如果模型打分和人工打分的一致性低于80%说明打分提示词需要调。4. 生产环境的关键工程问题与解决方案4.1 并发与延迟优化AI服务的延迟优化和传统后端完全不是一个思路。传统后端优化延迟你想到的是加缓存、加索引、减少网络跳数。AI服务优化延迟你首先要搞清楚延迟花在哪里了。一次典型的LLM调用延迟构成大概是这样的网络传输5-10%排队等待0-50%取决于并发量预填充prefill10-30%解码decode40-80%解码阶段是大头因为它是逐token生成的。优化解码延迟的手段有几个流式输出。这是最有效的用户体验优化。虽然总延迟没变但用户看到第一个token的时间TTFT大幅缩短感知延迟降低很多。实现上用SSE或WebSocket把生成的token实时推给前端。批处理。把多个请求攒在一起推理能显著提高GPU利用率。但批处理会增加排队延迟需要权衡。我的经验是设置一个很小的批处理窗口比如10-20ms既能提高吞吐又不会明显增加延迟。KV Cache复用。如果多个请求共享相同的前缀比如相同的系统提示词可以复用KV Cache省掉重复的预填充计算。这个优化在多轮对话场景下效果特别明显。# 流式输出的FastAPI实现示例 from fastapi.responses import StreamingResponse app.post(/generate_stream) async def generate_stream(prompt: str): async def token_generator(): async for token in model.stream_generate(prompt): yield fdata: {token}\n\n yield data: [DONE]\n\n return StreamingResponse( token_generator(), media_typetext/event-stream )4.2 成本控制AI服务的成本是个绕不开的话题。尤其是用商业API的时候token消耗直接对应真金白银。我见过一个团队上线第一个月账单超预算10倍就是因为没做成本控制。成本控制的手段按性价比排序第一缓存。相同或相似的请求直接返回缓存结果。语义缓存semantic cache比精确匹配缓存更有效用向量相似度判断两个请求是否等价。我实测下来在客服场景下语义缓存能省30-50%的token。第二模型分级。不是所有请求都需要用最大的模型。简单问题用小模型复杂问题用大模型。可以先用小模型试置信度不够再升级到大模型。第三提示词压缩。系统提示词里的冗余信息、重复示例能删就删。一个1000token的系统提示词如果每个请求都带一天10万请求就是1亿token。压缩到500token直接省一半。第四输出长度控制。设置max_tokens防止模型话痨。同时优化提示词让模型输出更简洁。注意成本优化不要牺牲效果。我的原则是先保证效果达标再在达标的前提下优化成本。反过来做很容易把产品做废。4.3 监控与可观测性AI服务的监控比传统服务复杂得多。传统服务看CPU、内存、QPS、错误率就够了。AI服务还要看Token消耗按模型、按接口、按用户维度统计延迟分布TTFT、总延迟、每token延迟输出质量badcase率、人工抽检评分缓存命中率精确缓存和语义缓存分别统计降级触发率多少次请求走了降级逻辑日志记录也有讲究。AI服务的日志要记录完整的输入输出但要注意脱敏和存储成本。我的做法是全量记录元数据token数、延迟、模型版本采样记录完整输入输出比如10%采样 100%记录badcase。import time import logging logger logging.getLogger(ai_service) def log_inference(request_id, prompt, response, model_version, start_time): latency time.time() - start_time logger.info({ request_id: request_id, model_version: model_version, prompt_tokens: count_tokens(prompt), completion_tokens: count_tokens(response), latency_ms: latency * 1000, prompt_hash: hash_prompt(prompt), # 用于去重分析 })4.4 错误处理与降级策略AI服务的错误处理有个特殊之处很多错误不是异常而是质量不达标。模型返回了结果但结果是胡言乱语这在代码层面不是错误但在业务层面是严重问题。所以错误处理要分两层技术层错误超时、连接失败、限流、显存不足。这些用常规的重试、降级、熔断处理。质量层错误幻觉、答非所问、格式错误、有害内容。这些需要输出校验 兜底策略。输出校验怎么做简单规则校验比如JSON格式、长度限制用代码做。复杂质量校验用另一个模型做LLM-as-Judge。兜底策略可以是返回预设的安全回复、转人工、或者用更保守的提示词重试一次。我的经验是降级策略一定要提前设计不要等出事了才想。而且降级逻辑要定期演练确保真的能用。我见过太多团队写了降级代码但从来没测过真到用的时候发现降级逻辑本身有bug。5. 常见问题排查与避坑经验实录5.1 效果不稳定问题排查问题现象同样的输入有时候回答很好有时候一塌糊涂。排查思路首先确认是不是模型本身的问题。用temperature0跑多次如果结果仍然不稳定说明是模型能力问题如果结果稳定了说明是采样参数问题。然后检查提示词。提示词里如果有模糊表述、矛盾指令、或者示例质量参差不齐都会导致输出不稳定。我的做法是提示词里的每个指令都要能通过一个新人看了能不能准确执行的测试。最后检查输入。输入里的噪声、格式不一致、长度差异过大都会影响输出稳定性。预处理环节要做好清洗和标准化。现象可能原因解决方案输出时好时坏temperature过高降低temperature关键任务设0输出格式不对提示词格式约束不明确加few-shot示例明确格式要求答非所问提示词指令冲突精简提示词消除矛盾幻觉严重缺乏参考资料约束加RAG要求仅基于给定资料回答长输入效果差超出有效上下文分段处理或换长上下文模型5.2 性能瓶颈定位问题现象服务响应越来越慢但CPU和GPU利用率都不高。这种资源没跑满但就是慢的情况八成是排队问题。可能是线程池太小、连接池太小、或者批处理窗口太大。排查方法打点记录请求在各个环节的耗时——排队时间、预处理时间、推理时间、后处理时间。哪个环节占比高就优化哪个。我遇到过一个典型案例服务延迟高但GPU利用率只有30%。查下来发现是Python的GIL限制了并发推理调用虽然释放了GIL但前后处理没释放。解决方案是把前后处理也放到线程池里或者用多进程架构。提示Python做AI服务GIL是个绕不开的坎。如果并发要求高考虑用多进程 负载均衡或者用C/Rust写推理服务Python只做编排。5.3 评测集污染问题问题现象评测分数很高但线上效果很差。这是典型的评测集污染。可能的原因评测集样本和训练数据重叠评测集太简单不覆盖真实场景的复杂度评测集被针对性地优化过比如反复调提示词直到评测集分数高解决方案评测集要锁起来。黄金集一旦确定就不允许随意修改。每次调提示词只能用回归集和抽样集黄金集只在最终上线前跑一次。如果黄金集分数掉了说明改动有问题回滚。另外评测集要定期更新。线上场景在变评测集也要跟着变。我的做法是每个季度review一次评测集把过时的样本替换掉把新的badcase加进去。5.4 模型版本升级的坑问题现象模型升级后原来好好的提示词突然不好用了。这是很常见的问题。不同模型对提示词的敏感度不同同一个提示词在A模型上效果好在B模型上可能完全不行。模型升级不是简单的换个端点而是一次完整的回归测试。我的升级流程是在新模型上跑黄金集确认基础能力达标在新模型上跑回归集确认没有引入新问题针对新模型重新调优提示词不要指望旧提示词直接能用灰度发布小流量对比新旧模型效果全量发布持续监控整个过程快则一周慢则一个月。不要想着今天升级明天上线那是给自己挖坑。6. 从项目到产品我踩过的那些坑6.1 过度依赖单一模型我早期做项目的时候所有功能都绑死在一个模型上。结果那个模型涨价了成本直接翻倍后来那个模型又限流了服务直接不可用。教训就是永远要有备选方案。现在我的做法是抽象一层模型接口底层可以切换不同的模型提供商。提示词针对每个模型做适配但业务逻辑不变。这样即使某个模型出问题切换过去只需要改配置不需要改代码。6.2 忽视数据飞轮AI产品最大的优势是数据飞轮用户用得越多数据越多效果越好。但很多团队不做数据收集白白浪费了这个优势。我的做法是从第一天就设计数据收集机制。用户的每次交互、每次反馈、每次badcase都要记录下来。这些数据是后续优化的燃料。当然数据收集要合规要脱敏要给用户知情权。6.3 提示词工程不是玄学很多人把提示词工程当成玄学觉得全靠感觉。其实提示词工程是有方法论的。ai-engineering-from-scratch里总结的几条原则我觉得很实用明确角色告诉模型它是谁比让它随便答效果好得多给出示例few-shot比zero-shot稳定尤其是格式要求高的任务分步思考复杂任务让模型一步步来比直接要答案准确率高约束输出明确格式、长度、风格要求减少后处理成本迭代优化提示词不是一次写好的要基于badcase持续迭代我自己的经验是提示词优化要基于数据不要基于感觉。每次改提示词都要有明确的假设我觉得这样改能解决XX问题然后用评测集验证。如果分数没提升就回滚。这样积累下来你对什么提示词有效的判断会越来越准。6.4 团队协作的坑AI工程项目和传统软件项目在团队协作上有个很大的不同提示词工程师和软件工程师的工作方式不一样。提示词工程师需要快速迭代、频繁实验软件工程师需要稳定、可测试、可复现。这两种节奏放在一个项目里很容易打架。我的解决方案是把提示词开发和代码开发解耦。提示词存在独立的仓库或目录有独立的版本管理和发布流程。提示词工程师可以快速迭代不需要走代码发布流程。软件工程师只需要保证提示词加载和渲染的逻辑稳定。这样两边都能按自己的节奏工作互不干扰。7. 一些实用的工具和资源推荐7.1 开发阶段uvPython依赖管理比pip快很多依赖解析也更靠谱pydantic-settings配置管理类型安全支持环境变量和.env文件FastAPIAPI框架异步支持好自动生成文档Jinja2提示词模板渲染功能足够学习成本低7.2 评测阶段promptfoo提示词评测工具支持多种指标和对比ragasRAG系统评测覆盖忠实度、相关性等维度自定义评测脚本复杂场景还是得自己写但建议基于pytest组织方便集成到CI7.3 生产阶段Prometheus Grafana监控和告警AI服务的指标也能接进去OpenTelemetry分布式追踪定位跨服务延迟问题Redis缓存精确缓存和语义缓存都能用7.4 学习资源ai-engineering-from-scratch这个项目本身就是一个很好的学习资源它的价值在于系统性强不是零散的知识点而是一条完整的工程化路径。我建议的学习方式是不要只看要跟着做。每个模块都自己动手实现一遍遇到问题再回去看项目里怎么讲的。这样学下来比看十篇博客都管用。另外我强烈建议养成记录踩坑日志的习惯。每次遇到问题、解决问题都记下来。这些记录是你最宝贵的财富比任何教程都值钱。因为教程讲的是通用情况而你踩的坑是你自己的项目特有的。最后分享一个我自己的小技巧每次上线新功能前先问自己三个问题——效果怎么衡量出问题了怎么发现发现后怎么回滚这三个问题答不上来就不要上线。这个习惯帮我避免了很多次事故。
返回列表