1. 项目到底在做什么:从零开始,把AI工程拆开来看
先说说这个项目的由来。这两年我带团队做了不少大模型落地的项目,接触过很多开发者,大家第一次上手的时候基本都有一个错觉:AI工程不就是写提示词、调接口、然后上线吗?真到了生产环境,你会发现这中间隔着一整条链路——需求拆解、模型选型、数据准备、提示词设计、输出约束、评估迭代、成本控制、异常兜底。ai-engineering-from-scratch这个名字说得很直白,就是抛开现成的封装,把这串环节逐个吃透,不走“调通就跑”的捷径。
这个项目适合谁?我的答案是:已经会写点Python、调过API,但想从“能跑通Demo”升级到“能扛住生产”的开发者。不管你是后端工程师、前端全栈,还是算法工程师,这套方法论都不绑定某个特定平台,换模型、换工具链,底层逻辑照样成立。我在项目里没有依赖任何低代码平台,所有核心链路自己搭,虽然前期慢一点,但一旦上线,哪里有坑我一清二楚。
1.1 为什么强调“from scratch”
我现在最怕听到的一句话是“我用某某平台搭好了”。搭好的时候确实爽,一旦出问题就非常痛苦:不知道答案是从哪来的,不知道平台给提示词套了什么壳,不知道成本是怎么算的。我去年接过一个内部知识库问答需求,第一版图快用了现成框架,两天跑通,结果上线后用户反馈三个问题:答非所问、张冠李戴、偶尔输出崩坏格式。我排查了一周,最后发现是框架默认的指令拼接逻辑把提示词顺序弄乱了,而我完全没有感知。从那以后我坚持一条原则:凡是生产级AI应用,核心链路必须自己可控。
打个比方,会用洗衣机的人不少,但能拆机看懂水路电路的人才算工程师。AI工程里的from scratch,不是让你从零去训练一个模型,而是把“提示词、模型调用、上下文、评估、上线”每个环节都变成你手里的可控模块。这样遇到问题才不会两眼一抹黑,才会知道该从哪里下刀。
1.2 谁来学最划算
我整理了一个简单的对照表,你可以看看自己属于哪一类,能从这个项目里拿走什么。
| 角色 | 现状 | 可以从这套实践中拿走什么 |
|---|---|---|
| 后端工程师 | 会写API,刚接触大模型 | 提示词结构化、输出约束、异常兜底方案 |
| 前端/全栈工程师 | 想做AI产品原型 | 完整工程链路视角、评估意识 |
| 算法工程师 | 熟悉模型,但对产品化陌生 | RAG落地、Agent工具调用、工程封装 |
| 独立开发者 | 想低成本验证一个想法 | 模型选型策略、成本控制手段 |
不管你是哪一类,我都建议你动手跟着做一遍,不要只看不练。AI工程是门手艺活,看十遍不如跑一个完整的案例。
2. 拆解核心工程栈:提示词、模型与上下文
很多人以为提示词工程(prompt engineering)就是“好好说话”。我承认,会说话确实有用,但它远不是文学创作,而是一套可以被测试、被版本管理、被度量质量的工程手段。这一节我把核心工程栈拆成三层:提示词、模型选型、上下文管理。这三层是任何AI应用的地基。
2.1 提示词工程:所有AI应用的“第一行代码”
提示词不是玄学,但它确实有很强的实践性。我见过太多人把提示词写成一大段自然语言,然后在末尾加一句“请回答”,这样也能跑,但效果极不稳定。换一个问法,结果就飘了。我个人的做法是标准化、结构化,把提示词当成接口约束来写。
我常用的一套模板长这样:
你是一个{角色}。 任务:{一句话任务描述}。 输入: {用户输入} 要求: 1. 只输出JSON,不要任何解释。 2. 字段必须包含:answer, confidence, source。 3. 如果信息不足,answer填"信息不足",confidence填0。 参考示例: {few-shot示例}不要小看这种格式。大模型对格式化的敏感度比想象中高,尤其是在开源模型上。如果你把约束散在一大段散文里,模型很容易漏掉;写成编号列表,漏执行的概率会明显下降。这里有个原因:模型在训练时见过大量“指令 + 约束列表”形态的数据,你越贴近它熟悉的形态,它越容易遵循。
再说few-shot示例,这是最被低估的提示词技巧。两个好的正反示例,比十句绕来绕去的约束都管用。比如你要模型做情感分类,与其写“请准确判断用户评论的情感”,不如给两个具体例子:
输入:这电池半天就没电,烦死了 输出:{"sentiment": "negative"} 输入:物流很快,包装也严实,好评 输出:{"sentiment": "positive"} 输入:{用户输入} 输出:模型看到示例后,会主动模仿你给出的输出格式和风格,这就是对齐成本最低的方式。
还有一点:对于复杂推理任务,可以考虑让模型“先想再答”,也就是思路链(chain-of-thought),例如“请先分步骤思考,再给出最终答案”。但生产环境要注意效率,思考步骤会消耗额外token,任务简单时没必要开。
2.2 模型选型:不是越强越好,是越合适越好
模型选型是AI工程里最容易被忽略的一环。我见过不少项目,一上来就选当前最强的旗舰模型,结果账单感人,延迟还高。选模型本质上是做权衡:任务复杂度、上下文需求、延迟敏感度、成本预算、数据合规,这五个维度一个都不能少。
| 任务类型 | 推荐档位 | 理由 |
|---|---|---|
| 简单分类、实体抽取 | 中小参数模型 | 延迟低,成本低,效果足够 |
| 文档问答 | 中端模型 + 检索 | 检索补充事实,模型负责组织语言 |
| 复杂推理、长文生成 | 旗舰模型 | 推理能力和指令遵循更强 |
| 高并发客服 | 分层策略 | 简单问题走小模型,复杂问题再升级 |
我有一个习惯:开发期先用强模型验证效果上限,跑通之后再拿评估集去测小模型,看能不能降级。很多任务看上去“非旗舰不可”,实际上换一个小模型只掉3%的准确率,但成本掉了80%。这笔账一定要算。
另外多说一句“多AI协作”。不要迷信单个模型包打天下。比如让一个强模型做任务规划,拆解成几个子任务,然后交给几个小模型并行执行,最后再由强模型汇总。这套思路在复杂workflow里很常见,本质上是用架构换成本和效果。模型不是越强越好,组合得当才是工程。
2.3 上下文管理:最硬的那道天花板
上下文窗口是有限的,这是AI工程里最刚性的约束之一。模型再强,也塞不下整本手册。你要像项目经理排期一样管理token预算,不能什么都往“脑子”里灌。
我的token预算经验是:prompt加输入内容占70%,预留给输出的占30%。如果输出是长文,输入占比还要降。超了怎么办?两个方向:截断和检索。
对于长文档场景,最常用的方案是RAG(检索增强生成)。思路很简单:把文档切分成小块,向量化后存进向量库,用户提问时先检索出最相关的几个片段,再把这些片段拼进上下文让模型回答。这个过程避免了“把整本书塞进对话”的奢侈操作。
切分策略有个细节:按标题和段落切,通常比按固定字符数硬切效果更好。比如一篇产品文档,按章节切,每个切片自带小标题,向量检索的命中精度会更高。按固定的512个字符硬切,很可能把一个完整知识点拦腰截断,召回质量自然差。
数据准备也很关键。few-shot里的示例务必人工清洗,模型会模仿你的示例,如果示例里有错误标签,它就会学着犯错。一句话:脏示例比没示例更可怕。
3. 实操:从零搭建一个“文档问答”AI应用
理论扯多了没用,我带你把一个完整的案例走一遍。这里我选的是最经典也最通用的场景:内部知识库文档问答。原因很简单,这个场景几乎涵盖了AI工程的所有核心环节,而且你可以在自己电脑上复现。
3.1 需求拆解:把一句“做个问答机器人”变成可执行计划
用户过来说“帮我做个问答机器人”,这不算需求。需求要拆到能落地的颗粒度。我一般问自己几个问题:
问答范围是什么?是公司那几十篇产品文档,还是全量知识库?回答风格有什么要求?要引用来源,还是随口答?系统性能底线是多少?单次响应超过几秒用户会不耐烦?输入有什么限制?比如问题最长多少字?
拿我实际做过的例子来说,最后拆出来的需求是这样的:
- 面向内部客服,覆盖产品手册、FAQ、故障排查文档共约200篇
- 回答必须给出文档来源,不许编造
- 知识库外的内容要直接说“不在当前知识范围内”
- 单次响应时间目标小于5秒
- 问题长度上限300字
基于这些约束,技术选型基本定了:一个通用模型API负责对话组织,一个向量库负责文档召回,中间用Python写一个轻量服务串起来。
3.2 构建评估集:没有评估就没有迭代
这一步是整个项目里最重要的,但90%的人会跳过。很多人改提示词靠感觉,评估靠“这次跑出来看起来好一些”,这根本不是工程。评估体系必须建立在数据上。
我的做法是:
先收集30条真实用户问题。注意是真实问题,不是你自己想当然编的。来源可以是历史客服记录、用户访谈、试用反馈。每个问题写一份“参考要点”,不需要标准答案,因为你允许模型有多种表达方式,但你得知道哪些要点必须出现。然后给输出打三档分:通过、部分通过、不通过。
跑评估时用脚本批量执行,把结果打到一个表格里。我贴一个简化版脚本,你自己扩展:
import json def run_eval(questions: list[dict], answer_fn, judge_fn): results = [] for q in questions: out = answer_fn(q["question"]) score = judge_fn(out, q["ref_points"]) results.append({ "question": q["question"], "score": score, "output": out }) passed = sum(r["score"] == 2 for r in results) print(f"通过率: {passed}/{len(results)}")评判这一环,我的建议是前期人工看,别急着搞自动化。人工看30条答案花不了一个小时,但你能发现很多自动判分发现不了的问题,比如语气不对、信息不完整、出处给错。等应用稳定了,再用强模型做裁判,自动化跑回归。
3.3 Prompt版本管理与模型调用封装
提示词要像代码一样做版本管理。我在项目里的目录长这样:
prompts/ 001_baseline.md 002_add_source_rules.md 003_fewshot_2cases.md每次改动记录三样东西:改了什么、为什么改、评估通过率的变化。这个习惯至关重要,因为提示词改动经常出现“这次改完感觉好了,但说不清哪里好了”的情况。没有版本记录和评估数据,你的优化就是原地转圈。
模型调用必须封装。不要在主业务代码里到处裸调接口,统一走一个封装函数。大模型接口再稳也不是100%可用,生产环境必须做重试和兜底。
import time def call_model(prompt, max_retries=3): for attempt in range(max_retries): try: return api.chat(prompt) except TimeoutError: time.sleep(2 * (attempt + 1)) return fallback_response(prompt)兜底逻辑也很重要。重试三次还不行,就别硬撑了,返回一个固定的友好话术,比如“系统繁忙,请稍后再试”,总比让用户等半分钟然后看到报错强。
3.4 引入Agent能力:从“一问一答”到“多工具协作”
当需求从“查一个知识库”变成“查三个库并汇总对比”,单纯的一次性RAG就不够用了。这时候需要Agent能力。Agent的核心是让模型决定“调用什么工具、以什么顺序调用”,而不是我们预先写死每一步。
工具定义就是一个JSON描述,模型会根据描述决定调用。举个例子:
{ "name": "search_docs", "description": "在知识库中搜索文档片段", "parameters": { "query": "string", "top_k": "integer" } }整个循环流程是这样的:模型分析用户请求,如果发现需要检索,就输出一个工具调用指令;程序执行工具,把结果回填给模型;模型继续推理,评估结果够不够,不够就再调一次工具;够了就生成最终答案返回。
我在实操里最大的教训是:Agent不会自觉停止。它会陷入“再搜一次、再看一眼、再多想一步”的循环,像个钻牛角尖的实习生。所以必须设置硬性上限,比如最大工具调用次数为5次,超出后直接回复“需要的信息太分散,建议缩小问题范围”。永远不要指望模型自己喊停,靠工程机制兜底才是正解。
4. 常见问题与排查技巧实录
做AI应用,不踩坑是不可能的。下面我把自己踩过的、帮别人排查过的高频问题整理成一份实录,你可以当成速查手册用。
4.1 幻觉:一本正经地胡说八道
这是所有AI应用的头号公敌。表现是答案流畅、语气笃定,但引用的内容根本不存在。我见过模型把某个不存在的接口名说得跟真的一样,连报错信息都编出来了。
幻觉的根源在于模型本质是统计生成,没有事实校验能力。排查时,把回答中的每个关键事实和原始文档比对一遍,你会发现幻觉通常集中在“细节补全”上。解法我按性价比排序:
- 提示词里明确写:信息不足时直接说不知道,不要猜测。
- 要求输出引用来源,前端展示时校验来源是否存在。
- 温度调低,别让它太放飞。
- 最治本的方式是RAG,让答案长在检索结果上,而不是靠模型记忆硬编。
我一直强调:提示词可以缓解幻觉,但解决不了幻觉。只要你的应用依赖开放生成,就必须有事实锚点,RAG就是那个锚点。
4.2 输出不稳定:同样的输入,每次答案不一样
模型本身有随机性,这是概率分布的固有属性。同一个问题跑三次,三次措辞不一样是正常的,但如果你需要稳定输出,这就会变成问题。
排查思路分几步:先看温度参数。我生产环境通常设0到0.3,越低越稳定。再检查输出格式,能开JSON模式就开,不能的话在提示词里做强制约束,然后用正则或schema校验,不合格自动重试一次。
还有一个很容易被忽略的点:升级模型版本后,行为可能悄悄漂移。同一个提示词,今天和三个月前效果可能完全不一样。所以我在项目里会固定模型版本,升级前必跑一遍评估集。
4.3 成本与延迟失控
成本的症状通常很统一:账单比预期高十倍。我排查时第一件事是查调用日志,把token消耗最高的几个请求拉出来看。一般会发现问题集中在几个场景:有用户把上万字的内容一次性丢进来、某个循环请求没有缓存、提示词里塞了太多历史对话。
解法有几个:
- 缓存重复请求。相同问题在一个时间窗口内直接返回缓存答案,能砍掉一大块成本。
- 模型分层。简单问题走小模型,复杂问题才用旗舰模型。
- 控制max_tokens,防止长文场景无脑输出。
- 减少few-shot数量,两三个高质量示例远比十个平庸示例有效。
延迟方面的经验:优先砍输入长度,而不是升级硬件。输入越长,首字响应越慢。砍掉无关上下文,比换快模型更直接。如果用户体感还是很慢,就上流式输出,至少让用户觉得“它已经在动了”。
4.4 上下文溢出与长文档处理
表现有两种:请求直接报错,或者模型聊着聊着忘了开头内容。原因只有一个:你把太多东西塞进上下文了。
解决思路我之前已经讲过,就是token预算加RAG。预算上,输入占比别超过70%。文档处理上,先检索再总结,千万别先总结再检索——把整篇文档压缩成摘要再丢给模型,摘要本身就会丢失细节,模型拿到残缺信息,回答自然残缺。
最后附一个排障速查表,你在线上遇到问题可以照着查:
| 现象 | 可能原因 | 优先检查 |
|---|---|---|
| 答非所问 | 上下文顺序错乱或检索命中错误 | 打印实际送入模型的上下文看看 |
| 格式崩坏 | 输出约束不明确 | 加JSON schema校验并自动重试 |
| 空转、超时 | 上下文过长或工具调用循环 | 查看token统计与Agent步数 |
| 成本异常飙升 | 模型档位过高、无缓存 | 拉取token日志,定位热点请求 |
5. 沉淀下来的工程习惯与最后一点建议
做了十几个AI工程项目之后,我最想告诉后来者的不是某个具体技术,而是几个工程习惯。第一,永远先跑通最小闭环再谈优化。先让一个简单版本的问答能work,再往上加RAG、加Agent、加评估,一次只动一个变量。第二,所有改动必须可回溯。提示词、模型版本、参数配置,全部纳入版本管理。第三,警惕“提示词全能论”。提示词很强大,但它解决不了检索缺失、解决不了数据质量问题、解决不了工具调用失控。工程是组合拳,不是一支独秀。
最后再分享一个小技巧:把评估集做成一个独立文件,放在项目仓库里,每次改完任何东西就顺手跑一遍。这个习惯帮我避免了至少五次“感觉优化了,上线反而更差”的事故。AI工程做到最后,拼的不是谁的提示词写得漂亮,而是谁的系统更稳定、更可度量、更容易迭代。这大概就是from scratch最大的价值:每一步你都清楚自己在做什么,以及为什么这么做。