Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

测试数据规范

文档目的

本文档定义了 SoulMem 测试数据格式规范(由数据生成脚本/工具产出,覆盖):

  • Graph JSON — 记忆图(MemoryNote 构成的带权图)
  • Query JSON — 检索测试用例(子查询、期望真值、权重调参)

一、Graph JSON — 记忆图文件

路径: fixtures/graphs/<name>.json 结构: 顶层为 GraphNodeRaw[](JSON 数组)

[
  {
    "id": "mem_rust",                   // string — 全局唯一可读ID,加载时映射为UUID
    "tags": ["Rust", "编程"],            // string[] — 标签(参与embedding计算)
    "mem_type": {
      "Semantic": { /* SemMemory */ }   // 详见下方 #mem_type
    },
    "mem_links": [ /* MemoryLink[] */ ] // 可选,默认为 []
  }
]

1.1 #mem_type — 节点类型

1.1.1 Semantic — 语义记忆

{
  "Semantic": {
    "content": "Rust语言",
    "aliases": ["Rust"],
    "concept_type": "Entity",
    "description": "一种注重内存安全和零成本抽象的系统编程语言"
  }
}
字段类型说明
contentstring主内容
aliasesstring[]别名列表
concept_type"Entity" / "Abstract"概念类型
descriptionstring描述文本

1.1.2 Situation::SpecificSituation — 具体情境记忆

{
  "Situation": {
    "SpecificSituation": {
      "narrative": "上午在办公室用Rust编写了一个HTTP服务器",
      "time_span": "2026-06-01T08:00:00Z",
      "context": {
        "location": {
          "name": "办公室",
          "coordinates": "北京,海淀"
        },
        "participants": [
          { "name": "张三", "role": "开发者" }
        ],
        "emotions": [
          { "name": "专注", "intensity": 0.9 }
        ],
        "sensory_data": [],
        "environment": {
          "atmosphere": "安静",
          "tone": "专业"
        },
        "event": [
          {
            "action": "编写代码",
            "action_intensity": 0.8,
            "initiator": "张三",
            "target": "Rust项目"
          }
        ]
      }
    }
  }
}
字段类型必填说明
narrativestring叙事文本
time_spanstring (ISO 8601)时间
context.location{name, coordinates?}地点
context.participants[][{name, role}]参与者数组
context.emotions[][{name, intensity}]情感数组
context.sensory_data[][]感官数据(当前保留,填空数组)
context.environment{atmosphere, tone}环境
context.event[][{action, action_intensity?, initiator?, target?}]事件数组

1.1.3 Situation::AbstractSituation — 抽象情境记忆

四种变体之一:

{ "Situation": { "AbstractSituation": { "Location": {"name": "学校", "coordinates": "北京"} } } }
{ "Situation": { "AbstractSituation": { "Participant": {"name": "张三", "role": "学生"} } } }
{ "Situation": { "AbstractSituation": { "Environment": {"atmosphere": "温暖", "tone": "舒适"} } } }
{ "Situation": { "AbstractSituation": { "Event": {"action": "学习", "action_intensity": 0.7, "initiator": "张三", "target": "知识"} } } }

1.1.4 Procedure — 程序记忆

{
  "Procedure": {
    "action": {
      "content": "使用搜索引擎",
      "action_type": { "Skill": {} }
    }
  }
}

action_type 枚举:

  • "Speak"
  • { "Skill": {} }
  • "Think"
{
  "from": "mem_rust",
  "to": "mem_python",
  "intensity": 0.8,
  "link_type": {
    "Sem": { "verb": "related", "confidence": 0.7 }
  }
}
字段类型说明
fromstring源节点 ID(必须在同一 graph 文件中)
tostring目标节点 ID
intensityf64关联强度
link_type以下三种之一

link_type 枚举

// 语义边
{ "Sem": { "verb": "related", "confidence": 0.7 } }

// 程序边
{ "Proc": { "TrigToAction": { "prob": 0.8 } } }

// 情境边
{ "Situation": { "AbstractToSpecific": {} } }
{ "Situation": { "SpecificToAbstract": {} } }  // 具体→抽象:PPR 从具体情境检出抽象模式的路径

二、Query JSON — 检索测试用例文件

路径: fixtures/queries/<name>.json 结构: 顶层为 RetrQueryFileRaw 对象

