测试数据规范
文档目的
本文档定义了 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": "一种注重内存安全和零成本抽象的系统编程语言"
}
}
| 字段 | 类型 | 说明 |
|---|---|---|
content | string | 主内容 |
aliases | string[] | 别名列表 |
concept_type | "Entity" / "Abstract" | 概念类型 |
description | string | 描述文本 |
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项目"
}
]
}
}
}
}
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
narrative | string | ✅ | 叙事文本 |
time_span | string (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"
1.2 #mem_links — 边定义
{
"from": "mem_rust",
"to": "mem_python",
"intensity": 0.8,
"link_type": {
"Sem": { "verb": "related", "confidence": 0.7 }
}
}
| 字段 | 类型 | 说明 |
|---|---|---|
from | string | 源节点 ID(必须在同一 graph 文件中) |
to | string | 目标节点 ID |
intensity | f64 | 关联强度 |
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": []
}
]
}
顶层字段说明
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
name | string | ✅ | 测试套件名称 |
description | string | ✅ | 描述 |
graph_path | string (PathBuf) | ✅ | 相对路径指向 Graph JSON |
config | TestConfigRaw | ✅ | 执行配置 |
blend_sweep | BlendSweepRaw? | ❌ | 权重调参配置 |
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_identifier | string | ✅ | 概念标识符,用于与 SemMemory.content 语义比较 |
description | string | ❌ | 描述文本,用于与 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"] }
]
| 字段 | 类型 | 说明 |
|---|---|---|
q | usize | 对应 sub_queries 数组下标 |
ranking | string[] | 期望的节点 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.rs的ActionMetrics)。
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(显式权重对)
精确指定每个权重对,可覆盖全部子字段:
| 字段 | 默认值 | 含义 |
|---|---|---|
tag | 0.3 | tag/variant 顶层融合—tag 权重 |
variant | 0.7 | tag/variant 顶层融合—variant 权重 |
sem_concept | 0.5 | 概念分 vs 描述分 |
sem_description | 0.5 | 描述分权重 |
sit_location_name | 0.6 | Location 名称 vs 坐标 |
sit_location_coord | 0.4 | |
sit_participant_name | 0.6 | Participant 名称 vs 角色 |
sit_participant_role | 0.4 | |
sit_env_atmosphere | 0.5 | Environment 氛围 vs 色调 |
sit_env_tone | 0.5 | |
sit_event_initiator | 0.3 | Event 三项权重(必须 ≥0 且三者之和建议为 1.0) |
sit_event_target | 0.3 | |
sit_event_action | 0.4 | |
sit_event_initiator_only_action | 0.6 | 缺 target 时 action 的权重(initiator = 1 - this) |
sit_event_target_only_action | 0.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个展开用例 pairs与tag_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 关键约束
- ID 一致性: query JSON 中
expected_per_query[].ranking[]和expected_combined_ranking[]引用的 ID 必须在对应 graph JSON 中存在 - 文件组织: graph JSON 建议放在
fixtures/graphs/,query JSON 放在fixtures/queries/ - graph_path 解析:
graph_path是相对于 query JSON 所在目录的路径 - 无 blend_sweep: 不写
blend_sweep字段 = 只执行一次(tag=0.3, variant=0.7) - 负样本测试: 期望
expected_combined_ranking为空数组的用例不会被视为“失败“
4.3 测试数据规模建议
| 级别 | 节点数 | 用例数 | 子查询/用例 | 适用场景 |
|---|---|---|---|---|
| 微 (unit) | 5-10 | 5-10 | 1-2 | CI 快速验证 |
| 小 (smoke) | 10-50 | 3-10 | 1-3 | 本地开发验证 |
| 中 (bench) | 100-500 | 20-50 | 1-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 提图启用后生效)。