安全知识库 RAG 系统:架构设计、技术选型与实现

安全团队的知识管理长期面临"三难困境"——文档分散导致检索低效、经验依赖导致知识流失、回答不一致导致决策风险。当安全事件响应需要在分钟级别完成时,翻阅十几个 Wiki 页面和共享文件夹寻找一个防火墙规则配置方案,代价是不可接受的。

本文以一个真实落地的安全知识库 RAG 系统为蓝本,从需求分析到架构设计、技术选型、核心模块实现、部署架构和质量评测,完整呈现一个企业级 RAG 项目从 0 到 1 的全过程。所有架构决策都基于实际工程约束,而非理想化的技术堆砌。


一、需求分析:安全团队的知识管理痛点

1.1 典型场景与问题

安全团队的知识管理与一般业务团队有本质区别:知识的时效性要求极高、准确性要求极严、来源的权威性必须可追溯。以下是三个核心痛点:

痛点一:文档分散,信息孤岛严重

安全知识散布在多个系统中——Confluence 上的安全策略文档、GitHub 上的 Playbook 代码、邮件中的事件复盘报告、飞书群聊中的应急经验分享、外部安全厂商的漏洞通告。一个典型的漏洞应急场景中,安全工程师需要在 5 个以上系统间反复切换,才能拼凑出完整的处置方案。

痛点二:知识检索效率低,人工筛选成本高

现有的全文搜索引擎(如 Elasticsearch 的简单关键词匹配)无法理解安全领域的语义关系。搜索"Redis 未授权访问修复"可能无法匹配到标题为"缓存服务安全加固方案"的文档,因为两者使用了不同的术语体系。安全团队平均需要 15-30 分钟才能定位到所需知识片段。

痛点三:回答不一致,缺乏权威来源

同一个安全问题,不同工程师给出的建议可能存在差异。例如,关于"如何配置 WAF 规则防御 SQL 注入",资深工程师和新人的理解深度不同,生成的方案质量参差不齐。更严重的是,过时的安全建议(如建议使用已废弃的加密算法)可能被当作最新标准执行。

1.2 需求矩阵

需求维度具体需求优先级量化指标
知识聚合支持多格式文档导入(PDF/Markdown/HTML/飞书文档)P0支持 5+ 种格式
语义检索基于语义理解的安全知识检索P0检索召回率 > 85%
引用溯源每个回答必须标注原始文档来源P0100% 回答可追溯
安全管控敏感信息脱敏、访问权限控制P0无敏感信息泄露
流式输出长回答实时流式展示P1首 Token 延迟 < 2s
反馈闭环用户可标记回答质量,驱动持续优化P1反馈覆盖率 > 30%
增量更新文档变更后自动同步索引P1更新延迟 < 5min
多轮对话支持上下文关联的深度安全咨询P2上下文窗口 10 轮

二、架构设计:前后端分离的 RAG 全链路

2.1 架构总览

系统采用前后端分离架构,核心由四个组件构成:LangChain 编排层(负责 RAG pipeline)、PGVector 向量存储层(负责向量索引与混合检索)、FastAPI 服务层(负责 API 网关与业务逻辑)、React 前端层(负责交互界面)。

┌──────────────────────────────────────────────────────────────────────┐
│                          用户交互层 (React)                           │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌──────────────────────┐ │
│  │ 对话界面  │  │ 文档管理  │  │ 反馈面板  │  │ 管理后台(权限/审计)│ │
│  └────┬─────┘  └────┬─────┘  └────┬─────┘  └──────────┬───────────┘ │
│       └──────────────┼─────────────┼───────────────────┘             │
│                      │  SSE / WebSocket                              │
└──────────────────────┼───────────────────────────────────────────────┘
                       │
┌──────────────────────┼───────────────────────────────────────────────┐
│                FastAPI 服务层(API Gateway)                          │
│  ┌──────────┐  ┌─────┴─────┐  ┌──────────┐  ┌───────────────────┐  │
│  │ 路由分发  │  │ 认证鉴权   │  │ 限流熔断  │  │ 审计日志          │  │
│  └────┬─────┘  └───────────┘  └──────────┘  └───────────────────┘  │
│       │                                                              │
│  ┌────┴─────────────────────────────────────────────────────────┐   │
│  │              LangChain RAG Pipeline                          │   │
│  │  ┌────────────┐ ┌────────────┐ ┌────────────┐ ┌───────────┐ │   │
│  │  │ 查询改写    │ → 混合检索   │ → 重排序     │ → LLM生成   │ │   │
│  │  └────────────┘ └─────┬──────┘ └─────┬──────┘ └─────┬─────┘ │   │
│  └───────────────────────┼──────────────┼──────────────┼────────┘   │
└──────────────────────────┼──────────────┼──────────────┼────────────┘
                           │              │              │
