去年双十一,我负责的电商平台 AI 客服系统遭遇了前所未有的挑战。那天凌晨0点,促销正式开始,涌入的并发请求直接打垮了基于闭源 API 的旧架构——响应延迟飙升至 8 秒,用户怨声载道,客诉率攀升了 340%。痛定思痛,我决定用开源大模型 Llama 4 + 向量数据库 Milvus 重新构建 RAG 知识库系统。今天这篇文章,就是我从零到一的完整实战记录,包含踩过的坑和调优经验。

为什么选择 Llama 4 + Milvus 的技术组合

在做技术选型时,我对比了市面上的主流方案。闭源模型虽然效果稳定,但成本是一道绕不过去的坎。以 GPT-4.1 为例,每百万 token 输出费用高达 $8,大促期间每天数百万次调用根本吃不消。而 HolySheheep API 提供的 DeepSeek V3.2 模型,每百万 token 仅需 $0.42,价格相差近 20 倍,而且国内直连延迟低于 50ms,非常适合高并发场景。

Llama 4 的优势在于可以在本地或私有云部署,完全掌控数据流向,安全性和成本都可控。配合 Milvus 这款专为向量检索优化的数据库,实测单节点 QPS 可达 5000+,完全能满足我们电商场景的需求。

系统架构设计

整体架构分为三层:数据层(Milvus + PostgreSQL)、推理层(Llama 4 + HolySheheep API 兜底)、应用层(FastAPI 网关 + Redis 缓存)。当 Llama 4 本地服务负载过高时,自动降级到 HolySheheep 的 DeepSeek V3.2 模型,确保服务质量不降级。

环境准备与依赖安装

首先是搭建开发环境,推荐使用 Python 3.10+。我建议用 conda 创建独立环境,避免包冲突:

# 创建并激活 conda 环境
conda create -n rag_system python=3.10
conda activate rag_system

安装核心依赖

pip install pymilvus==2.4.0 pip install sentence-transformers==2.5.1 pip install openai==1.12.0 pip install langchain==0.1.4 pip install fastapi==0.109.0 pip install uvicorn==0.27.0 pip install redis==5.0.1 pip install pydantic==2.5.3

安装文本处理工具

pip install tiktoken==0.5.2 pip install bs4==0.0.1

服务器配置方面,我的测试环境是 32GB 内存 + RTX 4090 显卡(24GB 显存),实测可以流畅运行 7B 参数的 Llama 4 量化版本。如果预算有限,14B 参数模型建议使用 Q4 量化,内存占用可以压缩到 14GB 左右。

Milvus 向量数据库配置与初始化

Milvus 的安装有三种方式:Docker Compose 单机部署、分布式集群、K8s 部署。对于我们这种日均百万级查询的中等规模系统,单机 Docker 部署完全够用。我推荐使用 milvus-lite 这个轻量版本,开发测试更方便:

# Milvus lite 安装(开发环境推荐)
pip install milvus-lite==0.5.4

生产环境使用 Docker 部署

docker-compose.yml 配置

version: '3.8' services: etcd: container_name: milvus-etcd image: quay.io/coreos/etcd:v3.5.5 environment: - ETCD_AUTO_COMPACTION_MODE=revision - ETCD_AUTO_COMPACTION_RETENTION=1000 - ETCD_QUOTA_BACKEND_BYTES=4294967296 volumes: - ./etcd:/etcd command: etcd -advertise-client-urls=http://127.0.0.1:2379 -listen-client-urls http://0.0.0.0:2379 --data-dir /etcd minio: container_name: milvus-minio image: minio/minio:RELEASE.2023-03-20T20-16-18Z environment: MINIO_ACCESS_KEY: minioadmin MINIO_SECRET_KEY: minioadmin volumes: - ./minio:/minio command: minio server /minio --console-address ":9001" milvus: container_name: milvus-standalone image: milvusdb/milvus:v2.3.3 command: ["milvus", "run", "standalone"] environment: ETCD_ENDPOINTS: milvus-etcd:2379 MINIO_ADDRESS: minio:9000 volumes: - ./milvus_config.yaml:/milvus/configs/milvus.yaml - ./milvus:/var/lib/milvus ports: - "19530:19530" - "9091:9091"

初始化 Milvus 连接和创建 Collection 的代码如下:

import numpy as np
from pymilvus import connections, Collection, CollectionSchema, FieldSchema, DataType, utility

