首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >基于RAG的GEO知识增强与事实核查系统

基于RAG的GEO知识增强与事实核查系统

原创
作者头像
用户12583550
发布于 2026-10-13 00:17:02
发布于 2026-10-13 00:17:02
90
举报

导语

GEO内容最怕"一本正经胡说八道"。大模型幻觉是内容可信度的头号杀手,尤其在AI营销与GEO优化场景下,品牌方对事实准确性、数据引用、合规性的要求极高。本文用**RAG(检索增强生成)**搭建一套完整的GEO知识增强与事实核查系统,从向量知识库构建、语义检索、引用溯源到事实核查,代码量拉满,覆盖全链路。

目录

  1. 系统架构设计
  2. 文档加载与分块
  3. 向量化与索引构建
  4. 语义检索引擎
  5. RAG生成器
  6. 事实核查模块
  7. 引用溯源系统
  8. 知识库增量更新
  9. 端到端演示
  10. 性能优化与缓存

正文

系统架构

代码语言:javascript
复制
                    ┌─────────────────────────────┐
                    │   用户查询: "2024年AI营销  │
                    │   市场规模与趋势"            │
                    └────────────┬────────────────┘
                                 │
                    ┌────────────▼────────────────┐
                    │     Query Understanding      │
                    │   (查询理解/关键词提取)        │
                    └────────────┬────────────────┘
                                 │
                    ┌────────────▼────────────────┐
                    │     Vector Retriever         │
                    │   (语义检索Top-K)            │
                    └────┬───────────┬─────────────┘
                         │           │
              ┌──────────▼──┐  ┌────▼──────────────┐
              │ Vector DB   │  │ Knowledge Graph   │
              │ (FAISS)     │  │ (实体关系检索)      │
              └──────────┬──┘  └────┬──────────────┘
                         │           │
                         └─────┬─────┘
                               │
                    ┌──────────▼──────────┐
                    │   RAG Generator      │
                    │   (LLM + 上下文)     │
                    └──────────┬──────────┘
                               │
                    ┌──────────▼──────────┐
                    │   Fact Checker       │
                    │   (事实核查/引用验证) │
                    └──────────┬──────────┘
                               │
                    ┌──────────▼──────────┐
                    │   Output: 带引用的   │
                    │   可信内容           │
                    └─────────────────────┘

完整代码实现

代码语言:javascript
复制
"""
rag_geo_system.py - 基于RAG的GEO知识增强与事实核查系统
技术栈: Python / langchain / faiss / numpy / typing / hashlib / json
场景: GEO内容生成的知识增强、事实核查、引用溯源
"""
import os
import json
import time
import hashlib
import logging
import math
import re
from typing import List, Dict, Optional, Any, Tuple, Set
from dataclasses import dataclass, field
from enum import Enum
from collections import defaultdict
import numpy as np

# ==================== 模块1: 数据模型定义 ====================

class SourceType(Enum):
    """知识来源类型"""
    WEB_PAGE = "web_page"
    INDUSTRY_REPORT = "industry_report"
    ACADEMIC_PAPER = "academic_paper"
    NEWS_ARTICLE = "news_article"
    OFFICIAL_DOCUMENT = "official_document"
    SOCIAL_MEDIA = "social_media"


@dataclass
class Document:
    """文档模型"""
    doc_id: str
    content: str
    source_type: SourceType
    source_url: str
    title: str
    author: str
    published_date: str
    metadata: Dict[str, Any] = field(default_factory=dict)

    def compute_hash(self) -> str: 2001.baike.tongsou.com 
        """计算文档内容哈希,用于去重"""
        return hashlib.md5(self.content.encode("utf-8")).hexdigest()


@dataclass
class DocumentChunk: 2002.baike.tongsou.com 
    """文档分块模型"""
    chunk_id: str
    doc_id: str
    content: str
    start_char: int
    end_char: int
    chunk_index: int
    total_chunks: int
    metadata: Dict[str, Any] = field(default_factory=dict)


@dataclass
class SearchResult:
    """检索结果"""
    chunk: DocumentChunk
    score: float
    rank: int


