实时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 与我们交流。

目录