{
  // ═══ 元信息 ═══
  "name": "retr_sim_smoke_zh",
  "description": "向量相似性搜索冒烟测试 — 覆盖语义和情境混合查询",

  // ═══ graph 引用(相对于 query JSON 所在目录的路径) ═══
  "graph_path": "../graphs/rust_small_zh.json",

  // ═══ 检索执行配置 ═══
  "config": {
    "similarity_threshold": 0.0,      // f32 - 相似度最低阈值
    "max_results": 10,                // usize - 每次搜索最大返回数
    "test_k_values": [1, 3, 5]        // usize[] - 评估 k 值列表
  },

  // ═══ 权重调参(可选,不写则使用默认值 0.3/0.7) ═══
  "blend_sweep": {
    // 方式一: 快捷扫 tag 权重(生成 pairs: (tag, 1.0-tag))
    "tag_sweep": [0.1, 0.3, 0.5, 0.7, 0.9],

    // 方式二: 显式权重对列表(与 tag_sweep 互斥,pairs 优先)
    "pairs": [
      { "tag": 0.3, "variant": 0.7 },
      { "tag": 0.5, "variant": 0.5, "sem_concept": 0.7, "sem_description": 0.3 }
    ]
  },

  // ═══ 测试用例列表 ═══
  "test_cases": [
    {
      "name": "用户问-Rust资讯",
      "description": "LLM拆解: Rust概念查询 + 编程上下文",
      "sub_queries": [ /* SubQuery[] —— 见 #sub_query */ ],
      "expected_per_query": [ /* PerQueryExpectation[] —— 见 #expected_per_query */ ],
      "expected_combined_ranking": ["mem_rust", "mem_python"],
      "expected_actions": []
    }
  ]
}

顶层字段说明

字段类型必填说明
namestring测试套件名称
descriptionstring描述
graph_pathstring (PathBuf)相对路径指向 Graph JSON
configTestConfigRaw执行配置
blend_sweepBlendSweepRaw?权重调参配置
test_cases[]TestCaseQueryRaw[]测试用例数组

2.1 #sub_query — 子查询

每个子查询模拟 LLM 拆解出的一个独立检索请求:

{
  "priority": 1,                   // u32 - 优先级(越大越重要)
  "tag": ["Rust", "编程"],          // string[] - 标签数组
  "variant": {
    /* MemoryRetrieveQueryVariant - 二选一: Semantic 或 Situation */
  }
}

Semantic 变体

{
  "variant": {
    "Semantic": [
      {
        "concept_identifier": "Rust语言",
        "description": "系统编程语言"
      }
    ]
  }
}

Semantic 是数组,每个元素包含:

字段类型必填说明
concept_identifierstring概念标识符,用于与 SemMemory.content 语义比较
descriptionstring描述文本,用于与 SemMemory.description 语义比较

Situation 变体

{
  "variant": {
    "Situation": [
      {
        "narrative": "用Rust写HTTP服务器",
        "location": [{ "name": "办公室" }],
        "participants": [{ "name": "张三", "role": "开发者" }],
        "time_span": [
          { "start": "2026-06-01T08:00:00Z", "end": "2026-06-01T12:00:00Z" }
        ],
        "environment": { "atmosphere": "安静" },
        "event": [{
          "action": "编写代码",
          "initiator": "张三",
          "target": "HTTP服务器"
        }]
      }
    ]
  }
}

Situation 是数组,所有子字段均为可选。每种字段的嵌入向量在缺失时为 None,计算时以 0 处理。

2.2 #expected_per_query — 子查询期望结果

[
  { "q": 0, "ranking": ["mem_rust"] },
  { "q": 1, "ranking": ["mem_rust", "mem_python"] }
]
字段类型说明
qusize对应 sub_queries 数组下标
rankingstring[]期望的节点 ID 排序(按相关度降序)

ID 引用 graph JSON 中定义的 "id" 值。

2.3 #expected_combined_ranking — 合并期望

所有子查询按优先级加权合并后的期望排序结果:

"expected_combined_ranking": ["mem_rust", "mem_python"]

2.4 expected_actions — 动作节点期望

检索算法判断出的“下一步行动“期望结果,用于测试 RetrAction 模块:

"expected_actions": []

:早期版本为填空数组(占位);当前 24 角色评测数据已含非空真值, 对应评测指标为 Action Hit Rate / Action Recall@K(见 soul-tune 引擎 engine/retrieve/data.rsActionMetrics)。

2.5 抽象检出期望(可选)