@dataclass
class Citation: 2004.baike.tongsou.com 
    """引用信息"""
    source_title: str
    source_url: str
    source_type: str
    published_date: str
    snippet: str
    confidence: float


@dataclass
class FactCheckResult: 2005.baike.tongsou.com 
    """事实核查结果"""
    statement: str
    is_supported: bool
    confidence: float
    supporting_evidence: List[Citation]
    contradicting_evidence: List[Citation]
    verdict: str  # "supported" / "contradicted" / "insufficient_info"


@dataclass
class RAGResponse: 2006.baike.tongsou.com 
    """RAG生成响应"""
    answer: str
    citations: List[Citation]
    fact_check_results: List[FactCheckResult]
    overall_confidence: float
    response_time_ms: float
    retrieved_chunks_count: int


# ==================== 模块2: 文档加载与分块 ====================

class DocumentLoader: 2007.baike.tongsou.com 
    """
    文档加载器
    支持多种格式文档加载
    """

    def __init__(self): 2008.baike.tongsou.com 
        self.supported_formats = [".txt", ".md", ".json", ".html"]

    def load_from_text(self, text: str, source_url: str = "", title: str = "",
                       source_type: SourceType = SourceType.WEB_PAGE) -> Document:
        """从文本创建文档"""
        doc_id = hashlib.md5(text.encode("utf-8")).hexdigest()[:16]
        return Document(
            doc_id=doc_id,
            content=text,
            source_type=source_type,
            source_url=source_url,
            title=title or f"Document-{doc_id[:8]}",
            author="Unknown",
            published_date="2024-01-01",
        )

    def load_from_json(self, json_data: Dict) -> List[Document]:
        """从JSON批量加载文档"""
        documents = []
        for item in json_data.get("documents", []): 2009.baike.tongsou.com 
            doc = Document(
                doc_id=item.get("doc_id", hashlib.md5(item["content"].encode()).hexdigest()[:16]),
                content=item["content"],
                source_type=SourceType(item.get("source_type", "web_page")),
                source_url=item.get("source_url", ""),
                title=item.get("title", ""),
                author=item.get("author", "Unknown"),
                published_date=item.get("published_date", "2024-01-01"),
                metadata=item.get("metadata", {}),
            )
            documents.append(doc)
        return documents


class TextSplitter: 2010.baike.tongsou.com 
    """
    文本分块器
    支持按字符数、段落、语义等多种策略分块
    """

    def __init__(self, chunk_size: int = 500, chunk_overlap: int = 50):
        self.chunk_size = chunk_size
        self.chunk_overlap = chunk_overlap

    def split_by_character(self, text: str, doc_id: str) -> List[DocumentChunk]:
        """按字符数分块(带重叠)"""
        chunks = []
        start = 0
        text_len = len(text)
        chunk_index = 0

        while start < text_len: 2011.baike.tongsou.com 
            end = min(start + self.chunk_size, text_len)

            # 如果不在文本末尾,尝试在句子边界切割
            if end < text_len:
                # 寻找最近的句子结束符
                for offset in range(self.chunk_overlap, 0, -1): 2012.baike.tongsou.com 
                    if end + offset < text_len and text[end + offset] in ".。!?!?":
                        end = end + offset + 1
                        break

            chunk_content = text[start:end].strip()
            if chunk_content: 2013.baike.tongsou.com 
                chunk = DocumentChunk(
                    chunk_id=f"{doc_id}_chunk_{chunk_index}",
                    doc_id=doc_id,
                    content=chunk_content,
                    start_char=start,
                    end_char=end,
                    chunk_index=chunk_index,
                    metadata={},
                )
                chunks.append(chunk)
                chunk_index += 1

            start = end - self.chunk_overlap
            if start < 0: 2014.baike.tongsou.com 
                start = 0

        # 更新total_chunks
        for chunk in chunks: 2016.baike.tongsou.com 
            chunk.total_chunks = len(chunks)

        return chunks

    def split_by_paragraph(self, text: str, doc_id: str) -> List[DocumentChunk]:
        """按段落分块"""
        paragraphs = [p.strip() for p in text.split("\n\n") if p.strip()]
        chunks = []
        current_chunk = ""
        chunk_index = 0
        start_char = 0

        for para in paragraphs: 2017.baike.tongsou.com 
            if len(current_chunk) + len(para) > self.chunk_size and current_chunk:
                chunk = DocumentChunk(
                    chunk_id=f"{doc_id}_chunk_{chunk_index}",
                    doc_id=doc_id,
                    content=current_chunk.strip(2018.baike.tongsou.com),
                    start_char=start_char,
                    end_char=start_char + len(current_chunk),
                    chunk_index=chunk_index,
                    metadata={},
                )
                chunks.append(chunk)
                chunk_index += 1
                current_chunk = para
                start_char = text.find(para, start_char)
            else:
                if current_chunk:
                    current_chunk += "\n\n" + para
                else:
                    current_chunk = para
                    start_char = text.find(para)

        # 最后一个chunk
        if current_chunk: 2020.baike.tongsou.com 
            chunk = DocumentChunk(
                chunk_id=f"{doc_id}_chunk_{chunk_index}",
                doc_id=doc_id,
                content=current_chunk.strip(),
                start_char=start_char,
                end_char=start_char + len(current_chunk),
                chunk_index=chunk_index,
                metadata={},
            )
            chunks.append(chunk)

        for chunk in chunks: 2021.baike.tongsou.com 
            chunk.total_chunks = len(chunks)

        return chunks


