ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

AI全栈开发最佳实践:把大模型变成可靠产品的工程方法

AI全栈开发最佳实践:把大模型变成可靠产品的工程方法 做AI应用开发这一年多我最大的感受是AI全栈开发的最佳实践不是某一种语言的框架也不是某个模型的API调用而是一整套把大模型这种“不太稳定”的东西变成可靠产品的工程方法。很多人觉得会写几个Prompt、能调通接口就等于会做AI应用真到上线才发现问题五花八门输出格式今天正常明天乱掉、上下文动不动超长、token成本飙到离谱、同一套代码想换不同模型就得推倒重写。这篇文章我把自己在真实项目中沉淀下来的整套思路完整拆一遍包含技术选型、架构分层、核心实现、部署运维和排查技巧适合正在从传统开发转向AI方向或者已经在做AI应用但总觉得在“补窟窿”的团队。内容不追求理论深奥重点是可落地、能直接抄作业。1. AI全栈开发到底在做什么1.1 技术栈全景从模型到前端要打通哪些环节AI全栈开发本质上还是全栈开发只是中间多了一个大模型并且这个大模型不是简单调用的HTTP服务而是一个有状态、有随机性、还会“胡说八道”的组件。因此技术栈的覆盖面比传统全栈更广我大致分成四层层需要掌握的技术典型选择前端交互层页面、流式交互、结果展示React、Next.js、Vue、Streamlit应用逻辑层业务编排、鉴权、数据流FastAPI、Spring Boot、Node.js模型接入层模型调用、统一网关、RAG、AgentOpenAI SDK、LangChain、LlamaIndex、LiteLLM数据与部署层向量库、缓存、容器、监控Qdrant、PostgreSQL、Redis、Docker、K8s这里面最容易踩的坑是很多人把“模型接入层”直接塞进了业务代码里。比如在业务Service里写死一段调用OpenAI的代码同时又把Prompt散落在各个函数中。结果就是模型升级要动业务代码换供应商要动业务代码改一句Prompt也要发一个版本。最佳实践是把模型接入和业务解耦让应用逻辑层只关心“调用一个接口拿到结果”至于这个结果来自哪个模型、怎么被加工出来的由模型接入层负责。另外如果是Java技术栈的团队可以关注一下Spring AI它对Java生态做了一整套封装内置了向量存储、结构化输出、Model接口抽象跟Spring Boot集成非常顺。Python团队则更自由直接用官方SDK加LangChain这类工具就能快速搭起来。技术栈没有绝对的好坏关键是团队能不能长期维护。1.2 与传统全栈相比多出来的“隐形工作量”传统全栈开发面对的依赖是确定性的数据库就是数据库接口返回什么就是什么。但大模型不一样它有四个让开发者头大的特性。第一是随机性。同一个Prompt温度设成0.7每次返回的内容都会有差异甚至有概率返回格式错误。传统开发不会遇到“昨天好的代码今天爆了”的情况但AI应用会。第二是延迟和成本不可控。模型推理一秒钟可能只是生成几十个token复杂Agent跑一次可能调用七八次模型账单会让人措手不及。第三是版本迭代太快。今天用的API明天可能就标记废弃某个模型效果不好说换就换。第四是安全性。模型的输出内容需要校验、过滤、审计不能把不被验证的信息直接呈现给终端用户。这些“隐形工作量”决定了一个AI应用能不能活得久。所以“最佳实践”不是教你写某一堆代码而是教你怎么用工程手段把这些不确定性管起来。后续每一章都是在解决这里面的具体问题。2. 架构设计先把AI应用的骨架想清楚2.1 分层设计每一层只做一件事做AI应用最忌讳“大杂烩式的单层应用”。我见过不少项目一个Python文件几百行里面既有API路由又有Prompt模板还直接连数据库、调模型最后调试起来根本无从下手。合理做法是严格分层让每一层只关心自己的职责。以一个企业知识问答助手为例完整链路是这样的用户在前端输入问题请求进入后端应用层应用层先做权限校验然后调用模型接入层的“检索问答服务”这个服务内部先查向量库召回相关文档再把文档和问题组装成Prompt调用统一网关模型返回结果后应用层做结构化校验最后把答案和引用来源返回给前端。应用层不应该知道模型是GPT还是Claude也不应该知道文档被切成多少份。它只负责业务流程的编排。这样设计有几个直接好处换模型只动模型接入层加功能不动老逻辑也方便写单元测试。我的习惯是在项目初期就把model_client和business_service分开建目录从代码结构上强制约束这个边界。2.2 模型网关统一接入多模型的关键一环很多小团队一开始只有一个模型供应商的Key觉得没必要再引入一层。等到业务跑起来想加一个竞品模型对比效果或者接入开源模型减少成本才发现所有业务代码里都写死了某个SDK的调用方式改动量巨大。这就是我坚持用模型网关的核心原因。LiteLLM就是一个非常典型的开源模型网关服务它把OpenAI、Anthropic、Google Gemini以及大量开源模型的接口统一成了同一种格式。你在业务代码里只需要写一套OpenAI SDK风格的调用网关负责把请求转发到真实的模型服务商同时还能做限流、重试、日志记录和成本统计。这里我不展开讨论网络层面的细节单说它在工程上的价值一是让上层代码与具体模型解耦二是多模型切换只改配置不改代码三是在一个集中位置做安全和审计。配置上也很轻量用一份YAML声明即可把多个模型挂进去model_list: - model_name: gpt-4o litellm_params: model: gpt-4o api_key: ${OPENAI_API_KEY} - model_name: claude-sonnet litellm_params: model: anthropic/claude-sonnet-4 api_key: ${ANTHROPIC_API_KEY}这样业务代码调用时只需要指定gpt-4o或claude-sonnet这个逻辑名称。线上发现Claude效果更好改一行配置就切过去完全不用改接口逻辑。2.3 Agent与工作流先判断是否真的需要现在“AI Agent”这个词被炒得很热好像什么应用不上Agent就是落伍。但我实际做下来的经验是大多数业务场景先用“工作流”解决解决不了再上Agent这才是最优路径。工作流和Agent的区别在于谁在控制过程。工作流是开发者用代码把步骤写死先判断意图再查数据最后让模型生成结果。每一步都是确定的好调试、好监控、改起来也安全。Agent则是把大目标交给模型让模型自主决定调用哪些工具、按什么顺序执行。Agent灵活但也是不确定性的放大器。我的判断标准很简单如果任务的步骤是固定的比如“查库存-算价格-生成订单摘要”就用工作流如果任务没法预判比如“帮我研究某个行业并写一份报告过程中需要自己找资料、筛选、总结”这时候才值得上Agent。如果需要上Agent核心是做好工具封装和终止条件。模型每调用一个工具都要有明确的输入输出定义并且要限制最大调用轮数防止它在循环里出不来。还要在每一轮记录思考过程方便后期排查“为什么它走了这条路径”。这个成本不小所以别轻易为了炫技上Agent。3. 核心细节与实操要点3.1 Prompt管理版本化与模板化Prompt是AI应用的核心资产但很多人把Prompt直接写在代码字符串里这其实是很危险的做法。一个成熟的AI应用Prompt一定会频繁调整。今天用户反馈回答太啰嗦你改几个字明天业务方要求语气更正式你又改一段。如果Prompt没有版本化你根本不知道当前线上跑的是哪一版出了问题也没法快速回滚。我的做法是把Prompt当成代码一样管理。每个Prompt独立成文件或独立成配置项命名带上版本特征例如QA_SYSTEM_V2.txt。同时建立Prompt评审流程每次修改都要记录改动原因和测试结果。这一步听起来繁琐但在团队协作时能省下大量扯皮时间。模板化也同样重要。不要在业务代码里拼接长字符串而是用占位符再用专门的模板引擎填充。这样改提示词的人完全不需要懂代码prompt_template 你是公司的运维知识库助手请根据以下知识内容回答用户问题。 context {context} /context 用户问题{question} 回答要求 1. 如果知识库中有相关内容请给出答案并标注引用来源编号。 2. 如果知识库中没有相关内容请直接说“我没有找到相关资料”。 这里有个很容易被忽略的细节上下文和问题要用清晰的分隔符包住模型才能准确区分“参考资料”和“即时问题”。很多人直接把文档往Prompt里一塞模型经常分不清哪些是用户问题、哪些是背景资料。3.2 结构化输出让模型稳定出JSON做AI应用最痛苦的事情之一就是模型返回的结果没法直接喂给前端或数据库。你说“请返回JSON”它给你一段带解释文字的代码块你让输出5个字段它随机少一个你说价格是数字它给你写“约35元左右”。这些随机性在传统开发里完全不可想象但模型就是这样的。解决思路不是靠“please”语气而是靠工程手段。第一是使用JSON Mode或结构化输出功能各家主流模型都开始支持。第二是在代码里做强校验和重试。我用的是Pydantic定义输出结构模型一返回就用Pydantic做校验解析失败就自动重试一次第二次还是失败则走兜底策略。from pydantic import BaseModel from openai import OpenAI class Answer(BaseModel): answer: str sources: list[str] confidence: float client OpenAI() resp client.beta.chat.completions.parse( modelgpt-4o, messages[{role: user, content: 介绍一下RAG的定义}], response_formatAnswer, ) result resp.choices[0].message.parsed这里的关键点是把“模型原始返回”原样保存到日志里而不是只存解析后的结果。很多诡异的问题要靠原始输出才能定位比如某次模型把JSON放在markdown代码块里或者插入了不可见字符。排查这些只能看原始日志。3.3 流式响应体验与工程挑战聊天类AI应用默认就应该用流式输出。用户看到文字一点点打出来心理等待时间会大幅缩短对后端来说首字时间Time to First Token比完整返回时间更重要。不流的接口在交互体验上基本属于“劣质产品”。后端实现流式通常用SSEServer-Sent Events。FastAPI里用异步生成器配合EventSourceResponse就能实现。核心是在模型返回每个增量时立刻通过SSE推给前端同时要处理前端断开连接的情况避免后端还在傻傻地继续生成token。from fastapi import FastAPI from fastapi.responses import StreamingResponse async def chat_stream(): stream client.chat.completions.create( modelgpt-4o, messages[], streamTrue, ) for chunk in stream: delta chunk.choices[0].delta.content if delta: yield fdata: {delta}\n\n app FastAPI() app.post(/chat) async def chat(): return StreamingResponse(chat_stream(), media_typetext/event-stream)前端用EventSource或者fetch读取流。需要注意几个坑SSE连接有最大超时时间需要心跳机制保活用户如果中途停止前端要调用AbortController中止连接代理服务器或网关如果做了缓冲会导致流式失效需要在响应头里关闭缓冲。3.4 RAG落地先解决召回质量再谈花活很多人一听RAG就兴奋觉得只要配个向量数据库就等于拥有了“私有知识库”。结果做出来发现回答经常引用无关段落有资料却答不出准确答案。问题往往出在最前面——文档切分和召回质量而不是模型不强。文档切分切忌“一刀切”。固定每块500个字符这种简单策略会让语义完好的长段落被切成碎片语义关联的信息被拆散。我的做法是优先按标题、段落、列表等结构切分再对超长段落做二次切分并保留重叠区。切分之后打上结构标签比如来源文档名、章节路径、页码这能让召回后更好地组装引用信息。向量检索也不是终点。真实业务中用户问题往往是口语化的跟文档原文差异很大单纯靠向量相似度不一定能召回正确资料。一种常用优化是“混合检索”向量检索用来找语义相似内容关键字检索用来精确匹配专有名词和编号最后用重排模型对合并结果重新打分。这样既能抓住模糊意图也能抓住精确线索。引用来源是我特别在意的点。面向企业场景的知识问答答案不能“无中生有”。我的做法是所有给模型的上下文都带上唯一编号Prompt里强制要求模型回答时必须标注引用了哪些编号。后端再把这些编号映射为真实的来源链接。这样用户能看到答案出自哪个文件也方便后续审计。4. 实操过程一个AI问答助手从0到14.1 场景设定与技术选型为了说明整套流程我用一个“团队内部知识问答助手”作为案例。这个助手的功能是用户问任何关于公司制度、技术规范的问题系统先从知识库检索相关资料再交给大模型生成回答并给出引用来源。技术选型如下后端Python FastAPI异步处理天然适合流式场景模型接入LiteLLM统一管理多个模型主模型用GPT-4o级别摘要类任务走轻量模型向量库Qdrant支持Docker一键启动过滤功能够用前端React Vite用SSE处理流式输出部署Docker Compose一个容器跑后端一个跑向量库前端用Nginx托管为什么要这么选核心是尽量简单可控。Qdrant比Elasticsearch轻量得多FastAPI和OpenAI SDK的配合也非常顺滑。团队如果要快速出活这个组合一周内就能跑通。4.2 后端关键实现后端接口负责两件事接收问题、检索上下文、组装Prompt、调用模型、把流式结果返回给前端。核心代码如下from qdrant_client import QdrantClient from openai import OpenAI qdrant QdrantClient(hostlocalhost, port6333) client OpenAI(base_urlhttp://localhost:4000) # LiteLLM统一入口 app.post(/api/query) async def query(request: QueryRequest): # Step 1: 检索相关文档 hits qdrant.search( collection_namecompany_knowledge, query_vectorembed(request.question), limit5, ) context_parts [] for i, hit in enumerate(hits): context_parts.append(f[{i1}] {hit.payload[text]}) context \n.join(context_parts) # Step 2: 组装Prompt messages [ {role: system, content: 你是企业内部知识助手...}, {role: user, content: f参考内容\n{context}\n\n问题{request.question}}, ] # Step 3: 流式返回 stream client.chat.completions.create( modelgpt-4o, messagesmessages, streamTrue, ) return StreamingResponse(stream_to_sse(stream), media_typetext/event-stream)需要注意两个点。第一向量检索里用的embedding模型要和写入知识库时用的是同一个否则向量空间不一致检索效果会非常差。第二检索结果建议多拿几条但Prompt里只保留按相关度排序后的Top N避免超长和噪音干扰。我一般取Top 5如果文档切分块很小取8到10条也行。4.3 前端集成要点前端这块最大的坑在流式处理上。如果用原生fetch响应头里没有Content-Type: text/event-stream拿到的body可能是一次性返回的。建议直接用EventSource但要注意EventSource只支持GET请求如果要做POST或者带自定义头就得回到fetch加流式读取。React里一个比较稳妥的流式读取写法const response await fetch(/api/query, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ question }), }); const reader response.body.getReader(); const decoder new TextDecoder(); let answer ; while (true) { const { done, value } await reader.read(); if (done) break; answer decoder.decode(value, { stream: true }); setOutput(answer); }这里要处理两种数据SSE的data:前缀要剥掉网络中断时要提示用户“生成被中断请重试”。前端在流式返回过程中不应该做额外解析保持“拿到一段就显示一段”即可。如果中间需要解析JSON片段等流结束再做统一处理更稳妥。4.4 部署与成本控制部署阶段Docker Compose是最快的方式。我一般分为三个服务后端API、Qdrant向量库、前端Nginx。环境变量通过.env文件管理绝不写死在代码里。services: qdrant: image: qdrant/qdrant ports: - 6333:6333 backend: build: ./backend env_file: .env ports: - 8000:8000 frontend: build: ./frontend ports: - 80:80成本控制是AI应用绕不开的话题。我的经验是能不上大模型的地方就别上。知识库索引可以用轻量开源模型做embedding摘要类的轻任务用小模型只有最终生成回答才用能力最强的大模型。再配合网关层的成本统计按天看每个模型的调用次数和费用一旦某个模型预算异常就能立刻发现。Redis也是一个很好的省钱工具把相同问题的结果缓存一段时间高频重复提问就能直接命中不再重复调用模型。5. 常见问题与排查技巧实录5.1 模型输出格式不稳定怎么办这是出现频率最高的问题。模型偶尔不按要求的JSON格式输出或者返回了多余的解释文字。遇到这种问题先不要急着改Prompt按照下面这个顺序排查。症状可能原因解决方案偶尔返回非JSON模型随机性导致开启JSON Mode用Pydantic强校验加自动重试字段总是缺失输出结构过于复杂拆分多个子任务每次只要求模型输出少量字段结果前后不一致推理温度过高将temperature调低到0.2以下需要事实性回答时尤其重要带markdown代码块没开启结构化模式强制关闭markdown或在后端剥掉代码块标记我见过最头疼的一种情况是模型输出了一个合法JSON但字段语义反了比如source和answer互换了。这种靠校验没法发现只能通过回归样本做测试。所以“输出格式稳定”这件事不是一个纯后端问题需要建立一套针对Prompt的回归测试集。5.2 上下文窗口不够用怎么办大模型的上下文窗口有上限文档一长就爆。很多人第一反应是“换一个窗口更大的模型”但这不是最优解。更好的策略是先优化输入再考虑升级模型。第一步是精简Prompt。系统提示词控制在几百字以内不要写一堆无关的“角色设定”。第二步是把知识外置把长文档放到向量库里只把检索到的相关片段放进上下文。第三步是对话历史压缩。多轮对话的场景下把早期对话丢给轻量模型做摘要只保留摘要加最近几轮原文。这三步做完绝大多数场景都不需要买更大的窗口。如果真的换了大窗口模型也要小心。上下文越长模型对中段内容的注意力越弱而且每轮调用费用会指数上升。不是窗口越大越划算。5.3 Token费用很快爆掉成本失控是AI应用最容易被低估的风险。我见过一个项目上线两周账单直接超了预算五倍。核心原因是没有监控也没有限流。要控制成本可以从这几个方面下手第一在模型网关层设置每日预算上限超过就自动熔断防止异常调用导致天价账单。第二给不同用户设置不同的速率限制免费用户和付费用户能用的次数不一样。第三用缓存拦截重复问题相同问题直接返回历史答案。第四按需分级使用模型翻译、分类、摘要这种简单任务用廉价小模型只有核心创作和复杂推理才用旗舰模型。每月的成本结构建议按“模型、功能、用户”三个维度来分析。哪块费用异常高就针对性地做优化。没有成本数据的AI项目就像开车没有仪表盘迟早出事。5.4 并发超时与稳定性AI应用的延迟天然比普通API高一个请求可能耗时几秒甚至几十秒。如果后端按同步方式处理并发一上来服务立刻挂掉。我的建议是高延迟场景全部走异步长任务用消息队列削峰。再就是超时和重试的设置。模型调用可能因为上游过载而超时这时候要设置合理的重试策略。重试不是盲目加重试次数而是用指数退避比如第一次等1秒、第二次2秒、第三次4秒避免上游还没恢复就把自己这边的线程打满。熔断也很重要。如果模型网关连续失败超过阈值就直接短路快速返回“当前服务繁忙”的兜底文案而不是让用户无限等待。这些策略在传统微服务里很常见放在AI应用上同样有效。我个人在实际项目里体会最深的一件事是AI全栈开发和传统软件开发最大的不同就是你要把“不确定性”当成系统的一部分来设计。模型会变、Prompt会变、成本会变只要提前做好了模型网关、结构化输出、日志监控和成本控制这四件事整个系统无论怎么迭代都比较稳。反过来如果一上来就追逐“效果最好”的模型却忽略了这些工程底座那不管换多强的模型都会陷入天天修问题的循环。把地基打牢AI这块上层建筑才能造高。
返回列表