实时AI应用开发完全教程

教程简介

零基础实时AI应用开发完全教程,涵盖实时AI架构设计、Redis向量搜索、PostgreSQL pgvector扩展、实时Embedding管线、流式RAG系统、WebSocket实时推理、SSE服务端推送、实时推荐引擎、实时异常检测、边缘计算推理等核心技能,配有实时智能客服与流式数据分析平台两大实战项目,适合后端开发者和AI工程师系统学习。

实时AI应用开发完全教程

适用人群:后端开发者、AI工程师、全栈工程师
前置要求:Python基础、SQL基础、HTTP/WebSocket基本概念
预计学习时间:40-60小时


目录


第一章:实时AI架构设计基础

1.1 什么是实时AI应用

实时AI应用是指在用户请求到达后,系统能在亚秒级到秒级延迟内完成AI推理并返回结果的应用。与传统的批处理AI系统不同,实时AI强调:

  • 低延迟:P99延迟通常要求在500ms以内
  • 高并发:同时处理数千到数万用户的请求
  • 持续更新:模型和特征数据需要实时或近实时更新
  • 流式交互:支持逐token输出、增量结果推送

典型应用场景包括:实时对话系统、实时推荐、实时风控、实时翻译、实时内容审核等。

1.2 实时AI的核心挑战

挑战 说明 解决策略
推理延迟 模型推理计算量大 模型量化、批处理、GPU优化
向量检索延迟 百万级向量中快速查找近邻 近似最近邻(ANN)算法、索引优化
数据新鲜度 用户行为变化需要即时反映 流式特征更新、增量索引
系统复杂度 多组件协同,故障传播 断路器、降级策略、幂等设计
成本控制 GPU资源昂贵 动态扩缩容、请求路由优化

1.3 架构模式对比

请求-响应模式(同步)

用户 → API网关 → 推理服务 → 向量数据库 → 返回结果

适用于简单查询,延迟要求不严格的场景。

流式推送模式(异步)

用户 → WebSocket/SSE → 推理服务(流式生成) → 逐token推送

适用于对话、内容生成等需要流式体验的场景。

事件驱动模式

数据源 → 消息队列 → 特征管线 → 向量索引更新
                     → 异常检测 → 告警推送

适用于实时推荐、异常检测等持续运行的场景。

1.4 技术栈选型指南

# 推荐技术栈组合

REALTIME_AI_STACK = {
    "向量数据库": {
        "低延迟场景": "Redis Stack (RedisSearch + RediSearch)",
        "大规模持久化": "PostgreSQL + pgvector",
        "纯向量场景": "Milvus / Qdrant",
    },
    "推理框架": {
        "Python生态": "FastAPI + vLLM / TGI",
        "高性能": "Triton Inference Server",
        "边缘部署": "ONNX Runtime / TensorRT",
    },
    "实时通信": {
        "双向交互": "WebSocket (FastAPI / Socket.IO)",
        "服务端推送": "SSE (Server-Sent Events)",
    },
    "流处理": {
        "轻量级": "Redis Streams",
        "企业级": "Apache Kafka / Apache Flink",
    },
    "Embedding模型": {
        "中文场景": "text2vec-large-chinese / bge-large-zh",
        "多语言": "text-embedding-3-small (OpenAI) / e5-large-v2",
        "轻量部署": "bge-small-zh-v1.5 / MiniLM",
    },
}

第二章:Redis向量搜索实战

2.1 Redis Stack与向量搜索概述

Redis Stack是Redis的扩展版本,内置了RediSearch模块,支持原生向量搜索。其核心优势:

  • 内存级速度:向量检索延迟通常在1-5ms
  • 混合查询:向量搜索 + 标量过滤 + 全文检索组合
  • 数据结构丰富:支持JSON、Set、Stream等多种结构
  • 部署简单:单节点即可运行,无需分布式集群

适用场景:实时推荐召回、实时相似搜索、会话级缓存向量。

2.2 环境搭建与配置

# 使用Docker启动Redis Stack
docker run -d \
  --name redis-stack \
  -p 6379:6379 \
  -p 8001:8001 \
  -e REDIS_ARGS="--requirepass your_password" \
  redis/redis-stack:latest
# Python客户端安装
# pip install redis numpy

import redis
import numpy as np

# 连接Redis
r = redis.Redis(
    host="localhost",
    port=6379,
    password="your_password",
    decode_responses=True
)

# 验证连接
print(r.ping())  # True

# 检查RediSearch模块是否加载
modules = r.execute_command("MODULE LIST")
print(modules)

2.3 向量索引创建与管理

from redis.commands.search.field import (
    VectorField, TagField, NumericField, TextField
)
from redis.commands.search.indexDefinition import IndexDefinition, IndexType

# 定义索引schema
schema = [
    # 向量字段:1536维,使用HNSW算法,余弦相似度
    VectorField(
        "embedding",
        "HNSW",
        {
            "TYPE": "FLOAT32",
            "DIM": 1536,
            "DISTANCE_METRIC": "COSINE",
            "INITIAL_CAP": 100000,
            "M": 16,              # HNSW图中每个节点的连接数
            "EF_CONSTRUCTION": 200,  # 构建时的搜索宽度
        },
    ),
    # 标量字段
    TagField("category", separator=","),
    NumericField("timestamp"),
    TextField("content"),
    TextField("doc_id"),
]

# 创建索引
try:
    r.ft("idx:documents").create_index(
        fields=schema,
        definition=IndexDefinition(
            prefix=["doc:"],
            index_type=IndexType.HASH,
        ),
    )
    print("索引创建成功")
except Exception as e:
    if "Index already exists" in str(e):
        print("索引已存在,跳过创建")
    else:
        raise

# 查看索引信息
info = r.ft("idx:documents").info()
print(f"索引文档数: {info['num_docs']}")
print(f"索引字段: {info['attributes']}")

2.4 向量数据写入与查询

import json
import time

def store_document(doc_id: str, content: str, embedding: list[float], 
                   category: str = ""):
    """存储文档及其向量"""
    key = f"doc:{doc_id}"
    r.hset(key, mapping={
        "doc_id": doc_id,
        "content": content,
        "embedding": np.array(embedding, dtype=np.float32).tobytes(),
        "category": category,
        "timestamp": int(time.time()),
    })

def search_similar(query_embedding: list[float], top_k: int = 10,
                   category_filter: str = None) -> list[dict]:
    """向量相似搜索"""
    from redis.commands.search.query import Query
    
    # 构建查询
    query_vector = np.array(query_embedding, dtype=np.float32).tobytes()
    
    # KNN查询语句
    knn_query = f"*=>[KNN {top_k} @embedding $vec AS score]"
    
    q = Query(knn_query).dialect(2)
    
    # 可选:添加标量过滤
    if category_filter:
        q = Query(knn_query).dialect(2).filter(
            f"@category=={{{category_filter}}}"
        )
    
    q.return_fields("doc_id", "content", "category", "score")
    q.sort_by("score", asc=True)  # 余弦距离,越小越相似
    q.paging(0, top_k)
    
    # 执行查询
    results = r.ft("idx:documents").search(
        q, query_params={"vec": query_vector}
    )
    
    return [
        {
            "doc_id": doc.doc_id,
            "content": doc.content,
            "category": doc.category,
            "score": float(doc.score),
        }
        for doc in results.docs
    ]

2.5 混合搜索:向量+标量过滤

def hybrid_search(
    query_embedding: list[float],
    categories: list[str] = None,
    time_range: tuple[int, int] = None,
    top_k: int = 10,
    text_keyword: str = None,
) -> list[dict]:
    """混合搜索:向量 + 分类过滤 + 时间范围 + 关键词"""
    from redis.commands.search.query import Query
    
    query_vector = np.array(query_embedding, dtype=np.float32).tobytes()
    
    # 构建过滤条件
    filters = []
    if categories:
        # Tag字段多值过滤
        cat_expr = "|".join(categories)
        filters.append(f"@category=={{{cat_expr}}}")
    
    if time_range:
        start, end = time_range
        filters.append(f"@timestamp:[{start} {end}]")
    
    if text_keyword:
        filters.append(f"@content:{text_keyword}")
    
    # 组合查询
    filter_str = ""
    if filters:
        filter_str = " ".join(filters) + " "
    
    knn_query = f"{filter_str}=>[KNN {top_k} @embedding $vec AS score]"
    
    q = Query(knn_query).dialect(2)
    q.return_fields("doc_id", "content", "category", "score", "timestamp")
    q.sort_by("score", asc=True)
    q.paging(0, top_k)
    
    results = r.ft("idx:documents").search(
        q, query_params={"vec": query_vector}
    )
    
    return [
        {
            "doc_id": doc.doc_id,
            "content": doc.content,
            "category": doc.category,
            "score": float(doc.score),
            "timestamp": int(doc.timestamp),
        }
        for doc in results.docs
    ]

2.6 性能优化与监控

# 关键配置参数调优
def optimize_hnsw_index():
    """HNSW索引优化建议"""
    tips = {
        "M": "每个节点的连接数。增大M提升召回率,但增加内存和构建时间。推荐值: 16-64",
        "EF_CONSTRUCTION": "构建时搜索范围。越大索引质量越好,但构建越慢。推荐值: 200-500",
        "EF_RUNTIME": "查询时搜索范围。可通过运行时参数调整,推荐值: 10-200",
        "INITIAL_CAP": "预估文档数量,减少动态扩容开销",
    }
    return tips

# 运行时调整搜索精度
def search_with_ef(query_embedding, top_k=10, ef_runtime=50):
    """通过EF_RUNTIME参数控制搜索精度/速度权衡"""
    query_vector = np.array(query_embedding, dtype=np.float32).tobytes()
    q = Query(f"*=>[KNN {top_k} @embedding $vec AS score]").dialect(2)
    q.return_fields("doc_id", "content", "score")
    q.sort_by("score", asc=True)
    
    # 设置运行时EF参数
    results = r.ft("idx:documents").search(
        q, query_params={"vec": query_vector}
    )
    return results

# 监控索引状态
def monitor_index(index_name: str = "idx:documents"):
    """监控Redis索引状态"""
    info = r.ft(index_name).info()
    
    stats = {
        "文档数量": info.get("num_docs", 0),
        "索引内存(MB)": round(
            int(info.get("inverted_sz_mb", 0)) + 
            int(info.get("vector_index_sz_mb", 0)), 2
        ),
        "向量索引大小(MB)": info.get("vector_index_sz_mb", 0),
        "索引构建状态": info.get("index_status", "unknown"),
    }
    
    # Redis内存使用
    memory_info = r.info("memory")
    stats["Redis使用内存(MB)"] = round(
        int(memory_info.get("used_memory", 0)) / 1024 / 1024, 2
    )
    stats["Redis峰值内存(MB)"] = round(
        int(memory_info.get("used_memory_peak", 0)) / 1024 / 1024, 2
    )
    
    return stats

第三章:PostgreSQL pgvector扩展深入

3.1 pgvector安装与配置

-- 安装pgvector扩展(需要PostgreSQL 15+)
CREATE EXTENSION IF NOT EXISTS vector;

-- 验证安装
SELECT * FROM pg_extension WHERE extname = 'vector';

-- 配置参数(postgresql.conf或运行时设置)
-- 增加共享内存用于向量索引构建
SET shared_buffers = '4GB';
SET work_mem = '256MB';
SET maintenance_work_mem = '1GB';
# Python连接pgvector
# pip install psycopg2-binary pgvector sqlalchemy

from sqlalchemy import create_engine, text
from pgvector.psycopg2 import register_vector
import psycopg2

# 连接数据库
conn = psycopg2.connect(
    host="localhost",
    port=5432,
    dbname="ai_db",
    user="postgres",
    password="your_password",
)
register_vector(conn)

# 创建表
with conn.cursor() as cur:
    cur.execute("""
        CREATE TABLE IF NOT EXISTS documents (
            id SERIAL PRIMARY KEY,
            doc_id VARCHAR(64) UNIQUE NOT NULL,
            content TEXT NOT NULL,
            category VARCHAR(32),
            embedding vector(1536),
            metadata JSONB DEFAULT '{}',
            created_at TIMESTAMP DEFAULT NOW(),
            updated_at TIMESTAMP DEFAULT NOW()
        );
    """)
    conn.commit()

3.2 向量数据类型与操作

import numpy as np

def insert_document(cur, doc_id: str, content: str, 
                    embedding: list[float], category: str = "",
                    metadata: dict = None):
    """插入文档及向量"""
    cur.execute("""
        INSERT INTO documents (doc_id, content, category, embedding, metadata)
        VALUES (%s, %s, %s, %s, %s)
        ON CONFLICT (doc_id) 
        DO UPDATE SET 
            content = EXCLUDED.content,
            embedding = EXCLUDED.embedding,
            category = EXCLUDED.category,
            metadata = EXCLUDED.metadata,
            updated_at = NOW()
    """, (
        doc_id, content, category, 
        np.array(embedding, dtype=np.float32),
        json.dumps(metadata or {}),
    ))

def search_by_vector(cur, query_embedding: list[float], 
                     top_k: int = 10, category: str = None) -> list[dict]:
    """余弦相似度搜索"""
    query_vec = np.array(query_embedding, dtype=np.float32)
    
    if category:
        cur.execute("""
            SELECT doc_id, content, category, 
                   1 - (embedding <=> %s::vector) AS similarity,
                   metadata
            FROM documents
            WHERE category = %s
            ORDER BY embedding <=> %s::vector
            LIMIT %s
        """, (query_vec, category, query_vec, top_k))
    else:
        cur.execute("""
            SELECT doc_id, content, category,
                   1 - (embedding <=> %s::vector) AS similarity,
                   metadata
            FROM documents
            ORDER BY embedding <=> %s::vector
            LIMIT %s
        """, (query_vec, query_vec, top_k))
    
    return [
        {
            "doc_id": row[0],
            "content": row[1],
            "category": row[2],
            "similarity": float(row[3]),
            "metadata": row[4],
        }
        for row in cur.fetchall()
    ]

# pgvector支持的三种距离操作符:
# <=>  余弦距离 (1 - cosine_similarity)
# <->  L2欧几里得距离
# <#>  内积距离 (负内积)

3.3 索引策略:IVFFlat vs HNSW

