去年双十一,我负责的电商平台 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=["*"],