拓冰建站拓冰建站
首页 / 资讯中心 / 正文

从零构建AI工程能力:环境、数据、模型封装与推理服务全链路

从零搭建AI工程能力这件事我前前后后折腾过好几轮。最早的时候我也走过弯路——上来就装框架、跑Demo、调API结果模型是跑起来了但整个系统脆得像纸糊的换个数据集就崩加个并发就挂想排查问题连日志都找不到在哪。后来我才慢慢想明白一个道理AI工程和AI调包是两码事。调包是让模型跑起来工程是让模型在真实环境里稳定、可维护、可迭代地跑下去。这个从零构建AI工程能力的思路就是我想在这篇里完整拆一遍的东西。不管你是刚转行想做AI应用的开发者还是已经会调模型但总觉得系统不够扎实的工程师又或者是带团队的技术负责人想理清AI项目的工程化路径这篇内容应该都能给你一些可以直接上手参考的东西。我会从最底层的环境管理开始一路讲到数据管道、模型封装、服务化、监控和迭代每个环节都会说清楚为什么这么做以及我踩过哪些坑。1. 为什么从零才是AI工程最该走的路1.1 调包侠的天花板在哪里我见过太多人学AI的路径是这样的找一篇教程pip install几个库复制一段代码跑通一个分类或者生成任务然后就觉得自己会AI了。这个阶段我称之为Demo级能力。Demo级能力的问题不在于它没用而在于它的天花板极低。你想想一个典型的Demo代码长什么样数据是写死的或者用内置数据集模型是直接加载预训练权重推理是单条循环没有异常处理没有日志没有配置管理。这段代码在你的笔记本上跑得好好的一旦要部署到服务器上给其他人用问题就全来了。数据格式变了怎么办模型加载失败怎么重试并发请求怎么处理推理延迟突然飙升怎么定位这些问题在Demo代码里根本没有答案。更关键的是Demo级能力不可迁移。你这次用某个框架跑通了一个任务换个框架、换个任务类型你又得从头找教程。因为你学到的是这个API怎么调而不是这类问题怎么拆解。这就是为什么我坚持认为AI工程能力必须从零构建——不是从零写算法而是从零理解一个AI系统到底由哪些部分组成每部分解决什么问题它们之间怎么协作。1.2 AI工程和传统软件工程的真正差异有人会说AI工程不就是软件工程加个模型吗这话对了一半。AI系统确实遵循软件工程的基本规律但它有几个传统软件没有的特性这些特性直接决定了工程方案的设计。第一个差异是不确定性。传统软件的输入输出是确定的你给一个函数传参数它返回什么你是知道的。但AI模型不一样同样的输入模型可能给出不同的输出尤其是生成式模型而且输出的质量是概率性的。这意味着你的测试策略、监控指标、容错机制都要围绕不确定性来设计。第二个差异是数据依赖。传统软件的逻辑写在代码里AI系统的逻辑很大程度上写在数据和权重里。数据分布变了模型表现就会变而且这种变化往往是悄无声息的。所以AI工程必须把数据管理提到和代码管理同等重要的位置。第三个差异是资源敏感性。模型推理对计算资源的需求远高于普通业务逻辑显存、内存、GPU利用率这些指标直接决定了系统的成本和稳定性。你不能像写CRUD一样写推理服务必须考虑批处理、缓存、降级这些策略。理解了这三个差异你就能明白为什么AI工程需要一套独立的工程方法论而不是简单套用后端开发的那一套。1.3 从零构建的能力地图我把AI工程能力拆成这么几层从下往上依次是层级能力项解决的核心问题基础层环境与依赖管理可复现的运行环境数据层数据管道与版本管理数据可追溯、可回滚模型层模型封装与配置管理模型可替换、可调参服务层推理服务与接口设计稳定对外提供服务运维层监控、日志、告警问题可发现、可定位迭代层实验管理与持续优化系统可持续进化这六层不是孤立的每一层都依赖下面一层。很多人跳过基础层直接搞服务层结果就是环境不一致导致的各种玄学问题。我的建议是老老实实从底层往上搭每一层都跑通了再往上走。接下来我就按这个顺序把每一层的实操细节拆开讲。2. 环境与依赖一切玄学问题的根源2.1 为什么虚拟环境不是可选项而是必选项我先说一个真实经历。早期我做项目的时候图省事所有包都装在系统Python里。结果有一次同时维护两个项目一个需要某个库的1.x版本另一个需要2.x版本两个版本API不兼容。我装了这个那个就崩装了那个这个就崩最后花了整整一个下午在卸载重装。从那以后我就学乖了每个项目必须有独立的虚拟环境这是铁律。虚拟环境的本质是隔离依赖。Python的包管理机制是全局的在没有虚拟环境的情况下不同项目的依赖会互相污染。虚拟环境通过创建一个独立的目录把Python解释器和第三方包都放进去让每个项目有自己的小世界。具体操作上我推荐用venvPython内置或者conda如果你需要管理非Python依赖比如CUDA。venv的好处是轻量、标准conda的好处是能管理二进制依赖。我的选择标准是纯Python项目用venv涉及GPU和科学计算用conda。# 用venv创建虚拟环境 python -m venv .venv source .venv/bin/activate # Linux/Mac # .venv\Scripts\activate # Windows # 用conda创建虚拟环境 conda create -n myproject python3.10 conda activate myproject注意虚拟环境的目录比如.venv一定要加到.gitignore里绝对不要提交到代码仓库。我见过有人把整个虚拟环境提交上去仓库瞬间膨胀几百兆。2.2 依赖锁定让环境可复现的关键一步光有虚拟环境还不够你还得保证别人或者未来的你能复现出一模一样的环境。这就涉及到依赖锁定。很多人习惯用pip freeze requirements.txt来导出依赖但这个方法有个大问题它会把所有间接依赖的精确版本都写进去导致requirements.txt又长又难维护而且不同平台Linux/Mac/Windows的依赖可能不一样锁死了反而装不上。我的做法是分层管理依赖# requirements.in - 只写直接依赖不锁版本 torch2.0 transformers4.30 fastapi uvicorn pydantic # 用pip-tools编译出锁定版本 pip-compile requirements.in -o requirements.txtrequirements.in里只写你直接用的包pip-compile会自动解析出所有间接依赖并锁定版本生成requirements.txt。这样既保证了可复现性又保持了可维护性。如果某个间接依赖出了问题你还能在编译时看到它是被谁引入的。对于更复杂的项目我建议用pyproject.toml配合poetry或者pdm它们能更好地处理依赖分组开发依赖、测试依赖、生产依赖分开和版本冲突。2.3 环境一致性的验证方法环境搭好了怎么确认它真的可复现我的做法是写一个环境自检脚本在项目启动时跑一遍检查关键依赖的版本、CUDA是否可用、必要的环境变量是否设置。# env_check.py import sys import importlib REQUIRED { torch: 2.0.0, transformers: 4.30.0, fastapi: 0.100.0, } def check(): issues [] for pkg, min_ver in REQUIRED.items(): try: mod importlib.import_module(pkg) ver getattr(mod, __version__, unknown) print(f[OK] {pkg} {ver}) except ImportError: issues.append(f[MISSING] {pkg}) if issues: print(\n.join(issues)) sys.exit(1) if __name__ __main__: check()这个脚本看起来简单但它能帮你省掉大量为什么在我机器上能跑的扯皮。每次部署新环境先跑一遍自检有问题早发现。3. 数据管道AI系统里最容易被低估的部分3.1 数据管道的三个核心职责很多人做AI项目数据处理就是写个脚本读文件、清洗、存下来然后就完事了。但真正的数据管道要承担三个职责可追溯、可复现、可扩展。可追溯是指你能知道每一份数据从哪来、经过了哪些处理、什么时候产生的。可复现是指给定同样的原始数据和处理逻辑你能得到同样的处理结果。可扩展是指数据量增长或者处理逻辑变化时管道能平滑地适应。我见过太多项目数据处理逻辑散落在各种脚本里今天改一点明天改一点最后没人说得清数据到底是怎么处理的。这种项目一旦出问题排查成本极高。3.2 用分层结构组织数据处理逻辑我的做法是把数据处理分成三层原始层、清洗层、特征层。原始层存放从各种来源采集的原始数据不做任何修改只做归档。这一层的原则是只增不改任何处理都在上层进行。清洗层对原始数据做标准化处理比如去重、格式统一、异常值处理。特征层根据具体任务生成模型需要的输入格式。data/ ├── raw/ # 原始数据只读 │ ├── 2024-01-01/ │ └── 2024-01-02/ ├── cleaned/ # 清洗后数据 │ └── latest/ └── features/ # 特征数据 └── v1/每一层的数据都带元信息记录来源、处理时间、处理脚本的版本。这样任何时候你都能回溯到任意一个中间状态。3.3 数据版本管理别再用文件名区分版本了我早期管理数据版本的方式特别原始就是data_v1.csv、data_v2.csv、data_final.csv、data_final_真的final.csv。这种方式在数据量小的时候还能凑合一旦数据多了就彻底失控——你根本不知道哪个版本对应哪次实验删又不敢删留着又占地方。后来我改用内容哈希 元数据的方式来管理。每次数据处理产出一个新版本计算数据的哈希值作为版本标识同时记录元数据处理脚本版本、参数、时间、上游数据版本。import hashlib import json from datetime import datetime def version_dataset(data_path, meta): with open(data_path, rb) as f: content_hash hashlib.sha256(f.read()).hexdigest()[:12] meta.update({ hash: content_hash, created_at: datetime.now().isoformat(), }) version_dir fdata/versions/{content_hash} # 保存数据和元数据 return content_hash这样做的好处是相同内容的数据只会存一份哈希相同不同版本之间有明确的血缘关系。配合实验管理工具后面会讲你能精确知道每次实验用的是哪个版本的数据。3.4 数据质量检查的实操清单数据管道里最容易被忽略但又最重要的是数据质量检查。我的经验是在管道的每个关键节点都插入检查点宁可多检查也不要让脏数据流到下游。我常用的检查项包括完整性必填字段是否有缺失缺失率是否超过阈值一致性字段类型是否符合预期枚举值是否在允许范围内分布数值字段的均值、方差、分位数是否在合理范围重复是否有意外的重复记录时效数据的时间戳是否在预期范围内def quality_check(df, rules): report {} for col, rule in rules.items(): if rule[type] not_null: null_rate df[col].isnull().mean() report[col] {null_rate: null_rate, pass: null_rate rule[threshold]} elif rule[type] range: in_range df[col].between(rule[min], rule[max]).mean() report[col] {in_range_rate: in_range, pass: in_range rule[threshold]} return report提示质量检查的结果要记录下来不要只打印到控制台。这些历史记录在排查模型效果为什么突然下降这类问题时非常有用。4. 模型封装让模型变成可替换的组件4.1 为什么要把模型关进笼子我见过很多项目模型加载和推理的代码散落在业务逻辑里到处都在model.predict()。这种写法的问题在于模型和业务耦合太紧想换个模型、改个参数、加个预处理都得动业务代码牵一发动全身。正确的做法是把模型封装成一个独立的组件对外只暴露清晰的接口内部实现细节用什么框架、怎么加载、怎么预处理都藏起来。这样业务代码只依赖接口不依赖具体实现模型可以随时替换。这个思路在软件工程里叫依赖倒置在AI工程里同样适用。我通常定义一个抽象基类规定模型必须实现的方法然后具体的模型实现继承这个基类。from abc import ABC, abstractmethod class BaseModel(ABC): abstractmethod def load(self, model_path: str): pass abstractmethod def predict(self, inputs): pass abstractmethod def batch_predict(self, inputs_list): pass有了这个抽象层业务代码只认BaseModel具体用哪个模型由配置决定。想换模型改配置就行业务代码一行不动。4.2 配置管理别把参数写死在代码里模型相关的参数特别多模型路径、推理超时、批大小、设备选择、精度设置……这些参数如果写死在代码里每次调整都要改代码重新部署效率极低。我的做法是用分层配置默认配置写在代码里环境相关配置用环境变量运行时配置用配置文件。优先级从低到高高优先级覆盖低优先级。from pydantic import BaseSettings class ModelConfig(BaseSettings): model_path: str models/default device: str cuda batch_size: int 8 timeout: float 30.0 precision: str fp16 class Config: env_prefix MODEL_ config ModelConfig()用pydantic的好处是它有类型校验配置写错了启动时就报错不会等到运行时才出问题。而且它天然支持从环境变量读取部署时改环境变量就行不用动代码。4.3 模型加载的容错与预热模型加载是个容易出问题的环节。文件可能损坏、路径可能不对、显存可能不够、依赖版本可能不匹配。如果加载失败直接抛异常服务就起不来了。我的做法是加载时重试 降级。import time import logging def load_with_retry(model, path, max_retries3, backoff2.0): for attempt in range(max_retries): try: model.load(path) logging.info(fModel loaded from {path}) return model except Exception as e: logging.warning(fLoad attempt {attempt1} failed: {e}) if attempt max_retries - 1: time.sleep(backoff ** attempt) raise RuntimeError(fFailed to load model after {max_retries} attempts)另外模型加载后最好做一次预热用几条典型输入跑一遍推理。这样能把一些延迟初始化的问题比如CUDA上下文创建、算子编译提前暴露出来避免第一个真实请求超时。4.4 批处理与动态批大小推理服务的吞吐量和延迟是一对矛盾。单条推理延迟低但吞吐上不去批处理吞吐高但单条延迟会增加。我的经验是动态批处理在短时间内积累请求凑够一批一起推理但设置最大等待时间避免请求等太久。import asyncio from collections import deque class DynamicBatcher: def __init__(self, model, max_batch32, max_wait0.05): self.model model self.max_batch max_batch self.max_wait max_wait self.queue deque() async def submit(self, input_data): future asyncio.Future() self.queue.append((input_data, future)) if len(self.queue) self.max_batch: await self._flush() return await future async def _flush(self): batch list(self.queue) self.queue.clear() inputs [item[0] for item in batch] results self.model.batch_predict(inputs) for (_, future), result in zip(batch, results): future.set_result(result)批大小的选择需要根据模型大小、显存、延迟要求来调。我的经验值是小模型1B参数可以到32-64中等模型1-10B8-16大模型10B1-4。当然这只是一个起点实际要压测确定。5. 推理服务从能跑到能扛5.1 接口设计别让模型细节泄漏到API设计推理服务的接口时一个常见错误是把模型的内部细节暴露出去。比如接口参数里出现model_name、temperature、top_p这种模型特有的参数。这样做的后果是每次换模型或者调参数客户端都得跟着改。我的做法是面向业务语义设计接口而不是面向模型。客户端只关心我要完成什么任务不关心用哪个模型、怎么调参。模型选择和参数调优是服务端的事。from fastapi import FastAPI from pydantic import BaseModel class PredictRequest(BaseModel): text: str task: str classification class PredictResponse(BaseModel): result: dict request_id: str app FastAPI() app.post(/predict, response_modelPredictResponse) async def predict(req: PredictRequest): # 内部根据task路由到不同模型客户端无感知 result router.dispatch(req.task, req.text) return PredictResponse(resultresult, request_idgenerate_id())这样设计的好处是服务端可以自由地换模型、调参数、加预处理客户端完全无感知。接口稳定客户端就稳定。5.2 超时、重试与熔断推理服务最容易出问题的地方是超时。模型推理时间不稳定偶尔来个长尾请求把线程池占满整个服务就雪崩了。所以超时控制是必须的。我的做法是给每个推理请求设置超时超时后返回降级结果比如默认值或者缓存结果而不是让请求一直挂着。同时配合重试和熔断偶发失败重试连续失败熔断避免故障扩散。import asyncio from circuitbreaker import circuit circuit(failure_threshold5, recovery_timeout30) async def infer_with_timeout(model, inputs, timeout10.0): try: return await asyncio.wait_for(model.predict(inputs), timeouttimeout) except asyncio.TimeoutError: logging.error(Inference timeout) return get_fallback_result()熔断器的参数失败阈值、恢复时间要根据实际业务来调。我的经验是失败阈值设在5-10次恢复时间30-60秒这样既能快速止损又不会因为偶发失败误熔断。5.3 并发模型的选择Python的并发模型有好几种多线程、多进程、异步IO。推理服务该用哪种取决于你的模型和框架。如果模型推理是CPU密集型的比如传统机器学习模型用多进程绕开GIL。如果推理是IO密集型的比如调用远程API用异步IO。如果是GPU推理情况复杂一些——GPU推理本身是异步的但Python的GIL会限制并发通常的做法是用异步框架配合批处理。# 异步IO 批处理的典型结构 app.post(/predict) async def predict(req): result await batcher.submit(req.text) return result我的经验是不要过早优化并发。先用最简单的同步方式跑通压测发现瓶颈了再针对性优化。很多项目其实并发量根本不高过早引入复杂的并发模型反而增加维护成本。5.4 健康检查与优雅关闭服务要能对外报告自己的状态这就是健康检查。健康检查不能只检查进程活着还要检查关键依赖模型、数据库、缓存是否可用。app.get(/health) async def health(): checks { model: model.is_ready(), db: await db.ping(), cache: cache.ping(), } status healthy if all(checks.values()) else degraded return {status: status, checks: checks}优雅关闭同样重要。服务收到关闭信号时不能直接退出要先把正在处理的请求处理完停止接受新请求然后释放资源。否则正在处理的请求会失败用户体验很差。import signal def shutdown_handler(signum, frame): logging.info(Shutting down gracefully...) server.should_exit True signal.signal(signal.SIGTERM, shutdown_handler)6. 监控与可观测性让问题无处遁形6.1 该监控哪些指标AI服务的监控指标和普通服务有重叠也有差异。我通常监控这几类系统指标CPU、内存、GPU利用率、显存占用、磁盘IO、网络IO。这些是基础能反映资源瓶颈。服务指标QPS、延迟P50/P95/P99、错误率、超时率。这些反映服务质量。模型指标推理延迟、批大小分布、输入长度分布、输出分布。这些反映模型行为。业务指标任务成功率、用户反馈、下游影响。这些反映业务效果。其中我特别想强调的是延迟的分位数。只看平均延迟会骗人P99延迟才是用户体验的真实反映。我见过平均延迟50ms但P99延迟5秒的服务平均看着很美但1%的用户在骂娘。6.2 日志的结构化与分级日志是排查问题的第一手资料但很多人的日志写得没法用——要么太少出问题啥也看不到要么太多关键信息淹没在噪音里。我的做法是结构化日志 分级。结构化是指日志用JSON格式字段固定方便检索和聚合。分级是指按严重程度分DEBUG/INFO/WARNING/ERROR生产环境只输出INFO以上。import logging import json class JsonFormatter(logging.Formatter): def format(self, record): log { time: self.formatTime(record), level: record.levelname, msg: record.getMessage(), module: record.module, } if hasattr(record, request_id): log[request_id] record.request_id return json.dumps(log, ensure_asciiFalse) logger logging.getLogger() handler logging.StreamHandler() handler.setFormatter(JsonFormatter()) logger.addHandler(handler)关键是要给每个请求分配一个request_id贯穿整个处理链路。这样排查问题时用request_id一搜这个请求经过的所有环节、产生的所有日志都能串起来。6.3 告警的设计原则告警设计不好要么天天误报让人麻木要么真出事了不响。我的原则是告警要少而准每个告警都要可行动。具体来说告警要基于症状而不是原因。比如P99延迟超过1秒是症状GPU利用率高是原因。症状告警直接反映用户体验原因告警容易误报GPU利用率高不一定是问题可能是正常的高负载。告警阈值要基于历史数据设定不能拍脑袋。我的做法是先观察一周的正常波动范围把阈值设在正常范围的边界外一点。同时设置告警抑制避免同一个问题反复告警。告警项阈值级别处理动作P99延迟 2s 持续5分钟P1立即排查错误率 1% 持续5分钟P1立即排查GPU显存 90% 持续10分钟P2关注准备扩容队列积压 100 持续1分钟P2关注检查消费能力6.4 分布式追踪的落地当服务拆成多个组件后一个请求可能经过网关、预处理、模型推理、后处理多个环节排查问题时需要知道每个环节花了多少时间。这就是分布式追踪要解决的。我用得比较多的是OpenTelemetry它能自动埋点也能手动加span。关键是每个环节都要传递trace context这样整条链路才能串起来。from opentelemetry import trace tracer trace.get_tracer(__name__) def process_request(req): with tracer.start_as_current_span(preprocess) as span: data preprocess(req) span.set_attribute(input_length, len(data)) with tracer.start_as_current_span(inference) as span: result model.predict(data) span.set_attribute(batch_size, len(data)) return result追踪数据配合日志排查问题的效率能提升一个数量级。以前靠猜现在靠数据。7. 实验管理与持续迭代7.1 实验记录别让好结果变成一次性做AI项目实验是家常便饭。调个参数、换个模型、改个预处理都要跑实验看效果。问题是实验多了之后你根本记不住哪个配置对应哪个结果。我早期就吃过这个亏——跑出一个好结果过两天想复现发现忘了当时用的什么参数。后来我强制自己每次实验都记录记录内容包括实验ID、时间、代码版本、数据版本、配置参数、评估指标、备注。这些记录用工具管理我常用的是MLflow或者Weights Biases轻量一点的话用CSV也行关键是坚持记。import mlflow with mlflow.start_run(): mlflow.log_params({lr: 1e-4, batch_size: 16, model: bert-base}) mlflow.log_metrics({accuracy: 0.92, f1: 0.89}) mlflow.log_artifact(model.pt)有了完整的实验记录你才能回答为什么选这个配置这种问题也才能在需要时复现任意一次实验。7.2 A/B测试的工程实现模型上线不是终点而是起点。新模型效果好不好不能只看离线指标要看线上表现。这就涉及到A/B测试。A/B测试的工程实现有几个关键点流量分割、指标对比、统计显著性。流量分割要保证随机性和一致性同一个用户始终分到同一组指标对比要选对指标不能只看点击率还要看转化率、留存率统计显著性要算对样本量够不够差异是不是偶然。import hashlib def assign_group(user_id, experiment_id, groups[A, B]): key f{experiment_id}:{user_id} hash_val int(hashlib.md5(key.encode()).hexdigest(), 16) return groups[hash_val % len(groups)]这个简单的哈希分桶能保证同一个用户在同一个实验里始终分到同一组而且分布均匀。实验结束后用统计方法对比两组的指标判断新模型是否真的更好。7.3 模型回滚机制新模型上线后如果发现问题要能快速回滚。回滚机制的设计要点是版本可切换、切换要快、切换要可追溯。我的做法是模型文件按版本存放服务启动时从配置读取版本号切换版本只需要改配置重启或者更高级的热加载。同时记录每次切换的时间、操作人、原因方便事后复盘。class ModelRegistry: def __init__(self, base_path): self.base_path base_path self.current_version None def switch(self, version): new_path f{self.base_path}/{version} if not os.path.exists(new_path): raise ValueError(fVersion {version} not found) old_version self.current_version self.current_version version logging.info(fSwitched model from {old_version} to {version})回滚不是失败而是工程成熟的表现。一个没有回滚机制的系统是不敢频繁迭代的。7.4 持续优化的闭环AI工程的终极形态是一个持续优化的闭环线上数据回流到训练数据训练出的新模型经过评估上线上线后的表现又产生新的数据。这个闭环转起来系统才能持续进化。闭环的关键是自动化。数据回流要自动训练要自动评估要自动上线要自动或者半自动关键决策人工确认。全自动有风险全手动效率低我的建议是评估和上线环节保留人工确认其他环节尽量自动化。这个闭环搭起来不容易但一旦搭起来你的AI系统就从一次性项目变成了持续进化的产品。这是AI工程和AI Demo最本质的区别。8. 一些踩坑之后的真心话上面讲的都是方法论和实操最后我想分享几个踩坑之后的体会这些是文档里不会写但实际很重要的。第一不要追求一步到位。我见过有人一上来就想搭一套完美的AI工程体系结果光设计就花了一个月代码一行没写。正确的做法是先跑通最小闭环然后逐步完善。先能跑再能扛最后才是能进化。第二工具是手段不是目的。有人迷信各种工具觉得用了某个框架就工程化了。其实工具只是帮你实现工程化的手段核心还是你对问题的理解。我见过用最土的工具搭出很稳的系统的也见过用最时髦的框架搭出很脆的系统的。第三文档和测试的时间不能省。这两个是最容易被省略的也是最容易在后期让你付出代价的。我现在坚持写两类文档架构文档系统由哪些部分组成怎么协作和运维文档出问题怎么排查怎么恢复。测试则覆盖关键路径和边界情况。第四保持对数据的敬畏。AI系统的大部分问题最终都能追溯到数据。数据质量、数据分布、数据时效这些比模型结构重要得多。我现在的习惯是模型效果出问题先查数据再查代码最后才怀疑模型。第五接受不确定性。AI系统天然有不确定性你不可能让所有请求都完美。工程的目标不是消除不确定性而是管理不确定性——让系统在不确定的情况下依然稳定、可预期。这个心态转变很重要想通了这一点很多工程决策就顺了。这套从零构建AI工程能力的思路我自己实践下来最大的感受是慢就是快。前期在环境、数据、封装这些不性感的地方多花时间后期在迭代、排查、扩展上就能省下大量时间。那些看起来很快的Demo式开发最后往往要花更多时间还债。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门