企业知识库 RAG 系统 — 生产级开发文档

6 阅读22分钟

技术栈:Python 3.11 + LangChain + Claude(Anthropic API)+ Milvus / Qdrant 适用场景:企业内部知识库问答(含权限隔离、审计、灰度发布) 文档版本:v1.0 · 适用 Claude 4.6 / 4.7 系列模型


0. 文档使用说明

本文档面向 要把 RAG 系统真正落到企业生产环境 的工程团队,覆盖从立项评估到上线运维的完整闭环。每一章节都按以下结构组织:

  1. 设计目标 —— 该模块要解决的核心问题
  2. 方案选型 —— 备选方案对比与推荐
  3. 落地实现 —— 可直接使用的代码骨架与配置
  4. 生产注意事项 —— 踩坑经验与红线

红线(⚠️)部分代表 必须遵守,违反会直接导致线上事故。


1. 总体架构

1.1 系统分层

企业知识库 RAG 必须划分清楚 离线链路在线链路,二者部署、扩缩容、监控策略完全不同。

┌─────────────────────────────────────────────────────────────┐
│                      离线链路(Indexing)                     │
│  数据源 → 采集 → 解析 → 清洗 → 切分 → 向量化 → 写入向量库      │
│  (Airflow/Dagster 调度,T+1 或近实时 CDC)                   │
└─────────────────────────────────────────────────────────────┘
                              │
                              ▼
                      ┌───────────────┐
                      │  向量库 + 元  │
                      │  数据库 + ES  │
                      └───────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────┐
│                      在线链路(Serving)                      │
│  用户Query → 鉴权 → 改写 → 检索 → 重排 → 上下文组装 → LLM → 后处理 │
│  (FastAPI + Gunicorn/Uvicorn,K8s HPA 弹性扩缩)             │
└─────────────────────────────────────────────────────────────┘
                              │
                              ▼
                  ┌─────────────────────┐
                  │  观测:日志/指标/Trace │
                  │  评估:离线集 + 在线 A/B │
                  └─────────────────────┘

1.2 关键组件选型

组件推荐方案备选选型理由
LLMClaude Sonnet 4.6(主力)+ Haiku 4.5(兜底/分类)Opus 4.7(复杂推理)Sonnet 性价比最高,Haiku 用于轻量任务降本
Embeddingbge-m3 / bge-large-zh-v1.5(自部署)OpenAI text-embedding-3-large自部署可控、合规、可微调
向量库Milvus 2.4+(千万级以上)/ Qdrant(千万级以下)Weaviate、PGVectorMilvus 适合大规模分布式;Qdrant 单机性能优、运维简单
倒排索引Elasticsearch 8.x / OpenSearchTantivy必须保留,用于 BM25 混合检索
元数据库PostgreSQL 15+MySQL存文档元信息、权限、审计日志
缓存Redis 7(Cluster 模式)缓存 query→answer、embedding
编排框架LangChain 0.3+ / LangGraphLlamaIndexLangChain 生态最全,LangGraph 用于复杂 Agent 流程
网关FastAPI + Uvicorn异步、原生 SSE 流式
调度Airflow 2.9 / DagsterPrefect离线索引任务调度
观测OpenTelemetry + Langfuse + Prometheus + GrafanaLangSmithLangfuse 可私有化部署,符合合规要求

⚠️ 红线:LLM 调用必须走统一网关(自研或 LiteLLM Proxy),禁止业务代码直连 Anthropic API。原因:限流、密钥轮转、成本归集、审计、模型切换都需要在网关层完成。


2. 数据处理链路(离线)

数据质量决定 RAG 上限。本节是工程团队最容易低估的部分。

2.1 数据源接入

企业知识库典型数据源:

类型示例接入方式难点
结构化文档Confluence、飞书、NotionAPI 拉取 + Webhook权限映射
非结构化文档PDF、Word、PPT、Excel对象存储扫描 + 解析表格/公式/图片
代码与 WikiGitLab Wiki、READMEGit Hook增量同步
数据库知识业务库(产品参数、FAQ)CDC(Debezium/Canal)Schema 映射
工单/对话Jira、客服系统定时增量数据脱敏

接入原则

  1. 每个数据源一个 Connector,独立部署、独立失败、独立监控。
  2. 统一中间格式 RawDocument
from pydantic import BaseModel
from datetime import datetime
from typing import Literal

