1. 从零搭建AI工程能力,为什么大多数人卡在第一步就放弃了
如果你最近在技术社区里频繁看到“ai-engineering-from-scratch”这个说法,不用怀疑,它不是什么新出的框架或者工具库,而是一种学习路径的统称——从零开始,把AI工程化落地所需的整套能力一点点搭起来。我身边有不少朋友,有做后端的、做前端的、甚至做产品经理的,都在问同一个问题:我想搞AI应用,但不想只停留在调API的层面,到底该从哪里下手?
这个问题的答案,其实取决于你怎么定义“从零”。很多人以为从零就是打开一个在线教程,跟着敲一遍pip install,然后跑通一个demo就算入门了。但真正做过项目的人都知道,跑通demo和能交付一个稳定可用的AI功能之间,隔着一整条工程化的鸿沟。模型选型、数据管道、推理服务、成本控制、效果评估、版本迭代——这些东西没有任何一个教程会一次性讲清楚,因为它们本身就是交叉的。
我写这篇东西的目的很直接:把“ai-engineering-from-scratch”这个路径拆开,告诉你每一层需要什么、为什么需要、以及我在实际项目中踩过的那些坑。适合的读者是那些有一定编程基础、想系统性地建立AI工程能力、但又不希望被各种营销概念带偏的人。不管你是想转行做AI应用开发,还是想在现有岗位上增加AI相关的交付能力,这套思路都能直接参考。
需要提前说明的是,AI工程不等于模型训练。绝大多数业务场景下,你不需要自己训练模型,你需要的是把已有的模型能力可靠地集成到系统里,并且让它持续稳定地产生价值。这个认知差,是很多人走弯路的根源。
2. 先搞清楚AI工程到底包含哪些层,别一上来就啃论文
2.1 把AI工程拆成四层来看,心里就有地图了
我习惯把AI工程能力分成四层:基础层、模型层、服务层、应用层。这个分法不是教科书上的标准分类,而是从实际交付角度倒推出来的,每一层对应不同的技能栈和关注点。
基础层是地基,包括Python工程能力、数据处理、API调用、基本的系统设计。这一层不过关,后面全是空中楼阁。我见过太多人直接跳到模型层,结果连一个异步请求都写不明白,服务一上量就崩。
模型层不是让你去训练大模型,而是理解模型的能力边界、输入输出格式、推理参数的含义。比如temperature和top_p到底怎么影响输出,token是怎么计算的,上下文窗口超了会怎样。这些东西不需要你懂Transformer的数学推导,但必须懂它的行为特征。
服务层是把模型能力封装成可调用的服务。这里涉及API设计、并发处理、缓存策略、错误重试、限流降级。这一层是AI工程和普通后端开发重叠最多的部分,也是最能体现工程功底的地方。
应用层是最终面向业务的部分,包括Prompt设计、结果后处理、效果评估、用户反馈闭环。这一层看起来简单,实际上最考验对业务的理解。
2.2 为什么我不建议一上来就学模型训练
很多人一提到AI工程,第一反应是“我得先学机器学习”。这个想法在五年前是对的,但现在情况变了。绝大多数业务场景用的是预训练好的模型,通过API或者本地部署的方式调用。你需要的不是梯度下降的推导能力,而是理解模型能做什么、不能做什么、怎么让它稳定输出你想要的结果。
我举个例子。假设你要做一个合同关键信息提取的功能。自己训练一个NER模型,你需要标注数据、选模型架构、调参、部署、维护,周期至少几个月。而用现成的模型能力加上合理的Prompt设计,可能一周就能跑通,效果还更好。这不是说模型训练不重要,而是说对于“从零搭建AI工程能力”这个目标来说,优先级应该放在工程化能力上,而不是算法研究上。
当然,如果你所在团队确实需要微调模型来解决特定问题,那另当别论。但那是进阶话题,不是起点。
2.3 一张表看清四层各自的核心技能和常见误区
| 层级 | 核心技能 | 常见误区 |
|---|---|---|
| 基础层 | Python异步编程、HTTP协议、JSON处理、环境管理 | 用同步思维写AI调用,忽略超时和重试 |
| 模型层 | 模型能力评估、Token计算、参数调优、上下文管理 | 把模型当黑盒,不测试边界情况 |
| 服务层 | API设计、并发控制、缓存、监控告警 | 没有降级方案,模型挂了整个服务不可用 |
| 应用层 | Prompt工程、结果校验、效果评估、反馈闭环 | 只看单次输出,不做系统性评估 |
这张表建议你保存下来,每学一块就对照一下自己是不是真的掌握了。我当初就是吃了亏,服务层没做好,模型一超时整个请求链路全堵死,排查了一整天才找到问题。
3. 基础层怎么打:Python工程能力比算法知识更紧迫
3.1 异步编程是AI应用的第一道门槛
AI应用的典型特征是高延迟、高并发、IO密集。你调用一次模型接口,可能等两三秒才返回。如果用同步的方式写,一个请求占一个线程,并发量稍微上来线程池就满了。所以异步编程是必须掌握的。
Python的asyncio和aiohttp是基础中的基础。我建议你从写一个简单的异步请求封装开始,把超时、重试、错误处理都加进去。下面是我常用的一个模板,你可以直接参考:
import asyncio import aiohttp from typing import Optional async def call_model_api( session: aiohttp.ClientSession, payload: dict, timeout: int = 30, max_retries: int = 3 ) -> Optional[dict]: for attempt in range(max_retries): try: async with session.post( "https://api.example.com/v1/chat", json=payload, timeout=aiohttp.ClientTimeout(total=timeout) ) as resp: if resp.status == 200: return await resp.json() elif resp.status == 429: await asyncio.sleep(2 ** attempt) continue else: resp.raise_for_status() except asyncio.TimeoutError: if attempt == max_retries - 1: raise await asyncio.sleep(1) return None这段代码看起来简单,但包含了几个关键设计:指数退避重试、超时控制、状态码分类处理。我见过很多项目直接用requests同步调用,上线后并发一高就各种超时,排查半天发现是线程池不够用。
3.2 环境管理和依赖隔离,别等到冲突了才后悔
Python的依赖管理是个老生常谈的问题,但在AI项目里尤其突出。因为AI相关的库更新快、依赖多、版本兼容性差。我强烈建议用poetry或者uv来管理依赖,不要用全局的pip install。
另外,模型相关的依赖和业务依赖最好分开。比如你把torch和fastapi装在一个环境里,镜像体积会大到离谱,部署的时候拉镜像都要等半天。我的做法是:模型推理单独一个服务,业务逻辑单独一个服务,通过HTTP或者消息队列通信。这样不仅依赖清晰,扩缩容也灵活。
3.3 日志和可观测性,从第一天就要做
AI应用有个特点:出问题的时候很难复现。同样的输入,模型可能返回不同的结果。所以日志必须记录完整的请求和响应,包括Prompt、参数、返回内容、耗时。我一般会在日志里记录这几个字段:
request_id:全链路追踪prompt_hash:方便去重和统计model_name和params:定位模型版本和参数latency_ms:监控性能token_usage:成本核算
这些字段看起来多,但真出问题的时候,少一个你就要花几倍的时间去排查。我吃过这个亏,有一次线上返回异常,但日志里只记了结果没记Prompt,完全不知道用户输入了什么,最后只能靠猜。
4. 模型层的关键认知:把模型当组件,不是当魔法
4.1 理解Token机制,才能控制成本和延迟
Token是模型处理文本的基本单位。英文大概一个单词对应一个多Token,中文一个字可能对应一到两个Token。这个机制直接影响两件事:成本和延迟。
成本方面,API调用通常按Token计费,输入和输出分别计价。如果你不注意控制Prompt长度,成本会迅速膨胀。我做过一个统计,把系统Prompt从500 Token压缩到200 Token,每月成本直接降了三分之一。
延迟方面,输出Token数越多,生成时间越长。如果你只需要一个分类结果,不要让模型输出一整段解释。用max_tokens参数限制输出长度,同时在Prompt里明确要求简短回答。
提示:不同模型对Token的计算方式略有差异,建议在项目初期就用实际数据测一下,建立自己的Token估算表。
4.2 参数调优不是玄学,有明确的取舍逻辑
模型推理参数里最常调的是temperature、top_p、max_tokens、presence_penalty和frequency_penalty。很多人调参数靠感觉,其实每个参数都有明确的作用方向。
temperature控制随机性。值越低输出越确定,适合分类、提取这类需要稳定结果的任务。值越高输出越多样,适合创意生成。我一般做信息提取时用0到0.3,做文案生成时用0.7到1.0。
top_p是另一种控制随机性的方式,通常和temperature二选一调。它的逻辑是只从累积概率前p的Token里采样。值越小输出越保守。
presence_penalty和frequency_penalty用来抑制重复。前者惩罚已经出现过的Token,后者按出现频率惩罚。做长文本生成时适当加一点,能有效减少车轱辘话。
我的经验是:先固定一组默认参数,然后在实际数据上做A/B测试,用效果指标说话,不要凭感觉调。
4.3 上下文窗口管理,超长输入的处理策略
每个模型都有上下文窗口限制,比如4K、8K、32K、128K Token。超过限制会直接报错。处理长文本有几种常见策略:
- 截断:只取前N个Token,简单但会丢信息
- 分段处理:把长文本切成多段分别处理,再合并结果
- 摘要压缩:先用模型把长文本压缩成摘要,再基于摘要做后续处理
- 检索增强:把长文本存到向量库,按需检索相关片段
这几种策略没有绝对优劣,取决于你的场景。做合同审查适合分段加合并,做知识问答适合检索增强。我一般会先评估信息密度,如果关键信息集中在局部,检索增强最划算;如果全文都重要,分段处理更稳妥。
5. 服务层才是分水岭:让AI能力稳定可用的工程手段
5.1 API设计的几个关键决策
把模型能力封装成API的时候,有几个设计决策会直接影响后续的维护成本。
第一个是同步还是异步。如果模型推理时间在几秒内,同步接口可以接受。但如果可能超过10秒,建议用异步任务模式:提交任务返回task_id,客户端轮询或者通过Webhook获取结果。这样避免连接超时,也方便做任务队列和优先级控制。
第二个是批量还是单条。有些场景适合批量处理,比如离线数据标注。批量接口能显著提高吞吐量,但要注意单次批量不要太大,否则容易超时。我一般控制在10到20条一批。
第三个是版本管理。模型会更新,Prompt会迭代,接口要能兼容不同版本。我的做法是在请求里加一个version字段,服务端根据版本路由到不同的处理逻辑。这样新老版本可以并行运行,逐步迁移。
5.2 缓存策略:省钱和提速的双刃剑
AI调用的成本不低,缓存是最直接的优化手段。但缓存有个关键问题:同样的输入,模型可能返回不同结果。所以缓存策略要分场景。
对于确定性任务,比如信息提取、分类,如果参数固定,结果基本稳定,可以放心缓存。缓存键用输入文本的哈希加上参数组合。
对于生成式任务,比如文案创作,每次结果都应该不同,缓存意义不大。但可以考虑缓存中间结果,比如检索到的文档片段。
我一般会用两级缓存:本地内存缓存热点请求,Redis缓存全量结果。本地缓存用LRU策略,设置合理的过期时间。这里有个坑:如果模型版本更新了,缓存必须失效,否则会返回旧结果。所以缓存键里一定要包含模型版本号。
5.3 限流、降级和熔断,别等故障了才想起来
AI服务依赖外部API,网络抖动、服务限流、额度耗尽都是可能发生的。如果没有降级方案,整个功能就不可用了。
限流方面,我一般会在客户端和服务端都做。客户端控制并发数,服务端按用户或按接口限流。用令牌桶或者滑动窗口算法都可以,Python里slowapi或者自己实现都不复杂。
降级方面,准备一个兜底逻辑。比如模型调用失败时,返回缓存结果或者默认值,而不是直接报错。对于非核心功能,甚至可以暂时关闭,保证主流程可用。
熔断方面,当错误率超过阈值时,自动切断对模型服务的调用,过一段时间再试探性恢复。这个用pybreaker之类的库就能实现。
注意:降级逻辑一定要在测试环境验证过,否则真出故障的时候,降级代码本身可能有bug,那就雪上加霜了。
5.4 监控告警:没有度量就没有改进
AI服务的监控和传统服务略有不同,除了QPS、延迟、错误率这些常规指标,还要关注模型特有的指标:
- Token消耗速率:突然飙升可能是被刷了或者Prompt有bug
- 输出长度分布:异常变长或变短都可能是模型行为变化
- 拒绝率:模型拒绝回答的比例,过高说明Prompt需要调整
- 缓存命中率:太低说明缓存策略有问题
告警阈值要根据历史数据来定,不要拍脑袋。我一般会观察一周的基线,然后设置3倍标准差作为告警线。告警通道用邮件加即时消息,确保能及时响应。
6. 应用层的实战细节:Prompt设计、结果校验和效果评估
6.1 Prompt设计的核心原则:清晰、具体、有约束
Prompt设计不是写作文,不需要华丽的辞藻。核心原则就三条:指令清晰、格式具体、边界明确。
指令清晰是指告诉模型做什么,而不是让它猜。比如“提取合同中的甲方名称”比“分析这个合同”要清晰得多。
格式具体是指明确输出格式。用JSON就用JSON Schema描述清楚,用列表就说明每项包含什么。我一般会在Prompt里给一个输出示例,模型模仿能力很强,有示例的情况下格式准确率会高很多。
边界明确是指告诉模型遇到不确定的情况怎么处理。比如“如果找不到相关信息,返回null,不要编造”。这一条能大幅减少幻觉。
我常用的Prompt结构是这样的:
角色:你是一个合同信息提取助手。 任务:从以下合同文本中提取甲方名称、乙方名称、合同金额、签署日期。 输出格式:JSON,包含字段 party_a, party_b, amount, sign_date。 约束: - 如果某个字段无法确定,值设为 null - 金额只保留数字,单位元 - 日期格式为 YYYY-MM-DD 合同文本: {contract_text}这个结构看起来简单,但比随便写一句“帮我提取合同信息”的效果要好得多。
6.2 结果校验:模型输出不可全信
模型输出必须经过校验才能进入业务流程。校验分两层:格式校验和内容校验。
格式校验用JSON Schema或者Pydantic做,确保字段齐全、类型正确。这一步能拦截大部分低级错误。
内容校验更复杂一些。比如提取的金额是否在合理范围内,日期是否合法,名称是否在已知列表里。这些规则需要结合业务来定。我一般会写一个校验函数,对每个字段做规则检查,不通过的标记出来人工复核。
还有一个实用技巧:让模型自己给出置信度。在Prompt里要求模型对每个提取结果标注高、中、低置信度,低置信度的结果优先人工检查。这样能大幅提高人工复核的效率。
6.3 效果评估:建立可量化的评估体系
没有评估就没有优化。AI应用的效果评估需要一套指标体系,不能只看“感觉准不准”。
我一般会从三个维度评估:
- 准确率:提取类任务看字段级准确率,生成类任务看人工评分
- 一致性:同样输入多次调用,结果是否稳定
- 覆盖率:能自动处理的比例,需要人工介入的比例
评估数据集要提前准备,覆盖各种边界情况。我一般会准备至少100条测试数据,包含正常案例、边界案例和异常案例。每次Prompt调整或者模型切换,都跑一遍评估集,对比指标变化。
这里有个经验:评估集要持续更新。线上发现的新case要补充进去,否则评估集和实际分布会越来越脱节。
6.4 反馈闭环:让系统越用越好
AI应用上线不是终点,而是起点。用户反馈是优化的重要来源。我一般会在产品里加一个简单的反馈入口,让用户标记结果是否正确。这些标记数据积累起来,就是宝贵的优化素材。
反馈数据可以用来做几件事:一是补充评估集,二是分析错误模式,三是作为微调的种子数据。即使不做微调,分析错误模式也能帮你发现Prompt的盲区。
我做过一个项目,上线初期准确率只有70%左右,通过分析用户反馈发现主要是日期格式和金额单位的问题。调整Prompt后准确率提升到90%以上。这个过程没有改模型,纯粹是工程优化。
7. 我踩过的几个典型坑和对应的解法
7.1 超时设置不合理导致雪崩
项目初期,我把模型调用的超时设成了60秒,觉得留足时间更稳妥。结果有一次模型服务响应变慢,大量请求堆积,线程池被占满,整个服务不可用。
后来我把超时改成动态的:根据历史P99延迟设置,一般比P99多50%的余量。同时加了熔断机制,错误率超过阈值直接快速失败,不再等待。这样即使模型服务出问题,也不会拖垮整个系统。
7.2 Prompt里放了太多示例导致Token爆炸
为了让模型输出更准确,我在Prompt里放了大量示例。结果Token消耗飙升,成本翻了好几倍,而且延迟也增加了。
后来我做了精简:只保留最典型的两个示例,其他用规则约束代替。效果没有明显下降,但Token消耗降了60%。这个经验告诉我,Prompt优化要在效果和成本之间找平衡,不是越多越好。
7.3 忽略模型版本更新导致效果波动
有一次模型服务商悄悄更新了模型版本,输出风格发生了变化,导致下游的解析逻辑大量报错。因为没有版本锁定机制,排查了很久才发现是模型变了。
从那以后,我在请求里固定模型版本号,服务商更新时先在小流量上测试,确认兼容后再全量切换。同时解析逻辑也做了兼容处理,对格式变化有一定的容错能力。
7.4 没有做输入长度检查导致报错
用户输入超长文本时,直接调用模型会报上下文超限错误。早期没有做检查,错误直接抛给用户,体验很差。
后来在入口处加了长度检查,超长文本自动走分段处理流程。同时在Prompt里也加了说明,让模型知道输入可能被截断,尽量基于已有信息回答。
8. 从能跑到好用,中间隔着持续迭代
“ai-engineering-from-scratch”这个路径,说到底是一个从理解基本概念到建立完整工程能力的过程。基础层让你能写出可靠的代码,模型层让你理解模型的行为特征,服务层让你能交付稳定的服务,应用层让你能持续优化效果。这四层缺一不可,但优先级应该是从下往上的。
我在实际项目中的体会是,大部分问题不是模型能力不够,而是工程没做到位。超时没设好、缓存没做对、降级没准备、评估没体系——这些看起来不酷的东西,恰恰决定了AI功能能不能真正用起来。
如果你正在走这条路,我的建议是:不要追求一步到位,先跑通一个最小闭环,然后针对每个环节逐步优化。每优化一个点,就记录下前后的指标变化。积累下来,你不仅有了一个可用的系统,还有了一套自己的工程方法论。这比任何教程都值钱。