1. 从零搭建AI工程能力为什么我劝你别一上来就调包这两年AI应用开发的门槛被各种框架和API拉得极低三行代码调用一个大模型接口就能做出一个看起来像模像样的Demo。但我带过不少新人也面试过很多号称“做过AI项目”的候选人发现一个很普遍的现象模型能跑通但一遇到线上并发、上下文管理、成本控制、效果评估这些工程问题就完全不知道从哪里下手。ai-engineering-from-scratch这个标题恰好戳中了这个痛点——它强调的不是“从零训练一个大模型”而是“从零建立AI工程化的能力体系”。我自己是从传统后端转过来的最早做AI应用时也走过弯路以为把Prompt写好就万事大吉结果上线后请求延迟飙到十几秒Token消耗像流水一样用户反馈答非所问还找不到原因。后来花了很长时间补工程侧的课才慢慢把整个链路理顺。这篇文章就是把我踩过的坑、验证过的方案、以及目前团队在用的工程实践完整地拆解一遍。适合两类人看一是刚入行AI应用开发、想建立系统认知的工程师二是有后端基础、想转型AI工程方向的开发者。全文会围绕“从零搭建”这个核心讲清楚每个环节为什么这么做、具体怎么做、以及做的时候容易在哪里翻车。2. 整体架构设计先想清楚AI工程到底在工程什么2.1 从“能跑”到“能扛”的思维转变很多人对AI工程的理解停留在“调API写Prompt”这其实只是冰山一角。一个真正能上线的AI应用背后至少包含六个工程模块请求接入与路由、上下文与记忆管理、模型调用与降级、输出解析与校验、效果评估与监控、成本与性能优化。这六个模块缺一个系统就只能在Demo阶段打转。我习惯用一个类比来解释把大模型当成一个能力很强但状态不稳定的远程同事。你不能每次找他办事都从头把背景讲一遍也不能指望他每次回答都格式规范更不能在他忙不过来的时候干等着。AI工程要做的就是给这位“远程同事”配一套完整的协作机制——记忆本、任务模板、备用联系人、质检员和工时统计。ai-engineering-from-scratch的核心就是把这套机制从零搭起来。为什么强调“从零”因为直接套用现成框架虽然快但一旦出问题你根本不知道是哪一层导致的。我见过团队用LangChain搭了复杂链路结果一个简单的超时问题排查了两天最后发现是框架内部某层重试逻辑和业务重试叠加了。从零搭建不是拒绝框架而是先理解每一层的职责边界再决定哪些用轮子、哪些自己造。2.2 分层架构的选型逻辑与取舍我推荐的架构分层是这样的从下往上依次是模型接入层统一封装不同厂商的模型接口屏蔽差异提供一致的调用契约。上下文管理层负责对话历史、知识片段、系统指令的组装与裁剪。编排调度层处理多步推理、工具调用、条件分支等复杂流程。输出处理层做结构化解析、格式校验、敏感内容过滤。评估监控层记录每次调用的输入输出、耗时、Token消耗、质量打分。应用接口层对外提供业务API处理鉴权、限流、缓存。这个分层不是拍脑袋定的而是根据“变更频率”来划分的。模型接入层变更最频繁因为厂商接口和模型版本一直在变应用接口层相对稳定因为业务契约一旦定了就不该轻易改。把易变的和稳定的隔离开改底层不会影响上层业务这是软件工程的老原则在AI系统里同样适用。注意不要一上来就追求“全链路可观测”。我见过小团队花两周搭了一套完整的监控大盘结果业务逻辑还没跑通。正确的顺序是先让主流程闭环再逐步加监控埋点。2.3 技术栈选择的几个关键决策具体到技术选型我的建议是模块推荐方案选择理由避坑提示语言Python TypeScriptPython生态最全TS做前端和BFF层类型安全不要用Python写高并发网关模型接入自封装SDK 官方SDK兜底自封装可控官方SDK保证新特性别直接裸调HTTP重试和超时难统一上下文存储Redis 向量库Redis存会话向量库存知识向量库别选太重的小规模用PG向量扩展就够编排代码编排优先框架为辅代码可调试、可测试复杂流程别硬写该用状态机就用评估规则模型双轨规则快且便宜模型补语义别只依赖模型打分会有偏见这个表格里的每一行背后都有具体的教训。比如“别直接裸调HTTP”是因为早期我们每个模型调用都手写requests结果超时时间不统一、重试策略各写各的后来统一封装成客户端问题少了一大半。再比如“向量库别选太重的”是因为我们曾经为了一个几千条知识的小项目部署了完整的Milvus集群运维成本远超收益后来换成PostgreSQL的向量扩展简单又够用。3. 核心模块拆解每个环节到底该怎么写3.1 模型接入层统一契约是稳定性的基石模型接入层看起来简单不就是发个请求吗但实际做起来坑非常多。不同厂商的接口在参数命名、返回结构、错误码、流式输出格式上都不一样。如果不做统一封装业务代码里会散落大量if-else判断维护成本极高。我的做法是定义一个ModelClient抽象类核心方法只有两个chat()和stream_chat()。所有厂商的实现都继承这个类对外暴露完全一致的入参和出参。入参里最关键的是messages列表和options字典出参统一成{content, usage, finish_reason, raw}结构。class ModelClient: def chat(self, messages: list, options: dict) - dict: raise NotImplementedError def stream_chat(self, messages: list, options: dict): raise NotImplementedError class VendorAClient(ModelClient): def chat(self, messages, options): # 参数映射把统一options转成厂商A的参数 params self._map_options(options) resp self._call_api(messages, params) return self._normalize_response(resp)这里的关键是_map_options和_normalize_response两个方法。参数映射负责把统一的temperature、max_tokens、top_p等转成各厂商的字段名响应归一化负责把不同结构的返回统一成标准格式。这样上层业务永远只面对一种数据结构换模型时只需要新增一个Client实现业务代码零改动。实操心得超时和重试一定要在Client层统一处理。我的配置是连接超时3秒、读取超时30秒、最多重试2次且只对5xx和超时重试4xx直接抛错。重试要加指数退避避免雪崩。3.2 上下文管理Token预算比Prompt技巧更重要上下文管理是AI工程里最容易被低估的环节。很多人把精力花在雕琢Prompt上却忽略了上下文窗口是有限资源。一个对话轮次多了之后历史消息会迅速吃满Token导致要么报错、要么被迫截断丢失关键信息。我的方案是“三层上下文”结构系统层固定的角色设定和输出规范每次请求都带但内容精简。摘要层对较早的对话历史做滚动摘要用模型压缩成一段话。近期层保留最近N轮完整对话N根据Token预算动态调整。具体实现时我会先算一个Token预算模型最大上下文减去系统层和预留输出空间剩下的给摘要层和近期层。然后从最近的消息往前累加直到接近预算上限再往前的内容触发摘要生成。摘要不是每轮都做而是当被挤出的消息累积到一定数量时批量做一次减少调用次数。def build_context(session, budget): system_tokens count_tokens(system_prompt) remaining budget - system_tokens - output_reserve recent, to_summarize [], [] for msg in reversed(session.messages): if count_tokens(msg) sum(count_tokens(m) for m in recent) remaining * 0.7: recent.insert(0, msg) else: to_summarize.insert(0, msg) summary get_or_create_summary(to_summarize) return [system_prompt] ([summary] if summary else []) recent这个逻辑里0.7这个系数是留缓冲的因为Token计数本身有误差而且模型输出长度不可控。我实测下来留30%缓冲能有效避免“刚好卡线”导致的截断问题。3.3 输出解析与校验别相信模型每次都听话即使你在Prompt里明确要求“返回JSON格式”模型仍有概率返回带Markdown代码块包裹的内容或者字段名拼错、类型不对。如果直接把模型输出丢给下游业务线上事故是迟早的事。我的处理流程是三步走提取、解析、校验。提取阶段用正则把可能的JSON片段从文本里抠出来兼容json包裹和纯文本两种情况。解析阶段用标准JSON解析器失败则触发一次“修复重试”——把错误信息和原始输出一起发给模型让它重新生成。校验阶段用Pydantic或JSON Schema做结构和类型检查不通过同样触发重试。def parse_output(raw_text, schema): json_str extract_json(raw_text) try: data json.loads(json_str) except json.JSONDecodeError: return retry_with_fix(raw_text, JSON解析失败) try: return schema(**data) except ValidationError as e: return retry_with_fix(raw_text, str(e))重试次数我一般限制在1次因为两次都失败说明Prompt或模型能力有问题继续重试只是浪费Token。这时候应该降级到兜底逻辑比如返回默认值或转人工。注意修复重试的Prompt要包含原始输出和具体错误不要只说“请重新生成”那样模型不知道错在哪。我通常会把校验错误的前两条贴进去效果最好。3.4 评估与监控没有度量就没有优化AI应用最麻烦的地方在于“效果”很难量化。传统接口测试断言返回码和字段就行但AI输出是自然语言对错边界模糊。我的做法是建立一套“规则模型人工”的三级评估体系。规则层负责可自动化的硬指标格式是否合法、是否包含敏感词、响应时间是否超标、Token消耗是否异常。这一层每次请求都跑成本极低。模型层负责语义质量打分用另一个模型对输出做相关性、完整性、准确性的1-5分评估抽样跑比如10%的请求。人工层负责定期抽检和badcase标注用来校准模型评估的准确性。监控看板我重点关注四个指标P95延迟、Token消耗趋势、解析失败率、重试率。这四个指标任何一个异常波动都意味着系统某处出了问题。比如解析失败率突然上升可能是模型版本更新导致输出格式变了Token消耗趋势上涨可能是上下文管理没生效。4. 实操落地从零到一搭建一个可用的AI服务4.1 环境准备与项目骨架假设我们要搭建一个“智能客服问答”服务支持多轮对话和知识库检索。项目骨架我习惯这样组织ai-service/ clients/ # 模型接入层 base.py vendor_a.py context/ # 上下文管理 builder.py summarizer.py pipeline/ # 编排调度 chat_pipeline.py output/ # 输出处理 parser.py validator.py eval/ # 评估监控 metrics.py sampler.py api/ # 应用接口 routes.py config/ settings.py依赖方面核心就几个fastapi做接口、redis做会话存储、pydantic做校验、tiktoken做Token计数。向量检索如果知识量不大直接用pgvector省去独立向量库的运维。配置管理我用pydantic-settings所有模型密钥、超时参数、预算阈值都走环境变量不硬编码。这样本地开发和线上部署用同一套代码只换配置。4.2 核心链路实现一次对话请求的完整旅程一次用户请求进来会经过这些步骤鉴权与限流校验API Key按用户维度限流防止单用户打爆。会话加载从Redis取出该用户的历史消息和摘要。知识检索如果问题涉及知识库用向量检索召回Top-K片段。上下文组装按三层结构拼装system、summary、recent和知识片段。模型调用走统一Client带超时和重试。输出解析提取、解析、校验失败则修复重试。会话更新把本轮问答追加到历史必要时触发摘要。指标上报记录耗时、Token、解析状态等。async def chat_handler(user_id, query): session await load_session(user_id) knowledge await retrieve_knowledge(query, top_k3) context build_context(session, knowledge, budget8000) raw await model_client.chat(context, options{temperature: 0.3}) result parse_output(raw[content], AnswerSchema) await update_session(session, query, result.content) report_metrics(user_id, raw[usage], result.status) return result这个链路里temperature设0.3是因为客服场景需要稳定、少发挥。如果是创意场景可以调高。top_k3是实测下来召回质量和上下文长度的平衡点太多会挤占对话历史空间。4.3 参数调优几个关键数字的来龙去脉工程落地时参数不是拍脑袋定的每个都有依据超时30秒大模型生成200字左右通常5-10秒留3倍余量应对高峰。重试2次第一次重试覆盖偶发网络抖动第二次覆盖服务端瞬时过载再多收益递减。上下文预算8000模型上限假设16K留一半给输出和缓冲实际输入控制在8K内。摘要触发阈值被挤出消息累计超过5轮或2000Token时触发避免频繁调用摘要模型。评估抽样10%全量评估成本太高10%抽样在统计上已能反映整体质量趋势。这些数字不是固定的需要根据实际模型和业务调整。但调整时要有依据比如发现P95延迟接近超时线就该考虑是优化Prompt缩短输出还是提高超时阈值。实操心得把所有这些参数集中在一个配置文件里改参数不用翻代码。我们团队的做法是每个参数旁边写一行注释说明依据新人接手时能快速理解为什么是这个值。5. 常见问题与排查技巧实录5.1 典型问题速查表现象可能原因排查方向解决方案响应突然变慢模型服务端限流/上下文过长看P95延迟和Token数降级到小模型或裁剪上下文解析失败率上升模型版本更新/输出格式漂移对比近期原始输出更新Prompt或解析规则Token消耗异常上下文未裁剪/摘要失效检查会话长度分布修复摘要触发逻辑回答答非所问检索召回不准/Prompt冲突看召回片段和最终Prompt调整检索阈值或Prompt优先级偶发超时网络抖动/重试叠加看超时和重试日志统一重试策略加退避5.2 几个我踩过的深坑坑一重试风暴。早期我们在Client层和业务层都写了重试结果一次超时触发了9次调用3x3直接把配额打满。后来规定重试只能在Client层做一次业务层不再重试问题解决。坑二摘要丢失关键信息。摘要模型如果Prompt没写好会把用户的关键约束比如“我要退订”压缩掉导致后续对话跑偏。后来我们在摘要Prompt里明确要求保留“用户意图、关键实体、未解决问题”三类信息效果好很多。坑三向量检索的相似度阈值。一开始不设阈值什么片段都召回结果模型被无关信息干扰。后来加了0.75的相似度下限低于这个值就不注入知识让模型基于自身知识回答准确率反而提升。坑四流式输出的解析。流式返回时JSON是分片到达的不能每片都解析。正确做法是累积到完整再解析或者用增量解析器。我们一开始每片都尝试解析导致大量误报错误。5.3 独家避坑技巧给模型输出加“指纹”在Prompt里要求输出以特定标记开头和结尾解析时先找标记能大幅提升提取准确率。保留原始输出每次调用的原始输入输出都存一份排查问题时这是最宝贵的证据别只存解析后的结果。灰度切换模型换模型时先切5%流量对比评估指标没问题再全量避免直接翻车。设置成本熔断当日Token消耗超过预算阈值时自动降级到小模型或返回兜底话术防止账单失控。6. 效果评估与持续迭代上线只是开始6.1 建立可量化的质量基线上线后第一件事是建立质量基线。我会跑一批标准测试集记录当前版本的准确率、格式合规率、平均延迟、平均Token消耗。这个基线是后续所有优化的参照系。没有基线你根本不知道一次改动是变好了还是变坏了。测试集要覆盖典型场景和边界场景正常问答、多轮追问、知识库命中、知识库未命中、敏感问题、超长输入。每个场景至少20条人工标注期望输出或评分标准。这个工作前期投入大但一次投入长期受益。6.2 基于badcase的迭代闭环日常运营中我会每周抽一批badcase做归因分析。归因分类大概这几类检索问题、Prompt问题、模型能力问题、解析问题、业务逻辑问题。不同类别对应不同解法检索问题调整分块策略、相似度阈值、召回数量。Prompt问题补充示例、明确约束、调整优先级。模型能力问题换更强模型或拆解任务。解析问题完善提取规则或校验逻辑。业务逻辑问题修代码。关键是形成闭环发现问题、归因、修复、回归测试、上线验证。没有闭环badcase永远是badcase。6.3 成本与性能的持续平衡AI应用的成本主要是Token费用而Token消耗和上下文长度、输出长度、调用次数直接相关。优化方向有三个减少不必要调用、压缩上下文、控制输出长度。减少调用方面能缓存的就缓存比如相同问题的回答在短时间内可以复用。压缩上下文方面摘要和裁剪是主要手段。控制输出方面在Prompt里明确要求简洁或者设置max_tokens上限。性能方面流式输出能显著提升用户感知速度即使总耗时不变首字到达时间从5秒降到1秒体验完全不同。另外把知识检索和模型调用并行化也能省下几百毫秒。最后分享一个小技巧给每个请求打上trace_id把检索、模型调用、解析各阶段的耗时都记下来。当用户反馈“慢”的时候你能立刻定位是哪个阶段的问题而不是盲目猜测。这个习惯让我排查性能问题的效率至少提升了一倍。