class MilvusVectorStore:
    def __init__(self, host="localhost", port="19530"):
        # 连接 Milvus 服务
        connections.connect(
            alias="default",
            host=host,
            port=port,
            timeout=30
        )
        self.collection_name = "product_knowledge_base"
        
    def create_collection(self, dimension=1024):
        """创建 Collection,Llama 4 的 embedding 维度是 1024"""
        # 删除已存在的同名 Collection
        if utility.has_collection(self.collection_name):
            utility.drop_collection(self.collection_name)
        
        # 定义字段
        fields = [
            FieldSchema(name="id", dtype=DataType.INT64, is_primary=True, auto_id=True),
            FieldSchema(name="content", dtype=DataType.VARCHAR, max_length=65535),
            FieldSchema(name="embedding", dtype=DataType.FLOAT_VECTOR, dim=dimension),
            FieldSchema(name="metadata", dtype=DataType.JSON)
        ]
        
        # 创建 Schema
        schema = CollectionSchema(
            fields=fields,
            description="电商产品知识库向量存储"
        )
        
        # 创建 Collection
        collection = Collection(name=self.collection_name, schema=schema)
        
        # 创建索引(使用 IVF_FLAT 算法,适合中等规模数据)
        index_params = {
            "index_type": "IVF_FLAT",
            "metric_type": "L2",  # L2 距离度量,适合文本相似度
            "params": {"nlist": 128}
        }
        collection.create_index(
            field_name="embedding",
            index_params=index_params
        )
        
        collection.load()
        print(f"✓ Collection '{self.collection_name}' 创建成功")
        return collection

初始化

vector_store = MilvusVectorStore() collection = vector_store.create_collection(dimension=1024)

数据预处理与向量化入库

电商知识库的数据来源很杂,有产品说明书、用户 FAQ、客服对话记录等。我用 LangChain 的 Document Loader 来统一处理:

from langchain.document_loaders import PyPDFLoader, WebBaseLoader, DirectoryLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.embeddings import HuggingFaceBgeEmbeddings
from pymilvus import Collection, Field, CollectionSchema, DataType
import os

class KnowledgeBaseIngestion:
    def __init__(self, milvus_collection):
        self.collection = milvus_collection
        # 初始化 BGE embeddings 模型(与 Llama 4 兼容性好)
        self.embeddings = HuggingFaceBgeEmbeddings(
            model_name="BAAI/bge-large-zh-v1.5",  # 中文优化模型
            model_kwargs={'device': 'cuda'},
            encode_kwargs={'normalize_embeddings': True}
        )
        self.text_splitter = RecursiveCharacterTextSplitter(
            chunk_size=500,
            chunk_overlap=50,
            length_function=len
        )
    
    def load_documents(self, source_path):
        """加载文档,支持 PDF、网页、文本文件"""
        documents = []
        
        if os.path.isdir(source_path):
            # 批量加载文件夹中的文档
            loader = DirectoryLoader(
                source_path,
                glob="**/*.pdf",
                loader_cls=PyPDFLoader
            )
            documents.extend(loader.load())
        elif source_path.endswith('.pdf'):
            loader = PyPDFLoader(source_path)
            documents.extend(loader.load())
        else:
            raise ValueError(f"不支持的文件格式: {source_path}")
        
        # 文本分块
        texts = self.text_splitter.split_documents(documents)
        print(f"✓ 加载文档 {len(documents)} 个,分割为 {len(texts)} 个文本块")
        return texts
    
    def ingest_documents(self, texts, batch_size=100):
        """批量向量化入库"""
        total = len(texts)
        for i in range(0, total, batch_size):
            batch = texts[i:i + batch_size]
            
            # 提取文本内容
            contents = [doc.page_content for doc in batch]
            metadatas = [doc.metadata for doc in batch]
            
            # 生成向量
            vectors = self.embeddings.embed_documents(contents)
            vectors = [np.array(v, dtype=np.float32).tolist() for v in vectors]
            
            # 插入 Milvus
            entities = [
                contents,      # VARCHAR 字段
                vectors,       # FLOAT_VECTOR 字段
                metadatas      # JSON 字段
            ]
            
            self.collection.insert(entities)
            
            if (i + batch_size) % 1000 == 0:
                print(f"  进度: {min(i + batch_size, total)}/{total}")
        
        self.collection.flush()
        print(f"✓ 入库完成,共 {total} 条记录")

使用示例

ingestion = KnowledgeBaseIngestion(collection) texts = ingestion.load_documents("/data/knowledge_base/products/") ingestion.ingest_documents(texts, batch_size=50)

接入 HolySheheep API 实现模型降级与成本优化

