跳转到主内容

用 Claude API 构建 RAG 知识库问答系统:企业级实战指南

手把手教你基于 Claude API 200K 超长上下文构建企业 RAG 知识库,涵盖向量检索、Prompt 工程、成本控制全链路,含完整 Python 代码示例。

开发指南Claude APIRAG知识库问答预计阅读15分钟
2026.05.06 发表
用 Claude API 构建 RAG 知识库问答系统:企业级实战指南

用 Claude API 构建 RAG 知识库问答系统:企业级实战指南

企业内部文档、产品手册、合规政策……这些"死"在 PDF 和 Wiki 里的知识,每年让员工浪费数以千计的小时在重复搜索上。

RAG(Retrieval-Augmented Generation,检索增强生成)是目前最成熟的解决方案。而 Claude API 的 200K Token 超长上下文窗口,让它在 RAG 场景下比其他模型多出一张底牌:当检索到的内容较多时,不需要截断——Claude 能处理相当于一整本中篇小说的上下文。

本文从架构设计到生产部署,完整拆解如何用 Claude API 搭建一套企业级 RAG 问答系统。


为什么企业 RAG 选 Claude?

先说清楚优势,再动手。

能力 Claude Opus 4.6/4.7 GPT-4o 说明
上下文窗口 200K tokens 128K tokens Claude 多出 56%,减少截断丢失
长文档理解 极强 较强 Claude 在"大海捞针"测试中表现更稳定
指令遵循 极强 严格按格式输出,适合结构化场景
中文理解 优秀 优秀 两者相当
API 价格(via ClaudeAPI.com Opus: $4/$20 per 1M 性价比可接受

对于企业 RAG 场景,Sonnet 4.6($2.4/$12 per 1M tokens)通常已经足够,在 ClaudeAPI.com 注册后可以直接切换。


系统架构总览

三个核心环节:

  1. 离线索引:文档解析 → 分块 → 向量化 → 存入向量数据库
  2. 在线检索:用户问题向量化 → 相似度检索 → 召回 Top-K 文档块
  3. 生成回答:将检索结果 + 问题注入 Claude,生成有来源引用的答案



---

## 环境准备

```bash
pip install anthropic chromadb sentence-transformers pypdf langchain-text-splitters



---

## 环境准备

```bash
pip install anthropic chromadb sentence-transformers pypdf langchain-text-splitters

API 密钥配置(使用 ClaudeAPI.com 中转,无需翻墙):

import os
os.environ["ANTHROPIC_API_KEY"] = "your-api-key"
os.environ["ANTHROPIC_BASE_URL"] = "https://gw.claudeapi.com"
import os
os.environ["ANTHROPIC_API_KEY"] = "your-api-key"
os.environ["ANTHROPIC_BASE_URL"] = "https://gw.claudeapi.com"

第一步:文档解析与分块

文档分块策略直接影响检索质量,这是很多教程略过的关键细节。

from langchain_text_splitters import RecursiveCharacterTextSplitter
from pypdf import PdfReader
import re

def parse_pdf(file_path: str) -> str:
    """提取 PDF 文本,保留基本结构"""
    reader = PdfReader(file_path)
    pages = []
    for i, page in enumerate(reader.pages):
        text = page.extract_text()
        if text.strip():
            # 标注页码,便于后续引用
            pages.append(f"[第{i+1}页]\n{text}")
    return "\n\n".join(pages)


def chunk_document(text: str, chunk_size: int = 800, chunk_overlap: int = 150):
    """
    分块策略说明:
    - chunk_size=800:约 400-600 中文字,一个完整语义段落
    - chunk_overlap=150:相邻块有重叠,防止答案被切断
    - 按段落/换行优先切分,而非机械按字数
    """
    splitter = RecursiveCharacterTextSplitter(
        chunk_size=chunk_size,
        chunk_overlap=chunk_overlap,
        separators=["\n\n", "\n", "。", ";", ",", " ", ""],
    )
    chunks = splitter.split_text(text)
    return chunks


# 使用示例
raw_text = parse_pdf("company_policy.pdf")
chunks = chunk_document(raw_text)
print(f"文档共分为 {len(chunks)} 个块")
# 文档共分为 47 个块
from langchain_text_splitters import RecursiveCharacterTextSplitter
from pypdf import PdfReader
import re

def parse_pdf(file_path: str) -> str:
    """提取 PDF 文本,保留基本结构"""
    reader = PdfReader(file_path)
    pages = []
    for i, page in enumerate(reader.pages):
        text = page.extract_text()
        if text.strip():
            # 标注页码,便于后续引用
            pages.append(f"[第{i+1}页]\n{text}")
    return "\n\n".join(pages)


def chunk_document(text: str, chunk_size: int = 800, chunk_overlap: int = 150):
    """
    分块策略说明:
    - chunk_size=800:约 400-600 中文字,一个完整语义段落
    - chunk_overlap=150:相邻块有重叠,防止答案被切断
    - 按段落/换行优先切分,而非机械按字数
    """
    splitter = RecursiveCharacterTextSplitter(
        chunk_size=chunk_size,
        chunk_overlap=chunk_overlap,
        separators=["\n\n", "\n", "。", ";", ",", " ", ""],
    )
    chunks = splitter.split_text(text)
    return chunks


# 使用示例
raw_text = parse_pdf("company_policy.pdf")
chunks = chunk_document(raw_text)
print(f"文档共分为 {len(chunks)} 个块")
# 文档共分为 47 个块

企业实践建议:对于表格密集的财务报告,优先考虑用专门的表格解析库(如 camelot)单独处理表格部分,避免向量语义失真。


第二步:向量化与存储

from sentence_transformers import SentenceTransformer
import chromadb
from chromadb.config import Settings

# 推荐中文向量模型:BAAI/bge-large-zh-v1.5
# 英文或中英混合:BAAI/bge-m3(多语言)
embedding_model = SentenceTransformer("BAAI/bge-large-zh-v1.5")

# 初始化 ChromaDB(本地持久化)
chroma_client = chromadb.PersistentClient(path="./knowledge_base_db")
collection = chroma_client.get_or_create_collection(
    name="company_docs",
    metadata={"hnsw:space": "cosine"}  # 余弦相似度
)


def index_documents(chunks: list[str], doc_name: str):
    """将文档块向量化并存入数据库"""
    embeddings = embedding_model.encode(chunks, show_progress_bar=True).tolist()
    
    collection.add(
        documents=chunks,
        embeddings=embeddings,
        ids=[f"{doc_name}_chunk_{i}" for i in range(len(chunks))],
        metadatas=[{"source": doc_name, "chunk_index": i} for i in range(len(chunks))]
    )
    print(f"已索引 {len(chunks)} 个文档块,来源:{doc_name}")


# 索引文档
index_documents(chunks, "公司合规政策2026")
from sentence_transformers import SentenceTransformer
import chromadb
from chromadb.config import Settings

# 推荐中文向量模型:BAAI/bge-large-zh-v1.5
# 英文或中英混合:BAAI/bge-m3(多语言)
embedding_model = SentenceTransformer("BAAI/bge-large-zh-v1.5")

# 初始化 ChromaDB(本地持久化)
chroma_client = chromadb.PersistentClient(path="./knowledge_base_db")
collection = chroma_client.get_or_create_collection(
    name="company_docs",
    metadata={"hnsw:space": "cosine"}  # 余弦相似度
)


def index_documents(chunks: list[str], doc_name: str):
    """将文档块向量化并存入数据库"""
    embeddings = embedding_model.encode(chunks, show_progress_bar=True).tolist()
    
    collection.add(
        documents=chunks,
        embeddings=embeddings,
        ids=[f"{doc_name}_chunk_{i}" for i in range(len(chunks))],
        metadatas=[{"source": doc_name, "chunk_index": i} for i in range(len(chunks))]
    )
    print(f"已索引 {len(chunks)} 个文档块,来源:{doc_name}")


# 索引文档
index_documents(chunks, "公司合规政策2026")

第三步:检索 + Claude 生成答案

这是整个系统的核心。Prompt 工程决定了答案质量的上限。

import anthropic

client = anthropic.Anthropic(
    api_key=os.environ["ANTHROPIC_API_KEY"],
    base_url=os.environ["ANTHROPIC_BASE_URL"],
)


def retrieve(query: str, top_k: int = 5) -> list[dict]:
    """检索最相关的文档块"""
    query_embedding = embedding_model.encode([query]).tolist()
    results = collection.query(
        query_embeddings=query_embedding,
        n_results=top_k,
        include=["documents", "metadatas", "distances"]
    )
    
    retrieved = []
    for doc, meta, dist in zip(
        results["documents"][0],
        results["metadatas"][0],
        results["distances"][0]
    ):
        # 距离转相似度,过滤低质量结果
        similarity = 1 - dist
        if similarity > 0.4:  # 相似度阈值,可调整
            retrieved.append({
                "content": doc,
                "source": meta["source"],
                "chunk_index": meta["chunk_index"],
                "similarity": round(similarity, 3)
            })
    
    return retrieved


def build_context(retrieved_chunks: list[dict]) -> str:
    """将检索结果格式化为 Claude 可读的上下文"""
    if not retrieved_chunks:
        return "未找到相关文档。"
    
    context_parts = []
    for i, chunk in enumerate(retrieved_chunks, 1):
        context_parts.append(
            f"【参考文档 {i}】来源:{chunk['source']}(相关度:{chunk['similarity']}\n"
            f"{chunk['content']}"
        )
    
    return "\n\n---\n\n".join(context_parts)


SYSTEM_PROMPT = """你是一名企业知识库助手,基于提供的参考文档回答员工问题。

回答规则:
1. 只基于"参考文档"中的信息回答,不要编造或补充文档中没有的内容
2. 如果参考文档中没有相关信息,直接说"文档中未找到相关信息,建议咨询相关部门"
3. 回答时标注信息来源,例如:(来源:公司合规政策2026)
4. 使用清晰的结构化格式,重要信息用加粗标注
5. 回答简洁专业,避免冗余"""


def answer_question(question: str) -> dict:
    """完整的 RAG 问答流程"""
    # 1. 检索
    retrieved = retrieve(question, top_k=5)
    context = build_context(retrieved)
    
    # 2. 构建消息
    user_message = f"""参考文档:
{context}

---

员工问题:{question}"""
    
    # 3. 调用 Claude
    response = client.messages.create(
        model="claude-sonnet-4-6",  # 企业场景推荐 Sonnet,性价比最优
        max_tokens=1500,
        system=SYSTEM_PROMPT,
        messages=[{"role": "user", "content": user_message}]
    )
    
    answer = response.content[0].text
    
    return {
        "question": question,
        "answer": answer,
        "sources": list(set(c["source"] for c in retrieved)),
        "retrieved_count": len(retrieved),
        "input_tokens": response.usage.input_tokens,
        "output_tokens": response.usage.output_tokens,
    }


# 测试
result = answer_question("员工出差报销餐费的上限是多少?")
print(result["answer"])
print(f"\n📚 来源:{result['sources']}")
print(f"💰 消耗:{result['input_tokens']} 输入 + {result['output_tokens']} 输出 tokens")
import anthropic

client = anthropic.Anthropic(
    api_key=os.environ["ANTHROPIC_API_KEY"],
    base_url=os.environ["ANTHROPIC_BASE_URL"],
)


def retrieve(query: str, top_k: int = 5) -> list[dict]:
    """检索最相关的文档块"""
    query_embedding = embedding_model.encode([query]).tolist()
    results = collection.query(
        query_embeddings=query_embedding,
        n_results=top_k,
        include=["documents", "metadatas", "distances"]
    )
    
    retrieved = []
    for doc, meta, dist in zip(
        results["documents"][0],
        results["metadatas"][0],
        results["distances"][0]
    ):
        # 距离转相似度,过滤低质量结果
        similarity = 1 - dist
        if similarity > 0.4:  # 相似度阈值,可调整
            retrieved.append({
                "content": doc,
                "source": meta["source"],
                "chunk_index": meta["chunk_index"],
                "similarity": round(similarity, 3)
            })
    
    return retrieved


def build_context(retrieved_chunks: list[dict]) -> str:
    """将检索结果格式化为 Claude 可读的上下文"""
    if not retrieved_chunks:
        return "未找到相关文档。"
    
    context_parts = []
    for i, chunk in enumerate(retrieved_chunks, 1):
        context_parts.append(
            f"【参考文档 {i}】来源:{chunk['source']}(相关度:{chunk['similarity']}\n"
            f"{chunk['content']}"
        )
    
    return "\n\n---\n\n".join(context_parts)


SYSTEM_PROMPT = """你是一名企业知识库助手,基于提供的参考文档回答员工问题。

回答规则:
1. 只基于"参考文档"中的信息回答,不要编造或补充文档中没有的内容
2. 如果参考文档中没有相关信息,直接说"文档中未找到相关信息,建议咨询相关部门"
3. 回答时标注信息来源,例如:(来源:公司合规政策2026)
4. 使用清晰的结构化格式,重要信息用加粗标注
5. 回答简洁专业,避免冗余"""


def answer_question(question: str) -> dict:
    """完整的 RAG 问答流程"""
    # 1. 检索
    retrieved = retrieve(question, top_k=5)
    context = build_context(retrieved)
    
    # 2. 构建消息
    user_message = f"""参考文档:
{context}

---

员工问题:{question}"""
    
    # 3. 调用 Claude
    response = client.messages.create(
        model="claude-sonnet-4-6",  # 企业场景推荐 Sonnet,性价比最优
        max_tokens=1500,
        system=SYSTEM_PROMPT,
        messages=[{"role": "user", "content": user_message}]
    )
    
    answer = response.content[0].text
    
    return {
        "question": question,
        "answer": answer,
        "sources": list(set(c["source"] for c in retrieved)),
        "retrieved_count": len(retrieved),
        "input_tokens": response.usage.input_tokens,
        "output_tokens": response.usage.output_tokens,
    }


# 测试
result = answer_question("员工出差报销餐费的上限是多少?")
print(result["answer"])
print(f"\n📚 来源:{result['sources']}")
print(f"💰 消耗:{result['input_tokens']} 输入 + {result['output_tokens']} 输出 tokens")

第四步:多轮对话支持

真实企业场景中,员工往往会追问。加入对话历史管理:

class RAGChatSession:
    def __init__(self, max_history: int = 6):
        self.history = []
        self.max_history = max_history  # 保留最近 N 轮,控制 token 消耗
    
    def chat(self, question: str) -> str:
        retrieved = retrieve(question, top_k=4)
        context = build_context(retrieved)
        
        # 当前轮的用户消息
        current_message = f"参考文档:\n{context}\n\n---\n\n问题:{question}"
        
        messages = self.history + [{"role": "user", "content": current_message}]
        
        response = client.messages.create(
            model="claude-sonnet-4-6",
            max_tokens=1500,
            system=SYSTEM_PROMPT,
            messages=messages
        )
        
        answer = response.content[0].text
        
        # 更新历史(只保存问题和答案,不保存 context 以节省 token)
        self.history.append({"role": "user", "content": question})
        self.history.append({"role": "assistant", "content": answer})
        
        # 截断过长历史
        if len(self.history) > self.max_history * 2:
            self.history = self.history[-(self.max_history * 2):]
        
        return answer


# 使用示例
session = RAGChatSession()
print(session.chat("出差住宿标准是什么?"))
print(session.chat("北京的标准和上海一样吗?"))  # 追问,模型理解上下文
class RAGChatSession:
    def __init__(self, max_history: int = 6):
        self.history = []
        self.max_history = max_history  # 保留最近 N 轮,控制 token 消耗
    
    def chat(self, question: str) -> str:
        retrieved = retrieve(question, top_k=4)
        context = build_context(retrieved)
        
        # 当前轮的用户消息
        current_message = f"参考文档:\n{context}\n\n---\n\n问题:{question}"
        
        messages = self.history + [{"role": "user", "content": current_message}]
        
        response = client.messages.create(
            model="claude-sonnet-4-6",
            max_tokens=1500,
            system=SYSTEM_PROMPT,
            messages=messages
        )
        
        answer = response.content[0].text
        
        # 更新历史(只保存问题和答案,不保存 context 以节省 token)
        self.history.append({"role": "user", "content": question})
        self.history.append({"role": "assistant", "content": answer})
        
        # 截断过长历史
        if len(self.history) > self.max_history * 2:
            self.history = self.history[-(self.max_history * 2):]
        
        return answer


# 使用示例
session = RAGChatSession()
print(session.chat("出差住宿标准是什么?"))
print(session.chat("北京的标准和上海一样吗?"))  # 追问,模型理解上下文

第五步:生产环境优化

5.1 Prompt Caching——大幅降低成本

当 System Prompt 较长(如包含企业制度摘要)时,启用 Prompt Caching 可节省 90% 的 System Prompt token 费用

response = client.messages.create(
    model="claude-sonnet-4-6",
    max_tokens=1500,
    system=[
        {
            "type": "text",
            "text": SYSTEM_PROMPT + "\n\n" + COMPANY_BACKGROUND,  # 固定的长文本
            "cache_control": {"type": "ephemeral"}  # 开启缓存
        }
    ],
    messages=messages
)
response = client.messages.create(
    model="claude-sonnet-4-6",
    max_tokens=1500,
    system=[
        {
            "type": "text",
            "text": SYSTEM_PROMPT + "\n\n" + COMPANY_BACKGROUND,  # 固定的长文本
            "cache_control": {"type": "ephemeral"}  # 开启缓存
        }
    ],
    messages=messages
)

成本测算示例:假设 System Prompt = 2000 tokens,每天 1000 次查询:

  • 不开缓存:2000 × 1000 × $2.4/1M = $4.8/天
  • 开启缓存(命中率 95%):约 $0.5/天,节省 90%

5.2 混合检索——提升召回率

纯向量检索有时会漏掉关键词精确匹配的结果。结合 BM25 关键词检索效果更好:

from rank_bm25 import BM25Okapi
import jieba

class HybridRetriever:
    def __init__(self, chunks: list[str]):
        self.chunks = chunks
        # BM25 索引
        tokenized = [list(jieba.cut(c)) for c in chunks]
        self.bm25 = BM25Okapi(tokenized)
    
    def retrieve(self, query: str, top_k: int = 5) -> list[str]:
        # 向量检索结果
        vec_results = retrieve(query, top_k=top_k * 2)
        vec_ids = {c["chunk_index"]: c["similarity"] for c in vec_results}
        
        # BM25 检索结果
        query_tokens = list(jieba.cut(query))
        bm25_scores = self.bm25.get_scores(query_tokens)
        bm25_top = sorted(enumerate(bm25_scores), key=lambda x: x[1], reverse=True)[:top_k * 2]
        
        # RRF 融合排序(Reciprocal Rank Fusion)
        rrf_scores = {}
        for rank, (idx, _) in enumerate(bm25_top):
            rrf_scores[idx] = rrf_scores.get(idx, 0) + 1 / (60 + rank + 1)
        for rank, (idx, _) in enumerate(sorted(vec_ids.items(), key=lambda x: x[1], reverse=True)):
            rrf_scores[idx] = rrf_scores.get(idx, 0) + 1 / (60 + rank + 1)
        
        top_indices = sorted(rrf_scores, key=rrf_scores.get, reverse=True)[:top_k]
        return [self.chunks[i] for i in top_indices]
from rank_bm25 import BM25Okapi
import jieba

class HybridRetriever:
    def __init__(self, chunks: list[str]):
        self.chunks = chunks
        # BM25 索引
        tokenized = [list(jieba.cut(c)) for c in chunks]
        self.bm25 = BM25Okapi(tokenized)
    
    def retrieve(self, query: str, top_k: int = 5) -> list[str]:
        # 向量检索结果
        vec_results = retrieve(query, top_k=top_k * 2)
        vec_ids = {c["chunk_index"]: c["similarity"] for c in vec_results}
        
        # BM25 检索结果
        query_tokens = list(jieba.cut(query))
        bm25_scores = self.bm25.get_scores(query_tokens)
        bm25_top = sorted(enumerate(bm25_scores), key=lambda x: x[1], reverse=True)[:top_k * 2]
        
        # RRF 融合排序(Reciprocal Rank Fusion)
        rrf_scores = {}
        for rank, (idx, _) in enumerate(bm25_top):
            rrf_scores[idx] = rrf_scores.get(idx, 0) + 1 / (60 + rank + 1)
        for rank, (idx, _) in enumerate(sorted(vec_ids.items(), key=lambda x: x[1], reverse=True)):
            rrf_scores[idx] = rrf_scores.get(idx, 0) + 1 / (60 + rank + 1)
        
        top_indices = sorted(rrf_scores, key=rrf_scores.get, reverse=True)[:top_k]
        return [self.chunks[i] for i in top_indices]

5.3 流式输出——提升用户体验

对于较长的答案,流式输出让用户不必等待:

def answer_stream(question: str):
    retrieved = retrieve(question)
    context = build_context(retrieved)
    
    with client.messages.stream(
        model="claude-sonnet-4-6",
        max_tokens=1500,
        system=SYSTEM_PROMPT,
        messages=[{"role": "user", "content": f"参考文档:\n{context}\n\n问题:{question}"}]
    ) as stream:
        for text in stream.text_stream:
            print(text, end="", flush=True)  # 或推送到前端 SSE
def answer_stream(question: str):
    retrieved = retrieve(question)
    context = build_context(retrieved)
    
    with client.messages.stream(
        model="claude-sonnet-4-6",
        max_tokens=1500,
        system=SYSTEM_PROMPT,
        messages=[{"role": "user", "content": f"参考文档:\n{context}\n\n问题:{question}"}]
    ) as stream:
        for text in stream.text_stream:
            print(text, end="", flush=True)  # 或推送到前端 SSE

成本与性能基准参考

基于 ClaudeAPI.com 的 Sonnet 4.6 定价,估算一个中型企业知识库(10万文档块,日均 500 次问答)的月度成本:

项目 估算
向量化(一次性) BGE 模型本地运行,接近免费
每次问答均摊 input tokens ~3000 tokens(检索结果 + 历史)
每次问答均摊 output tokens ~500 tokens
月度 API 费用(500次/天) 约 $65-80/月
开启 Prompt Caching 后 约 $30-40/月(节省约 50%)

这个成本对于企业来说完全可接受,而它替代的是每月数十万的人力重复答疑成本。


快速开始清单

  • [ ] 在 ClaudeAPI.com 注册并获取 API Key
  • [ ] 配置 ANTHROPIC_BASE_URL=https://gw.claudeapi.com
  • [ ] 安装依赖:pip install anthropic chromadb sentence-transformers
  • [ ] 下载中文向量模型:BAAI/bge-large-zh-v1.5
  • [ ] 将企业文档 PDF/Word 放入待处理目录
  • [ ] 运行索引脚本,完成向量化
  • [ ] 启动问答接口,开始测试

总结

Claude API 在 RAG 场景下的核心优势:200K 超长上下文让检索结果可以更充分地喂给模型,不必因为担心上下文溢出而激进地截断文档,答案质量更稳定。

完整的企业 RAG 系统关键决策点:

  • 分块策略影响检索召回质量,800 tokens + 150 重叠是个好起点
  • 混合检索(向量 + BM25)比纯向量更稳健
  • Prompt Caching 在固定 System Prompt 场景下可节省 50-90% 成本
  • 模型选择:Sonnet 4.6 是企业 RAG 的性价比之选,Opus 适合合规/法律等高精度场景

ClaudeAPI.com 提供 Claude 官方同款 API,无需额外网络配置,只需将 base_url 指向 https://gw.claudeapi.com 即可开始构建。

相关文章