┌──────────────────────────┼──────────────┼──────────────┼────────────┐
│                   数据存储层                                                  │
│  ┌──────────────┐  ┌─────┴──────┐  ┌───┴────┐  ┌─────┴──────────┐ │
│  │  PostgreSQL   │  │  PGVector  │  │ Redis  │  │ MinIO / S3     │ │
│  │  (元数据管理)  │  │ (向量存储)  │  │ (缓存) │  │ (文档原始文件) │ │
│  └──────────────┘  └────────────┘  └────────┘  └────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘

2.2 核心设计原则

原则一:检索质量优先于生成质量

RAG 系统的天花板由检索决定,而非生成。投入 80% 的精力优化检索环节(混合检索、重排序、查询改写),仅用 20% 的精力调优生成 Prompt,这是经过验证的投入产出比。

原则二:一切可追溯、可审计

安全领域的知识问答必须满足合规要求。每条回答都携带完整的检索链路信息——查询了哪些文档、命中了哪些片段、每个片段的相似度得分、最终使用的上下文窗口。这些数据同时服务于质量评测和审计追溯。

原则三:渐进式复杂度

系统从最小可用架构开始——LangChain + PGVector + FastAPI + React 四件套——通过模块化设计确保每个组件可以独立升级。例如,后续可以将 PGVector 替换为 Milvus 以获得更高吞吐量,或在 LangChain 之上叠加 LangGraph 实现多步推理,而不需要重构整体架构。


三、技术选型:每个决策背后的逻辑

3.1 编排框架:LangChain

为什么选择 LangChain

LangChain 在 RAG 场景中的优势不是性能,而是生态完整性和迭代效率。它提供了从文档加载器、文本分割器、Embedding 接口到检索链、生成链的全套抽象,使得 RAG pipeline 的搭建从"手写胶水代码"变成"组合式配置"。

from langchain_community.document_loaders import (
    PyPDFLoader,
    UnstructuredMarkdownLoader,
    BSHTMLLoader,
)
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_openai import OpenAIEmbeddings, ChatOpenAI
from langchain_community.vectorstores import PGVector
from langchain.chains import RetrievalQA

LOADERS = {
    ".pdf": PyPDFLoader,
    ".md": UnstructuredMarkdownLoader,
    ".html": BSHTMLLoader,
}

text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=512,
    chunk_overlap=64,
    separators=["\n\n", "\n", "。", ";", " "],
    length_function=len,
)

embeddings = OpenAIEmbeddings(model="text-embedding-3-small")

备选方案与放弃理由

方案放弃理由
LlamaIndex更侧重文档索引和查询引擎,对复杂对话链路的支持不如 LangChain 灵活
Haystack生态偏向搜索场景,对 LLM 生成链路的抽象较少
自建 pipeline过度工程化,初期迭代速度慢,且需要自行维护大量边界情况处理

风险控制:LangChain 的抽象层较厚,版本迭代频繁,API 变动较大。应对策略是通过自定义 Wrapper 隔离核心业务逻辑与 LangChain API,确保框架升级不会波及业务代码。

3.2 向量存储:PGVector

为什么选择 PGVector

PGVector 的核心优势不在于向量检索性能(在纯向量检索吞吐量上,Milvus 和 Qdrant 优于 PGVector),而在于与 PostgreSQL 的深度集成带来的工程便利性

  • 单数据库解决双需求:文档元数据(标题、标签、权限、版本)和向量索引共存于同一个 PostgreSQL 实例,避免了"元数据在 MySQL,向量在 Milvus"的跨库一致性问题
  • 混合查询原生支持:利用 PostgreSQL 的 SQL 能力实现元数据过滤 + 向量检索的联合查询,无需在应用层做二次过滤
  • 运维成本极低:安全团队已有 PostgreSQL 运维经验,无需额外引入新的数据库组件
