Phi-3 Mini开源模型部署案例:Docker Compose多服务编排(含向量DB)

1. 引言

想象一下,你有一个功能强大的轻量级AI模型,它不仅逻辑严谨、响应迅速,还能记住长达一本书的对话内容。现在,你想把它变成一个既有实用价值,又有审美趣味的在线应用,同时还要给它配上“记忆”能力,让它能记住你们聊过的所有事情。

这听起来是不是有点复杂?需要部署模型服务、搭建Web界面、配置向量数据库,还要让它们之间能顺畅通信。如果手动一个个去配置,光是环境依赖和网络设置就能让人头疼半天。

今天,我们就来解决这个问题。我将带你用Docker Compose,像搭积木一样,轻松部署一个完整的Phi-3 Mini AI对话系统。这个系统不仅包含了模型推理服务、美观的Web界面,还集成了向量数据库,让AI能记住你们的每一次对话。

2. 项目概览:我们要搭建什么?

在开始动手之前,我们先看看最终要搭建的系统长什么样,由哪些部分组成。

2.1 系统架构

整个系统由三个核心服务组成,它们通过Docker Compose编排在一起:

  1. 模型服务:基于Phi-3 Mini 128K Instruct模型,负责AI推理和对话生成
  2. Web界面:基于Streamlit的治愈系森林主题界面,提供用户交互
  3. 向量数据库:基于ChromaDB,用于存储和检索对话历史

这三个服务的关系很简单:你在Web界面上输入问题,界面把问题发给模型服务,模型生成回答后返回给界面显示。同时,重要的对话内容会被存入向量数据库,下次你可以基于历史对话继续聊天。

2.2 为什么选择这个组合?

你可能会问,为什么选这些技术?我来简单解释一下:

  • Phi-3 Mini模型:只有38亿参数,但在逻辑推理、代码生成等方面表现优秀,支持12.8万token的超长上下文,而且推理速度快
  • Streamlit界面:用Python就能快速搭建Web应用,开发简单,界面美观
  • ChromaDB向量数据库:轻量级、易部署,专门为AI应用设计,适合存储文本的向量表示
  • Docker Compose:一键启动所有服务,不用操心环境配置和网络连接

这个组合既保证了功能完整,又控制了部署复杂度,特别适合个人开发者和小团队使用。

3. 环境准备与快速部署

好了,理论说完了,现在开始动手。我会带你一步步完成部署,从零开始到完全运行。

3.1 你需要准备什么?

在开始之前,请确保你的电脑上已经安装了以下工具:

  • Docker:版本20.10以上
  • Docker Compose:版本2.0以上
  • 至少16GB内存:模型运行需要一定内存
  • 支持CUDA的NVIDIA显卡(可选):如果有显卡,推理速度会快很多

如果你还没有安装Docker,可以去Docker官网下载安装包,按照指引安装就行。安装完成后,打开终端输入以下命令检查是否安装成功:

docker --version
docker-compose --version

如果能看到版本号,说明安装成功了。

3.2 获取项目文件

我们需要先下载项目所需的文件。创建一个新文件夹,比如叫做phi3-forest-lab,然后在这个文件夹里创建以下文件。

第一步:创建docker-compose.yml文件

这是最重要的文件,它定义了所有服务如何运行。创建一个名为docker-compose.yml的文件,内容如下:

version: '3.8'