-- IVFFlat索引:适合大规模数据,构建速度快
-- lists数量建议:rows / 1000 对于 <1M行,sqrt(rows) 对于 >1M行
CREATE INDEX idx_documents_embedding_ivf 
ON documents 
USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 100);

-- HNSW索引:查询速度快,召回率高,但构建较慢
CREATE INDEX idx_documents_embedding_hnsw 
ON documents 
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 200);

-- 查看索引构建进度
SELECT phase, tuples_done, tuples_total
FROM pg_stat_progress_create_index;

-- 运行时调整HNSW搜索参数(会话级别)
SET hnsw.ef_search = 100;  -- 默认40,增大提升召回率

-- 调整IVFFlat探查的列表数
SET ivfflat.probes = 10;   -- 默认1,增大提升召回率但降低速度
# 索引选择建议
INDEX_GUIDE = {
    "IVFFlat": {
        "适用场景": "数据量大(>100万),允许近似搜索",
        "优势": "构建快,内存占用较小",
        "劣势": "需要先插入数据再建索引,召回率略低",
        "参数建议": "lists = sqrt(总行数),probes = lists的1%-10%",
    },
    "HNSW": {
        "适用场景": "对查询延迟和召回率要求高",
        "优势": "查询快,召回率高,支持增量插入",
        "劣势": "构建慢,内存占用较大",
        "参数建议": "m = 16-64, ef_construction = 200-500, ef_search = 40-200",
    },
}

3.4 与传统SQL的联合查询

def advanced_search(
    cur,
    query_embedding: list[float],
    keywords: str = None,
    category: str = None,
    date_from: str = None,
    date_to: str = None,
    min_similarity: float = 0.5,
    top_k: int = 10,
) -> list[dict]:
    """复杂的混合查询:向量 + 全文 + 标量过滤"""
    
    conditions = []
    params = []
    
    # 向量相似度基础查询
    base_select = """
        SELECT doc_id, content, category,
               1 - (embedding <=> %s::vector) AS similarity,
               metadata, created_at
        FROM documents
    """
    params.append(np.array(query_embedding, dtype=np.float32))
    
    # 全文搜索条件(使用tsvector)
    if keywords:
        conditions.append(
            "to_tsvector('chinese', content) @@ plainto_tsquery('chinese', %s)"
        )
        params.append(keywords)
    
    # 分类过滤
    if category:
        conditions.append("category = %s")
        params.append(category)
    
    # 时间范围
    if date_from:
        conditions.append("created_at >= %s")
        params.append(date_from)
    if date_to:
        conditions.append("created_at <= %s")
        params.append(date_to)
    
    # 相似度阈值
    conditions.append(
        f"1 - (embedding <=> %s::vector) >= %s"
    )
    params.append(np.array(query_embedding, dtype=np.float32))
    params.append(min_similarity)
    
    # 组装查询
    where_clause = " AND ".join(conditions) if conditions else "TRUE"
    query = f"""
        {base_select}
        WHERE {where_clause}
        ORDER BY embedding <=> %s::vector
        LIMIT %s
    """
    params.append(np.array(query_embedding, dtype=np.float32))
    params.append(top_k)
    
    cur.execute(query, params)
    
    return [
        {
            "doc_id": row[0],
            "content": row[1],
            "category": row[2],
            "similarity": float(row[3]),
            "metadata": row[4],
            "created_at": row[5].isoformat() if row[5] else None,
        }
        for row in cur.fetchall()
    ]

3.5 大规模向量数据管理

-- 分区表策略:按时间分区,每分区独立建索引
CREATE TABLE documents_partitioned (
    id SERIAL,
    doc_id VARCHAR(64) NOT NULL,
    content TEXT NOT NULL,
    category VARCHAR(32),
    embedding vector(1536),
    created_at TIMESTAMP DEFAULT NOW()
) PARTITION BY RANGE (created_at);

-- 创建月度分区
CREATE TABLE documents_2024_01 PARTITION OF documents_partitioned
    FOR VALUES FROM ('2024-01-01') TO ('2024-02-01');
CREATE TABLE documents_2024_02 PARTITION OF documents_partitioned
    FOR VALUES FROM ('2024-02-01') TO ('2024-03-01');

-- 为每个分区创建独立索引
CREATE INDEX ON documents_2024_01 
    USING hnsw (embedding vector_cosine_ops) WITH (m = 16, ef_construction = 200);
CREATE INDEX ON documents_2024_02 
    USING hnsw (embedding vector_cosine_ops) WITH (m = 16, ef_construction = 200);

-- 批量导入优化
-- 1. 临时禁用索引
-- 2. 使用COPY命令批量导入
-- 3. 重建索引

-- 监控向量索引大小
SELECT 
    schemaname,
    tablename,
    indexname,
    pg_size_pretty(pg_relation_size(indexrelid)) AS index_size
FROM pg_stat_user_indexes
WHERE indexname LIKE '%embedding%';

第四章:实时Embedding管线

4.1 Embedding模型选型

# 模型选型对比
EMBEDDING_MODELS = {
    "text2vec-large-chinese": {
        "维度": 1024,
        "语言": "中文优化",
        "速度": "中等",
        "质量": "高",
        "部署": "sentence-transformers",
        "适用": "中文语义搜索、问答匹配",
    },
    "bge-large-zh-v1.5": {
        "维度": 1024,
        "语言": "中文优化",
        "速度": "中等",
        "质量": "很高",
        "部署": "sentence-transformers / FlagEmbedding",
        "适用": "中文RAG、知识库检索",
    },
    "bge-small-zh-v1.5": {
        "维度": 512,
        "语言": "中文优化",
        "速度": "快",
        "质量": "中高",
        "部署": "轻量级,适合边缘部署",
        "适用": "资源受限场景、实时性要求极高",
    },
    "text-embedding-3-small": {
        "维度": 1536,
        "语言": "多语言",
        "速度": "API调用",
        "质量": "高",
        "部署": "OpenAI API",
        "适用": "多语言场景、快速原型",
    },
}

4.2 批量与流式Embedding生成

import asyncio
from typing import AsyncIterator
from sentence_transformers import SentenceTransformer
import numpy as np

class EmbeddingService:
    """Embedding生成服务,支持批量和流式"""
    
    def __init__(self, model_name: str = "BAAI/bge-large-zh-v1.5",
                 batch_size: int = 32, max_length: int = 512):
        self.model = SentenceTransformer(model_name)
        self.batch_size = batch_size
        self.max_length = max_length
        self._dimension = self.model.get_sentence_embedding_dimension()
    
    @property
    def dimension(self) -> int:
        return self._dimension
    
    def encode_batch(self, texts: list[str]) -> np.ndarray:
        """批量编码"""
        return self.model.encode(
            texts,
            batch_size=self.batch_size,
            normalize_embeddings=True,
            show_progress_bar=False,
        )
    
    async def encode_stream(
        self, text_iterator: AsyncIterator[str]
    ) -> AsyncIterator[tuple[str, np.ndarray]]:
        """流式编码:逐批处理输入流"""
        batch = []
        async for text in text_iterator:
            batch.append(text)
            if len(batch) >= self.batch_size:
                embeddings = self.encode_batch(batch)
                for t, e in zip(batch, embeddings):
                    yield t, e
                batch = []
        
        # 处理剩余
        if batch:
            embeddings = self.encode_batch(batch)
            for t, e in zip(batch, embeddings):
                yield t, e
    
    def encode_with_prefix(self, texts: list[str], 
                           prefix: str = "为这个句子生成表示以用于检索相关文章:") -> np.ndarray:
        """带指令前缀的编码(BGE系列推荐用法)"""
        prefixed = [f"{prefix}{t}" for t in texts]
        return self.encode_batch(prefixed)

4.3 Embedding缓存策略

import hashlib
import json
from functools import lru_cache

class EmbeddingCache:
    """多级缓存Embedding服务"""
    
    def __init__(self, embedding_service: EmbeddingService, 
                 redis_client=None, cache_ttl: int = 86400):
        self.service = embedding_service
        self.redis = redis_client
        self.cache_ttl = cache_ttl
        self._local_cache = {}  # 进程内LRU缓存
    
    def _cache_key(self, text: str) -> str:
        """生成缓存键"""
        text_hash = hashlib.md5(text.encode()).hexdigest()
        return f"emb:{text_hash}"
    
    def get_embedding(self, text: str) -> np.ndarray:
        """获取embedding,三级缓存:本地→Redis→模型"""
        cache_key = self._cache_key(text)
        
        # L1: 本地缓存
        if cache_key in self._local_cache:
            return self._local_cache[cache_key]
        
        # L2: Redis缓存
        if self.redis:
            cached = self.redis.get(cache_key)
            if cached:
                embedding = np.frombuffer(
                    bytes.fromhex(cached), dtype=np.float32
                )
                self._local_cache[cache_key] = embedding
                return embedding
        
        # L3: 模型推理
        embedding = self.service.encode_batch([text])[0]
        
        # 写回缓存
        self._local_cache[cache_key] = embedding
        if self.redis:
            self.redis.setex(
                cache_key, 
                self.cache_ttl,
                embedding.tobytes().hex(),
            )
        
        return embedding
    
    def get_embeddings_batch(self, texts: list[str]) -> np.ndarray:
        """批量获取,分离缓存命中与未命中"""
        results = [None] * len(texts)
        uncached_indices = []
        uncached_texts = []
        
        for i, text in enumerate(texts):
            cache_key = self._cache_key(text)
            
            # L1
            if cache_key in self._local_cache:
                results[i] = self._local_cache[cache_key]
                continue
            
            # L2
            if self.redis:
                cached = self.redis.get(cache_key)
                if cached:
                    embedding = np.frombuffer(
                        bytes.fromhex(cached), dtype=np.float32
                    )
                    self._local_cache[cache_key] = embedding
                    results[i] = embedding
                    continue
            
            uncached_indices.append(i)
            uncached_texts.append(text)
        
        # 批量推理未命中的
        if uncached_texts:
            new_embeddings = self.service.encode_batch(uncached_texts)
            for idx, embedding in zip(uncached_indices, new_embeddings):
                results[idx] = embedding
                cache_key = self._cache_key(texts[idx])
                self._local_cache[cache_key] = embedding
                if self.redis:
                    self.redis.setex(
                        cache_key, self.cache_ttl,
                        embedding.tobytes().hex(),
                    )
        
        return np.array(results)

4.4 多模态Embedding处理

class MultiModalEmbedding:
    """多模态Embedding统一处理"""
    
    def __init__(self):
        self.text_model = SentenceTransformer("BAAI/bge-large-zh-v1.5")
        # 图像模型需要额外安装
        # self.image_model = SentenceTransformer("clip-ViT-B-32")
    
    def embed_text(self, text: str) -> np.ndarray:
        """文本Embedding"""
        return self.text_model.encode(text, normalize_embeddings=True)
    
    def embed_image(self, image_path: str) -> np.ndarray:
        """图像Embedding(需要CLIP模型)"""
        # from PIL import Image
        # image = Image.open(image_path)
        # return self.image_model.encode(image)
        raise NotImplementedError("需要安装CLIP模型")
    
    def cross_modal_search(self, query_text: str, 
                           image_embeddings: np.ndarray,
                           top_k: int = 5) -> list[int]:
        """跨模态搜索:用文本搜图片"""
        query_vec = self.embed_text(query_text)
        # 计算相似度
        similarities = np.dot(image_embeddings, query_vec)
        # 返回Top-K索引
        return np.argsort(similarities)[::-1][:top_k].tolist()

4.5 管线监控与告警

import time
from dataclasses import dataclass, field
from collections import deque

@dataclass
class PipelineMetrics:
    """Embedding管线监控指标"""
    total_processed: int = 0
    total_errors: int = 0
    latency_history: deque = field(default_factory=lambda: deque(maxlen=1000))
    cache_hits: int = 0
    cache_misses: int = 0
    
    def record_latency(self, latency_ms: float):
        self.latency_history.append(latency_ms)
        self.total_processed += 1
    
    def record_cache_hit(self):
        self.cache_hits += 1
    
    def record_cache_miss(self):
        self.cache_misses += 1
    
    @property
    def avg_latency_ms(self) -> float:
        if not self.latency_history:
            return 0
        return sum(self.latency_history) / len(self.latency_history)
    
    @property
    def p99_latency_ms(self) -> float:
        if not self.latency_history:
            return 0
        sorted_latencies = sorted(self.latency_history)
        idx = int(len(sorted_latencies) * 0.99)
        return sorted_latencies[min(idx, len(sorted_latencies) - 1)]
    
    @property
    def cache_hit_rate(self) -> float:
        total = self.cache_hits + self.cache_misses
        if total == 0:
            return 0
        return self.cache_hits / total
    
    def check_alerts(self) -> list[str]:
        """检查是否需要告警"""
        alerts = []
        if self.p99_latency_ms > 500:
            alerts.append(f"P99延迟过高: {self.p99_latency_ms:.0f}ms > 500ms")
        if self.total_errors / max(self.total_processed, 1) > 0.05:
            alerts.append(f"错误率过高: {self.total_errors}/{self.total_processed}")
        if self.cache_hit_rate < 0.3 and self.cache_hits + self.cache_misses > 100:
            alerts.append(f"缓存命中率过低: {self.cache_hit_rate:.1%}")
        return alerts

第五章:流式RAG系统开发

5.1 RAG基础架构回顾

RAG(Retrieval-Augmented Generation)的核心流程:

用户提问 → 查询改写 → 向量检索 → 上下文组装 → LLM生成 → 流式输出

实时RAG的关键区别在于:

  1. 检索阶段:亚秒级向量检索,可能需要多路召回
  2. 生成阶段:流式token输出,首token延迟(TTFT)控制在1秒内
  3. 反馈阶段:支持用户中断、追问、上下文切换

5.2 流式检索设计

import asyncio
from dataclasses import dataclass

@dataclass
class RetrievalResult:
    doc_id: str
    content: str
    score: float
    source: str  # 来源标识

