做AI工程这件事我真正上手到现在快三年了。看到“ai-engineering-from-scratch”这个项目标题我第一反应就是共鸣它不是在讲某个模型多聪明而是在讲一条路——从一个什么都不懂的状态出发怎么一步步把AI能力做成真正能上线、能维护、能评估的工程系统。这类内容在技术社区里通常以仓库、课程笔记或者内部训练手册的形式出现如果你正准备入行或者已经在用AI但总觉得“离工程还差点意思”这份笔记就是照着这条路走下来的完整过程。1. AI工程的第一步是重新定义问题很多人问我AI工程和“玩模型”到底有什么区别。我说差别不在工具而在目标。玩模型的目标是“让它跑起来”出个结果发朋友圈就完事了。工程的目标是“在限定的资源、限定的时间、可维护、可评估的前提下稳定交付一个能解决业务问题的系统”。同一个项目前者一天能出Demo后者要先写设计、拆任务、定指标然后才开始写代码。这个过程就是“ai-engineering-from-scratch”和普通调用AI接口之间最本质的差异。1.1 先分清算法思维和工程思维我见过不少从算法转过来的朋友习惯了“我把loss降下来就算完成任务”但工程视角完全不同。举个例子你拿到一个需求“帮我看一下合同”这句话在工程里根本没法执行。你要拆成输入是什么扫描件还是Word扫描件要不要OCR输出是什么一段摘要还是结构化的JSON字段错了会怎样抽取错了是人工复核还是后续流程直接依赖这个结果这三个问题答案不同方案就完全不同。如果只是摘要调一个商用API可能五秒钟搞定如果要结构化抽取关键条款且错误会直接影响财务流程那就要做数据标注、效果评测、兜底规则、审计日志甚至要接人工复核节点。用做菜类比算法思维是研究火候工程思维是开一家能天天出餐、客人等太久会投诉、食材用完能补货的餐厅。前者是科学后者是系统工程。1.2 先画能力边界再谈模型选型我在项目里养成了一个习惯接到需求先不碰代码画一张“能力边界图”。横轴是输入类型纵轴是输出要求再用几个词标出容错率。这一步能避免大量无用功。举个例子“AI自动生成地形”这个需求听起来很酷但如果你不问清楚“生成的地形用于游戏场景还是用于地质模拟”做出来的方案可能完全跑偏。游戏场景要的是美观、随机、低延迟可以用生成式方法跑实时推理地质模拟要的是符合物理规律那就得走数值模拟路线AI只做参数拟合。同一个需求词边界不同技术栈完全不同。AI工程里最贵的错误不是代码写错而是问题定义错了。代码错了修复只要几小时方向错了返工要几周。1.3 给自己定一条从零开始的主线如果你真的是从零开始不要指望一个月学会所有东西。我建议按四个阶段推进每个阶段都有一个“可验证的结果”作为里程碑第一周跑通一个最小的模型调用闭环不管是云API还是本地模型写一个脚本能输入文本、输出文本。第二周做提示词工程把固定任务的输出质量调到“可接受”并写清楚Prompt的版本。第三周接数据做一个带检索的问答或文档处理流程把“上下文”的概念落地。第四周写评测脚本和简单的部署接口把项目从脚本升级成服务。每个阶段结束都问自己一句现在这个东西如果交给别人用需要配多少使用说明如果答案是“只有我能跑”那说明工程化还没完成。2. 环境与最小闭环先把模型跑起来不管你的目标多宏大AI工程的第一步永远是“让模型在你的机器上或者你的账号里跑起来”。这一步看似简单但我在带人时发现几乎所有初学者都会卡在环境问题上Python版本不对、依赖冲突、CUDA装不上、模型下载到一半中断。所以我强烈建议第一周不要上难度跑通一个最小闭环比什么都重要。2.1 云API还是本地部署别盲目跟风现在很多人一聊AI工程开口就是“本地部署”。但本地部署不是目的而是手段。我给一个很实际的选型逻辑只想学习和验证想法用云API最快按量付费不需要买显卡。数据敏感、必须内网运行或者长期高频调用算下来云API太贵才考虑本地部署。想深入理解模型推理原理本地部署一个小模型7B-14B级别足够你折腾没必要一上来就弄70B的大模型。我把常见选择整理成一张表方便对照方案起步成本单次调用成本隐私性学习价值推荐场景云API低按Token计费低数据出网中快速原型、业务验证本地Ollama中看硬件电费高高学习、离线环境、隐私项目自建推理服务高固定设备折旧高非常高大规模生产、深度定制我看到不少人在“本地部署”上花了太多时间结果一个月过去还在调显卡驱动。如果你是初学者第一次请先用云API跑通业务逻辑等你要交付了再考虑换本地部署也不迟。2.2 用Ollama跑起本地模型五分钟出结果如果你决定走本地部署我建议先用Ollama因为它把模型管理、量化、推理服务都封装好了不需要手写CUDA代码。以一台16GB内存的普通开发机为例# 安装之后拉取一个7B级别的中文指令模型 ollama pull qwen2.5:7b-instruct # 启动本地服务默认端口11434 ollama serve拉完模型后Ollama会自动暴露一个OpenAI兼容的REST接口用Python就能直接调用import requests resp requests.post( http://localhost:11434/api/chat, json{ model: qwen2.5:7b-instruct, messages: [ {role: system, content: 你是资深AI工程顾问回答要简洁。}, {role: user, content: 什么是提示词工程} ], stream: False, options: { temperature: 0.7, max_tokens: 1024 } }, timeout120 ) print(resp.json()[message][content])我特意把temperature和max_tokens亮出来因为这两个参数是初学者最容易忽略的。temperature控制随机性max_tokens控制最长输出长度。做抽取类任务时我会把temperature降到0.2以下让输出稳定做创意文案时才会调高到0.8以上。2.3 工程化的第一步是把调用封装成可切换的客户端跑通上面的代码之后很多人的习惯是直接把这段代码复制到各处用。这恰恰是“非工程”的写法。你要做的第一件工程化改造是把模型源变成可配置项。我一般会写一个LLMClient类支持通过环境变量切换Ollama、OpenAI兼容服务、或者未来要换的其他端点import os import requests class LLMClient: def __init__(self, providerollama): self.provider provider self.api_url os.getenv(LLM_API_URL, http://localhost:11434/v1/chat/completions) self.api_key os.getenv(LLM_API_KEY, EMPTY) self.model os.getenv(LLM_MODEL, qwen2.5:7b-instruct) def chat(self, messages, temperature0.7, max_tokens1024): headers {Authorization: fBearer {self.api_key}} payload { model: self.model, messages: messages, temperature: temperature, max_tokens: max_tokens } resp requests.post(self.api_url, jsonpayload, headersheaders, timeout120) return resp.json()[choices][0][message][content]这一小步的意义不是“代码更优雅”而是从今天起你的AI代码不再和某个具体平台绑死。今天用Ollama明天换成API只需改环境变量。这放在生产环境里是必须的否则你每次换模型都要全局改代码。3. 提示词工程与上下文管理AI应用的地基跑通模型之后你很快会发现一个真相模型本身只是引擎真正决定业务质量的是你怎么组织输入输出。提示词工程Prompt Engineering在这个阶段就是一个绕不开的核心技能。我见过太多人把提示词工程理解为“把话写得详细点”但真正做工程它需要系统性和可评测性。3.1 提示词的“四层结构”我总结过一套提示词的“四层结构”每次都按这个顺序去写能减少大量无效返工角色模型以什么身份回答问题。不是简单地加一句“你是一个专家”而是要说明这个角色拥有的信息边界。任务一句话说清楚你要它做什么越具体越好。不要说“帮我优化一下”要说“把这段产品简介压缩到100字以内保留功能亮点和适用场景”。约束明确不做什么。例如“不要输出Markdown格式”“不要解释你的思考过程”“如果信息不足直接回答不知道”。示例给一个输入和输出的对照样本。这一步对模型效果的影响最大比角色描述重要得多。举个例子我现在写抽取类Prompt模板大概是这样的你是合同审核助手只负责从合同文本中抽取关键信息。 抽取以下字段合同编号、签署日期、合同金额、甲方名称。 输出为JSON格式不要输出任何解释。 如果某个字段在文本中不存在值为null。 示例 合同文本甲方北京某某科技有限公司与乙方上海某公司于2023年5月1日签署编号HT-2023-001合同金额为人民币五十万元整。 输出{contract_no: HT-2023-001, sign_date: 2023-05-01, amount: 500000.00, party_a: 北京某某科技有限公司}为什么示例如此关键因为当前主流大模型的训练方式决定了它们非常擅长“模式补全”你给出示例它就是在做模仿。没有示例它靠猜测有了示例它靠对照。这是提示词工程里性价比最高的投入。3.2 Token预算别让模型“忘了”前面的内容提示词工程不光是写话术还要算内存账。每次调用模型时上下文窗口是有限的。假设模型上下文是8192 Token系统提示词占500历史对话占2000输出预留1000那你真正能用来放“参考资料”的空间只有不到5000 Token。这里有个工程公式我每次都用可用上下文 模型窗口总大小 - 系统提示词长度 - 历史对话长度 - 输出预留长度初学时最容易犯的错是把整篇文档塞进Prompt结果发现模型“答非所问”。这不是模型笨是它把前面的内容“遗忘”了。解决思路有三个截断、压缩、检索。截断最直接但丢失信息压缩对长文档有效但增加一次前处理调用检索RAG是最工程化的方案后面会专门讲。3.3 AI Agent从单轮问答到多步任务当你把单轮问答做稳了自然的下一步是让AI“做事”而不是“回答问题”。这就是AI Agent的范畴。我理解的Agent核心是一个循环模型根据目标思考下一步要调用什么工具然后执行工具再根据结果决定是继续还是收尾。这个结构并不神秘可以用一个极简的伪代码表达def agent_loop(task, tools, llm, max_steps5): messages [{role: system, content: 你是一个能调用工具完成任务的小助手。}, {role: user, content: task}] for _ in range(max_steps): reply llm.chat(messages) action parse_action(reply) # 解析出工具名和参数 if action.is_final(): return action.output result tools[action.name](**action.args) messages.append({role: assistant, content: reply}) messages.append({role: tool, content: str(result)}) return max steps exceeded真正生产级的Agent要比这复杂得多但核心循环不会变。这里值得参考的是行业里公开的一些智能体训练思路比如通过可验证的结果反馈来强化模型的规划能力——把“任务完成得对不对”变成一种可计算的奖励信号。工程侧对应的做法是给Agent加上结果校验器不让它无限试错而是让校验器判断当前结果是否满足要求不满足就重规划满足就输出。这套“校验-重试-终止”机制比单纯依赖模型的自觉要可靠得多。3.4 把Agent变成可复用工作流Agent灵活但也正因为灵活它不可控。工程上我不建议直接在生产环境放一个自由度很高的Agent而是把高频路径固化成工作流Workflow。工作流像餐厅的标准化SOPAgent则像自由发挥的私厨各有各的场景。一个典型的工作流节点链可能是内容分类 - 生成摘要 - 提取结构化信息 - 写入数据库。每一步都是独立的模块模块之间通过明确的输入输出契约连接。这样做的最大好处是出错时你能定位到具体节点而不是对着一个黑盒束手无策。4. 工程落地实战从Demo到可交付系统我从一开始就强调“可交付”。什么叫可交付别人按照你的文档部署之后不需要你现场指挥系统能自己稳定运行出了问题有日志可查。这一节我们用一个文档问答应用作为贯穿案例把从框架选型到部署观测的全流程走一遍。4.1 框架选型别被工具绑架现在市面上AI应用框架一大堆Spring AI、LangChain、LangGraph、LlamaIndex还有各种国产编排平台。我选框架的标准只有一个团队里大多数人能上手维护。Java团队优先考虑Spring AI因为它的抽象风格和Spring生态一致接入已有的Spring Cloud基础设施很顺。Python团队、追求快速原型LangChain/LangGraph资料多、社区大适合验证想法。如果你的团队很小业务逻辑也不复杂我建议连框架都别上直接用HTTP封装函数调用反而维护成本最低。PyCharm这类IDE自带的AI插件也可以用起来但定位是“写代码时的辅助”不是“工程架构的一部分”。我做这个项目时用AI插件辅助写了不少样板代码但架构决策始终是自己定的插件生成的代码我也会逐行审查。4.2 文档问答应用的完整流水线文档问答是入门AI工程最好的练手项目。它的技术栈覆盖了解析、切分、向量化、检索、生成、评测几乎跑了一遍AI工程的核心环节。我一边实现一边讲参数选择的理由# 1. 文本解析 from langchain_community.document_loaders import PyPDFLoader loader PyPDFLoader(contract.pdf) pages loader.load() # 2. 文本切分关键参数chunk_size和overlap from langchain.text_splitter import RecursiveCharacterTextSplitter splitter RecursiveCharacterTextSplitter( chunk_size512, # 每个块大约512字符 chunk_overlap50 # 前后重叠50字符避免语义断裂 ) chunks splitter.split_documents(pages)chunk_size为什么是512不是2048因为向量检索的“精确度”是跟块长度强相关的。块越长一个块里包含的无关信息越多检索命中的“精准度”就越差。块太短比如128又会把完整语义切开。512是我在大量中文文档上试出来的一个平衡点遇到明显段落边界较大的文档我会调到768。接下来是向量化和检索。不要把向量化也想得太神秘它就是给每段文本计算一个语义向量检索时计算问题向量与文本向量的相似度。# 3. 向量化与检索 from langchain_huggingface import HuggingFaceEmbeddings from langchain_community.vectorstores import Chroma embeddings HuggingFaceEmbeddings(model_nameBAAI/bge-small-zh-v1.5) vectorstore Chroma.from_documents(chunks, embeddings) # 4. 检索时返回最相关的4个块 docs vectorstore.similarity_search(query, k4)这里k4是另一个值得解释的参数。检索返回的块数量不是越多越好因为生成阶段所有返回块都会塞进上下文块太多会稀释关键信息也会增加Token消耗。4到6个块在大多数文档问答场景下是性价比最高的区间。最后把检索结果拼进Prompt交给大模型生成回答。整个RAG检索增强生成流程就闭环了。相比直接把整篇文档塞给模型RAG的响应速度更快、引用更准确而且文档更新时只需要重建向量库不用重训模型。4.3 没有评估就没有优化很多项目的失败点在“感觉”感觉回答变好了感觉效果不错。但感觉不能上线。我在自己的项目里强制要求任何Prompt改动和流程改动都必须跑一遍评测脚本对比。具体做法不复杂准备20到50条典型的测试问题这个集合叫Golden Set。每一条标注标准答案或评分标准。模型回答后用大模型当裁判LLM-as-judge打分或者用关键词/语义相似度算分。对比改动前后的分数用数据说话。下面是一个极简的评估脚本思路from rouge_score import rouge_scorer scorer rouge_scorer.RougeScorer([rougeL], use_stemmerTrue) def evaluate(questions, answers, reference_answers): total_score 0 for q, pred, ref in zip(questions, answers, reference_answers): scores scorer.score(ref, pred) total_score scores[rougeL].fmeasure return total_score / len(questions)Rouge-L只是词面重叠的一个近似更全面的评估还要加入语义相关性、忠实度是否基于检索内容而不是编造。但机制比指标更重要你有了这套评估机制每次修改都能量化效果而不是靠玄学。4.4 部署与观测最后十公里的坑Demo做完部署又是一个深坑。我踩过最典型的问题有两个一是流式输出做不好用户等十秒才看到完整回答体验很糟糕二是完全没有日志模型开始胡说八道时根本不知道是哪次请求、哪个参数引发的。工程上至少要补三件事用FastAPI包一层HTTP接口开启流式输出。记录每次请求的模型名、输入Token数、输出Token数、延迟、temperature参数。加一个简单的限流防止内部调试脚本把服务打挂。from fastapi import FastAPI from fastapi.responses import StreamingResponse import json app FastAPI() app.post(/chat) def chat(request: dict): messages request[messages] def generate(): # 模拟流式返回 yield 正在处理 result llm_client.chat(messages) yield result return StreamingResponse(generate(), media_typetext/plain)日志这块我用的方案很简单写一个装饰器把每次调用的关键信息追加到结构化日志文件里。上线后你一定会感激自己多写的这几十行代码。5. 常见问题与排查技巧实录写到这里我不打算回避那些坑。这一节是实战记录把我在“ai-engineering-from-scratch”这条路线上反复踩过、也帮别人排查过的问题列出来附上排查思路。5.1 本地部署显存不足和速度慢本地部署最不缺的就是问题。我的经验是部署前先查清楚参数规模和量化等级。拿7B模型来说不同的参数精度对显存的占用差别很大模型与量化显存占用估算适用硬件Qwen2.5 7B Q4_K_M约5GB8GB显存可跑Qwen2.5 7B FP16约14GB16GB显存起步Qwen2.5 14B Q4_K_M约9GB16GB显存可跑Qwen2.5 14B FP16约28GB32GB显存起步如果显存不够优先考虑4-bit量化模型不要硬塞FP16。如果是纯CPU跑7B模型速度会很感人但你要知道这个限制是正常的别以为是自己配置错了。用Ollama时可以用OLLAMA_GPU_DISCOVERY环境变量控制GPU也可以用ollama ps查当前加载了哪些模型。5.2 上下文越长效果反而越差很多人以为给模型的信息越多回答越准确。实际上LLM对长上下文的中间部分注意力是弱的你把海量资料塞进去它可能会“看漏”关键信息还会提高Token成本。解决办法不是硬堆而是先检索后生成。把“你要找的信息”从长文中筛出来再喂给模型。我们在文档问答里的RAG流程就是这个思路。如果检索不到好的片段宁可让模型说“资料中没有相关信息”也不要强行编一个答案。5.3 提示词越改越乱把Prompt当成代码管我见过最典型的“提示词崩溃”场景是改到第三版之后之前调好的任务反而变差了。原因是没有版本管理。Prompt和代码一样应该纳入版本控制而且要有测试用例。我的习惯是每个Prompt文件都带一个examples/目录里面放输入和期望输出。每次改Prompt跑一遍全部示例任何一个不通过就打回重调。这是工程化思维在提示词领域的具体应用。你不需要花大价钱买提示词管理工具一个Git仓库配一个测试脚本就足够了。5.4 开源模型和闭源API的选择误区不要因为“开源免费”就倾向本地部署。开源模型的完整成本包括显卡折旧、电费、运维时间、效果调优投入。把台账算清楚再决定。我见过一个团队花了三周部署了一个本地模型效果还是比不上商用API最后回去用API了。这并不是说本地部署不好而是它的价值在隐私、离线、可定制不在“免费”两个字。反过来如果你要做深度定制微调那闭源API就帮不上忙本地开源模型是更合理的选择。关键还是要回到需求本身。6. 最后我个人的一点体会沿着“ai-engineering-from-scratch”这条路线走下来我最大的体会是从零开始做AI工程最难的并不是技术本身而是把一个模糊的想法拆成一个个可验证的小任务。第一周只解决“模型能不能跑”第二周只解决“输出质量能不能稳定”第三周只解决“数据能不能接上”第四周只解决“服务能不能交付”。每个阶段都是一个小闭环每个闭环都有明确的产出。这些年我帮很多人看过项目凡是陷入泥潭的几乎都是因为想一步到位、跳过了某个环节。如果你也在走这条路我的建议是不要追求第一版就完美先把最小闭环跑通再谈优化。跑起来你就已经超过大多数停留在“想”的人了。