services:
  # 模型服务
  phi3-model:
    image: ghcr.io/huggingface/text-generation-inference:latest
    container_name: phi3-model
    runtime: nvidia  # 如果有GPU的话
    deploy:
      resources:
        reservations:
          devices:
            - driver: nvidia
              count: all
              capabilities: [gpu]
    ports:
      - "8080:80"
    environment:
      - MODEL_ID=microsoft/Phi-3-mini-128k-instruct
      - QUANTIZE=bitsandbytes-nf4
      - MAX_BATCH_PREFILL_TOKENS=32768
      - MAX_INPUT_LENGTH=131072
      - MAX_TOTAL_TOKENS=131072
    volumes:
      - ./models:/data
    command: --model-id ${MODEL_ID} --quantize ${QUANTIZE} --max-batch-prefill-tokens ${MAX_BATCH_PREFILL_TOKENS} --max-input-length ${MAX_INPUT_LENGTH} --max-total-tokens ${MAX_TOTAL_TOKENS}
    networks:
      - phi3-network

  # 向量数据库
  chromadb:
    image: chromadb/chroma:latest
    container_name: chromadb
    ports:
      - "8000:8000"
    environment:
      - IS_PERSISTENT=TRUE
      - PERSIST_DIRECTORY=/chroma/chroma
    volumes:
      - ./chroma_data:/chroma/chroma
    networks:
      - phi3-network

  # Web界面
  web-ui:
    build: .
    container_name: phi3-web-ui
    ports:
      - "7860:7860"
    environment:
      - MODEL_API_URL=http://phi3-model:80
      - CHROMA_API_URL=http://chromadb:8000
    depends_on:
      - phi3-model
      - chromadb
    networks:
      - phi3-network

networks:
  phi3-network:
    driver: bridge

第二步:创建Dockerfile

接下来创建Web界面的Dockerfile。创建一个名为Dockerfile的文件,内容如下:

FROM python:3.9-slim

WORKDIR /app

