Agent引用数据库知识过时的增量同步方案

数据库里的知识持续新增、修改、删除,而 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:源文档 ID
  • version:单调自增版本号
  • 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 != activevalid_to != null 的向量
  • 生成防线 :Self-RAG 相关性评分 + 事实校验,矛盾时优先采用 updated_at 更晚的内容

3. 工程化保障

  • 向量库元数据 Schema 必须含 doc_id / version / updated_at / valid_from / valid_to
  • 蓝绿部署实现零停机更新,异常秒级回滚
  • 人工审核闭环让系统越用越准
  • 监控同步延迟、孤儿向量、检索命中率

4. 成本与实时性权衡

业务场景 推荐策略
商品库存/价格 CDC 实时同步(秒级)
客服知识库 CDC + 每分钟批量
内部制度文档 每小时/每天增量
合规条款 CDC 实时 + 人工审核闭环

📌 一句话记忆法:文档变了 → chunk 必须变 → 向量必须变 → 元数据版本必须对齐。三者任一脱节,Agent 就会引用过时知识。

按这套架构落地,你的 Agent 就能在数据库持续新增/修改/删除的情况下,始终引用最新、最准确的知识------而不是停留在"第一天导入时的那个版本"。

相关推荐
qq_454245031 小时前
Systemprompt 体系全览:形式化公理驱动的分层系统设计
人工智能
大龄码农有梦想1 小时前
传统的 BPMN 工作流审批和 AI 工作流有什么区别?
人工智能·流程引擎·工作流·ai agent·ai工作流·审批流·智能体平台
DO_Community2 小时前
Claude Opus 5 现已上线 DigitalOcean AI 推理云
人工智能·llm·agent·claude
幸福指北2 小时前
🚀 开源了,一个人 + AI 肝出一个 AI 终端 | AShell 技术分享
运维·人工智能·ai·终端
可以飞的话2 小时前
一、机器学习概述
人工智能·机器学习
sunneo2 小时前
磐石2.0发布,科学建模新突破
人工智能
煎饼学大模型2 小时前
Agent 的“大脑-手“解耦架构:当推理层和工具执行层各自独立演进
数据库·人工智能·oracle·架构·agent
min(a,b)3 小时前
学习第 4 天:面向对象与异常处理
python·学习·学习方法
南京码讯光电技术有限公司3 小时前
2026年4G/5G工业CPE推荐:从极端场景看硬核选型
人工智能