从零手写AI工程:API调用、Prompt与部署全攻略

📅 发布时间:2026/9/28 7:09:32
从零手写AI工程:API调用、Prompt与部署全攻略
1. 为什么我要从零开始啃 AI Engineering这两年AI工程师这个头衔火得一塌糊涂各种培训课、训练营满天飞但说实话市面上大部分资料要么是教你调 API 的玩具教程要么是上来就甩一堆数学公式劝退新手的学术论文。我自己在真正动手做第一个 AI 项目之前也走过一段弯路看了一堆视频收藏了几十个仓库真到自己写代码的时候连该用哪个库、该怎么组织 prompt、模型返回的结果要不要校验都没想清楚。所以当我说AI Engineering from Scratch的时候我指的不是从零训练一个大模型——那是研究科学家的事。我指的是不依赖任何封装好的低代码平台不靠拖拽式的工作流工具从模型 API 的选择、环境的搭建、提示词的设计、数据流的组织、结果评估、再到部署和迭代全链路自己动手写代码做出来。这套东西学完之后你的能力会完全不一样别人只会对着聊天框提问你能把模型嵌进自己的业务流程里让它变成一个真正干活的生产工具。这篇内容适合谁我觉得有三类人最合适刚入门 AI 开发想从调 prompt 玩升级到写代码集成 AI 的人后端或全栈工程师想在自己的系统里接入大模型能力但不想用现成 AI 平台的傻瓜方案以及像我一样对黑盒有天然不信任感万事喜欢自己掌控底层的折腾型选手。我会按照我实际走过的路径来讲分阶段从基础环境搭建到第一个真正的 API 调用到提示词工程到构建一个带记忆的多轮对话系统再到评估和部署。每个阶段我都会解释背后的原理不光是给代码就完事还会把那些文档里不会写的坑和心得一并掏出来。2. 环境选型与基础依赖我踩过的版本坑2.1 Python 版本和虚拟环境别迷信最新版AI 工程里 Python 是绝对的主流语言这没什么好争的。但很多新手第一步就栽在版本上。我一开始图新鲜装了最新的 Python 3.12结果第三方库跟上节奏的没几个尤其是涉及编译的包动不动就报错。后来我老老实实退回 3.11。倒不是说 3.12 不行而是生态适配需要时间做工程最重要的是稳定可复现而不是追求尝鲜。虚拟环境我也吃过亏。最开始我嫌麻烦直接全局装包结果不同项目之间依赖冲突连锁反应拆东墙补西墙。后来养成了习惯每个项目一个独立的虚拟环境。工具上我用venvpip简单直接不想引入 extra 的复杂度。python3.11 -m venv .venv source .venv/bin/activate pip install --upgrade pip这个组合看起来土但在团队协作、服务器部署、CI 里面是最不容易出幺蛾子的。2.2 核心依赖库不要一次装太多关于 AI 工程你需要关心的库其实没那么多很多人的误区是看到一个项目里 imports 了五六个库就全都装一遍。我实际用下来最核心的就这几类模型访问官方 SDK如openai或者更通用的 HTTP 客户端。别小看这一点后面你换模型供应商的时候就知道了。数据处理pandas不是必需品但涉及结构化数据管线和评测时会非常好用。向量存储做语义搜索或长期记忆时会用到比如chromadb或qdrant-client。服务框架FastAPI是目前接 AI 服务最舒服的异步性能好文档自动生成社区也大。环境变量管理python-dotenv防止把 API Key 硬编码到代码里。我建议一次只按需装一个每个库确认它的依赖不会破坏原有的环境。别用pip install -r requirements.txt一口气装完一个几百行的清单那多半是给自己埋雷。2.3 模型 API 的选型别把鸡蛋放一个篮子里起步阶段我主要用了 OpenAI 的 API毕竟生态成熟、文档清晰、模型能力也够强。但from scratch的精神恰恰是提醒你不管你现在选了哪家未来都有可能换。所以我从一开始就养成了一个习惯不直接在生产代码里 import 某家 SDK 并到处散落 prompt 字符串而是先定义自己的统一接口层。举个例子我写了一个llm.py里面暴露一个chat(messages, modeldefault)函数内部根据配置决定调用哪家。这样做的目的是什么呢当你从 OpenAI 切到 Anyscale、Mistral 或国产模型的时候主业务代码几乎不用动只动这一个文件。这个抽象层成本很低收益在后面你绝对会感受到。3. 第一个真正的模型调用从聊天框到代码3.1 绕开 IDE 里对着对话框的舒适区很多人第一次调 API 就是打开 Jupyter Notebook输入三行代码看到模型回复一个你好觉得我学会了。但这是错觉。Notebook 适合做研究和验证不适合做工程。我更推荐从一开始就用正常的 Python 文件加命令行执行这样你会更早地遇到底层的问题——比如超时、重试、编码、环境变量找不到等等。我先给大家看一个最基础的调用代码这也是我们所有后续项目的骨架import os from openai import OpenAI from dotenv import load_dotenv load_dotenv() client OpenAI( api_keyos.getenv(OPENAI_API_KEY), base_urlos.getenv(OPENAI_BASE_URL, https://api.openai.com/v1), ) response client.chat.completions.create( modelgpt-4o-mini, messages[ {role: system, content: 你是一名严谨的AI工程师总是用简洁精准的语言回答问题。}, {role: user, content: 请用一句话解释什么是回调函数。}, ], temperature0.3, max_tokens300, ) print(response.choices[0].message.content)这里面有几个细节我不会跳过load_dotenv()必须放在构造客户端之前否则读不到环境变量base_url加了一个默认值这是为了将来切换兼容 OpenAI 格式的第三方网关时不用改代码temperature0.3不是随便填的解释型任务我通常设置低一些让模型更确定、更少发散如果是创意写作我再调高到 0.8 左右。3.2 响应对象到底长什么样新手最容易忽略的东西我刚才那步print(response.choices[0].message.content)很多人是照着文档抄的但根本不知道response里面还有什么。我强烈建议你打印一下完整的response看看里面的id、model、usage、created这些字段。它们有什么用id可以用于后续请求日志追踪、成本审计usage字段告诉你 prompt_tokens、completion_tokens、total_tokens这对估算账单至关重要model字段确认你实际调用的模型版本防止配置被覆盖而不自知。如果你不打开看你永远在黑盒里操作。做一个 AI 工程师解剖每个返回字段是基本功。3.3 重试与超时生产环境的第一课本地调试时网络通畅API 几乎不会失败。但一旦放到服务器上或者请求量上来你就会遇到连接超时、5xx 错误、限流。所以从from scratch开始你就要把容错机制写进去。OpenAI 的 Python SDK 其实自带重试逻辑但默认重试次数和超时时间未必符合你的场景。我更习惯显式控制from openai import OpenAI client OpenAI( api_keyos.getenv(OPENAI_API_KEY), timeout30.0, # 单次请求超时30秒 max_retries3, # 最多重试3次 )这里有个小注意点timeout不是说你等 30 秒就一定成功而是连接和读取的总预算如果模型输入非常长比如上下文好几千 tokens生成内容也多30 秒可能不够需要适当放大。我有一次跑批量任务一直报超时排查了半天才发现是max_tokens设得太高模型生成时间长客户端却等着。超时时间和最大生成长度要联动考虑。4. 提示词工程你的第二门必修课4.1 别把 System Prompt 当摆设很多人调 API把所有的指令一股脑塞进 user 消息里system 消息只知道写你是一个有用的助手。这其实是浪费了系统提示词这个最强力的控制面。在我的实践中system prompt 承担的角色是定义模型的角色、行为边界、输出格式、价值倾向。而 user 消息承载的是具体任务和数据。这种分工的好处有两个模型会优先遵循 system 中的全局指令较不容易被用户输入带偏保持 prompt 的结构清晰便于后续模板化管理。举个例子我构建一个文档摘要助手时system prompt 长这样你是一名资深技术文档分析师。 你的任务是对用户提供的技术文档进行精准摘要。 要求 1. 摘要不超过200字。 2. 使用列表形式输出每项一个要点。 3. 不得添加原文中不存在的信息。 4. 如果原文包含代码示例请直接忽略。你看比你是助手具体了不只一个数量级。4.2 Few-shot 示例比规则好使的调校手段规则写多了模型可能还是犯傻。这时候不要急着堆规则试试给两个正反例子。Few-shot 的本质是通过类比让模型理解你的预期格式和推理风格。我在做意图识别的时候用过很有效的 few-shot 写法few_shot_prompt 用户想要查询订单状态。 意图查询订单 用户想要取消订阅邮件。 意图取消订阅 用户想要修改收货地址。 意图修改地址 用户不小心把咖啡洒在键盘上想找客服理论。 意图投诉 别小看这种笨办法它在很多业务场景下比单纯描述规则效果更好因为模型能从例子中推断出分类的隐含边界尤其是那些你很难用语言量化的边界。4.3 输出格式控制JSON 才是工程界的硬通货如果只是聊天模型输出什么格式无所谓。但做工程你需要把模型的输出接进你的业务流程——拿去做数据库查询、填表单、触发动作这时候那就必须让模型返回结构化 JSON。我在代码里强制规定输出 JSON 的方案很简单在 system prompt 中明确写输出必须是 JSON 对象并规定字段结构然后加上response_format{type: json_object}。这个方法很多模型都支持省去了解析 Markdown 代码块的麻烦。response client.chat.completions.create( modelgpt-4o-mini, messages[ {role: system, content: 你是一个信息抽取助手。输出严格为JSON包含name、age、city三个字段。}, {role: user, content: 张三今年25岁住在杭州。} ], response_format{type: json_object}, )返回的content可以直接json.loads()。但注意即使你上了response_format我还是建议在解析时加上try...except并且做 schema 校验。为什么因为模型不是数据库它偶尔还是会给你超出预期的内容比如多了个嵌套字段或者字段名大小写不对。永远不要盲目信任模型输出这是 AI 工程最重要的信条。5. 打造带记忆的多轮对话系统状态管理的艺术5.1 聊天的本质是序列拼接很多人第一次做多轮对话天真的以为 API 会记住你之前说过什么。事实上OpenAI 的 Chat Completions 接口是无状态的——它只根据你传入的messages列表来生成回复。所谓记忆完全靠你在每次请求时把历史消息重新全部带上。本地做一个最简单可用的记忆系统我的思路是用一个 Python 列表保存messages每轮对话后追加用户消息和助手消息然后整体传给 API。messages [] while True: user_input input(你) messages.append({role: user, content: user_input}) response client.chat.completions.create( modelgpt-4o-mini, messagesmessages, ) reply response.choices[0].message.content messages.append({role: assistant, content: reply}) print(AI, reply)这段代码运行起来确实能做多轮但有个隐患随着对话进行消息列表无限膨胀上下文窗口很快会爆掉而且调用成本也越来越高。5.2 滑动窗口与摘要压缩控制上下文的两种策略解决上下文无限增长业界最常见的两个手段滑动窗口只保留最近 N 条消息把超过 N 条的历史直接丢掉。简单粗暴适合对早期内容不敏感的场景。摘要压缩当消息超过阈值时先把旧消息用模型做一个摘要放进系统提示词中然后再继续新的对话。这样既保留了关键信息又节省了 token。我结合两种方式做了一个简单的记忆管理器结构如下class ConversationMemory: def __init__(self, max_messages20): self.messages [] self.max_messages max_messages self.summary def add(self, role, content): self.messages.append({role: role, content: content}) if len(self.messages) self.max_messages: self._compress() def _compress(self): # 调用模型把当前 messages 压缩成摘要 ...摘要不是每次都重新生成而是增量刷新。每次压缩时把上一轮的摘要和旧消息一起丢给模型让它输出新的摘要这样摘要就不会反复丢失信息。代价是每次压缩会消耗一定 token但比起每次全量传递整个历史长期看还是划算的。5.3 长期记忆让 AI 记住用户的名字和偏好上面的对话内记忆有个局限应用一重启记忆全没了。如果你做的是客服机器人、学习助手这类需要跨会话记忆的产品就得考虑持久化存储。我的方案是把关键信息抽出来存成结构化记录比如用户说过我喜欢简洁的回答我会把它作为一个偏好项存进数据库。下次新会话开启时把偏好注入 system prompt。这就相当于 AI 的长期记忆。具体实现上可以用一个轻量的 KV 存储键是用户 ID值是偏好列表。因为数据结构很简单用 SQLite 就够了不需要上重型数据库。这个方案在个人项目和小型产品里非常实用再往上规模化才考虑向量数据库。6. 评估与测试怎么知道你的 AI 真的在工作6.1 别用感觉评估模型我有段时间觉得模型输出看起来都挺合理就直接上线了直到用户反馈说某类问题答案完全错误我才意识到问题严重性。从那以后我养成了习惯每次改动 prompt 或换模型版本时先建一套评测集用客观指标评估效果。最简单的评测集可以是一条条 JSON 格式的样本每条包含输入、期望输出、评估维度。比如对摘要任务可以建 20 条文档和人工写好的摘要逐条跑模型对比相似度或由第二个模型打质量分。[ { input: 目标检测是计算机视觉领域的一项基础任务..., expected_keywords: [目标检测, 边界框, 分类] } ]跑完评测后我至少会看三个数字成功率能正常解析并返回预期结构的比例关键词命中率对受限任务检查输出是否包含关键实体模型自评得分让一个更强的模型对输出质量按 1-5 打分取平均。6.2 回归测试每次改 prompt 都会影响全局改 prompt 是最容易按下葫芦浮起瓢的操作。你为了修 A 问题把 prompt 加了一句规则结果 B 场景的输出风格全变了。所以我强烈建议把评测集固化成脚本每次修改后跑一遍形成一个简单的回归测试。我个人的做法是在项目根目录建一个tests/目录里面写evaluate_summary.py用 pytest 组织用例。这样配合 CI每次 push 代码都能自动验证 prompt 是否退化在团队协作时尤其重要。pytest tests/ -v如果某次改动导致成功率下降了 10 个百分点那你就知道这个 prompt 修改需要谨慎而不是拍脑袋觉得差不多能行。6.3 成本评估一块钱一次和一分钱一次差别巨大AI 工程的评估不能只看质量还要看成本和延迟。我常用一个简单表格去对照不同模型/不同 prompt 长度下的费用。模型输入价格每百万 token输出价格每百万 token平均单次成本平均延迟gpt-4o-mini低低约 0.005 元1.2 sgpt-4o高高约 0.2 元3.5 s一个小技巧在每次响应的usage里取total_tokens乘以单价累加到日志里这样你可以随时统计线上一天花了多少钱。别等到月底看账单才傻眼。7. 部署上线把 AI 服务交给 FastAPI7.1 从脚本到服务的三个变化我最早写的对话脚本只能在终端里跑离产品差着十万八千里。部署成服务之后就需要考虑三个关键变化并发不再是一次一个用户请求而是同时有多个人调用接口要提供标准的 HTTP 接口方便前端或第三方系统对接稳定性要处理异常、超时、日志和健康检查。FastAPI 是我现在用的最多的框架主要因为它原生支持 async配合开放 AI 的异步客户端能把并发性能发挥得很好。7.2 一个最小可用的 AI 服务端下面这个代码是一个我反复使用的最小骨架包含了 /health 健康检查、聊天接口、基础异常处理from fastapi import FastAPI, HTTPException from pydantic import BaseModel from openai import OpenAI app FastAPI() client OpenAI() class ChatRequest(BaseModel): message: str user_id: str default class ChatResponse(BaseModel): reply: str app.get(/health) def health(): return {status: ok} app.post(/chat, response_modelChatResponse) async def chat(req: ChatRequest): try: response client.chat.completions.create( modelgpt-4o-mini, messages[ {role: system, content: 你是AI助手。}, {role: user, content: req.message} ], max_tokens500, ) return ChatResponse(replyresponse.choices[0].message.content) except Exception as e: raise HTTPException(status_code500, detailstr(e))这里的user_id字段可能现在还没用上但我们事先预留了后面要接记忆系统时几乎不用改接口结构。这就是好的工程习惯先定义稳定的接口边界内部实现随便换。7.3 本地运行与 Docker 部署本地跑uvicorn main:app --reload就能测试。但这只适合开发环境真要部署到服务器我会用 Docker。Docker 最大的价值是把 Python 环境、依赖、代码打包成一个标准镜像避免在我机器上是好的这种尴尬。一份极简 Dockerfile 长这样FROM python:3.11-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . CMD [uvicorn, main:app, --host, 0.0.0.0, --port, 8000]构建和运行docker build -t ai-service .docker run -p 8000:8000 ai-service。这套流程看着土但它是所有人从本地跑通到线上服务的必经之路。很多新手会觉得 Docker 是多余的重型工具等到你在服务器上被 Python 环境折磨两次就明白它的价值了。7.4 部署后的监控别等到用户骂了才发现服务上线后你至少要看三类指标请求量QPS、总请求数错误率4xx、5xx 的数量占比模型消耗每日 token 消耗、成本。最省事的方式是把日志结构化输出用现成的日志采集工具如 ELK 或轻量一点的 Loki收集起来。个人项目我一般只写日志文件隔几天看一眼。但关键错误日志必须单独标记方便快速定位。一个好习惯是每次调用模型前生成一个request_id在日志里贯穿从 HTTP 进入到 LLM 返回的全链路。这样一旦用户报障你可以按request_id查出那次请求的完整痕迹。8. 进阶优化缓存、批量与模型路由8.1 语义缓存省钱的第一个大招对话应用中很多问题其实是重复的比如用户反复问你们几点营业。如果每次都调用大模型既费钱又费时。我的做法是引入缓存层对用户输入做 embedding存进向量库每次先检索相似度极高的历史问题如果命中直接返回历史答案不再调用模型。这个方案落地起来不复杂但见效非常明显——我曾把一个客服机器人的模型调用量降了 40%。但要注意相似度阈值要调好太高会错过可复用的回答太低会答非所问。我常用 0.95 作为起步值再根据业务容忍度调整。8.2 模型路由便宜和贵混着用不是所有请求都需要 GPT-4o 这种顶级模型。比如意图识别这种简单任务用gpt-4o-mini甚至更小的模型就够了而复杂的代码生成、长篇推理才用大模型。我的路由策略是在接口层根据请求类型分配模型。最简单的判断可以基于关键词也可以单独训练一个分类模型早期直接用规则就行。def route_model(user_message: str) - str: if len(user_message) 20 and 查询 in user_message: return gpt-4o-mini return gpt-4o这种做法能让成本降低一半以上但前提是你得对每个模型的实际能力边界非常清楚。所以我还是建议你先用最强模型把效果调通再逐步把简单请求路由到便宜模型上。8.3 批处理优化别一个个请求慢吞吞如果你有一些离线任务比如分析一万条评论的情感不要一条条循环调用 API那样既慢又容易被限流。OpenAI 的 Batch API 允许你上传一个包含多条请求的 JSONL 文件异步执行完成后下载结果。价格比实时调用便宜一半只是延迟较大官方说 24 小时内完成通常几分钟到几小时。我处理批量任务的标准流程把任务列表写成 JSONL 文件用 SDK 创建批量任务提交文件轮询任务状态完成后下载结果文件解析并按原始顺序合并。这一套下来一万条数据可能半小时就处理完了成本比实时省 50%。如果哪天你需要处理几十万条数据这个方案的效率优势会更大。9. 避坑清单我在 From Scratch 路上踩过的雷这一节我整理成清单都是我真实经历过的每一条都对应了实际排查过程。坑现象根因与解法环境变量没加载调用 API 报 401忘了load_dotenv()或者.env文件位置不对。解法确保.env在项目根目录并在构造客户端前加载。上下文窗口溢出请求报错 400messages 太多超过模型 max context。解法引入滑动窗口或摘要压缩。模型输出解析失败json.loads 抛异常模型输出了 Markdown 代码块或多余文字。解法使用response_format try except 正则兜底。超时设置不合理频繁超时但任务并非真慢只设置了连接超时没管读取超时。解法统一设置 timeout或调大 max_tokens 同步调大 timeout。依赖冲突安装新包后旧功能报错全局环境装包导致版本漂移。解法所有项目用 venv锁 requirements 版本。本地能跑服务器不行部署后 API 报网络错误服务器没有配置外网代理或环境变量不同。解法容器化部署把环境变量统一注入。用价格高的模型跑简单任务账单爆炸没有模型路由。解法按任务复杂度分配模型优先用 mini 级。没有评测集乱调 prompt改一个场景坏另一个缺少回归测试。解法固定评测集每次改动跑批量对比。这其中的大多数坑都是靠好奇心 排查链路解决的。我举个例子有一次模型返回的内容总是被截断我首先看的是finish_reason发现是length而不是stop立刻明白是max_tokens不够压根不用猜。遇到任何异常第一个动作永远是看完整返回对象里的finish_reason、error.code和堆栈上下文而不是直接上网搜答案。10. 从个人项目到生产系统的延伸思考聊到这里你已经走完了从零开始搭建一个 AI 应用服务的主干路径环境 → API 调用 → prompt 工程 → 记忆管理 → 评测 → 部署 → 优化。但我想强调这个从零工程的理念不只是用于个人玩具项目。你在小项目里养成的每一个习惯——抽象接口、结构化评测、容错处理、日志留痕、成本意识——在大规模生产系统里只会更重要不会白费。我自己的体会是AI 工程的本质问题不是模型有多聪明而是你在多大程度上掌控了模型的不可控性。而掌控的办法不是依赖趋势里花里胡哨的框架而是老老实实把每一层拆开看一遍亲手接一遍然后把它变成一套属于你自己的方法论。这也是我从这个脏活累活里获得的最大收获。