CREATE EXTENSION IF NOT EXISTS vector;

CREATE TABLE security_documents (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    title TEXT NOT NULL,
    content TEXT NOT NULL,
    source_url TEXT,
    doc_type VARCHAR(50),
    tags TEXT[],
    access_level INTEGER DEFAULT 0,
    created_at TIMESTAMPTZ DEFAULT NOW(),
    updated_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE TABLE document_chunks (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    document_id UUID REFERENCES security_documents(id) ON DELETE CASCADE,
    chunk_index INTEGER NOT NULL,
    content TEXT NOT NULL,
    embedding vector(1536),
    token_count INTEGER,
    metadata JSONB
);

CREATE INDEX idx_chunks_embedding ON document_chunks
    USING ivfflat (embedding vector_cosine_ops)
    WITH (lists = 100);

CREATE INDEX idx_chunks_doc_type ON security_documents(doc_type);
CREATE INDEX idx_chunks_access_level ON security_documents(access_level);

备选方案与放弃理由

方案放弃理由
Milvus向量检索性能更强,但引入独立集群的运维成本对安全团队过高,且需要解决元数据同步问题
Chroma开发阶段友好,但生产环境的持久化和并发能力不足
Pinecone托管服务省心,但数据出境合规风险不可接受
Weaviate功能全面但部署复杂度高,团队缺乏 Go 技术栈经验

3.3 服务框架:FastAPI

为什么选择 FastAPI

安全知识库的问答场景对延迟敏感——用户在应急响应中等待答案时,每一秒都很重要。FastAPI 的异步原生设计使得 RAG pipeline 可以在 I/O 密集型操作(向量检索、LLM 调用)中充分利用并发能力:

from fastapi import FastAPI, Depends
from fastapi.responses import StreamingResponse
from pydantic import BaseModel

app = FastAPI(title="Security Knowledge Base API", version="1.0.0")


class QueryRequest(BaseModel):
    question: str
    session_id: str | None = None
    filters: dict | None = None


class QueryResponse(BaseModel):
    answer: str
    sources: list[dict]
    confidence: float
    session_id: str


@app.post("/api/v1/ask", response_model=QueryResponse)
async def ask_question(
    request: QueryRequest,
    user=Depends(get_current_user),
):
    result = await rag_chain.ainvoke(
        query=request.question,
        filters=request.filters,
        user_access_level=user.access_level,
    )
    return QueryResponse(
        answer=result["answer"],
        sources=result["sources"],
        confidence=result["confidence"],
        session_id=result["session_id"],
    )


@app.post("/api/v1/ask/stream")
async def ask_question_stream(
    request: QueryRequest,
    user=Depends(get_current_user),
):
    async def generate():
        async for event in rag_chain.astream(
            query=request.question,
            filters=request.filters,
            user_access_level=user.access_level,
        ):
            yield f"data: {event.json()}\n\n"
        yield "data: [DONE]\n\n"

    return StreamingResponse(generate(), media_type="text/event-stream")

备选方案与放弃理由

方案放弃理由
Django REST Framework同步模型不适合流式输出场景,异步支持需要额外适配
Flask生态成熟但缺乏类型安全和自动文档生成,对团队协作不友好
gRPC + FastAPI过度设计,初期 WebSocket/SSE 已满足需求

3.4 前端框架:React

选择 React 主要基于组件化模型对复杂对话界面的适配能力——对话消息流、文档预览面板、反馈组件、管理后台都可以拆分为独立组件。结合 TypeScript 的类型安全和 Ant Design 的企业级 UI 组件库,可以快速构建符合安全团队审美(简洁、高效、信息密度高)的交互界面。


四、文档处理管线:从原始文件到可检索的知识

4.1 管线总览

文档处理是 RAG 系统质量的基石。安全知识库的文档来源多样——PDF 漏洞报告包含复杂排版和表格,Markdown 格式的 Playbook 包含代码块和嵌套列表,HTML 格式的厂商通告包含嵌套标签。每种格式都需要专门的解析策略。

原始文档 ──→ 格式解析 ──→ 内容清洗 ──→ 语义分块 ──→ 向量化 ──→ 索引入库
  │              │              │              │            │           │
  │         PDF/MD/HTML     去噪/去重      递归分割     Embedding   PGVector
  │         飞书/Confluence  元数据提取    上下文保持     批量处理    元数据关联
  │                                                  维度管理

4.2 核心实现

import hashlib
from pathlib import Path
from langchain.schema import Document


class SecurityDocProcessor:
    def __init__(self, embeddings, vector_store, text_splitter):
        self.embeddings = embeddings
        self.vector_store = vector_store
        self.text_splitter = text_splitter

    async def process_document(
        self, file_path: Path, metadata: dict
    ) -> dict:
        loader_class = LOADERS.get(file_path.suffix.lower())
        if not loader_class:
            raise ValueError(f"Unsupported format: {file_path.suffix}")

        loader = loader_class(str(file_path))
        raw_docs = loader.load()

        cleaned_docs = self._clean_documents(raw_docs)
        chunks = self.text_splitter.split_documents(cleaned_docs)

        for i, chunk in enumerate(chunks):
            chunk.metadata.update({
                **metadata,
                "chunk_index": i,
                "total_chunks": len(chunks),
                "source_file": file_path.name,
                "content_hash": hashlib.md5(
                    chunk.page_content.encode()
                ).hexdigest(),
            })

        ids = [str(uuid4()) for _ in chunks]
        await self.vector_store.aadd_documents(chunks, ids=ids)

        return {
            "file": file_path.name,
            "chunks": len(chunks),
            "total_tokens": sum(
                len(c.page_content) for c in chunks
            ),
        }

    def _clean_documents(self, docs: list[Document]) -> list[Document]:
        cleaned = []
        seen_hashes = set()
        for doc in docs:
            content = doc.page_content.strip()
            if not content or len(content) < 20:
                continue
            content_hash = hashlib.md5(content.encode()).hexdigest()
            if content_hash in seen_hashes:
                continue
            seen_hashes.add(content_hash)
            cleaned.append(Document(
                page_content=content,
                metadata=doc.metadata,
            ))
        return cleaned

4.3 分块策略:安全文档的特殊考量

安全文档的分块策略与通用文档有显著差异。漏洞通告通常包含"漏洞描述 → 影响范围 → 修复方案"的固定结构,如果分块边界恰好切断了这个结构,检索到的片段将失去完整语义。因此我们采用基于标题层级的分块策略,优先在 ##### 标题处分割,其次才是按 Token 长度分割:

from langchain.text_splitter import MarkdownHeaderTextSplitter

headers_to_split = [
    ("#", "h1"),
    ("##", "h2"),
    ("###", "h3"),
]

markdown_splitter = MarkdownHeaderTextSplitter(
    headers_to_split_on=headers_to_split,
    strip_headers=False,
)

def split_security_doc(text: str, doc_type: str) -> list[Document]:
    if doc_type in ("playbook", "vulnerability_advisory"):
        md_sections = markdown_splitter.split_text(text)
        final_chunks = []
        for section in md_sections:
            if len(section.page_content) > MAX_CHUNK_SIZE:
                sub_chunks = text_splitter.split_documents([section])
                final_chunks.extend(sub_chunks)
            else:
                final_chunks.append(section)
        return final_chunks
    else:
        return text_splitter.split_text(text)

五、检索与生成:混合检索 + 重排序 + LLM 生成

5.1 混合检索策略

单一的向量检索无法覆盖所有查询场景。安全团队的查询模式大致分为两类:

  • 语义查询:“如何防御内网横向移动”——需要理解意图,匹配语义相关文档
  • 关键词查询:“CVE-2024-3094 xz 后门修复”——需要精确匹配 CVE 编号和特定术语

因此,系统同时使用向量检索(语义)和 BM25 关键词检索(精确),通过 RRF(Reciprocal Rank Fusion)进行分数融合:

from langchain.retrievers import EnsembleRetriever
from langchain_community.retrievers import BM25Retriever


def build_hybrid_retriever(vector_store, documents, k=10):
    vector_retriever = vector_store.as_retriever(
        search_type="similarity",
        search_kwargs={"k": k},
    )

    bm25_retriever = BM25Retriever.from_documents(documents)
    bm25_retriever.k = k

    ensemble_retriever = EnsembleRetriever(
        retrievers=[vector_retriever, bm25_retriever],
        weights=[0.6, 0.4],
    )
    return ensemble_retriever

权重调优经验:向量检索权重 0.6、BM25 权重 0.4 是经过评测验证的初始值。对于纯技术漏洞查询(如搜索 CVE 编号),BM25 的权重可以动态提升至 0.7;对于策略类咨询(如"如何设计零信任架构"),向量检索权重应提升至 0.8。这一动态权重调整通过查询分类器实现。

5.2 重排序(Reranking)

混合检索返回的候选集可能包含 20-30 个片段,直接全部送入 LLM 不仅浪费 Token 成本,还会因上下文过长导致注意力分散。使用 BGE-Reranker-v2-m3 对候选集进行精排,仅保留 Top-5 最相关片段:

from sentence_transformers import CrossEncoder

reranker = CrossEncoder("BAAI/bge-reranker-v2-m3")


def rerank_candidates(
    query: str, candidates: list[Document], top_k: int = 5
) -> list[Document]:
    pairs = [(query, doc.page_content) for doc in candidates]
    scores = reranker.predict(pairs)

    scored_docs = list(zip(candidates, scores))
    scored_docs.sort(key=lambda x: x[1], reverse=True)

    return [doc for doc, score in scored_docs[:top_k]]

5.3 安全问答 Prompt 模板

安全领域的 Prompt 设计需要特别注意两点:强制引用来源不确定时明确拒绝。一个幻觉的安全建议可能比没有建议更危险:

SECURITY_QA_TEMPLATE = """你是一个专业的网络安全知识助手。基于以下检索到的安全知识片段回答用户的问题。

重要规则:
1. 只基于提供的知识片段回答,不要使用你自己的知识
2. 每个关键陈述必须标注来源文档编号 [1], [2] 等
3. 如果知识片段中没有相关信息,明确告知用户"当前知识库中未找到相关资料"
4. 如果答案涉及具体操作步骤,请按顺序列出
5. 如果存在多种方案,请分别说明适用场景
6. 不要给出未经验证的安全建议

检索到的知识片段:
{context}

用户问题:{question}

回答:"""

5.4 完整 RAG Chain

from langchain.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough


def build_rag_chain(retriever, llm):
    prompt = ChatPromptTemplate.from_template(SECURITY_QA_TEMPLATE)

    def format_docs(docs):
        formatted = []
        for i, doc in enumerate(docs, 1):
            source = doc.metadata.get("source_file", "unknown")
            title = doc.metadata.get("title", "untitled")
            formatted.append(
                f"[{i}] 来源: {source} | 标题: {title}\n"
                f"{doc.page_content}"
            )
        return "\n\n".join(formatted)

    chain = (
        {
            "context": retriever | format_docs,
            "question": RunnablePassthrough(),
        }
        | prompt
        | llm
        | StrOutputParser()
    )
    return chain

六、前端架构:React 对话界面与流式输出

6.1 核心交互模型

前端采用 React + TypeScript + Ant Design 技术栈,核心交互包括三个层面:

  • 对话界面:支持 Markdown 渲染、代码高亮、流式打字效果
  • 来源面板:点击引用编号可展开查看原始文档片段和来源链接
  • 反馈机制:每条回答支持"有用/无用/有害"三级反馈,驱动检索质量持续优化

6.2 流式输出实现

SSE(Server-Sent Events)是流式输出的最佳选择——它比 WebSocket 更简单(单向通信即可),且天然支持 HTTP 语义和断线重连:

import React, { useState, useCallback } from "react";
import { Input, Button, Spin, Typography } from "antd";
import ReactMarkdown from "react-markdown";

const { Text } = Typography;

interface Source {
  doc_id: string;
  title: string;
  source_file: string;
  score: number;
  content: string;
}

interface ChatMessage {
  role: "user" | "assistant";
  content: string;
  sources?: Source[];
  isStreaming?: boolean;
}

function ChatInterface() {
  const [messages, setMessages] = useState<ChatMessage[]>([]);
  const [inputValue, setInputValue] = useState("");
  const [isLoading, setIsLoading] = useState(false);

  const handleSend = useCallback(async () => {
    if (!inputValue.trim() || isLoading) return;

    const userMessage: ChatMessage = {
      role: "user",
      content: inputValue,
    };

    const assistantMessage: ChatMessage = {
      role: "assistant",
      content: "",
      isStreaming: true,
    };

    setMessages((prev) => [...prev, userMessage, assistantMessage]);
    setInputValue("");
    setIsLoading(true);

    try {
      const response = await fetch("/api/v1/ask/stream", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ question: inputValue }),
      });

      const reader = response.body?.getReader();
      const decoder = new TextDecoder();

      while (true) {
        const { done, value } = await reader!.read();
        if (done) break;

        const chunk = decoder.decode(value);
        const lines = chunk.split("\n").filter((line) => line.startsWith("data: "));

        for (const line of lines) {
          const data = line.slice(6);
          if (data === "[DONE]") break;

          const event = JSON.parse(data);
          if (event.type === "token") {
            setMessages((prev) => {
              const updated = [...prev];
              const last = updated[updated.length - 1];
              updated[updated.length - 1] = {
                ...last,
                content: last.content + event.content,
              };
              return updated;
            });
          } else if (event.type === "sources") {
            setMessages((prev) => {
              const updated = [...prev];
              updated[updated.length - 1] = {
                ...updated[updated.length - 1],
                sources: event.sources,
                isStreaming: false,
              };
              return updated;
            });
          }
        }
      }
    } finally {
      setIsLoading(false);
    }
  }, [inputValue, isLoading]);

  return (
    <div style={{ display: "flex", height: "100vh" }}>
      <div style={{ flex: 1, display: "flex", flexDirection: "column" }}>
        <div style={{ flex: 1, overflow: "auto", padding: 24 }}>
          {messages.map((msg, idx) => (
            <div key={idx} style={{ marginBottom: 16 }}>
              <Text strong>{msg.role === "user" ? "我" : "安全助手"}</Text>
              <ReactMarkdown>{msg.content}</ReactMarkdown>
              {msg.sources && <SourcePanel sources={msg.sources} />}
            </div>
          ))}
        </div>
        <div style={{ padding: 16, borderTop: "1px solid #f0f0f0" }}>
          <Input.Search
            value={inputValue}
            onChange={(e) => setInputValue(e.target.value)}
            enterButton="发送"
            onSearch={handleSend}
            loading={isLoading}
            placeholder="输入安全相关问题..."
          />
        </div>
      </div>
    </div>
  );
}

