mcpField
E
entity-memory-management

entity-memory-management

github.com/itallstartedwithaideaAgent 工作流4476 次安装28 周前更新
代码仓库

关于 entity-memory-management

用于跨 Agent 会话提取、保存和检索分类实体的技能,适合需要持续跟踪广告活动、指标、决策、偏好和反复问题的 Agent。它通过重要性评分与相关性检索构建结构化上下文。

主要功能

  1. 01按类型提取实体
  2. 02重要性评分
  3. 03使用 Cloudflare KV 持久化
  4. 04按上下文检索
  5. 05更新实体并处理矛盾

适用场景

  1. 01跨会话延续广告活动上下文
  2. 02回忆过往决策及其原因
  3. 03跟踪用户偏好和反复问题

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/itallstartedwithaidea/agent-skills --skill entity-memory-management

安装后在 Claude Code 中输入/entity-memory-management确认已加载。

详细介绍

实体记忆管理

属于 Agent Skills™,由 googleadsagent.ai™ 提供

描述

实体记忆管理是对跨 Agent 会话的命名实体进行系统化提取、持久化和检索,使 Agent 能够长期感知上下文,将转瞬即逝的对话转化为持久关系。通用记忆系统存储会话摘要,而实体记忆则在更细粒度上运作——将具体的广告系列、指标、决策、用户痛点和偏好作为独立且可查询的对象进行追踪。这种精确度让 Agent 能记住“广告系列 Alpha 上周二改用了目标 CPA”,而不只是“我们最近讨论了出价调整”。

这项技能是 Buddy™ 的生产级实体记忆架构,由 googleadsagent.ai™ 提供。实体记忆会在数百个用户会话中追踪数千个命名实体。系统维护预定义类型的实体——广告系列(及其策略、预算和效果趋势)、指标(具体 KPI 及其历史值)、决策(所做的更改及其理由)和痛点(用户反复表达不满的问题)。每个实体都会经过提取、类型标注和重要性评分,存储在 Cloudflare KV 中,并在未来的会话中根据上下文检索。

最终,Agent 真正“记住”了用户——并非通过回放对话,而是依靠持久保存并不断演变的结构化类型知识。这带来了截然不同的用户体验:Agent 能基于先前的上下文继续工作,而不是从头开始。

适用场景

  • Agent 服务于希望跨会话保持连续体验的回访用户
  • 需要长期追踪领域实体(广告系列、账户、项目)
  • 过去会话中的决策应为未来的建议提供参考
  • 用户偏好和痛点应持久保存,无需用户重复说明
  • 需要检测跨会话的趋势(指标改善、反复出现的问题)
  • 合规或审计要求需要对 Agent 与用户的交互进行结构化记录

工作原理

graph TD
    A[Conversation Stream] --> B[Entity Extractor]
    B --> C{Entity Type}
    C -->|Campaign| D[Campaign Schema]
    C -->|Metric| E[Metric Schema]
    C -->|Decision| F[Decision Schema]
    C -->|Pain Point| G[Pain Point Schema]
    C -->|Preference| H[Preference Schema]
    D --> I[Importance Scorer]
    E --> I
    F --> I
    G --> I
    H --> I
    I --> J[KV Store Writer]
    J --> K[Entity Index]
    
    L[New Session Start] --> M[Context Builder]
    M --> N[Entity Retriever]
    N --> K
    K --> O[Relevance Filter]
    O --> P[Context Injection]

实体提取流水线会通过类型感知提取器处理对话流,识别命名实体并将其归类到预定义架构中。每个提取出的实体都会根据对话中的强调程度、可操作性和新颖性获得重要性评分(新信息的评分高于重复信息)。经过评分的实体会使用结构化键写入 Cloudflare KV,以便高效检索。每次新会话开始时,上下文构建器都会查询当前用户的实体索引,根据与新任务的相关性进行筛选,并将最相关的实体注入 Agent 的上下文。

实现

实体类型架构:

interface BaseEntity {
  id: string;
  userId: string;
  type: EntityType;
  name: string;
  content: string;
  importance: number;
  createdAt: number;
  updatedAt: number;
  sessionId: string;
  version: number;
}

type EntityType = "campaign" | "metric" | "decision" | "pain_point" | "preference";