带真值时才计入抽象指标(soul-tune 评测输出含 abstract_detected / abstract_direct_hit):

字段说明
has_expected_abstract该用例期望结果是否包含抽象情境节点
abstract_detected期望抽象节点是否出现在合并结果(相似度 + PPR)中
abstract_direct_hit期望抽象节点是否被相似度直接命中(数据侧泛化观测门)

三、BlendSweep — 权重扫描配置

blend_sweep 是 Query JSON 顶层的可选字段。配置后,每个基础测试用例会按每对权重展开为多个用例,用于测试不同权重组合下的检索效果。

不写 blend_sweep 字段 = 只执行一次 (tag=0.3, variant=0.7) 的默认权重。

"blend_sweep": {
  "tag_sweep": [0.1, 0.3, 0.5, 0.7, 0.9],
  "pairs": [
    { "tag": 0.3, "variant": 0.7 }
  ]
}

3.1 两种配置方式

方式一:tag_sweep(快捷扫描)

自动生成 (tag, variant = 1.0 - tag) 的权重对序列。

{ "tag_sweep": [0.3, 0.5, 0.7] }

等价于:

{ "pairs": [
  { "tag": 0.3, "variant": 0.7 },
  { "tag": 0.5, "variant": 0.5 },
  { "tag": 0.7, "variant": 0.3 }
] }

方式二:pairs(显式权重对)

精确指定每个权重对,可覆盖全部子字段:

字段默认值含义
tag0.3tag/variant 顶层融合—tag 权重
variant0.7tag/variant 顶层融合—variant 权重
sem_concept0.5概念分 vs 描述分
sem_description0.5描述分权重
sit_location_name0.6Location 名称 vs 坐标
sit_location_coord0.4
sit_participant_name0.6Participant 名称 vs 角色
sit_participant_role0.4
sit_env_atmosphere0.5Environment 氛围 vs 色调
sit_env_tone0.5
sit_event_initiator0.3Event 三项权重(必须 ≥0 且三者之和建议为 1.0)
sit_event_target0.3
sit_event_action0.4
sit_event_initiator_only_action0.6缺 target 时 action 的权重(initiator = 1 - this)
sit_event_target_only_action0.6缺 initiator 时 action 的权重(target = 1 - this)

3.2 测试用例展开

  • 不配置 blend_sweep:每个基础用例 → 1 个展开用例(默认权重)
  • 配置 tag_sweep: [0.3, 0.5, 0.7]test_cases.length × 3 个展开用例
  • 配置 pairs: [...]test_cases.length × pairs.length 个展开用例
  • pairstag_sweep 同时出现时,pairs 优先

3.3 示例

{
  "blend_sweep": {
    "tag_sweep": [0.3, 0.5, 0.7]
  },
  "test_cases": [
    { "name": "case1", ... },
    { "name": "case2", ... },
    { "name": "case3", ... }
  ]
}

3 base × 3 tag_sweep = 9 个展开用例,report 中会按 (tag, variant) 分组展示各权重下的平均指标。

{
  "blend_sweep": {
    "pairs": [
      { "tag": 0.3, "variant": 0.7 },
      {
        "tag": 0.5,
        "variant": 0.5,
        "sem_concept": 0.7,
        "sem_description": 0.3,
        "sit_participant_name": 0.8,
        "sit_participant_role": 0.2
      }
    ]
  }
}

具体实现在 expand_sweep_pairs() 函数中,参考测试用例 fixtures/queries/retr_sim_smoke_zh_blend.json


四、数据生成指南

4.1 数据流转

数据生成工具(如 soul_scraper 等)生成
  ├── graph JSON      → 反序列化 Vec<GraphNodeRaw> → BGE 嵌入 → WorkingMemory
  └── query JSON      → 反序列化 RetrQueryFileRaw   → 子查询嵌入 → RetrieveSuite.run_case()
        │
        └── graph_path 指向 graph JSON(相对 query JSON 的路径)

:仓库当前 fixtures/ 数据为手工/脚本产物,soul_scraper 工具本身不在本仓库中。

4.2 关键约束

  1. ID 一致性: query JSON 中 expected_per_query[].ranking[]expected_combined_ranking[] 引用的 ID 必须在对应 graph JSON 中存在
  2. 文件组织: graph JSON 建议放在 fixtures/graphs/,query JSON 放在 fixtures/queries/
  3. graph_path 解析: graph_path相对于 query JSON 所在目录的路径
  4. 无 blend_sweep: 不写 blend_sweep 字段 = 只执行一次 (tag=0.3, variant=0.7)
  5. 负样本测试: 期望 expected_combined_ranking 为空数组的用例不会被视为“失败“

