运维转大模型全栈:FastAPI+Ollama+LangChain实战与避坑指南

发布时间:2026/10/7 18:18:05
运维转大模型全栈:FastAPI+Ollama+LangChain实战与避坑指南 1. 从告警短信到Token流一个运维人的转型起点2022年之前我的日常基本被三样东西填满Zabbix告警、Kubernetes的Pod重启记录、以及凌晨三点被叫起来处理磁盘写满的工单。那时候我对“大模型”这三个字的理解大概停留在“能写诗的那个东西”的层面。直到有一次业务方提了一个需求——把内部知识库做成问答接口我才第一次认真去跑通一个本地推理服务。结果发现部署一个模型比部署一套微服务还折腾CUDA版本对不上、显存不够、推理速度慢得让人怀疑人生。但正是这次折腾让我意识到一件事运维的底层能力和大模型工程化之间其实只隔着一层窗户纸。这篇文章不是成功学也不是劝退帖。我想把这两年从系统运维转向大模型全栈开发的完整路径拆开来讲——包括我踩过的坑、做过的技术选型、以及那些“当时要是有人告诉我就好了”的关键节点。如果你也是运维出身正在观望要不要往大模型方向走或者你已经在大模型领域但工程化能力偏弱那这篇内容应该能给你一些可以直接抄作业的东西。先说结论运维转大模型全栈最大的优势不是算法而是对“系统稳定性”和“资源边界”的直觉。你知道一个服务在什么情况下会崩你知道日志该往哪里打你知道怎么用最少的机器扛住最多的请求。这些东西很多纯算法出身的人反而需要花很长时间补。而你需要补的是Python工程化能力、模型推理的基本原理、以及一套能把模型变成产品的后端框架。我选的技术栈很明确Python FastAPI Ollama LangChain。为什么是这套后面会详细拆。先说说我最初犯的一个错误——试图用Flask去扛大模型的流式输出。结果就是每次请求都像在等一个世纪前端体验极差。后来换成FastAPI原生支持异步和SSE才算是把这条路走通了。2. 为什么我最终选了FastAPI而不是Flask2.1 从运维视角看框架选型并发模型决定一切运维出身的人对“并发”这个词是有肌肉记忆的。我们习惯看QPS、看连接数、看线程池大小。所以在选后端框架的时候我第一反应就是去看它的并发模型。Flask默认是同步阻塞的一个请求进来线程就被占住直到处理完才释放。这在传统CRUD场景下没问题但大模型推理动辄几秒到几十秒如果还用同步模型你的服务并发能力基本等于线程数。FastAPI基于Starlette原生支持async/await。这意味着当一个请求在等模型推理结果的时候事件循环可以去处理其他请求。实测下来同样的机器配置FastAPI的并发吞吐量大概是Flask的3到5倍。这个差距在大模型场景下会被进一步放大因为推理时间越长同步模型浪费的线程资源就越多。还有一个细节FastAPI自动生成OpenAPI文档。这个功能在前后端联调的时候太省事了。你定义好Pydantic模型Swagger UI直接就能用前端同事不用再追着你问接口字段是什么。对于运维转开发的人来说这种“自带文档”的特性极大降低了沟通成本。2.2 FastAPI项目目录结构我踩过的三个坑刚开始写FastAPI的时候我按照Flask的习惯把所有路由都塞在main.py里。结果不到一周文件就膨胀到八百多行改一个接口要翻半天。后来参考了一些开源项目的结构才慢慢调整成现在这套project/ ├── app/ │ ├── main.py │ ├── api/ │ │ ├── __init__.py │ │ ├── routes/ │ │ │ ├── chat.py │ │ │ ├── model.py │ │ │ └── health.py │ │ └── dependencies.py │ ├── core/ │ │ ├── config.py │ │ └── logging.py │ ├── models/ │ │ └── schemas.py │ ├── services/ │ │ ├── llm_service.py │ │ └── vector_service.py │ └── utils/ │ └── helpers.py ├── tests/ ├── requirements.txt └── Dockerfile第一个坑是循环导入。routes里引用了servicesservices又引用了modelsmodels反过来引用routes里的某个类型。解决方法是把Pydantic的Schema单独放在models/schemas.py里所有层都只依赖它不依赖具体路由。第二个坑是配置管理混乱。开发环境和生产环境的Ollama地址不一样我一开始硬编码在代码里部署的时候改来改去。后来用pydantic-settings统一管理通过环境变量注入才算清爽了。第三个坑是日志丢失。这个问题后面会单独讲因为它是FastAPI Uvicorn组合下最容易被忽略的坑之一。2.3 异步接口的写法别在async函数里干同步的事FastAPI支持async def和普通def两种路由写法。我一开始图省事全写成async def然后在里面调用同步的requests库去请求Ollama。结果发现事件循环被阻塞了并发能力反而比Flask还差。正确的做法是如果调用的库是同步的就用普通def写路由FastAPI会自动把它放到线程池里执行。或者用httpx这种支持异步的HTTP客户端配合async def使用。我现在的做法是统一用httpx.AsyncClient所有外部调用都是异步的这样事件循环才能真正发挥作用。import httpx from fastapi import APIRouter router APIRouter() router.post(/chat) async def chat(prompt: str): async with httpx.AsyncClient(timeout60.0) as client: response await client.post( http://localhost:11434/api/generate, json{model: qwen2:7b, prompt: prompt} ) return response.json()这段代码看起来简单但背后有一个关键点超时时间必须设置。大模型推理有时候会卡住如果不设超时请求会一直挂着最终拖垮整个服务。我一般设60秒超过就返回一个友好的错误提示。3. 大模型本地部署Ollama是我试过最省心的方案3.1 为什么不用直接跑Transformers刚开始的时候我按照网上的教程用transformers库直接加载模型。下载权重、配置CUDA、处理显存分配一套流程走下来半天就没了。而且每次换模型都要重新折腾一遍。更麻烦的是transformers的推理服务需要自己写API封装要考虑批处理、并发、显存回收工程量不小。后来试了Ollama发现它把这些问题都解决了。Ollama的核心价值在于它把模型下载、量化、推理服务、API暴露全部打包成了一个命令行工具。你只需要ollama run qwen2:7b它就会自动下载模型并启动一个本地推理服务默认监听11434端口。对于运维出身的人来说这种“一条命令搞定”的体验太熟悉了就像用docker run启动一个容器一样自然。3.2 Ollama部署大模型的硬件门槛与量化选择这里必须说清楚一个现实问题大模型对显存的要求是有硬门槛的。我一开始用一台16GB显存的机器跑7B模型勉强能跑但上下文长度一开大就OOM。后来换成量化版本Q4_K_M显存占用降到6GB左右才算是稳定了。模型规模FP16显存需求Q4量化显存需求推荐场景7B约14GB约4-6GB个人开发、小规模问答13B约26GB约8-10GB中等规模知识库70B约140GB约35-40GB企业级私有化部署量化本质上是用精度换空间。Q4量化会把模型权重从16位浮点数压缩到4位整数显存占用降到原来的四分之一左右但推理质量会有一定下降。我的经验是7B模型用Q4量化日常问答基本够用如果要做代码生成或者复杂推理建议至少上13B并且用Q5或Q8量化。还有一个容易被忽略的点上下文长度直接影响显存占用。Ollama默认的上下文长度是2048如果你调到8192显存占用会显著增加。我一般根据实际需求设置不要盲目开大。3.3 FastAPI调用Ollama的完整封装把Ollama的API封装成FastAPI接口是我日常用得最多的功能。这里分享一个我实际在用的封装包含了流式输出和错误处理import json import httpx from fastapi import APIRouter, HTTPException from fastapi.responses import StreamingResponse router APIRouter() OLLAMA_BASE http://localhost:11434 async def stream_generator(prompt: str, model: str): async with httpx.AsyncClient(timeout120.0) as client: async with client.stream( POST, f{OLLAMA_BASE}/api/generate, json{model: model, prompt: prompt, stream: True} ) as response: async for line in response.aiter_lines(): if line: data json.loads(line) yield fdata: {json.dumps(data, ensure_asciiFalse)}\n\n router.post(/chat/stream) async def chat_stream(prompt: str, model: str qwen2:7b): return StreamingResponse( stream_generator(prompt, model), media_typetext/event-stream )这段代码的关键在于StreamingResponse和text/event-stream。前端用EventSource接收就能实现打字机效果。注意ensure_asciiFalse不然中文会被转义成Unicode编码前端显示会出问题。4. Uvicorn日志丢失一个让我排查了整整两天的问题4.1 问题现象日志为什么凭空消失了那是一个周五的下午我在调试一个FastAPI接口发现print出来的日志在终端里能看到但用logging模块打的日志却不见了。更诡异的是Uvicorn的访问日志正常输出唯独我自己定义的logger没有任何输出。我检查了日志级别是DEBUG检查了handler配置没问题检查了logger名称也对得上。但日志就是不出来。这个问题困扰了我整整两天。期间我试过重启服务、换Python版本、甚至重装了Uvicorn。最后在一个技术社区的角落里看到有人提到Uvicorn的日志配置会覆盖根logger的handler才算是找到了方向。4.2 根因定位Uvicorn的日志配置机制Uvicorn在启动的时候会调用logging.config.dictConfig来配置自己的日志系统。这个配置会清空根logger上已有的handler然后重新添加Uvicorn自己的handler。如果你的日志配置是在Uvicorn启动之前加载的那它就会被覆盖掉。具体来说Uvicorn的默认日志配置里有一个disable_existing_loggers选项默认值是False但在某些版本或者特定配置下它会变成True导致你之前定义的所有logger都被禁用。4.3 三种修复方案与我的最终选择方案一在Uvicorn启动之后再配置日志。把日志配置放到main.py的if __name__ __main__块里确保它在Uvicorn初始化之后执行。这个方案最简单但如果你用uvicorn命令行启动就不太好控制执行顺序。方案二自定义Uvicorn的日志配置。通过--log-config参数传入一个YAML文件在里面同时定义Uvicorn和你自己的logger。这个方案最规范但配置起来比较繁琐。方案三使用logging.getLogger(uvicorn)的父logger。把自定义logger挂到uvicorn.error下面这样Uvicorn的配置就不会影响到它。这个方案最取巧但不够优雅。我最终选了方案二因为项目要部署到生产环境日志格式需要统一。下面是我用的log_config.yamlversion: 1 disable_existing_loggers: false formatters: default: format: %(asctime)s - %(name)s - %(levelname)s - %(message)s handlers: console: class: logging.StreamHandler formatter: default stream: ext://sys.stdout loggers: uvicorn: handlers: [console] level: INFO propagate: false app: handlers: [console] level: DEBUG propagate: false root: handlers: [console] level: INFO关键就是**disable_existing_loggers: false**这一行。加上它之后我自定义的applogger就能正常输出了。提示如果你用的是uvicorn.run()启动可以直接传log_config参数如果用命令行就加--log-config log_config.yaml。5. 从单次对话到AI AgentLangChain和LangGraph的引入时机5.1 什么时候该上LangChain我一开始对LangChain是抗拒的。觉得它封装太厚出了问题不好排查。但当我需要做“根据用户问题自动选择工具”的时候手写路由逻辑变得越来越复杂。比如用户问“今天天气怎么样”我需要判断这是天气查询然后调用天气API用户问“帮我总结这份文档”我需要判断这是文档处理然后调用文件读取和摘要工具。这些判断逻辑如果全用if-else写代码会变得不可维护。LangChain的价值在于它提供了一套标准化的工具调用和链式编排接口。你定义一个ToolLangChain会自动处理参数解析和调用。你定义一个Chain它会把多个步骤串起来。虽然底层还是那些东西但代码组织上清晰很多。5.2 LangGraph当你的Agent需要“记住”上下文LangChain的Chain是线性的A步骤完了走B步骤。但实际场景中Agent往往需要根据中间结果决定下一步走哪里。比如用户问了一个问题Agent先检索知识库如果检索结果相关就直接回答如果不相关就调用搜索引擎。这种带分支和循环的逻辑用LangGraph表达更自然。LangGraph把Agent的状态定义成一个图节点是处理步骤边是跳转条件。你可以定义“如果检索置信度低于阈值就跳到搜索节点”这种逻辑用代码写就是几个if但用图来表达可读性和可维护性都好很多。我现在的做法是简单的单轮问答直接用FastAPI Ollama不引入LangChain需要多工具协作或者多轮推理的场景才上LangGraph。不要为了用框架而用框架这是我从运维转开发最大的体会。5.3 一个最小可用的Agent示例下面这个例子展示了如何用LangGraph定义一个“先检索再回答”的Agentfrom langgraph.graph import StateGraph, END from typing import TypedDict class AgentState(TypedDict): question: str context: str answer: str def retrieve(state: AgentState): # 模拟检索 return {context: 检索到的相关内容} def generate(state: AgentState): # 模拟生成 return {answer: f基于{state[context]}的回答} workflow StateGraph(AgentState) workflow.add_node(retrieve, retrieve) workflow.add_node(generate, generate) workflow.set_entry_point(retrieve) workflow.add_edge(retrieve, generate) workflow.add_edge(generate, END) app workflow.compile()这个例子很简单但结构清晰。你可以往里面加条件边、加循环、加人工审核节点。对于运维出身的人来说这种“状态机”的思维方式其实很熟悉和Prometheus的告警状态流转是一个道理。6. 企业私有化部署从“能跑”到“能用”的距离6.1 私有化部署的真实需求是什么很多人以为企业私有化部署就是把模型放到内网服务器上然后开个API就完事了。实际远不止这些。我接触过的需求里排在前面的几个是数据不能出内网、响应速度要稳定、并发要能扛住、模型要能换、日志要能审计。这几个需求叠加起来就不是一个ollama run能解决的了。数据不出内网意味着你不能调用任何外部API所有推理必须在本地完成。响应速度稳定意味着你不能让模型和别的服务抢资源最好有独立的GPU节点。并发要能扛住意味着你需要做请求队列和限流。模型要能换意味着你的架构不能和某个特定模型绑定。日志要能审计意味着每一次问答都要有记录包括谁问的、问了什么、模型答了什么。6.2 我的部署架构与踩坑记录我最终的部署架构是这样的一台GPU服务器跑Ollama一台CPU服务器跑FastAPI中间用内网HTTP通信。FastAPI层做鉴权、限流、日志记录Ollama层只负责推理。这样做的好处是GPU资源可以独立扩展FastAPI层可以水平扩容。踩过的坑有几个。第一个是GPU服务器的显存碎片化。Ollama长时间运行后显存会出现碎片导致新请求无法分配显存。解决方法是定期重启Ollama服务或者用ollama ps查看模型加载状态手动卸载不用的模型。第二个是FastAPI的请求队列。当并发请求超过Ollama的处理能力时请求会堆积。我一开始没做队列结果就是所有请求都超时。后来加了一个简单的信号量限流超过阈值的请求直接返回“服务繁忙”用户体验反而更好。第三个是日志审计的存储。每次问答都记日志一天下来就是几个GB。我一开始用文件存储后来发现查询太慢换成了SQLite。再后来数据量大了又换成了PostgreSQL。这个演进过程说明日志存储方案要跟着数据量走不要一开始就上重型方案。6.3 模型微调什么时候需要什么时候不需要关于大模型微调我的观点可能和很多人不一样大部分企业场景不需要微调用RAG就够了。微调的成本很高需要标注数据、需要GPU资源、需要反复调参。而且微调后的模型会“遗忘”一些通用能力在非特定任务上表现可能下降。RAG检索增强生成的思路是不改模型而是在提问的时候先从知识库里检索相关内容然后把检索结果和问题一起送给模型。这样模型就能基于最新的、私有的知识来回答。RAG的优点是灵活知识库更新了回答就更新了不需要重新训练。当然有些场景确实需要微调。比如你要让模型学会一种特定的输出格式或者你要让模型掌握某个垂直领域的专业术语。这种情况下微调是有效的。但我的建议是先试RAGRAG解决不了再考虑微调。7. 给运维转大模型开发者的几条实在建议7.1 Python工程化能力比算法知识更紧迫我见过很多运维同事Linux玩得很溜但写Python的时候还是脚本思维——所有逻辑堆在一个文件里没有类型注解没有异常处理没有单元测试。这种代码在个人项目里能跑但一旦要协作或者上生产就会出问题。我的建议是先把Python的工程化能力补上来。具体来说学会用pydantic做数据校验学会用pytest写测试学会用black和ruff做代码格式化学会用poetry或pip-tools管理依赖。这些东西不难但能让你写出来的代码从“能跑”变成“能用”。7.2 不要试图成为算法专家但要懂推理原理你不需要会推导Transformer的注意力公式但你需要知道上下文长度为什么影响显存、量化为什么影响质量、温度参数为什么影响输出的随机性。这些知识不需要很深但能帮你在选型和调参的时候做出正确判断。比如当业务方说“回答太死板了”你知道可以把温度调高一点。当业务方说“回答太发散了”你知道可以把温度调低。当业务方说“响应太慢了”你知道可以换更小的模型或者降低量化精度。这些判断不需要算法背景但需要你对推理过程有基本的理解。7.3 运维经验是你的差异化优势最后说一点不要觉得运维转开发是“降级”或者“从零开始”。你对系统稳定性的理解、对资源边界的敏感、对故障排查的直觉这些都是纯开发人员需要花时间积累的。在大模型工程化这个领域能把模型跑起来的人很多但能让模型稳定跑下去的人很少。后者需要的恰恰是运维的核心能力。我现在的日常工作大概30%在写业务代码30%在调优推理性能40%在处理各种工程化问题——部署、监控、日志、限流、容灾。后面这40%几乎全是运维的老本行。所以如果你正在考虑转型我的建议是不要丢掉你的运维底子把它当成你的核心竞争力然后用Python和FastAPI把大模型的能力包装成产品。这条路我走通了你也可以。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询