6.3 来源引用与反馈组件

来源面板是安全知识库区别于通用聊天机器人的关键差异——它让每条回答都可追溯、可验证:

function SourcePanel({ sources }: { sources: Source[] }) {
  const [expandedIdx, setExpandedIdx] = useState<number | null>(null);

  return (
    <div style={{ marginTop: 8 }}>
      <Text type="secondary" style={{ fontSize: 12 }}>
        参考来源 ({sources.length})
      </Text>
      {sources.map((source, idx) => (
        <div
          key={source.doc_id}
          style={{
            padding: "4px 8px",
            margin: "4px 0",
            borderRadius: 4,
            background: "#fafafa",
            cursor: "pointer",
          }}
          onClick={() => setExpandedIdx(expandedIdx === idx ? null : idx)}
        >
          <Text style={{ fontSize: 12 }}>
            [{idx + 1}] {source.title}
            <Text type="secondary"> ({source.source_file})</Text>
            <Text type="secondary"> 相似度: {(source.score * 100).toFixed(1)}%</Text>
          </Text>
          {expandedIdx === idx && (
            <div style={{ marginTop: 4, fontSize: 12, color: "#666" }}>
              {source.content}
            </div>
          )}
        </div>
      ))}
      <FeedbackButtons sources={sources} />
    </div>
  );
}