4.3 测试数据规模建议

级别节点数用例数子查询/用例适用场景
微 (unit)5-105-101-2CI 快速验证
小 (smoke)10-503-101-3本地开发验证
中 (bench)100-50020-501-5性能/准确率 benchmark
大 (stress)5000+200+2-10压力测试

五、完整参考示例

完整 Graph JSON (fixtures/graphs/rust_small_zh.json)

[
  {
    "id": "mem_rust",
    "tags": ["Rust", "编程", "系统"],
    "mem_type": {
      "Semantic": {
        "content": "Rust语言",
        "aliases": ["Rust"],
        "concept_type": "Entity",
        "description": "一种注重内存安全和零成本抽象的系统编程语言"
      }
    },
    "mem_links": []
  },
  {
    "id": "sit_coding_day",
    "tags": ["事件", "编码", "Rust"],
    "mem_type": {
      "Situation": {
        "SpecificSituation": {
          "narrative": "上午在办公室用Rust编写了一个HTTP服务器",
          "time_span": "2026-06-01T08:00:00Z",
          "context": {
            "location": { "name": "办公室", "coordinates": "北京,海淀" },
            "participants": [{ "name": "张三", "role": "开发者" }],
            "emotions": [{ "name": "专注", "intensity": 0.9 }],
            "sensory_data": [],
            "environment": { "atmosphere": "安静", "tone": "专业" },
            "event": [{
              "action": "编写代码",
              "action_intensity": 0.8,
              "initiator": "张三",
              "target": "Rust项目"
            }]
          }
        }
      }
    },
    "mem_links": []
  }
]

完整 Query JSON (fixtures/queries/retr_sim_smoke_zh.json)

{
  "name": "retr_sim_smoke_zh",
  "description": "向量相似性搜索冒烟测试",
  "graph_path": "../graphs/rust_small_zh.json",
  "config": {
    "similarity_threshold": 0.0,
    "max_results": 10,
    "test_k_values": [1, 3, 5]
  },
  "test_cases": [
    {
      "name": "用户问-Rust资讯",
      "description": "LLM拆解: Rust概念查询 + 编程上下文",
      "sub_queries": [
        {
          "priority": 1,
          "tag": ["Rust", "编程"],
          "variant": {
            "Semantic": [
              { "concept_identifier": "Rust语言", "description": "系统编程语言" }
            ]
          }
        },
        {
          "priority": 2,
          "tag": ["编程"],
          "variant": { "Semantic": [] }
        }
      ],
      "expected_per_query": [
        { "q": 0, "ranking": ["mem_rust"] },
        { "q": 1, "ranking": ["mem_rust", "mem_python"] }
      ],
      "expected_combined_ranking": ["mem_rust", "mem_python"],
      "expected_actions": []
    },
    {
      "name": "用户问-无意义XYZ",
      "description": "负样本 — 应返回空结果",
      "sub_queries": [
        {
          "priority": 1,
          "tag": ["XYZ"],
          "variant": {
            "Semantic": [
              { "concept_identifier": "不存在的概念" }
            ]
          }
        }
      ],
      "expected_per_query": [
        { "q": 0, "ranking": [] }
      ],
      "expected_combined_ranking": [],
      "expected_actions": []
    }
  ]
}

注: 在生成查询文本时,不应该使用疑问句,不应该包含语气或关系连接词,应当描述实体或用简短的陈述句描述查询的情境。

完整 Query JSON — 带权重扫描 (fixtures/queries/retr_sim_smoke_zh_blend.json)

{
  "name": "retr_sim_smoke_zh_blend",
  "description": "带权重扫描的向量相似性搜索冒烟测试 — tag_sweep: [0.3, 0.5, 0.7]",
  "graph_path": "../graphs/rust_small_zh.json",
  "config": {
    "similarity_threshold": 0.0,
    "max_results": 10,
    "test_k_values": [1, 3, 5]
  },
  "blend_sweep": {
    "tag_sweep": [0.3, 0.5, 0.7]
  },
  "test_cases": [
    {
      "name": "用户问-Rust资讯",
      "description": "LLM拆解: Rust概念查询 + 编程上下文",
      "sub_queries": [
        {
          "priority": 1,
          "tag": ["Rust", "编程"],
          "variant": {
            "Semantic": [
              { "concept_identifier": "Rust语言", "description": "系统编程语言" }
            ]
          }
        },
        {
          "priority": 2,
          "tag": ["编程"],
          "variant": { "Semantic": [] }
        }
      ],
      "expected_per_query": [
        { "q": 0, "ranking": ["mem_rust"] },
        { "q": 1, "ranking": ["mem_rust", "mem_python"] }
      ],
      "expected_combined_ranking": ["mem_rust", "mem_python"],
      "expected_actions": []
    }
  ]
}

