首页 / 资讯中心 / 文章详情

AI工程从零构建:手写推理服务底座的实战路径

AI工程从零构建:手写推理服务底座的实战路径 ★ FEATURED ARTICLE
1. 这不是调包是亲手搭起AI工程的骨架“AI Engineering from Scratch”——看到这个标题很多人第一反应是又要学Python又要啃论文又要配CUDA其实完全不是。我带过二十多个从零起步的团队真正卡住他们的从来不是算法公式而是不知道AI系统里哪根线该接在哪哪个模块一动就全崩更别说上线后日志炸屏、延迟飙升、模型突然输出乱码。这根本不是“写代码”的问题而是“建系统”的问题。AI Engineering from Scratch说白了就是扔掉Hugging Face一键加载、绕开LangChain自动链式调用、不碰任何“Auto”前缀的库从Linux进程管理开始一行一行敲出能跑、能查、能扩、能扛压的AI服务底座。它解决的不是“怎么让模型说话”而是“当一百个用户同时问‘今天天气怎么样’你的服务凭什么不挂、不慢、不错、不漏”。适合三类人刚转行想避开“调包工程师”标签的新人带团队却总被线上事故拖垮的Tech Lead还有那些在PPT里画完“AI中台架构图”结果连一个推理API都部署不稳的架构师。关键词里反复出现的ai-engineering和from-scratch不是修辞是动作指令——前者指向工业级可靠性、可观测性、可维护性后者意味着你得亲手摸到内存分配、线程调度、序列化协议、HTTP头字段这些被封装层彻底藏起来的“脏活”。这不是学术实验是把AI当成水电煤一样的基础设施来建。下面拆解的每一步我都实测过至少三种方案踩过坑才敢写进正文。2. 为什么必须放弃“黑盒启动”从进程与内存开始重建信任2.1 黑盒启动的三大幻觉正在毁掉你的交付节奏很多团队所谓“从零开始”其实是用pip install transformers python app.py启动一个Flask服务然后自信满满地宣布“AI工程落地”。这就像用乐高积木搭摩天楼——看着漂亮但风一吹就散。我见过最典型的三个幻觉幻觉一“模型加载快服务快”实测一个7B参数的LLM在A100上model.from_pretrained()耗时12秒但首次model.generate()触发CUDA上下文初始化显存预分配又卡住8秒。用户等20秒产品直接流失。而从scratch出发你会强制把模型加载、KV缓存预热、CUDA流绑定全部拆成独立进程用subprocess.Popen控制启动顺序和超时失败立刻kill重试而不是让整个Web服务进程卡死。幻觉二“Docker封装生产就绪”Docker镜像里pip install -r requirements.txt装了57个依赖其中3个包有C扩展编译时默认用-O2优化结果在生产环境CPU型号不匹配运行时SIGSEGV崩溃。从scratch要求你用manylinux2014标准构建wheel用auditwheel repair校验ABI兼容性甚至手动patch PyTorch的libtorch.so符号表——这些事没人教但线上凌晨三点告警时你只能自己干。幻觉三“Prometheus监控可观测”挂上/metrics端点看到http_requests_total数字在涨就以为一切正常。直到某天发现GPU显存占用98%但gpu_utilization指标始终为0——因为nvidia-smi驱动版本和DCGM exporter不匹配指标根本没采集。从scratch意味着你要亲手写/healthz探针检查CUDA context是否alive、显存碎片率是否40%、模型权重文件MD5是否匹配而不是依赖某个exporter的默认配置。提示所有“一键部署”方案本质都是把复杂度转移给下游——要么转移给运维他们要懂CUDA驱动兼容性要么转移给用户他们要忍受首请求延迟要么转移给未来技术债爆发时你已离职。AI Engineering from Scratch是把复杂度拉回自己可控的边界内。2.2 真正的“Scratch”起点Linux进程树与内存映射我们不用任何框架先写一个最简服务骨架# 创建基础目录结构 mkdir -p ai-engine/{bin,lib,config,logs,models} cd ai-engine核心不是代码而是进程设计哲学bin/launcher.sh主入口只做三件事——验证CUDA驱动版本、预分配显存页cudaMalloc(1GB)、fork子进程。它本身不加载模型只做守门人。bin/inference_worker.py真正干活的进程收到请求后从共享内存区读取tokenized输入执行forward结果写回共享内存。它没有网络栈只认IPC。bin/monitor.py独立进程每5秒用psutil抓取inference_worker的RSS内存、nvidia-smi --query-compute-appspid,used_memory显存占用、/proc/[pid]/io磁盘IO写入本地TSDB用SQLite模拟。为什么这样设计因为真正的“从零开始”第一步是把AI服务降维成操作系统能理解的实体进程、内存、文件描述符、信号。当你亲手用mmap()把模型权重映射到进程地址空间用os.kill(pid, signal.SIGUSR1)触发worker热重载用setrlimit(RLIMIT_AS, 8*1024*1024*1024)限制虚拟内存上限——你才真正拿到系统的控制权。Hugging Face的pipeline()再方便也掩盖不了它背后是multiprocessing.Process在偷偷fork而fork时的copy-on-write机制在大模型场景下会导致显存瞬间翻倍。这些细节只有从scratch才能暴露。2.3 工具链选择为什么拒绝“全家桶”坚持手搓关键组件市面上有LangChain、LlamaIndex、vLLM等成熟方案但它们解决的是“如何组织AI能力”而非“如何让AI能力稳定存活”。从scratch的工具链选择逻辑非常残酷组件常见方案Scratch方案选择理由HTTP服务器FastAPI/Flaskuvicorn --workers 1 自定义ASGI middlewareFastAPI的依赖注入太重middleware里要插request_id追踪、timeout熔断、rate_limit令牌桶手写更可控模型加载transformers.from_pretrainedtorch.load(..., map_locationcpu)torch.nn.Module.load_state_dict()避免from_pretrained内部的_load_pretrained_model自动下载、缓存、解压逻辑这些在离线环境全是雷序列化JSONProtocol Buffers 自定义ModelInputschemaJSON序列化torch.Tensor要转list再转numpy10MB tensor序列化耗时200msProtobuf二进制直传5ms日志structloglogging 自定义RotatingFileHandlerstructlog的bind()在高并发下产生大量dict拷贝实测QPS500时CPU占用飙升30%原生logging的Formatter.format()更轻量关键不是“哪个更好”而是“哪个的失控点最少”。比如选Uvicorn不选Gunicorn因为Gunicorn的pre-fork模式在CUDA环境下有已知的context泄漏问题选Protobuf不选JSON因为Tensor序列化性能差两个数量级而AI服务的瓶颈往往不在GPU计算而在数据搬运。这些决策背后是上百次压测对比的真实数据不是技术博客里的主观推荐。3. 核心细节解析从模型加载到请求处理的七层穿透3.1 模型加载绕过缓存陷阱直击权重文件解析“从零开始”加载模型第一步不是from_pretrained而是解析pytorch_model.bin或safetensors文件结构。以safetensors为例它的header是纯JSON但实际权重是二进制块# 手动解析safetensors文件无任何第三方库 with open(model.safetensors, rb) as f: header_size int.from_bytes(f.read(8), little) header_bytes f.read(header_size) header json.loads(header_bytes.decode(utf-8)) # header示例: {shape: [1, 4096], dtype: float16, data_offsets: [0, 8192]} # 定位第一个tensor的二进制数据 offset header[data_offsets][0] f.seek(offset) tensor_data f.read(8192) # float16 * 4096 8192 bytes # 转为torch.tensor tensor torch.frombuffer(tensor_data, dtypetorch.float16).reshape(header[shape])为什么这么做因为transformers的load_sharded_checkpoint会自动合并shard但在K8s环境下shard文件可能分布在不同节点NFS挂载延迟导致加载超时。而手动解析你可以并行下载所有shard到本地临时目录用concurrent.futures.ThreadPoolExecutor按data_offsets顺序拼接二进制流避免内存爆炸验证每个shard的SHA256防止传输损坏实操心得safetensors比.bin快3倍但它的data_offsets是相对整个文件的偏移不是相对shard的偏移——这个细节文档里没写但不处理就会读错数据。我踩过这个坑在客户现场花了6小时debug。3.2 内存管理显存与CPU内存的协同调度策略大模型推理最大的敌人不是算力是内存带宽争抢。当CPU把token id数组喂给GPUGPU计算完logits再写回CPU内存这条通路如果没管好吞吐量直接腰斩。Scratch方案的核心是显存预分配零拷贝传输显存预分配启动时用torch.cuda.memory_reserved()预留固定显存池避免runtime动态分配碎片化。例如7B模型预留12GB比理论需求多2GB防碎片。零拷贝传输CPU端用torch.pin_memory()锁定tensor内存页GPU端用torch.cuda.Stream异步DMA传输。关键代码# CPU端预分配 pinned memory input_ids torch.empty((1, 2048), dtypetorch.long, pin_memoryTrue) # GPU端创建专用stream compute_stream torch.cuda.Stream() # 异步传输不阻塞CPU with torch.cuda.stream(compute_stream): input_gpu input_ids.to(cuda, non_blockingTrue) logits model(input_gpu) # ... compute output_cpu logits.cpu().detach() # 同样non_blocking注意non_blockingTrue必须配合pin_memoryTrue否则无效。很多教程漏写这一句导致实际仍是同步拷贝。常见问题K8s Pod里nvidia-smi显示显存占用100%但torch.cuda.memory_allocated()只返回2GB——这是因为PyTorch的显存缓存机制caching allocator占用了剩余显存。解决方案在launcher.sh里设置export PYTORCH_CUDA_ALLOC_CONFmax_split_size_mb:128强制限制缓存块大小避免大模型加载时OOM。3.3 请求处理流水线解耦输入解析、模型执行、后处理三阶段一个HTTP请求进来传统做法是app.post(/infer)里一口气做完tokenize→model→detokenize。Scratch方案强制拆成三个独立函数并用asyncio.Queue解耦# 输入解析阶段CPU bound async def parse_request(request: Request) - Tuple[torch.Tensor, Dict]: # 1. 验证JSON schema用jsonschema不依赖Pydantic # 2. 调用tokenizer预先加载到CPU避免GPU context切换 # 3. 返回input_ids tensor metadata dict含request_id, timeout等 return input_ids, meta # 模型执行阶段GPU bound async def run_inference(input_ids: torch.Tensor) - torch.Tensor: # 1. 将input_ids移到GPUpin_memory保证零拷贝 # 2. 执行model.generate()设max_new_tokens512 # 3. 返回logits不detokenize留给后处理 return logits # 后处理阶段CPU bound async def post_process(logits: torch.Tensor, meta: Dict) - str: # 1. argmax取token id # 2. tokenizer.decode()转文本 # 3. 添加trace_id、latency等字段 return json.dumps({response: text, latency_ms: ...})这样拆解的好处输入解析失败如JSON格式错误不消耗GPU资源模型执行超时如max_new_tokens卡住可直接cancel()任务释放GPU后处理崩溃如decode失败不影响模型进程只丢弃单个请求实测数据QPS从85提升到132P99延迟从1200ms降到480ms。因为GPU不再被低效的JSON解析阻塞CPU也不再被GPU同步等待拖慢。3.4 错误处理从“Exception”到“Error Code”的工业级转换AI服务最常见的错误不是CUDA out of memory而是语义错误用户传了空字符串、token长度超限、temperature0导致重复生成。Scratch方案拒绝try...except Exception as e而是定义明确的错误码体系错误码HTTP状态场景处理方式E001400input为空或非UTF-8立即返回不进模型E002400token length 2048截断并warn记录truncation日志E003429request rate 10/s per IP返回Retry-After: 1不计费E004503GPU显存碎片率 60%拒绝新请求触发worker重启关键实现在ASGI middleware里拦截所有异常统一转换class ErrorHandlerMiddleware: async def __call__(self, scope, receive, send): try: await self.app(scope, receive, send) except InputEmptyError: await self._send_error(send, E001, 400) except TokenLengthExceeded: await self._send_error(send, E002, 400) # ... 其他错误好处是前端可以精准重试E001直接修正输入E003等1秒再发运维可以按错误码聚合告警E004出现3次立即升级而不是看一堆ValueError: Expected input to be a tensor日志。4. 实操过程从空目录到可上线服务的完整步骤4.1 环境准备最小化Docker镜像构建不要用python:3.10-slim它缺gcc编译PyTorch C扩展会失败。Scratch镜像必须精确控制# ai-engine/Dockerfile FROM nvidia/cuda:12.1.1-devel-ubuntu22.04 # 安装最小依赖 RUN apt-get update apt-get install -y \ build-essential \ libglib2.0-0 \ libsm6 \ libxext6 \ rm -rf /var/lib/apt/lists/* # 创建非root用户安全强制 RUN useradd -m -u 1001 -g root aiuser USER aiuser WORKDIR /home/aiuser # 复制源码不包含.git不包含test COPY --chownaiuser:root bin/ /home/aiuser/bin/ COPY --chownaiuser:root lib/ /home/aiuser/lib/ COPY --chownaiuser:root config/ /home/aiuser/config/ # 安装Python静态链接避免glibc版本冲突 RUN curl -O https://www.python.org/ftp/python/3.10.12/Python-3.10.12.tgz \ tar -xzf Python-3.10.12.tgz \ cd Python-3.10.12 \ ./configure --enable-optimizations --without-pymalloc --with-lto \ make -j$(nproc) \ sudo make altinstall \ cd .. rm -rf Python-3.10.12* # 安装PyTorch指定CUDA版本禁用conda RUN pip3.10 install torch2.1.0cu121 torchvision0.16.0cu121 \ --extra-index-url https://download.pytorch.org/whl/cu121 \ --no-cache-dir # 复制模型权重外部挂载镜像不打包模型 VOLUME [/home/aiuser/models]构建命令docker build -t ai-engine:1.0.0 .镜像大小仅1.2GB对比pytorch/pytorch:2.1.0-cuda12.1-cudnn8-runtime的3.8GB启动时间从18秒降到4.2秒。关键是--without-pymalloc选项禁用Python内存池在多进程场景下减少内存碎片。4.2 模型适配将Hugging Face模型转为Scratch可加载格式以Llama-2-7b-chat为例官方transformers加载需要tokenizer.json、pytorch_model.bin、config.json三个文件。Scratch要求扁平化# 步骤1下载原始模型离线环境用huggingface-cli huggingface-cli download meta-llama/Llama-2-7b-chat-hf --revision main --local-dir ./tmp-model # 步骤2转换为safetensors避免bin文件的pickle风险 python -c from transformers import AutoModelForCausalLM import safetensors.torch model AutoModelForCausalLM.from_pretrained(./tmp-model, device_mapcpu) safetensors.torch.save_file(model.state_dict(), ./models/llama2-7b.safetensors) # 步骤3提取tokenizer为纯vocab.json merges.txt删除tokenizer_config.json cp ./tmp-model/tokenizer.json ./models/vocab.json cp ./tmp-model/merges.txt ./models/merges.txt # 步骤4生成config.yaml精简版只保留scratch需要的字段 cat ./models/config.yaml EOF model_type: llama hidden_size: 4096 num_attention_heads: 32 num_hidden_layers: 32 max_position_embeddings: 4096 vocab_size: 32000 EOF最终./models/目录结构models/ ├── llama2-7b.safetensors # 权重二进制 ├── vocab.json # tokenizer词表 ├── merges.txt # BPE合并规则 └── config.yaml # 模型超参这样做的好处部署时只需rsync这4个文件无需git lfs、无需huggingface-cli认证符合金融、政企客户的离线交付要求。4.3 启动脚本launcher.sh的12个关键检查项bin/launcher.sh不是简单python worker.py而是包含12个生产环境必备检查#!/bin/bash # bin/launcher.sh # 1. 检查CUDA驱动版本必须525.60.13 DRIVER_VER$(nvidia-smi --query-gpudriver_version --formatcsv,noheader,nounits) if [[ $(printf %s\n $DRIVER_VER 525.60.13 | sort -V | tail -n1) ! $DRIVER_VER ]]; then echo ERROR: CUDA driver too old 2 exit 1 fi # 2. 检查GPU可见性防止K8s node selector失效 if ! nvidia-smi -L | grep -q UUID; then echo ERROR: No GPU detected 2 exit 1 fi # 3. 预分配显存避免后续OOM python3.10 -c import torch torch.cuda.set_per_process_memory_fraction(0.8) torch.cuda.memory_reserved() print(GPU memory reserved) # 4. 验证模型文件完整性SHA256 if ! sha256sum -c models/sha256sums.txt; then echo ERROR: Model files corrupted 2 exit 1 fi # ... 5-12. 其他检查config.yaml语法、log目录权限、PID文件锁、ulimit设置等每个检查项都对应一个真实线上事故Driver版本过低导致torch.compile()崩溃GPU不可见因K8s device plugin未安装模型文件损坏因NFS传输中断。这些不是“可能”而是“必然发生”。4.4 压测与调优Locust脚本与关键指标解读用Locust模拟真实流量不是简单curl而是复现用户行为# locustfile.py from locust import HttpUser, task, between import json class AIUser(HttpUser): wait_time between(1, 3) # 用户思考时间 task def infer(self): payload { prompt: Write a 3-line poem about rain, max_tokens: 64, temperature: 0.7 } # 发送POST请求带trace_id头 with self.client.post( /infer, jsonpayload, headers{X-Trace-ID: str(uuid.uuid4())}, catch_responseTrue ) as response: if response.status_code 200: data response.json() if error_code in data: response.failure(fAPI error: {data[error_code]}) else: response.success() else: response.failure(fHTTP {response.status_code}) # 运行命令locust -f locustfile.py --headless -u 100 -r 10 --run-time 5m关键指标解读来自Locust报告Response time (95%) 800ms达标GPU计算数据搬运总耗时Failure rate 0.1%主要来自E003限流非服务崩溃CPU utilization 70%说明CPU不是瓶颈GPU利用率应85%Memory leak?观察RSS曲线是否持续上升若5分钟内增长50MB存在tensor未释放实操心得压测时一定要开nvidia-smi dmon -s u监控GPU利用率很多团队看到QPS上不去以为是CPU瓶颈结果发现GPU利用率只有30%——原因是batch size太小GPU计算单元没喂饱。Scratch方案里batch_size不是配置项而是根据nvidia-smi --query-gpumemory.total动态计算batch_size (total_memory * 0.7) // (per_token_memory * max_seq_len)。5. 常见问题与排查技巧实录线上事故的21个真实现场5.1 显存相关问题从“OOM”到“显存碎片”的深度诊断问题现象服务运行2小时后nvidia-smi显示显存占用95%但torch.cuda.memory_allocated()只返回1.2GB新请求报CUDA out of memory。排查路径nvidia-smi --query-compute-appspid,used_memory查看各进程显存torch.cuda.memory_summary()输出详细内存分布注意必须在目标进程里执行关键指标allocatedvsreservedvscached根本原因PyTorch的caching allocator保留了大量小块显存无法合并。reserved10GBallocated1.2GBcached8.8GB。解决方案启动时加环境变量export PYTORCH_CUDA_ALLOC_CONFmax_split_size_mb:128在模型加载后手动清空缓存torch.cuda.empty_cache()更激进用torch.cuda.caching_allocator_delete()彻底销毁allocator需重启进程注意empty_cache()只是释放未使用的缓存对已分配的reserved无效。真正有效的是max_split_size_mb参数它限制了allocator每次申请的最大块大小强迫其合并小块。5.2 请求延迟突增定位“隐形阻塞点”问题现象P99延迟从500ms突然跳到3500ms但CPU/GPU利用率正常日志无ERROR。排查路径strace -p $(pgrep -f inference_worker) -e traceconnect,accept,read,write抓系统调用perf top -p $(pgrep -f inference_worker)看热点函数cat /proc/$(pgrep -f inference_worker)/stack查进程调用栈真实案例发现read()系统调用在/dev/urandom上阻塞。原因是torch.Generator初始化时调用os.urandom(32)而容器里/dev/urandom熵池不足K8s默认不挂载host的/dev。解决方案启动时预热/dev/urandom或改用torch.manual_seed(int(time.time()))。5.3 模型输出错乱字符编码与tokenizer的隐秘战争问题现象中文输出变成乱码如你好变成\xe4\xbd\xa0\xe5\xa5\xbd。排查路径检查tokenizer.decode()返回类型是str还是bytes检查HTTP响应头Content-Type: application/json; charsetutf-8是否正确检查Uvicorn配置--env PYTHONIOENCODINGutf-8是否设置根本原因tokenizer.decode()返回str但Uvicorn默认用latin-1编码响应体。解决方案在ASGI middleware里强制设置response.headers[Content-Type] application/json; charsetutf-8并在json.dumps()时加ensure_asciiFalse。5.4 K8s环境特有问题GPU设备与Pod生命周期问题现象Pod重启后nvidia-smi能识别GPU但torch.cuda.is_available()返回False。排查路径kubectl describe pod查看Events是否有nvidia.com/gpu: 0分配失败kubectl exec -it pod -- nvidia-smi -L确认GPU设备节点ls -l /dev/nvidia*检查设备文件权限解决方案DaemonSet里nvidia-device-plugin必须与宿主机驱动版本严格匹配Pod spec里添加securityContext: {privileged: true}某些驱动需要初始化容器里运行nvidia-smi -q -d MEMORY验证GPU健康实操心得K8s里GPU不是“资源”而是“设备”。resources.limits.nvidia.com/gpu: 1只是声明真正生效靠device plugin。很多团队以为配了limit就万事大吉结果发现Pod根本没挂载/dev/nvidia0。5.5 安全加固从“能跑”到“合规”的最后一步问题现象等保测评要求“应用无root权限”、“敏感信息加密存储”、“日志脱敏”。Scratch加固清单Dockerfile里USER aiuser禁止rootAPI密钥不硬编码用/run/secrets/挂载Docker Swarm或Secret卷K8s日志脱敏自定义logging.Filter匹配api_key: [^]替换为api_key: ***TLS证书用certbot自动续期launcher.sh里检查证书有效期关键检查docker scan ai-engine:1.0.0扫描CVE重点修复libjpeg-turbo、openssl等底层库漏洞。Scratch的优势在于你清楚知道镜像里每一个字节的来源不像pytorch:latest这种黑盒镜像连它用的glibc版本都不知道。6. 从Scratch到Scale单机服务如何演进为生产级AI平台6.1 横向扩展从单Worker到Worker Pool的平滑演进单机inference_worker.py跑满后不能简单--workers 4因为CUDA context不能跨进程共享。Scratch方案采用Master-Worker模型master.py监听HTTP接收请求分发到空闲Workerworker.py独立进程持有自己的CUDA context执行推理IPC机制Unix domain socket比TCP快3倍无网络栈开销通信协议用Protobuf定义// worker.proto message InferenceRequest { repeated int32 input_ids 1; int32 max_new_tokens 2; } message InferenceResponse { repeated int32 output_ids 1; double latency_ms 2; }启动时master.pyfork 4个worker.py每个worker绑定独立GPUCUDA_VISIBLE_DEVICES0master用round-robin分发请求。实测QPS从132提升到480且故障隔离一个worker崩溃master自动剔除不影响其他worker。6.2 模型热更新不重启服务的权重替换客户要求“模型更新不中断服务”Scratch方案用双缓冲权重加载worker.py维护两个模型实例model_active和model_stagingmodel_staging后台加载新权重从/models/new/目录加载完成后原子切换指针model_active, model_staging model_staging, model_active切换瞬间新请求走model_active旧请求继续用老模型直到完成关键切换必须线程安全用threading.Lock()保护指针赋值。实测热更新耗时200ms业务无感知。6.3 监控告警从Metrics到Tracing的全链路可观测Scratch不依赖Prometheus Operator而是自建轻量监控metrics.py暴露/metrics用prometheus_client注册自定义指标tracing.py基于OpenTelemetry记录request_id、model_name、input_length、output_length、cuda_time_msalert_rules.yml定义告警规则如gpu_memory_usage_percent 90 for 2m告警渠道直连企业微信机器人消息模板[AI-ENGINE ALERT] Env: prod Service: llama2-7b Metric: gpu_memory_usage_percent Value: 94.2% Threshold: 90% Time: 2023-10-15 14:22:33所有监控数据存本地SQLite/var/log/ai-engine/metrics.db避免依赖外部TSDB。这是Scratch哲学把不可控的外部依赖降到最低。6.4 最后的经验为什么“从零开始”反而更快交付很多人觉得Scratch慢。我用真实项目数据反驳阶段传统方案Hugging Face FastAPIScratch方案差异原因环境搭建3天调试CUDA、PyTorch、transformers版本1天版本锁定无兼容性问题模型接入2天适配tokenizer、处理padding4小时手动解析无抽象层干扰压测调优5天定位各种黑盒延迟1天所有环节透明可精准测量上线交付2天处理客户离线环境限制0.5天镜像纯净无多余依赖总计12天2.5天快的本质是把模糊的“可能出问题”变成确定的“这一步必须做什么”。Scratch不是炫技是在AI工程混沌中亲手凿出一条确定性的路。当你能说出每一行代码在OS层面做了什么你就拿到了AI服务的源代码——这才是真正的“from scratch”。
阅读完成 · 觉得有帮助?
咨询建站