RAG 系统部署实战:从文档索引到 Agent 工具集成

前三篇文章搭建了 Agent、加上监控、部署了 Gateway。但 Agent 的知识完全依赖 LLM 的训练数据——遇到内部文档、运维手册、私有知识就无能为力。RAG(检索增强生成)填上这个缺口。

这篇文章从文档加载到 Agent 集成,完整走一遍 RAG 流水线。


RAG 流水线

┌──────────┐   ┌──────────┐   ┌──────────┐   ┌──────────┐
│ 1. 加载  │ → │ 2. 切分  │ → │ 3. 嵌入  │ → │ 4. 存储  │
│ 文档     │   │ chunk    │   │ 向量化   │   │ Chroma   │
└──────────┘   └──────────┘   └──────────┘   └──────────┘
                                                    │
                                                    ▼
用户提问 → 嵌入向量 → 相似度检索 → 拼接上下文 → LLM 生成回答

四个步骤,每个都有工程决策。我们逐一拆解。


Step 1:文档加载

from langchain_core.documents import Document
import glob

def load_documents(data_dir: str = "data") -> list[Document]:
    docs = []
    for filepath in glob.glob(os.path.join(data_dir, "*.md")):
        with open(filepath, "r", encoding="utf-8") as f:
            content = f.read()
        docs.append(Document(
            page_content=content,
            metadata={"source": os.path.basename(filepath)},
        ))
    return docs

关键决策:把文件名存在 metadata 中。后续检索时可以直接引用来源,回答”这个信息来自哪篇文档”。


Step 2:Chunk 切分策略

这是 RAG 系统最重要的工程决策之一。chunk 太大 → 检索噪音多,LLM 上下文浪费;chunk 太小 → 语义不完整,检索不到。

from langchain_text_splitters import RecursiveCharacterTextSplitter

splitter = RecursiveCharacterTextSplitter(
    chunk_size=500,          # 每个 chunk 最多 500 字符
    chunk_overlap=100,       # 相邻 chunk 重叠 100 字符
    separators=["\n\n", "\n", "。", " ", ""],  # 优先按自然边界切分
)
chunks = splitter.split_documents(docs)

三个参数的选择逻辑:

参数值为什么
chunk_size500中文技术文档,500 字符约等于 2-3 个段落,是语义完整性和检索精度的平衡点
chunk_overlap10020% 重叠率,保证跨 chunk 的上下文不被切断
separators\n\n → \n → 。→ 空格优先按段落切,其次是自然句号,最后才硬切——最大程度保留语义

生产环境建议:chunk 策略没有万能解。应该根据文档类型评估——代码文档用 200-300 字符,长文用 800-1000 字符。RAGAS 等框架可以量化评估不同 chunk 策略的检索质量。


Step 3 & 4:嵌入向量化 + Chroma 存储

from langchain_openai import OpenAIEmbeddings
from langchain_chroma import Chroma

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

vectorstore = Chroma.from_documents(
    documents=chunks,
    embedding=embeddings,
    persist_directory="./chroma_db",  # 持久化到磁盘
)

为什么选 Chroma:

向量数据库适用场景不适用场景
Chroma本地开发、小规模(< 10 万 chunk)、快速原型高并发生产环境
Pinecone全托管、大规模、零运维预算有限、数据需本地化
Qdrant自托管、高性能、过滤查询强简单场景过度设计
Milvus十亿级向量、分布式小数据集起步

我选 Chroma 原因:本地开发零配置、持久化简单、API 跟 LangChain 深度集成。后续迁移到生产环境时,LangChain 的向量存储抽象层让切换成本很低——换一个 from_documents 调用即可。


RAG 问答链

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

prompt = ChatPromptTemplate.from_messages([
    ("system", """你是运维知识库助手。根据以下文档内容回答。

规则:
- 只使用提供的文档内容,不要编造
- 如果文档中没有,直接说「未找到相关内容」
- 回答时引用文档来源

文档内容:
{context}"""),
    ("user", "{question}"),
])

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

Prompt 设计的关键:明确告诉 LLM「只使用文档内容,不要编造」。这是 RAG 防幻觉的第一道防线。没有这个约束,LLM 会用自己的训练数据补充,产生”看起来合理但文档里没有”的回答。


集成到 LangGraph Agent

RAG 系统可以独立运行,但最强大的用法是作为 Agent 的一个工具。LLM 自主判断什么时候需要检索知识库:

@tool
def search_knowledge_base(query: str) -> str:
    """从运维知识库中检索相关信息。当用户询问 K8s、Docker 等运维问题时使用。"""
    docs = retriever.invoke(query)
    parts = []
    for doc in docs:
        source = doc.metadata.get("source", "unknown")
        parts.append(f"[来源: {source}]\n{doc.page_content}")
    return "\n\n---\n\n".join(parts)

Agent 的 bind_tools 会自动把这个函数注册为可用工具。LLM 遇到运维问题时,会生成 tool_calls: [{name: "search_knowledge_base", args: {query: "CrashLoopBackOff 排查"}}]。

Agent 模式 vs 直接 RAG 的区别:

直接 RAGAgent 模式
每次提问都检索LLM 自主决定是否检索
只做检索+生成可以检索 + 调用其他工具 + 多次检索
答案固定来自文档可以结合文档 + 自身知识 + 工具结果

检索质量评估

检索质量决定了 RAG 系统的好坏。最基础的评估方式是人工审查检索结果:

test_queries = [
    {"query": "容器启动失败怎么排查", "keywords": ["CrashLoopBackOff"]},
    {"query": "镜像安全扫描", "keywords": ["Trivy", "漏洞"]},
]

for item in test_queries:
    docs = retriever.invoke(item["query"])
    for doc in docs:
        hits = [kw for kw in item["keywords"] if kw in doc.page_content]
        if not hits:
            print(f"⚠️ 漏检: {doc.page_content[:80]}")

生产环境建议接入 RAGAS:自动评估 faithfulness(回答是否忠于文档)、context_recall(检索是否召回相关内容)、context_precision(检索结果是否精确)。


实际效果

👤 什么是 CrashLoopBackOff?怎么排查?

🔧 [工具] search_knowledge_base: "CrashLoopBackOff" → 检索到 4 个 chunk

🤖 根据知识库文档,CrashLoopBackOff 是 Kubernetes 中 Pod 的一种状态,
表示容器启动后立即退出。常见原因包括:
1. 配置错误
2. 依赖服务不可达
3. 端口冲突

排查步骤:
- kubectl describe pod 查看 Events
- kubectl logs 查看容器日志
(来源:kubernetes-ops-guide.md)

👤 Docker 镜像优化和 Kubernetes 资源管理有什么关系?

🤖 [Agent 自主调用两次 search_knowledge_base]
镜像优化可以减少容器启动时间和资源占用,
这与 Kubernetes 的资源管理(Limits/Requests)直接相关...

生产部署考量

层面当前实现生产增强
文档更新手动重建增量索引 + 定时同步
Chunk 策略固定 500按文档类型自适应
向量存储本地 ChromaChroma Server / Qdrant 集群
检索相似度 top-4混合检索(向量 + BM25) + 重排序
评估人工抽查RAGAS 自动化 + CI/CD 集成
监控无检索延迟、召回率、LLM 引用率

完整代码


下一步

RAG 让 Agent 有了私有知识。但调用 LLM 是要花钱的——Token 成本怎么控制?下一篇文章深挖 Token 成本控制:从账单分析到模型路由再到缓存策略,让 AI 系统的运营成本可控。