
1. 把“从零开始”当成一种工程方法“ai-engineering-from-scratch”这个标题准确描述了一种我在很多项目里反复看到的真实状态模型的能力早就不是瓶颈了但团队的工程能力还停留在把demo跑通的水平。所谓AI工程和传统的机器学习工程不一样和传统后端开发也不一样。它既要理解大模型的工作特性又要把检索、提示、编排、评估、部署这些环节像流水线一样串起来。而“from scratch”这个词很关键——它逼你不能依赖某个框架的魔法而是真正弄清楚每一步在做什么。这篇文章主要写给两类人。第一类是后端或全栈工程师想进入AI应用开发但面对LangChain、LlamaIndex、各种Agent框架感觉像在看天书。第二类是有一定模型调用基础的产品负责人或独立开发者能调通API但不知道怎么做成可以上线、可以迭代、可以交付的工程。如果你属于这两类这篇内容值得认真过一遍。我先给一个核心结论从零开始做AI工程最重要的不是某个大模型也不是某个框架而是建立一套可重复、可观测、可迭代的流程。你今天用某个模型跑通的方案换一个模型、换一批数据还能不能稳定工作这才是工程化水平的分水岭。下文我会用一个贯穿始终的企业内部文档问答系统案例把从问题定义到上线部署的全过程拆开讲。2. 起步前的准备先搭框架再写代码很多新手拿到任务就开始写代码调通一个模型API跑通一段prompt看到输出就以为完事了。但真正做工程开头半小时应该在白板上画框架而不是在编辑器里打字。这半小时想清楚的事决定了后续项目是越走越顺还是越走越堵。2.1 先把场景边界画出来第一步不是选模型而是定义任务边界。你做的是单轮问答还是多轮对话需要实时检索外部知识还是只靠模型自身知识输出是自由文本还是需要结构化JSON供程序调用允许模型犯错的程度有多大错误发生时系统该做什么兜底这些问题看似简单但实际项目中翻车的案例非常多。有人想做“智能客服”上来直接把历史客服记录全部扔给模型结果模型把一年前已经下架的产品功能一本正经地推荐给了用户。问题就出在没定义“知识时效性”这个边界——旧数据没被过滤也没有版本标记。做得好的项目场景定义通常是一份两页的文档写清楚输入维度、输出维度、可接受延迟、不可接受的错误类型。我的建议是动手前先用一张表把边界列出来。以文档问答为例输入是用户的问题通常一到两句自然语言输出是回答文本允许附带引用来源延迟目标小于3秒不可接受的错误是不能把来自其他项目文档的信息混进回答。这张表写清楚后后面的技术选型都围绕它展开不会被“某个框架看起来很酷”带偏。2.2 模型选型与访问方式API优先还是本地部署模型选型没有银弹只有合适。评估标准通常有三个效果、成本、延迟但不同场景的优先级排序完全不同。做离线数据处理延迟无所谓效果和成本优先做线上实时客服延迟和稳定性优先做代码生成类工具效果和上下文长度优先做隐私敏感的内容处理部署方式优先。先放一张我常用的对比表方便你快速判断走哪条路维度商业模型API内部部署开源模型上手难度低注册获取访问凭证即可高需要GPU环境与推理框架成本结构token按量付费起步低固定硬件投入高频场景更划算数据边界数据经过服务方需合规评估数据留在内网适合敏感场景效果大尺寸模型能力强推理更稳取决于模型尺寸中尺寸与大尺寸有差距运维负担几乎为零厂商负责要自管版本迭代、算力扩容、故障恢复我的原则是起步阶段一律API优先。原因很简单API让你把注意力放在应用逻辑上而部署一个模型要考虑GPU资源、并发控制、请求排队、故障恢复这些都会吃掉你本该用来打磨产品的精力。等用户量和调用量确实上来了再根据成本曲线决定是否迁到私有化部署。按我自己实测的经验很多场景用商业API的月开销远低于一台闲置GPU服务器的成本摊销没必要为了“自主可控”而过早背上运维包袱。选模型时还有一个常被忽略的细节不一定非要追最大尺寸的旗舰模型。不少问答、抽取、格式化任务中等尺寸模型完全能胜任延迟和价格却低一个量级。具体怎么选一定要拿自己的业务样例去评测而不是看公开榜单排名。2.3 工程目录代码结构也是工程资产从零开始做最容易犯的错是把所有代码堆在一个文件里。刚开始觉得方便等加了检索、加了缓存、加了评估就乱成一团。我不建议一上来就上重型框架一个职责清晰的普通Python工程比什么都好用。下面这个目录结构是我在多个项目中验证过的project/ ├── configs/ # 模型配置、prompt版本、环境参数 ├── data/ # 原始文档、处理后的chunk、评估集 ├── src/ │ ├── ingest/ # 文档解析、清洗、切分、向量化 │ ├── retrieval/ # 向量检索、重排序、召回过滤 │ ├── agent/ # 工作流编排、工具调用、记忆管理 │ ├── evaluate/ # 离线评估、规则检查、标注工具 │ └── serve/ # API服务、接口定义、限流逻辑 ├── tests/ # 单元测试与端到端测试 └── scripts/ # 数据构建、评估任务等一次性脚本这个结构体现AI工程的两个关键思想。第一数据处理和应用逻辑分离。数据管道不稳定经常要重跑如果把解析逻辑塞进API服务里改一个切分策略就可能牵连线上问题。第二提示词和代码分离。把prompt放到configs目录意味着你可以像管理配置一样管理它的版本换一版prompt不用重新发布代码。目录定下来之后还要同步定两件事所有依赖都通过配置文件锁定版本所有模型调用都走同一个封装层。封装层特别重要你以后换模型、加日志、做灰度都靠这层抽象别把模型厂商的SDK散落在各种业务代码里。3. 拆解AI工程的核心工作流框架搭好之后要理解AI工程每天处理的核心工作流。说白了就是三件事怎么给模型喂信息、怎么设计让模型按预期工作的流程、怎么确认模型干得好不好。这三件事循环迭代就是AI工程的日常工作。3.1 上下文构建让模型在正确的信息下工作大模型的上下文窗口是有限的而且你把无关信息塞进去答案质量不升反降。所以上下文构建本质上是一个信息筛选和编排的问题。最常用的手段是RAG检索增强生成把大文档切成块向量化后存起来用户问题进来时先检索最相关的几个块再把这些块和问题一起交给模型生成答案。听起来简单做起来细节不少。切块方式选不好检索效果立刻打折。很多人用固定长度去切比如每500个字一刀结果把一段完整表格拦腰切断检索到的内容语义都不完整。我的经验是优先按照文档本身的语义结构切标题、段落、表格各算一块没有明显结构再退回到按长度切并保留一定重叠。切块长度也要看使用的嵌入模型和检索场景几千字的块适合整篇文档召回几百字的块适合精确定位段落。嵌入模型的选择同样影响整个链路。你可以用商业embedding服务也可以用本地开源的向量模型。国内中文长文本召回场景下bge系列的中文召回稳定性不错作为默认项很可靠。但选嵌入模型时不要只看公开跑分一定要拿你自己领域的文档建一个小测试集看召回结果是不是你要的。这就跟你去面试一个候选人简历再漂亮也不如做一道现场题来得实在。上下文构建还要考虑对话历史。多轮对话场景直接把过去20轮全发给模型额度和延迟都会爆炸。我一般会把历史消息压缩成摘要保存只保留最近几轮原始消息并配合关键信息的概括。这个机制在开源框架里叫记忆管理但底层逻辑就是省token、保质量。3.2 Prompt与Agent设计从指令升级到流程提示工程prompt engineering很多人理解成“写几句好话让模型配合”实际远不止于此。工程意义上的提示设计要把一次模型调用的边界完全定义清楚系统指令明确角色和规则用户消息带足上下文输出格式用结构化方式约束遇到异常情况给出兜底指令。一套好的prompt应该让模型在大多数情况下不需要自由发挥。我写系统指令有一个固定套路先一句话定义任务再列清楚输入是什么、输出是什么、哪些事不能做最后特意加一句“如果信息不足直接说明不知道不要猜测”。如果需要模型输出JSON用格式约束或让模型先输出再校验如果需要多步判断就把判断拆成独立步骤。记住对模型说“请尽量”远不如一个明确的“必须”有效。再往上走一层就是Agent设计。Agent的核心不是“让模型自己决定怎么做”而是你把可用的工具、边界条件、失败重试逻辑都定义好让模型在给定的harness约束框架里做路径选择。这个harness的概念很关键。举个实际例子我在做工单分类Agent时给模型安排了三个工具查历史工单、查产品文档、创建标签。模型被允许自行决定先调用哪一个但调用任何一个工具都要先经过参数校验层非法参数会被拦截工具返回结果后还有一层二次确认逻辑。这些约束就是harness它保证模型再聪明也不能跳出业务规则乱来。单Agent够用的情况下不要急着上多Agent。多Agent协作听着高级但每多一个Agent就多一层不可控的失败面多一层token消耗。我做过一个项目三个Agent互相传递中间结果结果某个Agent输出格式一不稳定后面整个链路全崩。后来改成两个阶段每阶段用结构化输出约束稳定性立刻上来。3.3 没有评估闭环就没有“工程”模型输出可用不可用不能靠人肉看几眼就觉得“还行”。AI工程和传统工程一样必须有评估闭环。最基础的做法是准备一份评估集包含典型的用户问题、期望行为、不允许出现的错误。每次修改prompt、替换模型或调整检索参数后用同一份评估集跑一遍对比前后差异才不会出现“优化了一个细节搞崩了另一些回答”的尴尬。评估可以分三层。第一层是自动化客观指标比如检索阶段看召回率、排序准确率生成阶段看回答是否包含必需信息点。第二层是模型当评委用另一个更强或同级别的模型按照评分规则给回答的完整性、准确性、忠实度打分这个方法叫LLM-as-a-judge。实测下来只要评分规则写得足够细结果能接近人工评分。第三层是人工抽检每周抽一部分线上真实问题做标注发现自动化评估看不见的问题。光有评估还不够还要把评估变成行动。我在团队里立了一条规矩凡是改了prompt或者换模型必须提交一份前后对比的评估报告。这条规则看着繁琐但避免了太多“我感觉更好了”的虚假优化。尤其当模型厂商升级版本后看起来一切正常实际某些场景的行为已经悄悄变了没有评估集这种回归问题根本发现不了。4. 从零实现一个企业内部文档问答系统前面讲了一堆理念下面用一个端到端的案例把这些东西串起来。案例是企业内部产品文档问答系统用户用自然语言提问系统从文档库里检索相关知识点生成带引用来源的回答。这个案例覆盖面很广几乎涵盖了从零构建AI工程的全部关键环节。4.1 场景定义与数据情况先描述需求。公司内部有一批产品文档格式有Markdown和PDF总共几百份每份几千到几万字。目标是一个内部助手员工问一个产品功能它能找到对应文档段落并给出准确答案同时标明出处。这个场景对回答准确性要求高因为内部员工会拿回答去面向客户回答错了容易造成前端业务事故。基于这个需求场景定义表定成这样维度约定输入中文自然语言问题单轮优先支持简单追问输出回答正文 引用文档编号及段落延迟目标检索加生成平均小于4秒不可接受错误检索到无关文档并据此作答、引用来源错误模型策略商业API起步后续视成本再调整这张表的价值在实际开发里能立刻体现出来。比如“支持简单追问”这一条直接决定了对话记忆模块要不要做“引用来源错误不可接受”这一条决定了回答里必须带引用而且生成环节要明确要求模型“只能基于提供的引用内容回答”。4.2 构建离线数据管道与向量检索项目建设的第一阶段先把离线数据管道建好。整个过程是读文档、清洗、切块、向量化、入库。核心代码大致是这样# src/ingest/pipeline.py import re from pathlib import Path def load_documents(root_dir: str): docs [] for path in Path(root_dir).rglob(*): if path.suffix in (.md, .txt): docs.append({source: str(path), content: path.read_text(encodingutf-8)}) return docs def split_by_semantic_structure(doc, max_chunk800, overlap80): # 先按标题切再按段落切最后用长度回退切 sections re.split(r\n(?#{1,6}\s), doc[content]) chunks [] for section in sections: if len(section) max_chunk: if section.strip(): chunks.append({source: doc[source], text: section.strip()}) else: # 长段落按句子切保留重叠 sentences re.split(r(?[。]), section) buffer for sent in sentences: if len(buffer) len(sent) max_chunk: chunks.append({source: doc[source], text: buffer.strip()}) buffer buffer[-overlap:] sent else: buffer sent return chunks这段代码中切块策略是重点。先用正则按Markdown标题切尽量保留文档语义结构然后对长段落按句子切并设置重叠。重叠的作用是避免一句话恰好卡在边界上导致检索只召回半边内容。max_chunk设800、overlap设80这两组参数是拿真实文档测试后选定的不同文档类型应该自己调一遍不要盲抄。向量化入库的部分我用一个封装类来处理方便后续切换嵌入模型。向量库的选择上几百份文档的量级用Chroma这类轻量方案就够用但要注意它的并发能力有限不适合大规模生产。如果数据达到几万份以上或有复杂的过滤需求建议直接用Milvus或pgvector。选择向量库时要提前想清楚一件事要不要支持元数据过滤。比如按文档版本、按文档类型过滤最好在一开始设计schema时就预留否则后面加过滤条件会非常痛苦。4.3 实现Agent工作流与RAG问答离线管道跑通后核心在线流程分三步改写用户问题、召回候选文档、生成引用答案。这三步合在一起就是一个单Agent工作流。核心实现思路如下# src/agent/qa_agent.py from src.retrieval import search from src.serve.llm_client import chat_completion SYSTEM_PROMPT 你是一个企业内部产品文档问答助手。 规则 1. 只能基于【参考资料】中的内容回答参考资料不足时明确说明不知道。 2. 回答必须用【引用[编号]】的形式标注来源编号对应参考资料。 3. 不能编造文档里不存在的信息。 4. 回答控制在200字以内直接输出答案不要解释过程。 参考资料 {context} def qa_flow(question: str, history: list[dict] | None None): # 1. 可选用模型把有指代的问题补全 if history: question resolve_reference(question, history[-4:]) # 2. 检索 hits search(question, top_k6) context \n\n.join(f[{i1}] {hit.text} for i, hit in enumerate(hits)) # 3. 生成 messages [{role: system, content: SYSTEM_PROMPT.format(contextcontext)}] if history: messages.extend(history[-4:]) messages.append({role: user, content: question}) answer chat_completion(messages) return answer, hits这里有几个值得展开的细节。第一question补全。用户接着上一轮问“那部署要注意什么”“那”字指代的是上一轮的对象。我让模型补全问题用的指令很简单关键是补全结果要替换原问题再进入检索而不是直接丢给生成。第二检索top_k设为6不是拍脑袋是拿评估集测出来的。top_k太小召回容易漏太大上下文变长且噪声变多对几百页文档库6到8个块是个比较稳的区间。第三系统提示词里明确了“引用[编号]”和“不足时明确说明不知道”这两条直接关系回答可靠性和幻觉率。回答生成后还要加一个后校验步骤检查回答里出现的引用编号是否都在本次检索结果范围内。如果模型引用了不存在的编号说明生成结果异常这时宁可返回一条提示让用户重新表述也不要带着错误引用发给用户。4.4 部署上线与运行监控在线流程调试通过后把它包成一个API服务。我用FastAPI代码结构大致如下# src/serve/app.py from fastapi import FastAPI from pydantic import BaseModel from src.agent.qa_agent import qa_flow app FastAPI() class AskRequest(BaseModel): question: str session_id: str | None None class AskResponse(BaseModel): answer: str citations: list[str] app.post(/api/ask, response_modelAskResponse) def ask(req: AskRequest): answer, hits qa_flow(req.question, historyNone) citations [{source: h.source, text: h.text[:80]} for h in hits] return AskResponse(answeranswer, citationscitations)这里省略了历史会话管理和鉴权但有几个工程细节必须强调。接口层要有超时控制模型调用要配置重试和熔断否则上游模型服务的偶发抖动会直接打垮你的接口。我见过不少项目只盯着模型效果上线后才发现高峰期接口超时率飙到30%原因就是没做这些——这才是AI工程和demo的差距所在。监控方面除了常规的QPS、响应耗时、成功率之外一定还要记录三个AI特有指标检索命中率、回答引用率、用户负反馈率。检索命中率低说明索引或召回策略有问题引用率低说明prompt约束不够用户负反馈率高说明效果需要版本迭代。这些指标打日志后接到看板每天看一眼比你上线一周后被业务方提故障要强得多。我在生产环境还会对模型调用做全链路追踪把每轮请求的检索结果、prompt版本、模型输出都记录下来。这样线上出现的任何一个badcase都能回放现场查清是哪一环出了问题。现在有一些开源方案可以做自己实现也不复杂关键是这个习惯要从上线第一天就养成。5. 常见问题与排查技巧实录从零开始做AI工程路上一定会踩坑。这里整理几个典型的常见问题以及对应的排查思路。问题现象大概率原因排查与解决思路回答内容看着对但引用来源错误检索召回里混入相似文档模型引用错了编号检查检索结果看召回是否确实覆盖答案给引用规则加“只能引用所给资料中出现的内容”约束知识库更新后回答仍是旧内容向量库里旧版本数据未清理建链时把版本号写入元数据检索前强制按版本过滤追问时模型“失忆”对话历史没被注入或注入过多导致信息稀释引入记忆压缩把历史消息摘要后随最近几轮一起注入接口偶发超时甚至假死上游模型服务抖动没有重试或熔断增加超时、重试、熔断和降级策略降级返回缓存结果或兜底文案换了一个模型后效果突然变差不同模型对指令遵循能力不同prompt没有适配把prompt版本化管理换模型后跑同一套评估集针对差异调整约束成本月账单吓人token消耗没有治理压缩上下文、缓存高频问题答案、对长文档做分段精读而不是整体全塞这里特别说一个我踩过的大坑做RAG时总想往prompt里放尽量多的参考资料认为资料越多答案越准。实际结果完全相反参考资料一多模型反而容易被无关片段带偏回答里出现自相矛盾的内容。后来我把top_k从10降到6并加上相关度阈值过滤效果明显稳定。这个经验印证了那句老话给模型的信息不是越多越好而是越相关越好。另一个高频问题发生在评估阶段。很多人写一个评分prompt让模型给回答打分但不对评分标准做约束结果模型每次给的分数都偏高评估形同虚设。我的经验是评分prompt里必须定义明确的评分维度和扣分规则比如“是否出现幻觉”“是否遗漏核心信息点”“引用是否一致”还要给出具体的得分锚点。不然模型当评委只会当“老好人”。6. 我在实操中的几点体会把上面这些东西完整走一遍之后我最深的体会是AI工程从零开始真正的门槛不是技术而是“克制”。克制住看到新框架就想换的冲动克制住想给系统加更多功能的冲动也克制住“模型应该自己搞定一切”的幻想。老老实实把场景边界画清楚把评估集建起来把日志追踪装上工程质量自然就上来了。还有一个小技巧想分享项目开始的头一周不管业务多急都先不要碰在线服务。先花时间把离线数据管道和评估集做扎实哪怕只是一个很小的领域也要做得完整。我见过太多项目组因为着急上线第一版全部精力花在写API上结果数据有问题、评估缺失后面每一步都像在沙子上盖楼。最后再说一句。AI工程不是一次性交付而是持续迭代的过程。模型会升级、业务会变化、数据会增长今天有效的方案三个月后可能要推倒重来。但只要你底层的流程是健康的——可观测、可评估、可回放——任何变化都只是换一个组件的问题。这也是我理解“ai-engineering-from-scratch”的真正含义不是要从零发明技术而是从零建立起一套让AI系统稳定向前演进的工程能力。