七、部署架构:从开发到生产

7.1 Docker Compose 本地开发环境

version: "3.9"

services:
  api:
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - "8000:8000"
    environment:
      - DATABASE_URL=postgresql://admin:secret@db:5432/security_kb
      - OPENAI_API_KEY=${OPENAI_API_KEY}
      - REDIS_URL=redis://cache:6379/0
    depends_on:
      db:
        condition: service_healthy
      cache:
        condition: service_started
    volumes:
      - ./uploads:/app/uploads
    command: uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload

  web:
    build:
      context: ./frontend
      dockerfile: Dockerfile
    ports:
      - "3000:3000"
    environment:
      - REACT_APP_API_URL=http://localhost:8000
    depends_on:
      - api

  db:
    image: pgvector/pgvector:pg16
    ports:
      - "5432:5432"
    environment:
      POSTGRES_DB: security_kb
      POSTGRES_USER: admin
      POSTGRES_PASSWORD: secret
    volumes:
      - pgdata:/var/lib/postgresql/data
      - ./sql/init.sql:/docker-entrypoint-initdb.d/init.sql
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U admin -d security_kb"]
      interval: 5s
      timeout: 5s
      retries: 5

  cache:
    image: redis:7-alpine
    ports:
      - "6379:6379"

  worker:
    build:
      context: .
      dockerfile: Dockerfile
    environment:
      - DATABASE_URL=postgresql://admin:secret@db:5432/security_kb
      - OPENAI_API_KEY=${OPENAI_API_KEY}
    depends_on:
      - db
      - cache
    command: celery -A app.tasks worker --loglevel=info --concurrency=4

