首页 / 资讯中心 / 文章详情

运维转型大模型全栈开发:从零搭建问答服务的实战指南

运维转型大模型全栈开发:从零搭建问答服务的实战指南 ★ FEATURED ARTICLE
1. 从命令行到对话框一个运维老兵的转型起点两年前的我每天的工作半径基本就是那几百台服务器。早上到工位第一件事是打开终端ssh跳板机然后top、df -h、journalctl -f三连看看夜里有没有哪个服务偷偷挂了磁盘有没有被日志写满CPU 有没有被某个跑飞的进程吃干净。那时候我对“开发”这件事的理解停留在写写 Shell 脚本、用 Python 处理一下日志文件、偶尔改改 Ansible Playbook 的层面。你让我写个 Web 服务我第一反应是“那不是开发该干的活吗”。转折点出现在 2023 年下半年。公司内部开始推一个智能问答助手的项目需求很朴素把内部文档、运维手册、故障案例喂给一个大模型让一线同事能直接问“某某服务报 502 怎么排查”而不是每次都来翻 Confluence 或者群里 人。项目立项的时候领导环视一圈目光落在了我身上——理由很简单我既懂系统又懂点 Python还知道那些文档散落在哪。就这样我半推半就地踏进了大模型全栈开发这条河。现在回头看这两年我踩过的坑、熬过的夜、重构过的代码加起来能写一本小册子。这篇文章不打算讲什么宏大的技术趋势就想把这中间真实经历的东西拆开揉碎讲清楚一个系统运维出身的人要补哪些课、用什么技术栈、怎么从零搭起一个能跑起来的大模型应用、以及那些只有真正动过手才会知道的细节。如果你也是运维、测试、或者传统后端想往这个方向靠那这篇内容应该能帮你少走不少弯路。核心关键词先摆出来大模型、全栈开发、系统运维、Python、FastAPI。这五个词基本概括了我这两年技术栈迁移的主线。下面我会从整体思路、核心技术点、实操过程、踩坑记录几个维度把这条路径完整地摊开。2. 转型的整体思路与技术选型逻辑2.1 为什么运维转大模型全栈有天然优势很多人觉得运维转开发是“跨界”但我实际做下来发现运维背景在这个方向上有几个别人比不了的优势。第一个是对部署和运行环境的直觉。大模型应用最终是要跑在服务器上的涉及 GPU 资源调度、显存监控、服务常驻、日志采集、容器编排这些东西对纯前端或纯算法出身的人来说是黑盒但对我就是日常。我知道nvidia-smi里哪些指标要盯知道 Docker 容器的--shm-size不设大会导致推理进程莫名崩溃知道怎么用 systemd 把服务做成开机自启并限制内存。第二个优势是对稳定性的执念。运维的职业病就是“任何没有监控和告警的服务都是定时炸弹”。所以我在写大模型应用的时候从一开始就会把健康检查接口、超时重试、降级策略、日志分级这些东西考虑进去而不是等上线炸了再补。第三个优势是排查问题的系统性思维。大模型应用的故障链条特别长前端请求 → 网关 → 后端 API → 模型推理 → 向量库检索 → 返回。任何一环出问题表现可能都一样比如“回答很慢”运维出身的人习惯从链路两端往中间夹逼而不是瞎猜。当然劣势也很明显工程规范、代码组织、设计模式这些是短板。我刚开始写的 FastAPI 项目所有路由塞在一个main.py里数据库操作、业务逻辑、模型调用全糊在一起后来被组里的开发同事看到人家没说话默默给我发了篇关于分层架构的文章。这个后面会详细讲。2.2 技术栈选型的取舍为什么是 Python FastAPI大模型这个领域Python基本是绕不开的。原因很现实主流的模型推理库、微调框架、向量库客户端、数据处理工具第一支持语言都是 Python。你想用别的语言要么生态不全要么得自己造轮子。我一开始也想过用 Go 写后端性能好、部署简单但一查发现调用模型、处理 tokenizer、做 embedding 这些环节Go 的库要么没有要么不成熟最后还是老老实实回到 Python。后端框架的选择上我在Flask和FastAPI之间纠结过一阵。Flask 我熟以前写小工具用过简单直接。但大模型应用有个特点大量 IO 等待。一次请求里可能要等模型推理、等向量库检索、等外部 API 返回这些全是 IO 密集操作。Flask 默认是同步的一个请求卡住就占一个线程并发一上来就顶不住。FastAPI 原生支持async/await配合uvicorn跑异步同样的机器能扛的并发高一个量级。而且 FastAPI 自带Pydantic做数据校验、自动生成 OpenAPI 文档对于我这种要同时对接前端和模型服务的人来说省了大量写文档和校验参数的功夫。提示如果你是从 Flask 转过来的别一上来就把所有路由都改成async def。FastAPI 里同步函数会跑在线程池里异步函数跑在事件循环里。如果你在async def里调用了同步的阻塞库比如某些老的数据库驱动反而会把整个事件循环堵死性能比同步还差。判断标准很简单函数里有没有await没有就老老实实写def。2.3 全栈的“全”到底全在哪“全栈开发”这个词被用烂了但在大模型应用这个语境下它其实有很具体的含义。我把它拆成四层层级职责我实际用到的技术前端交互层对话界面、流式输出、文件上传原生 HTML/JS、SSE、少量 Vue后端服务层路由、鉴权、会话管理、限流FastAPI、Pydantic、SQLAlchemy模型推理层模型加载、推理、微调、量化Ollama、Transformers、LoRA数据与检索层向量化、检索、缓存向量库、Redis、Embedding 模型这四层里运维出身的人最容易上手的是模型推理层和数据层因为涉及部署和资源管理最难的是前端交互层。我一开始连 SSEServer-Sent Events是什么都不知道只知道大模型的回答要一个字一个字往外蹦后来才搞明白这就是个长连接推送。前端这块我的策略是“够用就行”不追求花哨能把流式输出和会话历史做出来就达标。3. 核心细节拆解那些必须搞懂的关键点3.1 大模型基础理论不用全懂但这几个概念必须清楚我不是算法出身也没打算去训一个自己的基座模型所以对 Transformer 的内部结构、注意力机制的数学推导我的态度是“知道个大概就行”。但有几个概念是做应用必须搞明白的否则你连参数都调不对。第一个是上下文长度Context Length。你可以把它理解成模型的“短期记忆容量”。模型一次能处理的 token 数量是有限的比如 4K、8K、32K、128K。这个数字直接决定了你能往 prompt 里塞多少东西。我早期做文档问答的时候傻乎乎地把整篇几万字的运维手册直接拼进 prompt结果要么报错超长要么模型把前面的内容全忘了。后来才学会用分块 检索的方式只把最相关的几段塞进去。第二个是Token。模型不是按“字”处理的是按 token。中文里一个汉字大概对应 1 到 2 个 token英文一个单词可能拆成几个 token。这直接影响你的成本估算和长度控制。我习惯用tiktoken这个库先算一下文本的 token 数心里有底再发请求。第三个是温度Temperature。这个参数控制输出的随机性。温度低比如 0.1模型回答稳定、保守适合做故障排查这种要准确答案的场景温度高比如 0.8回答更发散有创意适合写文案。我做运维问答助手的时候温度一直压在 0.2 以下因为一线同事要的是准确答案不是诗。第四个是微调Fine-tuning与 RAG 的区别。这是新手最容易混淆的。简单说微调是改变模型本身的行为让它学会某种风格或领域知识成本高、周期长RAG检索增强生成是不动模型在提问的时候把相关资料检索出来塞进 prompt成本低、更新快。我 90% 的场景都用 RAG只有在需要模型输出特定格式比如固定结构的 JSON 工单时才考虑微调。3.2 Python 环境搭建别小看这一步坑最多运维转过来的人Python 环境搭建按理说不该是问题但我还是在这上面栽过。核心原因是大模型相关的库对版本极其敏感。你系统自带的 Python 可能是 3.8但某些新库要求 3.10你pip install装了一堆包结果和系统包管理器装的包打架。我的建议是永远不要在系统 Python 上直接装大模型相关的库。用conda或者venv建独立环境。我个人的习惯是用conda因为它在处理 CUDA、cuDNN 这些底层依赖时更省心。创建环境的命令很简单conda create -n llm python3.10 conda activate llm选 3.10 是因为它在兼容性和新特性之间平衡得最好3.8 太老3.12 有些库还没跟上。装完环境第一件事是升级 pip然后装核心依赖pip install --upgrade pip pip install fastapi uvicorn[standard] pydantic requests这里有个细节uvicorn[standard]比裸uvicorn多了uvloop和httptools性能更好尤其是做流式输出的时候。我一开始不知道用的裸 uvicorn后来换成 standard 版本同样的并发下响应明显更跟手。注意如果你在 Windows 上开发uvloop是装不上的它只支持 Linux/macOSuvicorn[standard]会自动跳过它不影响使用。但生产环境强烈建议跑在 Linux 上Windows 下异步性能会打折扣。3.3 FastAPI 项目目录结构从一锅粥到分层清晰前面说过我一开始把所有代码塞在main.py里后来被同事“教育”了。一个能维护的 FastAPI 项目目录结构应该是这样的project/ ├── app/ │ ├── __init__.py │ ├── main.py # 入口创建 app 实例 │ ├── config.py # 配置管理 │ ├── api/ │ │ ├── __init__.py │ │ ├── routes_chat.py # 对话相关路由 │ │ └── routes_admin.py # 管理相关路由 │ ├── core/ │ │ ├── llm_client.py # 模型调用封装 │ │ └── security.py # 鉴权 │ ├── models/ │ │ └── schemas.py # Pydantic 模型 │ ├── services/ │ │ └── chat_service.py # 业务逻辑 │ └── db/ │ └── session.py # 数据库会话 ├── requirements.txt └── run.py这个结构的好处是职责分离。路由层只管接收请求、校验参数、返回响应业务逻辑放在services里模型调用这种外部依赖封装在core里。这样改模型、换向量库的时候不用动路由代码。我重构完之后最大的感受是以前改一个功能要翻遍整个文件现在直接定位到对应模块改完不影响别的地方。3.4 模型部署方式的选择本地、私有化还是调 API这是每个做大模型应用的人都要面对的第一个决策。我把三种方式的对比列出来方式优点缺点适用场景调用在线 API零部署成本、模型强数据出域、按量付费、有网络依赖快速验证、非敏感数据本地部署Ollama 等数据不出域、免费、可控吃硬件、模型能力受限内部工具、敏感数据、学习企业私有化部署数据安全、可定制成本高、需要运维生产环境、合规要求高我个人的路径是先用 Ollama 在本地跑起来把整个链路打通再考虑上生产。Ollama 的好处是安装极简一条命令就能拉模型跑起来特别适合学习和验证。比如ollama pull qwen2.5:7b ollama run qwen2.5:7b跑起来之后它会暴露一个兼容 OpenAI 格式的接口在http://localhost:11434你的 FastAPI 直接调这个接口就行。这样你的代码在本地和云端之间切换成本极低因为接口格式是一样的。提示本地跑模型对显存有要求。7B 的模型量化后大概需要 6-8G 显存13B 需要 12-16G。如果你只有 CPU也能跑但速度会慢到让你怀疑人生。我建议至少准备一张 12G 显存的卡能覆盖大部分 7B 到 13B 的量化模型。4. 实操过程从零搭一个能跑的大模型问答服务4.1 第一步把模型服务跑起来假设你已经装好了 Ollama第一步是拉一个模型下来。我选qwen2.5:7b作为示例因为它在中文场景下表现不错体积也适中。拉取命令ollama pull qwen2.5:7b拉完之后验证一下curl http://localhost:11434/api/generate -d { model: qwen2.5:7b, prompt: 用一句话解释什么是系统运维, stream: false }如果返回了正常的 JSON说明模型服务通了。这一步看着简单但它是后面所有工作的基础。我见过有人模型都没跑通就开始写前端结果调了半天发现是模型服务根本没起来。4.2 第二步用 FastAPI 封装模型调用接下来写一个最小的 FastAPI 服务把模型调用封装起来。先定义请求和响应的数据结构from pydantic import BaseModel class ChatRequest(BaseModel): question: str temperature: float 0.2 class ChatResponse(BaseModel): answer: str然后写模型调用的封装。这里我用httpx的异步客户端因为要配合 FastAPI 的异步import httpx OLLAMA_URL http://localhost:11434/api/generate async def call_llm(prompt: str, temperature: float 0.2) - str: payload { model: qwen2.5:7b, prompt: prompt, stream: False, options: {temperature: temperature} } async with httpx.AsyncClient(timeout120.0) as client: resp await client.post(OLLAMA_URL, jsonpayload) resp.raise_for_status() return resp.json()[response]注意这里的timeout设了 120 秒。大模型推理慢尤其是第一次加载模型的时候超时设短了会直接报错。我一开始设的 30 秒结果冷启动的时候经常超时排查了半天才发现是模型加载时间太长。路由部分from fastapi import APIRouter from app.models.schemas import ChatRequest, ChatResponse from app.core.llm_client import call_llm router APIRouter() router.post(/chat, response_modelChatResponse) async def chat(req: ChatRequest): answer await call_llm(req.question, req.temperature) return ChatResponse(answeranswer)这样最小可用的问答接口就有了。启动命令uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload--reload是开发时用的改代码自动重启生产环境一定要去掉。4.3 第三步加上流式输出让回答一个字一个字蹦非流式的接口用户要等模型全部生成完才能看到结果体验很差。大模型应用基本都要做流式输出。Ollama 支持stream: true返回的是一行行的 JSON。FastAPI 这边用StreamingResponse配合生成器from fastapi.responses import StreamingResponse import json async def stream_llm(prompt: str): payload { model: qwen2.5:7b, prompt: prompt, stream: True } async with httpx.AsyncClient(timeout120.0) as client: async with client.stream(POST, OLLAMA_URL, jsonpayload) as resp: async for line in resp.aiter_lines(): if line: data json.loads(line) chunk data.get(response, ) if chunk: yield chunk router.post(/chat/stream) async def chat_stream(req: ChatRequest): return StreamingResponse( stream_llm(req.question), media_typetext/event-stream )前端用EventSource或者fetch的流式读取来接。这里有个坑Nginx 反代默认会缓冲响应导致流式输出变成一次性返回。解决办法是在 Nginx 配置里加上proxy_buffering off;和X-Accel-Buffering: no响应头。我在这上面卡了整整一个下午一直以为是后端代码问题最后才发现是 Nginx 在捣鬼。4.4 第四步接入 RAG让模型能回答私有知识光有模型还不够它不知道你公司的内部文档。RAG 的思路是把文档切块、向量化、存进向量库用户提问时先把问题向量化检索出最相关的几块拼进 prompt 一起发给模型。文档切块我用的是简单的按字数切每块 500 字重叠 50 字。重叠是为了避免关键信息正好被切在边界上。向量化用sentence-transformers里的中文模型from sentence_transformers import SentenceTransformer model SentenceTransformer(BAAI/bge-small-zh-v1.5) embeddings model.encode([你的文档内容])向量库我选的是Chroma因为它轻量、纯 Python、不用额外起服务适合中小规模。存和查的代码import chromadb client chromadb.PersistentClient(path./chroma_db) collection client.get_or_create_collection(docs) # 存入 collection.add( documents[文档块内容], embeddings[embedding.tolist()], ids[doc_1] ) # 检索 results collection.query( query_embeddings[query_embedding.tolist()], n_results3 )检索出来的内容拼进 promptcontext \n.join(results[documents][0]) prompt f根据以下资料回答问题\n{context}\n\n问题{question}这一步做完你的问答助手就能回答私有知识了。实测下来7B 的模型配合好的检索在运维问答这种场景下准确率能到七八成剩下的靠人工兜底。4.5 第五步打包部署与资源监控开发完了要部署。我习惯用 Docker 打包Dockerfile 大概长这样FROM python:3.10-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . CMD [uvicorn, app.main:app, --host, 0.0.0.0, --port, 8000]模型服务单独跑一个容器用--gpus all挂载 GPU。两个容器用 Docker Compose 编排网络互通。监控这块运维的老本行派上用场了。我盯几个核心指标GPU 显存占用用nvidia-smi或pynvml、请求响应时间FastAPI 中间件记录、模型服务存活定时健康检查。显存泄漏是大模型服务常见问题跑久了显存只增不减最后 OOM。我的做法是设置一个请求数阈值超过就重启模型服务容器简单粗暴但有效。5. 常见问题与排查技巧实录5.1 模型相关的高频问题问题一模型回答很慢首字延迟高。排查顺序先看 GPU 利用率如果利用率低但延迟高可能是模型没加载到 GPU 上跑在 CPU 了如果利用率高那是模型本身大或者量化不够。解决办法换更小的量化模型或者开启 KV Cache。问题二回答内容胡编乱造幻觉。这是大模型的通病。缓解手段降低温度、在 prompt 里明确要求“如果资料里没有就说不知道”、提高检索质量。我实测下来把温度从 0.7 降到 0.2幻觉能减少一大半。问题三上下文超长报错。要么是 prompt 拼太长要么是历史对话没截断。解决办法对历史对话做滑动窗口只保留最近 N 轮对检索内容做 token 数限制。5.2 FastAPI 与部署相关的问题问题四uvicorn日志丢失。这个我专门查过。原因是 uvicorn 默认的日志配置和 Python logging 模块有冲突尤其是用--reload或者多 worker 的时候。解决办法是自定义 logging 配置在启动时传入uvicorn.run(app.main:app, log_configlog_config.yaml)或者在代码里显式配置 logger别依赖 uvicorn 默认的。问题五Windows 下打包后跑不起来。FastAPI 项目在 Windows 上打包比如用 PyInstaller经常遇到路径问题和依赖缺失。我的建议是别在 Windows 上折腾打包直接上 Linux。如果非要 Windows用uvicorn的--workers 1多 worker 在 Windows 上问题更多。问题六并发一高就卡死。检查是不是在async def里调了同步阻塞代码。比如用了同步的requests而不是httpx或者用了同步的数据库驱动。把阻塞调用丢到run_in_executor里或者换成异步库。5.3 问题速查表现象可能原因排查动作解决方向首字延迟高模型在 CPU 上跑看 GPU 利用率检查 CUDA 和模型加载回答乱编温度过高/检索差看 prompt 和检索结果降温、优化检索流式变一次性Nginx 缓冲看响应头关 proxy_buffering日志丢失logging 冲突看 uvicorn 配置自定义 log_config并发卡死异步里调同步看代码有无阻塞调用换异步库或线程池显存只增不减显存泄漏长时间监控定期重启服务提示排查大模型应用的问题最有效的办法是在每一层加日志。请求进来打一条检索完打一条模型调用前后各打一条返回前打一条。这样出问题的时候看日志就知道卡在哪一环比瞎猜快得多。6. 转型两年我真正想说的几句实话这两年最大的感受是大模型全栈开发难的不是模型本身而是工程。模型有现成的、有开源的、有 API但把它变成一个稳定、好用、能维护的服务靠的是扎实的工程能力。而这恰恰是运维出身的人可以发力地方——我们对部署、监控、稳定性的理解是很多纯算法背景的人欠缺的。另一个体会是别追求把每个环节都学透。我一开始想搞懂 Transformer 的每一个矩阵运算结果浪费了大量时间对做应用帮助有限。后来我调整策略把精力放在“怎么把模型用好”上——prompt 怎么写、检索怎么优化、服务怎么稳定这些才是应用层真正值钱的东西。最后分享一个我一直在用的小技巧给自己建一个“踩坑日志”。每次遇到问题、解决完就记一条现象、原因、解决办法。我现在的踩坑日志已经有一百多条了从“Ollama 端口被占用”到“Chroma 持久化路径写错”全是血泪。这个东西比任何教程都管用因为它是你自己踩出来的。下次再遇到类似问题翻一下日志五分钟解决不用再花一下午。如果你也是运维或者传统后端正在犹豫要不要往这个方向转我的建议是先动手跑起来一个最小的 demo别一上来就啃理论。跑通一个“提问-回答”的闭环你会立刻知道自己缺什么然后有针对性地补。这条路我走过来了虽然磕磕绊绊但确实值得。
阅读完成 · 觉得有帮助?
咨询建站