# 安装系统依赖
RUN apt-get update && apt-get install -y \
    gcc \
    g++ \
    && rm -rf /var/lib/apt/lists/*

# 复制依赖文件
COPY requirements.txt .

# 安装Python依赖
RUN pip install --no-cache-dir -r requirements.txt

# 复制应用代码
COPY app.py .
COPY utils.py .
COPY assets/ ./assets/

# 暴露端口
EXPOSE 7860

# 启动应用
CMD ["streamlit", "run", "app.py", "--server.port=7860", "--server.address=0.0.0.0"]

第三步:创建requirements.txt

这是Python依赖文件,创建一个名为requirements.txt的文件:

streamlit>=1.28.0
requests>=2.31.0
chromadb>=0.4.15
sentence-transformers>=2.2.2
pydantic>=2.0.0
python-dotenv>=1.0.0

第四步:创建应用代码文件

现在创建主要的应用文件。先创建app.py

import streamlit as st
import requests
import json
from typing import List, Dict
import chromadb
from sentence_transformers import SentenceTransformer
import time

# 页面配置
st.set_page_config(
    page_title="Phi-3 Forest Laboratory",
    page_icon="🌿",
    layout="wide",
    initial_sidebar_state="expanded"
)

# 自定义CSS样式
def load_css():
    st.markdown("""
    <style>
    /* 主背景 - 森林渐变 */
    .stApp {
        background: linear-gradient(135deg, #f5f7fa 0%, #e4efe9 100%);
    }
    
    /* 聊天气泡样式 */
    .stChatMessage {
        border-radius: 20px !important;
        padding: 20px !important;
        margin: 10px 0 !important;
        box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05) !important;
    }
    
    .stChatMessage[data-testid="user"] {
        background-color: #e8f5e9 !important;
        border-left: 5px solid #4caf50 !important;
    }
    
    .stChatMessage[data-testid="assistant"] {
        background-color: #ffffff !important;
        border-left: 5px solid #81c784 !important;
    }
    
    /* 输入框样式 */
    .stTextInput > div > div > input {
        border-radius: 10px !important;
        border: 2px solid #81c784 !important;
    }
    
    /* 按钮样式 */
    .stButton > button {
        border-radius: 10px !important;
        background: linear-gradient(135deg, #81c784 0%, #4caf50 100%) !important;
        color: white !important;
        border: none !important;
        padding: 10px 24px !important;
    }
    
    /* 侧边栏样式 */
    .css-1d391kg {
        background: linear-gradient(180deg, #f8fff9 0%, #e8f5e9 100%) !important;
    }
    </style>
    """, unsafe_allow_html=True)

# 初始化会话状态
def init_session_state():
    if "messages" not in st.session_state:
        st.session_state.messages = []
    if "chroma_client" not in st.session_state:
        st.session_state.chroma_client = None
    if "embedding_model" not in st.session_state:
        st.session_state.embedding_model = None
    if "collection" not in st.session_state:
        st.session_state.collection = None

# 初始化向量数据库
def init_chromadb():
    try:
        chroma_client = chromadb.HttpClient(
            host="chromadb",
            port=8000
        )
        
        # 创建或获取集合
        collection = chroma_client.get_or_create_collection(
            name="phi3_conversations",
            metadata={"description": "Phi-3对话历史存储"}
        )
        
        # 加载嵌入模型
        embedding_model = SentenceTransformer('all-MiniLM-L6-v2')
        
        st.session_state.chroma_client = chroma_client
        st.session_state.embedding_model = embedding_model
        st.session_state.collection = collection
        
        return True
    except Exception as e:
        st.error(f"初始化向量数据库失败: {str(e)}")
        return False

# 调用模型API
def call_phi3_api(prompt: str, temperature: float = 0.7) -> str:
    try:
        url = "http://phi3-model:80/generate"
        
        payload = {
            "inputs": prompt,
            "parameters": {
                "temperature": temperature,
                "max_new_tokens": 1024,
                "do_sample": True,
                "top_p": 0.95,
                "repetition_penalty": 1.1
            }
        }
        
        response = requests.post(url, json=payload, timeout=300)
        
        if response.status_code == 200:
            result = response.json()
            return result[0]["generated_text"]
        else:
            return f"请求失败: {response.status_code}"
            
    except Exception as e:
        return f"调用API时出错: {str(e)}"

# 保存对话到向量数据库
def save_to_chromadb(user_input: str, assistant_response: str):
    if not st.session_state.collection or not st.session_state.embedding_model:
        return
    
    try:
        # 生成嵌入向量
        text_to_embed = f"用户: {user_input}\n助手: {assistant_response}"
        embedding = st.session_state.embedding_model.encode(text_to_embed).tolist()
        
        # 准备元数据
        metadata = {
            "user_input": user_input,
            "assistant_response": assistant_response,
            "timestamp": time.time()
        }
        
        # 生成唯一ID
        doc_id = f"conv_{int(time.time())}_{hash(user_input) % 10000}"
        
        # 添加到集合
        st.session_state.collection.add(
            embeddings=[embedding],
            documents=[text_to_embed],
            metadatas=[metadata],
            ids=[doc_id]
        )
        
    except Exception as e:
        st.warning(f"保存对话历史时出错: {str(e)}")

# 从向量数据库检索相关历史
def search_chromadb(query: str, n_results: int = 3) -> List[Dict]:
    if not st.session_state.collection or not st.session_state.embedding_model:
        return []
    
    try:
        # 生成查询向量
        query_embedding = st.session_state.embedding_model.encode(query).tolist()
        
        # 搜索相似对话
        results = st.session_state.collection.query(
            query_embeddings=[query_embedding],
            n_results=n_results
        )
        
        # 格式化结果
        formatted_results = []
        if results["documents"]:
            for i in range(len(results["documents"][0])):
                formatted_results.append({
                    "content": results["documents"][0][i],
                    "metadata": results["metadatas"][0][i],
                    "distance": results["distances"][0][i]
                })
        
        return formatted_results
        
    except Exception as e:
        st.warning(f"检索历史对话时出错: {str(e)}")
        return []

# 主应用
def main():
    # 加载CSS
    load_css()
    
    # 初始化会话状态
    init_session_state()
    
    # 侧边栏
    with st.sidebar:
        st.title("🌿 森林控制台")
        
        # 温度调节
        temperature = st.slider(
            "🌡️ 创造力温度",
            min_value=0.1,
            max_value=1.5,
            value=0.7,
            step=0.1,
            help="值越低回答越严谨,值越高越有创意"
        )
        
        # 历史记录开关
        use_history = st.checkbox("📚 启用对话记忆", value=True)
        
        # 重置按钮
        if st.button("🍂 拂去往事", use_container_width=True):
            st.session_state.messages = []
            st.rerun()
        
        st.divider()
        
        # 系统状态
        st.subheader("系统状态")
        
        # 初始化向量数据库
        if st.session_state.chroma_client is None:
            if st.button("初始化记忆库", use_container_width=True):
                with st.spinner("正在连接记忆库..."):
                    if init_chromadb():
                        st.success("记忆库已连接")
                        st.rerun()
        else:
            st.success("✅ 记忆库已连接")
            
            # 显示历史统计
            if st.session_state.collection:
                count = st.session_state.collection.count()
                st.info(f"已存储对话: {count} 条")
    
    # 主界面
    st.title("🌿 Phi-3 Forest Laboratory")
    st.caption("在森林的深处,听见智慧的呼吸。")
    
    # 显示聊天历史
    for message in st.session_state.messages:
        with st.chat_message(message["role"]):
            st.markdown(message["content"])
    
    # 聊天输入
    if prompt := st.chat_input("向森林深处发出讯息..."):
        # 添加用户消息
        st.session_state.messages.append({"role": "user", "content": prompt})
        with st.chat_message("user"):
            st.markdown(prompt)
        
        # 准备上下文
        context = ""
        if use_history and st.session_state.collection:
            # 检索相关历史对话
            historical_contexts = search_chromadb(prompt)
            if historical_contexts:
                context = "\n\n相关历史对话:\n"
                for i, hist in enumerate(historical_contexts[:2], 1):
                    context += f"{i}. {hist['content'][:200]}...\n"
        
        # 构建完整提示
        full_prompt = f"{context}\n\n当前对话:\n用户: {prompt}\n助手:"
        
        # 生成助手回复
        with st.chat_message("assistant"):
            with st.spinner("🌲 森林正在思考..."):
                response = call_phi3_api(full_prompt, temperature)
                
                # 流式显示回复
                message_placeholder = st.empty()
                full_response = ""
                
                # 模拟流式输出
                for chunk in response.split():
                    full_response += chunk + " "
                    message_placeholder.markdown(full_response + "▌")
                    time.sleep(0.05)
                
                message_placeholder.markdown(full_response)
        
        # 保存到会话状态
        st.session_state.messages.append({"role": "assistant", "content": response})
        
        # 保存到向量数据库
        if use_history:
            save_to_chromadb(prompt, response)

if __name__ == "__main__":
    main()

第五步:创建工具函数文件

再创建一个utils.py文件,用于存放一些工具函数:

import hashlib
from datetime import datetime
from typing import Optional

def generate_conversation_id(user_input: str, timestamp: Optional[float] = None) -> str:
    """生成对话ID"""
    if timestamp is None:
        timestamp = datetime.now().timestamp()
    
    # 使用用户输入和时间戳生成唯一ID
    input_hash = hashlib.md5(user_input.encode()).hexdigest()[:8]
    return f"conv_{int(timestamp)}_{input_hash}"

def format_timestamp(timestamp: float) -> str:
    """格式化时间戳"""
    return datetime.fromtimestamp(timestamp).strftime("%Y-%m-%d %H:%M:%S")

def truncate_text(text: str, max_length: int = 200) -> str:
    """截断文本"""
    if len(text) <= max_length:
        return text
    return text[:max_length] + "..."

def validate_model_response(response: str) -> bool:
    """验证模型响应是否有效"""
    if not response or response.strip() == "":
        return False
    
    # 检查是否包含错误信息
    error_keywords = ["错误", "失败", "出错", "error", "failed", "exception"]
    for keyword in error_keywords:
        if keyword.lower() in response.lower():
            return False
    
    return True

第六步:创建assets文件夹

最后创建一个assets文件夹,里面可以放一些静态资源,比如图片、图标等。这里我们先创建一个空文件夹:

mkdir assets

3.3 一键启动所有服务

现在所有文件都准备好了,目录结构应该是这样的:

phi3-forest-lab/
├── docker-compose.yml
├── Dockerfile
├── requirements.txt
├── app.py
├── utils.py
└── assets/

打开终端,进入这个文件夹,然后运行一个简单的命令:

docker-compose up -d

这个命令会做以下几件事:

  1. 下载Phi-3 Mini模型(第一次运行需要下载,大概7GB)
  2. 启动ChromaDB向量数据库
  3. 构建并启动Web界面
  4. 把所有服务连接在一起

第一次运行可能需要10-20分钟,主要时间花在下载模型上。你可以用下面的命令查看运行状态:

docker-compose logs -f

看到所有服务都正常启动后,打开浏览器,访问 http://localhost:7860,就能看到你的Phi-3 Forest Laboratory了!

4. 使用你的AI对话系统

现在系统已经运行起来了,我来带你看看怎么使用它。

4.1 第一次对话

打开浏览器,访问 http://localhost:7860,你会看到一个清新简洁的界面。在底部的输入框里,试着问一些问题:

  • "你好,介绍一下你自己"
  • "Python里怎么快速排序一个列表?"
  • "帮我写一个简单的网页登录界面"

你会看到AI的回复以流式的方式显示出来,就像真的在思考一样。

4.2 调节创造力

在左侧的侧边栏,你可以看到一个叫"创造力温度"的滑块。这个值控制着AI回答的创造性:

  • 低温度(0.1-0.5):回答更加严谨、准确,适合技术问题、代码生成
  • 中等温度(0.6-0.9):平衡创造性和准确性,适合一般对话
  • 高温度(1.0-1.5):回答更加有创意、多样化,适合写故事、诗歌

你可以根据不同的需求调整这个值。

4.3 使用对话记忆

系统默认启用了"对话记忆"功能。这意味着你的对话会被保存到向量数据库里。当你问类似的问题时,AI可以参考之前的对话历史,给出更连贯的回答。

比如你可以先问:"Python里列表和元组有什么区别?" 然后问:"那我应该什么时候用列表,什么时候用元组呢?"

第二个问题AI会参考第一个问题的回答,给出更贴切的建议。

4.4 查看和管理历史

在侧边栏,你可以看到"已存储对话"的数量。如果你想清空所有历史,点击"🍂 拂去往事"按钮,所有的对话记忆都会被清除。

5. 实际应用场景

这个系统不只是个玩具,它在很多实际场景中都能发挥作用。

5.1 个人学习助手

你可以用它来学习编程、准备考试、或者了解新知识。比如:

  • "解释一下机器学习中的梯度下降算法"
  • "帮我制定一个学习Python的30天计划"
  • "用简单的例子说明什么是递归函数"

Phi-3 Mini的逻辑推理能力很强,能给出清晰、准确的解释。

5.2 代码编写和调试

作为开发者,你可以用它来:

  • "写一个Flask REST API的示例"
  • "帮我调试这段Python代码,为什么报错?"
  • "用React写一个TODO列表组件"

模型支持128K的上下文,意味着你可以把大段的代码贴给它看,它会帮你分析问题。

5.3 创意写作和头脑风暴

调高创造力温度,它可以帮你:

  • "写一个关于人工智能的短篇科幻故事开头"
  • "为我的咖啡店想10个有创意的名字"
  • "帮我写一封给客户的道歉邮件"

5.4 长期项目协作

因为有了向量数据库的记忆功能,你可以:

  • 长期在同一个项目上和AI协作
  • AI会记住你们之前讨论过的需求、设计决策
  • 每次对话都基于完整的历史上下文

这对于软件开发、论文写作、研究项目特别有用。

6. 常见问题与解决

在使用的过程中,你可能会遇到一些问题。这里我整理了一些常见问题和解决方法。

6.1 模型下载太慢怎么办?

第一次运行需要下载7GB左右的模型文件。如果下载慢,你可以:

  1. 使用镜像加速:修改docker-compose.yml中的模型地址为国内镜像
  2. 手动下载:先到HuggingFace下载模型,放到./models文件夹
  3. 使用已经下载好的模型:如果你有其他地方下载的模型,可以直接使用

6.2 内存不够怎么办?

Phi-3 Mini需要一定的内存来运行:

  • 纯CPU运行:需要至少8GB空闲内存
  • GPU运行:需要至少4GB显存(如RTX 3060以上)

如果内存不足,可以尝试:

# 在docker-compose.yml中调整资源限制
phi3-model:
  deploy:
    resources:
      limits:
        memory: 8G

6.3 如何备份对话历史?

所有的对话都保存在./chroma_data文件夹里。你可以定期备份这个文件夹:

# 备份
tar -czf chroma_backup_$(date +%Y%m%d).tar.gz chroma_data/

# 恢复
tar -xzf chroma_backup_20240315.tar.gz

6.4 如何更新到新版本?

如果你想更新模型或者界面:

# 停止服务
docker-compose down

# 拉取最新镜像
docker-compose pull

# 重新启动
docker-compose up -d

6.5 性能优化建议

如果觉得响应速度不够快,可以尝试:

  1. 使用GPU:确保你的显卡驱动和Docker GPU支持已正确安装
  2. 调整参数:减少max_new_tokens的值,生成更短的回复
  3. 使用量化:我们已经使用了4-bit量化,这是内存和速度的很好平衡

7. 进阶配置与定制

如果你对这个基础版本满意了,想要更多功能,这里有一些进阶的定制方法。

7.1 更换模型

如果你想试试其他模型,只需要修改docker-compose.yml中的一行:

environment:
  - MODEL_ID=microsoft/Phi-3-mini-128k-instruct  # 改成其他模型

支持的模型包括:

  • microsoft/Phi-3-mini-4k-instruct:4K上下文版本,更快
  • microsoft/Phi-3-small-128k-instruct:小尺寸版本
  • microsoft/Phi-3-medium-128k-instruct:中等尺寸,能力更强

7.2 添加身份验证

如果你想给Web界面加上密码保护,修改app.py:

# 在main函数开头添加
def check_password():
    if 'authenticated' not in st.session_state:
        st.session_state.authenticated = False
    
    if not st.session_state.authenticated:
        password = st.text_input("请输入访问密码", type="password")
        if password == "你的密码":  # 设置你的密码
            st.session_state.authenticated = True
            st.rerun()
        elif password:
            st.error("密码错误")
        return False
    return True

# 然后在main()开头调用
if not check_password():
    st.stop()

7.3 集成其他工具

你还可以集成其他AI工具,比如:

  1. 联网搜索:添加搜索功能,让AI能获取最新信息
  2. 文件上传:让用户上传文档,AI基于文档内容回答
  3. 多模态支持:添加图片理解功能

这些都需要对代码进行一些扩展,但基本思路是一样的:添加新的服务,然后在Web界面中集成。

7.4 部署到服务器

如果你想在云服务器上部署:

  1. 选择服务器:建议至少4核CPU、16GB内存、50GB硬盘
  2. 安装Docker:按照服务器系统的文档安装
  3. 上传文件:把整个项目文件夹上传到服务器
  4. 修改配置:调整docker-compose.yml中的端口映射
  5. 启动服务:同样运行docker-compose up -d

记得配置防火墙,开放7860端口(Web界面)和必要的其他端口。

8. 总结

通过这个教程,我们完成了一个完整的AI对话系统的部署。让我简单总结一下我们都做了什么:

我们搭建的系统有这些特点:

  • 基于强大的Phi-3 Mini模型,逻辑推理能力强,响应速度快
  • 拥有美观的森林主题界面,使用体验好
  • 集成了向量数据库,能记住对话历史
  • 用Docker Compose一键部署,简单方便

你学到的关键技能:

  1. 如何使用Docker Compose编排多个服务
  2. 如何部署大语言模型服务
  3. 如何集成向量数据库
  4. 如何用Streamlit快速搭建Web界面
  5. 如何让这些服务协同工作

这个项目的价值:

  • 对于学习者:一个随时可用的AI学习伙伴
  • 对于开发者:一个可扩展的AI应用基础框架
  • 对于研究者:一个方便的模型测试和演示平台

最重要的是,你现在有了一个完全在自己控制下的AI系统。你可以随意修改它、扩展它、用它来解决实际问题。所有的代码都在你手里,所有的数据都在你的服务器上,不用担心隐私问题,也不用担心服务突然不可用。

AI技术正在快速发展,但很多时候我们觉得它离我们很远。通过这个项目,我希望你能感受到:搭建自己的AI应用并没有那么难。有了Docker这样的工具,有了开源模型,每个人都可以成为AI技术的使用者和创造者。


获取更多AI镜像

想探索更多AI镜像和应用场景?访问 CSDN星图镜像广场,提供丰富的预置镜像,覆盖大模型推理、图像生成、视频生成、模型微调等多个领域,支持一键部署。

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