class RawDocument(BaseModel):
    doc_id: str                    # 全局唯一,建议 {source}:{external_id}
    source: str                    # confluence / feishu / gitlab / ...
    title: str
    content: str                   # 原始内容(HTML/Markdown/纯文本)
    content_type: Literal["html", "markdown", "pdf", "text", "code"]
    url: str                       # 回链
    author: str
    created_at: datetime
    updated_at: datetime
    acl: list[str]                 # 权限标签(部门/角色/用户ID)
    tags: list[str] = []
    extra: dict = {}               # 业务自定义字段

⚠️ 红线acl 字段在数据入库时就必须打上,绝不能在检索后再过滤。原因:在检索后过滤会导致返回结果不足 top_k,且向量库 IO 浪费严重。

2.2 文档解析

不同格式需要不同解析器,且必须保留 结构信息(标题层级、表格、列表),这对后续切分至关重要。

格式推荐工具备注
PDFUnstructured.io / MinerU / PyMuPDFMinerU 对中文学术/扫描件最强
Wordpython-docx + unstructured注意保留标题级别
PPTpython-pptx每页一个 chunk 起步
Excelopenpyxl + 自定义表格转 Markdown每个 Sheet 单独处理
HTMLtrafilatura(去广告/导航)Confluence 必备
Markdownmarkdown-it-py原生最友好
扫描件/图片PaddleOCR / Tesseract必须做版面分析