class StreamingRAG:
    """流式RAG系统"""
    
    def __init__(self, redis_client, pg_conn, llm_client, 
                 embedding_service):
        self.redis = redis_client
        self.pg = pg_conn
        self.llm = llm_client
        self.embedder = embedding_service
    
    async def multi_source_retrieve(
        self, query: str, top_k: int = 5
    ) -> list[RetrievalResult]:
        """多路召回:Redis快速检索 + PostgreSQL精确检索"""
        query_embedding = self.embedder.encode_batch([query])[0]
        
        # 并行执行两路检索
        redis_task = asyncio.create_task(
            self._redis_retrieve(query_embedding, top_k)
        )
        pg_task = asyncio.create_task(
            self._pg_retrieve(query_embedding, top_k)
        )
        
        redis_results, pg_results = await asyncio.gather(
            redis_task, pg_task
        )
        
        # 结果融合(RRF: Reciprocal Rank Fusion)
        return self._rrf_merge(redis_results, pg_results, k=60)
    
    async def _redis_retrieve(self, embedding, top_k):
        """Redis快速召回"""
        # 参考第二章的search_similar实现
        from redis.commands.search.query import Query
        import numpy as np
        
        query_vector = np.array(embedding, dtype=np.float32).tobytes()
        q = Query(f"*=>[KNN {top_k} @embedding $vec AS score]").dialect(2)
        q.return_fields("doc_id", "content", "score")
        q.sort_by("score", asc=True)
        
        results = self.redis.ft("idx:documents").search(
            q, query_params={"vec": query_vector}
        )
        
        return [
            RetrievalResult(r.doc_id, r.content, float(r.score), "redis")
            for r in results.docs
        ]
    
    async def _pg_retrieve(self, embedding, top_k):
        """PostgreSQL精确召回"""
        import numpy as np
        
        with self.pg.cursor() as cur:
            cur.execute("""
                SELECT doc_id, content, 1 - (embedding <=> %s::vector) AS score
                FROM documents
                ORDER BY embedding <=> %s::vector
                LIMIT %s
            """, (np.array(embedding, dtype=np.float32), 
                  np.array(embedding, dtype=np.float32), top_k))
            
            return [
                RetrievalResult(row[0], row[1], float(row[2]), "pg")
                for row in cur.fetchall()
            ]
    
    def _rrf_merge(self, *result_lists, k=60) -> list[RetrievalResult]:
        """RRF排序融合"""
        scores = {}
        doc_map = {}
        
        for results in result_lists:
            for rank, r in enumerate(results):
                if r.doc_id not in scores:
                    scores[r.doc_id] = 0
                    doc_map[r.doc_id] = r
                scores[r.doc_id] += 1 / (k + rank + 1)
        
        sorted_ids = sorted(scores.keys(), key=lambda x: scores[x], reverse=True)
        return [doc_map[doc_id] for doc_id in sorted_ids]

5.3 上下文窗口管理

class ContextManager:
    """上下文窗口管理器"""
    
    def __init__(self, max_context_tokens: int = 3000, 
                 max_history_turns: int = 5):
        self.max_context_tokens = max_context_tokens
        self.max_history_turns = max_history_turns
    
    def build_context(
        self, 
        query: str,
        retrieved_docs: list[RetrievalResult],
        chat_history: list[dict],
        system_prompt: str = "",
    ) -> list[dict]:
        """构建发送给LLM的上下文消息"""
        messages = []
        
        # 1. 系统提示
        if system_prompt:
            messages.append({"role": "system", "content": system_prompt})
        
        # 2. 检索文档上下文
        doc_context = self._format_retrieved_docs(retrieved_docs)
        messages.append({
            "role": "system", 
            "content": f"以下是相关参考资料,请基于这些内容回答用户问题:\n\n{doc_context}"
        })
        
        # 3. 对话历史(限制轮数)
        recent_history = chat_history[-self.max_history_turns * 2:]
        messages.extend(recent_history)
        
        # 4. 当前用户提问
        messages.append({"role": "user", "content": query})
        
        # 5. Token预算检查
        messages = self._trim_to_fit(messages)
        
        return messages
    
    def _format_retrieved_docs(self, docs: list[RetrievalResult]) -> str:
        """格式化检索结果"""
        parts = []
        for i, doc in enumerate(docs, 1):
            parts.append(
                f"[参考资料{i}] (来源: {doc.source}, 相关度: {doc.score:.3f})\n{doc.content}"
            )
        return "\n\n".join(parts)
    
    def _trim_to_fit(self, messages: list[dict]) -> list[dict]:
        """确保消息不超过token预算(简化实现)"""
        # 简化:按字符估算token数(中文约1.5字/token)
        total_chars = sum(len(m["content"]) for m in messages)
        estimated_tokens = int(total_chars / 1.5)
        
        if estimated_tokens <= self.max_context_tokens:
            return messages
        
        # 从历史消息中裁剪
        while estimated_tokens > self.max_context_tokens and len(messages) > 3:
            # 移除最早的非系统消息
            for i in range(len(messages)):
                if messages[i]["role"] != "system":
                    removed = messages.pop(i)
                    estimated_tokens -= int(len(removed["content"]) / 1.5)
                    break
        
        return messages

5.4 流式生成与增量输出

import json
from typing import AsyncIterator

class StreamingGenerator:
    """流式生成器"""
    
    def __init__(self, llm_client):
        self.llm = llm_client
    
    async def generate_stream(
        self, messages: list[dict]
    ) -> AsyncIterator[str]:
        """流式生成回答"""
        async for chunk in self.llm.chat.completions.create(
            model="gpt-4o-mini",
            messages=messages,
            stream=True,
            temperature=0.7,
            max_tokens=2000,
        ):
            if chunk.choices and chunk.choices[0].delta.content:
                yield chunk.choices[0].delta.content
    
    async def generate_with_citation(
        self, messages: list[dict], 
        docs: list[RetrievalResult]
    ) -> AsyncIterator[dict]:
        """带引用标注的流式生成"""
        # 在系统提示中要求引用来源
        citation_prompt = """
请在回答中标注引用来源,格式为 [来源X]。引用格式示例:
根据[来源1]的内容,...,同时[来源2]指出...
"""
        messages.insert(1, {"role": "system", "content": citation_prompt})
        
        full_response = ""
        async for chunk in self.generate_stream(messages):
            full_response += chunk
            yield {
                "type": "token",
                "content": chunk,
                "full_text": full_response,
            }
        
        # 解析引用
        citations = self._extract_citations(full_response, docs)
        yield {
            "type": "citations",
            "citations": citations,
        }
    
    def _extract_citations(self, text: str, 
                           docs: list[RetrievalResult]) -> list[dict]:
        """提取引用标注"""
        import re
        citations = []
        pattern = r'\[来源(\d+)\]'
        for match in re.finditer(pattern, text):
            idx = int(match.group(1)) - 1
            if 0 <= idx < len(docs):
                citations.append({
                    "index": idx + 1,
                    "doc_id": docs[idx].doc_id,
                    "source": docs[idx].source,
                    "score": docs[idx].score,
                })
        return citations

5.5 RAG质量评估与优化

class RAGEvaluator:
    """RAG系统质量评估"""
    
    def evaluate_retrieval(
        self, 
        queries: list[str],
        ground_truth: list[list[str]],  # 每个query对应的相关doc_id列表
        retriever,
        top_k: int = 10,
    ) -> dict:
        """评估检索质量"""
        mrr_scores = []
        recall_scores = []
        
        for query, relevant_ids in zip(queries, ground_truth):
            results = retriever(query, top_k)
            retrieved_ids = [r.doc_id for r in results]
            
            # MRR (Mean Reciprocal Rank)
            for rank, doc_id in enumerate(retrieved_ids, 1):
                if doc_id in relevant_ids:
                    mrr_scores.append(1.0 / rank)
                    break
            else:
                mrr_scores.append(0.0)
            
            # Recall@K
            hits = len(set(retrieved_ids) & set(relevant_ids))
            recall_scores.append(hits / len(relevant_ids))
        
        return {
            "MRR": sum(mrr_scores) / len(mrr_scores),
            "Recall@K": sum(recall_scores) / len(recall_scores),
            "num_queries": len(queries),
        }
    
    def evaluate_generation(
        self, 
        predictions: list[str],
        references: list[str],
    ) -> dict:
        """评估生成质量(简化版)"""
        # 实际项目中应使用更专业的评估指标
        metrics = {
            "avg_response_length": sum(len(p) for p in predictions) / len(predictions),
            "num_responses": len(predictions),
        }
        
        # 可以集成ROUGE、BLEU或基于LLM的评估
        return metrics

第六章:WebSocket实时推理

6.1 WebSocket协议基础

WebSocket是一种在单个TCP连接上进行全双工通信的协议,特别适合实时AI应用:

  • 持久连接:避免HTTP反复建连的开销
  • 双向通信:客户端和服务端可随时发送消息
  • 低延迟:帧头仅2-14字节,远小于HTTP头
  • 适用场景:实时对话、流式推理、协同编辑

6.2 推理服务WebSocket接口设计

# pip install fastapi uvicorn websockets

from fastapi import FastAPI, WebSocket, WebSocketDisconnect
from fastapi.websockets import WebSocketState
import json
import asyncio
from typing import Optional

app = FastAPI()

class InferenceHandler:
    """推理处理器"""
    
    def __init__(self, rag_system):
        self.rag = rag_system
        self.active_sessions: dict[str, list[dict]] = {}
    
    async def handle_message(self, websocket: WebSocket, 
                              session_id: str, message: dict):
        """处理客户端消息"""
        msg_type = message.get("type", "")
        
        if msg_type == "query":
            await self._handle_query(websocket, session_id, message)
        elif msg_type == "context_update":
            await self._handle_context_update(session_id, message)
        elif msg_type == "stop":
            await self._handle_stop(session_id)
        elif msg_type == "ping":
            await websocket.send_json({"type": "pong"})
        else:
            await websocket.send_json({
                "type": "error",
                "content": f"未知消息类型: {msg_type}",
            })
    
    async def _handle_query(self, websocket: WebSocket, 
                             session_id: str, message: dict):
        """处理查询请求"""
        query = message.get("content", "")
        request_id = message.get("request_id", "")
        
        # 初始化会话历史
        if session_id not in self.active_sessions:
            self.active_sessions[session_id] = []
        
        try:
            # 发送开始信号
            await websocket.send_json({
                "type": "start",
                "request_id": request_id,
            })
            
            # 1. 检索
            await websocket.send_json({
                "type": "status",
                "content": "正在检索相关信息...",
            })
            
            docs = await self.rag.multi_source_retrieve(query)
            
            await websocket.send_json({
                "type": "status",
                "content": f"找到 {len(docs)} 条相关文档",
                "docs_count": len(docs),
            })
            
            # 2. 构建上下文
            context = self.rag.context_manager.build_context(
                query=query,
                retrieved_docs=docs,
                chat_history=self.active_sessions[session_id],
            )
            
            # 3. 流式生成
            full_response = ""
            async for chunk in self.rag.generator.generate_stream(context):
                full_response += chunk
                await websocket.send_json({
                    "type": "token",
                    "content": chunk,
                    "request_id": request_id,
                })
            
            # 4. 发送完成信号
            await websocket.send_json({
                "type": "done",
                "request_id": request_id,
                "full_response": full_response,
            })
            
            # 更新会话历史
            self.active_sessions[session_id].append(
                {"role": "user", "content": query}
            )
            self.active_sessions[session_id].append(
                {"role": "assistant", "content": full_response}
            )
            
        except Exception as e:
            await websocket.send_json({
                "type": "error",
                "content": str(e),
                "request_id": request_id,
            })
    
    async def _handle_context_update(self, session_id: str, 
                                      message: dict):
        """处理上下文更新"""
        if session_id in self.active_sessions:
            # 更新系统提示或上下文配置
            pass
    
    async def _handle_stop(self, session_id: str):
        """处理停止生成请求"""
        # 通过取消任务来停止生成
        pass

6.3 连接管理与负载均衡

from collections import defaultdict
import uuid

class ConnectionManager:
    """WebSocket连接管理器"""
    
    def __init__(self):
        # session_id -> websocket
        self.active_connections: dict[str, WebSocket] = {}
        # user_id -> [session_ids]
        self.user_sessions: dict[str, list[str]] = defaultdict(list)
        # 连接元数据
        self.connection_meta: dict[str, dict] = {}
    
    async def connect(self, websocket: WebSocket, 
                      user_id: str = None) -> str:
        """接受新连接"""
        await websocket.accept()
        session_id = str(uuid.uuid4())
        
        self.active_connections[session_id] = websocket
        self.connection_meta[session_id] = {
            "user_id": user_id,
            "connected_at": asyncio.get_event_loop().time(),
            "last_active": asyncio.get_event_loop().time(),
            "message_count": 0,
        }
        
        if user_id:
            self.user_sessions[user_id].append(session_id)
        
        return session_id
    
    async def disconnect(self, session_id: str):
        """断开连接"""
        if session_id in self.active_connections:
            meta = self.connection_meta.get(session_id, {})
            user_id = meta.get("user_id")
            
            del self.active_connections[session_id]
            if session_id in self.connection_meta:
                del self.connection_meta[session_id]
            
            if user_id and session_id in self.user_sessions.get(user_id, []):
                self.user_sessions[user_id].remove(session_id)
    
    async def send_to_session(self, session_id: str, message: dict):
        """向特定会话发送消息"""
        ws = self.active_connections.get(session_id)
        if ws and ws.client_state == WebSocketState.CONNECTED:
            try:
                await ws.send_json(message)
                if session_id in self.connection_meta:
                    self.connection_meta[session_id]["last_active"] = \
                        asyncio.get_event_loop().time()
                    self.connection_meta[session_id]["message_count"] += 1
            except Exception:
                await self.disconnect(session_id)
    
    async def broadcast(self, message: dict):
        """广播消息到所有连接"""
        disconnected = []
        for session_id, ws in self.active_connections.items():
            try:
                await ws.send_json(message)
            except Exception:
                disconnected.append(session_id)
        
        for sid in disconnected:
            await self.disconnect(sid)
    
    def get_stats(self) -> dict:
        """获取连接统计"""
        return {
            "total_connections": len(self.active_connections),
            "unique_users": len(self.user_sessions),
            "avg_messages": (
                sum(m["message_count"] for m in self.connection_meta.values()) 
                / max(len(self.connection_meta), 1)
            ),
        }

6.4 流式推理结果推送

