1. 项目背景与核心挑战
去年我在做一个智能客服系统的原型开发时,遇到了一个看似简单但实际棘手的问题:如何用FastAPI高效对接本地部署的Ollama大模型。最初我以为这不过是写个HTTP客户端的事,但实际开发中却被Python的异步机制结结实实上了一课。
这个项目的核心需求是通过Web界面与本地运行的Ollama模型进行对话交互。听起来很直接对吧?但当你真正开始处理以下场景时,问题就来了:
- 大模型生成响应通常需要数秒甚至更长时间
- 需要实时将生成的token流式传输到前端
- 前端需要保持连接等待完整响应
- 服务端不能因为长时间请求而阻塞其他客户端
我最初用同步方式实现的版本,在并发请求时直接导致服务不可用。后来改用asyncio时,又遇到了各种await使用不当导致的"协程未等待"警告。最崩溃的是,当我想用httpx.AsyncClient调用Ollama的API时,发现文档里那些简单的示例在实际生产场景中根本不够用。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 技术栈选型与基础配置
2.1 为什么选择FastAPI+Ollama这个组合?
FastAPI的异步特性理论上非常适合这种IO密集型的模型服务场景。它内置的Starlette服务器支持ASGI标准,可以很好地处理长时间运行的连接。而Ollama作为本地大模型运行工具,提供了简洁的REST API接口,特别适合快速原型开发。
安装基础环境:
bash复制# 创建虚拟环境
python -m venv ollama_env
source ollama_env/bin/activate # Linux/Mac
ollama_env\Scripts\activate # Windows
# 安装核心依赖
pip install fastapi uvicorn httpx jinja2
2.2 Ollama的安装与模型准备
Ollama的安装过程看似简单,但在国内环境有几个坑需要注意:
bash复制# 官方安装方式(可能很慢)
curl -fsSL https://ollama.com/install.sh | sh
# 国内用户建议使用镜像源
export OLLAMA_HOST=镜像服务器地址
安装完成后,下载模型(以llama2为例):
bash复制ollama pull llama2
注意:模型下载可能非常耗时,建议使用国内镜像源或预先下载好的模型文件。如果遇到"network problem"错误,可以尝试设置HTTP代理或更换下载源。
3. 异步编程的深坑与解决方案
3.1 同步调用的问题重现
我最开始写的同步版本代码是这样的:
python复制from fastapi import FastAPI
import requests
app = FastAPI()
@app.post("/chat")
def chat(prompt: str):
response = requests.post(
"http://localhost:11434/api/generate",
json={"model": "llama2", "prompt": prompt}
)
return response.json()
这个版本在单个请求时工作正常,但当多个用户同时访问时:
- 每个请求都会阻塞工作线程
- 模型生成响应期间无法处理其他请求
- 最终导致服务完全卡死
3.2 初探asyncio的陷阱
改成异步版本时,我犯的第一个错误是忽略了HTTP客户端的异步特性:
python复制@app.post("/chat")
async def chat(prompt: str):
# 错误!requests是同步库
response = requests.post(...)
return response.json()
这会导致事件循环被阻塞,失去了异步的优势。正确的做法是使用异步HTTP客户端,如httpx:
python复制import httpx
@app.post("/chat")
async def chat(prompt: str):
async with httpx.AsyncClient() as client:
response = await client.post(
"http://localhost:11434/api/generate",
json={"model": "llama2", "prompt": prompt}
)
return response.json()
3.3 流式响应的实现难题
真正的挑战在于实现流式响应。Ollama的API支持流式返回生成的token,但正确处理这些数据流需要:
- 保持客户端连接不中断
- 实时将token发送到前端
- 正确处理连接中断的情况
最终解决方案:
python复制from fastapi.responses import StreamingResponse
async def generate_stream(prompt: str):
async with httpx.AsyncClient(timeout=60.0) as client:
async with client.stream(
"POST",
"http://localhost:11434/api/generate",
json={"model": "llama2", "prompt": prompt, "stream": True}
) as response:
async for chunk in response.aiter_bytes():
yield chunk
@app.post("/chat_stream")
async def chat_stream(prompt: str):
return StreamingResponse(
generate_stream(prompt),
media_type="application/json"
)
4. 完整对话系统实现
4.1 前端界面搭建
使用Jinja2模板构建简单对话界面:
python复制from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
app.mount("/static", StaticFiles(directory="static"), name="static")
templates = Jinja2Templates(directory="templates")
@app.get("/")
async def chat_page(request: Request):
return templates.TemplateResponse("chat.html", {"request": request})
chat.html关键部分:
html复制<div id="chat-container">
<div id="chat-history"></div>
<input id="user-input" type="text">
<button onclick="sendMessage()">发送</button>
</div>
<script>
async function sendMessage() {
const input = document.getElementById('user-input').value;
const response = await fetch('/chat_stream', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({prompt: input})
});
const reader = response.body.getReader();
while(true) {
const {done, value} = await reader.read();
if(done) break;
const text = new TextDecoder().decode(value);
document.getElementById('chat-history').innerHTML += text;
}
}
</script>
4.2 性能优化技巧
- 连接池管理:重用AsyncClient实例
python复制async def get_client():
if not hasattr(get_client, "client"):
get_client.client = httpx.AsyncClient(timeout=60.0)
return get_client.client
- 超时设置:避免僵尸连接
python复制@app.on_event("shutdown")
async def shutdown_event():
await get_client().aclose()
- 并发控制:限制同时处理的请求数
python复制from asyncio import Semaphore
concurrency_limit = Semaphore(5)
@app.post("/chat")
async def chat(prompt: str):
async with concurrency_limit:
# 处理逻辑
5. 生产环境部署建议
5.1 配置优化
在config.py中添加关键配置:
python复制import os
class Config:
OLLAMA_HOST = os.getenv("OLLAMA_HOST", "http://localhost:11434")
MODEL_NAME = os.getenv("MODEL_NAME", "llama2")
MAX_CONCURRENT_REQUESTS = int(os.getenv("MAX_CONCURRENT_REQUESTS", 5))
5.2 使用Gunicorn运行
对于生产环境,建议使用Gunicorn+Uvicorn:
bash复制gunicorn -w 4 -k uvicorn.workers.UvicornWorker main:app
5.3 监控与日志
添加Prometheus监控:
python复制from prometheus_fastapi_instrumentator import Instrumentator
Instrumentator().instrument(app).expose(app)
6. 踩坑经验总结
-
异步上下文管理器:一定要用
async with而不是普通的with,否则会导致资源无法正确释放 -
流式响应中断:前端断开连接时,后端可能仍在处理,需要添加中断检测:
python复制async def generate_stream(prompt: str):
try:
async with httpx.AsyncClient() as client:
# ...流处理逻辑
except asyncio.CancelledError:
print("客户端断开连接")
raise
- 模型加载时间:首次请求可能需要等待模型加载,建议预热:
python复制@app.on_event("startup")
async def startup_event():
async with httpx.AsyncClient() as client:
await client.post(f"{Config.OLLAMA_HOST}/api/generate", json={"model": Config.MODEL_NAME, "prompt": ""})
- 内存泄漏排查:长时间运行后,发现内存持续增长。解决方案是定期重启工作进程,并检查AsyncClient实例是否正确关闭。
这个项目让我深刻理解了Python异步编程的复杂性,也让我认识到文档示例与实际生产需求之间的差距。现在回头看,那些让我崩溃的问题其实都是宝贵的经验积累。如果你也在尝试类似的技术栈,希望我的这些踩坑记录能帮你少走弯路。
