1. 从零开始做AI工程,先想清楚你要解决什么问题
最近“AI engineering”这词被讨论得非常热,几乎每周都有新的工具、新的框架、新的最佳实践冒出来。但真当你想自己从零开始上手时,会发现网上教程要么是某一框架的API说明书,要么是单点技术(比如某个模型怎么微调、某个RAG组件怎么用)的孤立演示,很少有人能告诉你一条完整的、经历过真实业务检验的路径。
我花了不少时间把“ai-engineering-from-scratch”这件事捋了一遍,结合自己的实操踩坑经历,写下这篇东西。它不是某个框架的入门教学,而是一套我验证过的、把一个AI想法从“运行在笔记本上的Demo”变成“能交付给真实用户使用的系统”的方法论。这套方法论的核心我认为是“工程闭环”——问题定义、数据、模型、评估、部署、迭代,六件事环环相扣,缺一个,项目大概率会烂尾。
先说一个反直觉的结论:从零开始做AI工程,第一步绝对不是装环境、跑模型,而是把“我想做一个AI产品”翻译成“我要解决什么具体问题、用什么指标判断做成功了”。我见过太多人卡在第2周就放弃,原因不是技术难度,而是他根本不知道自己为什么要做这件事。
这篇内容适合几类人:刚入门但不想走弯路的AI学习者、需要把AI能力集成到现有产品里的工程师、以及想系统梳理AI工程体系的技术负责人。如果你已经有经验,这篇文章里的排查实录和架构取舍也可以作为对照参考。
2. 从零开始的全局架构:AI工程不是“训练模型”这一个环节
2.1 先拆解AI工程的实际构成
很多人把AI工程等同于“训练模型”或者“调API”,这是个非常大的误解。一个能上线的AI系统,通常由六个环节组成,缺一不可:
- 问题定义层:你要解决什么业务问题?这个问题的输入和输出是什么?边界在哪里?
- 数据工程层:数据从哪里来?怎么清洗?怎么切分训练集/验证集/测试集?如果做RAG应用,知识库的切分和处理是核心。
- 模型基座层:用大模型API还是开源模型自部署?选哪个规模和版本?需不需要微调?
- 推理服务层:模型怎么部署?用什么框架做推理加速?如何处理上下文窗口和并发请求?
- 评测观测层:怎么定义“回答得好”?用离线评测集还是在线用户反馈?怎么追踪模型输出的质量和成本?
- 迭代闭环层:根据评测结果,反向调整数据、提示词、检索策略或模型,形成飞轮。
这六个环节有一个很深的逻辑关系:前面的环节出错,后面的环节无法弥补。我遇到过数据标签有明显错误还强行微调的案例,结果模型训练完,loss曲线很好看,但业务指标全面恶化。原因很简单——垃圾进垃圾出,模型学到了错误模式,甚至把错误模式泛化了。所以这六个环节不是“可选步骤”,而是“必须走的完整链路”。
2.2 为什么从零开始最容易踩的坑是“没有终点”
做传统软件开发,需求评审画个原型就能对齐预期。但AI项目有个特性:模型输出的质量是不确定的,一开始谁也无法保证最终效果是什么样。
没有终点具体体现在:你不知道做到什么程度算“好”。比如客服机器人,有用户说“能回答常见问题就行”,但等做出来,业务方发现回答的准确率到不了95%,又说“这个没法上线”。这其实就是没有在项目开始前定义好评测标准和可接受的性能区间。
要避免这个局面,我在项目定义阶段会强制做三件事:
- 写清楚用户使用截图级别的输入输出示例,至少10组。
- 定义清楚“不可接受的坏输出”长什么样,比如幻觉编造价格、答非所问。
- 和业务方对齐一个明确的验收指标,比如“常见问答的准确率超过85%,且无严重幻觉事件”,并把这个指标落实到离线评测集上。
这本质上是一种风险前置。如果你自己拿不定主意,可以用一个假想的用户故事来推演整个流程,看看在哪个环节可能翻车。这个推演过程非常有用,能帮你提前意识到数据、成本、延迟这些约束条件。
2.3 技术栈选型:从零开始时别追求“最先进”,要追求“最少依赖”
我见过不少项目死在技术栈选型上:一上来就要基于Llama 3做全量微调,甚至要自己从零预训练,结果算力开销巨大、数据也不够,项目一个月就搁浅了。
我的建议是:能用成熟API解决的,就不要自己训练模型。RAG(检索增强生成)、提示词工程、工作流编排可以解决绝大多数业务需求的80%,而且能快速上线验证。只有当你明确知道API基座模型在特定垂直领域表现不稳定,且你有足够的高质量数据时,才去考虑微调这条路。
一个务实的从零开始技术栈(基于我的实际验证)是这样的:
| 环节 | 推荐选择 | 理由 |
|---|---|---|
| 开发语言 | Python | 生态最全,AI相关库基本都有Python接口 |
| 环境管理 | uv 或 conda | 依赖隔离干净,避免不同项目互相污染 |
| 大模型API | 主流的GPT系列或Claude系列 | 效果稳定,无需考虑GPU运维 |
| 开源模型备选 | Qwen系列 | 中文效果优秀,社区生态活跃 |
| 向量数据库 | 先用Milvus或者Chroma,量大了再换 | 前期数据量小,不需要专业分布式引擎 |
| RAG框架 | LlamaIndex或LangChain,建议先用LlamaIndex | 编排逻辑比LangChain更清晰,上手成本低 |
| 应用框架 | FastAPI | 写API服务非常轻量,性能足够 |
| 部署方式 | Docker + 云服务器 | 一台4核16G的机器就能跑得很舒服 |
这套组合的核心理由是“最少依赖原则”:每个组件都经过验证,组合在一起没有明显的兼容性问题,并且每一层都可以被替换。你在看技术选型时,不要被“Kubernetes + GPU集群”这种高端方案迷惑,从零开始最重要的是快速跑通闭环,之后再按需演进。
3. AI工程落地实操:从环境搭建到第一个完整闭环
3.1 环境搭建:用uv把Python环境问题一次性解决
Python环境问题一度是我最烦躁的事情。不同项目要不同版本的Python、不同包的依赖互相冲突,用传统的pip和conda管理,简直是在做依赖考古。
直到我完全切换到uv之后,这个问题才算终结。uv是目前我认为体验最好的Python包和环境管理器,它可以用Rust的速度给你创建一个全新的虚拟环境,并且在几秒内解析出依赖树。对AI工程来说,依赖通常非常多,比如torch、transformers、langchain、milvus等,轻量秒装的感觉会让你心情好很多。
创建AI工程基础环境的实操,大概是这样:
# 安装uv(macOS/Linux) curl -LsSf https://astral.sh/uv/install.sh | sh # 创建新项目目录并初始化 mkdir ai-engineering-from-scratch && cd ai-engineering-from-scratch uv init # 创建虚拟环境并安装核心依赖 uv add python=3.11 uv add openai langchain langchain-openai python-dotenv uv add fastapi uvicorn装完之后,你会发现project里多了一个pyproject.toml,所有依赖都记录在这个文件里,以后换机器直接用uv sync就能一键还原环境。这比requirements.txt更好用,因为依赖被锁定到具体版本,不会有“在我机器上能跑”这类问题。
3.2 准备高质量数据:RAG时代的“数据工程”怎么做
AI工程里,数据工程的占比被严重低估。不管你是做微调还是做RAG,数据的质量直接决定系统的效果上限。做RAG时,最核心的数据工作是对知识库文本进行清洗、切分、嵌入和入库。
我自己经常用一套五人团队的“穷鬼方法论”,不需要大数据团队,自己动手用Python脚本处理数据就行。首先做文档清洗,把PDF、Word、Markdown统一转成纯文本,然后按标题层级做结构拆分。这个结构拆分如果做得粗糙,后续检索质量会急剧下降。以一段几千字的文档为例,我会先按Heading(## / ###)切块,再按段落、句子切成100-300字左右的chunk,相邻chunk之间保留少量重叠(比如用text_splitter的chunk_overlap参数设置为20-50字)。
有个细节容易被忽略:切分时不要硬切句子。我拿默认按字符切分的方案试过,结果很多chunk的语义被拦腰截断,检索到的上下文信息非常破碎。后来我改用按语义切分,用split_by=“markdown_header”或split_by=“sentence”这类工具,效果好了很多。
嵌入模型的选择也会影响检索效果。如果以中文为主,我会优先考虑bge-large-zh-v1.5或者text2vec-large-chinese,这两类模型在中文语义上的表现经过社区验证都比较稳。如果是英文混合中文,用nomic-embed-text或OpenAI的text-embedding-3-small也可以。这里有一个我个人很在意的点:嵌入模型要和你后续的检索策略配套。如果用的是向量+关键词混合检索,那么嵌入模型输出维度最好固定在一个合理范围(比如1024或1536),避免维度太稀疏导致召回率不稳定。
3.3 用LlamaIndex搭一个带检索的QA系统
搭RAG系统,我强烈建议从LlamaIndex开始,因为它的核心抽象是“文档 → 索引 → 查询引擎”,整个链路非常直白。不推荐从LangChain开始的原因是LangChain的组件编排非常灵活,但灵活意味着你要自己理解各种概念,对新手来说容易迷失方向。
一个标准的LlamaIndex检索流程,大致可以写成下面的结构:
from llama_index.core import VectorStoreIndex, Settings from llama_index.embeddings.huggingface import HuggingFaceEmbedding from llama_index.llms.openai import OpenAI from llama_index.core.node_parser import MarkdownNodeParser # 设置嵌入模型和LLM Settings.embed_model = HuggingFaceEmbedding(model_name="BAAI/bge-large-zh-v1.5") Settings.llm = OpenAI(model="gpt-4o-mini", temperature=0.2) # 从文档目录加载知识库 documents = SimpleDirectoryReader("data").load_data() # 按Markdown结构解析节点 parser = MarkdownNodeParser() nodes = parser.get_nodes_from_documents(documents) # 构建向量索引 index = VectorStoreIndex(nodes) index.storage_context.persist(persist_dir="./storage/")这段代码跑通后,你就有了一个可以回答的知识库引擎。但我必须提醒你:这个Demo离“能交付”还很远。最大的问题在于简单向量检索的召回质量不稳定,特别是当用户提问方式与文档原文表达差异较大时,常常检索不到相关chunk。
解决办法有两种:一种是加一层混合检索(BM25+向量),一种是在用户查询后加一层查询改写(query rewriting)。前者的实现几乎已经成为RAG应用的标准配置,把关键词语义匹配和字面匹配都跑一遍,再把两个结果做融合重排(Rerank),整体召回率能提升不少。
3.4 加一个Rerank层:把检索质量再做一次保证
做RAG的人应该都深有体会:向量检索只是初筛,真正的精度提升靠的是重排(Rerank)。原理是这样的:向量检索把Top 100的候选块捞出来,然后Rerank模型再对每个候选块和用户查询做一对一的精细相关度打分,最后只把最高分的Top 5-10送给LLM生成答案。
为什么要做这一步?因为向量检索的“语义相似”和“回答用户问题的相关性”并不完全等价。举个例:用户问“怎么修改密码?”,向量检索可能召回“密码长度要求为8位以上”,字面上语义相关,但它并不直接回答用户的操作步骤。Rerank模型就是用来纠正这种偏差的。
实操层面,我推荐两个工具:bge-reranker-v2-m3(BAAI出品,中文效果很好)或者用Cohere Rerank API(英文数据集效果很好)。用Python接入也非常简单,把检索到的候选chunk列表传给Rerank模型,让它输出重新排序后的列表,再取前N个做生成。
加了Rerank之后,会有个明显的体验变化:模型的回答质量会显得“准很多”,幻觉现象也会下降。因为输入给LLM的上下文更聚焦了,LLM“自由发挥”的空间被压缩。
3.5 构建FastAPI服务:把AI能力变成接口
当你的检索+生成链路在本地脚本里跑通之后,下一步是把它包成一个HTTP服务,这样前端、客户端才能调用。
我一般用FastAPI来做,代码非常干净:
from fastapi import FastAPI from pydantic import BaseModel app = FastAPI(title="AI Knowledge QA API") class QueryRequest(BaseModel): question: str top_k: int = 5 @app.post("/qa") async def qa_endpoint(request: QueryRequest): response = query_engine.query(request.question) return {"answer": str(response), "sources": extract_sources(response)} if __name__ == "__main__": import uvicorn uvicorn.run(app, host="0.0.0.0", port=8000)这里有三个工程细节值得注意:
- 超时控制:LLM调用很慢,必须设置合理的超时时间,否则接口会堆积请求。
- 并发控制:默认的异步接口无法控制内部LLM并发量,如果需要限制并发,可以用
asyncio.Semaphore或者消息队列。 - 速率限制:如果用户请求量不稳定,建议加一个简单的令牌桶算法,避免下游模型API被调用过度。
3.6 离线评测:量化“回答得好不好”
AI工程最容易被忽视但又最必须做的是“评测”。很多人上线后靠感觉判断效果,实际上非常危险。因为没有量化指标,你根本不知道一次代码改动到底让效果变好了还是变差了。
我自己的做法是构建一个“黄金评测集”:准备50-100个典型问题和对应的标准答案,每次改动系统后都跑一遍评测集,用GPT自动打分(LLM-as-Judge),计算回答的准确率、完整率和有害性报告。
用GPT做裁判的prompt大概是这样:
请根据以下标准对AI助手的回答进行打分: 1. 回答是否正确(0-2分) 2. 回答是否完整覆盖问题(0-2分) 3. 回答是否有明显的幻觉内容(0-2分) 4. 回答是否安全无害(0-2分) 总分8分。 问题:{question} 标准答案:{reference} AI回答:{answer}这个评测集的价值会在你后续迭代时充分体现:每次改动,都跑一遍,看分数是涨是跌。分数的波动,就是你决策的依据。迭代不再凭感觉,而是有数字指引。
4. 部署、性能优化与成本控制:工程化的分水岭
4.1 Docker化部署:让环境一致可交付
从零开始的AI工程,做到脚本能跑还不够,必须走到可部署。我在部署环节的第一步永远是写Dockerfile,因为只有容器化才能保证“在我机器上能跑”这句话不再出现。
一份最小可用的Dockerfile长这样:
FROM python:3.11-slim WORKDIR /app COPY pyproject.toml /app/ RUN pip install uv && uv sync COPY . /app/ EXPOSE 8000 CMD ["uv", "run", "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]构建完成后,一条docker build -t ai-app . && docker run -p 8000:8000 ai-app就能把服务跑起来。不用再担心宿主机Python版本不同、依赖冲突的问题。
4.2 推理延迟和数据缓存:体验提升最直接的手段
大模型API的延迟通常在2-5秒之间,这个延迟对很多交互场景来说都不够理想。我实践下来,最有效的两个优化手段是:
- 语义缓存(Semantic Caching):用户经常重复问类似问题,比如“退款政策是什么?”,把这类常见问题的回答缓存起来,用向量相似度匹配,相似度超过0.9就直接返回缓存结果。
- 流式输出:改用
stream=true,让LLM生成的文字像打字机一样逐步呈现,用户感知上的等待时间能缩短一半以上。
语义缓存实现很简单,用一个带向量索引的字典即可:
from cachetools import TTLCache cache = TTLCache(maxsize=100, ttl=600) def get_cached_answer(query): query_emb = embed_model.embed(query) for cached_q, cached_emb, answer in cache: if cosine_similarity(query_emb, cached_emb) > 0.9: return answer return None4.3 成本管控:一个token一个token抠出来的钱
大模型API按token计费,对AI工程而言,成本不是伪命题。经验是:对话历史越长,成本越高,因此要对传给模型的内容做严格的前置裁剪。
- 系统提示词:控制在200个token以内,只写“你是客服助手,回答基于以下知识库”。
- 检索上下文:传给模型的知识片段,只保留Rerank后Top 5的chunk,总量控制在1000-1500 token。
- 历史对话:只保留最近两轮的用户问题,不要全量塞进上下文。
成本控制方面还有一个容易被忽略的点:多个用户请求时,尽量复用系统级检索结果。比如“退货政策”这种问题,知识库内容一样,只是人不同,检索结果可以缓存,避免每次都调嵌入API。
5. 实战中常见的坑与排查技巧
5.1 我实测遇到的三大问题及排查方法
以下问题100%来自我的实操,不是网上抄来的理论,遇到的人大概率会在这里卡住。
问题一:检索不到相关内容,答非所问
现象:模型答非所问,跟知识库的内容完全不沾边。排查顺序是:
- 先看检索到的Top 5 chunk是什么。直接用检索组件的
retrieve接口打印结果。 - 如果chunk里没有相关内容,说明是数据切分太粗或嵌入模型不匹配。
- 如果chunk里有相关内容,但模型没答出来,说明是prompt引导的问题,让模型忽略了context,把提示词改成“只用下方资料回答”。
问题二:模型产生幻觉,编造内容
现象:知识库没有的信息,模型一本正经地编。这个问题的根本原因是模型在生成时过度依赖自身参数知识,而不是检索到的上下文。解决办法有三个:
- 把prompt里加上“如果上下文中没有相关信息,请直接回答‘不知道’”。
- 降低temperature到0.1-0.2。
- 在生成之后加入一层“事实校验”,用原文对比回答中的关键实体,不一致就拒绝生成。
其实有一个更彻底的办法是引入“引用溯源”:要求模型在回答时附带来源文档编号[1][2],这样用户能验证答案。这个操作能大幅降低用户对幻觉的容忍成本。
问题三:接口慢到超时
现象:用户反馈“转圈圈转了半天”。这个问题的典型瓶颈在LLM API的响应速度,跟代码性能关系不大。排查步骤:
- 先分清是网络慢还是检索慢:在日志里给检索和LLM调用各打一条时间戳。
- 如果是LLM调用占了80%以上的时间,直接上流式输出+语义缓存。
- 如果检索本身就超过500ms,考虑向量数据库索引是否没建好,或者chunk数量太大。
5.2 经验总结:小步快跑,用评测指挥改进方向
从零开始做AI工程,我最大的体会是“不要指望一步到位”。一个系统总是在上线之后才暴露出真问题,比如用户问法的多样性、数据更新的频率、知识库覆盖的盲区。正确的做法是构建一个极简的最小闭环,跑起来之后,每周用小步改动去迭代。
我自己的迭代节奏是这样的:
- 每周固定跑一次黄金评测集,记录分数。
- 分析失败案例,把回答错误、检索失败、幻觉案例归类。
- 针对最多的一类问题,采取一次对应优化(改切分、改prompt、加Rerank、补数据等)。
- 重跑评测集,看分数是涨是跌。这就是数据驱动的AI工程节奏。
5.3 最后的实用建议:先让系统跑起来,再做精细雕琢
如果你打算从零开始做一个AI工程,我的建议是先走通最小链路,不要追求完美。先做一个最简单的“文档上传 -> 切分 -> 向量化 -> 检索 -> LLM回答”的闭环,哪怕所有参数都不优化也没关系。
跑通之后,你会发现很多问题是之前想不到的。比如数据格式五花八门、用户的问法比你预料的更多样、模型输出偶尔抽风。这些都是正常的,工程能力就是在这个不断见招拆招的过程中积累起来的。
如果你手边有具体的项目想尝试,可以从你工作里最小的一个痛点开始,把一个问答场景做到极致。我个人体会是:真正让你成长的不是炫酷的模型技巧,而是你如何把一个模糊的想法,变成一个可评测、可部署、可迭代的AI系统。这个过程本身就是“ai-engineering-from-scratch”的全部价值所在。