
一年前我在GitHub上建了一个名为ai-engineering-from-scratch的仓库本意只是整理学习笔记后来发现它慢慢变成了我完整走通一个AI工程项目的见证。这个项目从零开始一点一点长成了一整套可复用的AI工程体系需求拆解、提示词设计、Agent开发、自动评估、生产部署、成本监控全都有。过程中踩过的坑、推翻的代码、重新思考的架构比任何教程都值钱。如果你也处于想入门又不知道从哪下手的状态或者已经会调API但一上线就崩这篇文章就是写给你们的。我会从一个空目录开始讲清楚把AI应用变成成熟产品的每一步。1. AI工程和AI研究根本不是一回事先纠正几个常见误解1.1 研究管找上限工程管守住下限很多朋友一听到AI工程就以为要读论文、复现模型。我最初也有这个误解结果把大量时间花在看Transformer源码、看微调教程上最后发现这些在业务项目里几乎用不上。AI研究解决的是模型还能做多难的事而AI工程解决的是模型做的每件事是否稳定、可控、可维护、成本是否可接受。举个最直白的例子研究阶段模型十次里有一次生成了精彩答案就能发论文但到了工程阶段十次里有一次跑偏用户就会流失系统就会被判定为不可用。工程的核心就是守住下限——让随机性被约束在业务可接受范围内。ai-engineering-from-scratch这个项目给我的最大教训就是先分清研究性玩法和工程性做法。1.2 AI工程的能力地图如果把AI工程能力拆开我看大致有六块第一是需求工程把模糊的做一个智能助手拆成可验收的功能点第二是数据与提示词工程包括上下文构造、示例编写、输出格式控制第三是Agent工程涉及工具调用、状态管理、多步规划第四是评估工程建设测试集、指标、回归门禁第五是部署运维把模型推理变成高可用的在线服务第六是成本治理从Token消耗到算力利用率。这六块不是线性的而是一个循环。我在仓库里画了一张能力循环图后面用文字描述需求 - 设计 - 评估 - 部署 - 监控 - 再回到需求。没有哪一块可以跳过只要跳过上线后就会以更痛的方式补课。1.3 为什么从零开始比看教程更有效市面上90%的AI教程都在教你调用API教你跑通一个Demo。但Demo和产品之间隔着一整套工程基础设施。ai-engineering-from-scratch的项目名就是要从零开始不用任何框架模板先手动实现一个最简链路然后再引入工具来优化。这样你才知道哪个环节省掉了什么。比如一开始我直接用requests调用模型接口没有封装任何服务。虽然代码很丑但恰恰是这份丑让我理解了超时、重试、限流、JSON解析失败这些问题从哪来。之后再用LangChain、LangSmith等框架才真正理解它们解决的是什么。从零开始不是要你重复造轮子而是让你获得判断车轮子是否合格的能力。2. 从空仓库到可运行项目我的最小工程骨架与选型2.1 技术选型不追求时髦只看场景当时我手里的条件很简单一个人、一台带GPU的本地开发机、若干大模型API额度。技术选型上我最终定了Python 3.11 FastAPI Pydantic pytest。语言用Python没有悬念AI生态最全写起来快。Web框架选了FastAPI因为它自带Pydantic校验和OpenAPI文档对AI应用这种既要快速开发又要严格入参校验的场景很合适。模型侧没有直接私有化部署大模型而是接国内几家主流APIDeepSeek、通义、Kimi都跑过。原因也很直白个人项目先验证业务逻辑不需要承担GPU运维成本。等真实用户量上来再考虑推理优化。这个决策帮我省了至少一个月的时间。2.2 项目目录怎么组织让所有东西都有归属我见过很多AI项目代码全堆在main.py里prompt写在字符串里测试没有目录上了生产根本不敢动。所以ai-engineering-from-scratch从一开始就定了清晰的结构。ai-engineering-from-scratch/ ├── app/ │ ├── api/ # FastAPI路由 │ ├── core/ # 配置、依赖、全局异常 │ ├── services/ # 业务逻辑调用LLM │ ├── agents/ # Agent定义与工具注册 │ ├── prompts/ # 提示词模板按版本管理 │ └── utils/ # 输出解析、重试、缓存 ├── tests/ │ ├── unit/ # 纯逻辑单测 │ ├── integration/ # 调用真实API或mock网络 │ └── eval/ # 评估集与评估脚本 ├── data/ # 本地数据样本 ├── scripts/ # 构建、启动、评估脚本 ├── deploy/ # docker、nginx、systemd配置 └── pyproject.toml这个结构不是凭空想的。prompts/单独建目录是因为提示词会频繁改动如果混在代码里每次改一个标点都要动业务代码容易引起事故。tests/eval/独立是因为评估不是简单断言它需要独立的语料和指标脚本。2.3 第一版封装调用LLM也能写出工程感第一版代码我写了一个非常薄的LLM客户端重点不是功能多而是统一处理三件事APIKey管理、超时重试、JSON解析。# app/services/llm.py import json import time from typing import Any import requests from tenacity import retry, stop_after_attempt, wait_exponential class LLMClient: def __init__(self, api_key: str, base_url: str, model: str): self.api_key api_key self.base_url base_url self.model model retry(stopstop_after_attempt(3), waitwait_exponential(multiplier1, max10)) def chat(self, messages: list[dict], temperature: float 0.2) - str: resp requests.post( f{self.base_url}/chat/completions, headers{Authorization: fBearer {self.api_key}}, json{model: self.model, messages: messages, temperature: temperature}, timeout30, ) resp.raise_for_status() return resp.json()[choices][0][message][content] def chat_json(self, messages: list[dict]) - dict[str, Any]: content self.chat(messages) # 优先提取 json 代码块防止模型输出多余文字 if json in content: content content.split(json)[1].split()[0] return json.loads(content)这段代码看起来简单但包含了三层经验第一用tenacity实现指数退避重试LLM接口在高峰期经常503不重试就等着被用户投诉第二chat_json里先提取Markdown代码块因为很多模型即使你让它返回纯JSON它也会忍不住加一段解释第三把所有超时统一为30秒避免某些请求无限挂起拖垮进程。3. 提示词工程的落地姿势接口、版本、校验一起上3.1 把Prompt当成函数签名来设计我早期写提示词就依托ChatGPT网页复制粘贴效果好坏全靠运气。后来我意识到提示词的本质是一个不稳定的函数入口。工程化思路就是要让这个入口稳定、可预测、可度量。我的做法是给每个任务定义一个结构化Prompt模板包含5个部分角色定义、任务目标、约束条件、输入内容、输出要求。输出要求里必须明确只输出JSON对象不要解释并且给出一个具体的JSON示例。比如做关键词提取时模板长这样你是一个关键词提取引擎。 请从用户的输入文本中提取最多5个关键短语。 约束不要输出任何解释文字。 输出格式为JSON数组例如 [人工智能, 工程实践, 大模型] 用户输入 {{input}}注意这里我特意给了few-shot示例。模型对只看要求的理解远不如看一个完整例子来得准确。这个经验在工程里非常重要——少样本示例few-shot是提升输出稳定性的性价比之王。3.2 JSON Schema校验别让模型输出毁了下游只让模型输出JSON还不够。比如提取关键词的任务业务要求返回数组模型某次返回了数组某次返回了字符串。为了挡住这种不确定性我用Pydantic定义了输出模型并在解析后强制校验。# app/schemas/keyword.py from pydantic import BaseModel, Field, ValidationError class KeywordResult(BaseModel): keywords: list[str] Field(..., min_length1, max_length5) def parse_keywords(content: str) - KeywordResult: data json.loads(content) return KeywordResult(**data)这个校验的意义在于把错误拦截在早期。如果模型输出垃圾就直接抛异常触发重试而不是带着垃圾数据往下游走最后生成一个莫名其妙的报告。我在项目早期吃过教训模型某次把JSON里的逗号全写成了中文逗号下游直接崩溃。从那以后凡是模型输出进入业务流程一律过Schema。3.3 提示词也有版本改名、记录、回滚提示词改动引发的Bug往往很隐蔽。可能上周效果很好的prompt今天因为改了半句话输出格式全变了。所以我把每个prompt文件用Markdown编写在文件头记录版本、日期、改动原因、评测效果。这不是多余劳动而是当线上效果突然下降时能第一时间看出是不是有人改了prompt。ai-engineering-from-scratch里的prompts目录下我看到每个任务一个子目录里面有v1.md、v2.md。每次改动必须配套更新对应的评测脚本结果。这项工作让我的提示词迭代变得可审计长期价值极高。4. Agent开发没有想象中难工具调用与状态管理是核心4.1 Agent不是玄学理解成模型循环AI Agent的术语很多规划、推理、记忆、工具调用。说实话卸下概念外壳核心就是一个循环模型生成下一步动作 - 执行动作 - 把结果拼回上下文 - 再交给模型决策。像一个不断查资料的实习生每一步都在问接下来做什么。我在项目里做了一个比较典型的资料查询Agent用户提问后Agent先判断是否需要检索知识库如果需要就调用一个search_knowledge_base(query)工具拿到结果后再生成回答。整个过程不是一次LLM调用而是多次。4.2 工具注册让模型安全地使用内部API给Agent加工具最忌讳的就是把函数裸奔给模型调用。模型不可信你给它一个删除数据库的工具它就真可能被prompt injection诱导去执行。工程化的做法是定义工具Schema白名单式暴露。# app/agents/tools.py from pydantic import BaseModel class SearchKBInput(BaseModel): query: str Field(..., description检索关键词最好是一个短语) top_k: int Field(5, ge1, le10) TOOLS [ { name: search_knowledge_base, description: 在内部知识库中检索相关文档片段, parameters: SearchKBInput.model_json_schema(), } ]这里有两个关键设计一是参数用Pydantic定义模型调用工具时如果参数类型不对无法通过校验二是top_k限制在1到10防止某次检索拉回几百条上下文把Token撑爆。模型虽然在决策但它活动的边界被你严格地约束住了这就是前面提到的harness engineering思想——像缰绳一样控制模型而不是放任它自由发挥。4.3 状态管理不要让多轮对话变成无底洞Agent的多轮对话有两种状态一种是和用户的显式对话历史另一种是Agent内部的思考历史包括工具调用和结果。很多人在实现时把两种状态混在一起结果上下文越来越长最后超出模型窗口。我的做法是把内部思考历史放进一个AgentState对象只保留最近N轮的推理链工具结果太长时直接做摘要压缩。这样上下文不至于无限膨胀。同时给每个会话设置一个max_turns上限比如最多允许5轮内部循环超过则强制让模型给出最终答案防止Agent陷入死循环。5. 没有评估体系就不要说系统可用测试集和自动化回归5.1 LLM应用最大的坑没有测试集传统软件测试很好做一个函数输入输出是确定的断言一写就完事。LLM应用不行同一条输入温度调成0也可能出现细微变化。如果没有一批固定案例你根本不知道这次改动是变好了还是变坏了。我为此建了一个tests/eval/cases/目录里面放了两类评估集。一类是输出格式评估集纯测模型是否按指定JSON格式输出包含正常输入和异常输入如空字符串、超长文本、夹带Markdown的输入。另一类是业务效果评估集比如做资料查询Agent就准备30条标准问题每条记录期望包含的关键信息点。5.2 评估指标怎么定准确率只是起点针对资料查询Agent我设计了几个量化指标准确率答案是否包含期望信息点、格式合规率是否符合JSON Schema、幻觉率是否出现知识库不存在的实体、端到端延迟从发出请求到收到完整回答的时间。其中幻觉率我采用半自动方式先让模型基于答案生成支持事实再人工抽检。这些指标写进scripts/eval.py每次改Prompt或Agent逻辑后跑一遍输出效果对比表。这个习惯救了我好几次有两次优化后准确率提升了但幻觉率也升高了多亏评估脚本能立刻发现这个“牺牲可靠性换精度”的苗头。5.3 把评估接入CI用自动化门禁拦住回归评估脚本跑完不算结束我把它接入了GitHub Actions。每次Pull Request自动跑单元测试和一组轻量级评估比如抽20条关键案例如果格式合规率低于95%或准确率低于上次基准CI直接失败。这样团队里任何一个人改了prompt或工具逻辑都能立刻看到是否有回归。这里要特别提醒一点评估集不是一成不变的。每过一两周把线上用户的真实问题加入评估集形成一个“线上反馈 - 评估集扩充 - 回归验证”的闭环。不然评估集慢慢会脱离实际场景变成自说自话。6. 生产环境那一道坎部署、监控、成本优化的实操记录6.1 从Notebook到在线服务最少要过几关本地跑通之后我信心满满把服务部署到云服务器结果第一个星期就被打脸。Notebook里跑一次调用不受约束生产服务则要面对并发、超时、限流、安全、日志。我总结下来最小可上线清单至少有五件事进程管理systemd或容器、HTTPS网关、API鉴权、结构化日志、优雅关闭。以部署为例我最后选择了Docker systemd的方式。Docker统一了运行环境systemd负责守护进程和开机自启配置简单成本又低。对于个人项目和中小业务这样做比直接上Kubernetes更务实。Kubernetes是很好的技术但如果只有几十个请求每秒运维复杂度会反噬开发效率。6.2 性能优化三板斧缓存、并发、流式输出上线后第一周我就发现Token消耗比预想快得多因为每次请求都在重复传入大段上下文。后来做了三个优化response_cache缓存层、并发限制、流式输出。效果立竿见影。优化项做法效果缓存层对用户查询做语义相似的简易匹配命中后直接返回热门问题Token消耗降低约40%并发限制只允许最多10个并发LLM请求其余排队避免限流触发错误率下降流式输出FastAPI返回StreamingResponse首Token体验从3秒变800毫秒缓存层我没有用向量数据库而是先做了个简单的MD5过期时间实现后来才根据调用数据优化为语义缓存。我的经验是不要一开始就上重武器先用最简单的方案解决最痛的点。6.3 监控不是看仪表盘而是看“异常模式”传统的监控指标是CPU、内存、QPS。AI应用除了这些还要盯三类更有语义的指标Token消耗速率、请求错误原因分布、输出格式异常率。尤其是格式异常率如果某版本提示词改动后这个数字突然从0.1%涨到5%还没等用户投诉你就能从日志里嗅到问题。我写了一个简单的日志中间件每次LLM调用都会打印模型名、输入Token数、输出Token数、耗时、是否重试、最终是否成功。然后搭配grep和几个统计脚本虽然没有高大上的监控平台但定位问题足够了。6.4 模型灰度发布永远留一条退路大模型API厂商经常升级模型版本。某个版本可能在官方的benchmark上表现更好但到你的具体场景里就是不如以前。所以我做了一个模型路由配置每个服务使用一个model参数线上白名单里同时保留旧版本和新版本。灰度发布的操作方式先在内部体验环境切到新版本跑一遍评估集没问题后线上按10%流量灰度用前面提到的评估指标对比三天确认稳定后逐步扩大到100%。如果效果异常直接配置一键回滚到旧版本。这个流程给我留了好几次余路。7. 从单人实验到团队协作让AI工程在整个组织跑起来7.1 代码规范和评审AI项目更需要纪律一个人写代码时总觉得规范限制效率。但真的和团队协作没有规范的AI项目就是灾难。随机性已经让系统很不可控了如果代码风格、prompt管理、错误处理还各有各的想法出了问题根本没法查。我给团队定了几条硬规矩所有Prompt必须走prompts/目录禁止在代码里写裸字符串所有模型输出必须经过Pydantic校验每次Prompt改动必须附带评估脚本结果LLM调用必须走统一封装的LLMClient不允许直接 import 其他SDK。这些规矩不复杂但能拦下80%的隐性Bug。7.2 跨角色协作产品、算法、工程、测试各司其职AI工程不仅是工程师的事。产品经理要理解模型的能力边界不要承诺模型做不到的事情算法工程师要负责模型选型和评估体系后端工程师要解决服务稳定性测试工程师则要为非确定场景设计专门的用例策略。我见过最失败的协作模式是产品拿ChatGPT上的表现来提需求工程师为了满足需求开始各种无限制的调prompt最后双方都精疲力竭。更合理的方式是先由工程师和算法建一个“能力基线”给产品看一组真实测试集上的成功失败案例让产品基于客观事实做需求取舍。7.3 沉淀公共组件把经验固化为平台能力当团队里同时跑三四个AI应用时重复劳动开始显现每个项目都要写一套LLM客户端、一套缓存、一套评估脚本。这时就应该把可复用的能力抽到一个公共的ai-platform库中包括模型网关、提示词管理后台、评估服务等。ai-engineering-from-scratch项目到后期就承担了这个角色。它的很多模块被直接复制到公司其他项目里再经过大家的迭代变成更健壮的公共组件。这也是我认为“从零开始”最大的价值你亲手写过一遍底层逻辑后续用任何平台能力时都知道它在干什么、出了问题怎么排查。7.4 踩过的经典坑几个值得反复回看的教训最后梳理几个我踩过、也看着朋友踩过的典型坑。第一永远不要在模型输出里直接拼接SQL或Shell命令除非你做好了严格的参数白名单和转义否则一次prompt injection就会让你付出代价。第二重试策略必须配指数退避国内很多模型API限流是每秒级别的固定间隔重试往往会火上浇油。第三Token成本要按每百万Token的价格拆算到每次请求很多看似微弱的选择放大到百万次请求后差距惊人。第四一个AI服务出问题时先查输入和输出日志不要一上来就怀疑模型大概率是你的上游传了脏数据。这些坑没有一个是模型“不够聪明”造成的全都是工程管理上的疏忽。这也再次印证了这篇文章的核心观点AI工程的难点从来不只是算法而是把算法稳稳地嵌进真实业务的那一整套功夫。