volumes:
  pgdata:

7.2 生产部署关键考量

维度开发环境生产环境迁移要点
数据库单实例 PGVector主从复制 + 读写分离向量索引构建在从库执行,避免影响主库写入性能
LLM 调用直连 OpenAI API通过 LiteLLM Proxy 池化管理多 Key 轮询、速率限制、成本归因
文件存储本地磁盘MinIO / 阿里云 OSS保证文档持久化和跨节点共享
任务队列内存 CeleryRedis + Celery 集群文档处理和索引构建异步化
监控日志输出Prometheus + Grafana关键指标:检索延迟、LLM Token 消耗、用户满意度
HTTPSNginx 反向代理 + Let’s Encrypt全链路加密,尤其是 LLM API 通信

7.3 性能优化策略

语义缓存:对于高频重复查询(如"什么是 SQL 注入"),将查询结果缓存在 Redis 中,TTL 设置为 24 小时。缓存命中率通常在 15-25% 之间,可以显著降低 LLM 调用成本。

import hashlib
import json
from redis.asyncio import Redis


class SemanticCache:
    def __init__(self, redis: Redis, ttl: int = 86400):
        self.redis = redis
        self.ttl = ttl

    async def get(self, query: str) -> dict | None:
        key = f"rag_cache:{hashlib.sha256(query.encode()).hexdigest()}"
        cached = await self.redis.get(key)
        return json.loads(cached) if cached else None

    async def set(self, query: str, result: dict):
        key = f"rag_cache:{hashlib.sha256(query.encode()).hexdigest()}"
        await self.redis.setex(key, self.ttl, json.dumps(result))