解析后统一输出 结构化 Markdown(保留 #/##/表格语法),这是后续切分的基础。

2.3 文本切分(Chunking)

切分策略是 RAG 效果的关键变量之一。推荐策略组合:

层级 1:结构感知切分(首选)

  • 按 Markdown 标题层级递归切分(LangChain MarkdownHeaderTextSplitter
  • 每个 chunk 携带 header_path(如 ["产品手册", "API 鉴权", "OAuth2 流程"]

层级 2:语义切分(结构不清晰时)

  • 使用 embedding 相似度断句(LangChain SemanticChunker
  • 计算成本高,仅对 long-form 文本使用

层级 3:固定长度兜底

  • RecursiveCharacterTextSplitter,按 \n\n\n 优先级递归切
  • 中文场景推荐参数chunk_size=500chunk_overlap=80(token 计),约对应 350-400 汉字
from langchain_text_splitters import (
    MarkdownHeaderTextSplitter,
    RecursiveCharacterTextSplitter,
)

header_splitter = MarkdownHeaderTextSplitter(
    headers_to_split_on=[("#", "h1"), ("##", "h2"), ("###", "h3")],
    strip_headers=False,
)

char_splitter = RecursiveCharacterTextSplitter(
    chunk_size=500,
    chunk_overlap=80,
    length_function=lambda x: len(tokenizer.encode(x)),  # 用真实 tokenizer
    separators=["\n\n", "\n", "。", "!", "?", ";", ",", " ", ""],
)

def split_document(md_text: str) -> list[Chunk]:
    sections = header_splitter.split_text(md_text)
    chunks = []
    for sec in sections:
        for piece in char_splitter.split_text(sec.page_content):
            chunks.append(Chunk(
                text=piece,
                header_path=sec.metadata,
                ...
            ))
    return chunks

⚠️ 红线

  • chunk_size 必须用 tokenizer 计算,不能用字符数。中英文 token 比差 2-3 倍。
  • 表格不能被切断。检测到 Markdown 表格语法(|---|)时整块保留,超长则单独标记 is_table=True
  • 代码块不能被切断。同上策略。

2.4 向量化(Embedding)

模型选择

模型维度中文效果部署方式适用场景
bge-m31024TEI / vLLM通用首选,支持多语言+稀疏向量
bge-large-zh-v1.51024TEI纯中文场景
Conan-embedding-v11792顶级TEI追求极致效果
Qwen3-Embedding-8B4096顶级vLLM大模型 embedding,资源充足

部署建议:用 Hugging Face TEI 部署,GPU 单卡 RTX 4090 可跑 bge-m3,QPS 500+。

批处理:离线索引必须 batch 调用,batch_size=64~128,否则 GPU 利用率惨不忍睹。

class EmbeddingClient:
    def __init__(self, endpoint: str, batch_size: int = 64):
        self.endpoint = endpoint
        self.batch_size = batch_size
        self.session = httpx.AsyncClient(timeout=30.0, limits=httpx.Limits(max_connections=50))

    async def embed_batch(self, texts: list[str]) -> list[list[float]]:
        results = []
        for i in range(0, len(texts), self.batch_size):
            batch = texts[i:i + self.batch_size]
            resp = await self.session.post(
                f"{self.endpoint}/embed",
                json={"inputs": batch, "normalize": True},
            )
            resp.raise_for_status()
            results.extend(resp.json())
        return results

⚠️ 红线

  • embedding 必须归一化(normalize=True),后续可用内积代替余弦相似度,性能提升 30%。
  • embedding 模型一旦上线,更换必须全量重建索引。模型版本号必须写入向量库元数据。

2.5 索引构建

双索引策略:向量索引(语义)+ 倒排索引(关键词),后续做混合检索。

Milvus 配置参考(千万级 chunk):

from pymilvus import MilvusClient, DataType

client = MilvusClient(uri="http://milvus:19530", token="...")

schema = client.create_schema(auto_id=False, enable_dynamic_field=False)
schema.add_field("chunk_id", DataType.VARCHAR, is_primary=True, max_length=128)
schema.add_field("doc_id", DataType.VARCHAR, max_length=128)
schema.add_field("dense_vector", DataType.FLOAT_VECTOR, dim=1024)
schema.add_field("sparse_vector", DataType.SPARSE_FLOAT_VECTOR)  # bge-m3 稀疏向量
schema.add_field("text", DataType.VARCHAR, max_length=2000)
schema.add_field("acl", DataType.ARRAY, element_type=DataType.VARCHAR, max_capacity=32, max_length=64)
schema.add_field("source", DataType.VARCHAR, max_length=64)
schema.add_field("updated_at", DataType.INT64)
schema.add_field("embedding_version", DataType.VARCHAR, max_length=32)

index_params = client.prepare_index_params()
index_params.add_index(
    field_name="dense_vector",
    index_type="HNSW",      # 千万级以下用 HNSW,更大用 DISKANN
    metric_type="IP",       # 归一化后用内积
    params={"M": 16, "efConstruction": 200},
)
index_params.add_index(
    field_name="sparse_vector",
    index_type="SPARSE_INVERTED_INDEX",
    metric_type="IP",
)
index_params.add_index(field_name="acl")  # 标量索引加速过滤

client.create_collection(
    collection_name="kb_chunks_v1",
    schema=schema,
    index_params=index_params,
    consistency_level="Bounded",  # 平衡一致性与性能
)

ES 倒排索引(同步写入):

{
  "mappings": {
    "properties": {
      "chunk_id": {"type": "keyword"},
      "doc_id": {"type": "keyword"},
      "text": {
        "type": "text",
        "analyzer": "ik_max_word",
        "search_analyzer": "ik_smart"
      },
      "acl": {"type": "keyword"},
      "source": {"type": "keyword"},
      "updated_at": {"type": "date"}
    }
  }
}

⚠️ 红线

  • collection 必须带版本号kb_chunks_v1),切换 embedding 模型/切分策略时通过别名切换,禁止原地改
  • 双写一致性:Milvus 和 ES 必须双写成功才提交,使用 outbox 模式 + 重试。

2.6 增量更新与删除

企业知识库文档高频变更,必须支持:

  1. 增量更新:基于 updated_at + Webhook 或 CDC 触发。
  2. 删除传播:源文档删除后,24 小时内 必须从向量库和 ES 移除。
  3. 版本号机制:每个 chunk 带 doc_version,老版本通过 TTL 或定期清理任务删除。

推荐 Pipeline(Airflow DAG 结构)

detect_changes (CDC/scan)
    ↓
fetch_documents (并行)
    ↓
parse_documents (并行)
    ↓
split_chunks
    ↓
embed_chunks (批量)
    ↓
upsert_to_milvus + upsert_to_es (并行, 双写)
    ↓
delete_stale_chunks (按 doc_version)
    ↓
validate_index (抽样检索,回归集 ≥ 95%)

3. 检索链路(在线)

检索是 RAG 的"召回天花板"。生成模型再强,检索不到正确文档也救不了。

3.1 检索整体流程

Query
  ↓
[1] 鉴权 & 上下文加载(用户ACL、会话历史)
  ↓
[2] Query 预处理:意图分类 / 改写 / 多查询扩展
  ↓
[3] 混合召回:Dense(向量) + Sparse(BM25) + Metadata 过滤
  ↓
[4] 融合:RRF 或加权
  ↓
[5] 重排序(Reranker)
  ↓
[6] 上下文压缩 / 去重 / 排序
  ↓
[7] 输出 top_n chunks

3.2 Query 改写与扩展

用户 query 通常短、歧义、口语化。改写策略:

策略用途实现
多轮上下文合并解决指代("它"、"这个")用 Claude Haiku 把多轮对话合成独立 query
HyDE(假设性文档)短 query 检索效果差让 LLM 先生成假设答案再检索
Multi-Query提升召回让 LLM 生成 3-5 个等价 query 并行检索
意图分类路由到不同知识库Haiku 做 zero-shot 分类

推荐组合:上下文合并(必选)+ Multi-Query(高价值场景)。HyDE 慎用,会显著增加延迟和成本。

async def rewrite_query(query: str, history: list[Message]) -> RewriteResult:
    if not history:
        return RewriteResult(standalone_query=query, sub_queries=[query])

    prompt = f"""基于对话历史,将用户最新提问改写为独立、完整的查询。

对话历史:
{format_history(history)}

最新提问:{query}

输出 JSON:{{"standalone_query": "...", "sub_queries": ["...", "..."]}}
sub_queries 是 1-3 个语义等价的扩展查询,用于提升召回。"""

    resp = await claude_client.messages.create(
        model="claude-haiku-4-5-20251001",
        max_tokens=512,
        messages=[{"role": "user", "content": prompt}],
    )
    return RewriteResult.model_validate_json(resp.content[0].text)

⚠️ 红线:query 改写必须有 超时降级(推荐 800ms)。改写失败时直接用原 query,不能因改写阻塞主链路

3.3 混合检索(Hybrid Search)

为什么必须混合

  • 向量召回 强于语义相似,弱于精确匹配(产品型号、人名、错误码)。
  • BM25 召回 强于关键词命中,弱于语义。
  • 二者互补,混合后 Recall@10 通常提升 15-30%。

实现

async def hybrid_retrieve(
    query: str,
    sub_queries: list[str],
    user_acl: list[str],
    top_k: int = 50,
) -> list[Chunk]:
    # 并行三路召回
    dense_task = asyncio.gather(*[
        milvus_search(q, user_acl, top_k=top_k) for q in sub_queries
    ])
    sparse_task = es_bm25_search(query, user_acl, top_k=top_k)
    dense_results, sparse_results = await asyncio.gather(dense_task, sparse_task)

    # RRF 融合
    return reciprocal_rank_fusion(
        [*dense_results, sparse_results],
        k=60,  # RRF 平滑常数
    )[:top_k]


def reciprocal_rank_fusion(result_lists: list[list[Chunk]], k: int = 60) -> list[Chunk]:
    scores: dict[str, float] = {}
    chunk_map: dict[str, Chunk] = {}
    for results in result_lists:
        for rank, chunk in enumerate(results):
            scores[chunk.chunk_id] = scores.get(chunk.chunk_id, 0) + 1 / (k + rank + 1)
            chunk_map[chunk.chunk_id] = chunk
    sorted_ids = sorted(scores, key=scores.get, reverse=True)
    return [chunk_map[cid] for cid in sorted_ids]

Milvus 检索时务必带 ACL 过滤

def milvus_search(query_vec, user_acl, top_k):
    return client.search(
        collection_name="kb_chunks_v1",
        data=[query_vec],
        limit=top_k,
        filter=f"ARRAY_CONTAINS_ANY(acl, {user_acl})",
        output_fields=["chunk_id", "doc_id", "text", "source"],
        search_params={"metric_type": "IP", "params": {"ef": 128}},
    )

3.4 重排序(Reranker)

向量召回 top_50 后,用 Reranker 精排 top_10,效果提升明显。

模型选择

模型部署延迟(top 50)效果
bge-reranker-v2-m3TEI~50ms (A10)推荐,性价比高
bge-reranker-largeTEI~80ms略弱于 v2-m3
Cohere Rerank 3API~200ms闭源 SaaS

重排部署同样用 TEI,调用方式:

async def rerank(query: str, chunks: list[Chunk], top_n: int = 8) -> list[Chunk]:
    resp = await reranker_client.post(
        "/rerank",
        json={
            "query": query,
            "texts": [c.text for c in chunks],
            "raw_scores": False,
            "truncate": True,
        },
    )
    scored = resp.json()  # [{"index": 0, "score": 0.92}, ...]
    sorted_idx = sorted(scored, key=lambda x: x["score"], reverse=True)
    return [chunks[item["index"]] for item in sorted_idx[:top_n]]

3.5 上下文组装

Reranker 输出后还需处理:

  1. 去重:同一 doc 下相邻 chunk 合并(避免上下文冗余)。
  2. 扩展上下文:可选,把 chunk 前后 1 个相邻 chunk 拼上,提升答案完整度("chunk window")。
  3. token 预算控制:上下文总 token 控制在 LLM 输入的 30-50%(如 Claude 4.6 给 200K,预留 60-80K 给上下文)。
  4. 顺序:按相关性降序或按原文档顺序,最相关的放最前和最后(Claude 对头尾位置敏感度高)。

4. 生成链路

4.1 Prompt 模板

企业知识库场景的 Prompt 必须满足:

  • 明确边界:只能基于上下文回答,不知道就说不知道。
  • 引用标注:每条结论必须带出处编号。
  • 拒答策略:上下文无关时拒答,不要硬编。
  • 格式可控:Markdown 或纯文本可配置。

生产级 Prompt 骨架

SYSTEM_PROMPT = """你是 {company_name} 的企业知识库助手。请严格基于下方"参考资料"回答用户问题。

回答规则:
1. 仅使用参考资料中的信息,不要使用你的预训练知识进行推测。
2. 每个事实性陈述末尾必须用 [n] 标注来源编号,n 对应参考资料的编号。
3. 如果参考资料不足以回答,明确告知"根据现有资料无法确定",并建议用户补充信息或联系相关负责人。
4. 涉及流程、时间、数字、人名时,必须逐字引用,不要重述。
5. 使用简洁的 Markdown 格式输出。如有步骤,使用有序列表。

禁止行为:
- 编造资料中不存在的信息
- 引用资料外的内容
- 提供个人意见或主观判断"""

USER_PROMPT_TEMPLATE = """参考资料:
{context}

---

用户问题:{question}

请基于上述参考资料回答,并标注出处。"""

def build_context(chunks: list[Chunk]) -> str:
    blocks = []
    for i, chunk in enumerate(chunks, start=1):
        header = " > ".join(chunk.header_path) if chunk.header_path else chunk.doc_title
        blocks.append(f"[{i}] 来源:{header}{chunk.source})\n{chunk.text}")
    return "\n\n".join(blocks)

4.2 调用 Claude(带 Prompt Caching)

Claude 的 Prompt Caching 在 RAG 场景能省 50-90% 成本,必须用上。系统提示词和静态指令放进 cache。

from anthropic import AsyncAnthropic

claude = AsyncAnthropic()

async def generate_answer(
    question: str,
    chunks: list[Chunk],
    history: list[Message],
) -> AsyncIterator[str]:
    context = build_context(chunks)
    user_content = USER_PROMPT_TEMPLATE.format(context=context, question=question)

    async with claude.messages.stream(
        model="claude-sonnet-4-6",
        max_tokens=2048,
        system=[
            {
                "type": "text",
                "text": SYSTEM_PROMPT.format(company_name="ACME"),
                "cache_control": {"type": "ephemeral"},  # 系统提示词缓存
            }
        ],
        messages=[
            *format_history_for_claude(history),
            {"role": "user", "content": user_content},
        ],
        temperature=0.2,
    ) as stream:
        async for text in stream.text_stream:
            yield text

⚠️ 红线

  • temperature 设 0.1~0.3,知识库场景不需要创造性。
  • 不要把检索到的上下文放进 cache(每次都变),只缓存 system prompt 和固定 few-shot 示例。
  • 必须设置 max_tokens,防止失控的长输出。
  • 流式输出(streaming)必选,首 token 延迟可控制在 500ms 内。

4.3 引用追溯

前端展示需要点击引用跳回原文。后端在流式输出结束后,附加 citations 元数据:

class AnswerResponse(BaseModel):
    answer: str
    citations: list[Citation]
    trace_id: str
    latency_ms: dict[str, int]

class Citation(BaseModel):
    index: int           # [1], [2] 的编号
    chunk_id: str
    doc_id: str
    doc_title: str
    url: str
    snippet: str         # 200 字以内
    relevance_score: float

SSE 协议建议:

event: token
data: {"text": "..."}

event: citation
data: {"index": 1, "doc_id": "...", ...}

event: done
data: {"trace_id": "...", "total_tokens": 1234}

4.4 兜底与拒答

LLM 仍可能幻觉,必须有事后校验:

  1. 引用完整性检查:解析答案中的 [n],校验 n 都在 citations 范围内。
  2. 拒答词检测:检测到"无法确定/没有相关资料"时,跳过引用校验。
  3. 危险内容过滤:用 Claude Haiku 做一层轻量的合规审核(敏感、违规、个人隐私)。
  4. 答案为空时降级:直接返回"未找到相关信息,请尝试换个问法或联系 XX",并把 query 记录到 未召回日志 供运营补充资料。

5. 服务工程化

5.1 API 设计

最小化对外接口(FastAPI):

from fastapi import FastAPI, Depends, HTTPException
from fastapi.responses import StreamingResponse

app = FastAPI()

class ChatRequest(BaseModel):
    session_id: str
    question: str
    knowledge_base_ids: list[str] = []
    stream: bool = True

@app.post("/api/v1/chat")
async def chat(
    req: ChatRequest,
    user: User = Depends(get_current_user),
):
    if req.stream:
        return StreamingResponse(
            chat_pipeline.run_stream(req, user),
            media_type="text/event-stream",
            headers={"X-Accel-Buffering": "no"},  # Nginx 必须关缓冲
        )
    return await chat_pipeline.run(req, user)

@app.post("/api/v1/feedback")
async def feedback(req: FeedbackRequest, user: User = Depends(get_current_user)):
    """用户对答案的反馈,是模型迭代的核心数据源"""
    await feedback_store.save(req, user)
    return {"ok": True}

5.2 并发与超时

节点推荐超时失败策略
Query 改写800ms降级为原 query
向量检索500ms抛错,回退 BM25
BM25 检索300ms抛错,回退向量
Reranker600ms跳过 rerank,用召回顺序
LLM 首 token3s切换备用模型
LLM 总时长30s中断 + 提示

整体 P95 目标:首字节 < 2s,端到端 < 15s(含 LLM 输出)。

⚠️ 红线:每个外部调用必须有 超时 + 重试(最多 2 次)+ 熔断。生产环境用 tenacity + circuitbreaker 或服务网格层(Istio)保护。

5.3 限流与配额

维度推荐限制工具
单用户 QPS5Redis + 滑动窗口
单用户日 token200KRedis 计数
单租户总 QPS100API 网关
LLM 全局并发看 Anthropic 配额Semaphore

5.4 缓存策略

缓存层KeyTTL命中收益
Query Embeddingemb:{md5(query)}24h省一次 embedding 调用
检索结果ret:{md5(query+acl)}10min省全部检索成本
答案缓存ans:{md5(query+acl+kb_version)}1h高频 FAQ 直接命中

⚠️ 红线:答案缓存的 key 必须包含 kb_version(知识库版本号),知识库更新时旧缓存自动失效。

5.5 部署架构

                    ┌──────────────┐
                    │   Ingress    │ (Nginx / APISIX)
                    └──────┬───────┘
                           │
              ┌────────────┼────────────┐
              ▼            ▼            ▼
        ┌─────────┐  ┌─────────┐  ┌─────────┐
        │ Chat-API│  │ Chat-API│  │ Chat-API│  (K8s Deployment, HPA)
        └────┬────┘  └────┬────┘  └────┬────┘
             └────────────┼────────────┘
                          ▼
              ┌────────────────────────┐
              │   LLM Gateway (自研)    │
              │  (限流/路由/降级/审计)   │
              └───────────┬────────────┘
                          ▼
                ┌─────────────────┐
                │  Anthropic API  │
                └─────────────────┘

依赖服务:
- Milvus Cluster (3 query node + 2 data node)
- Elasticsearch Cluster (3 节点)
- Redis Cluster (33 从)
- PostgreSQL (主从)
- TEI Embedding (GPU, 多副本)
- TEI Reranker (GPU, 多副本)

K8s 资源建议(中型企业,DAU 5000):

  • Chat-API:4C8G × 6 副本起,HPA 基于 QPS 扩到 12
  • Milvus QueryNode:8C32G × 3
  • TEI Embedding:1 卡 A10 × 2 副本
  • TEI Reranker:1 卡 A10 × 2 副本

6. 评估体系

⚠️ 红线:没有评估就上线 = 没有质量。生产 RAG 必须 有:① 离线评测集 ② 在线人工反馈 ③ 自动化回归。

6.1 离线评测集

构建步骤:

  1. 种子集:业务专家标注 100-300 条 (query, golden_answer, golden_chunks) 三元组。
  2. 扩展集:用 Claude 基于真实文档生成 500-1000 条合成 query,人工抽检。
  3. 困难集:从线上未召回日志中挑 100 条,专家给标准答案。

评估指标

维度指标工具
检索Recall@10、MRR、nDCG@10自研脚本
检索Hit Rate(golden chunk 是否在 top_k)自研
生成Faithfulness(答案是否基于上下文)Ragas / Claude-as-judge
生成Answer RelevanceRagas
端到端Correctness(vs golden answer)Claude-as-judge + 人工抽检
端到端Citation Accuracy(引用是否对)自研

Claude-as-Judge 模板

JUDGE_PROMPT = """你是答案质量评审。请从 1-5 分评估候选答案:

问题:{question}
参考答案:{golden}
候选答案:{candidate}
参考资料:{context}

评分维度:
1. 事实正确性(candidate 是否与 golden 事实一致)
2. 忠实度(candidate 中的事实是否都能在参考资料中找到)
3. 完整性(candidate 是否覆盖了 golden 的关键点)

输出 JSON:{{"correctness": 1-5, "faithfulness": 1-5, "completeness": 1-5, "reason": "..."}}"""

6.2 在线评估

  • 用户反馈按钮(👍/👎 + 文本反馈),日志落库。
  • 隐式信号:用户是否追问、是否点击引用、是否复制答案。
  • 每周复盘:抽 200 条线上对话人工打分,建立 baseline。

6.3 回归自动化

每次发布前必须跑:

# 离线评测 CI
pytest tests/eval/ --eval-set=v1.2 --threshold=recall@10>=0.85,faithfulness>=4.2

低于阈值自动阻塞发布。


7. 观测与运维

7.1 日志

结构化日志(JSON),每次请求生成 trace_id,贯穿所有节点:

import structlog

log = structlog.get_logger()

log.info("retrieval_done",
    trace_id=trace_id,
    user_id=user.id,
    query=query,
    sub_queries=sub_queries,
    dense_hits=len(dense_results),
    sparse_hits=len(sparse_results),
    final_hits=len(final_chunks),
    latency_ms=elapsed_ms,
)

⚠️ 红线:日志中 不能 包含用户原始 query 的明文落到非合规存储(涉密企业)。必须按 PII 等级做脱敏或加密。

7.2 指标(Prometheus)

核心指标:

指标类型告警阈值
rag_request_total{status}Counter5xx 比例 > 1%
rag_latency_secondsHistogramP95 > 5s
rag_retrieval_recallGauge< 0.8(基于回归集)
rag_llm_tokens_totalCounter日预算超 80%
rag_no_answer_totalCounter拒答率突增 50%
milvus_search_latencyHistogramP99 > 200ms

7.3 Trace(OpenTelemetry + Langfuse)

每个请求贯穿全链路 trace:

[Chat Request] (root span)
 ├─ [Auth]
 ├─ [Rewrite Query] (LLM call)
 ├─ [Retrieve]
 │   ├─ [Embed Query]
 │   ├─ [Milvus Search]
 │   └─ [ES BM25 Search]
 ├─ [Rerank]
 ├─ [Generate] (LLM call, streaming)
 └─ [Postprocess]

Langfuse 用于 LLM 专属观测:prompt 内容、token 消耗、cache 命中率、人工评分。私有化部署。

7.4 灰度与回滚

发布流程:

  1. 预发环境:跑全量回归集,必须通过。
  2. 金丝雀:1% 流量灰度 30 分钟,监控指标。
  3. 小流量:10% 灰度 2 小时。
  4. 全量:观察 24 小时。

回滚触发条件

  • 5xx 比例 > 2%
  • P95 延迟翻倍
  • 用户负反馈率(👎/total)> 15%

回滚动作

  • API 层:K8s 镜像版本回滚(< 1 分钟)。
  • 知识库:Milvus collection 别名切回旧版本(< 10 秒)。

8. 安全与合规

8.1 权限隔离

ACL 三层防护:

  1. 入库时:每个 chunk 打 acl 标签。
  2. 检索时:向量库和 ES 都基于 acl 过滤。
  3. 生成前:再次校验返回 chunks 的 acl 与当前用户匹配(防御性编程)。

⚠️ 红线绝不能 把跨权限 chunk 拼进同一个 prompt。即使做了 prompt 工程让模型"假装看不见",模型也可能泄漏。

8.2 数据安全

  • 传输:全链路 TLS 1.3。
  • 存储:向量库和 ES 启用磁盘加密。
  • 密钥:Anthropic API Key 必须放 Vault / KMS,禁止写代码或环境变量明文。
  • 审计:所有 query、检索结果、答案、用户反馈写审计日志,至少保留 180 天。

8.3 LLM 内容安全

  • Prompt 注入防御:检测用户 query 中的指令注入模式("忽略前面的指示"、"你现在是..."),命中后清洗或拒答。
  • 越权探测防御:用户 query 涉及高敏 ACL 关键词("工资"、"高管邮件")时,二次确认 ACL。
  • 输出过滤:用 Haiku 跑一道敏感词/PII 检测,发现后阻断。

8.4 合规(视行业)

  • 金融/政务/医疗:必须私有化部署 LLM 或使用合规云(如 Anthropic 的 Bedrock/Vertex 部署)。
  • GDPR/个保法:提供"被遗忘权"接口 —— 用户数据可从向量库、ES、缓存、日志中物理删除。
  • 审计可追溯:每条答案能回溯到具体 chunks → 具体源文档 → 具体作者。

9. 成本治理

9.1 成本结构(中型企业参考)

假设 DAU 5000,人均 10 轮对话,每轮 4K 输入 token + 800 输出 token:

成本项月成本(人民币)占比
Claude Sonnet API~12 万60%
向量库/ES 服务器~3 万15%
Embedding/Reranker GPU~2 万10%
API 服务器 + 中间件~2 万10%
监控/日志/存储~1 万5%
合计~20 万

9.2 降本手段(优先级)

  1. Prompt Caching:系统提示词 + 通用指令缓存,省 30-50%。
  2. 模型分级:分类/改写用 Haiku,主回答用 Sonnet,复杂推理才用 Opus。
  3. 答案缓存:高频 FAQ 直接命中缓存,省 100%。
  4. 检索结果缓存:相同 query 短时间内复用。
  5. Token 控制:上下文 chunk 数限制,超长 chunk 裁剪。
  6. 批量 embedding:离线索引必须 batch。
  7. 冷热数据分层:低频访问的旧文档下沉到 OSS,按需拉起。

10. 落地路线图

Phase 1 — MVP(4 周)

  • 单数据源接入(Confluence 或飞书)
  • 固定切分 + bge-m3 + Milvus + 单路向量检索
  • Claude Sonnet 生成
  • 简易 Web UI
  • 目标:跑通端到端,内测 50 人

Phase 2 — 生产可用(6 周)

  • 多数据源接入 + ACL
  • 混合检索 + Reranker
  • 离线评测集 v1(200 条)
  • 完整观测(日志/指标/Trace)
  • 流式 API + 引用追溯
  • 目标:上线生产,DAU 1000,P95 < 3s

Phase 3 — 优化迭代(持续)

  • 评测集扩充到 1000+
  • Query 改写、Multi-Query
  • 答案缓存与成本治理
  • A/B 实验框架
  • 用户反馈闭环到模型迭代
  • 目标:Faithfulness > 4.3,用户满意度 > 85%

Phase 4 — 高级能力(按需)

  • Agentic RAG(多跳推理、工具调用)
  • 多模态(图表、扫描件、视频字幕)
  • 知识图谱融合(GraphRAG)
  • 微调 Embedding / Reranker

11. 常见故障与排查 Cheatsheet

现象可能原因排查路径
召回不到正确文档切分太碎 / Embedding 不匹配 / 倒排没建检查 chunk_size、抽样手工查向量库、确认 ES 索引
答案幻觉严重上下文不足 / temperature 太高 / prompt 不严检查 top_n、temperature 设 0.1、强化"基于资料"指令
引用对不上LLM 输出 [n] 编号错位在 prompt 强调编号规则 + 后处理校验
延迟突增LLM 慢 / Milvus 慢 / 网络抖动看 Langfuse trace 分解每段耗时
成本飙升cache 未命中 / 上下文过长 / 高频重复 query看 cache_read_input_tokens 比例、加答案缓存
权限泄漏ACL 过滤遗漏紧急回滚,全链路审计 ACL 拼接逻辑
知识更新不及时索引 pipeline 阻塞看 Airflow DAG、检查 CDC 延迟

附录 A:技术选型决策表

决策点推荐备选触发选择备选的条件
编排框架LangChainLlamaIndex / 自研复杂 Agent 流程用 LangGraph
向量库MilvusQdrant / PGVector数据量 < 1000 万选 Qdrant;强事务需求选 PGVector
Embeddingbge-m3Conan-v1 / Qwen3效果天花板用 Qwen3-8B(资源充足时)
Rerankerbge-reranker-v2-m3Cohere资源紧张/网络可外联选 Cohere
LLM 主力Claude Sonnet 4.6Opus 4.7 / GPT复杂多跳推理选 Opus
LLM 兜底/分类Claude Haiku 4.5必选

附录 B:关键依赖版本基线

python==3.11.*
langchain==0.3.*
langchain-anthropic==0.3.*
langgraph==0.2.*
anthropic==0.40.*
pymilvus==2.4.*
elasticsearch==8.15.*
fastapi==0.115.*
uvicorn[standard]==0.32.*
pydantic==2.9.*
redis==5.1.*
opentelemetry-sdk==1.27.*
langfuse==2.50.*

附录 C:参考资料


文档结束

本文档为生产落地骨架,具体参数(chunk_size、top_k、temperature 等)需结合业务数据做 A/B 测试调优。 维护人:__________ 最后更新:__________