class StreamingPusher:
    """流式推送管理器"""
    
    def __init__(self, connection_manager: ConnectionManager):
        self.conn_mgr = connection_manager
        self._cancel_flags: dict[str, asyncio.Event] = {}
    
    async def push_stream(
        self, session_id: str, request_id: str,
        token_iterator
    ):
        """推送流式推理结果"""
        cancel_event = asyncio.Event()
        self._cancel_flags[request_id] = cancel_event
        
        try:
            token_count = 0
            async for token_data in token_iterator:
                # 检查取消标志
                if cancel_event.is_set():
                    await self.conn_mgr.send_to_session(session_id, {
                        "type": "cancelled",
                        "request_id": request_id,
                    })
                    break
                
                # 推送token
                if isinstance(token_data, str):
                    await self.conn_mgr.send_to_session(session_id, {
                        "type": "token",
                        "content": token_data,
                        "request_id": request_id,
                        "index": token_count,
                    })
                else:
                    token_data["request_id"] = request_id
                    token_data["index"] = token_count
                    await self.conn_mgr.send_to_session(session_id, token_data)
                
                token_count += 1
                
                # 控制推送速率,避免客户端过载
                if token_count % 10 == 0:
                    await asyncio.sleep(0.01)  # 10ms间隔
            
            # 发送完成信号
            if not cancel_event.is_set():
                await self.conn_mgr.send_to_session(session_id, {
                    "type": "done",
                    "request_id": request_id,
                    "total_tokens": token_count,
                })
        finally:
            self._cancel_flags.pop(request_id, None)
    
    def cancel(self, request_id: str):
        """取消正在推送的流"""
        event = self._cancel_flags.get(request_id)
        if event:
            event.set()

6.5 错误处理与重连机制

# 客户端重连机制(JavaScript示例代码注释)
"""
// WebSocket客户端重连逻辑
class ReconnectingWebSocket {
    constructor(url, options = {}) {
        this.url = url;
        this.maxRetries = options.maxRetries || 10;
        this.baseDelay = options.baseDelay || 1000;
        this.maxDelay = options.maxDelay || 30000;
        this.retryCount = 0;
        this.connect();
    }
    
    connect() {
        this.ws = new WebSocket(this.url);
        
        this.ws.onopen = () => {
            console.log('WebSocket连接成功');
            this.retryCount = 0;
        };
        
        this.ws.onclose = (event) => {
            if (!event.wasClean && this.retryCount < this.maxRetries) {
                const delay = Math.min(
                    this.baseDelay * Math.pow(2, this.retryCount),
                    this.maxDelay
                );
                console.log(`将在${delay}ms后重连...`);
                setTimeout(() => this.connect(), delay);
                this.retryCount++;
            }
        };
        
        this.ws.onerror = (error) => {
            console.error('WebSocket错误:', error);
        };
    }
    
    send(data) {
        if (this.ws.readyState === WebSocket.OPEN) {
            this.ws.send(JSON.stringify(data));
        }
    }
}
"""

# 服务端错误处理
@app.websocket("/ws/inference/{session_id}")
async def websocket_endpoint(websocket: WebSocket, session_id: str):
    connection_mgr = ConnectionManager()  # 实际应为单例
    handler = InferenceHandler(rag_system)  # 实际应注入
    
    session_id = await connection_mgr.connect(websocket, session_id)
    
    try:
        while True:
            data = await websocket.receive_json()
            await handler.handle_message(websocket, session_id, data)
    except WebSocketDisconnect:
        await connection_mgr.disconnect(session_id)
    except json.JSONDecodeError:
        await websocket.send_json({
            "type": "error",
            "content": "无效的JSON格式",
        })
    except Exception as e:
        await websocket.send_json({
            "type": "error",
            "content": f"服务器内部错误: {str(e)}",
        })
        await connection_mgr.disconnect(session_id)

第七章:SSE服务端推送

7.1 SSE vs WebSocket选型

特性 SSE WebSocket
通信方向 服务端→客户端单向 双向
协议 HTTP/1.1或HTTP/2 独立协议(ws://)
数据格式 纯文本 文本或二进制
自动重连 浏览器原生支持 需手动实现
防火墙友好 是(标准HTTP) 可能被拦截
适用场景 AI流式输出、通知推送 实时对话、协同编辑

选择建议

  • AI流式输出(一问一答)→ SSE
  • 实时双向对话(支持中断、追问)→ WebSocket
  • 简单状态推送(模型训练进度)→ SSE
  • 复杂交互(多轮对话+上下文管理)→ WebSocket

7.2 SSE服务端实现

from fastapi import FastAPI, Request
from fastapi.responses import StreamingResponse
import asyncio
import json
import time

app = FastAPI()

async def sse_generator(query: str, session_id: str):
    """SSE事件生成器"""
    
    # 发送连接建立事件
    yield f"event: connected\ndata: {json.dumps({'session_id': session_id, 'timestamp': time.time()})}\n\n"
    
    try:
        # 阶段1:检索
        yield f"event: status\ndata: {json.dumps({'phase': 'retrieval', 'message': '正在检索...'})}\n\n"
        
        docs = await rag_system.multi_source_retrieve(query)
        
        yield f"event: status\ndata: {json.dumps({'phase': 'retrieval_done', 'docs_count': len(docs)})}\n\n"
        
        # 阶段2:流式生成
        yield f"event: status\ndata: {json.dumps({'phase': 'generation', 'message': '正在生成回答...'})}\n\n"
        
        context = rag_system.context_manager.build_context(
            query=query,
            retrieved_docs=docs,
            chat_history=session_store.get(session_id, []),
        )
        
        full_response = ""
        token_index = 0
        async for chunk in rag_system.generator.generate_stream(context):
            full_response += chunk
            token_index += 1
            
            yield f"event: token\ndata: {json.dumps({'content': chunk, 'index': token_index})}\n\n"
            
            # 心跳:每20个token发送一次,防止连接超时
            if token_index % 20 == 0:
                yield f"event: heartbeat\ndata: {json.dumps({'ts': time.time()})}\n\n"
        
        # 完成
        yield f"event: done\ndata: {json.dumps({'full_response': full_response, 'total_tokens': token_index})}\n\n"
        
    except Exception as e:
        yield f"event: error\ndata: {json.dumps({'error': str(e)})}\n\n"

@app.post("/api/chat/stream")
async def chat_stream(request: Request):
    """SSE流式聊天接口"""
    body = await request.json()
    query = body.get("query", "")
    session_id = body.get("session_id", str(uuid.uuid4()))
    
    return StreamingResponse(
        sse_generator(query, session_id),
        media_type="text/event-stream",
        headers={
            "Cache-Control": "no-cache",
            "Connection": "keep-alive",
            "X-Accel-Buffering": "no",  # Nginx禁用缓冲
        },
    )

7.3 客户端消费与错误恢复

# Python SSE客户端示例
import httpx
import json

class SSEClient:
    """SSE客户端"""
    
    def __init__(self, url: str, max_retries: int = 5):
        self.url = url
        self.max_retries = max_retries
        self._retry_count = 0
    
    async def consume(self, query: str, session_id: str,
                      on_token=None, on_done=None, on_error=None):
        """消费SSE流"""
        async with httpx.AsyncClient() as client:
            async with client.stream(
                "POST",
                self.url,
                json={"query": query, "session_id": session_id},
                headers={"Accept": "text/event-stream"},
                timeout=120.0,
            ) as response:
                event_type = None
                async for line in response.aiter_lines():
                    line = line.strip()
                    
                    if line.startswith("event:"):
                        event_type = line[6:].strip()
                    elif line.startswith("data:"):
                        data_str = line[5:].strip()
                        try:
                            data = json.loads(data_str)
                        except json.JSONDecodeError:
                            data = data_str
                        
                        if event_type == "token" and on_token:
                            await on_token(data)
                        elif event_type == "done" and on_done:
                            await on_done(data)
                        elif event_type == "error" and on_error:
                            await on_error(data)
// JavaScript SSE客户端
async function streamChat(query, sessionId) {
    const response = await fetch('/api/chat/stream', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ query, session_id: sessionId }),
    });
    
    const reader = response.body.getReader();
    const decoder = new TextDecoder();
    let buffer = '';
    
    while (true) {
        const { done, value } = await reader.read();
        if (done) break;
        
        buffer += decoder.decode(value, { stream: true });
        const lines = buffer.split('\n');
        buffer = lines.pop(); // 保留未完成的行
        
        let eventType = '';
        for (const line of lines) {
            if (line.startsWith('event:')) {
                eventType = line.slice(6).trim();
            } else if (line.startsWith('data:')) {
                const data = JSON.parse(line.slice(5).trim());
                handleEvent(eventType, data);
            }
        }
    }
}

function handleEvent(type, data) {
    switch (type) {
        case 'token':
            appendToOutput(data.content);
            break;
        case 'done':
            finalizeResponse(data);
            break;
        case 'error':
            showError(data.error);
            break;
        case 'status':
            updateStatus(data.message);
            break;
    }
}

7.4 SSE在AI流式输出中的应用

# 高级SSE功能:多事件流合并
async def enhanced_sse_generator(query: str, session_id: str):
    """增强版SSE生成器,支持进度、引用、token混合推送"""
    
    # 使用asyncio.Queue合并多个事件源
    event_queue = asyncio.Queue()
    
    async def retrieval_worker():
        """检索工作器"""
        docs = await rag_system.multi_source_retrieve(query)
        await event_queue.put(("docs", {"docs": [
            {"doc_id": d.doc_id, "score": d.score, "preview": d.content[:100]}
            for d in docs
        ]}))
        return docs
    
    async def generation_worker(docs):
        """生成工作器"""
        context = rag_system.context_manager.build_context(
            query=query, retrieved_docs=docs,
            chat_history=session_store.get(session_id, []),
        )
        async for chunk in rag_system.generator.generate_stream(context):
            await event_queue.put(("token", {"content": chunk}))
        await event_queue.put(("done", {}))
    
    # 启动检索任务
    docs = await retrieval_worker()
    # 启动生成任务
    gen_task = asyncio.create_task(generation_worker(docs))
    
    # 持续从队列中取出事件并推送
    while True:
        try:
            event_type, data = await asyncio.wait_for(
                event_queue.get(), timeout=60.0
            )
            yield f"event: {event_type}\ndata: {json.dumps(data)}\n\n"
            if event_type == "done":
                break
        except asyncio.TimeoutError:
            # 发送心跳
            yield f"event: heartbeat\ndata: {json.dumps({'ts': time.time()})}\n\n"

第八章:实时推荐引擎

8.1 推荐系统架构概述

实时推荐引擎的核心组成:

用户行为 → 行为收集服务 → 特征更新管线 → 向量索引更新
                                              ↓
用户请求 → 推荐服务 → 向量召回 → 精排 → 业务过滤 → 推荐结果
              ↑
         实时特征(用户画像、上下文)

关键设计原则:

  1. 双塔模型:用户塔和物品塔独立计算,物品向量离线预计算
  2. 实时更新:用户行为即时反映到用户向量
  3. 多路召回:向量召回 + 热门召回 + 协同过滤,融合排序
  4. AB实验:支持多策略并行对比

8.2 实时特征工程

from datetime import datetime
from collections import defaultdict
import numpy as np

class RealTimeFeatureStore:
    """实时特征存储"""
    
    def __init__(self, redis_client):
        self.redis = redis_client
    
    def record_user_action(self, user_id: str, item_id: str, 
                           action: str, timestamp: float = None):
        """记录用户行为并更新特征"""
        timestamp = timestamp or time.time()
        
        # 1. 写入行为日志(Redis Stream)
        self.redis.xadd(f"user_actions:{user_id}", {
            "item_id": item_id,
            "action": action,
            "timestamp": str(timestamp),
        }, maxlen=10000)
        
        # 2. 更新实时特征
        self._update_user_features(user_id, item_id, action, timestamp)
        self._update_item_features(item_id, action, timestamp)
    
    def _update_user_features(self, user_id: str, item_id: str, 
                               action: str, timestamp: float):
        """更新用户实时特征"""
        key = f"features:user:{user_id}"
        
        # 滑动窗口统计(最近1小时、24小时)
        pipe = self.redis.pipeline()
        
        # 最近行为列表(用于计算实时兴趣向量)
        pipe.lpush(f"user_recent:{user_id}", item_id)
        pipe.ltrim(f"user_recent:{user_id}", 0, 99)  # 保留最近100个
        
        # 行为计数
        pipe.hincrby(key, f"count:{action}", 1)
        pipe.hincrby(key, "count:total", 1)
        
        # 最后活跃时间
        pipe.hset(key, "last_active", str(timestamp))
        
        # 活跃时段统计
        hour = datetime.fromtimestamp(timestamp).hour
        pipe.hincrby(key, f"active_hour:{hour}", 1)
        
        pipe.execute()
    
    def _update_item_features(self, item_id: str, action: str, 
                               timestamp: float):
        """更新物品实时特征"""
        key = f"features:item:{item_id}"
        
        pipe = self.redis.pipeline()
        pipe.hincrby(key, f"count:{action}", 1)
        pipe.hincrby(key, "count:total", 1)
        pipe.hset(key, "last_interact", str(timestamp))
        
        # 近期热度分(时间衰减)
        decay_score = 1.0 / (1.0 + (time.time() - timestamp) / 3600)
        pipe.zincrby("item_hot_score", decay_score, item_id)
        
        pipe.execute()
    
    def get_user_realtime_vector(self, user_id: str) -> np.ndarray:
        """计算用户实时兴趣向量(基于最近行为加权平均)"""
        recent_items = self.redis.lrange(f"user_recent:{user_id}", 0, 49)
        
        if not recent_items:
            return np.zeros(128)  # 默认零向量
        
        # 获取这些物品的向量
        embeddings = []
        for item_id in recent_items:
            item_vec = self.redis.get(f"item_vector:{item_id}")
            if item_vec:
                embeddings.append(
                    np.frombuffer(bytes.fromhex(item_vec), dtype=np.float32)
                )
        
        if not embeddings:
            return np.zeros(128)
        
        # 时间加权平均:越近的行为权重越高
        weights = np.array([1.0 / (i + 1) for i in range(len(embeddings))])
        weights = weights / weights.sum()
        
        user_vector = np.average(embeddings, axis=0, weights=weights)
        return user_vector / np.linalg.norm(user_vector)  # L2归一化

8.3 向量召回与排序