增量索引:基于文档内容哈希判断是否需要重新索引,避免重复计算 Embedding 向量。对于已存在的文档,仅处理新增或修改的片段。


八、质量评测:RAGAS 指标与持续改进

8.1 RAGAS 评测框架

RAGAS(Retrieval Augmented Generation Assessment)是目前最主流的 RAG 系统评测框架,涵盖四个核心指标:

指标含义测量目标目标值
Faithfulness(忠实度)生成内容是否忠于检索到的上下文幻觉检测> 0.85
Answer Relevancy(答案相关性)回答是否与问题相关生成质量> 0.80
Context Precision(上下文精确度)检索到的上下文中有多少是相关的检索精确率> 0.75
Context Recall(上下文召回率)相关上下文是否被检索到检索召回率> 0.80

8.2 评测数据集构建

构建评测集是质量保障的第一步。安全知识库的评测集需要覆盖多种查询类型:

eval_dataset = [
    {
        "question": "如何检测 Redis 未授权访问漏洞?",
        "ground_truth": "通过执行 redis-cli INFO 命令检查服务信息...",
        "contexts": ["Redis 安全加固指南 v2.1 第三章..."],
        "metadata": {"category": "vulnerability_detection", "difficulty": "easy"},
    },
    {
        "question": "内网横向移动的常见手法有哪些?",
        "ground_truth": "横向移动手法包括:Pass-the-Hash、WMI远程执行...",
        "contexts": ["横向移动检测与防御 Playbook..."],
        "metadata": {"category": "threat_analysis", "difficulty": "hard"},
    },
    {
        "question": "CVE-2024-3094 影响范围和修复方案",
        "ground_truth": "XZ Utils 5.6.0 和 5.6.1 版本存在后门...",
        "contexts": ["CVE-2024-3094 应急通告..."],
        "metadata": {"category": "vulnerability_response", "difficulty": "medium"},
    },
]

8.3 用户反馈闭环

仅靠离线评测无法覆盖所有长尾场景。系统内置三级反馈机制,每条回答下方提供"有用/无用/有害"按钮:

