1. AI工程从零开始,到底在做哪件事
很多朋友看到"ai-engineering-from-scratch"这个项目名,第一反应是"又要从张量开始写神经网络"。这是最大的误会。过去两年我经手过不少大模型落地项目,真正考验人的,从来不是怎么训练模型,而是怎么把一个有概率输出的黑盒模型,变成一条稳定、可控、能迭代的业务链路。
这篇文章面向的读者很明确:想用大模型做产品但不知道怎么动手的开发者,被老板要求"一周内跑通AI Agent"的工程师,以及在RAG、模型部署、Prompt调优里转了很久还是觉得工程化没谱的同学。我会按我自己搭建项目的顺序,从整体设计讲到目录结构,再到第一次调用、Agent编排、部署监控和问题排查,把一个最小可用的AI工程从0到1完整拆开。不发散到模型训练,也不聊那些只有大厂才用得上的分布式系统,就聊个人和中小团队能直接落地的部分。
好,先说设计思路。
1.1 先分清AI研究、AI应用和AI工程
我在很多项目评审会上发现,大家嘴上说的"AI工程",其实是三件完全不同的事。
AI研究是探索算法边界,比如设计新的模型结构、提出新的训练目标。这东西需要有深厚的理论基础,普通业务团队基本碰不到。
AI应用是定义产品交互,比如"用户上传一段需求,我们自动拆分任务并发给对应Agent"。这更接近产品经理和业务架构师的工作。
AI工程则是把AI应用变成可运行的软件系统:怎么选模型、怎么写Prompt、怎么接数据、怎么让调用不超时、怎么记录日志、怎么评估效果、怎么控制成本。这三个词一直被混用,导致我现在带项目第一件事就是拉齐认知:我们不做研究,不做PPT,我们要交付的是一个能被持续维护的系统。
所以如果你准备把"ai-engineering-from-scratch"当作一个学习路径,我建议你直接把重心放在AI工程上。训练自己的小模型不是第一优先级,而是先掌握"基于成熟模型做应用"的工程能力。现代大模型的API已经足够强,个人团队完全可以站在上面搭自己的产品。真正拉开差距的地方,在于工程体系是否完整。
1.2 从零开始的核心技术栈和知识地图
一个完整的AI工程项目,从需求到上线,至少要经过下面这几层。我自己习惯把它画成一张表,每往下一层,抽象程度都不一样。
| 层次 | 要解决的问题 | 常用手段 |
|---|---|---|
| 业务层 | 用户到底要什么,边界在哪 | 需求分析、场景拆解、评估指标定义 |
| 交互层 | 模型怎么说人话、怎么不跑偏 | Prompt Engineering、Few-shot、系统提示词 |
| 编排层 | 单次调用不够用怎么办 | Agent循环、工具调用、多Agent协作 |
| 数据层 | 模型不知道你的私有知识怎么办 | RAG、向量检索、重排序、数据清洗 |
| 服务层 | 模型部署在哪,怎么扛流量 | API网关、私有化部署、容器编排 |
| 观测层 | 出问题能不能快速定位 | 日志、链路追踪、Token消耗统计、评估集 |
这套知识地图,基本就是一个AI工程从零起步的全部地盘。你不需要一开始全部精通,但必须知道每一层大概有哪些坑。比如很多人第一个Demo跑通之后就高高兴兴上线,结果Prompt里一个小改变,线上效果直接崩掉,就是因为没有评估集;又比如Agent循环没有设置最大步数,模型在工具调用里绕圈子,一次对话烧掉几块钱,就是因为编排层没有加约束。
我的建议是:先按这个地图把最小闭环跑通——调用模型,加上Prompt,接一个工具,记录日志,然后评估。后面的所有方案,都会围绕这条主链路展开。
2. 搭一个可运行的AI工程骨架:目录、配置和第一次调用
理论说多了容易飘,还是直接动手。这一节我完整给出一个最小项目结构,同时解释每个目录为什么存在。可以说,目录设计是AI工程里最容易被忽视、但后续最影响迭代速度的部分。
2.1 项目目录到底怎么设计
网上很多AI项目的Demo就是单个Python文件,所有Prompt写在字符串里,密钥直接写在代码里,工具函数堆在一起。一个人玩没问题,一旦要加人、加需求,马上变成泥潭。
我推荐从第一天就按下面的结构来,哪怕项目还很小:
my-ai-app/ ├── app/ │ ├── main.py # 入口:CLI或API服务 │ ├── agents/ # Agent角色与执行逻辑 │ ├── tools/ # 模型可以调用的外部工具 │ ├── prompts/ # 所有的提示词,按版本管理 │ └── services/ # 模型调用、日志、评估的封装 ├── config/ │ ├── config.yaml # 环境无关的基础配置 │ └── .env.example # 密钥模板,真实密钥不进仓库 ├── data/ │ ├── raw/ # 原始数据 │ └── processed/ # 清洗后的数据 ├── tests/ │ ├── test_prompts.py │ └── test_tools.py └── pyproject.toml为什么要这样分?核心原因是把"经常变的东西"和"不经常变的东西"分开。Prompt几乎天天改,如果它和业务逻辑耦合在一起,每改一句话都要动代码、重新部署,效率极低。配置文件也一样,模型服务商的地址、密钥、超时时间这些东西不该写在代码里。我自己踩过的坑是,早期图省事把Prompt放在代码文件里,后来要同时测三个Prompt版本,只能复制整个文件,最后改到怀疑人生。
还有一点,tests目录一定要从第一天就留出来。AI工程的测试不是传统的单测,而是"给一组固定输入,看输出是否符合预期",这个后面在评估那一节展开。先把目录留出来,后面补起来会舒服很多。
2.2 第一次模型调用:Prompt Engineering的最小实践
目录搭好了,接下来要做的事情特别简单:调用一次模型,但这次调用要兼顾稳定性、可观测性和可配置性。我习惯封装一个统一的模型接口,而不是在业务代码里直接调SDK。
下面这段是一个最小可用的调用封装,兼容OpenAI接口的大多数服务商:
import os import time from openai import OpenAI client = OpenAI( api_key=os.getenv("LLM_API_KEY"), base_url=os.getenv("LLM_BASE_URL", "https://api.openai.com/v1"), ) DEFAULT_SYSTEM = "你是一个严谨的AI助手。不知道的信息明确说不知道,不做猜测。" def call_llm(messages, temperature=0.2, max_tokens=1024, retry=3): for attempt in range(retry): try: resp = client.chat.completions.create( model=os.getenv("LLM_MODEL", "gpt-4o-mini"), messages=messages, temperature=temperature, max_tokens=max_tokens, ) return resp.choices[0].message.content except Exception as e: if attempt == retry - 1: raise time.sleep(2 ** attempt) # 指数退避这段里有两个容易被忽略的工程细节。
第一,temperature要按场景明确设置。如果是事实问答、信息抽取,温度调到0或者0.2,减少随机性;如果是写文案、头脑风暴,可以调到0.7以上。很多新手全程默认一个温度,是效果不稳定的重要原因。
第二,max_tokens要设置合理上限。不设置的话,模型可能在长输出任务中无限生成,既费钱又影响延迟。但也不要设置得过大,否则和下游缓存、队列策略会打架。
下面看一个Prompt Engineering的最小例子。假设我们要做一个"从用户留言中提取结构化信息"的功能,系统提示词可以这样写:
你是客户工单分析助手。从用户输入中提取以下字段: - intent: 用户意图,只能取值 complaint/suggestion/inquiry - urgency: 1-5的整数,5表示最紧急 - summary: 一句话总结,不超过50字 要求: 1. 只输出JSON,不要输出任何解释。 2. 如果无法提取,对应字段填null。 3. 输出示例:{"intent": "complaint", "urgency": 5, "summary": "充值后未到账"}然后把用户留言作为user消息传入。你会发现,只要在Prompt里给出明确字段、取值范围和输出示例,模型基本能稳定地返回可用JSON。用代码去json.loads就省心很多。
这个例子看着简单,但它包含了Prompt Engineering的三板斧:角色定位业务边界、任务输出约束、示例提供格式锚点。这三板斧,足够覆盖绝大多数结构化输出需求。
2.3 从第一次调用就要加上的可观测性
很多人在这一步会跳过去,觉得"能跑就行"。我强烈建议不要省。因为AI应用的失败模式和传统软件不一样——它很少直接崩溃,更多是"返回了一个看起来合理但其实是错的答案"。如果没有日志,你根本无从判断是Prompt的问题、模型的问题还是数据的问题。
我每次调模型,至少记录四样东西:输入消息、输出内容、Token数、耗时。再往后加,可以记录模型名、温度、成本估算。下面是一个极简的日志封装:
import json import time def log_llm_call(messages, response, usage, latency_ms): entry = { "ts": time.time(), "messages": messages, "response": response, "total_tokens": usage.total_tokens if usage else 0, "latency_ms": latency_ms, } # 写入本地JSONL或上报到日志平台 with open("logs/llm_calls.jsonl", "a") as f: f.write(json.dumps(entry, ensure_ascii=False) + "\n")一个小技巧:total_tokens这个值,大部分模型接口都会在返回的usage字段里给出。把它记下来,后面做成本核算和性能分析都有据可查。我自己曾经因为没有记录耗时,排查一个"页面偶尔很慢"的问题查了一整天,最后发现是模型服务偶发超时,而当时日志里压根没有这个字段,只能靠猜。
3. 核心环节的进阶实现:选型、RAG和Agent编排
最小骨架跑通之后,一个工程才真正开始。接下来这一段是AI工程的核心内容:模型选型与部署、RAG引入私有知识、从单次调用进化到Agent。
3.1 模型选型与部署:先别急着上GPU服务器
模型选型是个很现实的工程问题。每次听到"哪个模型最强"这个问题,我都想反问一句:你的场景真的需要最强模型吗?
我一般按四个维度选型:
- 数据敏感度:数据能不能出域,决定你要不要私有化部署。
- 响应延迟:C端交互需要低延迟,离线分析可以容忍慢。
- 调用成本:高频场景对单位token成本极其敏感。
- 能力边界:是否需要复杂推理、长上下文、多模态。
把它们放到一张决策表里会很清楚:
| 场景 | 推荐方案 | 原因 |
|---|---|---|
| 个人原型 | 托管API | 零运维,成本低,速度快 |
| 企业低敏内部工具 | 托管API + 数据脱敏 | 省人力,安全性可控 |
| 高敏感数据 | 私有化开源模型 | 数据不出域 |
| 高频低价值任务 | 小模型 + 缓存 | 成本优先 |
| 复杂推理任务 | 大模型 | 效果优先 |
关于部署,我必须说句实在话:如果只是团队内部用,托管API永远是最划算的。不要为了追求私有化而私有化。你租一台GPU服务器,至少要面对显存管理、并发排队、模型热更新、监控告警一堆问题,而这些托管API都已经帮你处理好了。
如果真到了必须私有化部署的那天,我建议从这类开源模型入手:7B到14B参数级别的小模型,用vLLM或者Ollama就能扛住不少场景。部署时最需要注意的就是显存。简单估算一下,14B模型用FP16加载,光权重就需要约28GB显存,加上推理时的KV Cache,一张40GB的卡基本是起步配置。所以别拿笔记本跑,也别用CPU硬扛,那不是工程化,是自虐。
3.2 RAG:让模型只说自己有依据的话
大模型的知识截止于训练数据,很多业务私有知识它根本不知道。RAG(检索增强生成)是目前最主流的解法,本质是三步:把知识库切成小块存进向量库;用户提问时先检索相关片段;把这些片段塞进上下文,让模型基于片段回答。
我画过最简洁的流程图,其实就是:文档切分 -> 向量化 -> 召回 -> 重排 -> 生成。不展开画图,直接给核心代码骨架:
# 伪代码:RAG主流程 def rag_query(question): # 1. 召回:从向量库中取topK相关文档块 chunks = vector_store.search(question, top_k=5) # 2. 重排:对召回结果打分,取最相关的3块 reranked = reranker.rerank(question, chunks)[:3] # 3. 构造上下文 context = "\n---\n".join([c.text for c in reranked]) # 4. 生成回答 prompt = f"请只基于下面的资料回答问题,不要编造。\n\n资料:\n{context}\n\n问题:{question}" return call_llm([{"role": "user", "content": prompt}])工程上最容易出问题的不是向量化,而是切分策略。很多人习惯按固定长度硬切,比如每512个字符一刀,结果把一句话从中间切断、把表格拆成碎片,召回质量自然差。我的经验是:优先按语义边界切,比如Markdown标题、段落、代码块,再配合少量重叠。一个没有重叠的切分,在跨块信息上几乎是必坑的。
还有,RAG落地后必须做召回评估,不能只看几个例子觉得"差不多"。最简单的做法是准备一批"问题->相关文档块"的标注数据,算召回率。召回率如果低于70%,生成答案再好也是空中楼阁。很多项目死在最后一步,就是因为没有这个评估集。
3.3 从单次调用到Agent:编排的本质是约束循环
如果说RAG解决的是"模型不知道",Agent解决的是"模型不会做多步任务"。所谓Agent,简单理解就是:模型 + 工具 + 循环。模型根据任务决定要不要调用工具,然后观察工具结果,再决定下一步动作,直到结束。
一个最基础的ReAct循环,代码骨架长这样:
def agent_loop(task, max_steps=5): messages = [{"role": "user", "content": task}] for step in range(max_steps): resp = call_llm(messages, temperature=0.2) # 模型返回的文本里,如果包含Action标记,就执行对应工具 action = extract_action(resp) if action is None: return final_answer(resp) result = run_tool(action.name, action.args) messages.append({"role": "assistant", "content": resp}) messages.append({"role": "tool", "content": result}) raise TimeoutError("Agent超过最大步数")注意这里有个关键参数:max_steps。这是AI工程里必须加的护栏。没有它,模型在工具调用中一旦陷入循环,会无限烧钱。我的惯例是,默认5步起步,复杂任务最多10步,步数超了就强制终止,让用户手动介入。
多Agent协作是更进阶的编排方式。常见套路是一个Planner拆解任务,一个Worker执行子任务,一个Critic做审查。听起来很美好,但我必须说一句:不要为了炫技上多Agent。每多一个Agent,就多一层模型调用、多一种失败模式、多一份成本。我自己见过太多团队,任务本来单Agent能搞定,非要拆成三个角色,结果消息传递出错、职责边界模糊,效果反而不如一个Agent加几个工具来得稳定。
3.4 AI编程与AI测试:工程链路上的两个增量
很多热词里都有AI编程和AI测试,这两个其实也是AI工程的一部分。AI编程是用LLM写代码,但它写出来的代码本质上和业务代码一样需要测试。AI测试则相反,是指用AI自动生成测试用例、做异常兜底。
我在项目里常见的做法是,让Agent生成一段核心函数后,自动追加两个测试用例,然后跑一遍。这也算一种轻量的Agent实践。但要注意,AI生成的单测不等于测试完成,覆盖率、边界条件还是得人肉看。我觉得最值得做的不是让AI写全部测试,而是让AI生成"用户可能问出什么刁钻问题"的用例集,然后拿这些用例去测Prompt。这属于AI工程里非常关键的质量环节。
4. 线上常见问题和排查技巧实录
无论前面设计多完备,线上总会出问题。这一节是我最想写的,因为每一个问题都是我或身边团队真实踩过的。
4.1 回复不稳定、幻觉多,先别怪模型
很多朋友一看到模型回答不靠谱,第一反应是"换个更强的模型"。但换模型成本高、周期长,而且很多时候根本不解决问题。我排查这类问题的顺序是固定的。
先看输入。你是不是把用户的原始问题直接丢给模型了?用户问题时常含糊不清,系统提示词有没有把它约束成"你是一个客服助手,只回答订单相关问题"?如果输入复杂,先做意图分类或改写。
再看输出约束。需要结构化结果时,有没有在Prompt里给出JSON示例?有没有设置temperature为0?有没有要求模型"资料里没有就回答不知道"?这三点做好,大多数"幻觉"都能压下去。
最后才看模型。同一批Prompt,不同模型效果确实不同,但这往往是最后一步判断。我印象最深的一次,是客服工单分类模型总是把"催发货"识别成"退货",看起来像是模型能力不足。后来排查发现,是我们给模型的Few-shot示例里,两个类别的样例太相似,没有给出"催发货"特有的关键词信号。调整示例之后,准确率立刻上去了。所以别急着换模型,先检查你是不是给了模型足够清晰的"边界"。
4.2 成本失控和性能瓶颈怎么调
Token成本可以在项目第一天就估算。每次请求的费用大概是:
每次调用成本 = 输入token数 / 1000 × 输入单价 + 输出token数 / 1000 × 输出单价假设一个模型输入0.15元/千token,输出0.6元/千token。一次调用输入2000 token、输出500 token,单次成本就是0.3 + 0.3 = 0.6元。如果每天1万次调用,一天就是6000元。这么一算,很多团队就不敢乱调了。
控制成本有几个实操抓手:
- 做Prompt压缩:把历史对话摘要化,而不是把所有聊天记录原样塞进上下文。五个会话轮次可能就超过一万token,你不裁剪,钱就白烧了。
- 做语义缓存:相同或相似问题直接命中缓存,不重复调模型。对于FAQ类场景,缓存命中率能到40%以上。
- 分层用模型:简单任务用小模型,复杂任务才用大模型。我常用一个规则,先让一个小模型做意图分类,只有"复杂推理类"才转给大模型,成本能降一半。
- 异步批处理:一些不追求实时响应的任务(如离线批量总结),可以用异步队列,既降成本又降峰值压力。
性能方面,最大的瓶颈往往不是模型,而是下游工具和网络。比如Agent里某个工具要调外部接口,一次要等5秒,Agent要调三次,整个链路就慢到不可用。这种问题只能靠链路的可观测性去发现,所以前面日志一定要记耗时。
4.3 私有数据和合规边界怎么处理
做AI工程,数据安全是绕不开的坎。我不是法务,只从工程师角度说说落地经验。
先判断数据能不能出域。如果企业内部数据完全不能传到外部接口,那就不要硬撑着走托管API,老老实实私有化部署小模型。如果部分数据可以出域,也应该先做脱敏再调用。最简单的脱敏是去掉姓名、手机号、身份证号等字段,再做日志脱敏。
日志是重灾区。很多团队把模型输入输出全量记录,方便排查问题,结果日志里全是用户真实信息。我的做法是:日志里存请求ID、消息的哈希值,或者只存脱敏后的内容。另有一个原则:不记录模型收到的原始附件和图片,只记录文件路径。这样即便日志泄露,影响面也可控。
还有权限:调用模型的服务账号,应该只拥有它必需的权限,不能让它顺便读库、写文档。这个和传统后端的安全策略完全一致,但到了AI应用里经常被忽略。
4.4 一套亲测好用的排查速查表
最后把问题现象、可能原因和优先级梳理成一张表,方便大家直接对着查:
| 症状 | 最可能的原因 | 排查重点 | 优先级 |
|---|---|---|---|
| 答案经常跑偏 | Prompt边界不清 | 系统提示词、Few-shot示例 | 高 |
| 偶尔返回乱码/空 | 输出格式约束不足 | 增加JSON示例、后处理校验 | 高 |
| 响应很慢 | 上下文太长或外部工具慢 | 看耗时日志、压缩上下文 | 高 |
| 成本涨得吓人 | 上下文膨胀、无缓存 | 算Token分布、加摘要和缓存 | 高 |
| 引入了错误知识 | RAG召回质量差 | 查切分策略、召回率评估 | 中 |
| 工具调用了不该调的 | Agent权限过大 | 加白名单、限制工具暴露 | 高 |
这张表我基本每次培训学员都会发,因为大多数团队的AI工程问题都能在这里对号入座。
写到这里,我自己最大的感受是:AI工程这个领域,门槛不在"AI",在"工程"。模型能力再强,没有一套稳定的调用、评估和可观测体系,就是一台没有安全带的跑车。我的实践经验是,从零起步时宁可慢一点,也要先把日志、版本化、成本护栏这些"不性感"的东西打好。它们不会让你发朋友圈,但会让你在三个月后不翻车。顺着这条路继续做下去,下一步值得研究的是Agent模式拆解和RAG的离线评估体系,这些都可以在现有骨架上慢慢长出来。