今年我做了一个身边朋友都觉得“费力不讨好”的决定从零开始把一套AI工程化体系完整搭一遍。不是接一个API、套几个Prompt就算完事而是把模型接入、提示词管理、Agent编排、知识库检索、服务封装、测试部署整条链路走通。我给这个项目起名就叫ai-engineering-from-scratch因为市面上太多教程只教“怎么调用大模型”却很少讲清楚“怎么把大模型变成一个稳定、可控、可维护的工程系统”。这篇文章是项目的完整复盘包括技术选型、环境搭建、Prompt Engineering、AI Agent、RAG、FastAPI封装、测试方法还有我踩过的真实坑。适合两类人想从零开始做AI应用的新手以及已经在做但总觉得AI输出不稳定、流程难维护的开发者。看完你至少能避开我趟过的八成泥坑。1. 项目定位与整体设计思路1.1 从零开始搭AI工程到底在搭什么很多人以为AI工程就是“选个大模型然后写提示词”。但真正把项目跑起来后发现模型调用只是最外层的一小部分。一个能上线、能被业务用起来的AI应用至少要回答这些问题用户输入怎么进系统上下文怎么管理模型能力不够时靠什么补充模型输出不可信怎么办并发来了怎么不崩新版本提示词改了怎么回归测试这些问题单独看都不难但串在一起就需要一套明确的工程结构。我做的第一件事不是写代码而是把项目拆成五个层次接入层负责接收用户输入做清洗、格式化、权限校验。编排层承担Agent逻辑、Prompt组合、工具调用、知识库检索。模型层统一封装本地或远端模型处理流式输出、重试、超时。工具层提供模型可以调用的外部能力比如检索、计算、查询。基础设施层日志、监控、存储、缓存、部署脚本。这样分层的好处是每一层都能独立替换。今天用本地模型明天换更大的模型不需要重写整个业务逻辑。如果一开始就把代码全堆在一个文件里后期改Prompt都要提心吊胆。1.2 技术选型我为什么放弃“全家桶框架”最开始我也跟风选了LangChain想着生态全、省事。但实际用下来发现抽象层级太多出了问题要一层层扒源码而且版本升级经常不兼容。后来我试过LlamaIndex它在RAG场景确实舒服但超出问答场景后还是得自己补不少胶水代码。至于Spring AI如果你是Java团队可以重点考虑但我这边是Python技术栈没必要再引一套体系。最终我采用的是“轻量自研管线 少量成熟组件”的策略。核心编排逻辑自己写向量存储用Chroma模型接入统一走OpenAI兼容协议服务层用FastAPI。这样做的理由很实际项目的核心痛点是可控性而不是“炫技”。自己写的几十行代码在调试时候一眼就能看穿远比在一堆抽象类里找bug高效。方案优势劣势适合场景LangChain生态丰富组件多抽象重升级易踩坑快速原型、复杂生态LlamaIndexRAG工具链完整聚焦检索问答扩展需要补知识库问答为主Spring AIJava生态友好与Python生态隔离Java后端团队自研轻量管线可控、易定位、依赖少需要自己造轮子长期维护、深度定制想清楚这一点后我就不再纠结框架了。AI工程的核心不在框架而在数据流、状态管理和失败处理。1.3 整体架构分层设计实际落地的架构并不复杂画出来就四个环节输入接入 - Agent编排 - 模型调用 - 输出校验。每个环节之间用清晰的接口定义连接。输入接入这一层只干两件事把用户请求转成统一的内部消息结构以及做基础的内容安全过滤。这看起来没什么技术含量但能挡住很多后续麻烦。比如有的用户会通过输入注入指令试图让模型绕过系统设定如果入口就把这类内容标记出来后面就少很多风险。Agent编排层是整套系统最核心的部分。它维护着“当前要解决什么目标、已经做了哪些步骤、有哪些工具可用”这些状态然后决定下一步是调用模型、调用工具还是直接把结果返回给用户。这个循环我后面细讲这里只说一点状态管理一定要独立出来不要藏在对话历史里。模型层统一封装成“输入消息列表 - 输出消息”的函数内部再处理模型名、采样参数、超时重试。输出校验层负责确认模型返回的格式符合预期不合预期就重新生成或者报错。整个过程像一条流水线每一站都有明确职责。2. 环境搭建与工程规约2.1 本地部署大模型的真实体验要跑通AI工程第一步是手里要有一个“能干活的模型”。我刚开始是在云端调用API但做工程实验时发现网络延迟、费用、数据隐私都会制约迭代速度所以后来改成本地部署。本地部署最常见的方式是用Ollama装量化模型一条命令就能跑起来ollama run qwen2.5:7b这个命令会下载模型并启动一个交互式对话。如果只是想快速验证效果这样做完全够。想要工程化调用可以把它的服务模式打开ollama serve它默认提供http://localhost:11434/v1的OpenAI兼容接口也就是说我可以用OpenAI SDK的写法访问本地模型。这点极其关键它让我在不改业务代码的情况下切换模型供应商。实测下来在我那台8G显存的机器上跑7B量化模型每秒大概能生成十几个token做交互式问答能接受但做批量处理就很吃力。如果想跑更大的模型最好准备16G以上显存或者干脆用云端API。我把硬件限制写在项目README里避免团队里其他人浪费时间。2.2 项目脚手架与依赖管理环境不干净后面所有调试都会变成灾难。所以项目一开始我就建好了隔离环境Python用的3.11mkdir ai-engineering-from-scratch cd ai-engineering-from-scratch python -m venv .venv source .venv/bin/activate pip install --upgrade pip pip install openai chromadb fastapi uvicorn pydantic-settings python-dotenv pytest httpx依赖我全部锁进requirements.txt并且给关键的包固定了主版本号。AI生态的库更新太快今天能用、明天升级后接口就变了。固定版本当然也会带来安全问题所以需要定期做安全更新但这不是一个从零开始的项目要考虑的首要问题。依赖之外项目结构从一开始就立好规矩ai_engineering/ ├── app/ │ ├── api/ # HTTP层 │ ├── agents/ # Agent编排 │ ├── llm/ # 模型封装 │ ├── memory/ # 记忆与向量存储 │ ├── prompts/ # Prompt模板 │ └── core/ # 配置与日志 ├── tests/ # 测试集 ├── scripts/ # 运维脚本 └── pyproject.toml目录划分没有标准答案关键是让“Prompt改了、模型换了、接口变了”这些高频变更落在不同目录里。后来维护阶段这套结构帮我省了很多找代码的时间。2.3 配置管理密钥与模型参数不进代码本地部署不需要密钥但接云端API就会用到。密钥一旦硬编码进代码早晚会被推到Git仓库里。我从一开始就规定所有配置走环境变量用pydantic-settings统一读取。from pydantic_settings import BaseSettings class Settings(BaseSettings): model_endpoint: str http://localhost:11434/v1 model_name: str qwen2.5:7b api_key: str local temperature: float 0.2 max_tokens: int 1024 top_k: int 5 class Config: env_file .env env_prefix AI_ENG_.env文件只留在本地进入.gitignore。这样同事克隆仓库后复制一份.env.example改成自己的配置就能跑起来。模型参数也统一放配置里而不是散落在各处调参时候只需改一处一个人就能维护所有环境。3. Prompt Engineering从“会聊天”到“会干活”3.1 把Prompt当代码来管理我见过很多团队把Prompt写死在业务代码里改一句话就要发一次版本。更离谱的是完全凭感觉写导致模型今天给这个结果明天给另一个结果。真正把Prompt当成工程来做有两点特别重要模板化和版本化。模板化是指Prompt不要跟业务逻辑混在一起。我会把所有Prompt放在prompts/目录下每个模板单独一个文件类似prompts/ ├── chat_summary.md ├── intent_extract.md └── tool_response.md版本化是指每次Prompt变更都要记录改了哪句话、为什么要改、期望影响是什么。我在模板文件头部写了一段注释记录变更时间和变更原因。这听起来有点繁琐但当你需要在“加一句示例”和“模型输出暴涨”之间建立因果联系时版本记录会救你一命。3.2 一个能直接用的Prompt模板下面这个模板是我在项目里最常用的一种结构适用于大多数内容生成类任务你是{角色}。你的目标是{任务目标}。 请根据以下输入完成任务 {user_input} 要求 1. 直接给出结果不要解释你的思考过程。 2. 如果信息不足请明确说“信息不足”不要编造。 3. 输出格式必须严格按照以下JSON结构 {result: 你的回答, confidence: 0-1之间的数字, needs_more_info: true/false} 参考示例 输入: {example_input} 输出: {example_output}这个模板把“角色、目标、输入、约束、格式、示例”六个要素全占了。尤其是参考示例哪怕只有一组也能显著提升模型输出的稳定性。少样本示例相当于给模型画了一条“输出轨迹”比单纯用嘴说“你要输出JSON”管用得多。3.3 用JSON模式锁定输出格式如果业务系统需要把模型的输出“喂”给下游程序我就要求模型必须返回结构化JSON。直接用Prompt约束能解决大部分问题但偶尔模型还是会漏一个逗号。更稳的做法是开启JSON模式或者Function Calling。在OpenAI兼容协议下可以这样调用from openai import OpenAI client OpenAI(base_urlhttp://localhost:11434/v1, api_keylocal) resp client.chat.completions.create( modelqwen2.5:7b, messages[ {role: system, content: 你是一个信息抽取助手。请从用户输入中提取日期、地点、事件。}, {role: user, content: 上周六我在深圳参加了AI工程分享会。} ], response_format{type: json_object} ) print(resp.choices[0].message.content)返回的字符串可以直接用json.loads解析。遇到解析失败就再补一个“自动修正”步骤让模型基于报错信息重新输出。我在实测中发现只要把“必须是合法JSON”这句话写进约束条件解析成功率能做到95%以上剩下的5%用重试机制兜底。4. AI Agent 与 RAG 的工程实现4.1 最小可运行的Agent循环AI Agent的核心价值是让模型能“使用工具”去完成目标而不仅仅是输出一段话。我实现的最小编排逻辑是这样的def agent_loop(system, user_input): messages [{role: system, content: system}, {role: user, content: user_input}] for step in range(MAX_STEPS): resp llm.chat(messages, toolsTOOLS) msg resp.choices[0].message if msg.tool_calls: messages.append({role: assistant, content: , tool_calls: msg.tool_calls}) for tool_call in msg.tool_calls: result execute_tool(tool_call.function.name, tool_call.function.arguments) messages.append({ role: tool, tool_call_id: tool_call.id, content: str(result) }) continue return msg.content return 达到最大步数停止这个循环只做了三件事让模型决定是调用工具还是给出最终答案执行工具并返回结果重复这个过程直到答案产出。看起来简单但工程上最需要注意的其实是工具执行的安全性。模型只能调用白名单内的工具工具只能访问授权过的资源绝不能因为模型说“执行一下”就去跑任意系统命令。我在项目里最多同时注册了五个工具查天气、查库存、计算器、向量检索、百科查询。工具数量不宜过多模型的选择能力会跟着下降。如果某次任务必须依赖超过五个工具我会先做一步“工具路由”让模型先选领域再进子Agent。4.2 RAG让模型在你的知识库上说话大模型最大的问题是“懂很多但不一定懂你的业务”。要解决这个RAG是绕不开的路。RAG全称检索增强生成说白了就是先把你自己的文档切成小块、向量化、存起来用户提问时先去库里检索相关内容把检索结果塞进Prompt再让模型基于这些内容回答。我用Chroma做向量数据库过程分三步走第一步把文档加载进来。最常见的格式是Markdown、PDF、TXT用对应加载器解析成文本块。第二步切分。这一步很关键块太小会丢失上下文块太大会超出模型窗口。我试下来中文场景500到800字符一个块块与块之间重叠80到100字符效果比较均衡。第三步向量化和入库。调用嵌入模型把每个块转成向量写入Chroma。检索时把用户问题转成向量做相似度搜索返回TopK个块。我一般设TopK为3到5个太多会让Prompt变得很长模型反而抓不住重点。检索结果拼进Prompt的写法也有讲究你是一个知识库助手。请只基于以下资料回答如果资料中没有相关信息请直接回答“资料中未找到”。 参考资料 {retrieved_chunks} 用户问题{question}不确定领域里所有没有依据的问题一定不能让模型自由发挥。自由发挥一次两次没事三次以后你就会发现它开始一本正经地编造产品参数了。4.3 上下文与记忆管理对话式AI应用一定会遇到上下文窗口不够用的问题。模型能同时看的内容有限总不能每次把从头到尾的聊天记录都塞进去。我的经验是分三层管理记忆短期记忆当前对话轮次。直接保留最近几轮原始消息用于理解用户当下意图。中期记忆把更早的对话做摘要。每五轮对话就触发一次摘要压缩成一小段话。长期记忆关键事实进入向量库。用户提到“我在深圳工作”这个信息存到向量库里等下次需要时可被检索出来。这个三层结构解决了我实际踩过的“失忆”坑模型聊到第三十轮时彻底忘了用户一开始说的需求。引入摘要记忆之后它终于能记到第六十几轮还能说出用户的初始目标。记忆不是越多越好而是要把信息分层让模型在最合适的层级里找最合适的内容。5. 服务封装与测试评估5.1 把AI能力封装成HTTP服务内部验证通过还不够业务方要接入必须有一个稳定的接口。我用FastAPI把Agent包装成了标准HTTP服务一个最基础的接口长这样from pydantic import BaseModel from fastapi import FastAPI app FastAPI() class ChatRequest(BaseModel): user_id: str message: str session_id: str default class ChatResponse(BaseModel): reply: str session_id: str app.post(/v1/chat, response_modelChatResponse) async def chat(req: ChatRequest): reply await run_agent_async(req.user_id, req.session_id, req.message) return ChatResponse(replyreply, session_idreq.session_id)注意这个接口没有把“模型名”、“温度”、“提示词”暴露给调用方。AI应用的前端和后端一定要有严格协议外部系统只传业务参数底层AI策略由平台方控制。否则调用方今天想调temperature明天想换prompt接口很快就失控了。5.2 测试AI应用的正确姿势AI项目测试最让人头疼的是“没有标准答案”。同一个问题模型两次回答可能不一样。所以我不追求断言“回答内容一字不差”而是把测试分成三层第一层结构化测试。检查返回结果是不是合法JSON、关键字段是否存在、字段类型对不对。这些是可自动判定的。第二层规则测试。比如“如果资料中没有信息必须回答‘资料中未找到’”。这类规则我用子串匹配来测。第三层人工回归。固定一组评测集每次Prompt改动后把这组问题跑一遍人工打分。评分维度就两个真实性和有用性。为了减少主观性我会用另一套模型来做初筛排序再由人确认。用pytest写一个简单测试def test_agent_json_output(): result agent_loop(你是信息抽取助手, 我叫张三在杭州工作) data json.loads(result) assert name in data assert city in data assert data[city] 杭州这套测试不解决“回答好不好”的问题但至少能挡住“回答崩了”的问题。没有这层保护改一句Prompt就上线等于在赌模型心情。5.3 部署与性能优化服务写好后我用Docker打包部署基础镜像尽量精简FROM python:3.11-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . CMD [uvicorn, app.api.main:app, --host, 0.0.0.0, --port, 8080]部署之后第一个瓶颈是同步调用模型太慢。我的解决方式是把耗时的模型调用放到异步队列里接口立刻返回一个任务ID业务方通过轮询拿到结果。这个变化直接让API的响应时间从“用户等十秒”变成“用户等两秒拿任务ID”。流式输出也能大幅提升体验。模型边生成边把token推送过去用户看到第一个字的时间能从几秒缩短到几百毫秒。但流式会带来“用户断了连接”的问题所以必须做好连接清理和任务取消。6. 常见问题排查实录6.1 模型输出飘忽不定怎么办同一套Prompt上午输出还行下午就“发疯”。这种情况优先排查三件事第一temperature是不是设得太高了稳定场景直接0或者0.2第二Prompt里有没有“兜底路径”比如信息不足时明确说不知道第三有没有加少样本示例很多问题都是“少一个例子”导致的。还有一次我遇到模型结果跟业务系统期望值对不上最后发现是上游传给模型的字段顺序变了。所以排查的时候别只盯Prompt先看清楚输入到底长什么样。6.2 上下文太长导致“变笨”或者超限上下文超限有两个典型现象接口直接报错或者模型开始重复、答非所问。前者是硬限制后者是有效信息被无关内容淹没了。我解决方式就是把第4.3节那套记忆管理真正用起来。每轮对话后先做“是否需要摘要”的判断而不是无脑存全文。如果项目里对话轮次特别长还可以引入“关键信息抽取”环节每轮只保存“用户目标、已确认事实、待确认问题”把无意义寒暄直接丢掉。这样即使聊200轮上下文里也始终只保留最近10轮的原文加上早先的摘要。6.3 本地部署太慢影响体验本地模型慢是常态优化手段就三板斧量化、流式、并发控制。量化能直接降低显存占用和计算量7B模型从fp16降到int4能提速不少。流式能把“总时长”转换成“首字时长”用户体感会好很多。最后一定要给会话接口做并发限制否则几个请求同时打进来显存爆掉全部请求一起卡死。如果预算允许直接给模型调用层加一台带大显存GPU的专用服务效果最立竿见影。纯靠CPU跑大模型做实时对话基本上很难让用户满意。6.4 内容安全与合规AI工程里不能省的一环AI应用上线前内容安全必须放在跟功能同等重要的位置。我从一开始就加了输入和输出两道审核。输入侧拦截明显不合规的请求输出侧对模型生成的内容做二次检查确保没有越界。这不是为了“限制”而是为了让项目能长期稳定落地。工具调用环节尤其要注意权限。模型给出的“下一步动作”不能无脑执行我要求每个工具都带一个“权限声明”比如“本工具只允许读取授权目录下的文件”Agent循环在执行前判断一下做不到就直接拒绝并向用户说明。这样即使模型被诱导生成恶意调用底层权限也能兜住。6.5 写在最后的实在建议整个项目做完我的体会其实很朴素AI工程的重点不在“AI”而在“工程”。模型能力当然重要但稳定输出、可维护、可观测、可恢复才是决定项目能不能长期跑下去的关键。我踩过的最深的坑不是模型回答错误而是错误发生后不知道是Prompt问题、环境问题还是上游数据问题。后来我把日志、版本、反馈链路补齐整个系统才真正变得可干预。如果你也打算从零开始入坑我的建议是先把最小链路跑通再一层层加东西。不要一开始就追求框架齐全、功能丰富先用本地小模型搭一个“模型调用 - 输出JSON - 接口返回”的骨架然后一天加一个能力提示词模板、工具调用、向量记忆、测试集。每一步都能看见效果每一步都不至于失控。把项目做成“自己看得懂、改得了、坏了能恢复”的样子比任何热门框架都重要。