interface CampaignEntity extends BaseEntity {
  type: "campaign";
  metadata: {
    campaignId?: string;
    strategy?: string;
    budget?: number;
    status?: string;
    lastPerformance?: Record<string, number>;
  };
}

interface DecisionEntity extends BaseEntity {
  type: "decision";
  metadata: {
    action: string;
    rationale: string;
    affectedEntities: string[];
    reversible: boolean;
    outcome?: string;
  };
}

interface PainPointEntity extends BaseEntity {
  type: "pain_point";
  metadata: {
    severity: "low" | "medium" | "high";
    frequency: number;
    resolved: boolean;
    relatedCampaigns: string[];
  };
}

interface PreferenceEntity extends BaseEntity {
  type: "preference";
  metadata: {
    category: "communication" | "analysis" | "risk" | "reporting";
    value: string;
    confidence: number;
  };
}

实体提取提示词:

EXTRACTION_PROMPT = """Analyze this conversation and extract structured entities.

Entity types and what to capture:
- CAMPAIGN: Campaign names, strategies, budgets, performance data, status changes
- METRIC: Specific KPI values mentioned (CPA, ROAS, CTR, etc.) with context
- DECISION: Choices made or agreed upon, with rationale and affected entities
- PAIN_POINT: Frustrations, recurring issues, or problems the user expressed
- PREFERENCE: Stated preferences for communication style, risk tolerance, analysis depth

Conversation:
{conversation}

Extract entities as JSON:
{{
  "entities": [
    {{
      "type": "campaign|metric|decision|pain_point|preference",
      "name": "short identifier",
      "content": "full description of the entity",
      "importance": 0.0-1.0,
      "metadata": {{}}
    }}
  ]
}}

Rules:
- Only extract entities with clear evidence in the conversation
- Importance 0.9+: explicit decisions, critical metrics, stated preferences
- Importance 0.5-0.8: mentioned campaigns, context metrics, implied preferences
- Importance <0.5: background information, passing mentions
- Merge with existing entities when updating rather than creating duplicates"""

使用 Cloudflare KV 的实体存储:

class EntityStore {
  constructor(private kv: KVNamespace) {}

  async upsert(entity: BaseEntity): Promise<void> {
    const key = `entity:${entity.userId}:${entity.type}:${entity.id}`;
    const existing = await this.kv.get<BaseEntity>(key, "json");

    if (existing) {
      entity.version = existing.version + 1;
      entity.createdAt = existing.createdAt;
      if (entity.importance < existing.importance) {
        entity.importance = (entity.importance + existing.importance) / 2;
      }
    }

    await this.kv.put(key, JSON.stringify(entity), {
      expirationTtl: 60 * 60 * 24 * 180,
      metadata: {
        type: entity.type,
        importance: entity.importance,
        updatedAt: entity.updatedAt,
      },
    });

    await this.updateIndex(entity);
  }

  private async updateIndex(entity: BaseEntity): Promise<void> {
    const indexKey = `index:${entity.userId}:entities`;
    const index = await this.kv.get<EntityIndex>(indexKey, "json") || { entries: [] };

    const existingIdx = index.entries.findIndex(
      (e) => e.id === entity.id && e.type === entity.type
    );
    const entry = {
      id: entity.id,
      type: entity.type,
      name: entity.name,
      importance: entity.importance,
      updatedAt: entity.updatedAt,
    };

    if (existingIdx >= 0) {
      index.entries[existingIdx] = entry;
    } else {
      index.entries.push(entry);
    }
    await this.kv.put(indexKey, JSON.stringify(index));
  }

  async retrieveForContext(userId: string, taskContext: string, limit = 20): Promise<BaseEntity[]> {
    const indexKey = `index:${userId}:entities`;
    const index = await this.kv.get<EntityIndex>(indexKey, "json");
    if (!index) return [];

    const ranked = index.entries
      .map((entry) => ({
        ...entry,
        score: this.contextRelevance(entry, taskContext) * 0.6 + entry.importance * 0.3 + this.recencyScore(entry.updatedAt) * 0.1,
      }))
      .sort((a, b) => b.score - a.score)
      .slice(0, limit);

    const entities: BaseEntity[] = [];
    for (const entry of ranked) {
      const key = `entity:${userId}:${entry.type}:${entry.id}`;
      const entity = await this.kv.get<BaseEntity>(key, "json");
      if (entity) entities.push(entity);
    }
    return entities;
  }

