数据库里的知识持续新增、修改、删除,而 Agent 仍引用旧内容,本质是向量索引与源头数据不同步 。解决思路不是"更频繁地全量重建",而是搭建一条从源头变更到向量库、再到 Agent 检索生成的端到端增量同步链路,并在 Agent 侧加一层"自我反思 + 人工闭环"来兜底。下面给出完整设计、步骤和代码。
一、整体架构:六层同步链路
源数据库 ──CDC──> 消息队列(Kafka) ──> 嵌入服务 ──> 向量库(带元数据版本)
│
▼
LangGraph Agent: [检索] ─> [相关性评分] ─> [生成] ─> [事实校验] ─> [人工审核闭环]
│
▼
监控 / 评测 / 回滚
核心原则:增量更新负责日常变化,定期全量重建负责长期健康;索引时用的 Embedding 模型必须与查询时用的模型一致。
二、步骤 1:源头变更捕获(CDC)
传统定时拉取存在轮询间隔与实时性成反比、空轮询浪费资源的问题。推荐 CDC(Change Data Capture):
- MySQL/PostgreSQL:Debezium 监听 binlog/WAL
- MongoDB:Change Streams(带 resume token,支持断点续传)
- API 服务:通过 Kafka 发布/订阅传递变更事件
每条变更事件必须携带 operation_type(insert/update/delete)、doc_id、以及整行数据。
💡 关键:每一次 insert/update/delete 都必须触发向量库的对应动作,否则"已下架商品/已删除条款"还会被召回。
三、步骤 2:知识抽取与版本化(Chunk 级别)
这是防"引用过时"最核心的一步。不要做文档级粗粒度更新,要做 Chunk 级增量:
3.1 文档切分与指纹
每个 chunk 计算 SHA-256 作为内容寻址身份:
python
import hashlib
def normalize(text: str) -> str:
"""归一化:去空白、转小写,保证 hash 确定性"""
return " ".join(text.strip().lower().split())
def chunk_id(doc_id: str, position: int, content: str) -> str:
h = hashlib.sha256(normalize(content).encode()).hexdigest()
return f"{doc_id}:{position}:{h[:16]}"
def split_with_metadata(row: dict, doc_id: str):
"""把数据库行转为带元数据的 chunk 列表"""
# 只把语义有意义的列拼进嵌入文本
text_to_embed = f"{row['name']}. {row['description']}"
# 数值/状态/时间戳作为元数据,不参与嵌入
metadata = {
"doc_id": doc_id,
"price": row.get("price"),
"category": row.get("category"),
"status": row.get("status"), # active / discontinued
"updated_at": row.get("updated_at"),
"version": row.get("version", 1),
}
return [{
"content": text_to_embed,
"metadata": metadata
}]
3.2 变更检测逻辑
在 Hash Store 中维护 doc_id -> [chunk_hash1, chunk_hash2, ...] 的映射,比较新旧版本:
| 检测结果 | 含义 | 动作 |
|---|---|---|
| 新 hash | 该位置新增内容 | 计算嵌入 + 插入向量库 |
| hash 变化 | 内容被修改 | 删除旧向量 + 插入新向量 |
| 旧 hash 消失 | 内容被删除 | 从向量库删除 |
| hash 不变 | 内容未变 | 跳过,省下嵌入成本 |
这样可以把嵌入计算量从 O©(全量重嵌)降到 O(ΔC)(仅变部分)。
四、步骤 3:嵌入与向量库 Upsert / Delete
4.1 CDC 消费者代码
python
from kafka import KafkaConsumer
import json
consumer = KafkaConsumer(
"db_changes",
bootstrap_servers=["localhost:9092"],
value_deserializer=lambda v: json.loads(v.decode()),
group_id="embedding_service",
auto_offset_reset="latest",
)
for msg in consumer:
event = msg.value
op = event["op"] # "c"(insert) / "u"(update) / "d"(delete)
doc_id = event["doc_id"]
row = event.get("after", {})
if op == "d":
# 硬删除:直接移除该 doc 所有 chunk 向量
vector_store.delete(filter={"doc_id": doc_id})
hash_store.pop(doc_id, None)
continue
# insert / update:切分 + 指纹比对
new_chunks = split_with_metadata(row, doc_id)
old_hashes = hash_store.get(doc_id, [])
for i, chunk in enumerate(new_chunks):
chunk_hash = chunk_id(doc_id, i, chunk["content"])
if chunk_hash in old_hashes:
continue # 未变化,跳过
# 先删旧(若存在)
vector_store.delete(filter={
"doc_id": doc_id,
"position": i
})
# 嵌入 + 写入
embedding = embed_model.embed(chunk["content"])
vector_store.upsert(
id=chunk_hash,
vector=embedding,
payload={
**chunk["metadata"],
"position": i,
"valid_from": row["updated_at"],
"valid_to": None,
"content": chunk["content"],
}
)
# 更新 hash 映射
hash_store[doc_id] = [
chunk_id(doc_id, i, c["content"]) for i, c in enumerate(new_chunks)
]
4.2 向量库元数据 Schema(防过时关键)
每个向量必须带以下元数据字段:
doc_id:源文档 IDversion:单调自增版本号updated_at:最后更新时间戳valid_from/valid_to:版本生效时间窗(TTL 淘汰依据)status:业务状态(如商品"已下架")source_id:溯源用
⚠️ 最常见的坑:只插入新向量,不删除旧版本。这会导致新旧政策同时被召回,用户拿到过期答案------比查不到还糟。删除文档时必须同步清理其所有 chunk 向量,否则会残留"幽灵文档"。
4.3 嵌入模型一致性
硬规则:索引时用的 Embedding 模型必须与查询时用的模型完全一致。换模型时必须全量重建。
五、步骤 4:Agent 检索层------过滤 + 重排
5.1 元数据过滤(排除过期内容)
python
def retrieve(query: str, top_k: int = 5):
embedding = embed_model.embed(query)
results = vector_store.query(
vector=embedding,
top_k=top_k * 3, # 多召回,后续重排
filter={
"status": {"$eq": "active"}, # 排除已下架
"valid_to": {"$isnull": True}, # 排除已被取代的旧版本
"updated_at": {"$gte": cutoff_time}, # 可选:时间窗过滤
}
)
return rerank(query, results)[:top_k]
5.2 重排序
用 Cross-Encoder 重排,提升准确率:
python
from sentence_transformers import CrossEncoder
reranker = CrossEncoder("BAAI/bge-reranker-v2-m3")
def rerank(query: str, candidates: list):
pairs = [[query, c["content"]] for c in candidates]
scores = reranker.predict(pairs)
for c, s in zip(candidates, scores):
c["rerank_score"] = s
return sorted(candidates, key=lambda x: x["rerank_score"], reverse=True)
六、步骤 5:LangGraph Agent 自我反思(Self-RAG)
光有同步还不够,Agent 生成时仍需校验。用 LangGraph 构建带"反思节点"的图:
python
from langgraph.graph import StateGraph, START, END
from typing import TypedDict, List
from langchain_core.messages import HumanMessage
class AgentState(TypedDict):
query: str
documents: List[dict]
generation: str
relevance_grade: str # "relevant" / "irrelevant"
support_grade: str # "fully_supported" / "partially" / "no_support"
# 节点 1:检索
def retrieve_node(state: AgentState):
docs = retrieve(state["query"])
return {"documents": docs}
# 节点 2:文档相关性评分
def grade_documents(state: AgentState):
llm = get_llm()
prompt = """判断以下文档是否与问题相关。
问题:{q}
文档:{d}
只回答 relevant 或 irrelevant。""".format(
q=state["query"], d=state["documents"]
)
grade = llm.invoke([HumanMessage(content=prompt)]).content
return {"relevance_grade": grade}
# 节点 3:生成
def generate(state: AgentState):
if state["relevance_grade"] == "irrelevant":
# 改写查询,重新检索
return {"generation": "", "relevance_grade": "rewrite_needed"}
llm = get_llm()
ctx = "\n".join([d["content"] for d in state["documents"]])
prompt = f"基于以下上下文回答问题。若上下文矛盾,优先使用 updated_at 更晚的内容。\n\n上下文:{ctx}\n\n问题:{state['query']}"
gen = llm.invoke([HumanMessage(content=prompt)]).content
return {"generation": gen}
# 节点 4:事实校验(防止幻觉引用过时内容)
def grade_generation_vs_documents(state: AgentState):
llm = get_llm()
prompt = """校验生成内容是否被检索文档充分支持。
文档:{d}
生成:{g}
只回答 fully_supported / partially / no_support。""".format(
d=state["documents"], g=state["generation"]
)
grade = llm.invoke([HumanMessage(content=prompt)]).content
return {"support_grade": grade}
# 构建图
workflow = StateGraph(AgentState)
workflow.add_node("retrieve", retrieve_node)
workflow.add_node("grade_docs", grade_documents)
workflow.add_node("generate", generate)
workflow.add_node("grade_generation", grade_generation_vs_documents)
workflow.add_edge(START, "retrieve")
workflow.add_edge("retrieve", "grade_docs")
workflow.add_conditional_edges(
"grade_docs",
lambda s: "generate" if s["relevance_grade"] == "relevant" else "retrieve",
{"generate": "generate", "retrieve": "retrieve"} # 改写查询后重检
)
workflow.add_edge("generate", "grade_generation")
workflow.add_conditional_edges(
"grade_generation",
lambda s: END if s["support_grade"] == "fully_supported" else "generate",
{"generate": "generate", "END": END}
)
app = workflow.compile()
这套 Self-RAG 流程能在检索质量差时自动改写查询重检,在生成与文档不符时重新生成,大幅降低引用过时/错误内容的概率。
七、步骤 6:人工闭环(Human-in-the-Loop)
对于高风险的生成结果,用 LangGraph 的 interrupt() 暂停等待人工审核:
python
from langgraph.types import interrupt, Command
def human_review_node(state: AgentState) -> Command:
# 中断,等待人工审批
decision = interrupt({
"action": "approve_or_edit",
"draft": state["generation"],
"sources": [d["doc_id"] for d in state["documents"]],
})
if decision["type"] == "accept":
return Command(goto=END)
elif decision["type"] == "edit":
# 用户编辑后的内容,可作为新知识写回向量库
updated_content = decision["edited_response"]
# 触发增量索引更新
trigger_reindex(state["query"], updated_content)
return Command(
update={"generation": updated_content},
goto=END
)
else: # reject
return Command(goto=END)
人工编辑后的内容自动存入向量库,形成越用越准的闭环。
八、步骤 7:监控、评测与回滚
8.1 关键监控指标
- 同步延迟:CDC 事件产生到向量库生效的时间差
- 孤儿向量数:向量库中存在但源库已删除的 chunk 数
- 检索命中率 :含
valid_to != null旧版本的比例(应趋近 0) - 生成支持率 :Self-RAG 中
fully_supported占比
8.2 蓝绿部署 + 自动回滚
python
def blue_green_switch():
"""新旧索引并行,原子切换"""
# 新索引构建完成后,通过配置中心一键切换流量
# 旧索引保留 24-48 小时,异常时秒级回滚
pass
8.3 定期全量重建
增量更新为主,但以下场景必须全量重建:
- Embedding 模型升级
- 每周/每月的健康检查(处理索引碎片)
- 故障恢复
python
# 每周日凌晨全量重建
@app.task(schedule="0 2 * * 0")
def full_rebuild():
vector_store.rebuild_from_source(
source_db=db,
embed_model=embed_model,
cleanup="full"
)
九、最终总结
保证 LangChain/LangGraph Agent 引用知识不过时,本质是把"同步"做成一等公民,而非事后补救。整套方案的关键设计决策:
1. 同步策略
- 日常用 CDC + Chunk 级增量(hash 指纹检测变更,只处理 ΔC)
- 周期性 全量重建 维护索引健康
- Embedding 模型必须前后一致,换模型即全量重建
2. 防过时三道防线
- 写入防线 :delete 事件必须传播到向量库,旧版本向量带
valid_to标记 - 检索防线 :元数据过滤排除
status != active且valid_to != null的向量 - 生成防线 :Self-RAG 相关性评分 + 事实校验,矛盾时优先采用
updated_at更晚的内容
3. 工程化保障
- 向量库元数据 Schema 必须含
doc_id / version / updated_at / valid_from / valid_to - 蓝绿部署实现零停机更新,异常秒级回滚
- 人工审核闭环让系统越用越准
- 监控同步延迟、孤儿向量、检索命中率
4. 成本与实时性权衡
| 业务场景 | 推荐策略 |
|---|---|
| 商品库存/价格 | CDC 实时同步(秒级) |
| 客服知识库 | CDC + 每分钟批量 |
| 内部制度文档 | 每小时/每天增量 |
| 合规条款 | CDC 实时 + 人工审核闭环 |
📌 一句话记忆法:文档变了 → chunk 必须变 → 向量必须变 → 元数据版本必须对齐。三者任一脱节,Agent 就会引用过时知识。
按这套架构落地,你的 Agent 就能在数据库持续新增/修改/删除的情况下,始终引用最新、最准确的知识------而不是停留在"第一天导入时的那个版本"。