在生产环境中,我们不可能完全依赖本地模型。当 Llama 4 服务过载或出现故障时,需要可靠的兜底方案。HolySheheep API 的优势在于:国内直连延迟低于 50ms,汇率按 ¥7.3=$1 计算,比官方 $8/M 的 GPT-4.1 便宜 85% 以上,而且支持微信/支付宝充值,对国内开发者非常友好。

我设计了智能降级策略:正常情况下用本地 Llama 4,当响应时间超过 3 秒或错误率超过 5% 时,自动切换到 HolySheheep 的 DeepSeek V3.2($0.42/M)处理请求:

from openai import OpenAI
import time
from functools import wraps
from typing import Optional

class HybridLLMService:
    def __init__(self):
        # HolySheheep API 配置
        self.holy_client = OpenAI(
            api_key="YOUR_HOLYSHEHEP_API_KEY",  # 替换为你的 HolySheheep API Key
            base_url="https://api.holyshehe.ai/v1"
        )
        
        # 本地 Llama 4 服务(使用 vLLM 部署)
        self.local_client = OpenAI(
            api_key="EMPTY",
            base_url="http://localhost:8000/v1"
        )
        
        # 降级阈值配置
        self.LOCAL_TIMEOUT = 3.0  # 秒
        self.ERROR_RATE_THRESHOLD = 0.05
        self.error_count = 0
        self.request_count = 0
        
    def _check_degrade(self):
        """检查是否需要降级"""
        if self.request_count < 100:
            return False
        return (self.error_count / self.request_count) > self.ERROR_RATE_THRESHOLD
    
    def _record_success(self):
        self.request_count += 1
    
    def _record_error(self):
        self.request_count += 1
        self.error_count += 1
    
    def chat_completion(self, messages: list, system_prompt: str = "") -> str:
        """
        智能路由:优先本地 Llama 4,必要时降级到 HolySheheep
        """
        # 构造完整消息
        full_messages = []
        if system_prompt:
            full_messages.append({"role": "system", "content": system_prompt})
        full_messages.extend(messages)
        
        # 尝试本地模型
        if not self._check_degrade():
            try:
                response = self.local_client.chat.completions.create(
                    model="llama-4-mixtral-8x7b",
                    messages=full_messages,
                    temperature=0.7,
                    max_tokens=1024,
                    timeout=self.LOCAL_TIMEOUT
                )
                self._record_success()
                return response.choices[0].message.content
            except Exception as e:
                print(f"⚠ 本地模型调用失败: {e},切换到 HolySheheep")
                self._record_error()
        
        # 降级到 HolySheheep(DeepSeek V3.2)
        try:
            response = self.holy_client.chat.completions.create(
                model="deepseek-v3.2",
                messages=full_messages,
                temperature=0.7,
                max_tokens=1024
            )
            self._record_success()
            return response.choices[0].message.content
        except Exception as e:
            print(f"❌ HolySheheep API 也失败了: {e}")
            self._record_error()
            raise RuntimeError("所有 LLM 服务均不可用")

初始化服务

llm_service = HybridLLMService()

使用示例

result = llm_service.chat_completion( messages=[{"role": "user", "content": "这款手机支持5G吗?"}], system_prompt="你是一个专业的电商客服,请根据知识库回答用户问题。" ) print(result)

RAG 检索与生成完整流程实现

现在把向量检索和 LLM 生成串联起来,形成完整的 RAG 链路。我在实战中总结出几点优化经验:检索时用 ANN 近似最近邻可以提升 3 倍速度,生成时把检索到的 context 放在 prompt 最前面效果最好:

from pymilvus import Collection
import numpy as np

