这个标题听起来很像是某个GitHub仓库的名字,但它其实正是我这几个月在做的真实项目——一份从零开始的AI工程实践记录。我把自己的学习路径、踩坑记录、代码模板和工具选型思路全部收进了一个叫ai-engineering-from-scratch的目录里。今天把这套思路完整展开,写给那些想入门AI应用开发、又不想只当“调包侠”的人。
1. AI工程从零起步:先看清全局地图
1.1 什么是“AI工程”,它和算法岗、调包侠差在哪
先说一个很多人都会问的问题:AI工程到底是不是写Python调一下大模型API?我觉得不是。算法岗要研究模型结构、训练技巧、评估指标,这是偏学术和研究的路线;而“调包侠”是拿来一个现成SDK,堆几个接口调用,能跑通就完事。AI工程师恰好站在两者中间:既要知道模型能做什么、不能做什么,又要解决实际场景里的工程问题。
我在这个项目里最早犯的错误,就是把AI工程等同于“调用大模型”。结果写出来的Demo在本地跑得很欢,一到真实业务里就崩:上下文太长、输出格式飘、API限流、内存暴涨。真正让我意识到问题的,是某次做一个文本整理工具,模型输出偶尔会多出几句废话,用户直接拿这些废话去入库,后续流程全乱。那一刻我才明白,AI工程的核心从来不是“模型多聪明”,而是“怎么让一个不完全可控的东西,在可控的流程里稳定工作”。
所以这套ai-engineering-from-scratch项目从一开始就定下目标:用项目驱动的方式,把下面几块拼图凑齐——大模型的接入与调用、提示词的结构化设计、智能体工作流的编排、上下文与记忆管理、异常排查和性能优化。每块内容都配套一个最小可运行的代码示例,而不是甩一堆理论链接。
1.2 两条学习路线的对比:课刷完型 vs 项目驱动型
很多入门的朋友喜欢先花两个月把网课刷完,把里面每一个概念抄到笔记里,再开始动手。我试过,效果很一般。因为网课是按知识点组织的,而现实项目是按问题组织的。你在题目里学到的“温度系数调低会让输出更确定”,到了真实项目里,却不知道温度、top_p、max_tokens这几个参数一起变化时,到底谁在起作用。
项目驱动型的思路完全不同:先确定一个足够小的目标,小到能在一周内跑出第一版。比如先做一个“能根据我的自然语言输入,整理出一份带标题和要点的会议纪要”的脚本。这个目标看起来很窄,但它逼迫你在一周内接触至少五块核心知识:怎么读取文档、怎么调用大模型、怎么写提示词、怎么解析模型输出、怎么处理输出不合规的情况。
我整理过一张对比表,放在项目README里,这里直接搬过来:
| 课刷完型路线 | 项目驱动型路线 | |
|---|---|---|
| 知识组织 | 按学科划分,学完不一定知道用在哪 | 按真实问题划分,学了马上能用 |
| 正反馈周期 | 慢,学一个月可能还是不会做东西 | 快,一两天就能看到一个能跑的程序 |
| 记忆保留 | 容易忘,概念和场景是脱节的 | 记得牢,每个概念都连着一段踩坑经历 |
| 工程能力 | 偏弱,只写过孤立练习题 | 强,被迫处理依赖、环境、异常、部署 |
| 适合人群 | 有大量时间、想系统打底子的在校生 | 在职转方向、想尽快做出东西的人 |
我推荐后者,但也不是完全放弃前者。正确姿势是:先定一个小项目,然后只学“今晚能用到的那点理论”,用完再补相关性高的下一块。比如写提示词之前,只需要知道模型是按概率生成文本的,了解温度和top_p两个参数就够;等做到了智能体,再去补函数调用的底层实现逻辑。
1.3 我的仓库目录结构:怎么把零散知识装进一个框架
零散的知识如果不装进框架,约等于没有。我在ai-engineering-from-scratch里用的目录结构长这样:
ai-engineering-from-scratch/ ├── 01-basic-env/ # 环境搭建与第一个调用 ├── 02-prompt-basic/ # 提示词基础:指令、约束、示例 ├── 03-output-parsing/ # 模型输出的结构化解析 ├── 04-memory-context/ # 上下文管理与记忆机制 ├── 05-agents-workflow/ # 智能体与工作流编排 ├── 06-local-deploy/ # 本地部署与模型选型 ├── 07-debugging/ # 常见问题与排查脚本 └── README.md每个目录里都有一份README.md说明思路,一份main.py或main.ipynb放可运行代码,一份lessons.md记录当时的坑。这个结构不是为了好看,而是为了逼自己在学每一块新知识时都问三个问题:这个知识点解决了什么实际问题?它能被写成一个最小复现吗?如果老手看到这段代码,会挑什么毛病?
事实证明,这样组织学得极快。以前我看文章读到“上下文窗口”“检索增强”这些词只是眼熟,现在一看到它们,脑子里自动浮现出对应的代码文件和出错现场。
2. 工具链与运行环境:稳比炫技重要
2.1 搭建Python环境:虚拟环境和依赖锁
AI工程里有个很尴尬的现状:跑了半天报错,最后发现是numpy版本不对。所以环境搭建这一块我放在所有代码之前,而且用的是最笨但最稳的方式:
python -m venv .venv source .venv/bin/activate # Windows下用 .venv\Scripts\activate pip install --upgrade pip pip install openai python-dotenv pydantic pip freeze > requirements.lock这里python-dotenv用来管理API密钥,绝不让密钥出现在代码里。.env文件长这样:
LLM_API_KEY=你的密钥 LLM_BASE_URL=https://你的接口地址 LLM_MODEL=你使用的模型名用pydantic是提前为输出解析做准备。很多刚入门的朋友不理解为啥要freeze出requirements.lock,我举个真实例子:某次我把依赖升级了两个小版本,结果pydantic从v1切到了v2,所有.parse_obj()的写法全部失效。如果没有锁版本,这种问题能让人查一整天。
如果是Java后端,我建议直接看spring ai这套Spring官方生态,它对常用模型都有统一接入封装,切换模型不乱改业务代码;但学原理阶段还是先用Python,原因是Python的数据处理生态更直接,写实验代码最快。
实操心得:虚拟环境不是给项目用的,是给“你自己”用的。每开一个新实验项目,就新建一个虚拟环境,别嫌麻烦。定期跑一次pip list看看装了什么没用的包,顺手清掉。环境乱了之后排查问题的时间成本,远超你一开始多花的那三分钟。
2.2 大模型接入的三种方式:在线API、SDK封装、本地部署
我在项目里总结了大模型接入的三种典型方式,它们各有用途,别只看名气选。
第一种是在线API,最省事,适合做原型验证和常规业务。只需一个密钥、一个HTTP请求,不需要GPU、不需要运维。缺点是数据要出公网,部分行业没法接受;还有按token计费,调得不好成本很高。
第二种是SDK封装。官方SDK解决的问题不只是“少写几行HTTP”,它帮你处理了重试、超时、请求日志这类基础工程问题。比如Python生态里的openai库、Java生态里做Spring AI封装,都不需要我们手写鉴权和连接池。但用SDK也有坑——版本升级非常频繁,接口动不动就变。所以我的习惯是:在自己的代码里再包一层薄薄的LLMClient类,把模型调用集中在一个文件里,未来换库、换模型,只动这一个文件。
第三种是本地部署,适合数据敏感、离线运行、需要深度定制代码的场景。这个对硬件有要求,显存越大越好。我自己的经验是:先在Hugging Face上找量化过的模型,优先考虑参数量中等、社区活跃的版本,不要一上来就塞一个几百B的巨无霸。本地部署的核心是“能跑起来”优先,“效果更好”其次,现阶段不少商用模型接口已经足够好用,除非必要,不建议轻易走本地路线。
2.3 AI编程辅助:PyCharm AI插件与代码补全的配合
再说一个很多人忽视的“隐形工具”——AI编程助手。我用的是PyCharm,装了一个官方的AI插件,另外开了GitHub Copilot做补全。这两者定位完全不同:Copilot管“行级补全”,写繁复的样板代码很快;AI插件管“对话式分析”,适合让它解释报错、生成单元测试、重构函数。
但我也想提醒一件事:AI编程助手解决的是手速问题,不能解决判断问题。它生成的提示词模板、参数配置、异常处理逻辑,看起来很像回事,但经常埋着逻辑漏洞。有一次我让它帮忙写一个带重试的调用函数,它确实写了重试,但重试时没有退避,也没检查每次响应是不是真的成功,结果在限流场景下把错误全吞了。从那以后我给自己定了个规矩:AI写的代码,必须逐行读懂,并且用真实输入跑一遍测试。
3. 核心知识拆解:提示词、智能体、工作流
3.1 提示词工程的基础逻辑:指令、约束、示例、格式
说到提示词工程(Prompt Engineering),很多人觉得它就是“把问题问清楚一点”。对了一半。真正工程化的时候,提示词不只是一个句子,而是一套数据结构。我常用的模板分四个部分:
【系统指令】你是负责文本整理的中文助手,只输出处理后的结果,不解释过程。 【用户输入】请把下面这段文字转成三条要点列表,每条不超过20字。 【约束条件】不要使用感叹号和问号;不要输出任何额外内容。 【输出格式】每条要点以“- ”开头。为什么把约束和格式单独拆出来?因为模型对模糊的口语化描述容易按自己的理解发挥。你写“请帮我润色一下”,它不知道你要正式还是活泼、要不要保留口语词;但你写了“每段开头不要重复”这类具体约束,输出稳定性会明显提升。
另外还有一个核心技巧:给示例。给一个输入和输出的配对示例,效果往往比多写三句抽象说明都好。这本质上是让模型在生成时有一个“形态复刻”的锚点。我在项目里专门做过实验,同一个任务,不带示例的准确率大约七成,带一个示例后几乎稳定在九成以上。
3.2 用智能体思路组合模型能力:从单次调用到自主循环
一次调用解决不了所有问题,这时就要上“智能体”(AI Agent)。我之前一直以为智能体是某种神奇框架,后来自己动手写了一个极简版本才明白,它的本质就是一个循环:先观察结果,再决定下一步动作,然后调用工具,再观察,直到任务完成。
我在05-agents-workflow目录里留了一个最简实现,核心逻辑只有不到一百行:
messages = [{"role": "system", "content": system_prompt}] for step in range(max_steps): response = client.chat.completions.create( model=config["model"], messages=messages, tools=tool_definitions, ) if response.choices[0].message.tool_calls: # 解析工具调用,执行真实函数,把结果追加到对话 handle_tool_call(response, messages) else: final_answer = response.choices[0].message.content break这个循环看起来简单,但工程点全藏在细节里:工具调用协议怎么定义参数?执行函数出错后怎么把错误信息传给模型?循环最多跑多少步才能防止死循环?每一步都涉及大量打磨。我的建议是,第一次接触智能体时,不要上来就套LangChain之类的重型框架,先手写一次这个循环。手写过了,再去看框架的抽象设计,立刻就能看懂它为什么那么设计。
3.3 AI工作流编排:把非结构化需求变成结构化任务
把多个模型调用、多个工具调用组合成一条生产线,就是AI工作流。举个例子,做一个短视频文案生成器,单独调一次模型只能得到一段文案;但引入工作流之后,流程可以拆成:第一步先根据主题生成三个标题备选;第二步让模型对每个标题打分;第三步选最高分的标题扩写正文;第四步把正文拆成适合口播的短句。每个步骤的输出都作为下一步的输入,步骤之间可以插入规则判断。
工作流的意义不只是“多调几次模型”,而是把不确定性控制在局部。比如第四步的“拆短句”如果交给模型自由发挥,它可能会漏掉重点;那我可以在第四步后面加一个正则校验,逐条检查结果里是不是包含了标题里的关键字。规则可以兜住模型的底,模型可以为规则补充灵活性,两者配合才是工程实践。
我推荐使用json作为步骤间的传输格式,因为大部分模型输出都能被解析成JSON字符串,后续处理方便。但解析JSON时一定要用异常捕获,别假设模型每次都会输出合法JSON。真实情况是,模型可能在你格式要求极严时,还是顺手输出一段Markdown代码块包裹的假JSON,这种坑我踩过不止一次。
4. 实战过程:从零写一个可运行的AI助手
4.1 目标与需求:我要做一个什么样的小工具
理论讲再多,不如动手做一个完整的小工具。我们的目标定为:一个“会议纪要整理助手”。它接收一段原始会议记录文本,输出结构化的Markdown文档,包含会议主题、讨论要点、待办事项三个部分。
为什么选这个需求?因为它麻雀虽小五脏俱全:需要读取输入文本、调用大模型、要求输出结构化数据、又要处理“模型输出跟格式要求不匹配”的常见故障。整个工具不依赖数据库,不依赖前端,能在一杯咖啡的时间里跑通,非常适合作为第一个AI应用的起点。
4.2 手敲实现:核心代码与关键参数说明
我一上来把需求拆成了三块:读取输入、构造调用、解析输出。环境用之前配好的虚拟环境,模型接口统一走LLMClient。
import os import json from dotenv import load_dotenv from openai import OpenAI load_dotenv() client = OpenAI( api_key=os.getenv("LLM_API_KEY"), base_url=os.getenv("LLM_BASE_URL"), ) system_prompt = """ 你是一个会议纪要整理助手。 请根据用户提供的原始会议记录,输出JSON格式的结构化结果。 要求: 1. meeting_topic: 一句话概括会议主题。 2. discussion_points: 讨论要点列表,每条不超过30字。 3. action_items: 待办事项列表,每条包含负责人和事项。 不要输出任何解释性文字,只输出JSON对象。 """ def summarize_meeting(raw_text: str) -> dict: completion = client.chat.completions.create( model=os.getenv("LLM_MODEL"), messages=[ {"role": "system", "content": system_prompt}, {"role": "user", "content": raw_text}, ], temperature=0.3, ) content = completion.choices[0].message.content return parse_json_result(content) def parse_json_result(content: str) -> dict: if content.startswith("```"): content = content.strip("`") content = content.removeprefix("json").strip() try: return json.loads(content) except json.JSONDecodeError as e: # 真实项目里这里是重试逻辑入口,先简单抛异常 raise RuntimeError(f"模型输出不是合法JSON: {e}")注意这里temperature=0.3,我刻意调低了随机性,因为会议纪要追求的是稳定格式和事实摘要,不是发散创作。如果做文案灵感生成,温度会调高到0.8甚至1.0。这两个场景在同一套代码里,只是参数不同,效果完全不同。
调通第一版后,我发现实际输入里经常有大量口语词、碎片话,模型会把那些“嗯嗯啊啊”也整理进讨论要点。于是我在用户输入之前加了一步清洗,把常见语气词和重复词用正则先过滤掉。这又印证了一个判断:AI应用里,一部分问题根本不需要模型解决,用传统代码处理更便宜、更稳定。
4.3 联调与迭代:如何根据输出反推Prompt修正
第一次跑通的结果并不理想:输出JSON里action_items存在,但缺少负责人字段;而且会议主题抓偏了。这时候才开始真正有价值的迭代。我的方法很简单——把错误输出当成诊断信息,反向去改提示词。
第一步,我看输出里JSON格式是对的,说明格式约束生效了;但待办事项缺负责人,说明提示词里“每条包含负责人和事项”不够明确。于是我改成了这样的约束:
action_items: 待办事项列表,每个元素必须包含lowner(负责人)、task(任务)、due_date(截止日期或“待定”)三个字段。第二步,主题抓偏是因为原始记录里有大量寒暄内容。我在用户输入前面加上一句:“忽略寒暄和与业务无关的闲聊,只关注实质内容。”这一句看着不起眼,实际作用极大。
改完提示词再跑,结果稳定了不少。但我没停在这里,而是继续做了两个增强:一是用retry机制,当JSON解析失败时,把报错信息拼接进系统提示词,让模型自己修正;二是把关键输入截断到2000字以内,避免长文本把输出质量拖垮。这两个增强让工具从“在演示时能用”变成了“实际能用”。
5. 问题定位与调试思路:AI应用开发的真正门槛
5.1 上下文溢出与输出截断
我做过一个批量总结的脚本,输入是几十份长文档,跑着跑着就突然崩溃。排查后发现,不是程序崩溃,而是模型在超过上下文窗口后直接报错,或者输出在某个位置被硬截断,后面的内容丢失。上下文管理是AI工程最常见的问题,没有之一。
解决办法分两层。第一层是输入侧:在调用模型之前,先估算文本的token数量,超过阈值就做分段处理,比如按标题分块,每块单独调一次模型,最后再合并结果。第二层是输出侧:把max_tokens设得足够大,同时提示模型“先输出结论,再补充细节”,这样即使被截断,核心内容也在。
我还加了一个简单函数来估算token数,大概是中文字符数乘以1.5到2,这是按常见模型的分词行为粗略估的。虽然不精确,但用来做阈值判断足够了。关键是不要等到进了模型才报错,在代码里提早拦下来。
5.2 幻觉、重复和格式漂移
模型一本正经地编造不存在的会议结论,这就是“幻觉”。这没法彻底根除,只能缓解。我的经验是:给模型的上下文里放入越多可验证的原始素材,幻觉越少。所以提示词里要写“只能基于用户提供的材料作答,材料中没有的信息一律输出‘未提及’”,这句话能有效压低编造概率。
重复也很常见,尤其是生成长文本时,模型会在后半段反复念叨同一句话。把temperature调低一点、把frequency_penalty调高一点,通常能缓解。格式漂移则是模型跑着跑着突然不用"而用",或者多个字段不按顺序来了。应对方式是用解析后的schema校验兜底,不合法就走重试,而不是相信模型每次都老实。
5.3 限流、超时与异常恢复
接在线API时,限流和超时是绕不开的。多线程并发调用比单线程更容易触发限流。我在项目里推荐一个简单的“指数退避重试”策略:第一次失败后等1秒再试,第二次等2秒,第三次等4秒,最多重试5次。这比自己瞎写time.sleep(1)要可靠得多,也避免了对服务端造成额外压力。
超时设置也要分场景:普通对话请求给30秒足够;但碰到需要长思考的复杂任务,建议调低到10秒内快速失败,改用异步队列异步处理。真实项目里,同步调用+异步任务队列是两种互补模式,而不是非此即彼。
5.4 成本与性能平衡策略
最后把账算算。我做过一个统计:一个客服话题分类应用,如果用高精度大模型给每条消息做分类,月成本会追上服务器费用。后来我把方案改成“先用小模型做初筛,遇到低置信度的样本再升级到大模型”,成本直接降了一半还多,准确率几乎没有下降。
这个策略叫“链路分级”,它背后的思路是:不要让每一条请求都为最坏情况买单。性能调优也一样,prompt里把示例压缩到最短、能用缓存的结果不重复调用、批量请求尽量合并成一个请求批次,这些都是实打实的优化手段。
6. 事后复盘:几条让我少走弯路的经验
这个ai-engineering-from-scratch项目做到现在,对我最有用的不是某个代码片段,而是几条做事原则。第一条,任何模型调用都要放在自己封装的函数后面,别让第三方SDK直接散落在业务代码里,否则换一个模型版本,你会想原地离职。第二条,一定要准备好“模型不配合”的应急预案,JSON解析失败要重试,上下文超限要分段,并发受限要排队,这些不是极端情况,而是日常。第三条,传统代码能做好的事,就不要交给模型,正则、字典映射、规则判断既便宜又稳定,模型的价值在于处理“规则定义不清楚”的语义问题,而不是取代所有代码。
我最近还在做一些扩展,比如把智能体循环里的工具调用改成异步并发、把工作流的每一步都加上耗时和token统计、把提示词模板变成可以挂在配置中心的参数。每一步都是小而实用的改动,但合在一起,这个项目才像是真正可落地的AI工程,而不是一组脆弱的脚本。
如果你也想照这个路线走一遍,我的建议很简单:别收藏太多资料,先照着配好环境跑通一个几十行的调用,然后去改它、去拆它、去让它出错。出错一次,学到的东西比看十篇文章都多。这就是“从零开始”最大的价值——你不只是学会了工具,而是有了自己的判断力。