3 个基础测试用例 × 3 组权重 (tag=0.3, 0.5, 0.7) = 9 个展开用例。Report 按 (tag, variant) 分组展示指标。


四、Forget JSON — 遗忘效果测试

路径: fixtures/forget/<name>.json 结构: 顶层对象,自包含(不依赖 graph 文件),覆盖艾宾浩斯遗忘曲线的五个可观测推论(T1–T5)。

遗忘算法的效果判据是「信息量的遗忘是否符合艾宾浩斯遗忘曲线」。直接验证曲线本身会陷入循环论证(缺失度就是按曲线算的),因此测试验证曲线的可观测推论

推论用例类型 kind判定
T1 时间单调性time_monotonic缺失度随经过时间单调不减
T2 激活抑制activation缺失度随激活次数单调不增(封顶后不再下降)
T3 量级校准magnitude半衰期处缺失度 ≈ 期望值(容差内)
T4 分段行为branch三个时间点分别落入 NoAction / MaskOnly / Revised 区间
T5 节点效果effect遗忘触发与否 + 动作强度 + 语义熵增(前后 trigram 相似度下降)

顶层字段

{
  "name": "forget_ebbinghaus_smoke",
  "description": "艾宾浩斯遗忘曲线效果冒烟测试",
  "config": {
    "base_half_life_hours": 24.0,   // 半衰期(小时),R=0.5 的时间点
    "active_factor": 0.1,           // 激活抑制系数:半衰期 ×= (1 + active_factor × min(retrieval, cap))
    "max_activation_cap": 50        // 激活次数计入遗忘的上限
  },
  "test_cases": [ /* 见下方各类型 */ ]
}

time_monotonic(T1)

{
  "kind": "time_monotonic",
  "name": "T1-时间单调性",
  "text": "今天下午我和张三在北京王府井的星巴克讨论了项目进展",
  "time_offsets_hours": [0, 6, 12, 24, 48, 96, 168],  // 升序扫描
  "retrieval_count": 0
}

activation(T2)

{
  "kind": "activation",
  "name": "T2-激活抑制",
  "text": "鲁迅原名周树人浙江绍兴人",
  "activation_counts": [0, 5, 20, 50, 200],  // 扫描激活次数
  "time_offset_hours": 48
}

magnitude(T3)

{
  "kind": "magnitude",
  "name": "T3-半衰期校准",
  "text": "机器学习是人工智能的一个重要分支",
  "time_offset_hours": 24,
  "expected_missing_degree": 0.5,
  "tolerance": 0.08,
  "retrieval_count": 0
}

branch(T4)

{
  "kind": "branch",
  "name": "T4-分段行为",
  "text": "昨天下午我们团队在会议室开了三个小时的 Sprint 回顾会议",
  "time_offsets_hours": [0, 4, 96],  // 应分别落入 NoAction / MaskOnly / Revised
  "retrieval_count": 0
}

effect(T5)

{
  "kind": "effect",
  "name": "T5-语义节点遗忘",
  "mem_kind": "semantic",        // "semantic"(SemMemory)或 "situation"(SpecificSituation)
  "text": "张三上个月去杭州出差在西湖边吃了东坡肉和龙井虾仁",
  "retrieval_count": 0,
  "time_offset_hours": 96,
  "expected": {
    "should_forget": true,       // 是否应触发遗忘(缺失度超过阈值)
    "min_action": "MaskOnly"     // "NoAction" | "MaskOnly" | "Revised",动作强度下限
  }
}

运行方式

# headless 单数据集
soul-tune run forget fixtures/forget/forget_ebbinghaus_smoke.json

# 或 TUI 中按 F / 命令模式 `test forget`

Report 指标:用例通过率、遗忘触发率(T5 中 should_forget 且实际触发的比例)、平均缺失度、平均图谱变换评分(预留,LLM 提图启用后生效)。