系列目标: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 表
faq:id,question,answer,category
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 记住对话历史,实现多轮上下文理解!