前段时间我把自己的一个项目命名成ai-engineering-from-scratch本意是从零开始做AI工程结果一个朋友看到后问我这不是从零开始学AI的课程笔记吗我愣了一下发现这个误解其实很普遍。很多人以为 AI 工程是学会用 AI 工具或者看懂几篇模型论文但真正上手后才发现难点几乎全在engineering这个词上——稳定、可评估、可控、成本可接受地交付一个 AI 功能而不是调通一次对话。我身边不少朋友收藏了几十个 AI 工具网站、买了提示词模板、看了 Agent 架构图三个月后再问还是没能让一个 AI 接口稳定地跑在自己的业务里。问题不在他们不努力而在没人告诉他们这条路应该先走哪几步、每一步做到什么程度才算合格。所以我决定把从零到能交付一个 AI 功能的能力链路完整梳理一遍送给那些准备认真做 AI 工程而不是停留在玩 AI层面的读者。1. 先拆清楚AI工程和用AI工具根本是两回事在开始之前我花了很大力气才想明白这件事。ai-engineering-from-scratch里的 engineering 不是修饰词它是主语。AI 只是你手里的一个组件工程才是你要搭建的系统。1.1 我以为的起点和实际的起点差距很大我在立项初期翻了很多资料差点把自己的第一个里程碑定成读完整本深度学习花书理由是不懂模型原理怎么做AI工程。现在回头看这个想法差点把我劝退。做个类比你想开车通勤需要的是交规、路感和油门刹车的配合而不是先读完整本内燃机原理。真正的模型训练、损失函数、梯度下降那是造车的人关心的事。我们绝大多数人做的是开车调用成熟的模型能力把它接进业务流程里。这不是说底层原理毫无价值而是说它不该是起点。AI工程的起点是用起来不是造出来。1.2 AI工程要碰的四层东西按照我后来的实践一个完整的 AI 工程能力栈可以粗分成四层。每一层的目标和关注点完全不同层级核心问题典型动作模型层用什么模型能力边界在哪选型、对比测试、了解上下文长度和输出限制数据层给模型喂什么怎么喂上下文组装、知识库切分与检索、few-shot 样本流程层一次调用怎么变成一套流程重试、缓存、状态机、工具调用、Agent 编排系统层怎么保证能用且可维护评估集、成本监控、日志追踪、隐私保护我见过很多人把全部精力放在第一层天天换模型、比参数却忽略了后面三层。结果就是Demo 一时爽上线火葬场——单次调用看着挺聪明放进业务流程里就各种不听话。1.3 为什么不建议从算法论文开始from scratch 这个短语有迷惑性。它确实有从零开始的意思但在工程领域它更接近搭出一个完整可用的最小系统而不是把每个底层组件都亲手造一遍。我的建议是把论文阅读和源码研究放到跑通两个完整项目之后。那时候你对上下文窗口处理的是什么结构化输出卡在哪Agent 决策为什么不可控会有切肤之痛再看论文一眼就能抓住重点。反过来先啃论文再碰代码大概率会在第三周就放弃。2. 第一个项目应该有多小最小闭环跑起来再说如果让我给from scratch定义一个真正的第一步那就是在一个小时内用代码完成一次真实的模型调用并且让返回结果能被程序正确处理。2.1 用20行代码把一次对话打通我当时用的是 Python注册了服务商的开发者平台拿到 API Key 之后第一段代码大概长这样import os from openai import OpenAI client OpenAI( api_keyos.environ[API_KEY], base_urlos.environ.get(API_BASE_URL, None) ) response client.chat.completions.create( modelgpt-4o-mini, messages[ {role: system, content: 你是一个数据提取助手只输出JSON。}, {role: user, content: 提取下面这段客户反馈中的问题类型和紧急程度今天发货又延迟了客服也不回消息。} ], temperature0.2, ) print(response.choices[0].message.content)这段代码没什么高深的但它完成了三件大事第一你证明了 API 通路是通的第二你第一次用temperature这个参数控制了输出的确定性第三你第一次意识到模型返回的是一段文本不是一个 Python 对象——后面所有麻烦都从这里开始。2.2 理解token、计费和延迟这三个数字决定后续所有设计初次跑通之后我看了一眼账单和日志才意识到AI工程和普通API开发最不一样的地方每一次调用都在烧钱每一次输出都有延迟每一个字都占上下文空间。token 是模型处理文本的基本单位。英文单词大致一个词一两个 token中文通常一个汉字对应一到两个 token。所以上面那段 20 行的代码每次调用差不多消耗 100 到 200 个 token。成本公式很简单每次调用成本 输入token数 × 输入单价 输出token数 × 输出单价。这还没算重试和多次 Agent 循环的成本。延迟也一样输出 token 越多等待越久因为模型是一个词一个词蹦出来的。参数作用建议初值temperature控制随机性越低越确定0~0.3提取类任务用低值max_tokens限制输出长度按任务估算别给太大model模型能力和成本的权衡先用小模型跑通再按需升级2.3 把人话对话升级成程序接口跑通调用之后我做的第一件事不是加功能而是加把输出变成数据的代码。最简单的方式是要求模型输出 JSON然后在代码里json.loads解析解析失败就走重试或人工兜底import json content response.choices[0].message.content try: result json.loads(content) except json.JSONDecodeError: result {raw: content, error: parse_failed}这一步的价值在于你不再把模型当成聊天机器人而是当成一个不稳定但不笨的接口。从这一刻起你才开始做工程。3. Prompt不是靠灵感是把它当API去设计很多人以为提示工程就是把话说得漂亮一点。我一开始也这么觉得直到我在同一个任务上换了十几个说法效果忽上忽下才发现根本问题在于我没有把提示词当成一份需要严格维护的技术资产。3.1 为什么我从背模板改成拆字段网上能找到大量提示词模板什么扮演一个资深分析师、请一步一步思考。抄过来用时灵时不灵。后来我发现与其被模板牵着走不如把提示词当成一段有结构的配置拆成固定字段角色定位这个模型在这个任务里扮演什么角色任务描述它要完成的具体事情最好一句话说清输入内容用户提供的原始数据用分隔符包起来约束条件什么不能做比如不要编造不存在的订单号输出格式字段名、类型、是否必填示例一两个输入输出示例帮助模型对齐我在代码里通常这样组织提示词方便维护和版本管理[角色] 你是一个细心的客户反馈整理助手。 [任务] 从用户反馈中提取问题分类、紧急程度、关键诉求。 [约束] 只输出合法JSON不要输出任何其他文字。 如果信息缺失字段值为 null。 [输出格式] {category: string, urgency: high|medium|low, request: string} [示例] 输入东西收到了但少了一个配件。 输出{category: 缺件, urgency: medium, request: 补发配件}这种结构化写法的好处是出一版改一版你永远知道上一个大版本发生了什么变化。提示词从玄学变成了可迭代的配置。3.2 few-shot 和结构化输出的真正作用模板里那段示例就是 few-shot意思是给模型看几个例子告诉它你要什么。它的作用不是提供知识而是统一风格和格式。模型看到第一个例子是 JSON它更倾向继续输出 JSON。但 few-shot 不是越多越好。每加一个示例输入 token 就增加一段成本往上涨响应时间也拉长。我自己常用的量级是两到三个示例够让它对齐格式就行。同时我建议在代码层做双保险提示词里要求 JSON代码里仍然做解析兜底和字段校验。把提示词当成提高正确概率的手段而不是保证一定正确的承诺。3.3 一个具体的提示词版本迭代案例我做过一个从客户评价里提取三要素的小功能迭代过程大概是这样版本我改了什么结果v0.1一句话要求提取问题类型、紧急程度和诉求输出格式飘忽不定有时是散文v0.2加了只输出JSON格式稳定了一些但字段名不固定v0.3明确了字段名和类型格式基本稳定偶尔有多余文字v0.4加了两个示例准确率明显上升格式 95% 以上合规每一版改完我都拿同一批测试样本跑一边对比结果的正确率和格式合规率。没有数据支撑的感觉变好了都不算优化。3.4 提示工程的边界在哪提示词不是万能的。我遇到过三类问题靠提示词怎么改都没用一是任务本身超出模型知识边界它没有这个信息二是输出要求过于复杂字段多到模型容易漏三是矛盾要求既要求详细又要求简短。遇到这类情况别再死磕提示词了。要么把任务拆成更小的子步骤要么引入外部知识检索要么换更大的模型。提示词解决不了的问题工程结构可以解决。4. 把模型包进流程从一个调用到一个会工作的系统跑通单次调用之后下一步是把它变成不会突然挂掉的服务。这一章讲的全都是经典工程手段只不过服务对象从数据库变成了模型。4.1 模型应用也需要重试与缓存模型 API 不是 100% 可靠的超时、限流、临时 500 都很常见。而 AI 应用里重试还有个特殊意义同样的输入模型可能因为随机性返回不同结果。如果你的业务对一致性有要求重试策略就得小心有时候重试反而会引入新错误。我采用的最简方案import time from tenacity import retry, stop_after_attempt, wait_exponential retry(stopstop_after_attempt(3), waitwait_exponential(multiplier1, max10)) def call_llm(safe_params): ...缓存也一样。完全相同的请求同样的 prompt、同样的模型参数不应该重复烧钱。我在系统里加了一个简单的精确缓存以 prompt 和参数的哈希作为 key第一次请求落库后续命中直接返回。效果立竿见影实验阶段能省下三成以上的 token 费用。4.2 用数据而不是感觉调优建立你的第一个评估集这是我觉得整个ai-engineering-from-scratch过程中最重要的一步给 AI 功能建一套测试集。没有评估集你所有的优化都是在盲调。我当时的做法很朴素从真实请求里攒了 20 条样本每一条都手工标注了期望输出。然后写一个脚本把提示词版本跑在全部样本上统计正确率、格式合规率、失败原因分类。20 条不多但足够暴露大部分问题。失败类型常见原因应对思路格式错误提示词没说清 / 模型偶尔抽风加 few-shot、代码兜底解析信息遗漏任务步骤太多模型顾此失彼拆分子任务一次只做一件事幻觉补充模型编造了原文没有的信息明确禁止编造必要时换低 temperature有了这 20 条样本之后我再也不纠结这版提示词好不好直接跑一遍数据用结果说话。4.3 让 AI 进入你的代码function calling 的 JSON 契约如果说提示词是让模型说话function calling 就是让模型动作。它做的事情很简单定义一组函数告诉模型你可以调用这些函数模型在需要时返回一个结构化的调用请求由你的程序真正执行。典型的工具定义长这样{ type: function, function: { name: query_order_status, description: 根据订单号查询物流状态, parameters: { type: object, properties: { order_id: {type: string} }, required: [order_id] } } }流程上要做一次循环模型返回我想调用 query_order_status → 你的代码执行这个函数 → 把真实结果回传给模型 → 模型基于结果组织最终答案。这里有一个我踩过的重要教训永远让代码去执行函数而不是相信模型的任何直接输出。模型只负责决定调哪个函数、传什么参数真正读写数据库、发消息的必须是你的代码。4.4 多轮对话的记忆别把历史一股脑塞进去做对话类功能时最容易犯的错误是把所有历史消息都塞进上下文。上下文窗口是有限的而且成本随 token 数线性上涨历史太长还会把模型的注意力稀释掉。我试过三个方案按复杂度排序滑动窗口只保留最近五轮对话超出部分直接丢弃。实现最简单适合闲聊场景。摘要压缩每进行几轮先把前面的对话总结成一小段摘要塞进系统提示词。适合需要长期记忆但不需要逐字记忆的场景。外部检索把历史消息向量化存库每轮按相关性捞回最相关的几条。适合知识库类应用。从零起步的人我建议先做滑动窗口把链路跑通再逐步升级。别上来就上重型方案。5. Agent一层一层剥开自主的外壳Agent 是这两年最火的词也是最容易被包装成玄学的词。我一开始对 Agent 持怀疑态度觉得不过是循环调用模型的噱头。后来自己实现了一个最小的 Agent 循环才发现它的核心思想确实朴素但朴素不代表简单。5.1 为什么我一开始不信 Agent后来又真香早期我看到各种Agent 自主规划、自我进化的宣传第一反应是抗拒。直到我读到 ReAct 模式的描述Agent 思考Reasoning 行动Act的循环——模型根据当前状态决定下一步做什么调用一个工具观察结果再决定下一步。这个描述击中了我的痛点遇到分支不确定的任务时传统的硬编码流程根本写不全所有情况而 Agent 可以在运行时动态决定走哪条路。它不是玄学它是把决策从程序员手里转移到了模型手里。5.2 ReAct 循环的最小实现我用伪代码记录过这个循环核心逻辑非常简单状态 初始消息 工具列表 [查订单, 查库存, 发消息] for 步数 in range(最大步数): 决策 调用模型(状态, 工具列表) if 决策 输出最终答案: break 结果 执行工具(决策.工具名, 决策.参数) 状态 追加观察结果(状态, 结果)每一步里模型看当前状态决定调用哪个工具你的程序执行工具并返回结果模型再接着思考。这个循环之所以有用是因为模型能在现实反馈的基础上继续推理而不是凭空想象。5.3 让 Agent 可控的护栏设计Agent 最大的问题不是蠢而是太自由。让模型自己决定调用哪些工具意味着它可能调用你没打算让它碰的东西。我的护栏设计是四件套工具白名单模型只能调用你注册过的函数天然碰不到别的系统。最大循环轮数限制 5~10 轮防止出现死循环。成本上限每次执行记录 token 消耗超过阈值立即熔断。关键操作人工审批涉及删除、转账、发送消息这类高危动作模型只能生成申请由人来确认。这四条每一条都救过我。有一次 Agent 在调试数据时差点把测试库的表清空就是因为我在工具定义里没有限制写操作后来加了审批才兜住。5.4 从单 Agent 到串行工作流把任务拆成节点做完单 Agent 循环后我的下一个顿悟是大部分真实业务不需要一个什么都干的 Agent而是需要一条工作流。工作流的每个节点可能是固定逻辑也可能是模型调用。举个例子我做过一个资料整理流水线意图识别节点判断用户输入是查资料还是写摘要数据提取节点从原文抽出关键实体生成节点用提示词生成结构化结果校验节点检查 JSON 合法性、必填字段完整度这条流水线每一步都是确定的只有生成节点用到了模型。它的好处是每一步都可以单独调优、单独测试、单独替换。能用工作流解决的问题我绝不用自由 Agent 解决。6. 真正拉开差距的工程细节评估、成本、可观测性跑通 Demo 的人很多能把 AI 功能稳定上线的人很少。差距不在提示词技巧而在三个容易被忽视的工程细节你有没有测试集、你知不知道钱花在哪、你能不能事后复盘。6.1 评估集给 AI 应用建一套自己的测试通用 benchmark 测的是模型能力不是你的业务效果。你的业务有自己的特殊性你的客户评价长什么样、你的订单编号规则是什么、你的用户喜欢用什么词——这些必须靠自有评估集。我的建议是从 20 条开始逐步扩充到 100 条以上。每条样本包含输入原始文本期望输出手工标注允许的偏差范围比如字段值同义是否算对评估方式可以分层先做规则校验JSON 合法、字段存在、枚举值正确再做模型打分用一个强模型当裁判对比输出和期望的语义一致性最后人工抽检。三层跑下来你对这版改动是变好还是变坏就有了数。6.2 成本控制token 在哪里飞走的就在哪里抓回来我一开始根本不看账单直到一个月底发现费用超出预期三倍。逐条翻日志后问题集中在三个地方系统提示词写得过长一段 1000 字的角色设定每次请求都带着再乘以调用量就是一大笔钱。每轮对话都携带完整历史聊到十轮的时候十轮的历史成了最贵的输入。无差别的模型选型简单分类任务也用了最大模型纯属浪费。我的调优动作包括系统提示词精简到 200 字以内、历史改用摘要压缩、简单任务用小模型、引入语义缓存。这三个动作加起来月度成本降了接近一半。6.3 日志与可观测当 AI 出错时你要能事后复盘模型应用的调试和传统后端不一样。传统后端报错有堆栈、有固定逻辑模型应用报错经常是它没报错但答错了。这时候如果没有日志你连它是怎么错的都不知道。我给每次请求都分配了一个request_id在日志里记录字段说明request_id一次完整任务的唯一标识model用了哪个模型、哪个版本prompt_hash提示词内容的哈希便于对比版本tokens输入/输出 token 数latency耗时raw_output模型原始返回需脱敏有了这些日志一次线上事故就能完整回溯找到request_id→ 看链路里每一步的输入输出 → 定位是哪一步的提示词或上下文设计导致了错误。我还养成了一个习惯把每次错误但没报异常的案例单独存一份。这些案例是教程里学不到的是你自己的专属训练数据。7. 如果只允许我说一句话的经验我把这个项目从头走完一遍之后最想分享的经验是先跑通最小闭环再谈优化。很多人卡住的真正原因不是不懂原理而是迟迟不肯写那 20 行代码、建那 20 条测试样本。AI 工程的门槛从不在起点而在能不能持续迭代。最后再分享一个我现在一直在用的习惯每次模型输出和预期不符我不着急改提示词而是先把错误案例记下来。攒够十个错误案例之后集中分析一次你会发现错误模式非常清晰——有的输入就是格式难解析有的任务就是需要拆成两步有的场景就是需要换模型。这些一手数据比任何提示词模板都值钱。从零开始做 AI 工程真正难的不是 AI是工程两个字里包含的耐心和系统性。希望这篇拆解能帮你少走几步弯路。