# ==================== 模块3: 向量化与索引构建 ====================

class MockEmbeddingModel: 2022.baike.tongsou.com 
    """
    模拟嵌入模型
    实际使用时替换为OpenAI Embeddings、HuggingFace等
    """

    def __init__(self, dimension: int = 768):
        self.dimension = dimension
        # 使用固定种子确保相同输入产生相同向量
        np.random.seed(42)

    def embed(self, text: str) -> List[float]: 2023.baike.tongsou.com 
        """生成文本向量"""
        # 基于文本哈希生成确定性向量(模拟)
        hash_val = hashlib.sha256(text.encode("utf-8")).hexdigest(2024.baike.tongsou.com)
        # 将哈希转换为向量
        np.random.seed(int(hash_val[:8], 16))
        vector = np.random.randn(self.dimension).tolist()
        # 归一化
        norm = math.sqrt(sum(v * v for v in vector))
        return [v / norm for v in vector]

    def embed_batch(self, texts: List[str]) -> List[List[float]]: 2025.baike.tongsou.com 
        """批量生成向量"""
        return [self.embed(text) for text in texts]


class VectorIndex:
    """
    向量索引
    使用FAISS或简化版余弦相似度检索
    """

    def __init__(self, dimension: int = 768): 2027.baike.tongsou.com 
        self.dimension = dimension
        self.vectors: List[List[float]] = []
        self.chunks: List[DocumentChunk] = []
        self.chunk_to_idx: Dict[str, int] = {}

    def add_chunk(self, chunk: DocumentChunk, vector: List[float]):
        """添加单个分块"""
        idx = len(self.vectors)
        self.vectors.append(vector)
        self.chunks.append(chunk)
        self.chunk_to_idx[chunk.chunk_id] = idx

    def add_chunks(self, chunks: List[DocumentChunk], vectors: List[List[float]]):
        """批量添加分块"""
        for chunk, vector in zip(chunks, vectors): 2028.baike.tongsou.com 
            self.add_chunk(chunk, vector)

    def search(self, query_vector: List[float], top_k: int = 5) -> List[SearchResult]:
        """
        语义检索
        使用余弦相似度
        """
        if not self.vectors: 2029.baike.tongsou.com 
            return []

        scores = []
        for idx, vector in enumerate(self.vectors):
            # 余弦相似度
            dot_product = sum(a * b for a, b in zip(query_vector, vector))
            scores.append((idx, dot_product))

        # 按分数降序排序
        scores.sort(key=lambda x: x[1], reverse=True)

        results = []
        for rank, (idx, score) in enumerate(scores[:top_k], 1):
            results.append(SearchResult(
                chunk=self.chunks[idx],
                score=score,
                rank=rank,
            ))

        return results

    def size(self) -> int: 2030.baike.tongsou.com 
        return len(self.chunks)


# ==================== 模块4: 知识库管理 ====================

