1. 从零开始前,先搞明白AI工程和AI算法到底差在哪
1.1 那次让我推倒重来的"模型优先"项目
先说个真实经历。去年我刚接手一个智能客服意图分类的需求,当时脑子里的第一反应是"赶紧找模型,把准确率刷上去"。我在笔记本上用了当时最顺手的开源中文模型,微调出92%的验证集准确率,心里挺美。结果真正要上线时,问题一个接一个地冒出来:训练脚本是在Python 3.9里跑的,生产服务器是3.11,依赖冲突直接让服务起不来;训练时用的数据、预处理方式没有记录,过了一周连自己都说不清版本;更别提接口并发一上来,单次推理慢到让人怀疑人生。那个项目最后被我叫停,重新用工程化的思路做了一遍才活过来。
那次之后我重新理解了一个词——ai-engineering-from-scratch,从零开始的AI工程。它不等于"从零训练一个模型",而是指把数据、模型、评测、部署、监控串成一条稳定、可迭代、可复现的流水线。这也是我这篇文章想完整复盘的:一个普通开发者把AI项目从想法推到真线上服务需要经历的全部环节,以及那些文档里不会告诉你的坑。
如果你和我一样,平时写过代码、跑过模型,但还没完整打造过一个AI服务;或者你已经被各种"几分钟上线AI应用"的营销文案骗过,想真正掌握底层工作原理——这篇文章很适合你。我会按自己的实操顺序讲,不绕弯子,也不讲空理论。
1.2 AI工程的最小闭环是什么
我总结的AI工程最小闭环是:数据进入 -> 模型处理 -> 服务输出 -> 结果评估 -> 反馈再进入。这个环里,任何一环断了,整个项目都谈不上"工程化"。很多从零开始的人最常犯的错,就是只盯着"模型"这一环,忽视前后五个环节的稳定性。
举个直观的例子:模型训练得很好,但线上数据分布和训练数据差别很大,效果立刻崩;服务接口响应慢,用户不会等你;评测只看打印出的几个数字,坏case没沉淀,模型永远在原地打转。所以从零开始做AI工程,第一天就要把整条链路放在脑子里,而不是先陷入模型调参的快感。
基于这个思路,我搭这套工程时选了这样一套基础技术栈:
| 层次 | 工具选型 | 核心考量 |
|---|---|---|
| 语言与依赖 | Python 3.11 + uv | 快速、锁版本、团队复现一致 |
| 配置管理 | Pydantic Settings + .env | 密钥和实验参数分离 |
| API服务 | FastAPI + Uvicorn | 异步、轻量、文档友好 |
| 数据存储 | SQLite起步,后期PostgreSQL | 先跑通,再扩展 |
| 模型推理 | vLLM / Ollama | 本地权重部署,兼顾并发与易用 |
| 评测与追踪 | 手工Golden Set + MLflow | 没有评测体系,一切优化都是猜 |
别被这张表唬住,后续每一层我都会展开说为什么这么选,以及有没有更轻量的替代。从零开始最大的优势,就是你不需要推翻任何既定架构,可以照着最终目标长线布局。
1.3 造轮子之前,先学会站在轮子旁边看工厂
很多人一听"from-scratch"就以为是所有东西都自己写。我的看法完全不同:从零开始意味着理解每一步在干什么,而不是重复造每一个轮子。比如向量检索,我不会自己去实现IVF算法,但必须知道它解决什么问题、影响召回效果的参数有哪些;模型推理,我不用手写CUDA算子,但必须搞懂量化和KV-Cache对显存和速度的影响。
这样做的原因很实际:AI工程的技术栈更新速度很快,今天你手写的工具,三个月后可能就被人优化成更完善的开源项目。但底层逻辑——数据怎么流动、延迟从哪里来、评估怎么引导迭代——是稳定的。养成"站在轮子旁边看工厂"的习惯后,你会发现网上那些吹得天花乱坠的AI工程方案,剥掉营销外壳,内核永远是我上面说的那个闭环。
2. 搭好工程骨架:环境管理、配置、目录与可复现性
2.1 用uv替代pip/conda,从环境地狱里解脱
我最开始做AI项目时用的是系统Python + pip install,后来被依赖冲突折磨到想摔键盘。直到把环境管理换成了uv,这个从零搭建的工程才算有了真正稳固的地基。uv是一个用Rust写的Python包管理工具,不仅能安装包,还能接管虚拟环境和Python版本管理,速度比pip快好几倍,最关键的是一份uv.lock锁文件就能让所有协作者安装出完全相同的依赖。
初始化一个项目可以这样操作:
uv init ai-engineering-demo cd ai-engineering-demo uv python install 3.11 uv venv --python 3.11 uv add fastapi uvicorn vllm pydantic-settings datasets注意最后一行,uv add会同时更新pyproject.toml和锁文件。有了锁文件之后,换新机器只需要执行uv sync,装出来的依赖版本不会有一丝偏移。以前用pip时我经常遇到"在我机器上能跑啊"的灵异问题,换用uv之后这类问题几乎绝迹了。
还有一个小建议:环境内不要装顺手而不必要的包。每多一个没有锁进pyproject.toml的包,都是未来排查问题的一颗地雷。保持环境精简,这是我现在非常坚持的工程洁癖。
2.2 我的项目目录模板与配置管理心得
环境搞定了,接下来是目录结构。我目前跑了多个AI项目,最终沉淀下来一套自己很顺手的目录模板:
. ├── configs/ # 各种YAML配置,按环境区分 │ ├── base.yaml │ ├── dev.yaml │ └── prod.yaml ├── data/ │ ├── raw/ # 原始、不可变数据 │ ├── processed/ # 清洗后特征化数据 │ └── eval_set/ # 评测集 ├── models/ # 模型权重或缓存 ├── src/ │ ├── data_pipeline/ # 采集、清洗、标注 │ ├── model_serving/ # 推理封装,FastAPI路由 │ ├── rag/ # 向量库、检索、召回 │ └── evaluation/ # Golden Set评测与统计 ├── tests/ # 单元测试、接口测试 └── pyproject.toml这个结构的核心思想是"数据、代码、配置三者分离"。数据目录下的raw文件夹只读,process不代表能随便改;配置不经过环境变量不传参;代码不管数据在哪里,只从配置读路径。这套约定在一个人开发时显得多余,但项目一旦超过三个月或换人接手,价值立刻体现。
配置文件我用YAML + Pydantic Settings管理,举一个简化例子:
from pydantic_settings import BaseSettings class Settings(BaseSettings): model_path: str = "./models/qwen25-7b-instruct-q4.gguf" chunk_size: int = 512 overlap: int = 64 vector_db_url: str = "sqlite:///./data/vector.db" class Config: env_file = ".env" env_prefix = "AIENGINE_"密钥放.env,参数放YAML或环境变量,代码里不允许出现硬编码路径。这样你换GPU机器测试、生产部署,只需要改配置文件,不必动代码,也不会误把密钥提交到Git。
2.3 让每一次实验都可以被复现
模型实验最大的敌人就是"这次效果好了,但我说不清为什么好"。为了避免这种事,我从工程第一天就强制自己记录四类信息:
| 复现要素 | 具体操作 |
|---|---|
| 代码版本 | 每次实验前Git提交,记录commit hash |
| 数据版本 | 对数据文件计算MD5或使用DVC记录版本 |
| 运行参数 | 所有参数通过configs下的YAML传入,实验时留存副本 |
| 环境依赖 | uv.lock文件和Docker镜像tag配套记录 |
听起来麻烦,但它救过我很多回。有一回我把RAG版本的检索阈值从0.7调到0.5,效果好了,可后来想调回来时发现阈值散落在代码里根本没记录下来。重跑只能靠回忆,整个下午就这么废了。自那以后,任何参数改动都走配置文件,并且实验目录会带上commit短hash。这段好习惯的建立,让后续模型迭代、评测都变得非常顺畅。
3. 数据管道的真实修行:清洗、标注与质量评估
3.1 数据从哪里来,又到哪里去
聊完工程骨架,进入AI工程里最费时间、也最决定天花板的一环:数据。很多人从零开始都会忽略它,以为只要把模型跑起来就完事,结果训练出的模型在真实场景里完全没法用。实际上,数据工作的占比至少是整体工程的50%以上。
数据来源我一般分三类:第一是公开授权数据集,比如Hugging Face上各种合规开放的中文数据集、政府公开数据等;第二是自有业务数据,比如客服对话记录、工单、评论;第三是通过合法接口或人工整理的数据。这里我非常认真地提醒一句:数据版权和隐私合规是底线,尤其是用户生成内容,必须做匿名化、脱敏处理。一个不合规的数据管道,工程做得再漂亮,都可能在一瞬间把整个项目拖入深渊。
数据到手的处理流程,我遵循"原样保存 -> 清洗 -> 加工 -> 版本记录"的顺序。原始数据永远不动,清洗脚本输出到processed目录,并且record每个清洗规则的名称和参数。这样当数据质量出现问题时,可以一路回溯到源头,而不是面对一堆已经变形的数据无从查起。
3.2 清洗规则的编写思路:不是堆正则
我刚入门时有个误区,以为清洗就是写各种正则以穷举所有脏数据。后来发现这完全是防御性的、被动的做法。好的清洗规则应该按这四步来:
1. 去重:消除完全重复和近似重复,结合标题/内容归一化 2. 去噪:删除无意义字符、HTML标签、过度截断文本 3. 格式统一:统一日期、数字、大小写,全半角转换 4. 结构性筛选:按任务长度、复杂度、关键词分布过滤真正需要用心的是第四步——结构性筛选。比如做问答对,不能只留短的,还得考虑样本覆盖不同难度层级。我有一个很直观的比喻:数据清洗像筛面粉,你的任务不是把面粉变成空气,而是筛出最合适烘焙的颗粒分布。堆100条万能正则,不如先理解自己需要什么样的数据分布。
3.3 一个人如何快速迭代标签体系
对于有监督任务,标注几乎是绕不开的。一个人怎么在有限时间标注出一套有质量的数据?我的实践是"规则预标注 + 人工抽取审核"。先用关键词、正则或一个强模型给出初标,每类标签抽取30~50条让人工核对,不对的就转头修改规则或词表。这样做一轮轮迭代,比纯手工逐条标注快得多,也更容易发现标签定义不清晰的地方。
下面是我标注表里常用的字段:
| 字段 | 示例 | 说明 |
|---|---|---|
| raw_text | "我想退款,物流太慢了" | 原始文本 |
| label | complaint | 主标签 |
| label_confidence | 0.92 | 预标注模型信心 |
| manual_review | accepted | 人工复核结果 |
| note | "情绪激烈,可作为困难样本" | 补充意见 |
这套字段让我能快速统计哪些样本需要更多人工关注。当某个标签的label_confidence普遍很低时,我一般不是想着换模型去提高信心,而是先去调整标签定义或补充标注指导。这比我逐条标注效率高得多,也更科学。
3.4 数据质量不如预期时,先看分布,再看内容
数据清洗和标注做完后,我会习惯性做一次数据体检。第一步看分布:每个标签的样本量有没有极端不均衡?文本长度是否符合模型上下文?第二步随机抽样看内容:有没有明显错误标注?有没有语义重复?我看到很多人拿到数据第一时间就训练,等模型效果差才回头怀疑数据,这一来一回至少浪费两三周。
后来我给自己定了一个硬性指标:在训练/评测之前,必须对数据集跑一次质量报告,包括样本总量、去重率、标签分布、平均长度、抽样错误率。抽样错误率超过5%就停下,先修数据,再走下一步。这套机制让我的项目很少在"数据锅"上翻车。
4. 模型选型与本地部署:从跑通到跑好的工程权衡
4.1 开源模型和托管API怎么选
数据管道打造完,终于到了大家最爱聊的"模型选型"。先开门见山给出我的对比原则:
| 对比维度 | 本地开源权重(Qwen、ChatGLM、DeepSeek、Llama) | 托管API服务 |
|---|---|---|
| 成本 | 一次性硬件成本,后续无单次调用费 | 按token付费,用多花多 |
| 数据隐私 | 全本地处理,敏感数据不出内网 | 数据经过第三方,需审核合规 |
| 可控性 | 可量化、可换base模型、可微调 | 黑盒更新,能力波动不自知 |
| 运维负担 | 需要自己管GPU、推理框架、监控 | 服务商负责,省心 |
| 上手难度 | 需要理解加载、量化、并发 | 一行API调用即可 |
对于没有强隐私要求、只是想快速验证的场景,托管API当然是最省事的。但如果你做的是公司业务流程、数据不能外传的任务,本地开源模型几乎是唯一选择。我目前的主力是Qwen系列开源模型,中文能力强、社区活跃、权重量级覆盖从0.5B到70B都有,很适合作为从零开始接触本地部署的模型。选型的一个重要原则是:根据你能拿到的GPU显存和要求的响应速度,倒推模型参数量,而不是反过来。
4.2 本地推理的第一道坎:显存与量化
很多人第一次在本地跑模型,被显存不足的报错劝退。这里我解释下最核心的量化概念。模型权重默认用FP16或BF16保存,一个数值占2字节。一个70亿参数的模型,光权重就需要约14GB显存;再加上激活、KV-Cache,实际占用会达到20GB以上。普通显卡根本扛不住。量化就是把浮点数精度降低,比如4bit下每个参数只占0.5字节,同样70亿参数的模型,权重仅约3.5GB,一张12GB显卡就能比较舒服地跑起来。
我用Qwen2.5-7B-Instruct举例,可以这样直接在本地启动一个OpenAI兼容服务:
ollama pull qwen2.5:7b-instruct-q4_K_M ollama create my-qwen -f Modelfile ollama serve另外也可以用vLLM来做高性能离线或在线推理,尤其是在并发较高的场景。vLLM启动命令如下:
vllm serve Qwen/Qwen2.5-7B-Instruct-GPTQ-Int4 \ --max-model-len 8192 \ --gpu-memory-utilization 0.9 \ --port 8000从零开始跑通之后,你会明显感觉本地部署的主动权在自己手里。显存不够时优先想到的就是继续降量化等级或切更小的模型,而不是急着堆硬件。
4.3 推理性能与并发控制:别让单次快代替整体吞吐
调好模型后,我踩过的一个大坑是只看"单次推理延迟"。单次450毫秒看似不错,但并发10个请求时,居然开始排队超时。原因是我没用服务器型推理框架,而是每来一个请求都加载一遍模型权重。后来切到vLLM,它通过Continuous Batching把多个请求放在同一批次里,吞吐量提升了4倍以上。
判断模型服务性能,我不会盯着第一个token延迟,而是关注并发QPS和平均每个request的端到端耗时。简单来说,稳定的批处理机制对线上服务很重要。为了保证服务稳定性,我在封装接口时还做了两件小事:一是设置合理的超时时间与最大并发数,二是给推理加队列缓冲。工程不是为了秀参数,而是让用户无论高峰低谷,体验都保持在可接受范围。
5. 从"能跑通"到"答得对":提示词工程与检索增强
5.1 提示词模板本身就是代码,也需要版本管理
如果说数据决定了模型天花板,提示词工程就是决定你在现实里能摸到多高天花板。很多教程教"提示词就是写几句话",那是把标准和例外混为一谈了。在工程链路上,提示词模板应该像代码一样被管理。
比如一个简单的问答系统,我会用Jinja2写模板:
[system] 你是一名严谨的AI助手。如果给定的知识库内容与问题无关,明确表示"知识库中未找到相关信息",不要编造。 [user] 问题:{{ question }} 知识片段: {% for chunk in search_results %} [{{ loop.index }}] {{ chunk.content }} {% endfor %} 请结合知识片段作答。注意几个细节:模板里区分了system和user;知识片段用编号罗列,方便模型引用;还有明确的"信息不足时不要编造"的指令。这些都会在实验记录里保存。提示词微小的改动会明显影响效果,所以我把提示词也纳入Git管理,并随模型版本一起发布。这样线上行为出了问题,我可以迅速判断是不是最近改了提示词导致的。
5.2 RAG是让我从"漂亮演示"走向"真实可用"的关键
大语言模型有两个天生缺陷:一是训练知识有截止时间,二是它会在不确定时一本正经地编造。解决这两个问题的常用方案就是RAG(Retrieval-Augmented Generation,检索增强生成)。简单理解:不直接让模型回答,而是先从知识库里检索相关片段,再把这些片段作为上下文给模型做参考。这样既能把知识实时更新到库里,又能有效降低幻觉发生的概率。
我的RAG实现路径是:先对文档切片,然后让embedding模型把切好的片段转成向量,存进向量库;用户提问时也转成向量,在库里做相似度检索,得到Top K片段后交给大模型生成。我用的是bge-m3作为embedding模型,中文效果稳,且对长文本支持很好。
确保检索效果的关键参数,我自己用的是:
| 参数 | 推荐 | 说明 |
|---|---|---|
| chunk_size | 300~500字符 | 太小丢失上下文,太大稀释相关性 |
| overlap | 30~80字符 | 避免切断关键句上下文 |
| top_k | 4~8 | 太少可能漏信息,太多容易让模型抓不住重点 |
| 检索阈值 | 0.3~0.5 | 低于阈值不返回结果,防止无关片段噪音 |
这种"外部知识库 + 向量检索 + 大模型生成"的组合,让我能把公司内部的制度文档、商品知识、历史工单一口气聚合起来,而不是靠重新训练模型。工程上省下了大量算力,业务上也获得了实时的知识更新能力。
5.3 上下文窗口再大,也不能什么都往里塞
现在很多模型支持长上下文,但塞得越长,生成速度越慢,而且模型容易在长文本里"迷失重点"。RAG检索出来top-10片段未必都要塞进提示词,我建议先做一次重排序。典型做法是用一个轻量模型或规则,把检索到的片段再打分,只保留与问题真正相关的4~5段。这一步能把最终答案质量提升不少。
另一个隐藏注意点是token预算。我写接口时会在内部计算当前请求的token数,超出模型上下文就直接截断策略返回提示,而不是让调用方等一个必然失败的请求。别等显存或服务报错才意识到过头了,工程上主动防御比被动止损舒服得多。
6. 建立评估体系:没有标准,优化就是空谈
6.1 先从100条Golden Set开始
模型服务搭起来后,很多人下一步就急着上线,但我在这个环节会强制自己停下来,先建一个Golden Set(黄金评测集)。Gold Set就是从真实业务场景里抽出的、有人工预期答案的测试样本集合。不一定多,起步100条就够,但每条必须经过人工校验,保证答案质量是可靠的。
我构建Golden Set的经验是:样本要覆盖常见场景、边界场景、模糊场景和主动拒答场景。比如客服问答里,除了常规问题,必须包含"上下文不足但用户期望回答"和"问题涉及隐私需婉拒"这两类。这样才能逼着模型暴露问题,而不是只在展示集上好看。
Golden Set不是一次性建完。每当线上出现Bad Case,我会把该Case加入评测集或更新样本,让评测集始终保持与真实场景同步。这样的评测集,才是优化迭代的锚点。
6.2 自动评测:定量指标与LLM-as-Judge结合
有了评测集,下一步是怎么评。如果做意图分类这类可枚举标签的任务,可以用精确率、召回率、F1这类传统指标。但到了开放式问答,开卷必考题不是唯一标准,答案的语义正确性和格式合规性就没法用字符串匹配来测了。
我常用的方案是LLM-as-Judge:用一个更强的模型(或者同一模型的更高版本)给回答打分。评分Prompt里会包含问题、标准答案、模型回答,以及明确的评分标准。为了避免单个打分模型产生偏差,我会先随机抽20条让两个不同模型打分,人工比较一致性。
很多人担心大模型打分不靠谱,我的建议是别指望一次就完美,先把打分维度拆成"信息正确性、覆盖度、简洁性、遵循指令程度",并逐条说明打分标准。这样得到的评分数值,虽然不能说绝对严谨,但足以发现版本间的相对差异,用于回归测试已经足够。
6.3 回归测试与Bad Case闭环
我通常每个模型或提示词更新后,都会跑一次Golden Set,然后生成一份评测对比表:
| 版本 | F1/打分 | 拒答准确率 | 幻觉率 | 平均延迟 |
|---|---|---|---|---|
| v1.0 baseline | 0.82 | 0.65 | 12% | 800ms |
| v1.1 + RAG | 0.87 | 0.80 | 5% | 1.2s |
| v1.2 + prompt优化 | 0.88 | 0.82 | 4% | 1.1s |
通过表格可以明显看到:增加RAG之后,整体正确率和拒答率都有提升,哪怕延迟变高一些也是非常值得的。与此同时,我维护一个Bad Case清单,每次迭代必须决定每个Bad Case是"修复了"、"接受了"还是"屏蔽了"。没有这个闭环,评测很容易变成形式主义——跑出了分数,问题原封不动。
这些动作表面上繁琐,但它保证了你每次优化都是向前走。我见过太多人凭感觉调Prompt,今天换几个词,明天换温度,结果效果原地打转。有了评估体系,你做的每个改动,事后都能说清楚到底是变好还是变坏。
7. 工程化最后一公里:API封装、日志、监控与持续迭代
7.1 FastAPI封装推理服务,三件小事别偷懒
到了上线阶段,推理效果再好也要通过API暴露给业务方。我用FastAPI封装,理由很简单:异步支持好、类型校验方便、自带交互式文档。封装时有三个细节我绝不能少:
第一,输入输出必须用Pydantic模型。请求结构变化时,调用方会立刻收到清晰的报错,而不是几百毫秒后某个字段神秘失踪。
第二,调用推理部分尽量用异步或放到线程池,并用run_in_executor包装。这样做防止阻塞事件循环,保证并发时其它请求还能正常响应。
第三,设置合理的超时与限流。没有超时的接口就是一颗定时炸弹,它会让用户等待无意义的时间,也会让进程积累大量挂起连接。
一个简化版的服务示例:
from fastapi import FastAPI from pydantic import BaseModel from src.model_serving import generate app = FastAPI() class ChatRequest(BaseModel): question: str top_k: int = 4 class ChatResponse(BaseModel): answer: str latency_ms: float @app.post("/chat", response_model=ChatResponse) async def chat(req: ChatRequest): import time start = time.time() answer = await generate(req.question, req.top_k) return ChatResponse(answer=answer, latency_ms=(time.time() - start) * 1000)端口打通后,先不急着接前端,而是用几组真实请求验证鉴权、超时、异常路径,确保你的服务不只是"能跑",而是"能抗"。
7.2 日志系统里应该记录什么,才不叫白记
线上AI服务的日志让很多人头疼:记太少了出问题无从查,记太多了又占空间、泄露隐私。我的日志字段加起来差不多就下面这些:
| 字段 | 作用 |
|---|---|
| 时间戳请求ID | 关联全链路 |
| 输入/输出摘要 | 定位语义层面问题 |
| 模型版本&提示词版本 | 快速区分回归原因 |
| Token用量 | 成本与限流优化依据 |
| 检索到的文档ID与得分 | 排查RAG召回错误 |
| 用户反馈标签 | 了解线上真实满意度 |
| 业务敏感标记 | 强制脱敏,合规审计 |
特别注意:输入输出日志直接落明文是大坑,尤其是涉及个人信息时。我在记录前会跑一个脱敏函数,把手机号、邮箱、姓名等替换成占位符。哪怕数据不出内网,也建议你开启这个习惯。没人希望第二天因为日志泄露隐私而出问题。
7.3 监控指标:不只盯延迟和QPS
服务上线后,亮眼的延迟和QPS确实让人安心,但我要提醒你:AI服务的核心监控不能只看性能指标,必须看效果指标。性能好只能说明系统没崩,不代表模型答得好。我的监控面板常放三块:
- 系统资源:GPU使用率、显存占用、推积请求数
- 性能指标:P95/P99延迟、QPS、超时率
- 效果指标:在线用户反馈量、辅助标注分布、耗材和拒答率
对于效果指标,我会设计一个轻量方案:每次回答后让用户一键点击"有用/没用",同时对一部分请求做自动规则检测(比如答案是否为空、是否包含"未找到信息"等)。当数据积累到一定程度,每天统计一次效果趋势,如果某天效果指标突然下滑,及时定位是模型更新、数据变化还是提示词改动造成的。
这套监控体系也许比一些大公司里的系统简陋,但对我来说已经构成完整的反馈闭环。AI工程不是静态交付,它是一条持续运营、每天都有可能调整的管道。有了日志、监控和用户反馈,我才敢说这个项目真正从开发阶段迈入了运营阶段。
如果只能留一个经验,我会说:从零开始做AI工程,最重要的事情不是某个高级模型,而是建立"系统可掌控"的信心。当环境可复现、数据可追溯、评估可量化、行为可监控时,你再换模型、加场景、扩团队都不会慌。那些看起来高大上的AI能力,本质上都是在这条扎实管道上长出来的枝叶。希望我这套从零起步的链路,能帮你少走几个月的弯路,把项目真正做成工程,而不是一台只能跑通一次的玩具。