class RealtimeRecommender:
    """实时推荐引擎"""
    
    def __init__(self, redis_client, feature_store: RealTimeFeatureStore):
        self.redis = redis_client
        self.feature_store = feature_store
    
    async def recommend(
        self, user_id: str, top_k: int = 20,
        candidate_pool_size: int = 200,
        context: dict = None,
    ) -> list[dict]:
        """生成实时推荐"""
        
        # 1. 获取用户实时向量
        user_vector = self.feature_store.get_user_realtime_vector(user_id)
        
        # 2. 多路召回
        candidates = {}
        
        # 路径1:向量召回
        vector_candidates = self._vector_recall(
            user_vector, candidate_pool_size
        )
        for item_id, score in vector_candidates:
            candidates[item_id] = {"vector_score": score}
        
        # 路径2:热门召回
        hot_candidates = self._hot_recall(50)
        for item_id, score in hot_candidates:
            if item_id not in candidates:
                candidates[item_id] = {"vector_score": 0}
            candidates[item_id]["hot_score"] = score
        
        # 路径3:协同过滤召回
        cf_candidates = self._collaborative_recall(user_id, 50)
        for item_id, score in cf_candidates:
            if item_id not in candidates:
                candidates[item_id] = {"vector_score": 0}
            candidates[item_id]["cf_score"] = score
        
        # 3. 精排
        ranked = self._rank_candidates(
            user_id, candidates, context
        )
        
        # 4. 业务过滤(去重、黑名单等)
        filtered = self._apply_filters(user_id, ranked)
        
        # 5. 返回Top-K
        return filtered[:top_k]
    
    def _vector_recall(self, user_vector: np.ndarray, 
                       top_k: int) -> list[tuple[str, float]]:
        """向量召回"""
        # 使用Redis向量搜索
        from redis.commands.search.query import Query
        
        query_vector = user_vector.astype(np.float32).tobytes()
        q = Query(f"*=>[KNN {top_k} @embedding $vec AS score]").dialect(2)
        q.return_fields("item_id", "score")
        q.sort_by("score", asc=True)
        
        results = self.redis.ft("idx:items").search(
            q, query_params={"vec": query_vector}
        )
        
        return [(r.item_id, float(r.score)) for r in results.docs]
    
    def _hot_recall(self, top_k: int) -> list[tuple[str, float]]:
        """热门召回"""
        items = self.redis.zrevrangebyscore(
            "item_hot_score", "+inf", "-inf",
            start=0, num=top_k, withscores=True,
        )
        return [(item.decode() if isinstance(item, bytes) else item, score) 
                for item, score in items]
    
    def _collaborative_recall(self, user_id: str, 
                               top_k: int) -> list[tuple[str, float]]:
        """协同过滤召回(简化:基于相似用户的行为)"""
        # 获取相似用户(预计算存储在Redis中)
        similar_users = self.redis.zrevrange(
            f"similar_users:{user_id}", 0, 9, withscores=True
        )
        
        item_scores = defaultdict(float)
        for sim_user_id, similarity in similar_users:
            recent_items = self.redis.lrange(
                f"user_recent:{sim_user_id}", 0, 19
            )
            for item_id in recent_items:
                item_id = item_id.decode() if isinstance(item_id, bytes) else item_id
                item_scores[item_id] += similarity
        
        sorted_items = sorted(
            item_scores.items(), key=lambda x: x[1], reverse=True
        )
        return sorted_items[:top_k]
    
    def _rank_candidates(self, user_id: str, candidates: dict,
                          context: dict = None) -> list[dict]:
        """精排打分"""
        ranked = []
        
        for item_id, scores in candidates.items():
            # 加权融合各路得分
            final_score = (
                scores.get("vector_score", 0) * 0.5 +
                scores.get("hot_score", 0) * 0.2 +
                scores.get("cf_score", 0) * 0.3
            )
            
            # 上下文加权
            if context:
                # 时间衰减
                if "time_decay" in context:
                    final_score *= context["time_decay"]
                # 多样性惩罚(与已选结果的相似度)
                if "diversity_penalty" in context:
                    final_score *= (1 - context["diversity_penalty"])
            
            ranked.append({
                "item_id": item_id,
                "score": final_score,
                "scores_breakdown": scores,
            })
        
        ranked.sort(key=lambda x: x["score"], reverse=True)
        return ranked
    
    def _apply_filters(self, user_id: str, 
                        candidates: list[dict]) -> list[dict]:
        """业务过滤"""
        # 获取用户已交互物品
        seen_items = set(
            self.redis.smembers(f"user_seen:{user_id")
        )
        seen_items = {
            x.decode() if isinstance(x, bytes) else x 
            for x in seen_items
        }
        
        filtered = [
            c for c in candidates 
            if c["item_id"] not in seen_items
        ]
        
        return filtered

8.4 实时兴趣更新

class InterestTracker:
    """用户实时兴趣追踪"""
    
    def __init__(self, redis_client, embedding_service):
        self.redis = redis_client
        self.embedder = embedding_service
    
    def update_interest_from_action(
        self, user_id: str, item_id: str, 
        item_content: str, action: str
    ):
        """根据用户行为更新兴趣向量"""
        
        # 获取物品Embedding
        item_embedding = self.embedder.encode_batch([item_content])[0]
        
        # 存储物品向量
        self.redis.set(
            f"item_vector:{item_id}",
            item_embedding.astype(np.float32).tobytes().hex(),
            ex=86400 * 7,  # 7天过期
        )
        
        # 根据行为类型加权更新用户兴趣
        action_weights = {
            "click": 1.0,
            "like": 2.0,
            "share": 3.0,
            "purchase": 4.0,
            "skip": -0.5,
            "dislike": -1.0,
        }
        
        weight = action_weights.get(action, 1.0)
        
        # 指数移动平均更新用户向量
        user_vector_key = f"user_vector:{user_id}"
        current = self.redis.get(user_vector_key)
        
        alpha = 0.1  # 学习率
        
        if current:
            current_vector = np.frombuffer(
                bytes.fromhex(current), dtype=np.float32
            )
            new_vector = (1 - alpha) * current_vector + alpha * weight * item_embedding
        else:
            new_vector = item_embedding * weight
        
        # L2归一化
        norm = np.linalg.norm(new_vector)
        if norm > 0:
            new_vector = new_vector / norm
        
        self.redis.set(
            user_vector_key,
            new_vector.astype(np.float32).tobytes().hex(),
            ex=86400 * 30,  # 30天过期
        )

8.5 推荐结果缓存与刷新

class RecommendationCache:
    """推荐结果缓存"""
    
    def __init__(self, redis_client, recommender: RealtimeRecommender):
        self.redis = redis_client
        self.recommender = recommender
        self.cache_ttl = 300  # 5分钟缓存
    
    async def get_recommendations(
        self, user_id: str, top_k: int = 20, 
        force_refresh: bool = False
    ) -> list[dict]:
        """获取推荐结果(带缓存)"""
        cache_key = f"rec_cache:{user_id}:{top_k}"
        
        # 尝试从缓存获取
        if not force_refresh:
            cached = self.redis.get(cache_key)
            if cached:
                return json.loads(cached)
        
        # 缓存未命中,重新生成
        results = await self.recommender.recommend(user_id, top_k)
        
        # 写入缓存
        self.redis.setex(
            cache_key, self.cache_ttl,
            json.dumps(results, ensure_ascii=False),
        )
        
        return results
    
    async def invalidate_user_cache(self, user_id: str):
        """用户行为变化时使缓存失效"""
        pattern = f"rec_cache:{user_id}:*"
        cursor = 0
        while True:
            cursor, keys = self.redis.scan(cursor, match=pattern, count=100)
            if keys:
                self.redis.delete(*keys)
            if cursor == 0:
                break

第九章:实时异常检测

9.1 异常检测算法概览

算法 类型 适用场景 实时性
Z-Score 统计 单指标阈值检测 极高
IQR 统计 离群值检测 极高
Isolation Forest ML 多维特征异常
LSTM Autoencoder DL 时序模式异常
DBSCAN 聚类 密度异常

9.2 流式数据异常检测

import numpy as np
from collections import deque
from dataclasses import dataclass

@dataclass
class AnomalyResult:
    is_anomaly: bool
    score: float
    threshold: float
    metric_name: str
    timestamp: float
    details: dict = None

class StreamingAnomalyDetector:
    """流式异常检测器"""
    
    def __init__(self, window_size: int = 1000, 
                 z_threshold: float = 3.0):
        self.window_size = window_size
        self.z_threshold = z_threshold
        self.windows: dict[str, deque] = {}
        self.stats: dict[str, dict] = {}
    
    def detect(self, metric_name: str, value: float, 
               timestamp: float = None) -> AnomalyResult:
        """检测单个数据点是否异常"""
        timestamp = timestamp or time.time()
        
        if metric_name not in self.windows:
            self.windows[metric_name] = deque(maxlen=self.window_size)
            self.stats[metric_name] = {"mean": 0, "std": 0, "count": 0}
        
        window = self.windows[metric_name]
        stats = self.stats[metric_name]
        
        # 如果窗口未满,先积累数据
        if len(window) < 30:
            window.append(value)
            self._update_stats(metric_name)
            return AnomalyResult(
                is_anomaly=False, score=0, threshold=self.z_threshold,
                metric_name=metric_name, timestamp=timestamp,
            )
        
        # 计算Z-Score
        mean = stats["mean"]
        std = max(stats["std"], 1e-10)  # 防止除零
        z_score = abs(value - mean) / std
        
        is_anomaly = z_score > self.z_threshold
        
        # 更新窗口(即使异常也加入,但可选择排除)
        window.append(value)
        self._update_stats(metric_name)
        
        return AnomalyResult(
            is_anomaly=is_anomaly,
            score=z_score,
            threshold=self.z_threshold,
            metric_name=metric_name,
            timestamp=timestamp,
            details={
                "value": value,
                "mean": mean,
                "std": std,
                "window_size": len(window),
            },
        )
    
    def _update_stats(self, metric_name: str):
        """更新统计量"""
        window = self.windows[metric_name]
        values = list(window)
        self.stats[metric_name] = {
            "mean": np.mean(values),
            "std": np.std(values),
            "count": len(values),
        }

