
1. 项目背景与问题起源上周我在尝试用FastAPI对接ollama大模型时遇到了asyncio的深坑。原本以为简单的接口调用结果在实现流式响应(StreamingResponse)时遭遇了各种异步编程的陷阱。这个案例特别适合分享给正在尝试将大模型能力集成到Web服务中的开发者们。ollama作为当前热门的本地大模型运行工具确实让开发者能够快速在本地环境运行各类开源模型。但当我试图将其与FastAPI这个异步Web框架结合时发现官方文档中的简单示例远远不能满足实际生产需求。特别是在处理长时间运行的模型推理请求时传统的同步调用方式会导致服务完全阻塞。2. 技术栈选型分析2.1 为什么选择FastAPIollama组合FastAPI的异步特性理论上非常适合大模型服务原生支持ASGI标准内置对WebSocket和SSE的支持自动生成的API文档出色的性能基准测试数据ollama的优势在于简化了本地大模型的部署流程提供统一的REST API接口支持模型的热加载和版本管理活跃的社区和持续更新2.2 关键依赖版本在实际开发中版本兼容性至关重要fastapi0.109.1 ollama0.1.27 httpx0.27.0 python-multipart0.0.63. 基础实现与首次翻车3.1 初始同步版本实现我最开始的实现是这样的from fastapi import FastAPI import ollama app FastAPI() app.post(/chat) def chat(prompt: str): response ollama.chat(modelllama3, messages[{role: user, content: prompt}]) return response[message][content]这个版本的问题立即显现每个请求都会阻塞事件循环无法处理并发请求响应时间不可控3.2 第一次异步改造意识到问题后我尝试了异步改造app.post(/chat) async def chat(prompt: str): response await ollama.chat(modelllama3, messages[{role: user, content: prompt}]) return response[message][content]结果发现ollama的Python客户端并不原生支持异步这就是第一个大坑。4. 深入异步编程解决方案4.1 使用httpx实现异步HTTP客户端解决方案是绕过官方客户端直接使用httpx调用ollama的HTTP接口import httpx async with httpx.AsyncClient(timeout60.0) as client: response await client.post( http://localhost:11434/api/chat, json{ model: llama3, messages: [{role: user, content: prompt}] } )4.2 处理流式响应真正的挑战在于实现流式输出。大模型的响应往往需要较长时间用户希望看到逐步输出的结果而不是等待全部生成完毕。from fastapi.responses import StreamingResponse app.post(/stream-chat) async def stream_chat(prompt: str): async with httpx.AsyncClient() as client: async with client.stream( POST, http://localhost:11434/api/chat, json{model: llama3, messages: [{role: user, content: prompt}]}, timeoutNone ) as response: async for chunk in response.aiter_bytes(): yield chunk5. 前端对话窗口实现5.1 基本HTML/JS实现配合后端流式接口前端实现也很关键div idchat-container div idchat-history/div input typetext iduser-input button onclicksendMessage()发送/button /div script async function sendMessage() { const input document.getElementById(user-input).value; const response await fetch(/stream-chat, { 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; } } /script5.2 优化用户体验实际使用中发现几个需要改进的点添加消息发送禁用状态实现打字机效果处理网络中断情况添加消息历史持久化6. 性能优化与错误处理6.1 连接池管理频繁创建HTTP连接会导致性能问题正确的做法是from contextlib import asynccontextmanager from fastapi import FastAPI import httpx client None asynccontextmanager async def lifespan(app: FastAPI): global client client httpx.AsyncClient(timeout60.0) yield await client.aclose() app FastAPI(lifespanlifespan)6.2 超时与重试机制大模型响应不可预测必须添加合理的超时和重试from tenacity import retry, stop_after_attempt, wait_exponential retry(stopstop_after_attempt(3), waitwait_exponential(multiplier1, min4, max10)) async def safe_chat_request(prompt: str): try: async with client.stream( POST, http://localhost:11434/api/chat, json{model: llama3, messages: [{role: user, content: prompt}]}, timeout30.0 ) as response: async for chunk in response.aiter_bytes(): yield chunk except httpx.ReadTimeout: yield b模型响应超时请重试或简化问题7. 部署与扩展考量7.1 生产环境配置实际部署时需要关注ollama服务的启动参数FastAPI的worker数量配置反向代理的超时设置日志和监控集成7.2 水平扩展方案当单机无法满足需求时可以考虑ollama多实例负载均衡模型并行计算请求队列管理结果缓存策略8. 完整代码示例8.1 后端完整实现from fastapi import FastAPI, Request from fastapi.responses import StreamingResponse from contextlib import asynccontextmanager import httpx from tenacity import retry, stop_after_attempt, wait_exponential import logging client None asynccontextmanager async def lifespan(app: FastAPI): global client client httpx.AsyncClient(timeout60.0) yield await client.aclose() app FastAPI(lifespanlifespan) retry(stopstop_after_attempt(3), waitwait_exponential(multiplier1, min4, max10)) async def generate_stream(prompt: str): try: async with client.stream( POST, http://localhost:11434/api/chat, json{ model: llama3, messages: [{role: user, content: prompt}], stream: True }, timeout30.0 ) as response: async for chunk in response.aiter_bytes(): yield chunk except Exception as e: logging.error(f请求失败: {str(e)}) yield b服务暂时不可用请稍后重试 app.post(/api/chat) async def chat_endpoint(request: Request): data await request.json() return StreamingResponse( generate_stream(data[prompt]), media_typeapplication/octet-stream )8.2 前端优化版本!DOCTYPE html html head titleOllama Chat/title style #chat-container { max-width: 800px; margin: 0 auto; } #chat-history { height: 500px; overflow-y: auto; border: 1px solid #ccc; padding: 10px; margin-bottom: 10px; white-space: pre-wrap; } #user-input { width: 80%; padding: 8px; } button { padding: 8px 15px; } .typing { color: #666; font-style: italic; } /style /head body div idchat-container div idchat-history/div input typetext iduser-input placeholder输入你的问题... button idsend-button onclicksendMessage()发送/button /div script let isProcessing false; const chatHistory document.getElementById(chat-history); const userInput document.getElementById(user-input); const sendButton document.getElementById(send-button); function updateUIState() { userInput.disabled isProcessing; sendButton.disabled isProcessing; sendButton.textContent isProcessing ? 处理中... : 发送; } function appendMessage(role, content) { const messageDiv document.createElement(div); messageDiv.innerHTML strong${role}:/strong ${content}; chatHistory.appendChild(messageDiv); chatHistory.scrollTop chatHistory.scrollHeight; } async function sendMessage() { if (isProcessing || !userInput.value.trim()) return; isProcessing true; updateUIState(); const prompt userInput.value; userInput.value ; appendMessage(你, prompt); const typingIndicator document.createElement(div); typingIndicator.className typing; typingIndicator.textContent 模型正在思考...; chatHistory.appendChild(typingIndicator); chatHistory.scrollTop chatHistory.scrollHeight; try { const response await fetch(/api/chat, { method: POST, headers: {Content-Type: application/json}, body: JSON.stringify({prompt: prompt}) }); chatHistory.removeChild(typingIndicator); const messageDiv document.createElement(div); messageDiv.innerHTML strongAI:/strong ; chatHistory.appendChild(messageDiv); const reader response.body.getReader(); while(true) { const {done, value} await reader.read(); if(done) break; const text new TextDecoder().decode(value); messageDiv.innerHTML text; chatHistory.scrollTop chatHistory.scrollHeight; } } catch (error) { chatHistory.removeChild(typingIndicator); appendMessage(系统, 请求失败: ${error.message}); } finally { isProcessing false; updateUIState(); } } userInput.addEventListener(keypress, (e) { if (e.key Enter) sendMessage(); }); /script /body /html9. 经验教训与最佳实践9.1 异步编程的注意事项避免阻塞操作任何同步IO操作都会破坏事件循环合理设置超时特别是对于大模型这种响应时间不确定的服务资源清理确保所有异步资源都正确关闭错误传播异步栈中的错误处理需要特别小心9.2 ollama集成技巧模型预热首次加载模型可能很慢可以预先发送简单请求内存管理ollama默认会保留最近使用的模型注意系统内存使用版本控制明确指定模型版本避免自动更新导致的不兼容本地缓存对于常见问题可以在应用层实现缓存机制10. 性能监控与调优10.1 关键指标监控请求响应时间分布错误率和重试次数模型推理时间系统资源使用率10.2 实用调试技巧# 在FastAPI中添加中间件记录请求时间 app.middleware(http) async def add_process_time_header(request: Request, call_next): start_time time.time() response await call_next(request) process_time time.time() - start_time response.headers[X-Process-Time] str(process_time) return response11. 安全考量输入验证防止Prompt注入攻击速率限制防止滥用导致服务不可用敏感信息过滤模型输出可能包含训练数据中的敏感信息认证授权至少实现基本的API密钥验证12. 扩展功能思路对话历史持久化使用Redis或数据库存储对话上下文多模态支持结合ollama的视觉模型能力插件系统允许动态加载不同的功能模块性能分析仪表盘实时监控模型使用情况13. 国内环境特别优化模型下载加速配置国内镜像源备用服务部署考虑在多个区域部署ollama实例离线包准备预先下载模型并打包分发网络连接优化调整TCP参数适应国内网络环境14. 常见问题解决方案ollama服务无响应检查服务是否正常运行ollama serve验证端口11434是否可访问查看日志中的错误信息流式响应中断检查客户端是否过早关闭连接增加网络超时设置实现断点续传机制模型加载失败确保磁盘空间充足验证模型文件完整性尝试重新拉取模型内存不足错误限制并发请求数量使用较小规模的模型增加交换空间或物理内存15. 进阶话题探索自定义模型集成如何加载自己训练的模型性能基准测试不同硬件配置下的表现对比混合部署方案结合云端和本地模型自动扩展策略基于负载的动态资源分配这个项目从最初的简单设想到最终稳定可用的服务经历了多次迭代和优化。最大的收获是深入理解了Python异步编程在实际项目中的应用要点以及如何平衡用户体验与系统性能。对于想要尝试类似项目的开发者我的建议是从最简单的版本开始逐步添加功能并在每个阶段进行充分的测试和性能评估。