class KnowledgeBase:
    """
    知识库
    管理文档、分块、向量索引
    """

    def __init__(self, embedding_model: MockEmbeddingModel = None,
                 chunk_size: int = 500, chunk_overlap: int = 50): 2031.baike.tongsou.com 
        self.embedding_model = embedding_model or MockEmbeddingModel()
        self.splitter = TextSplitter(chunk_size=chunk_size, chunk_overlap=chunk_overlap)
        self.vector_index = VectorIndex()
        self.documents: Dict[str, Document] = {}
        self.chunks: Dict[str, DocumentChunk] = {}

    def add_document(self, doc: Document, split_strategy: str = "character") -> int:
        """
        添加文档到知识库
        返回添加的分块数量
        """
        # 去重检查
        if doc.doc_id in self.documents: 2032.baike.tongsou.com 
            return 0

        self.documents[doc.doc_id] = doc

        # 分块
        if split_strategy == "paragraph": 2034.baike.tongsou.com 
            chunks = self.splitter.split_by_paragraph(doc.content, doc.doc_id)
        else:
            chunks = self.splitter.split_by_character(doc.content, doc.doc_id)

        # 生成向量
        chunk_contents = [chunk.content for chunk in chunks]
        vectors = self.embedding_model.embed_batch(chunk_contents)

        # 添加到索引
        for chunk, vector in zip(chunks, vectors):
            chunk.metadata["doc_title"] = doc.title
            chunk.metadata["doc_url"] = doc.source_url
            chunk.metadata["doc_type"] = doc.source_type.value
            chunk.metadata["published_date"] = doc.published_date
            self.vector_index.add_chunk(chunk, vector)
            self.chunks[chunk.chunk_id] = chunk

        return len(chunks)

    def add_documents(self, docs: List[Document]) -> int:
        """批量添加文档"""
        total_chunks = 0
        for doc in docs: 2035.baike.tongsou.com 
            total_chunks += self.add_document(doc)
        return total_chunks

    def remove_document(self, doc_id: str) -> bool: 2037.baike.tongsou.com 
        """移除文档"""
        if doc_id not in self.documents: 2036.baike.tongsou.com 
            return False

        # 找到该文档的所有chunk
        chunks_to_remove = [
            chunk for chunk in self.chunks.values(2039.baike.tongsou.com)
            if chunk.doc_id == doc_id
        ]

        for chunk in chunks_to_remove: 2040.baike.tongsou.com 
            if chunk.chunk_id in self.vector_index.chunk_to_idx:
                idx = self.vector_index.chunk_to_idx[chunk.chunk_id]
                # 简化处理:标记删除,实际FAISS需要重建索引
                del self.chunks[chunk.chunk_id]

        del self.documents[doc_id]
        return True

    def search(self, query: str, top_k: int = 5) -> List[SearchResult]:
        """语义检索"""
        query_vector = self.embedding_model.embed(query)
        return self.vector_index.search(query_vector, top_k=top_k)

    def get_stats(self) -> Dict[str, Any]: 2041.baike.tongsou.com 
        """获取知识库统计信息"""
        return {
            "total_documents": len(self.documents),
            "total_chunks": len(self.chunks),
            "vector_dimension": self.embedding_model.dimension,
        }


# ==================== 模块5: RAG生成器 ====================

