🌟 LangChain 30 天保姆级教程 · Day 20|自定义 Retriever 实战!把 Elasticsearch、数据库、API 变成 RAG

0 阅读3分钟

系列目标:30 天从 LangChain 入门到企业级部署
今日任务:理解 Retriever 接口 → 封装 Elasticsearch/DB/API → 构建“外部检索 + 本地生成”混合 RAG!


🔌 一、为什么需要自定义 Retriever?

在 Day 17–19 中,我们使用 Chroma 作为向量库。
真实企业环境往往已有成熟检索系统

  • 📈 Elasticsearch:公司全文搜索引擎
  • 💾 MySQL/PostgreSQL:结构化 FAQ 表
  • ☁️ 内部 API:知识图谱服务、Confluence 搜索接口

如果为了 RAG 重新建一套向量库:

  • ❌ 数据重复存储
  • ❌ 运维成本翻倍
  • ❌ 无法利用现有权限/审计体系

解决方案

✅ 自定义 Retriever —— 让 LangChain 的 RAG 链直接调用你的系统!

💡 今天,我们就封装三大典型场景:Elasticsearch、数据库、HTTP API!


🧱 二、Retriever 核心接口

LangChain 要求 Retriever 实现一个方法:

from langchain_core.documents import Document
from typing import List

class MyRetriever:
    def get_relevant_documents(self, query: str) -> List[Document]:
        # 返回 List[Document(page_content=..., metadata={})]
        pass

✅ 只需实现这个方法,就能无缝接入 RetrievalQA


🛠️ 三、实战 1:封装 Elasticsearch(企业搜索)

假设

  • 已有 ES 集群,索引 company_knowledge
  • 字段:content(文本)、source(来源)、category(分类)
# day20_custom_retrievers.py
from langchain_core.documents import Document
from elasticsearch import Elasticsearch

class ElasticsearchRetriever:
    def __init__(self, es_url: str, index_name: str, k: int = 4):
        self.es = Elasticsearch(es_url)
        self.index = index_name
        self.k = k

    def get_relevant_documents(self, query: str) -> List[Document]:
        resp = self.es.search(
            index=self.index,
            query={"match": {"content": query}},
            size=self.k
        )
        docs = []
        for hit in resp["hits"]["hits"]:
            source = hit["_source"]
            docs.append(Document(
                page_content=source["content"],
                metadata={
                    "source": source.get("source", "unknown"),
                    "score": hit["_score"]
                }
            ))
        return docs

✅ 利用企业现有 ES,无需迁移数据!


🛠️ 四、实战 2:封装数据库(FAQ 表)

假设

  • PostgreSQL 表 faqidquestionanswercategory
from sqlalchemy import create_engine, text

class DatabaseRetriever:
    def __init__(self, db_url: str, k: int = 3):
        self.engine = create_engine(db_url)
        self.k = k

    def get_relevant_documents(self, query: str) -> List[Document]:
        # 简单关键词匹配(生产环境可用 pgvector 做向量检索)
        sql = text("""
            SELECT question, answer, category 
            FROM faq 
            WHERE question ILIKE :q OR answer ILIKE :q
            LIMIT :k
        """)
        with self.engine.connect() as conn:
            results = conn.execute(sql, q=f"%{query}%", k=self.k).fetchall()
        
        docs = []
        for row in results:
            content = f"问题:{row.question}\n答案:{row.answer}"
            docs.append(Document(
                page_content=content,
                metadata={"category": row.category}
            ))
        return docs

💡 若数据库支持向量(如 PGVector),可替换为语义检索!


🛠️ 五、实战 3:封装 HTTP API(Confluence/知识图谱)

假设

  • 内部 API:POST /search,接收 {"query": "..."},返回 JSON
import requests

class HTTPAPIRetriever:
    def __init__(self, api_url: str, headers: dict = None, k: int = 5):
        self.api_url = api_url
        self.headers = headers or {}
        self.k = k

    def get_relevant_documents(self, query: str) -> List[Document]:
        try:
            resp = requests.post(
                self.api_url,
                json={"query": query, "top_k": self.k},
                headers=self.headers,
                timeout=10
            )
            resp.raise_for_status()
            data = resp.json()
            
            docs = []
            for item in data.get("results", []):
                docs.append(Document(
                    page_content=item["content"],
                    metadata={
                        "title": item.get("title"),
                        "url": item.get("url")
                    }
                ))
            return docs
        except Exception as e:
            print(f"⚠️ API 调用失败: {e}")
            return []

✅ 适用于 Confluence、Notion、自研知识库等!


🤖 六、整合到 RAG Chain

from langchain_ollama import ChatOllama
from langchain.chains import RetrievalQA

# 选择任一 Retriever
retriever = ElasticsearchRetriever(
    es_url="http://localhost:9200",
    index_name="company_knowledge"
)

# 或
# retriever = DatabaseRetriever("postgresql://user:pass@localhost/company")
# retriever = HTTPAPIRetriever("https://api.mycompany.com/kb/search")

# 创建 RAG 链
llm = ChatOllama(model="qwen:7b", temperature=0)
qa_chain = RetrievalQA.from_chain_type(
    llm=llm,
    chain_type="stuff",
    retriever=retriever,  # 直接传入自定义 Retriever!
    return_source_documents=True
)

# 测试
result = qa_chain({"query": "如何申请远程办公?"})
print("🤖 AI 回答:", result["result"])

✅ 完全兼容!LangChain 不关心你背后用什么检索系统。


⚙️ 七、高级技巧:混合 Retriever(本地 + 外部)

想同时查 Chroma 和 ES?

from langchain.retrievers import EnsembleRetriever

# 本地向量库
chroma_retriever = Chroma(...).as_retriever()

# 外部 ES
es_retriever = ElasticsearchRetriever(...)

# 融合
hybrid_retriever = EnsembleRetriever(
    retrievers=[chroma_retriever, es_retriever],
    weights=[0.4, 0.6]  # 更信任 ES
)

💡 适用于迁移过渡期或多源知识融合!


⚠️ 八、安全与最佳实践

表格

风险建议
API 密钥泄露使用环境变量;不要硬编码
SQL 注入用参数化查询(如 text().bindparam
ES 查询注入对 query 做清洗或使用 DSL 白名单
超时拖垮服务设置 timeout;加熔断机制
敏感数据返回在 Retriever 层做脱敏(结合 Day 14 Callback)

💡 生产建议

  • 所有 Retriever 实现统一日志格式
  • 监控检索延迟与失败率
  • 对高频查询结果缓存

📦 九、配套代码结构

langchain-30-days/
└── day20/
    └── custom_retrievers.py  # ES/DB/API 三大 Retriever 实现

📝 十、今日小结

  • ✅ 理解了 Retriever 的核心接口设计
  • ✅ 学会了封装 Elasticsearch、数据库、HTTP API
  • ✅ 实现了“外部检索 + 本地 LLM 生成”的混合 RAG
  • ✅ 掌握了多源检索融合技巧
  • ✅ 知道了企业级安全与监控要点

🎯 明日预告:Day 21 —— Memory 机制!让 AI 记住对话历史,实现多轮上下文理解!