技术栈:Python 3.11 + LangChain + Claude(Anthropic API)+ Milvus / Qdrant 适用场景:企业内部知识库问答(含权限隔离、审计、灰度发布) 文档版本:v1.0 · 适用 Claude 4.6 / 4.7 系列模型
0. 文档使用说明
本文档面向 要把 RAG 系统真正落到企业生产环境 的工程团队,覆盖从立项评估到上线运维的完整闭环。每一章节都按以下结构组织:
- 设计目标 —— 该模块要解决的核心问题
- 方案选型 —— 备选方案对比与推荐
- 落地实现 —— 可直接使用的代码骨架与配置
- 生产注意事项 —— 踩坑经验与红线
红线(⚠️)部分代表 必须遵守,违反会直接导致线上事故。
1. 总体架构
1.1 系统分层
企业知识库 RAG 必须划分清楚 离线链路 与 在线链路,二者部署、扩缩容、监控策略完全不同。
┌─────────────────────────────────────────────────────────────┐
│ 离线链路(Indexing) │
│ 数据源 → 采集 → 解析 → 清洗 → 切分 → 向量化 → 写入向量库 │
│ (Airflow/Dagster 调度,T+1 或近实时 CDC) │
└─────────────────────────────────────────────────────────────┘
│
▼
┌───────────────┐
│ 向量库 + 元 │
│ 数据库 + ES │
└───────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 在线链路(Serving) │
│ 用户Query → 鉴权 → 改写 → 检索 → 重排 → 上下文组装 → LLM → 后处理 │
│ (FastAPI + Gunicorn/Uvicorn,K8s HPA 弹性扩缩) │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────┐
│ 观测:日志/指标/Trace │
│ 评估:离线集 + 在线 A/B │
└─────────────────────┘
1.2 关键组件选型
| 组件 | 推荐方案 | 备选 | 选型理由 |
|---|---|---|---|
| LLM | Claude Sonnet 4.6(主力)+ Haiku 4.5(兜底/分类) | Opus 4.7(复杂推理) | Sonnet 性价比最高,Haiku 用于轻量任务降本 |
| Embedding | bge-m3 / bge-large-zh-v1.5(自部署) | OpenAI text-embedding-3-large | 自部署可控、合规、可微调 |
| 向量库 | Milvus 2.4+(千万级以上)/ Qdrant(千万级以下) | Weaviate、PGVector | Milvus 适合大规模分布式;Qdrant 单机性能优、运维简单 |
| 倒排索引 | Elasticsearch 8.x / OpenSearch | Tantivy | 必须保留,用于 BM25 混合检索 |
| 元数据库 | PostgreSQL 15+ | MySQL | 存文档元信息、权限、审计日志 |
| 缓存 | Redis 7(Cluster 模式) | — | 缓存 query→answer、embedding |
| 编排框架 | LangChain 0.3+ / LangGraph | LlamaIndex | LangChain 生态最全,LangGraph 用于复杂 Agent 流程 |
| 网关 | FastAPI + Uvicorn | — | 异步、原生 SSE 流式 |
| 调度 | Airflow 2.9 / Dagster | Prefect | 离线索引任务调度 |
| 观测 | OpenTelemetry + Langfuse + Prometheus + Grafana | LangSmith | Langfuse 可私有化部署,符合合规要求 |
⚠️ 红线:LLM 调用必须走统一网关(自研或 LiteLLM Proxy),禁止业务代码直连 Anthropic API。原因:限流、密钥轮转、成本归集、审计、模型切换都需要在网关层完成。
2. 数据处理链路(离线)
数据质量决定 RAG 上限。本节是工程团队最容易低估的部分。
2.1 数据源接入
企业知识库典型数据源:
| 类型 | 示例 | 接入方式 | 难点 |
|---|---|---|---|
| 结构化文档 | Confluence、飞书、Notion | API 拉取 + Webhook | 权限映射 |
| 非结构化文档 | PDF、Word、PPT、Excel | 对象存储扫描 + 解析 | 表格/公式/图片 |
| 代码与 Wiki | GitLab Wiki、README | Git Hook | 增量同步 |
| 数据库知识 | 业务库(产品参数、FAQ) | CDC(Debezium/Canal) | Schema 映射 |
| 工单/对话 | Jira、客服系统 | 定时增量 | 数据脱敏 |
接入原则:
- 每个数据源一个 Connector,独立部署、独立失败、独立监控。
- 统一中间格式
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 文档解析
不同格式需要不同解析器,且必须保留 结构信息(标题层级、表格、列表),这对后续切分至关重要。
| 格式 | 推荐工具 | 备注 |
|---|---|---|
| Unstructured.io / MinerU / PyMuPDF | MinerU 对中文学术/扫描件最强 | |
| Word | python-docx + unstructured | 注意保留标题级别 |
| PPT | python-pptx | 每页一个 chunk 起步 |
| Excel | openpyxl + 自定义表格转 Markdown | 每个 Sheet 单独处理 |
| HTML | trafilatura(去广告/导航) | Confluence 必备 |
| Markdown | markdown-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=500、chunk_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-m3 | 1024 | 优 | TEI / vLLM | 通用首选,支持多语言+稀疏向量 |
| bge-large-zh-v1.5 | 1024 | 优 | TEI | 纯中文场景 |
| Conan-embedding-v1 | 1792 | 顶级 | TEI | 追求极致效果 |
| Qwen3-Embedding-8B | 4096 | 顶级 | 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 增量更新与删除
企业知识库文档高频变更,必须支持:
- 增量更新:基于
updated_at+ Webhook 或 CDC 触发。 - 删除传播:源文档删除后,24 小时内 必须从向量库和 ES 移除。
- 版本号机制:每个 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-m3 | TEI | ~50ms (A10) | 推荐,性价比高 |
| bge-reranker-large | TEI | ~80ms | 略弱于 v2-m3 |
| Cohere Rerank 3 | API | ~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 输出后还需处理:
- 去重:同一 doc 下相邻 chunk 合并(避免上下文冗余)。
- 扩展上下文:可选,把 chunk 前后 1 个相邻 chunk 拼上,提升答案完整度("chunk window")。
- token 预算控制:上下文总 token 控制在 LLM 输入的 30-50%(如 Claude 4.6 给 200K,预留 60-80K 给上下文)。
- 顺序:按相关性降序或按原文档顺序,最相关的放最前和最后(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 仍可能幻觉,必须有事后校验:
- 引用完整性检查:解析答案中的
[n],校验 n 都在 citations 范围内。 - 拒答词检测:检测到"无法确定/没有相关资料"时,跳过引用校验。
- 危险内容过滤:用 Claude Haiku 做一层轻量的合规审核(敏感、违规、个人隐私)。
- 答案为空时降级:直接返回"未找到相关信息,请尝试换个问法或联系 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 | 抛错,回退向量 |
| Reranker | 600ms | 跳过 rerank,用召回顺序 |
| LLM 首 token | 3s | 切换备用模型 |
| LLM 总时长 | 30s | 中断 + 提示 |
整体 P95 目标:首字节 < 2s,端到端 < 15s(含 LLM 输出)。
⚠️ 红线:每个外部调用必须有 超时 + 重试(最多 2 次)+ 熔断。生产环境用 tenacity + circuitbreaker 或服务网格层(Istio)保护。
5.3 限流与配额
| 维度 | 推荐限制 | 工具 |
|---|---|---|
| 单用户 QPS | 5 | Redis + 滑动窗口 |
| 单用户日 token | 200K | Redis 计数 |
| 单租户总 QPS | 100 | API 网关 |
| LLM 全局并发 | 看 Anthropic 配额 | Semaphore |
5.4 缓存策略
| 缓存层 | Key | TTL | 命中收益 |
|---|---|---|---|
| Query Embedding | emb:{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 (3 主 3 从)
- 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 离线评测集
构建步骤:
- 种子集:业务专家标注 100-300 条 (query, golden_answer, golden_chunks) 三元组。
- 扩展集:用 Claude 基于真实文档生成 500-1000 条合成 query,人工抽检。
- 困难集:从线上未召回日志中挑 100 条,专家给标准答案。
评估指标:
| 维度 | 指标 | 工具 |
|---|---|---|
| 检索 | Recall@10、MRR、nDCG@10 | 自研脚本 |
| 检索 | Hit Rate(golden chunk 是否在 top_k) | 自研 |
| 生成 | Faithfulness(答案是否基于上下文) | Ragas / Claude-as-judge |
| 生成 | Answer Relevance | Ragas |
| 端到端 | 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} | Counter | 5xx 比例 > 1% |
rag_latency_seconds | Histogram | P95 > 5s |
rag_retrieval_recall | Gauge | < 0.8(基于回归集) |
rag_llm_tokens_total | Counter | 日预算超 80% |
rag_no_answer_total | Counter | 拒答率突增 50% |
milvus_search_latency | Histogram | P99 > 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% 流量灰度 30 分钟,监控指标。
- 小流量:10% 灰度 2 小时。
- 全量:观察 24 小时。
回滚触发条件:
- 5xx 比例 > 2%
- P95 延迟翻倍
- 用户负反馈率(👎/total)> 15%
回滚动作:
- API 层:K8s 镜像版本回滚(< 1 分钟)。
- 知识库:Milvus collection 别名切回旧版本(< 10 秒)。
8. 安全与合规
8.1 权限隔离
ACL 三层防护:
- 入库时:每个 chunk 打
acl标签。 - 检索时:向量库和 ES 都基于
acl过滤。 - 生成前:再次校验返回 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 降本手段(优先级)
- Prompt Caching:系统提示词 + 通用指令缓存,省 30-50%。
- 模型分级:分类/改写用 Haiku,主回答用 Sonnet,复杂推理才用 Opus。
- 答案缓存:高频 FAQ 直接命中缓存,省 100%。
- 检索结果缓存:相同 query 短时间内复用。
- Token 控制:上下文 chunk 数限制,超长 chunk 裁剪。
- 批量 embedding:离线索引必须 batch。
- 冷热数据分层:低频访问的旧文档下沉到 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:技术选型决策表
| 决策点 | 推荐 | 备选 | 触发选择备选的条件 |
|---|---|---|---|
| 编排框架 | LangChain | LlamaIndex / 自研 | 复杂 Agent 流程用 LangGraph |
| 向量库 | Milvus | Qdrant / PGVector | 数据量 < 1000 万选 Qdrant;强事务需求选 PGVector |
| Embedding | bge-m3 | Conan-v1 / Qwen3 | 效果天花板用 Qwen3-8B(资源充足时) |
| Reranker | bge-reranker-v2-m3 | Cohere | 资源紧张/网络可外联选 Cohere |
| LLM 主力 | Claude Sonnet 4.6 | Opus 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:参考资料
- Anthropic 官方文档:docs.claude.com
- Milvus 官方文档:milvus.io/docs
- Langfuse 自部署指南:langfuse.com/docs/deploy…
- BGE 系列模型:github.com/FlagOpen/Fl…
- TEI 部署:github.com/huggingface…
- Ragas 评估框架:docs.ragas.io
文档结束
本文档为生产落地骨架,具体参数(chunk_size、top_k、temperature 等)需结合业务数据做 A/B 测试调优。 维护人:__________ 最后更新:__________