class RAGSystem:
    def __init__(self, collection: Collection, llm_service: HybridLLMService):
        self.collection = collection
        self.llm = llm_service
        from langchain.embeddings import HuggingFaceBgeEmbeddings
        self.embedder = HuggingFaceBgeEmbeddings(
            model_name="BAAI/bge-large-zh-v1.5",
            model_kwargs={'device': 'cuda'},
            encode_kwargs={'normalize_embeddings': True}
        )
    
    def retrieve(self, query: str, top_k: int = 5) -> list:
        """
        从 Milvus 检索最相关的文档块
        """
        # 生成查询向量
        query_vector = self.embedder.embed_query(query)
        
        # ANN 检索(使用 IVF_FLAT 索引)
        search_params = {
            "metric_type": "L2",
            "params": {"nprobe": 10}  # nprobe 越大精度越高,但速度越慢
        }
        
        results = self.collection.search(
            data=[query_vector],
            anns_field="embedding",
            param=search_params,
            limit=top_k,
            output_fields=["content", "metadata"]
        )
        
        # 整理检索结果
        retrieved_docs = []
        for hit in results[0]:
            retrieved_docs.append({
                "content": hit.entity.get("content"),
                "metadata": hit.entity.get("metadata"),
                "distance": hit.distance
            })
        
        return retrieved_docs
    
    def generate(self, query: str, retrieved_docs: list) -> str:
        """
        基于检索结果生成回答
        """
        # 构造 context
        context_parts = []
        for i, doc in enumerate(retrieved_docs, 1):
            source = doc["metadata"].get("source", "未知来源")
            content = doc["content"]
            context_parts.append(f"[文档{i}] 来源: {source}\n{content}")
        
        context = "\n\n".join(context_parts)
        
        # 构造 prompt(关键:context 在前,query 在后)
        system_prompt = f"""你是一个专业的电商客服助手。请根据以下知识库内容回答用户问题。

【知识库内容】
{context}

【回答要求】
1. 只基于知识库内容回答,不要编造信息
2. 如果知识库没有相关信息,请如实告知用户
3. 回答要专业、礼貌、简洁
4. 如果涉及商品参数,请标注来源文档"""
        
        # 调用 LLM(自动降级)
        answer = self.llm.chat_completion(
            messages=[{"role": "user", "content": query}],
            system_prompt=system_prompt
        )
        
        return answer
    
    def query(self, question: str, top_k: int = 5) -> dict:
        """
        完整的 RAG 查询流程
        """
        # 1. 检索相关文档
        retrieved_docs = self.retrieve(question, top_k=top_k)
        
        # 2. 生成回答
        answer = self.generate(question, retrieved_docs)
        
        return {
            "question": question,
            "answer": answer,
            "retrieved_docs": retrieved_docs,
            "retrieval_count": len(retrieved_docs)
        }

初始化 RAG 系统

rag = RAGSystem(collection=collection, llm_service=llm_service)

测试查询

result = rag.query("iPhone 15 的电池续航是多少?") print(f"问题: {result['question']}") print(f"回答: {result['answer']}") print(f"检索到 {result['retrieval_count']} 条相关文档")

FastAPI 网关与服务部署

为了让 RAG 系统对外提供服务,我用 FastAPI 搭建了一个高性能网关,集成了限流、缓存、监控等功能:

from fastapi import FastAPI, HTTPException, Request
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
from typing import List, Optional
import redis
import time
from slowapi import Limiter
from slowapi.util import get_remote_address

app = FastAPI(title="RAG 知识库 API", version="1.0.0")

限流配置(防止恶意刷接口)

limiter = Limiter(key_func=get_remote_address) app.state.limiter = limiter

Redis 缓存(热点问题缓存 5 分钟)

redis_client = redis.Redis(host='localhost', port=6379, db=0, decode_responses=True) class QueryRequest(BaseModel): question: str top_k: Optional[int] = 5 user_id: Optional[str] = None class QueryResponse(BaseModel): answer: str sources: List[dict] latency_ms: float model: str @app.on_event("startup") async def startup(): print("🚀 RAG API 服务启动") # 预热向量模型 rag.embedder.embed_query("预热") @app.get("/health") async def health_check(): return {"status": "healthy", "timestamp": time.time()} @app.post("/api/v1/query", response_model=QueryResponse) @limiter.limit("100/minute") # 每个 IP 每分钟最多 100 次 async def query(request: Request, body: QueryRequest): """ RAG 查询接口 """ start_time = time.time() # 检查缓存 cache_key = f"rag:query:{body.question}:{body.top_k}" cached = redis_client.get(cache_key) if cached: import json data = json.loads(cached) data["latency_ms"] = round((time.time() - start_time) * 1000, 2) data["cached"] = True return QueryResponse(**data) try: # 执行 RAG 查询 result = rag.query(body.question, top_k=body.top_k or 5) # 构造响应 sources = [ {"content": doc["content"][:100] + "...", "source": doc["metadata"].get("source", "未知")} for doc in result["retrieved_docs"] ] latency_ms = round((time.time() - start_time) * 1000, 2) response = QueryResponse( answer=result["answer"], sources=sources, latency_ms=latency_ms, model="llama-4 + milvus" ) # 写入缓存(5 分钟 TTL) import json redis_client.setex( cache_key, 300, json.dumps(response.model_dump(), ensure_ascii=False) ) return response except Exception as e: raise HTTPException(status_code=500, detail=str(e))

CORS 配置

app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"],