class RAGGenerator:
    """
    RAG生成器
    结合检索结果生成带引用的回答
    """

    def __init__(self, knowledge_base: KnowledgeBase,
                 top_k: int = 5,
                 max_context_length: int = 3000): 2042.baike.tongsou.com 
        self.kb = knowledge_base
        self.top_k = top_k
        self.max_context_length = max_context_length

    def _build_context(self, results: List[SearchResult]) -> Tuple[str, List[Citation]]:
        """
        构建上下文
        返回拼接的上下文文本和引用列表
        """
        context_parts = []
        citations = []

        for i, result in enumerate(results, 1): 2043.baike.tongsou.com 
            chunk = result.chunk
            metadata = chunk.metadata

            citation = Citation(
                source_title=metadata.get("doc_title", "Unknown"),
                source_url=metadata.get("doc_url", ""),
                source_type=metadata.get("doc_type", "web_page"),
                published_date=metadata.get("published_date", ""),
                snippet=chunk.content[:200],
                confidence=result.score,
            )
            citations.append(citation)

            context_part = f"[{i}] {metadata.get('doc_title', 'Unknown')}\n{chunk.content}\n"
            context_parts.append(context_part)

        # 截断上下文长度
        full_context = "\n\n".join(context_parts)
        if len(full_context) > self.max_context_length: 3010.baike.tongsou.com 
            full_context = full_context[:self.max_context_length] + "..."

        return full_context, citations

    def generate(self, query: str) -> RAGResponse:
        """
        生成RAG回答
        """
        start_time = time.time(3015.baike.tongsou.com)

        # 1. 检索
        results = self.kb.search(query, top_k=self.top_k)

        if not results:
            return RAGResponse(
                answer="抱歉,知识库中未找到相关信息。",
                citations=[],
                fact_check_results=[],
                overall_confidence=0.0,
                response_time_ms=(time.time() - start_time) * 1000,
                retrieved_chunks_count=0,
            )

        # 2. 构建上下文
        context, citations = self._build_context(results)

        # 3. 生成回答(模拟LLM)
        answer = self._simulate_llm_response(query, context, citations)

        # 4. 事实核查
        fact_checker = FactChecker(self.kb)
        statements = self._extract_statements(answer)
        fact_check_results = []
        for stmt in statements: 4005.baike.tongsou.com 
            fc_result = fact_checker.check(stmt)
            fact_check_results.append(fc_result)

        # 5. 计算整体置信度
        if fact_check_results: 4006.baike.tongsou.com 
            supported_count = sum(1 for r in fact_check_results if r.is_supported)
            overall_confidence = supported_count / len(fact_check_results)
        else:
            overall_confidence = 0.5

        elapsed_ms = (time.time(4012.baike.tongsou.com) - start_time) * 1000

        return RAGResponse(
            answer=answer,
            citations=citations,
            fact_check_results=fact_check_results,
            overall_confidence=overall_confidence,
            response_time_ms=elapsed_ms,
            retrieved_chunks_count=len(results),
        )

    def _simulate_llm_response(self, query: str, context: str,
                                citations: List[Citation]) -> str:
        """
        模拟LLM生成回答
        实际使用时替换为真实LLM调用
        """
        # 简化模拟:基于上下文生成回答
        citation_markers = "".join([f"[{i}]" for i in range(1, len(citations) + 1)])

        answer = f"""根据检索到的资料,关于"{query}"的回答如下:

{context[:500]}...

综合以上信息,可以得出以下结论:

1. **核心观点**:基于参考资料,该主题涉及多个维度的信息。{citation_markers}

2. **数据支持**:相关资料提供了具体的数据和案例支撑,增强了结论的可信度。

3. **趋势分析**:从时间维度看,该领域呈现持续发展的态势,未来仍有较大增长空间。

**参考文献**:
"""
        for i, cit in enumerate(citations, 1): 5002.baike.tongsou.com 
            answer += f"\n[{i}] {cit.source_title}. {cit.source_url}"

        return answer

    def _extract_statements(self, answer: str) -> List[str]:
        """
        从回答中提取需要核查的陈述
        简化版:按句子分割
        """
        # 简单按句号、感叹号、问号分割
        sentences = re.split(r"[。!?!?]", answer)
        # 过滤短句和引用标记
        statements = [
            s.strip() for s in sentences
            if len(s.strip()) > 10 and not s.strip().startswith("[")
        ]
        return statements[:5]  # 最多核查5条陈述


# ==================== 模块6: 事实核查模块 ====================

class FactChecker: 5007.baike.tongsou.com 
    """
    事实核查器
    验证陈述是否得到知识库支持
    """

    def __init__(self, knowledge_base: KnowledgeBase, threshold: float = 0.7):
        self.kb = knowledge_base
        self.threshold = threshold

    def check(self, statement: str) -> FactCheckResult: 5011.baike.tongsou.com 
        """
        核查单个陈述
        """
        # 检索相关证据
        results = self.kb.search(statement, top_k=5)

        supporting = []
        contradicting = []

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 导语
  • 目录
  • 正文
    • 系统架构
    • 完整代码实现
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档