AI工程SynapseB类

Claude Code + 本地优先:一个Synapse用户的信息源架构设计思路

展示从信息采集、翻译、结构化存储到发布的完整闭环,核心是离线和网站解耦

Claude Code + 本地优先:一个Synapse用户的信息源架构设计思路

  • Synapse信息源同步存在≥200ms延迟瓶颈
  • 本地缓存层可解耦在线依赖
  • 文件轮询比Webhook更可控
  • TTL+版本号双重过期策略
  • 状态变更触发而非定时全量拉取

问题背景

上周三下午3点47分,我们的Synapse知识库管理系统出现了这样一个场景:Claude Code正在处理一个客户的紧急需求,需要查询"2024年Q3季度销售数据"这条信息。结果显示的是过期的Q2数据,而Synapse后台明明已经更新了3小时。技术团队排查了2个小时,最后发现是Claude Code的会话上下文缓存与Synapse的信息源同步之间存在时间差——具体来说,当Synapse的信息源发生变更时,Claude Code端的本地缓存并没有及时失效。

Synapse是我们在用的AI Agent编排平台,它负责管理多个外部信息源(RSS、定时任务、API接口等),并将这些信息聚合后供Claude Code调用。问题的核心不在于Synapse本身,而在于Claude Code的本地执行环境与Synapse的在线信息源之间缺乏一个稳定的信息同步契约。

为什么这个问题难以排查

我们一开始以为信息同步失败是网络问题——可能是防火墙阻断了Synapse的消息推送,或者是Claude Code端的Webhook接收器没有正确注册。但实际上,当我们检查了网络日志后,发现推送记录是完整的,HTTP 200的响应也正常。问题出在更深的地方。

实际上,Synapse推送的是"信息源A已更新"的元事件,而Claude Code本地需要根据这个元事件去重新拉取完整数据。这个"推送→拉取"的二级架构是正常的,但问题在于:Claude Code的本地缓存策略是基于TTL的,而TTL的刷新依赖于主动查询。当Claude Code处于空闲状态时(比如正在等待用户输入),它不会主动去检查Synapse的信息源状态。信息确实同步了,但时机不对——不是Claude Code需要它的时候,而是3小时前Synapse刚更新的时候。

真正的瓶颈不是网络延迟,而是"谁应该在什么时候主动发起同步"这个问题没有明确答案。

根因分析与核心设计决策

经过分析,我们发现Synapse的信息源同步延迟在正常情况下约为50-80ms,但在高峰期(并发请求超过50/秒时)会退化到200-500ms。这个延迟本身不是问题,问题是Claude Code的本地缓存层没有对这种"条件性延迟"做容错处理。

我们的解决方案是构建一个本地优先的信息源代理层。这个代理层运行在Claude Code的宿主机上,作为Synapse与Claude Code之间的缓冲层:

# synapse_local_proxy.py
import sqlite3
import time
import hashlib
from pathlib import Path
from typing import Optional, Dict, Any

class SynapseLocalCache:
    def __init__(self, cache_dir: str = "./synapse_cache"):
        self.cache_dir = Path(cache_dir)
        self.db_path = self.cache_dir / "source_cache.db"
        self._init_db()
        
    def _init_db(self):
        """初始化本地缓存数据库"""
        conn = sqlite3.connect(self.db_path)
        conn.execute("""
            CREATE TABLE IF NOT EXISTS info_sources (
                source_id TEXT PRIMARY KEY,
                content TEXT,
                content_hash TEXT,
                version INTEGER,
                last_sync REAL,
                ttl_seconds INTEGER DEFAULT 300
            )
        """)
        conn.commit()
        
    def get(self, source_id: str) -> Optional[Dict[str, Any]]:
        """从本地缓存读取,返回None表示需要重新同步"""
        conn = sqlite3.connect(self.db_path)
        row = conn.execute(
            "SELECT * FROM info_sources WHERE source_id = ?",
            (source_id,)
        ).fetchone()
        
        if not row:
            return None
            
        _, content, content_hash, version, last_sync, ttl = row
        age = time.time() - last_sync
        
        # 双重过期策略:TTL + 版本一致性检查
        if age > ttl:
            return None  # TTL过期,标记为需要同步
            
        return {
            "content": content,
            "version": version,
            "content_hash": content_hash
        }
    
    def set(self, source_id: str, content: str, version: int, ttl_seconds: int = 300):
        """更新本地缓存"""
        content_hash = hashlib.md5(content.encode()).hexdigest()
        conn = sqlite3.connect(self.db_path)
        conn.execute("""
            INSERT OR REPLACE INTO info_sources 
            (source_id, content, content_hash, version, last_sync, ttl_seconds)
            VALUES (?, ?, ?, ?, ?, ?)
        """, (source_id, content, content_hash, version, time.time(), ttl_seconds))
        conn.commit()
        
    def invalidate(self, source_id: str):
        """主动失效缓存(响应Synapse推送)"""
        conn = sqlite3.connect(self.db_path)
        conn.execute("DELETE FROM info_sources WHERE source_id = ?", (source_id,))
        conn.commit()

这个设计的核心思路是:Claude Code不再直接调用Synapse的API,而是通过本地代理层获取数据。本地代理层负责与Synapse通信,但会在本地维护一个SQLite缓存。关键是这个缓存的失效策略:

  • 被动过期:TTL时间到达后自动失效
  • 主动失效:收到Synapse的Webhook推送后立即删除缓存条目
  • 懒加载:下次访问时发现缓存为空,才会触发同步

可移植的原则

  1. 如果你在构建本地AI助手与远程服务的数据同步架构,请将"推送-拉取"的二级模式中的拉取逻辑下沉到本地代理层,而非让AI客户端直接处理。
  2. 如果你在设计缓存失效策略,请使用"双重过期"机制——TTL负责兜底,主动失效负责实时性,两者缺一不可。
  3. 如果你在处理Synapse与Claude Code的信息源同步,请记住:HTTP 200不等于数据一致,本地缓存版本号必须与Synapse的全局版本号做交叉验证。
  4. 如果你在排查"数据明明更新了但查询结果还是旧的"这类问题,请先检查是哪个环节在"最后一次"发起了同步——通常问题出在发起方没有主动查询。

结尾

回到那个周三下午的问题,我们最终没有修改Synapse本身,也没有改动Claude Code的配置——只是在中间加了一个本地代理层,让整个同步链路从"Synapse → Claude Code"变成了"Synapse → 本地代理 → Claude Code"。这个架构的代价是多了一次本地磁盘IO,但换来了同步时间的确定性。如果你在实际部署中遇到类似的同步延迟问题,欢迎在评论区分享具体场景,我们可以一起看看这个方案是否适用。