1. 从零搭建AI工程能力:为什么我劝你别一上来就调包
这两年AI应用开发的门槛肉眼可见地降低了,随便拉个框架、调个API就能跑出一个能对话的Demo。但我带过的新人里,十个有八个卡在同一个地方:Demo跑通了,一上真实业务就崩。要么是响应慢得离谱,要么是成本失控,要么是数据一多就各种诡异报错。问题的根子不在模型本身,而在于大多数人跳过了“AI工程”这一层,直接站在了应用层。
ai-engineering-from-scratch这个标题,说的就是把这层被跳过的能力补回来。它不是教你从零训练一个大模型,那既不现实也没必要;它讲的是从零构建一套能支撑AI应用稳定运行的工程体系——数据怎么流转、推理怎么调度、成本怎么控制、效果怎么评估、线上怎么排障。这套东西,才是把“能跑的Demo”变成“能赚钱的产品”之间的那道坎。
这篇文章适合三类人:一是刚转行做AI应用、只会调API的开发者;二是有后端经验但没接触过AI系统、想补齐工程认知的工程师;三是带团队做AI产品、需要一套可落地工程规范的技术负责人。我会按真实项目里的推进顺序,把每个环节的选型逻辑、参数计算、踩坑经验都摊开讲,你能直接抄作业,也能理解每一步为什么这么做。
2. 整体架构设计:先想清楚数据怎么流,再动手写代码
2.1 为什么“先跑通再说”是最大的坑
我见过太多项目,第一周就把模型接进业务代码,第二周开始加功能,第三周发现要换模型,结果发现模型调用散落在几十个文件里,改一处漏三处。这就是典型的没有架构设计直接开干。AI工程和传统后端最大的区别在于:模型是不稳定的。同一个输入,不同版本、不同参数、不同负载下,输出可能完全不同。传统后端你写个函数,输入A永远输出B;AI系统里,输入A可能输出B、C、D,取决于温度参数、上下文长度、甚至GPU的显存碎片。
所以架构设计的第一原则是:把模型当成一个“不可靠的外部依赖”来对待。就像你不会把数据库查询逻辑散落在业务代码里一样,模型调用也必须收敛到统一的抽象层。这个抽象层要负责:请求格式化、参数注入、重试与降级、结果解析、日志记录、成本统计。业务代码只跟这个抽象层打交道,换模型、调参数、加缓存,都只改这一层。
2.2 分层架构的四个核心模块
我推荐的分层是这样的,从上到下依次是:
- 接入层:处理用户请求、鉴权、限流、请求路由。这一层不碰模型,只做流量治理。
- 编排层:决定一次请求要经过哪些步骤。比如先查缓存、再检索知识库、再调模型、最后做后处理。这一层是业务逻辑的核心,但依然不直接调模型。
- 模型抽象层:统一封装所有模型调用。不管是本地部署的还是远程API,都通过同一套接口访问。这一层负责重试、降级、超时控制、成本记录。
- 基础设施层:向量数据库、缓存、消息队列、监控告警。这些是支撑上面三层跑起来的地基。
这么分的好处是,每一层可以独立演进。比如你想从远程API切换到本地部署的小模型,只改模型抽象层的实现,编排层和接入层完全不用动。再比如你想加一个“先查缓存”的逻辑,只在编排层加一个步骤,不影响其他部分。
2.3 一个具体的请求生命周期
拿一个典型的“智能客服问答”场景来说,一次请求的完整路径是这样的:
- 用户提问进入接入层,鉴权通过后生成一个请求ID,写入日志。
- 编排层收到请求,先拿问题去缓存里查有没有现成答案。缓存key是问题的语义哈希,不是字符串精确匹配。
- 缓存没命中,编排层调用检索模块,从向量数据库里找出最相关的几段知识。
- 编排层把问题、检索结果、系统提示词组装成一个完整的prompt,交给模型抽象层。
- 模型抽象层根据当前配置选择模型(比如优先用便宜的小模型,失败再切大模型),发起调用,记录耗时和token消耗。
- 模型返回结果,编排层做后处理(比如格式化、敏感词过滤),再写回缓存。
- 结果返回给用户,同时把这次请求的完整链路信息写入监控系统。
这个流程里,每一步都有工程决策。比如缓存为什么用语义哈希而不是字符串匹配?因为用户问“怎么退款”和“退款流程是什么”字符串不同但语义相同,字符串匹配命中率极低。语义哈希需要把问题先转成向量再算相似度,这又涉及到向量模型的选择和阈值设定。这些细节,后面会逐个展开。
3. 模型抽象层:把不稳定的模型关进笼子里
3.1 统一接口设计的三个关键决策
模型抽象层的核心是定义一套统一的接口,让上层不用关心底层是哪个模型。这套接口至少要包含这几个方法:
class ModelProvider: def generate(self, prompt: str, **kwargs) -> ModelResponse: """生成文本,返回统一格式的响应""" pass def generate_stream(self, prompt: str, **kwargs) -> Iterator[str]: """流式生成,用于需要逐字返回的场景""" pass def count_tokens(self, text: str) -> int: """计算token数,用于成本预估和上下文管理""" pass def health_check(self) -> bool: """健康检查,用于故障转移""" pass这里有几个设计决策值得展开。第一,为什么要有count_tokens?因为成本控制和上下文窗口管理都依赖它。不同模型的token计算方式不同,必须在抽象层统一。第二,为什么要有health_check?因为线上模型服务可能因为各种原因不可用,抽象层需要能感知并自动切换。第三,generate_stream和generate分开,是因为流式和非流式的错误处理、超时逻辑完全不同,混在一起会很难维护。
3.2 重试与降级的参数计算
模型调用失败是常态,不是异常。网络抖动、限流、服务过载都会导致失败。重试策略不能拍脑袋定,要有计算依据。
假设单次调用成功率是99%,那么连续失败3次的概率是0.01³=0.000001,也就是百万分之一。但重试会增加延迟,如果单次调用平均耗时2秒,重试3次最坏情况就是6秒。所以重试次数和超时时间要配合设定:
- 单次超时:根据P99延迟设定,比如P99是3秒,超时设5秒。
- 最大重试次数:2次(总共3次尝试),覆盖绝大多数瞬时故障。
- 重试间隔:指数退避,第一次等0.5秒,第二次等1秒,避免加重服务端压力。
- 总超时:单次超时×最大尝试次数+退避时间总和,比如5×3+1.5=16.5秒,超过这个时间直接降级。
降级策略也要提前设计。比如主模型不可用时,切到备用模型;备用模型也不可用时,返回缓存中的相似答案;连缓存都没有,返回一个友好的兜底话术。降级不是失败,是保证系统始终有响应。
3.3 成本控制的实操方法
成本失控是AI应用最常见的死法。我见过一个项目,上线第一周账单就超了预算的十倍,原因是没有做token限制,用户输入超长文本直接透传给模型。
成本控制要在抽象层做三件事:
第一,输入长度截断。每个模型都有上下文窗口限制,比如8K、32K、128K。但你不能等到超了才截断,要在调用前就检查。截断策略不是简单砍掉尾部,而是优先保留系统提示词和最近几轮对话,中间的历史对话做摘要压缩。
第二,输出长度限制。通过max_tokens参数强制限制输出长度。这个值根据业务场景定,比如客服问答一般200token足够,内容生成可能需要1000token。设太大浪费钱,设太小回答不完整。
第三,成本实时统计。每次调用后记录token消耗和对应费用,按请求ID、用户ID、业务模块三个维度聚合。这样你能清楚知道钱花在哪里,哪个功能最烧钱,哪个用户最费token。
提示:成本统计不要只记总数,要记明细。我吃过亏,月底发现账单暴涨,但因为没有按模块拆分,查了一天才定位到是一个测试接口忘了关。
4. 数据流转与检索增强:让模型用上你的私有数据
4.1 为什么检索增强是AI工程的必修课
模型本身的知识是训练时固化的,它不知道你公司的产品文档、不知道昨天的会议纪要、不知道刚上线的功能。检索增强生成(RAG)就是解决这个问题的标准方案:先把相关知识检索出来,再连同问题一起交给模型生成答案。
但RAG不是“把文档塞进向量库就完事”。我见过太多RAG项目效果差,问题都出在数据流转的细节上。文档怎么切分、向量怎么生成、检索怎么排序、结果怎么组装,每一步都有讲究。
4.2 文档切分的参数选择
文档切分是RAG的第一步,也是最容易被忽视的一步。切分粒度太粗,检索出来的内容包含大量无关信息,浪费token还干扰模型;切分太细,单段信息不完整,模型拼不出完整答案。
我的经验值是:中文文档按300-500字切分,英文按200-300词切分。这个范围是基于两个考虑:一是大多数嵌入模型的最佳输入长度在256-512token之间,超出部分会被截断;二是模型生成答案时,3-5段相关内容的token总量在1500-2500之间,加上问题和系统提示词,刚好在常见模型的舒适区内。
切分时还要注意重叠。相邻两段之间保留10%-20%的重叠内容,避免一个完整句子被切断后,两段都丢失关键信息。比如一段500字的文档,切分成两段各300字,中间重叠100字。
def split_document(text, chunk_size=400, overlap=80): chunks = [] start = 0 while start < len(text): end = start + chunk_size chunk = text[start:end] chunks.append(chunk) start = end - overlap return chunks这个简单的函数就是切分的核心逻辑。实际项目中还要处理标题、列表、表格等结构化内容,但基本原理不变。
4.3 向量检索的阈值设定
向量检索返回的是相似度分数,你需要设定一个阈值来决定哪些结果算“相关”。阈值设太高,召回不足,模型没有足够信息;设太低,引入噪声,模型被误导。
阈值不能拍脑袋,要用真实数据测。方法是:准备一批问题和对应的标准答案,对每个问题检索出top-10结果,人工标注哪些是真正相关的。然后看相关结果和不相关结果的分数分布,取一个能最大化区分两者的值。通常这个值在0.7-0.85之间(余弦相似度)。
如果检索结果分数普遍偏低,说明嵌入模型不适合你的领域,需要换模型或者做微调。如果分数普遍偏高但质量差,说明文档切分有问题,需要调整切分策略。
4.4 检索结果的组装策略
检索出相关文档后,怎么组装进prompt也有讲究。我的做法是按相关性排序,取top-3到top-5,每段前面加一个来源标记,比如[文档1]、[文档2]。这样模型在生成答案时可以引用来源,也方便后续做溯源。
组装格式大致如下:
你是一个客服助手,请根据以下参考资料回答用户问题。 如果参考资料中没有相关信息,请如实告知,不要编造。 参考资料: [文档1] 退款流程:用户可以在订单页面点击“申请退款”... [文档2] 退款时效:退款申请提交后,1-3个工作日到账... 用户问题:退款要多久才能到账?这个格式的关键是明确告诉模型“参考资料是什么”和“没有相关信息时怎么办”。很多RAG项目效果差,就是因为没有明确这两点,模型要么忽略参考资料自己编,要么强行从无关资料里找答案。
5. 效果评估与线上排障:没有度量就没有优化
5.1 离线评估的四个核心指标
AI应用的效果评估比传统软件难,因为输出是自然语言,没有精确的“对错”。但也不是完全没法量化,我常用的四个指标是:
- 相关性:回答是否切题。用另一个模型来打分,1-5分,取平均。
- 忠实度:回答是否基于给定资料,没有编造。同样用模型打分。
- 完整性:回答是否覆盖了问题的所有方面。人工抽样评估。
- 格式合规率:回答是否符合预期格式(比如JSON、特定字段)。这个可以程序自动检查。
这四个指标里,忠实度最重要。一个回答再流畅、再完整,如果是编造的,就是有害的。我通常把忠实度低于4分的case全部拉出来人工复核,找出是检索问题还是生成问题。
5.2 线上监控的关键指标
离线评估通过不代表线上没问题。线上环境有真实用户的千奇百怪的输入,有并发压力,有网络波动。必须监控这些指标:
| 指标 | 含义 | 告警阈值 |
|---|---|---|
| P99延迟 | 99%请求的响应时间 | 超过5秒 |
| 错误率 | 调用失败的比例 | 超过1% |
| 降级率 | 触发降级的比例 | 超过5% |
| 平均token消耗 | 每次请求的token数 | 突增50% |
| 缓存命中率 | 缓存命中的比例 | 低于30% |
这些指标要按分钟粒度采集,按小时聚合。突增突降都要告警,因为往往意味着上游出了问题。
5.3 常见问题排查速查表
线上出问题时,快速定位比什么都重要。我整理了一份速查表,覆盖了80%的常见故障:
| 现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 响应突然变慢 | 模型服务过载 | 看模型端P99延迟 | 切备用模型或限流 |
| 回答质量下降 | 检索结果变差 | 看检索分数分布 | 检查向量库是否更新 |
| 成本突增 | 输入变长 | 看token消耗分布 | 加输入截断 |
| 错误率上升 | 网络抖动 | 看错误类型分布 | 加重试或切线路 |
| 缓存命中率下降 | 问题分布变化 | 看缓存key分布 | 调整缓存策略 |
这张表是我踩了无数坑总结出来的,基本覆盖了日常运维会遇到的问题。建议打印出来贴在工位上。
5.4 一个真实的排障案例
有一次线上突然大量用户反馈“回答不相关”。我先看监控,发现检索分数正常,模型调用正常,延迟正常。然后抽样看了几条具体请求,发现检索出来的文档确实相关,但模型生成时完全忽略了文档内容。
进一步排查发现,当天上午有人更新了系统提示词,把“请根据参考资料回答”改成了“请回答用户问题”。少了“根据参考资料”这几个字,模型就放飞自我了。改回来之后问题立刻消失。
这个案例的教训是:提示词的任何改动都要走变更流程,不能随手改。提示词就是AI应用的代码,改代码要测试,改提示词也要测试。
6. 从零到一的推进节奏:我的实操时间线
6.1 第一周:搭骨架,不碰模型
第一周的目标是把分层架构搭起来,用mock数据跑通全流程。接入层、编排层、模型抽象层、基础设施层,每层都写最简实现。模型抽象层先用一个假的provider,固定返回“这是测试回答”。
这一周不碰真实模型,是为了把工程骨架先立住。很多人反过来,先接模型跑通,再补架构,结果补架构时发现到处都要改,成本极高。先搭骨架再填肉,是更稳妥的路径。
6.2 第二周:接模型,跑通单条链路
第二周接入真实模型,但只跑通一条最简单的链路:用户提问→直接调模型→返回结果。不加检索、不加缓存、不加降级。目的是验证模型抽象层的接口设计是否合理,重试、超时、日志是否正常工作。
这一周会暴露很多接口设计问题。比如你发现generate方法需要支持传入图片,但接口里没有这个参数;或者你发现流式和非流式的错误处理逻辑差异太大,需要拆成两个方法。这些问题在只有一条链路时改起来很快,等链路多了再改就麻烦了。
6.3 第三周:加检索,调效果
第三周加入检索增强。先把文档灌进向量库,跑通检索→组装→生成的完整链路。然后开始调效果:调整切分粒度、调整检索阈值、调整组装格式。这一周要反复做离线评估,用真实问题测试,看相关性、忠实度、完整性三个指标的变化。
调效果是个体力活,没有捷径。我的经验是每次只改一个变量,改完测一批问题,记录指标变化。同时改多个变量,你根本不知道是哪个起了作用。
6.4 第四周:加缓存和降级,准备上线
第四周加入缓存和降级,做压力测试。缓存要测命中率和一致性,降级要测触发条件和恢复逻辑。压力测试用真实流量的1.5倍打,看P99延迟、错误率、降级率是否在可接受范围内。
这一周还要把监控告警配好。没有监控就上线,等于闭着眼睛开车。至少要有延迟、错误率、成本三个维度的告警。
6.5 上线后的持续迭代
上线不是终点,是起点。上线后每周看一次效果指标和成本报表,每月做一次全量离线评估。发现效果下降就排查原因,发现成本上升就优化调用策略。AI应用的效果会随着用户输入分布的变化而漂移,不持续迭代就会慢慢变差。
提示:上线第一个月一定要每天看监控。我见过太多项目上线第一周没人管,第二周发现账单爆了、效果崩了,再救就来不及了。
7. 几个我踩过的坑和对应的解法
7.1 坑一:向量库更新导致检索结果突变
有一次我们更新了产品文档,重新灌入向量库。结果更新后检索效果突然变差,很多之前能答对的问题答错了。排查发现,新文档的切分方式和旧文档不一致,导致向量分布变了,原来的检索阈值不再适用。
解法:向量库更新必须走完整流程——切分、嵌入、灌库、评估。评估不通过不能上线。而且新旧文档的切分策略要一致,不能这次按300字切,下次按500字切。
7.2 坑二:缓存key设计不当导致答案错乱
早期我们用问题的MD5作为缓存key,结果发现“怎么退款”和“如何退款”是两个不同的key,缓存命中率极低。后来改成语义哈希,命中率上去了,但出现了新问题:语义相近但意图相反的问题(比如“怎么开通”和“怎么关闭”)被当成同一个key,返回了错误答案。
解法:语义哈希的相似度阈值要设得足够高,比如0.95以上才认为是同一个问题。同时缓存里要存原始问题,命中后做一次二次确认,确保意图一致。
7.3 坑三:降级策略过于激进导致用户困惑
有一次主模型服务抖动,降级策略触发,所有请求都返回了缓存中的“相似答案”。但很多问题在缓存里没有足够相似的答案,返回的内容答非所问。用户以为系统坏了,投诉量暴涨。
解法:降级要分级。一级降级切备用模型,二级降级返回缓存答案但加提示“当前为缓存结果,可能不准确”,三级降级才返回兜底话术。不要一上来就返回兜底,那等于直接告诉用户“我坏了”。
7.4 坑四:提示词版本管理缺失导致回滚困难
前面提到的提示词改动导致效果下降的案例,根本原因是提示词没有版本管理。改了就改了,没有记录,出问题不知道改了什么,也没法回滚。
解法:提示词必须纳入版本控制,每次改动记录改动人、改动内容、改动原因、评估结果。上线前用离线评估跑一遍,通过才发布。发布后监控效果指标,下降就回滚。
8. 写给想认真做AI工程的人
ai-engineering-from-scratch这个方向,核心不是学某个框架或某个模型,而是建立一套工程思维:把模型当不可靠依赖、把数据流当一等公民、把评估当持续过程、把成本当硬约束。这套思维建立起来之后,具体用什么框架、什么模型,都是可以替换的细节。
我个人的体会是,AI工程最难的不是技术,是克制。克制住“先跑通再说”的冲动,克制住“加个功能试试”的欲望,克制住“提示词随手改改”的随意。把工程规范立起来,把评估闭环建起来,把监控告警配起来,剩下的就是持续迭代。这个过程不性感,但能让你在别人Demo崩掉的时候,系统还稳稳跑着。
最后分享一个我一直在用的小技巧:每次上线新功能前,先问自己三个问题——效果怎么度量?成本怎么控制?出问题怎么回滚?三个问题都有答案了再动手。这三个问题帮我省下了至少三次重大事故。