  private contextRelevance(entry: { name: string; type: string }, context: string): number {
    const contextLower = context.toLowerCase();
    const nameWords = entry.name.toLowerCase().split(/\s+/);
    const matches = nameWords.filter((w) => contextLower.includes(w)).length;
    return Math.min(1, matches / Math.max(nameWords.length, 1));
  }

  private recencyScore(timestamp: number): number {
    const ageMs = Date.now() - timestamp;
    const ageDays = ageMs / (1000 * 60 * 60 * 24);
    return Math.max(0, 1 - ageDays / 180);
  }
}

根据实体组装上下文:

def assemble_entity_context(entities: list[dict], max_tokens: int = 4000) -> str:
    grouped = {}
    for entity in entities:
        grouped.setdefault(entity["type"], []).append(entity)

    type_labels = {
        "campaign": "Known Campaigns",
        "metric": "Historical Metrics",
        "decision": "Prior Decisions",
        "pain_point": "Known Pain Points",
        "preference": "User Preferences",
    }

    sections = ["<entity_memory>"]
    token_count = 0

    for entity_type in ["preference", "pain_point", "decision", "campaign", "metric"]:
        items = grouped.get(entity_type, [])
        if not items:
            continue
        label = type_labels.get(entity_type, entity_type.title())
        section = f"\n### {label}\n"
        for item in items[:5]:
            entry = f"- **{item['name']}**: {item['content']}\n"
            entry_tokens = len(entry.split()) * 1.3
            if token_count + entry_tokens > max_tokens:
                break
            section += entry
            token_count += entry_tokens
        sections.append(section)

    sections.append("</entity_memory>")
    return "\n".join(sections)

最佳实践

  1. 预先定义实体类型 — 使用固定分类体系(campaign、metric、decision、pain_point、preference),比开放式实体识别能产生更一致的提取结果。
  2. 执行更新插入,而非重复创建 — 同一实体在新会话中出现时,应更新现有记录,而不是新建记录;保留版本历史以供审计。
  3. 在提取时评估重要性 — 模型掌握完整对话上下文时,重要性评分最准确;不要等到检索时再评分。
  4. 优先注入偏好和痛点 — 这些实体类型对用户体验的影响最大;只要有,就始终注入。
  5. 按类型设置实体 TTL — 偏好可能保留 180 天,而具体指标可能在 30 天后过期;应让 TTL 与实体的有效期限相匹配。
  6. 按上下文检索,而非穷举检索 — 只注入与当前任务相关的实体;完整倾倒所有实体会浪费上下文窗口容量。
  7. 检测实体矛盾 — 当新提取的内容与已存储实体冲突时(例如策略发生变化),更新已存储的实体并标记变更。

平台兼容性

功能Claude CodeCursorCodexGemini CLI
实体提取✅ 完整支持✅ 完整支持✅ 完整支持✅ 完整支持
KV 存储✅ 任意 KV/文件✅ 任意 KV/文件✅ 任意 KV/文件✅ 任意 KV/文件
会话钩子✅ Stop hooks✅ Extensions⚠️ Custom⚠️ Custom
上下文注入✅ CLAUDE.md✅ Skills✅ Instructions✅ System prompts
索引管理✅ 完整支持✅ 完整支持✅ 完整支持✅ 完整支持

相关技能

  • 记忆持久化 - 提供实体持久化所需的底层存储和检索层
  • 知识库注入 - 汇总实体上下文,并与领域知识模式一同注入
  • 持续学习 - 提取的实体可用于挖掘模式,自动推导技能
  • Google Ads 审计 - Campaign、metric 和 decision 实体可跨会话跟踪审计发现

关键词

entity-memory、named-entities、cross-session-continuity、cloudflare-kv、entity-extraction、entity-types、importance-scoring、contextual-retrieval、user-preferences、agent-skills


© 2026 googleadsagent.ai™ | Agent Skills™ | MIT License

评论

支持 Markdown