from fastapi import APIRouter

router = APIRouter()


@router.post("/api/v1/feedback")
async def submit_feedback(
    feedback: FeedbackRequest,
    user=Depends(get_current_user),
):
    await db.execute(
        """INSERT INTO feedback
           (session_id, message_id, rating, comment, user_id, created_at)
           VALUES ($1, $2, $3, $4, $5, NOW())""",
        feedback.session_id,
        feedback.message_id,
        feedback.rating,
        feedback.comment,
        user.id,
    )

    if feedback.rating == "harmful":
        await alert_service.notify_security_team(
            message_id=feedback.message_id,
            reason=feedback.comment,
        )

    return {"status": "recorded"}

反馈数据驱动三个改进循环:

  • 短期(天级):“有害"反馈触发人工审查,快速修正错误回答
  • 中期(周级):“无用"反馈聚类分析,识别检索盲区并补充知识文档
  • 长期(月级):全量反馈数据用于 RAGAS 重新评测,量化系统质量趋势

九、系统架构图与项目结构

9.1 完整数据流

用户提问
    │
    ▼
┌─────────────┐    ┌─────────────┐    ┌──────────────┐
│ 查询改写     │ →  │ 查询分类     │ →  │ 路由分发      │
│ (HyDE/扩展)  │    │ (意图识别)   │    │ (检索策略)    │
└─────────────┘    └─────────────┘    └──────┬───────┘
                                             │
                                    ┌────────┴────────┐
                                    │                 │
                                    ▼                 ▼
                              ┌──────────┐    ┌──────────┐
                              │ 向量检索  │    │ BM25检索  │
                              │ (语义)    │    │ (关键词)  │
                              └────┬─────┘    └────┬─────┘
                                   │               │
                                   └───────┬───────┘
                                           ▼
                                   ┌──────────────┐
                                   │  RRF 分数融合  │
                                   └──────┬───────┘
                                          ▼
                                   ┌──────────────┐
                                   │  BGE 重排序    │
                                   └──────┬───────┘
                                          ▼
                                   ┌──────────────┐
                                   │  上下文构建    │
                                   └──────┬───────┘
                                          ▼
                                   ┌──────────────┐
                                   │  LLM 流式生成  │
                                   └──────┬───────┘
                                          ▼
                                   ┌──────────────┐
                                   │  来源标注输出   │
                                   └──────────────┘

9.2 项目目录结构

security-kb-rag/
├── backend/
│   ├── app/
│   │   ├── main.py              # FastAPI 入口
│   │   ├── api/                 # 路由定义
│   │   │   ├── ask.py           # 问答接口
│   │   │   ├── documents.py     # 文档管理接口
│   │   │   └── feedback.py      # 反馈接口
│   │   ├── core/
│   │   │   ├── config.py        # 配置管理
│   │   │   ├── security.py      # 认证鉴权
│   │   │   └── dependencies.py  # 依赖注入
│   │   ├── rag/
│   │   │   ├── chain.py         # RAG Chain 编排
│   │   │   ├── retriever.py     # 混合检索器
│   │   │   ├── reranker.py      # 重排序模块
│   │   │   ├── prompt.py        # Prompt 模板
│   │   │   └── cache.py         # 语义缓存
│   │   ├── document/
│   │   │   ├── loader.py        # 多格式文档加载
│   │   │   ├── splitter.py      # 分块策略
│   │   │   └── processor.py     # 文档处理管线
│   │   └── models/
│   │       ├── document.py      # 文档数据模型
│   │       └── feedback.py      # 反馈数据模型
│   ├── sql/
│   │   └── init.sql             # 数据库初始化
│   ├── Dockerfile
│   └── requirements.txt
├── frontend/
│   ├── src/
│   │   ├── components/
│   │   │   ├── ChatInterface.tsx
│   │   │   ├── SourcePanel.tsx
│   │   │   └── FeedbackButtons.tsx
│   │   ├── hooks/
│   │   │   └── useStreamChat.ts
│   │   ├── services/
│   │   │   └── api.ts
│   │   └── App.tsx
│   ├── Dockerfile
│   └── package.json
├── docker-compose.yml
├── docker-compose.prod.yml
└── README.md                     # <!-- [TODO](link-to-repo) -->

GitHub 仓库


十、延伸阅读