class IsolationForestDetector:
    """基于Isolation Forest的多维异常检测"""
    
    def __init__(self, contamination: float = 0.05,
                 n_estimators: int = 100):
        from sklearn.ensemble import IsolationForest
        self.model = IsolationForest(
            contamination=contamination,
            n_estimators=n_estimators,
            random_state=42,
        )
        self.is_fitted = False
        self.buffer = []
        self.buffer_size = 1000
    
    def add_sample(self, features: np.ndarray):
        """添加样本到缓冲区"""
        self.buffer.append(features)
        
        # 缓冲区满时重新训练
        if len(self.buffer) >= self.buffer_size:
            self._retrain()
    
    def _retrain(self):
        """重新训练模型"""
        X = np.array(self.buffer)
        self.model.fit(X)
        self.is_fitted = True
        # 保留一半旧数据 + 新数据
        self.buffer = self.buffer[len(self.buffer) // 2:]
    
    def detect(self, features: np.ndarray) -> AnomalyResult:
        """检测多维特征是否异常"""
        if not self.is_fitted:
            return AnomalyResult(
                is_anomaly=False, score=0, threshold=0,
                metric_name="multivariate", timestamp=time.time(),
            )
        
        score = self.model.decision_function(features.reshape(1, -1))[0]
        prediction = self.model.predict(features.reshape(1, -1))[0]
        
        return AnomalyResult(
            is_anomaly=prediction == -1,
            score=-score,  # 越小越异常,取反
            threshold=0,
            metric_name="multivariate",
            timestamp=time.time(),
            details={"raw_score": score},
        )

9.3 时序异常检测实战

class TimeSeriesAnomalyDetector:
    """时序异常检测器"""
    
    def __init__(self, seasonality: int = 24):
        self.seasonality = seasonality
        self.history: dict[str, list[float]] = {}
        self.seasonal_pattern: dict[str, np.ndarray] = {}
    
    def detect_with_seasonality(
        self, metric_name: str, value: float, 
        timestamp: float
    ) -> AnomalyResult:
        """考虑季节性的异常检测"""
        if metric_name not in self.history:
            self.history[metric_name] = []
        
        self.history[metric_name].append(value)
        history = self.history[metric_name]
        
        # 至少需要2个完整周期的数据
        if len(history) < self.seasonality * 2:
            return AnomalyResult(
                is_anomaly=False, score=0, threshold=3.0,
                metric_name=metric_name, timestamp=timestamp,
            )
        
        # 计算季节性模式
        recent = history[-self.seasonality * 2:]
        pattern = np.array(recent[:self.seasonality])
        
        # 去季节性后的残差
        expected = pattern[len(history) % self.seasonality]
        residual = value - expected
        
        # 基于残差的Z-Score
        residuals = [
            history[i] - pattern[i % self.seasonality]
            for i in range(self.seasonality, len(history))
        ]
        
        if len(residuals) < 10:
            return AnomalyResult(
                is_anomaly=False, score=0, threshold=3.0,
                metric_name=metric_name, timestamp=timestamp,
            )
        
        mean_r = np.mean(residuals)
        std_r = max(np.std(residuals), 1e-10)
        z_score = abs(residual - mean_r) / std_r
        
        return AnomalyResult(
            is_anomaly=z_score > 3.0,
            score=z_score,
            threshold=3.0,
            metric_name=metric_name,
            timestamp=timestamp,
            details={
                "value": value,
                "expected": expected,
                "residual": residual,
                "seasonal_index": len(history) % self.seasonality,
            },
        )

9.4 告警系统设计

from enum import Enum
from dataclasses import dataclass, field
import asyncio

class AlertLevel(Enum):
    INFO = "info"
    WARNING = "warning"
    CRITICAL = "critical"

@dataclass
class Alert:
    level: AlertLevel
    metric_name: str
    message: str
    value: float
    threshold: float
    timestamp: float
    resolved: bool = False
    alert_id: str = ""

class AlertManager:
    """告警管理器"""
    
    def __init__(self, redis_client, notification_channels: list = None):
        self.redis = redis_client
        self.channels = notification_channels or []
        self.alert_cooldown: dict[str, float] = {}  # 告警冷却
        self.cooldown_seconds = 300  # 5分钟冷却
    
    async def process_anomaly(self, result: AnomalyResult):
        """处理异常检测结果"""
        if not result.is_anomaly:
            # 检查是否有未恢复的告警需要恢复
            await self._check_recovery(result.metric_name)
            return
        
        # 检查冷却期
        alert_key = f"{result.metric_name}:{result.timestamp // self.cooldown_seconds}"
        if alert_key in self.alert_cooldown:
            return
        
        # 确定告警级别
        level = self._determine_level(result)
        
        # 创建告警
        alert = Alert(
            level=level,
            metric_name=result.metric_name,
            message=self._format_message(result),
            value=result.details.get("value", 0) if result.details else 0,
            threshold=result.threshold,
            timestamp=result.timestamp,
            alert_id=str(uuid.uuid4()),
        )
        
        # 存储告警
        self.redis.xadd("alerts:stream", {
            "alert_id": alert.alert_id,
            "level": alert.level.value,
            "metric": alert.metric_name,
            "message": alert.message,
            "timestamp": str(alert.timestamp),
        }, maxlen=10000)
        
        # 设置冷却
        self.alert_cooldown[alert_key] = time.time()
        
        # 发送通知
        await self._send_notifications(alert)
    
    def _determine_level(self, result: AnomalyResult) -> AlertLevel:
        """根据异常程度确定告警级别"""
        if result.score > result.threshold * 2:
            return AlertLevel.CRITICAL
        elif result.score > result.threshold * 1.5:
            return AlertLevel.WARNING
        else:
            return AlertLevel.INFO
    
    def _format_message(self, result: AnomalyResult) -> str:
        """格式化告警消息"""
        details = result.details or {}
        return (
            f"指标 [{result.metric_name}] 异常: "
            f"当前值={details.get('value', 'N/A')}, "
            f"均值={details.get('mean', 'N/A'):.2f}, "
            f"Z-Score={result.score:.2f}"
        )
    
    async def _send_notifications(self, alert: Alert):
        """发送告警通知"""
        for channel in self.channels:
            try:
                await channel.send(alert)
            except Exception as e:
                print(f"告警发送失败: {e}")
    
    async def _check_recovery(self, metric_name: str):
        """检查告警恢复"""
        # 查找未恢复的告警
        alerts = self.redis.xrevrange("alerts:stream", count=100)
        for alert_id, fields in alerts:
            if (fields.get(b"metric", b"").decode() == metric_name and
                fields.get(b"level", b"").decode() != "recovered"):
                # 发送恢复通知
                recovery = Alert(
                    level=AlertLevel.INFO,
                    metric_name=metric_name,
                    message=f"指标 [{metric_name}] 已恢复正常",
                    value=0, threshold=0,
                    timestamp=time.time(),
                    resolved=True,
                )
                await self._send_notifications(recovery)
                break

第十章:边缘计算推理

10.1 边缘推理概述

边缘计算推理将AI模型部署在靠近数据源的设备上,而非云端数据中心:

优势

  • 极低延迟(<10ms)
  • 数据不出设备,保护隐私
  • 离线可用
  • 减少带宽成本

适用场景

  • 移动端实时翻译
  • IoT设备异常检测
  • 车载自动驾驶推理
  • 工业质检实时判断

挑战

  • 计算资源受限(CPU/内存/功耗)
  • 模型需要压缩和优化
  • 模型更新和同步

10.2 模型量化与压缩

# 模型量化:将FP32权重转为INT8,减少75%内存,加速推理

# 方法1:使用ONNX Runtime量化
import onnxruntime as ort
from onnxruntime.quantization import quantize_dynamic, QuantType

def quantize_model(input_path: str, output_path: str):
    """动态量化ONNX模型"""
    quantize_dynamic(
        model_input=input_path,
        model_output=output_path,
        weight_type=QuantType.QUInt8,
    )
    print(f"量化完成: {input_path} → {output_path}")

# 方法2:PyTorch量化
import torch

def pytorch_quantize(model, calibration_data=None):
    """PyTorch动态量化"""
    quantized_model = torch.quantization.quantize_dynamic(
        model,
        {torch.nn.Linear, torch.nn.Conv2d},
        dtype=torch.qint8,
    )
    return quantized_model

# 方法3:模型剪枝
def prune_model(model, sparsity: float = 0.5):
    """结构化剪枝"""
    import torch.nn.utils.prune as prune
    
    for name, module in model.named_modules():
        if isinstance(module, torch.nn.Linear):
            prune.l1_unstructured(module, name='weight', amount=sparsity)
            prune.remove(module, 'weight')
    
    return model

# 模型大小对比
def compare_model_sizes(original_path: str, quantized_path: str):
    """对比模型大小"""
    import os
    
    original_size = os.path.getsize(original_path) / 1024 / 1024
    quantized_size = os.path.getsize(quantized_path) / 1024 / 1024
    
    print(f"原始模型: {original_size:.1f} MB")
    print(f"量化模型: {quantized_size:.1f} MB")
    print(f"压缩比: {original_size / quantized_size:.2f}x")

10.3 ONNX Runtime部署

import onnxruntime as ort
import numpy as np

class OnnxInferenceEngine:
    """ONNX Runtime推理引擎"""
    
    def __init__(self, model_path: str, use_gpu: bool = False):
        providers = ['CPUExecutionProvider']
        if use_gpu and 'CUDAExecutionProvider' in ort.get_available_providers():
            providers.insert(0, 'CUDAExecutionProvider')
        
        # 会话选项优化
        sess_options = ort.SessionOptions()
        sess_options.graph_optimization_level = (
            ort.GraphOptimizationLevel.ORT_ENABLE_ALL
        )
        sess_options.intra_op_num_threads = 4
        sess_options.inter_op_num_threads = 2
        
        # 执行模式
        sess_options.execution_mode = ort.ExecutionMode.ORT_PARALLEL
        
        self.session = ort.InferenceSession(
            model_path, sess_options, providers=providers,
        )
        
        # 获取输入输出信息
        self.input_names = [inp.name for inp in self.session.get_inputs()]
        self.output_names = [out.name for out in self.session.get_outputs()]
    
    def infer(self, inputs: dict[str, np.ndarray]) -> dict[str, np.ndarray]:
        """执行推理"""
        # 确保输入数据类型正确
        prepared = {}
        for name in self.input_names:
            if name in inputs:
                data = inputs[name]
                # 获取模型期望的数据类型
                expected_type = self.session.get_inputs()[0].type
                if 'float32' in expected_type:
                    data = data.astype(np.float32)
                elif 'int64' in expected_type:
                    data = data.astype(np.int64)
                prepared[name] = data
        
        results = self.session.run(self.output_names, prepared)
        return dict(zip(self.output_names, results))
    
    def benchmark(self, input_data: dict[str, np.ndarray], 
                  num_iterations: int = 100) -> dict:
        """性能基准测试"""
        import time
        
        # 预热
        for _ in range(10):
            self.infer(input_data)
        
        # 测试
        latencies = []
        for _ in range(num_iterations):
            start = time.perf_counter()
            self.infer(input_data)
            latencies.append((time.perf_counter() - start) * 1000)
        
        return {
            "avg_ms": np.mean(latencies),
            "p50_ms": np.percentile(latencies, 50),
            "p95_ms": np.percentile(latencies, 95),
            "p99_ms": np.percentile(latencies, 99),
            "min_ms": np.min(latencies),
            "max_ms": np.max(latencies),
            "throughput_rps": 1000 / np.mean(latencies),
        }

10.4 边缘-云协同架构

class EdgeCloudOrchestrator:
    """边缘-云协同推理编排器"""
    
    def __init__(self, edge_engine: OnnxInferenceEngine, 
                 cloud_endpoint: str):
        self.edge = edge_engine
        self.cloud_endpoint = cloud_endpoint
        self.fallback_threshold = 0.5  # 置信度阈值
    
    async def infer(self, input_data: dict) -> dict:
        """协同推理:边缘优先,复杂场景上云"""
        
        # 1. 边缘推理
        edge_result = self.edge.infer(input_data)
        confidence = self._get_confidence(edge_result)
        
        # 2. 高置信度直接返回
        if confidence > self.fallback_threshold:
            return {
                "result": edge_result,
                "source": "edge",
                "confidence": confidence,
            }
        
        # 3. 低置信度上云推理
        cloud_result = await self._cloud_infer(input_data)
        
        return {
            "result": cloud_result,
            "source": "cloud",
            "confidence": self._get_confidence(cloud_result),
            "edge_confidence": confidence,
        }
    
    def _get_confidence(self, result: dict) -> float:
        """从推理结果中提取置信度"""
        # 假设输出包含logits,通过softmax计算置信度
        if "logits" in result:
            logits = result["logits"]
            probs = np.exp(logits) / np.exp(logits).sum()
            return float(np.max(probs))
        return 1.0
    
    async def _cloud_infer(self, input_data: dict) -> dict:
        """调用云端推理服务"""
        import httpx
        
        async with httpx.AsyncClient(timeout=5.0) as client:
            # 序列化numpy数组
            serialized = {
                k: v.tolist() for k, v in input_data.items()
            }
            response = await client.post(
                f"{self.cloud_endpoint}/infer",
                json=serialized,
            )
            return response.json()

# 模型同步管理
class ModelSyncManager:
    """边缘模型同步管理"""
    
    def __init__(self, redis_client, model_dir: str = "./models"):
        self.redis = redis_client
        self.model_dir = model_dir
    
    async def check_for_update(self, device_id: str, 
                                 current_version: str) -> dict:
        """检查是否有模型更新"""
        latest = self.redis.hgetall("model:latest")
        
        if not latest:
            return {"has_update": False}
        
        latest_version = latest.get(b"version", b"").decode()
        
        if latest_version != current_version:
            return {
                "has_update": True,
                "version": latest_version,
                "download_url": latest.get(b"url", b"").decode(),
                "checksum": latest.get(b"checksum", b"").decode(),
            }
        
        return {"has_update": False}
    
    async def report_device_status(self, device_id: str, 
                                     status: dict):
        """上报设备状态"""
        self.redis.hset(f"device:{device_id}", mapping={
            "model_version": status.get("model_version", ""),
            "last_inference": str(time.time()),
            "avg_latency_ms": str(status.get("avg_latency_ms", 0)),
            "memory_usage_mb": str(status.get("memory_usage_mb", 0)),
        })
        self.redis.expire(f"device:{device_id}", 86400)

第十一章:实战项目一——实时智能客服系统

11.1 需求分析与架构设计

功能需求

  1. 用户发起咨询,系统实时回复
  2. 基于知识库的问答(RAG)
  3. 支持多轮对话,上下文理解
  4. 无法回答时转人工
  5. 对话质量监控

架构设计

┌─────────────┐     ┌──────────────┐     ┌─────────────────┐
│   前端应用   │────→│  API Gateway │────→│  WebSocket服务   │
│  (React/Vue)│←────│  (Nginx)     │←────│  (FastAPI)       │
└─────────────┘     └──────────────┘     └────────┬────────┘
                                                   │
                    ┌──────────────────────────────┼──────────────┐
                    │                              │              │
                    ▼                              ▼              ▼
           ┌───────────────┐            ┌──────────────┐  ┌──────────────┐
           │  对话管理服务   │            │  RAG检索服务  │  │  LLM生成服务  │
           │  (Session Mgr) │            │  (Retriever) │  │  (Generator) │
           └───────┬───────┘            └──────┬───────┘  └──────────────┘
                   │                           │
                   ▼                           ▼
           ┌───────────────┐            ┌──────────────┐
           │   Redis        │            │  向量数据库   │
           │  (会话缓存)     │            │ (Redis/PgVec) │
           └───────────────┘            └──────────────┘

11.2 知识库构建与索引

import hashlib
from pathlib import Path

class KnowledgeBase:
    """客服知识库管理"""
    
    def __init__(self, redis_client, pg_conn, embedding_service):
        self.redis = redis_client
        self.pg = pg_conn
        self.embedder = embedding_service
    
    def add_document(self, doc_id: str, content: str, 
                     category: str, metadata: dict = None):
        """添加文档到知识库"""
        # 文档分块
        chunks = self._split_document(content, chunk_size=500, overlap=50)
        
        for i, chunk in enumerate(chunks):
            chunk_id = f"{doc_id}_chunk_{i}"
            
            # 生成Embedding
            embedding = self.embedder.encode_batch([chunk])[0]
            
            # 写入Redis
            self.redis.hset(f"kb:{chunk_id}", mapping={
                "doc_id": doc_id,
                "chunk_id": chunk_id,
                "content": chunk,
                "category": category,
                "embedding": embedding.astype(np.float32).tobytes(),
                "metadata": json.dumps(metadata or {}),
            })
            
            # 写入PostgreSQL(持久化)
            with self.pg.cursor() as cur:
                cur.execute("""
                    INSERT INTO knowledge_base 
                    (doc_id, chunk_id, content, category, embedding, metadata)
                    VALUES (%s, %s, %s, %s, %s, %s)
                    ON CONFLICT (chunk_id) DO UPDATE SET
                        content = EXCLUDED.content,
                        embedding = EXCLUDED.embedding,
                        category = EXCLUDED.category
                """, (
                    doc_id, chunk_id, chunk, category,
                    embedding.astype(np.float32),
                    json.dumps(metadata or {}),
                ))
            self.pg.commit()
    
    def _split_document(self, text: str, chunk_size: int = 500,
                        overlap: int = 50) -> list[str]:
        """智能分块:按段落和句子分割"""
        # 先按段落分割
        paragraphs = text.split("\n\n")
        
        chunks = []
        current_chunk = ""
        
        for para in paragraphs:
            para = para.strip()
            if not para:
                continue
            
            if len(current_chunk) + len(para) <= chunk_size:
                current_chunk += "\n\n" + para if current_chunk else para
            else:
                if current_chunk:
                    chunks.append(current_chunk)
                # 长段落需要进一步分割
                if len(para) > chunk_size:
                    sentences = para.replace("。", "。\n").split("\n")
                    current_chunk = ""
                    for sent in sentences:
                        if len(current_chunk) + len(sent) <= chunk_size:
                            current_chunk += sent
                        else:
                            if current_chunk:
                                chunks.append(current_chunk)
                            current_chunk = sent
                else:
                    current_chunk = para
        
        if current_chunk:
            chunks.append(current_chunk)
        
        # 添加重叠
        if overlap > 0 and len(chunks) > 1:
            overlapped = [chunks[0]]
            for i in range(1, len(chunks)):
                prev_tail = chunks[i-1][-overlap:]
                overlapped.append(prev_tail + chunks[i])
            chunks = overlapped
        
        return chunks
    
    def search(self, query: str, top_k: int = 5, 
               category: str = None) -> list[dict]:
        """知识库检索"""
        query_embedding = self.embedder.encode_batch([query])[0]
        
        # Redis快速检索
        from redis.commands.search.query import Query
        
        query_vector = query_embedding.astype(np.float32).tobytes()
        filter_str = f"@category=={{{category}}} " if category else ""
        knn_query = f"{filter_str}=>[KNN {top_k} @embedding $vec AS score]"
        
        q = Query(knn_query).dialect(2)
        q.return_fields("doc_id", "chunk_id", "content", "category", "score")
        q.sort_by("score", asc=True)
        
        results = self.redis.ft("idx:knowledge_base").search(
            q, query_params={"vec": query_vector}
        )
        
        return [
            {
                "doc_id": r.doc_id,
                "chunk_id": r.chunk_id,
                "content": r.content,
                "category": r.category,
                "score": float(r.score),
            }
            for r in results.docs
        ]

11.3 实时对话引擎

class CustomerServiceEngine:
    """实时智能客服引擎"""
    
    def __init__(self, knowledge_base: KnowledgeBase, 
                 llm_client, redis_client):
        self.kb = knowledge_base
        self.llm = llm_client
        self.redis = redis_client
        self.context_manager = ContextManager(
            max_context_tokens=3000, max_history_turns=5
        )
        
        self.system_prompt = """你是一个专业的客服助手。请遵循以下规则:
1. 基于提供的知识库内容回答问题
2. 如果知识库中没有相关信息,请诚实告知并建议转人工
3. 回答要简洁、专业、友好
4. 如果用户情绪激动,请先安抚再解答
5. 引用知识库内容时请注明来源"""
    
    async def handle_message(
        self, session_id: str, user_message: str
    ) -> AsyncIterator[str]:
        """处理用户消息,流式返回"""
        
        # 1. 意图识别(简化版)
        intent = await self._classify_intent(user_message)
        
        if intent == "transfer_human":
            yield "正在为您转接人工客服,请稍候...\n"
            await self._notify_human_agent(session_id)
            return
        
        # 2. 知识库检索
        docs = self.kb.search(user_message, top_k=3)
        
        # 3. 构建上下文
        history = self._get_session_history(session_id)
        
        context = self.context_manager.build_context(
            query=user_message,
            retrieved_docs=[
                RetrievalResult(
                    d["doc_id"], d["content"], d["score"], "knowledge_base"
                )
                for d in docs
            ],
            chat_history=history,
            system_prompt=self.system_prompt,
        )
        
        # 4. 流式生成
        full_response = ""
        async for chunk in self._generate_stream(context):
            full_response += chunk
            yield chunk
        
        # 5. 保存对话历史
        self._save_to_history(session_id, user_message, full_response)
        
        # 6. 记录对话质量指标
        self._record_metrics(session_id, user_message, full_response, docs)
    
    async def _classify_intent(self, message: str) -> str:
        """意图分类(简化版)"""
        transfer_keywords = ["转人工", "人工客服", "找人", "投诉"]
        if any(kw in message for kw in transfer_keywords):
            return "transfer_human"
        return "qa"
    
    async def _generate_stream(self, context: list[dict]) -> AsyncIterator[str]:
        """流式生成回答"""
        async for chunk in self.llm.chat.completions.create(
            model="gpt-4o-mini",
            messages=context,
            stream=True,
            temperature=0.7,
            max_tokens=1000,
        ):
            if chunk.choices and chunk.choices[0].delta.content:
                yield chunk.choices[0].delta.content
    
    def _get_session_history(self, session_id: str) -> list[dict]:
        """获取会话历史"""
        history_json = self.redis.get(f"session:{session_id}:history")
        if history_json:
            return json.loads(history_json)
        return []
    
    def _save_to_history(self, session_id: str, user_msg: str, 
                          assistant_msg: str):
        """保存对话历史"""
        history = self._get_session_history(session_id)
        history.append({"role": "user", "content": user_msg})
        history.append({"role": "assistant", "content": assistant_msg})
        
        # 保留最近10轮对话
        history = history[-20:]
        
        self.redis.setex(
            f"session:{session_id}:history",
            3600,  # 1小时过期
            json.dumps(history, ensure_ascii=False),
        )
    
    async def _notify_human_agent(self, session_id: str):
        """通知人工客服"""
        self.redis.xadd("transfer_queue", {
            "session_id": session_id,
            "timestamp": str(time.time()),
        })
    
    def _record_metrics(self, session_id: str, query: str, 
                         response: str, docs: list[dict]):
        """记录对话指标"""
        self.redis.xadd("metrics:conversations", {
            "session_id": session_id,
            "query_length": str(len(query)),
            "response_length": str(len(response)),
            "docs_count": str(len(docs)),
            "top_score": str(docs[0]["score"]) if docs else "0",
            "timestamp": str(time.time()),
        }, maxlen=100000)

11.4 多轮对话状态管理

class ConversationStateManager:
    """多轮对话状态管理"""
    
    def __init__(self, redis_client):
        self.redis = redis_client
    
    def create_session(self, user_id: str, metadata: dict = None) -> str:
        """创建新会话"""
        session_id = str(uuid.uuid4())
        
        self.redis.hset(f"conv:{session_id}", mapping={
            "user_id": user_id,
            "status": "active",
            "created_at": str(time.time()),
            "turn_count": "0",
            "metadata": json.dumps(metadata or {}),
        })
        self.redis.expire(f"conv:{session_id}", 7200)  # 2小时
        
        return session_id
    
    def get_session(self, session_id: str) -> dict:
        """获取会话信息"""
        data = self.redis.hgetall(f"conv:{session_id}")
        if not data:
            return None
        
        return {
            k.decode() if isinstance(k, bytes) else k: 
            v.decode() if isinstance(v, bytes) else v
            for k, v in data.items()
        }
    
    def update_turn(self, session_id: str, user_msg: str, 
                     bot_msg: str, metadata: dict = None):
        """更新对话轮次"""
        pipe = self.redis.pipeline()
        
        # 递增轮次计数
        pipe.hincrby(f"conv:{session_id}", "turn_count", 1)
        pipe.hset(f"conv:{session_id}", "last_active", str(time.time()))
        
        # 追加到对话日志
        log_entry = {
            "turn": self.redis.hget(f"conv:{session_id}", "turn_count"),
            "user": user_msg,
            "bot": bot_msg,
            "timestamp": str(time.time()),
        }
        if metadata:
            log_entry["metadata"] = json.dumps(metadata)
        
        pipe.xadd(f"conv_log:{session_id}", log_entry, maxlen=100)
        
        pipe.execute()
    
    def get_conversation_summary(self, session_id: str) -> str:
        """生成对话摘要(用于长对话上下文压缩)"""
        logs = self.redis.xrange(f"conv_log:{session_id}", count=20)
        
        summary_parts = []
        for _, fields in logs:
            user_msg = fields.get(b"user", b"").decode()
            bot_msg = fields.get(b"bot", b"").decode()
            summary_parts.append(f"用户: {user_msg}\n助手: {bot_msg}")
        
        return "\n".join(summary_parts)

11.5 完整部署方案

# docker-compose.yml
version: '3.8'

services:
  # Redis Stack(向量搜索 + 会话缓存)
  redis:
    image: redis/redis-stack:latest
    ports:
      - "6379:6379"
      - "8001:8001"
    volumes:
      - redis_data:/data
    command: redis-server --requirepass ${REDIS_PASSWORD} --maxmemory 2gb --maxmemory-policy allkeys-lru

  # PostgreSQL + pgvector
  postgres:
    image: pgvector/pgvector:pg16
    ports:
      - "5432:5432"
    environment:
      POSTGRES_DB: customer_service
      POSTGRES_USER: ${PG_USER}
      POSTGRES_PASSWORD: ${PG_PASSWORD}
    volumes:
      - pg_data:/var/lib/postgresql/data
      - ./init.sql:/docker-entrypoint-initdb.d/init.sql

  # FastAPI应用
  app:
    build: .
    ports:
      - "8000:8000"
    environment:
      REDIS_URL: redis://:${REDIS_PASSWORD}@redis:6379
      DATABASE_URL: postgresql://${PG_USER}:${PG_PASSWORD}@postgres:5432/customer_service
    depends_on:
      - redis
      - postgres
    deploy:
      replicas: 2
      resources:
        limits:
          memory: 4G
          cpus: '2'

  # Nginx反向代理
  nginx:
    image: nginx:latest
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf
    depends_on:
      - app

volumes:
  redis_data:
  pg_data:
# main.py - 完整应用入口
from fastapi import FastAPI, WebSocket, WebSocketDisconnect, Request
from fastapi.responses import StreamingResponse
import os

app = FastAPI(title="实时智能客服系统")

# 初始化组件(实际应使用依赖注入)
redis_client = redis.Redis(
    host=os.getenv("REDIS_HOST", "localhost"),
    port=int(os.getenv("REDIS_PORT", 6379)),
    password=os.getenv("REDIS_PASSWORD"),
    decode_responses=True,
)

# 创建向量索引
# ... (参考第二章索引创建代码)

@app.websocket("/ws/chat/{session_id}")
async def chat_websocket(websocket: WebSocket, session_id: str):
    await websocket.accept()
    
    engine = CustomerServiceEngine(knowledge_base, llm_client, redis_client)
    state_mgr = ConversationStateManager(redis_client)
    
    try:
        while True:
            data = await websocket.receive_json()
            user_message = data.get("message", "")
            
            # 流式推送回复
            full_response = ""
            async for chunk in engine.handle_message(session_id, user_message):
                full_response += chunk
                await websocket.send_json({
                    "type": "token",
                    "content": chunk,
                })
            
            # 更新状态
            state_mgr.update_turn(session_id, user_message, full_response)
            
            await websocket.send_json({"type": "done"})
            
    except WebSocketDisconnect:
        pass

@app.post("/api/chat/stream")
async def chat_sse(request: Request):
    body = await request.json()
    session_id = body.get("session_id", str(uuid.uuid4()))
    query = body.get("query", "")
    
    engine = CustomerServiceEngine(knowledge_base, llm_client, redis_client)
    
    async def generate():
        async for chunk in engine.handle_message(session_id, query):
            yield f"event: token\ndata: {json.dumps({'content': chunk})}\n\n"
        yield f"event: done\ndata: {{}}\n\n"
    
    return StreamingResponse(generate(), media_type="text/event-stream")

第十二章:实战项目二——流式数据分析平台

12.1 平台架构设计

功能需求

  1. 实时数据采集与预处理
  2. 流式异常检测
  3. 实时聚合分析
  4. 动态阈值告警
  5. 可视化仪表盘

技术架构

数据源(日志/指标/事件)
        │
        ▼
┌──────────────────┐
│   数据采集层      │
│  (Redis Streams)  │
└────────┬─────────┘
         │
    ┌────┴────┐
    ▼         ▼
┌────────┐ ┌────────────┐
│实时聚合│ │  异常检测   │
│ 引擎   │ │   引擎     │
└───┬────┘ └─────┬──────┘
    │            │
    ▼            ▼
┌──────────────────┐
│   Redis缓存层    │
│ (聚合结果/告警)   │
└────────┬─────────┘
         │
    ┌────┴────┐
    ▼         ▼
┌────────┐ ┌────────┐
│API服务 │ │告警服务 │
└───┬────┘ └───┬────┘
    │          │
    ▼          ▼
┌────────┐ ┌────────┐
│前端仪表│ │通知渠道 │
│  盘    │ │(钉钉等) │
└────────┘ └────────┘

12.2 数据采集与预处理

import json
import time
from typing import Callable

class DataCollector:
    """实时数据采集器"""
    
    def __init__(self, redis_client):
        self.redis = redis_client
        self.processors: list[Callable] = []
    
    def add_processor(self, processor: Callable):
        """添加数据处理器"""
        self.processors.append(processor)
    
    def ingest(self, source: str, data: dict):
        """摄入一条数据"""
        # 添加元数据
        enriched = {
            **data,
            "_source": source,
            "_ingest_time": str(time.time()),
            "_id": str(uuid.uuid4()),
        }
        
        # 写入Redis Stream
        stream_key = f"stream:{source}"
        self.redis.xadd(stream_key, enriched, maxlen=100000)
        
        # 触发处理器
        for processor in self.processors:
            try:
                processor(enriched)
            except Exception as e:
                print(f"处理器错误: {e}")
    
    def ingest_batch(self, source: str, data_list: list[dict]):
        """批量摄入"""
        pipe = self.redis.pipeline()
        for data in data_list:
            enriched = {
                **data,
                "_source": source,
                "_ingest_time": str(time.time()),
            }
            pipe.xadd(f"stream:{source}", enriched, maxlen=100000)
        pipe.execute()

class StreamConsumer:
    """Stream消费者"""
    
    def __init__(self, redis_client, consumer_group: str, 
                 consumer_name: str):
        self.redis = redis_client
        self.group = consumer_group
        self.consumer = consumer_name
    
    def ensure_group(self, stream_key: str):
        """确保消费者组存在"""
        try:
            self.redis.xgroup_create(
                stream_key, self.group, id="0", mkstream=True
            )
        except redis.ResponseError as e:
            if "BUSYGROUP" not in str(e):
                raise
    
    def consume(self, stream_key: str, count: int = 100,
                block_ms: int = 5000) -> list[dict]:
        """消费消息"""
        self.ensure_group(stream_key)
        
        messages = self.redis.xreadgroup(
            self.group, self.consumer,
            {stream_key: ">"},
            count=count,
            block=block_ms,
        )
        
        results = []
        for stream, entries in messages:
            for msg_id, fields in entries:
                # 解码fields
                decoded = {
                    k.decode() if isinstance(k, bytes) else k:
                    v.decode() if isinstance(v, bytes) else v
                    for k, v in fields.items()
                }
                decoded["_msg_id"] = msg_id.decode() if isinstance(msg_id, bytes) else msg_id
                results.append(decoded)
                
                # 确认消息
                self.redis.xack(stream_key, self.group, msg_id)
        
        return results
    
    def consume_stream(self, stream_key: str):
        """持续消费流"""
        while True:
            messages = self.consume(stream_key, count=10, block_ms=1000)
            for msg in messages:
                yield msg

12.3 实时分析引擎

class RealtimeAnalyticsEngine:
    """实时分析引擎"""
    
    def __init__(self, redis_client, anomaly_detector: StreamingAnomalyDetector):
        self.redis = redis_client
        self.detector = anomaly_detector
        self.alert_manager = AlertManager(redis_client)
    
    async def process_metric(self, data: dict):
        """处理一条指标数据"""
        metric_name = data.get("metric", "")
        value = float(data.get("value", 0))
        timestamp = float(data.get("_ingest_time", time.time()))
        tags = data.get("tags", {})
        
        # 1. 更新实时聚合
        self._update_aggregations(metric_name, value, timestamp, tags)
        
        # 2. 异常检测
        anomaly_result = self.detector.detect(metric_name, value, timestamp)
        
        # 3. 处理告警
        if anomaly_result.is_anomaly:
            await self.alert_manager.process_anomaly(anomaly_result)
    
    def _update_aggregations(self, metric_name: str, value: float,
                              timestamp: float, tags: dict):
        """更新实时聚合"""
        pipe = self.redis.pipeline()
        
        # 1分钟窗口聚合
        window_key = f"agg:1m:{metric_name}:{int(timestamp) // 60}"
        pipe.hincrby(window_key, "count", 1)
        pipe.hincrbyfloat(window_key, "sum", value)
        
        # 更新最大最小值
        pipe.eval("""
            local key = KEYS[1]
            local val = tonumber(ARGV[1])
            local cur_max = tonumber(redis.call('hget', key, 'max') or '-inf')
            local cur_min = tonumber(redis.call('hget', key, 'min') or 'inf')
            if val > cur_max then redis.call('hset', key, 'max', val) end
            if val < cur_min then redis.call('hset', key, 'min', val) end
        """, 1, window_key, value)
        
        pipe.expire(window_key, 3600)  # 1小时后过期
        
        # 按标签聚合
        for tag_key, tag_value in tags.items():
            tag_agg_key = f"agg:tag:{metric_name}:{tag_key}:{tag_value}"
            pipe.hincrby(tag_agg_key, "count", 1)
            pipe.hincrbyfloat(tag_agg_key, "sum", value)
            pipe.expire(tag_agg_key, 3600)
        
        pipe.execute()
    
    def get_metric_stats(self, metric_name: str, 
                          window_minutes: int = 5) -> dict:
        """获取指标统计"""
        current_window = int(time.time()) // 60
        
        total_count = 0
        total_sum = 0
        max_val = float("-inf")
        min_val = float("inf")
        
        for i in range(window_minutes):
            window_key = f"agg:1m:{metric_name}:{current_window - i}"
            stats = self.redis.hgetall(window_key)
            
            if stats:
                count = int(stats.get(b"count", 0))
                total_count += count
                total_sum += float(stats.get(b"sum", 0))
                max_val = max(max_val, float(stats.get(b"max", "-inf")))
                min_val = min(min_val, float(stats.get(b"min", "inf")))
        
        if total_count == 0:
            return {"count": 0, "avg": 0, "max": 0, "min": 0}
        
        return {
            "count": total_count,
            "avg": total_sum / total_count,
            "max": max_val,
            "min": min_val,
            "sum": total_sum,
        }

12.4 可视化与告警

# API接口:为前端仪表盘提供数据

@app.get("/api/metrics/{metric_name}/stats")
async def get_metric_stats(metric_name: str, window: int = 5):
    """获取指标统计"""
    stats = analytics_engine.get_metric_stats(metric_name, window)
    return {"metric": metric_name, "window_minutes": window, **stats}

@app.get("/api/metrics/{metric_name}/timeseries")
async def get_timeseries(metric_name: str, minutes: int = 60):
    """获取时间序列数据"""
    current_window = int(time.time()) // 60
    series = []
    
    for i in range(minutes, 0, -1):
        window_key = f"agg:1m:{metric_name}:{current_window - i}"
        stats = analytics_engine.redis.hgetall(window_key)
        
        if stats:
            count = int(stats.get(b"count", 0))
            total = float(stats.get(b"sum", 0))
            series.append({
                "timestamp": (current_window - i) * 60,
                "avg": total / count if count > 0 else 0,
                "count": count,
            })
    
    return {"metric": metric_name, "series": series}

@app.get("/api/alerts/recent")
async def get_recent_alerts(limit: int = 50):
    """获取最近告警"""
    alerts = analytics_engine.redis.xrevrange(
        "alerts:stream", count=limit
    )
    
    return [
        {
            "id": fields.get(b"alert_id", b"").decode(),
            "level": fields.get(b"level", b"").decode(),
            "metric": fields.get(b"metric", b"").decode(),
            "message": fields.get(b"message", b"").decode(),
            "timestamp": float(fields.get(b"timestamp", 0)),
        }
        for _, fields in alerts
    ]

# WebSocket实时推送指标更新
@app.websocket("/ws/metrics")
async def metrics_websocket(websocket: WebSocket):
    await websocket.accept()
    
    # 客户端订阅的指标
    subscribed_metrics = set()
    
    try:
        while True:
            # 接收订阅请求
            data = await websocket.receive_json()
            
            if data.get("action") == "subscribe":
                subscribed_metrics.add(data["metric"])
            elif data.get("action") == "unsubscribe":
                subscribed_metrics.discard(data["metric"])
            
            # 推送最新数据
            for metric in subscribed_metrics:
                stats = analytics_engine.get_metric_stats(metric, 1)
                await websocket.send_json({
                    "type": "metric_update",
                    "metric": metric,
                    "data": stats,
                })
            
            await asyncio.sleep(1)  # 1秒刷新间隔
            
    except WebSocketDisconnect:
        pass

12.5 性能调优与扩展

# 性能调优清单
PERFORMANCE_CHECKLIST = {
    "Redis优化": [
        "使用pipeline批量操作,减少RTT",
        "合理设置maxmemory-policy(推荐allkeys-lru)",
        "监控slow log:CONFIG SET slowlog-log-slower-than 10000",
        "避免大key(单个key value不超过10KB)",
        "使用SCAN代替KEYS命令",
    ],
    "向量搜索优化": [
        "HNSW参数调优:M=16-64, ef_construction=200-500",
        "运行时调整ef_search控制精度/速度权衡",
        "分批插入数据,避免一次性写入过多",
        "监控索引内存使用",
    ],
    "流处理优化": [
        "合理设置Stream maxlen防止内存溢出",
        "使用Consumer Group实现并行消费",
        "批量确认消息减少XACK调用",
        "监控pending消息数量",
    ],
    "WebSocket优化": [
        "设置合理的ping/pong心跳间隔(30秒)",
        "限制单用户连接数",
        "使用连接池管理后端连接",
        "监控活跃连接数和内存使用",
    ],
}

# 水平扩展方案
class ScalableConsumer:
    """可水平扩展的消费者"""
    
    def __init__(self, redis_client, num_workers: int = 4):
        self.redis = redis_client
        self.num_workers = num_workers
    
    async def start(self, stream_key: str, processor: Callable):
        """启动多个消费者工作器"""
        tasks = []
        for i in range(self.num_workers):
            consumer = StreamConsumer(
                self.redis, 
                consumer_group="analytics_group",
                consumer_name=f"worker_{i}",
            )
            task = asyncio.create_task(
                self._worker_loop(consumer, stream_key, processor)
            )
            tasks.append(task)
        
        await asyncio.gather(*tasks)
    
    async def _worker_loop(self, consumer: StreamConsumer, 
                            stream_key: str, processor: Callable):
        """单个工作器循环"""
        while True:
            try:
                messages = consumer.consume(stream_key, count=10, block_ms=1000)
                for msg in messages:
                    await processor(msg)
            except Exception as e:
                print(f"Worker error: {e}")
                await asyncio.sleep(1)

附录A:常见问题与排错指南

Q1: Redis向量搜索返回空结果

可能原因

  1. 索引前缀与实际key不匹配
  2. 向量维度不一致
  3. 数据类型错误(embedding字段应为bytes)

排查步骤

# 1. 检查索引是否存在
print(r.ft("idx:documents").info())

# 2. 检查key是否存在
print(r.exists("doc:your_doc_id"))

# 3. 检查数据类型
print(r.type("doc:your_doc_id"))  # 应为hash
print(r.hget("doc:your_doc_id", "embedding"))  # 应为bytes

# 4. 检查向量维度
embedding = r.hget("doc:your_doc_id", "embedding")
print(len(np.frombuffer(embedding, dtype=np.float32)))  # 应与索引DIM一致

Q2: pgvector查询慢

可能原因

  1. 缺少向量索引
  2. 索引参数不合理
  3. work_mem设置过低

解决方案

-- 1. 检查是否创建了索引
SELECT * FROM pg_indexes WHERE tablename = 'documents';

-- 2. 检查索引构建进度
SELECT phase, tuples_done, tuples_total 
FROM pg_stat_progress_create_index;

-- 3. 调整参数
SET work_mem = '256MB';
SET hnsw.ef_search = 100;  -- 增大搜索范围

-- 4. 使用EXPLAIN ANALYZE查看执行计划
EXPLAIN ANALYZE 
SELECT * FROM documents 
ORDER BY embedding <=> '[0.1, 0.2, ...]'::vector 
LIMIT 10;

Q3: WebSocket连接频繁断开

可能原因

  1. 负载均衡器超时设置过短
  2. 缺少心跳机制
  3. Nginx代理缓冲

解决方案

# nginx.conf
location /ws/ {
    proxy_pass http://backend;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_read_timeout 3600s;  # 1小时超时
    proxy_send_timeout 3600s;
    proxy_buffering off;  # 禁用缓冲
}
# 服务端心跳
async def heartbeat(websocket: WebSocket, interval: int = 30):
    """定期发送心跳"""
    while True:
        try:
            await websocket.send_json({"type": "ping"})
            await asyncio.sleep(interval)
        except Exception:
            break

Q4: Embedding缓存命中率低

可能原因

  1. 文本未归一化(空格、标点、大小写不一致)
  2. 缓存TTL过短
  3. 缓存容量不足

解决方案

def normalize_text(text: str) -> str:
    """文本归一化"""
    import re
    text = text.strip()
    text = re.sub(r'\s+', ' ', text)  # 合并空白
    text = text.lower()  # 小写
    text = re.sub(r'[^\w\s\u4e00-\u9fff]', '', text)  # 保留中英文和数字
    return text

# 缓存键应基于归一化后的文本
def _cache_key(self, text: str) -> str:
    normalized = normalize_text(text)
    text_hash = hashlib.md5(normalized.encode()).hexdigest()
    return f"emb:{text_hash}"

Q5: 流式RAG首token延迟高

可能原因

  1. 向量检索慢
  2. LLM首token延迟(TTFT)
  3. 网络往返

优化方案

  1. 并行化:检索和历史构建并行执行
  2. 缓存:热门查询预缓存结果
  3. 模型选择:选择TTFT低的模型
  4. 预热:保持连接池温暖
# 预热连接池
async def warmup_connections():
    """预热所有连接"""
    # 预热Redis
    await redis.ping()
    
    # 预热LLM(发送空请求)
    await llm.chat.completions.create(
        model="gpt-4o-mini",
        messages=[{"role": "user", "content": "hi"}],
        max_tokens=1,
    )
    
    # 预热向量搜索
    dummy_vec = np.zeros(1536, dtype=np.float32)
    # 执行一次dummy搜索...

Q6: 实时推荐系统冷启动

问题:新用户没有行为数据,无法生成个性化推荐

解决方案

async def recommend_with_cold_start(
    self, user_id: str, top_k: int = 20
) -> list[dict]:
    """带冷启动处理的推荐"""
    user_vector = self.feature_store.get_user_realtime_vector(user_id)
    
    # 判断是否为冷启动用户
    is_cold = np.linalg.norm(user_vector) < 0.01
    
    if is_cold:
        # 冷启动策略
        context = self.feature_store.get_user_context(user_id)
        
        if context.get("注册来源"):
            # 基于来源推荐热门内容
            return self._hot_recall(top_k)
        else:
            # 探索-利用策略:80%热门 + 20%随机
            hot = self._hot_recall(int(top_k * 0.8))
            random_items = self._random_recall(int(top_k * 0.2))
            return hot + random_items
    else:
        # 正常推荐流程
        return await self.recommend(user_id, top_k)

附录B:推荐学习路径

入门阶段(1-2周)

  1. 基础概念:理解向量、Embedding、ANN搜索原理
  2. Redis入门:安装Redis Stack,完成第二章基础操作
  3. pgvector入门:安装扩展,完成第三章基础操作
  4. 简单RAG:实现一个基于向量搜索的问答Demo

进阶阶段(2-4周)

  1. 实时通信:实现WebSocket和SSE接口(第六、七章)
  2. 流式RAG:实现完整的流式RAG系统(第五章)
  3. Embedding优化:实现缓存和批处理管线(第四章)
  4. 项目实战:完成实时智能客服系统(第十一章)

高级阶段(4-6周)

  1. 推荐系统:实现实时推荐引擎(第八章)
  2. 异常检测:实现流式异常检测系统(第九章)
  3. 边缘部署:模型量化和ONNX部署(第十章)
  4. 综合项目:完成流式数据分析平台(第十二章)

推荐学习资源

  • Redis官方文档:https://redis.io/docs/stack/search/
  • pgvector GitHub:https://github.com/pgvector/pgvector
  • FastAPI文档:https://fastapi.tiangolo.com/
  • Sentence Transformers:https://www.sbert.net/
  • ONNX Runtime:https://onnxruntime.ai/

本教程完。涵盖实时AI应用开发的核心技术栈,从向量数据库到流式推理,从推荐系统到边缘部署,配有完整的代码示例和两个实战项目。建议按学习路径循序渐进,动手实践每个章节的代码。

内容声明

本文内容为AI技术学习教程,仅供学习参考。如涉及技术问题,欢迎通过 xurj005@163.com 与我们交流。

目录