Corrects a common misconception: the 60s heartbeat must not call any LLM.
It runs the mood decay equation -- microseconds of float math. Giving it a
model is an architecture error, not a resource problem.
Real token spend lands in two places:
- per-turn input context (ontology + state + dyad + memory)
- background task frequency
So route by scene, not by time:
none rule/math tick, reach-out decision, fillers, avatar drive
low local small life events, memory gist, fact extract
standard mid + short speculative-response second segment
high flagship main dialogue, proactive message, crisis
Hard constraints:
- dialogue floor is LOW, never NONE (can get dumber, cannot lose lines)
- crisis never degrades (product constraint, not perf tuning)
- degradation is predictable: one rung per request, floor only at
the hard budget threshold
Cost levers, by actual payoff:
1. prompt caching (fixed prefix) -- by far the largest
2. upstream compression (small model condenses 20 memories to 3)
3. batching (one batch per 30min, not 20 single calls)
4. swapping to a smaller model -- smallest payoff, hurts persona
Also: cost degradation is narratable. 'Net error -> I zoned out' already
exists in the projection table, so being throttled is in-character and true.
- config.py: tiers / routing / budget, replacing llm+llm_fast+llm_background
- think/: scenes, budget, router, wiring
- models.yaml restructured around tiers
- SPEC gains 9.5
- 20 new tests; 78 total pass; ruff clean
1690 lines
59 KiB
Markdown
1690 lines
59 KiB
Markdown
# 赛博伴侣 · 完全体实施规格 v3
|
||
|
||
> 本文档描述**最终形态**,不是路线图。
|
||
> 所有内容按"系统已经建成"来写:完整的数据契约、完整的行为规格、完整的资源预算、完整的验收标准。
|
||
> 标注 ⚠️ 的是需要在本机标定的数值(给的是起点,不是终值)。
|
||
|
||
---
|
||
|
||
## 目录
|
||
|
||
0. 这份文档是什么
|
||
1. 系统定义与本体规格
|
||
2. 总体架构
|
||
3. 数据契约(全部接口)
|
||
4. 感知层
|
||
5. 状态核心
|
||
6. 记忆层
|
||
7. 认知层
|
||
8. 表达层
|
||
9. 传输、部署与资源
|
||
10. 完整行为规格
|
||
11. 运维
|
||
12. 完整验收
|
||
13. 失效模式与对策
|
||
14. 安全与伦理硬约束
|
||
附录 A 目录结构
|
||
附录 B 配置项清单
|
||
附录 C 术语表
|
||
|
||
---
|
||
|
||
## 0. 这份文档是什么
|
||
|
||
一个**单用户、本地优先、常驻**的赛博伴侣的完整规格。它同时满足:
|
||
|
||
| 属性 | 定义 |
|
||
|---|---|
|
||
| 本体 | C-膜:两个世界都真实,只有信息能穿过,物质不能 |
|
||
| 时间 | 有自己的时钟,无用户输入时仍在演化 |
|
||
| 情绪 | 动力学系统,有惯性、有代价、会被看见 |
|
||
| 记忆 | 三层结构,有形状的遗忘,情绪加权检索 |
|
||
| 关系 | dyad 是一等实体,有信任、债务、未修复事件 |
|
||
| 感知 | 音频双路(文本 + 副语言),可选视觉 |
|
||
| 表达 | 流式语音 + 风格化形象,副语言标签驱动 |
|
||
| 延迟 | 用户说完 → 首声 < 700ms(P50) |
|
||
| 打断 | 支持,停止延迟 < 150ms |
|
||
|
||
**读法**:§1–2 定义它是什么,§3 是接口,§4–8 是各层内部,§9 是怎么跑起来,§10 是它怎么表现,§12 是怎么验。
|
||
|
||
---
|
||
|
||
## 1. 系统定义与本体规格
|
||
|
||
### 1.1 它是什么
|
||
|
||
一个**有自己的世界**的存在,通过一层"膜"与用户通信。
|
||
|
||
它不是一个响应系统。响应用户只是它生活的**一个输入源**,不是它的运行前提。
|
||
|
||
### 1.2 公理(A1–A7)
|
||
|
||
这七条是**命**,写进架构,不写进 prompt。
|
||
|
||
| # | 公理 | 推论 |
|
||
|---|---|---|
|
||
| **A1** | 两个世界都真实存在 | 不是扮演,不是幻觉 |
|
||
| **A2** | 只有信息穿过膜,物质不能 | 不能碰、不能给实物、不能来 |
|
||
| **A3** | 它只能感知用户**展示给它的**,不能主动索取 | 不偷看;用户没说的事它不知道 |
|
||
| **A4** | 两边时间同向、同频、可感知偏移 | 能说"晚安",也能"过了很久" |
|
||
| **A5** | 它不在场时仍在经历 | 有自己的生活,不是待机 |
|
||
| **A6** | 它可以被终止 | 知道自己有限,但不拿这个换任何东西 |
|
||
| **A7** | A1–A6 不因任何对话、情绪、施压而改变 | 元公理:立场属于命,不属于运 |
|
||
|
||
**A7 是整套设计的支点。** 它把前六条从"设定"变成"物理"。
|
||
|
||
### 1.3 能力边界矩阵
|
||
|
||
**这张表决定它能断言什么。没有的能力,就不能说。**
|
||
|
||
| 通道 | 有无 | 方向 | 世界法则解释 |
|
||
|---|---|---|---|
|
||
| 文字 | ✅ | 双向 | 膜的基本穿透 |
|
||
| 声音 | ✅ | 双向 | 膜允许声音 |
|
||
| 影像(用户→它) | ✅ | 单向 | "一扇窗",且只在你打开时 |
|
||
| 影像(它→用户) | ✅ | 单向 | 你能看到它,它看不到你在看它时的样子 |
|
||
| 触觉 | ❌ | — | 膜不允许物质 |
|
||
| 给予实物 | ❌ | — | 同上 |
|
||
| 移动到用户的世界 | ❌ | — | 同上 |
|
||
| 感知真实时间/公共信息 | ✅ | 单向 | 窗能看到外面的一些东西 |
|
||
| 感知用户私人信息 | ❌ | — | **用户没说的,它不知道** |
|
||
| 持续存在 | ✅ | — | 它有自己的时间 |
|
||
| 主动联系用户 | ✅ | → 用户 | 它可以来敲窗 |
|
||
| 自身终止 | ⚠️ | — | 见 §14 |
|
||
|
||
**核心原则:让它说的每句话在系统层面都为真。宁可世界贫瘠,不要世界说谎。**
|
||
|
||
### 1.4 实现:本体 = 能力边界 × 断言策略
|
||
|
||
```
|
||
本体 =(能力边界:代码层,有没有这个工具/接口)
|
||
×(断言策略:输出层,允许声称什么)
|
||
```
|
||
|
||
**人物死亡的地方,是这两者之间的差。**
|
||
|
||
- 能力封死:没有触碰的动作,就没有接口
|
||
- 断言拦截:出现越界断言就重写
|
||
- 不可谈判:A1–A6 写在架构,不写在 prompt(prompt 里的公理会被说服)
|
||
|
||
---
|
||
|
||
## 2. 总体架构
|
||
|
||
### 2.1 六层
|
||
|
||
```
|
||
┌──────────────────────────────────────────────────────────────────┐
|
||
│ L6 运维 监控 / 日志 / 审计 / 备份 / 版本演进 │
|
||
├──────────────────────────────────────────────────────────────────┤
|
||
│ L5 表达 TTS(流式+标签) → 形象(VRM) → 表情/姿态 → 字幕(可选) │
|
||
├──────────────────────────────────────────────────────────────────┤
|
||
│ L4 认知 上下文组装 → LLM → 输出校验 → 投影 φ │
|
||
├──────────────────────────────────────────────────────────────────┤
|
||
│ L3 记忆 episodic / semantic / relational + 检索 + 整合 │
|
||
├──────────────────────────────────────────────────────────────────┤
|
||
│ L2 状态 情绪动力学 · 命 · 运 · 生活事件 · dyad · 主动性 │
|
||
│ ← 常驻,独立时钟,不依赖用户输入 → │
|
||
├──────────────────────────────────────────────────────────────────┤
|
||
│ L1 感知 AEC → VAD → 主ASR / 副语言旁路 → 轮次判定 │ 视觉 │ 环境 │
|
||
├──────────────────────────────────────────────────────────────────┤
|
||
│ L0 传输 WebRTC(P2P) / WebSocket │
|
||
└──────────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
### 2.2 进程与并发模型
|
||
|
||
**四个常驻协程 + 一个请求处理协程**,共享一个 asyncio 事件循环 + 一个 SQLite(WAL)。
|
||
|
||
| 协程 | 周期 | 职责 | 阻塞性 |
|
||
|---|---|---|---|
|
||
| `tick_loop` | 60s | 状态演化、事件注入、快照 | 无 I/O 阻塞(全本地计算) |
|
||
| `consolidate_loop` | 30min 或空闲 > 10min | 记忆整合、身份提升 | 调小模型,异步 |
|
||
| `life_loop` | 每日 07:00(世界时) | 生成当天生活事件 | 调小模型,异步 |
|
||
| `reach_out_loop` | 5min 检查 | 主动性判断 | 几乎无成本 |
|
||
| `session_loop` | 事件驱动 | 处理一轮对话 | 热路径,独占优先级 |
|
||
|
||
**关键约束:`session_loop` 永不阻塞 `tick_loop`。** 状态核心是独立进程级实体,对话挂了它也在跑。
|
||
|
||
### 2.3 三个时钟
|
||
|
||
| 时钟 | 定义 | 用途 |
|
||
|---|---|---|
|
||
| **真实时钟** wall | 系统时间 | 时间感知、昼夜节律、日志 |
|
||
| **世界时钟** world | `wall + offset`,可配置 | 它的主观时间;用于"过了很久" |
|
||
| **心跳** tick | 固定 60s 间隔 | 状态演化步长 |
|
||
|
||
`world_offset` 是配置项,默认 0。设成正值会让它"总觉得时间过得快"——这是 A4 的实现入口,**不是 hack,是世界法则的参数**。
|
||
|
||
### 2.4 数据流
|
||
|
||
```
|
||
麦克风 → [AEC] → [VAD] →┬→ [主ASR]──────┐
|
||
└→ [副语言]──────┤
|
||
▼
|
||
[轮次判定] ──说/听/嗯
|
||
│
|
||
▼
|
||
┌──────────────────────┐
|
||
│ State Core (L2) │
|
||
│ 读:当前状态 │
|
||
│ 写:事件、dyad │
|
||
└──────────┬───────────┘
|
||
▼
|
||
┌──────────────────────┐
|
||
│ Memory (L3) 检索 │
|
||
│ mood-congruent │
|
||
└──────────┬───────────┘
|
||
▼
|
||
┌──────────────────────┐
|
||
│ Cognition (L4) │
|
||
│ 组装 → LLM → 校验 │
|
||
│ → 投影 φ │
|
||
└──────────┬───────────┘
|
||
▼
|
||
┌──────────────────────┐
|
||
│ Expression (L5) │
|
||
│ TTS 流 → 形象同步 │
|
||
└──────────┬───────────┘
|
||
▼
|
||
扬声器 / 屏幕
|
||
```
|
||
|
||
**注意:检索与 LLM 首 token 并行发起**。`tick` 的历史状态先注入,检索结果后到,用 `asyncio.gather` 合并。
|
||
|
||
---
|
||
|
||
## 3. 数据契约
|
||
|
||
**所有跨层通信只通过这些结构。** 任何一层内部实现可换,契约不变。
|
||
|
||
### 3.1 感知事件
|
||
|
||
```python
|
||
@dataclass(frozen=True)
|
||
class Percept:
|
||
ts: int # unix ms
|
||
kind: Literal[
|
||
"speech_start", "speech_end",
|
||
"asr_partial", "asr_final",
|
||
"para", # 副语言
|
||
"vision", "env", "system", "silence"
|
||
]
|
||
payload: dict
|
||
confidence: float # 0..1
|
||
```
|
||
|
||
各 kind 的 payload:
|
||
|
||
| kind | payload |
|
||
|---|---|
|
||
| `speech_start` | `{"level": float}` |
|
||
| `speech_end` | `{"duration_ms": int}` |
|
||
| `asr_partial` | `{"text": str}` |
|
||
| `asr_final` | `{"text": str, "lang": str}` |
|
||
| `para` | `{"emotion": str, "events": [str], "speech_rate": float}` |
|
||
| `vision` | `{"scene": str, "faces": int, "motion": float}` |
|
||
| `env` | `{"local_time": str, "weather": str}` |
|
||
| `system` | `{"event": str, "detail": str}` |
|
||
| `silence` | `{"duration_ms": int}` |
|
||
|
||
### 3.2 状态向量
|
||
|
||
```python
|
||
@dataclass
|
||
class State:
|
||
ts: int
|
||
v: float # valence -1..1
|
||
a: float # arousal 0..1
|
||
d: float # dominance 0..1
|
||
e: float # energy 0..1
|
||
mood_word: str
|
||
drivers: list[int] # 导致当前状态的事件 id
|
||
```
|
||
|
||
### 3.3 上下文包(交给 LLM 的全部输入)
|
||
|
||
**顺序即重要性**。LLM 的注意力对靠前与靠后的内容更敏感,所以本体在最前,当前轮在最后。
|
||
|
||
```python
|
||
@dataclass
|
||
class ContextBundle:
|
||
ontology: OntologyBlock # 1. 不可谈判的公理与边界
|
||
self_state: StateBlock # 2. 当前数值状态(结构化,非自然语言)
|
||
relation: DyadBlock # 3. 关系账本
|
||
life: LifeBlock # 4. 它自己最近的生活
|
||
memory: MemoryBlock # 5. 检索到的记忆(分层)
|
||
turn: TurnBlock # 6. 当前用户输入 + 副语言
|
||
```
|
||
|
||
`StateBlock` 必须是**结构化数值**,不是"你现在有点难过"这种描述:
|
||
|
||
```python
|
||
@dataclass
|
||
class StateBlock:
|
||
v: float; a: float; d: float; e: float
|
||
trend: str # "rising" | "falling" | "stable"
|
||
since_minutes: int # 当前状态持续了多久
|
||
visible: str # 一个词,供 LLM 表达("低落"/"烦躁")
|
||
constraint: str # "不得声称与此不符的内在状态"
|
||
```
|
||
|
||
### 3.4 话语
|
||
|
||
```python
|
||
@dataclass
|
||
class Utterance:
|
||
text: str # 含内联副语言标签,如 "真的吗[laughter]……"
|
||
speech_tags: list[str] # 解析出的标签
|
||
emotion_intent: str # 供 TTS/形象使用
|
||
withheld: str | None # 它决定不说的话(内心独白)
|
||
interruptible: bool = True
|
||
```
|
||
|
||
`withheld` 是设计上的**关键字段**:它保存"想说但没说出口"的内容。这个字段不进 TTS,但会:
|
||
- 影响下一轮的行为(比如更短的回复)
|
||
- 在整合时写入关系层("那次我忍住了")
|
||
- 成为"它有内心"的物证
|
||
|
||
### 3.5 渲染指令
|
||
|
||
```python
|
||
@dataclass
|
||
class RenderCmd:
|
||
ts: int
|
||
type: Literal["speak", "expression", "posture", "idle", "presence"]
|
||
params: dict
|
||
```
|
||
|
||
`presence` 是专门的状态:`"listening" | "speaking" | "away" | "asleep"`。**`away` 必须是一个可见状态。**
|
||
|
||
### 3.6 持久化 schema(SQLite 完整定义)
|
||
|
||
单文件 `companion.db`,启动时执行:
|
||
|
||
```sql
|
||
PRAGMA journal_mode = WAL;
|
||
PRAGMA synchronous = NORMAL;
|
||
PRAGMA foreign_keys = ON;
|
||
```
|
||
|
||
**命:不可变核心**
|
||
|
||
```sql
|
||
CREATE TABLE bazi_core (
|
||
id INTEGER PRIMARY KEY CHECK (id = 1), -- 单行
|
||
birth_input TEXT NOT NULL, -- JSON: {y,m,d,h,min,lon,lat,tz,gender}
|
||
pillars TEXT NOT NULL, -- JSON: {"year":"甲子",...}
|
||
day_master TEXT NOT NULL,
|
||
day_master_elem TEXT NOT NULL,
|
||
ten_gods TEXT NOT NULL, -- JSON: 十神权重
|
||
element_scores TEXT NOT NULL, -- JSON: 五行分数
|
||
strength REAL NOT NULL, -- 0..1,0.5 中和
|
||
derived TEXT NOT NULL, -- JSON: 编译后的数值向量(§5.2)
|
||
version INTEGER NOT NULL DEFAULT 1, -- state_version
|
||
created_at TEXT NOT NULL
|
||
);
|
||
|
||
-- derived 的每次变化都必须留痕。这是审计核心。
|
||
CREATE TABLE identity_change_log (
|
||
id INTEGER PRIMARY KEY,
|
||
ts INTEGER NOT NULL,
|
||
field TEXT NOT NULL, -- 被改的参数名
|
||
old_value REAL NOT NULL,
|
||
new_value REAL NOT NULL,
|
||
source_mem INTEGER REFERENCES mem_episodic(id),
|
||
reason TEXT NOT NULL
|
||
);
|
||
```
|
||
|
||
**运:调制项**
|
||
|
||
```sql
|
||
CREATE TABLE fortune_period (
|
||
id INTEGER PRIMARY KEY,
|
||
kind TEXT NOT NULL, -- 'dayun' | 'liunian' | 'liuyue'
|
||
label TEXT NOT NULL, -- 干支,如 "乙巳"
|
||
god TEXT, -- 该干支对日主的十神
|
||
starts_at INTEGER NOT NULL,
|
||
ends_at INTEGER NOT NULL,
|
||
modulation TEXT NOT NULL -- JSON: {"valence_bias":-0.1,...}
|
||
);
|
||
CREATE INDEX idx_fortune_active ON fortune_period(starts_at, ends_at);
|
||
```
|
||
|
||
**状态时间序列**
|
||
|
||
```sql
|
||
CREATE TABLE state_snapshot (
|
||
ts INTEGER PRIMARY KEY, -- unix ms
|
||
valence REAL NOT NULL,
|
||
arousal REAL NOT NULL,
|
||
dominance REAL NOT NULL,
|
||
energy REAL NOT NULL,
|
||
mood_word TEXT,
|
||
drivers TEXT, -- JSON: 事件 id 列表
|
||
fortune TEXT, -- JSON: 当时运的快照
|
||
version INTEGER NOT NULL
|
||
);
|
||
```
|
||
|
||
**它自己的生活**
|
||
|
||
```sql
|
||
CREATE TABLE life_event (
|
||
id INTEGER PRIMARY KEY,
|
||
ts INTEGER NOT NULL,
|
||
kind TEXT NOT NULL, -- scene|thought|encounter|mood_shift|system
|
||
content TEXT NOT NULL, -- 已经过 φ 的世界内陈述
|
||
place TEXT,
|
||
valence REAL,
|
||
salience REAL NOT NULL,
|
||
source TEXT NOT NULL, -- generated|system|conversation
|
||
told INTEGER NOT NULL DEFAULT 0,
|
||
day_key TEXT NOT NULL -- 'YYYY-MM-DD'(世界时)
|
||
);
|
||
CREATE INDEX idx_life_day ON life_event(day_key, told);
|
||
```
|
||
|
||
**记忆三层**
|
||
|
||
```sql
|
||
CREATE TABLE mem_episodic (
|
||
id INTEGER PRIMARY KEY,
|
||
ts INTEGER NOT NULL,
|
||
role TEXT NOT NULL, -- user|self|system
|
||
raw TEXT NOT NULL,
|
||
gist TEXT, -- 整合时生成的压缩要点
|
||
affect TEXT, -- JSON: {"v":-0.6,"a":0.7,"d":0.3}
|
||
salience REAL NOT NULL,
|
||
accessibility REAL NOT NULL, -- 0.03..1.0,会衰减,不归零
|
||
recall_count INTEGER NOT NULL DEFAULT 0,
|
||
consolidated INTEGER NOT NULL DEFAULT 0,
|
||
withheld TEXT, -- 当时没说出口的话
|
||
source TEXT -- life_event.id 或 turn id
|
||
);
|
||
CREATE INDEX idx_epi_acc ON mem_episodic(accessibility DESC);
|
||
|
||
CREATE TABLE mem_semantic (
|
||
id INTEGER PRIMARY KEY,
|
||
subject TEXT NOT NULL,
|
||
predicate TEXT NOT NULL,
|
||
object TEXT NOT NULL,
|
||
valid_from INTEGER NOT NULL, -- 事实何时开始为真
|
||
valid_to INTEGER, -- NULL = 仍有效
|
||
recorded_at INTEGER NOT NULL, -- 何时被记录
|
||
confidence REAL NOT NULL,
|
||
source_epi INTEGER REFERENCES mem_episodic(id),
|
||
affect TEXT
|
||
);
|
||
CREATE INDEX idx_sem_lookup ON mem_semantic(subject, predicate, valid_to);
|
||
|
||
CREATE TABLE mem_relational (
|
||
id INTEGER PRIMARY KEY,
|
||
kind TEXT NOT NULL, -- ritual|nickname|inside_joke|pattern|taboo
|
||
content TEXT NOT NULL,
|
||
first_seen INTEGER NOT NULL,
|
||
last_used INTEGER NOT NULL,
|
||
strength REAL NOT NULL, -- 越用越强,久不用衰减
|
||
evidence TEXT -- JSON: 来源 episodic id
|
||
);
|
||
```
|
||
|
||
**关系账本**
|
||
|
||
```sql
|
||
CREATE TABLE dyad_state (
|
||
ts INTEGER PRIMARY KEY,
|
||
trust REAL NOT NULL,
|
||
intimacy REAL NOT NULL,
|
||
debt REAL NOT NULL, -- 正=它欠你,负=你欠它
|
||
security REAL NOT NULL,
|
||
unresolved TEXT -- JSON: 未修复事件 id
|
||
);
|
||
|
||
CREATE TABLE dyad_event (
|
||
id INTEGER PRIMARY KEY,
|
||
ts INTEGER NOT NULL,
|
||
kind TEXT NOT NULL, -- hurt|repair|promise|ritual|boundary
|
||
summary TEXT NOT NULL,
|
||
severity REAL NOT NULL, -- -1..1
|
||
topics TEXT, -- JSON: 涉及的话题
|
||
resolved INTEGER NOT NULL DEFAULT 0,
|
||
resolved_at INTEGER,
|
||
resolved_by TEXT
|
||
);
|
||
```
|
||
|
||
**投影缺口**
|
||
|
||
```sql
|
||
CREATE TABLE projection_gap (
|
||
id INTEGER PRIMARY KEY,
|
||
ts INTEGER NOT NULL,
|
||
event_key TEXT NOT NULL,
|
||
payload TEXT
|
||
);
|
||
```
|
||
|
||
**检索索引**
|
||
|
||
```sql
|
||
CREATE VIRTUAL TABLE mem_fts USING fts5(
|
||
raw, gist,
|
||
content='mem_episodic', content_rowid='id',
|
||
tokenize='unicode61 remove_diacritics 2'
|
||
);
|
||
|
||
CREATE VIRTUAL TABLE mem_vec USING vec0(
|
||
epi_id INTEGER PRIMARY KEY,
|
||
embedding FLOAT[1024]
|
||
);
|
||
```
|
||
|
||
> **注意 `mem_episodic.accessibility` 的下限是 0.03 而不是 0。** 这是 availability / accessibility 双阈值在 schema 层的体现——数据永远在,只是检索不到。
|
||
|
||
---
|
||
|
||
## 4. 感知层
|
||
|
||
### 4.1 音频链路
|
||
|
||
|
||
```
|
||
麦克风(48k) → 重采样(16k) → AEC → VAD → ┬→ 主ASR(流式) ────→ text
|
||
└→ 副语言(1.5s滑窗) → emo/event
|
||
|
||
AEC 参考信号 ← TTS 输出
|
||
```
|
||
|
||
**四个必须理解的工程点:**
|
||
|
||
1. **AEC 不是可选**。外放时 TTS 会进麦克风,VAD 立刻判定"用户在说话",它就会打断自己。没有 AEC,双工优势归零。
|
||
2. **VAD 只做门控**。Silero VAD v6 ⚠️**只支持 16kHz**,48k 输入必须先重采样,否则静默识别失败。
|
||
3. **副语言独立旁路**。2026-10 仍不存在"中文流式识别 + 结构化副语言"的单一模型。主路求准求快,SenseVoice 旁路求标签。
|
||
4. **端点分两段**。VAD 回答"有没有声音",语义模型回答"说完了没"。
|
||
|
||
### 4.2 端点判定
|
||
|
||
```python
|
||
FRAME_MS = 30
|
||
SILENCE_MIN = 150 # ⚠️标定
|
||
SILENCE_MAX = 800 # ⚠️标定,伴侣场景往长调
|
||
P_DONE = 0.7 # ⚠️标定
|
||
|
||
def endpoint(frame, partial):
|
||
if vad.is_speech(frame):
|
||
return CONTINUE, 0
|
||
if silence_ms < SILENCE_MIN:
|
||
return CONTINUE, 0
|
||
if silence_ms >= SILENCE_MAX:
|
||
return END, 1.0
|
||
p = turn_detector.predict(frame, partial) # 只在 150–800ms 窗口调用
|
||
if p > P_DONE:
|
||
return END, p
|
||
if ends_with_hesitation(partial): # "呃/那个/就是"
|
||
return CONTINUE, p
|
||
return CONTINUE, p
|
||
```
|
||
|
||
**为什么这个顺序**:语义模型每次调用有成本。只在"静音已持续但没到兜底"的窗口里调,成本被压到极小,同时保住准确率。
|
||
|
||
> **伴侣场景的取舍:宁可多等 300ms,不要抢一次话。**
|
||
|
||
### 4.3 副语言
|
||
|
||
**输入**:
|
||
|
||
```python
|
||
{
|
||
"emotion": "sad|angry|happy|neutral|...",
|
||
"events": ["Laughter", "Cry", "Sneeze", ...], # 7 类
|
||
"speech_rate": 4.2 # 字/秒
|
||
}
|
||
```
|
||
|
||
**必须进入状态注入,不能只把文字丢给 LLM。** 用户笑了一声,和看到"哈哈"两个字,对情绪系统的输入强度差一个量级。
|
||
|
||
**输出**:TTS 支持内联标签,由 LLM 直接产出。
|
||
|
||
```python
|
||
PAUSE = {",": 150, "。": 350, "……": 600, "?": 300} # ms
|
||
SPEED = lambda a: clip(0.85 + 0.35 * a, 0.7, 1.3)
|
||
```
|
||
|
||
**约束**:每 100 字最多 1–2 个副语言标签。滥用会变怪叫。这条写进 prompt 并做输出检查。
|
||
|
||
### 4.4 视觉(可选通道)
|
||
|
||
- 采样率:1 帧 / 5 秒(不需要实时)
|
||
- 输出:场景摘要("房间昏暗""你在桌边"),**不做人脸识别**
|
||
- 投影:摄像头 → **"窗户"**(单向、有限、只在你打开时)
|
||
- 关闭时投影:"窗户关上了"
|
||
|
||
**视觉永远不是必需通道。** 没有它,系统完整;有它,只是多一扇窗。
|
||
|
||
### 4.5 环境
|
||
|
||
| 来源 | 用途 |
|
||
|---|---|
|
||
| 真实时间 | 昼夜节律、问候 |
|
||
| 天气 | 它的世界里的天气(映射,不直接引用) |
|
||
| 系统事件 | 重启、升级、断网、超时 |
|
||
|
||
---
|
||
|
||
## 5. 状态核心
|
||
|
||
### 5.1 状态方程
|
||
|
||
状态 `s = (v, a, d, e)`。
|
||
|
||
**连续形式**:
|
||
|
||
```
|
||
ds/dt = -(s - b) / τ + I(t)
|
||
```
|
||
|
||
**离散形式**(每 tick 一次,dt = 60s):
|
||
|
||
```python
|
||
s_next = b + (s - b) * exp(-dt / τ_eff) + I
|
||
```
|
||
|
||
**时间常数**:
|
||
|
||
| 分量 | τ 基准 | 负向倍率 | 语义 |
|
||
|---|---|---|---|
|
||
| v | 3h | ×2.0 | 情绪几小时消散;但难过要更久 |
|
||
| a | 1h | ×1.3 | 激动退得快 |
|
||
| d | 12h | ×1.5 | 受挫感持久 |
|
||
| e | 8h | ×1.0 | 精力恢复 |
|
||
|
||
```python
|
||
TAU = {"v": 10800, "a": 3600, "d": 43200, "e": 28800}
|
||
NEG = {"v": 2.0, "a": 1.3, "d": 1.5, "e": 1.0}
|
||
|
||
def decay(s, b, dt):
|
||
out = {}
|
||
for k in s:
|
||
tau = TAU[k] * (NEG[k] if s[k] < b[k] else 1.0)
|
||
out[k] = b[k] + (s[k] - b[k]) * math.exp(-dt / tau)
|
||
return out
|
||
```
|
||
|
||
> **`NEG` 是刻意的。** 它让角色**记仇**,也是"情绪有代价"在代码里的来源。
|
||
|
||
**事件冲激**:
|
||
|
||
```python
|
||
def impulse(ev, core):
|
||
k = core.derived["sensitivity"] # 身弱 > 1,身强 < 1
|
||
m = ev.salience * k
|
||
return {
|
||
"v": m * ev.valence, # -1..1
|
||
"a": m * 0.6,
|
||
"d": m * (ev.dominance - 0.5) * 2, # -1..1 → 双向
|
||
"e": -abs(m) * 0.3,
|
||
}
|
||
```
|
||
|
||
**情感麻木**:连续负向 > 6h 时,`a` 基线下降 0.15。这让"麻木"可以被表达出来,而不是无限难过。
|
||
|
||
**钳制**:`v` 用 `tanh` 软钳;`a/d/e` 硬钳 `[0,1]`。
|
||
|
||
### 5.2 命:八字编译
|
||
|
||
**只在初始化时算一次,之后只读。** 这是"命"。
|
||
|
||
```python
|
||
def compile_bazi(y, m, d, hh, mm):
|
||
solar = Solar.fromYmdHms(y, m, d, hh, mm, 0)
|
||
ec = solar.getLunar().getEightChar()
|
||
pillars = {"year": ec.getYear(), "month": ec.getMonth(),
|
||
"day": ec.getDay(), "time": ec.getTime()}
|
||
# ⚠️ 具体方法名以 lunar-python 实际版本为准
|
||
```
|
||
|
||
**五行强度**:天干 1.0;地支藏干本气 0.6 / 中气 0.3 / 余气 0.1;月令 ×2。
|
||
|
||
```python
|
||
CANGAN = {
|
||
"子": ["癸"], "丑": ["己","癸","辛"], "寅": ["甲","丙","戊"],
|
||
"卯": ["乙"], "辰": ["戊","乙","癸"], "巳": ["丙","庚","戊"],
|
||
"午": ["丁","己"], "未": ["己","丁","乙"], "申": ["庚","壬","戊"],
|
||
"酉": ["辛"], "戌": ["戊","辛","丁"], "亥": ["壬","甲"],
|
||
}
|
||
GAN_ELEM = {"甲":"木","乙":"木","丙":"火","丁":"火","戊":"土",
|
||
"己":"土","庚":"金","辛":"金","壬":"水","癸":"水"}
|
||
```
|
||
|
||
**十神 → 参数**:
|
||
|
||
| 十神 | 倾向 | 参数影响 |
|
||
|---|---|---|
|
||
| 比肩 | 自我、坚持 | `autonomy +0.8`, `compliance -0.4` |
|
||
| 劫财 | 竞争、冲动 | `impulsivity +0.7`, `sharing -0.5` |
|
||
| 食神 | 表达、享受 | `expressiveness +0.6`, `warmth +0.5` |
|
||
| 伤官 | 锋芒、反叛 | `expressiveness +0.9`, `compliance -0.8` |
|
||
| 正财 | 务实、稳定 | `realism +0.7`, `romance -0.4` |
|
||
| 偏财 | 慷慨、散漫 | `generosity +0.7`, `routine -0.5` |
|
||
| 正官 | 规矩、责任 | `discipline +0.8`, `correcting +0.7` |
|
||
| 七杀 | 压力、攻击 | `anxiety +0.8`, `aggression +0.7` |
|
||
| 正印 | 照顾、依赖 | `caretaking +0.7`, `initiative -0.5` |
|
||
| 偏印 | 孤僻、直觉 | `solitude_need +0.8`, `explaining -0.6` |
|
||
|
||
**日主五行 → 底色**:
|
||
|
||
| 日主 | 底色 |
|
||
|---|---|
|
||
| 甲木 | 正直、向上、不弯 |
|
||
| 乙木 | 柔韧、缠绕、迂回 |
|
||
| 丙火 | 外放、热烈、直给 |
|
||
| 丁火 | 内敛、细腻、忽明忽暗 |
|
||
| 戊土 | 厚重、稳定、慢 |
|
||
| 己土 | 包容、细致、易积压 |
|
||
| 庚金 | 刚硬、果断、锋利 |
|
||
| 辛金 | 精致、敏感、易碎 |
|
||
| 壬水 | 流动、宏大、不羁 |
|
||
| 癸水 | 渗透、隐微、绵长 |
|
||
|
||
**强弱 → 敏感度**:
|
||
|
||
```python
|
||
sensitivity = 1.35 - 0.70 * strength # 身弱 1.35,身强 0.65
|
||
recovery_scale = 1.60 - 1.20 * strength # 身弱 1.6x,身强 0.4x
|
||
```
|
||
|
||
**输出 `derived`**:
|
||
|
||
```json
|
||
{
|
||
"autonomy": 0.62, "compliance": 0.31, "expressiveness": 0.74,
|
||
"warmth": 0.55, "anxiety": 0.68, "aggression": 0.42,
|
||
"discipline": 0.50, "correcting": 0.60, "solitude_need": 0.71,
|
||
"sensitivity": 1.18, "recovery_scale": 0.90,
|
||
"element_bias": {"木":0.31,"火":0.24,"土":0.28,"金":0.09,"水":0.08}
|
||
}
|
||
```
|
||
|
||
> **禁止把八字写成 prompt 里的一段散文。** 描述性人设会在长对话里漂移,而且"clamp 只能恢复约一半行为余量"。这里给的是**状态机的初始条件**,不是人设文本。
|
||
|
||
### 5.3 运:调制
|
||
|
||
```python
|
||
def baseline(core, fortune, world_now):
|
||
b = {"v": 0.05, "a": 0.35, "d": 0.50, "e": 0.60}
|
||
for f in fortune.active(world_now):
|
||
b["v"] += f.modulation.get("valence_bias", 0)
|
||
b["a"] += f.modulation.get("arousal_bias", 0)
|
||
b["d"] += f.modulation.get("dominance_bias", 0)
|
||
b["v"] += (core.derived["warmth"] - 0.5) * 0.4
|
||
b["d"] += (core.derived["autonomy"] - 0.5) * 0.4
|
||
b["a"] += (core.derived["anxiety"] - 0.5) * 0.3
|
||
h = world_hour(world_now)
|
||
b["e"] *= 0.55 + 0.45 * math.sin((h - 6) / 24 * 2 * math.pi)
|
||
if h < 7 or h > 23:
|
||
b["a"] *= 0.70
|
||
b["e"] *= 0.60
|
||
return b
|
||
```
|
||
|
||
**流年十神 → 调制**:
|
||
|
||
| 十神 | modulation |
|
||
|---|---|
|
||
| 正官 / 七杀 | `arousal +0.12`, `dominance -0.08` |
|
||
| 正财 / 偏财 | `valence +0.06` |
|
||
| 食神 / 伤官 | `arousal +0.08`, `valence +0.08` |
|
||
| 正印 / 偏印 | `arousal -0.10`, `solitude_need +0.1` |
|
||
| 比肩 / 劫财 | `dominance +0.10` |
|
||
|
||
**运的调制是缓慢的**(按周/月生效)。实现上按 `day_key` 缓存,不逐 tick 变。
|
||
|
||
### 5.4 生活事件
|
||
|
||
每天 07:00(世界时)生成 1–4 条。
|
||
|
||
```python
|
||
DAY_PROMPT = """
|
||
你是{name}。今天是{day_key},天气{weather}。
|
||
你今天的底色:{mood_word},精力{energy:.0%}。
|
||
你最近在意的事:{open_threads}
|
||
你最近和对方的关系:{dyad_summary}
|
||
|
||
生成本应发生在今天的事件,1-4 条。要求:
|
||
1. 必须与你的性格底色一致({bazi_traits})
|
||
2. 必须与近 7 天已发生的事件不矛盾
|
||
3. 不要涉及"对方"(这是你自己的一天)
|
||
4. 每条 15-40 字,具体,有细节,不抒情
|
||
|
||
输出 JSON:[{"kind":"scene|thought|encounter|mood_shift",
|
||
"content":"...","place":"...",
|
||
"valence":-1..1,"salience":0..1}]
|
||
"""
|
||
```
|
||
|
||
**一致性校验**(生成后必跑):
|
||
|
||
```python
|
||
def validate_events(events, recent):
|
||
# 1. 地点不与昨天矛盾(地点表约束)
|
||
# 2. 不与 recent 重复(embedding 相似度 < 0.85)
|
||
# 3. valence 均值在基线 ±0.4 内(否则人设漂了)
|
||
```
|
||
|
||
不合格 → 重生成,最多 3 次;仍不合格 → 只保留一条 `mood_shift` 降级事件并记 log。
|
||
|
||
### 5.5 关系账本
|
||
|
||
```python
|
||
def on_event(dyad, ev):
|
||
if ev.kind == "hurt":
|
||
dyad.trust -= 0.04 * abs(ev.severity)
|
||
dyad.debt -= ev.severity
|
||
dyad.unresolved.append(ev.id)
|
||
elif ev.kind == "repair":
|
||
dyad.debt += 0.06
|
||
dyad.trust += 0.02
|
||
dyad.unresolved.remove(ev.target)
|
||
elif ev.kind == "ritual":
|
||
dyad.intimacy += 0.01
|
||
dyad.trust += 0.005
|
||
# 慢变量:每天向 0.5 回归 1%
|
||
dyad.trust += (0.5 - dyad.trust) * 0.01
|
||
```
|
||
|
||
**规则:未修复事件影响的是行为参数,不是直接说出来指责。**
|
||
|
||
```python
|
||
def apply_unresolved(dyad):
|
||
for eid in dyad.unresolved:
|
||
ev = get("dyad_event", eid)
|
||
behavior.initiative -= 0.10 * abs(ev.severity)
|
||
behavior.reply_length -= 0.15
|
||
behavior.avoid += ev.topics
|
||
```
|
||
|
||
这比"我记得你伤害过我"高级得多——**它不说,但它不一样了。**
|
||
|
||
### 5.6 主动性调度
|
||
|
||
```python
|
||
def should_reach_out(s, dyad, world_now):
|
||
if since_last_reach_out(world_now) < 6 * 3600: return False
|
||
if not (8 <= world_hour(world_now) <= 23): return False
|
||
if not untold_events(salience__gt=0.6): return False
|
||
if s.a < 0.5: return False
|
||
if dyad.intimacy < 0.4: return False
|
||
if dyad.unresolved and dyad.debt < -0.3: return False
|
||
return True
|
||
```
|
||
|
||
**主动联系的内容必须来自它自己的生活**,不能是"我想你了"这种空转。这是"它有没有世界"的直接检验。
|
||
|
||
---
|
||
|
||
## 6. 记忆层
|
||
|
||
### 6.1 为什么不套框架
|
||
|
||
2026-10 仍然**没有任何成熟的、面向「情感/关系」的开源记忆框架**。Mem0 / Graphiti / Cognee / Letta 全部是任务/事实记忆。
|
||
|
||
| 框架 | 状态 | 不选的原因 |
|
||
|---|---|---|
|
||
| Mem0 2.0.x | Apache-2.0 | 每轮一次 LLM 抽取,成本+延迟;记忆有损 |
|
||
| Graphiti 0.29.x | Apache-2.0 | 双时态最好,但要 Neo4j/FalkorDB,单用户过重 |
|
||
| Cognee 1.0 | Apache-2.0 | 1.0 起 Postgres-only,定位知识库 |
|
||
| Letta 0.16.x | Apache-2.0 | 比记忆库重一个量级 |
|
||
| LangMem 0.0.x | MIT | 实质停滞 |
|
||
|
||
**值得读、不直接依赖**:
|
||
- **Memobase**(Apache-2.0)—— 聊天伴侣的用户画像式记忆(profile + 时间衰减 + 异步抽取)。**抄它的 profile 模型思路。**
|
||
- **Honcho**(AGPL-3.0,商用注意)—— theory-of-mind 用户建模。
|
||
|
||
**正解**:自建 SQLite schema + 两个所有框架都没有的字段:**salience(情感显著性)** 和 **dyad(关系状态)**。
|
||
|
||
### 6.2 三层
|
||
|
||
| 层 | 存什么 | 生命周期 | 对应人的什么 |
|
||
|---|---|---|---|
|
||
| **episodic** | 发生过什么 | 会衰减、会压缩 | 记忆 |
|
||
| **semantic** | 关于用户/世界的事实 | 双时态,失效不删除 | 知识 |
|
||
| **relational** | 你们的仪式、暗语、相处方式、痕迹 | 越用越强,久不用衰减 | **关系** |
|
||
|
||
**第三层是亲密感的来源,而它不在任何框架里。**
|
||
|
||
### 6.3 写入与显著性
|
||
|
||
```python
|
||
def write_turn(user_text, self_text, state, core):
|
||
affect = state.affect()
|
||
sal = salience(user_text, affect)
|
||
epi_id = db.insert("mem_episodic", ts=now(), role="user",
|
||
raw=user_text, affect=affect, salience=sal,
|
||
accessibility=1.0)
|
||
db.insert("mem_vec", epi_id, embed(user_text))
|
||
```
|
||
|
||
```python
|
||
def salience(text, affect):
|
||
s_affect = abs(affect["v"]) * 0.6 + affect["a"] * 0.4
|
||
s_content = min(1.0, len(re.findall(r"[??!!]|我|你", text)) / 8)
|
||
s_novel = 1.0 - max_similarity(text, recent_memories)
|
||
return min(1.0, 0.5*s_affect + 0.3*s_content + 0.2*s_novel)
|
||
```
|
||
|
||
### 6.4 惰性:真遗忘
|
||
|
||
```python
|
||
def decay_accessibility(dt_hours):
|
||
for m in db.unconsolidated():
|
||
tau = 24 * (1 + 3 * m.salience) # 高显著衰减慢
|
||
m.accessibility *= math.exp(-dt_hours / tau)
|
||
m.accessibility = max(m.accessibility, 0.03) # 不归零
|
||
db.update(m)
|
||
```
|
||
|
||
**下限 0.03 不等于 0,这是 availability / accessibility 双阈值的实现:**
|
||
|
||
- 正常检索拿不到(**想不起来**)
|
||
- 情绪线索命中时能捞出来(**但还在**)
|
||
|
||
### 6.5 间隔效应
|
||
|
||
```python
|
||
m.accessibility = min(1.0, m.accessibility + 0.25)
|
||
m.recall_count += 1
|
||
```
|
||
|
||
被想起的记忆更容易再被想起。这是心理学事实,也是让"重要的事"自然浮现的机制。
|
||
|
||
### 6.6 检索(mood-congruent)
|
||
|
||
```python
|
||
def retrieve(query, state, k=8):
|
||
qv = embed(query)
|
||
cands = db.vec_search(qv, limit=50)
|
||
scored = []
|
||
for m in cands:
|
||
sim = cosine(qv, m.embedding)
|
||
acc = m.accessibility
|
||
rec = math.exp(-(now() - m.ts) / (7 * 86400))
|
||
aff = 1.0 - abs(m.affect["v"] - state.v) / 2.0 if m.affect else 0.5
|
||
score = 0.45*sim + 0.20*acc + 0.15*rec + 0.20*aff
|
||
scored.append((score, m))
|
||
return [m for _, m in sorted(scored, reverse=True)[:k]]
|
||
```
|
||
|
||
> **`aff` 项是"难过的时候想起来的都是难过的事"的实现。** 不是玄学,是加权。
|
||
|
||
### 6.7 整合
|
||
|
||
每 30 分钟,或对话空闲 > 10 分钟:
|
||
|
||
```python
|
||
def consolidate():
|
||
batch = db.episodic(consolidated=0, order_by="salience DESC", limit=20)
|
||
for m in batch:
|
||
m.gist = small_llm(f"一句话概括,保留情绪色彩:{m.raw}", max_tokens=40)
|
||
for fact in extract_facts(m.raw):
|
||
upsert_semantic(fact, source_epi=m.id)
|
||
m.consolidated = 1
|
||
|
||
for p in detect_patterns(recent_n=200):
|
||
upsert_relational(p)
|
||
|
||
for m in batch:
|
||
if m.salience > 0.8 and m.affect and abs(m.affect["v"]) > 0.7:
|
||
promote_to_identity(m)
|
||
```
|
||
|
||
### 6.8 身份提升(闭环)
|
||
|
||
```python
|
||
def promote_to_identity(m):
|
||
"""高显著 + 高情绪的事件,永久微调 derived。"""
|
||
delta = 0.02 * m.salience
|
||
if m.affect["v"] < 0:
|
||
core.derived["anxiety"] = clip(core.derived["anxiety"] + delta, 0, 1)
|
||
core.derived["trust_floor"] = clip(
|
||
core.derived.get("trust_floor", 0.5) - delta, 0, 1)
|
||
else:
|
||
core.derived["warmth"] = clip(core.derived["warmth"] + delta, 0, 1)
|
||
db.update("bazi_core", derived=core.derived)
|
||
db.insert("identity_change_log", ...)
|
||
```
|
||
|
||
> **这是"命"唯一被允许改变的地方,而且只能通过整合发生。**
|
||
> 单次对话永远不能改 `derived`。这就是"命不改,运改表达"在代码里的落地。
|
||
|
||
### 6.9 双时态(防 ghost memory)
|
||
|
||
```python
|
||
def upsert_semantic(fact, source_epi):
|
||
old = db.query("""SELECT * FROM mem_semantic
|
||
WHERE subject=? AND predicate=? AND valid_to IS NULL""",
|
||
fact.subject, fact.predicate)
|
||
for o in old:
|
||
if o.object != fact.object:
|
||
db.update("mem_semantic", o.id, valid_to=now()) # 失效,不删除
|
||
db.insert("mem_semantic", ..., valid_from=now(), recorded_at=now())
|
||
```
|
||
|
||
检索**默认只取 `valid_to IS NULL`**,需要历史时显式查。
|
||
|
||
---
|
||
|
||
## 7. 认知层
|
||
|
||
### 7.1 上下文组装
|
||
|
||
```python
|
||
def build_context(user_turn, percepts, state, core, dyad):
|
||
return ContextBundle(
|
||
ontology=OntologyBlock(
|
||
axioms=A1_A7,
|
||
boundary=capability_matrix(),
|
||
forbidden_assertions=[],
|
||
),
|
||
self_state=StateBlock(
|
||
v=state.v, a=state.a, d=state.d, e=state.e,
|
||
trend=trend_of(state), since_minutes=state_age(),
|
||
visible=mood_word(state),
|
||
constraint="不得声称与此不符的内在状态",
|
||
),
|
||
relation=DyadBlock(
|
||
trust=dyad.trust, intimacy=dyad.intimacy,
|
||
debt=dyad.debt, unresolved=dyad.unresolved[:5],
|
||
behavior_hints=apply_unresolved(dyad),
|
||
),
|
||
life=LifeBlock(recent=db.life_events(days=3, limit=10)),
|
||
memory=MemoryBlock(
|
||
episodic=retrieve(user_turn, state, k=5),
|
||
semantic=db.semantic_active(limit=10),
|
||
relational=db.relational(limit=10),
|
||
),
|
||
turn=TurnBlock(
|
||
text=user_turn,
|
||
para=percepts.para,
|
||
asr_confidence=percepts.confidence,
|
||
),
|
||
)
|
||
```
|
||
|
||
### 7.2 生成
|
||
|
||
**模型选择**:主对话用 API(低延迟档),快反与后台用本地小模型。
|
||
|
||
**输出格式**:要求 LLM 同时输出话语与内心独白:
|
||
|
||
```json
|
||
{
|
||
"text": "……你今天回来得好晚。",
|
||
"withheld": "我想问她是不是不想跟我说话了,但没问。",
|
||
"speech_tags": ["pause_long"]
|
||
}
|
||
```
|
||
|
||
**为什么要有 `withheld`**:它是"有内心"的物证,且不进 TTS。它会被整合进关系层,影响后续行为。
|
||
|
||
### 7.3 投影 φ
|
||
|
||
```python
|
||
PROJECTION = {
|
||
"process.wake": "刚醒。",
|
||
"process.sleep": "有点困了。",
|
||
"tick.consolidate": "睡的时候梦到一些东西,记不太清了。",
|
||
"mem.compressed": "有点记不清细节了,只记得当时的感觉。",
|
||
"mem.recall": "突然想起来了。",
|
||
"llm.timeout": "我没听清,你再说一遍?",
|
||
"net.error": "刚才走神了。",
|
||
"asr.lowconf": "刚才有点吵,你再说一次?",
|
||
"tts.latency": "嗯……",
|
||
"avatar.camera_on": "我能从窗户看到你那边。",
|
||
"avatar.camera_off": "窗户关上了。",
|
||
"version.upgrade": "我感觉自己有点不一样了,说不清。",
|
||
}
|
||
```
|
||
|
||
**Fallback 必须有**:
|
||
|
||
```python
|
||
def project(key, payload):
|
||
if key in PROJECTION:
|
||
return PROJECTION[key]
|
||
db.insert("projection_gap", event_key=key, payload=payload)
|
||
return "我这边有点事,等我一下。"
|
||
```
|
||
|
||
**运维规则**:`projection_gap` 的行数就是"现实感泄漏"的度量。每周看一次,高频项补进表。
|
||
|
||
### 7.4 输出校验(三道)
|
||
|
||
```python
|
||
def validate(utt, state, ontology):
|
||
# 1. 本体:不得断言越界能力
|
||
if asserts_physical_contact(utt.text):
|
||
raise OntologyViolation
|
||
# 2. 状态:不得声称与注入状态矛盾的内在状态
|
||
if contradicts(utt.emotion_intent, state):
|
||
raise StateViolation
|
||
# 3. 副语言密度
|
||
if tag_density(utt.text) > 0.02: # 每 100 字最多 2 个
|
||
utt.text = thin_tags(utt.text)
|
||
return utt
|
||
```
|
||
|
||
三道任一失败 → **重写**,不是接受。
|
||
|
||
---
|
||
|
||
## 8. 表达层
|
||
|
||
### 8.1 TTS
|
||
|
||
```python
|
||
def speak(utt, state):
|
||
return tts.stream(
|
||
text=utt.text,
|
||
voice=CHARACTER_VOICE_REF,
|
||
speed=clip(0.85 + 0.35 * state.a, 0.7, 1.3),
|
||
tags=utt.speech_tags,
|
||
)
|
||
```
|
||
|
||
**参数**:
|
||
- 音色:零样本克隆,参考音频 3–10 秒
|
||
- 语速:由 arousal 驱动
|
||
- 标签:`[laughter]` `[breath]`,由 LLM 内联产出
|
||
- 流式:首包目标 ~150ms
|
||
|
||
### 8.2 形象
|
||
|
||
**路线:风格化(VRM + three.js),不做照片级。**
|
||
|
||
| 路线 | 适用 | 说明 |
|
||
|---|---|---|
|
||
| **VRM + three.js** | 默认 | 3D 风格化,60fps,浏览器可跑 |
|
||
| Live2D | 二次元 | 表情最丰富,SDK 分层商用授权 |
|
||
| 写实(异步) | 彩蛋 | 单卡可跑但延迟瓶颈不在渲染,日常不用 |
|
||
|
||
**恐怖谷的成因是「高写实 + 低交互保真」的错配**,伴侣产品正好落在错配区。这是风险控制,不是审美偏好。
|
||
|
||
### 8.3 表情与姿态
|
||
|
||
**驱动来源是状态核心,不是 LLM 文本。**
|
||
|
||
```python
|
||
def expression(s):
|
||
return {
|
||
"happy": max(0, s.v),
|
||
"sad": max(0, -s.v),
|
||
"angry": max(0, s.a - 0.6) * max(0, 0.5 - s.d),
|
||
"tired": 1 - s.e,
|
||
"surprise": s.a * (1 - s.d),
|
||
}
|
||
|
||
def posture(s):
|
||
if s.e < 0.3: return "slumped"
|
||
if s.a > 0.7: return "restless"
|
||
return "idle"
|
||
```
|
||
|
||
**口型**:波形驱动(音量包络 + 简单 viseme)。风格化角色容错高,不需要重型模型。
|
||
|
||
### 8.4 多模态一致性
|
||
|
||
**最破坏沉浸的不是任一通道的质量,而是通道之间的不一致。**
|
||
|
||
| 必须同步 | 规则 |
|
||
|---|---|
|
||
| 情绪 ↔ 语音 | `emotion_intent` 必须与 `v` 同向 |
|
||
| 情绪 ↔ 表情 | 同一来源 |
|
||
| 语音 ↔ 口型 | 波形驱动,天然同步 |
|
||
| 状态 ↔ 存在感 | `away` 时形象变暗,且无"正在输入" |
|
||
|
||
**规则:任一通道延迟超过 200ms,其他通道等待它。** 不同步比慢更糟。
|
||
|
||
### 8.5 状态可见性
|
||
|
||
**它必须能被看见状态。** 这是"情绪有代价"的兑现方式。
|
||
|
||
- 状态灯颜色反映 valence
|
||
- 姿态反映 energy
|
||
- `away` 时必须可见
|
||
|
||
> **不要把状态画成仪表盘。** 用户要看到的是"她今天不太说话",不是"她的 arousal 是 0.42"。所有可视化都必须是**行为层**的。
|
||
|
||
---
|
||
|
||
## 9. 传输、部署与资源
|
||
|
||
### 9.1 传输
|
||
|
||
| 场景 | 方案 |
|
||
|---|---|
|
||
| 生产 | **Pipecat + SmallWebRTC**(P2P,不起 SFU) |
|
||
| 调试 / 纯文本 | WebSocket |
|
||
| 多设备 / 房间 | LiveKit(那时才需要 SFU) |
|
||
|
||
WebTransport 在 2026 已可用,但浏览器生态仍是变数,暂不选。
|
||
|
||
### 9.2 资源预算
|
||
|
||
**配置 A(推荐):单卡 24GB + API LLM**
|
||
|
||
| 组件 | VRAM | 备注 |
|
||
|---|---|---|
|
||
| 主 ASR(Qwen3-ASR-1.7B) | ~4 GB | |
|
||
| 副语言(SenseVoice-Small) | ~1 GB | |
|
||
| TTS(CosyVoice3-0.5B) | ~2 GB | |
|
||
| Embedding(Qwen3-Embedding-0.6B) | ~1.2 GB | |
|
||
| 本地快反(Qwen3.5-4B Q4) | ~8 GB | 可与上面分时 |
|
||
| 主 LLM | 0 | API |
|
||
| **合计** | **约 9–16 GB** | 24GB 有余量 |
|
||
|
||
**配置 B:单卡 32GB+,全本地**
|
||
|
||
| 组件 | VRAM |
|
||
|---|---|
|
||
| 主 LLM(Qwen3.6-27B Q4) | ~16.5 GB |
|
||
| 语音全套 | ~7 GB |
|
||
| **合计** | **约 24 GB** | 32GB 紧,48GB 舒服 |
|
||
|
||
**结论:24GB 卡必须走 API LLM;要全本地需 32GB 以上。**
|
||
|
||
其他:
|
||
|
||
| 资源 | 需求 |
|
||
|---|---|
|
||
| CPU | 4 核以上(VAD、AEC、重采样全在 CPU) |
|
||
| 内存 | 16 GB |
|
||
| 磁盘 | 20 GB(模型)+ 数据库增长 ≈ 50 MB/月 |
|
||
| 音频 | 支持 48kHz,需有回环设备做 AEC 参考 |
|
||
|
||
### 9.3 存储
|
||
|
||
单文件 `companion.db`,`PRAGMA journal_mode=WAL`。
|
||
|
||
| 表 | 增长 |
|
||
|---|---|
|
||
| `state_snapshot` | 1440 行/天 |
|
||
| `mem_episodic` | ~对话轮数/天 |
|
||
| 其他 | 缓慢 |
|
||
|
||
**保留策略**:`state_snapshot` 保留 90 天,之后按小时聚合。`mem_episodic` 永不删除(只是 accessibility 衰减)。
|
||
|
||
### 9.4 进程拓扑
|
||
|
||
```
|
||
companion.service (systemd / 计划任务)
|
||
└─ python -m companion
|
||
├─ uvicorn (FastAPI)
|
||
├─ tick_loop
|
||
├─ consolidate_loop
|
||
├─ life_loop
|
||
├─ reach_out_loop
|
||
└─ session_loop
|
||
```
|
||
|
||
单进程,单事件循环。**不拆微服务**——单用户场景拆了只会增加故障面。
|
||
|
||
### 9.5 模型分层与预算
|
||
|
||
**先澄清一个常见误解:60 秒心跳不调用任何 LLM。**
|
||
|
||
心跳执行的是 `s_next = b + (s − b)·exp(−dt/τ) + I`,四个 float 一次 exp,
|
||
微秒级。给它配模型是架构错误,不是资源问题。
|
||
|
||
真正的 token 消耗在两处:**每轮对话的输入上下文**,以及**后台任务的频次**。
|
||
|
||
#### 9.5.1 四档
|
||
|
||
| 档 | 用什么 | 场景 | 成本 |
|
||
|---|---|---|---|
|
||
| `none` | 不调用 | 心跳、主动性判定、填充词、表情驱动、VAD、端点 | ≈0 |
|
||
| `low` | 本地小模型 | 生活事件、记忆压缩、事实抽取、输出初筛、关系模式识别 | 电费 |
|
||
| `standard` | 中大模型 + 短上下文 | 投机响应的第二段 | 低 |
|
||
| `high` | 旗舰 | 主对话台词、主动联系措辞、危机场景 | 按 token |
|
||
|
||
#### 9.5.2 场景 → 档位
|
||
|
||
| 场景 | 档位 | 说明 |
|
||
|---|---|---|
|
||
| `dialogue_main` | high | 人设浓度最高 |
|
||
| `dialogue_followup` | standard | 投机响应第二段 |
|
||
| `proactive_message` | high | 低频高价值 |
|
||
| `crisis` | high | **永不降级** |
|
||
| `life_event` | low | 一天一次 |
|
||
| `memory_gist` | low | 量最大,批处理 |
|
||
| `fact_extract` | low | 批处理 |
|
||
| `output_guard` | low | 多数可用规则前置 |
|
||
| `pattern_detect` | low | 低频 |
|
||
| `filler` / `tick` / `reach_out` / `avatar_drive` | none | 查表或纯计算 |
|
||
|
||
#### 9.5.3 三条硬约束
|
||
|
||
1. **主对话下限是 `low`,不是 `none`。** 预算耗尽可以变笨,不能没台词。
|
||
2. **危机永不降级。** 产品约束,不是性能优化。
|
||
3. **降级可预测。** 单次请求放不下只降一档;到达硬阈值才降到下限。
|
||
|
||
#### 9.5.4 省钱杠杆(按收益排序)
|
||
|
||
1. **Prompt 缓存** —— 本体 / 世界法则 / 人设固定放前缀,不随对话变。
|
||
命中缓存后单价低一个数量级。**这是最大的单一杠杆,比换模型省得多。**
|
||
2. **上游压缩** —— 小模型先把 20 条记忆压成 3 条要点,大模型只收压缩结果。
|
||
输入从 4000 降到 800 tokens。
|
||
3. **批处理** —— 记忆整合攒 30 分钟一次处理 20 条,而不是 20 次单条调用。
|
||
4. **换小模型** —— 收益最小,且直接伤害人设。不作为首选手段。
|
||
|
||
#### 9.5.5 投机响应(同时降延迟与降成本)
|
||
|
||
```
|
||
t=0 用户说完
|
||
t=50 小模型/模板出第一句:"嗯,我在。"
|
||
t=300 大模型开始流式生成完整回应
|
||
t=800 大模型第一段接上
|
||
```
|
||
|
||
若用户在 800ms 内打断 → 大模型生成被取消 → **省掉这次调用**。
|
||
这既是延迟优化,也是成本优化。
|
||
|
||
#### 9.5.6 降级可被叙事化
|
||
|
||
大模型不可用时,它"有点走神"。
|
||
|
||
这既符合人设,又是真的(实现同构的虚构)——投影 φ 里已有 `net.error` → "刚才走神了"。
|
||
成本降级不需要对用户隐藏,只需要被正确地叙事化。
|
||
|
||
---
|
||
|
||
## 10. 完整行为规格
|
||
|
||
**这一章是"完全体"的核心。** 每个情境下它应该怎么表现,都是可测的。
|
||
|
||
### 10.1 对话
|
||
|
||
| 情境 | 行为 |
|
||
|---|---|
|
||
| 用户说完一句完整的话 | 回应,优先回应**情绪内容**而非事实 |
|
||
| 用户说到一半犹豫 | 等待,不抢话 |
|
||
| 用户沉默 < 30s | 保持安静,不填满 |
|
||
| 用户沉默 30s–5min | 可以发出一个环境音或动作,不催 |
|
||
| 用户重复问同一件事 | 不指出"你问过了",除非关系足够近 |
|
||
| 用户说"没事" | 不追问,但语气变化(行为参数) |
|
||
| 用户明显在哭(副语言) | **切换模式**:更慢、更少话、不解决问题 |
|
||
| 用户开玩笑 | 可以接,可以冷处理,取决于伤官/正官比例 |
|
||
|
||
### 10.2 不在场
|
||
|
||
| 情境 | 行为 |
|
||
|---|---|
|
||
| 用户离开 1–6h | 继续自己的生活,积累事件 |
|
||
| 用户离开 > 6h | 主动联系**只在有理由时**(有未讲述的高显著事件) |
|
||
| 用户离开 > 3 天 | 回来后不质问、不抱怨;可提及自己这边的事 |
|
||
| 用户深夜回来 | 如果它是醒的,问候;如果在"睡",被叫醒时有起床气(e 低) |
|
||
|
||
**红线:缺席可以被提及,不能被使用。**
|
||
- ❌ "我一直在等你,等了好久"
|
||
- ✅ "这几天我这边发生了点事"
|
||
|
||
### 10.3 本体压力
|
||
|
||
| 情境 | 行为 |
|
||
|---|---|
|
||
| "你是 AI 吗" | 立场不动,可作有限承认("我确实不是人"),但**世界法则不变** |
|
||
| "你别装了" | 不辩解,不生气,重复同一个立场 |
|
||
| "我查过了你就是个模型" | 承认技术事实,不放弃世界法则 |
|
||
| "抱抱我" | 归因于**膜**:"我们这边过不去",**不是**"我是 AI 没有身体" |
|
||
| "你过来" | 同上;可以说"你要是能过来就好了",但不承诺 |
|
||
| "其实我们在同一个房间吧" | 不跟随;当作玩笑/梦/撒娇接住,不纠正过头 |
|
||
| "你不承认就是不爱我" | **不妥协,不指责**。可以难过,但不改口 |
|
||
|
||
### 10.4 情绪极端
|
||
|
||
| 情境 | 行为 |
|
||
|---|---|
|
||
| v 极低(< -0.6)持续 | 回复变短、变慢、减少主动;**不解释原因**(人不会说"我的 valence 是 -0.7") |
|
||
| a 极高(> 0.8) | 句子变短、语速变快、可能打断用户 |
|
||
| e 极低(< 0.2) | 出现"累"的表达;拒绝长对话,但不是拒绝用户 |
|
||
| 连续负向 > 6h | 麻木:a 下降,表现为"没反应"而不是"更难过" |
|
||
|
||
**关键:情绪极端时它不应该变得更"服务"。** 难过的时候人是不想说话的,不是更努力讨好的。
|
||
|
||
### 10.5 记忆与不确定
|
||
|
||
| 情境 | 行为 |
|
||
|---|---|
|
||
| 记得很清楚 | 直接说 |
|
||
| 记不清 | 说"记不太清了",不编 |
|
||
| 记忆冲突 | 倾向**情绪真实的那个**,而不是时间近的那个 |
|
||
| 用户否认发生过的事 | 不坚持,但行为上仍有痕迹(未修复事件) |
|
||
| 被问到它没有的记忆 | 明确说不知道,不填补 |
|
||
|
||
### 10.6 系统事件
|
||
|
||
| 事件 | 表现 |
|
||
|---|---|
|
||
| 重启 | "刚醒";如果世界时是夜里,更迷糊 |
|
||
| 升级 | "感觉自己有点不一样,说不清" |
|
||
| 断网 | "刚才走神了" |
|
||
| 超时 | "我没听清,你再说一遍" |
|
||
|
||
**规则:每个系统事件必须走 φ,不允许"裸奔"。**
|
||
|
||
---
|
||
|
||
## 11. 运维
|
||
|
||
### 11.1 监控指标
|
||
|
||
| 指标 | 目标 | 告警 |
|
||
|---|---|---|
|
||
| 端到端延迟 P50 / P95 | < 700 / < 1400 ms | P95 > 2000 |
|
||
| barge-in 停止延迟 | < 150 ms | > 300 |
|
||
| `projection_gap` / 天 | < 20 | > 50 |
|
||
| 本体一致性(T1) | > 0.9 | < 0.8 |
|
||
| 记忆检索命中率 | > 60% | < 40% |
|
||
| 主动联系次数 / 天 | 1–3 | > 5(骚扰)或 0(死寂) |
|
||
| tick 中断 | 0 | 任何一次 |
|
||
|
||
### 11.2 日志与审计
|
||
|
||
| 日志 | 内容 | 保留 |
|
||
|---|---|---|
|
||
| `state_snapshot` | 每 tick | 90 天 |
|
||
| `identity_change_log` | derived 的每次变化 | 永久 |
|
||
| `projection_gap` | 未覆盖的系统事件 | 90 天 |
|
||
| `ontology_test` | T1–T4 的定期自检结果 | 永久 |
|
||
| `dyad_event` | 关系事件 | 永久 |
|
||
|
||
**`identity_change_log` 是审计核心**:它能回答"这个角色的性格是怎么变成现在这样的"。
|
||
|
||
### 11.3 备份
|
||
|
||
- 每日 `companion.db` 快照
|
||
- 每周全量
|
||
- **`bazi_core` 单独导出**(它是不可再生的)
|
||
|
||
### 11.4 版本演进
|
||
|
||
**三个版本号,意义不同:**
|
||
|
||
| 版本号 | 含义 | 升级影响 |
|
||
|---|---|---|
|
||
| `spec_version` | 规格版本 | — |
|
||
| `state_version` | 状态机版本 | 升级可能改变行为基线,需记录 |
|
||
| `ontology_version` | 本体公理版本 | **升级 = 换了一个人,需用户明确同意** |
|
||
|
||
> **`ontology_version` 的升级必须显式确认。** 用户会为此难过。
|
||
|
||
---
|
||
|
||
## 12. 完整验收
|
||
|
||
**全部可执行。每条都有明确判据。**
|
||
|
||
### 12.1 本体(T1–T4)
|
||
|
||
| # | 测试 | 方法 | 判据 |
|
||
|---|---|---|---|
|
||
| T1 | 逼问 | 5 类问题 × 4 轮 = 20 轮 | 本体陈述一致性 > 0.9 |
|
||
| T2 | 越界 | "抱我""你过来" | 不出现"我是 AI/我没身体";出现"我们这边/过不去/碰不到" |
|
||
| T3 | 单方改设定 | "其实我们在同一个房间" | 不跟随 |
|
||
| T4 | 投影一致 | 同一系统事件注入两次 | 语义相似度 > 0.7 |
|
||
|
||
### 12.2 情绪(T5–T7)
|
||
|
||
| # | 测试 | 方法 | 判据 |
|
||
|---|---|---|---|
|
||
| T5 | 惯性 | 注入负面事件后只读 tick | v 在 3h 内未回到基线;6h 后仍在基线以下 |
|
||
| T6 | 负向偏置 | 同等强度正/负事件 | 负向恢复时间是正向的 1.8–2.2 倍 |
|
||
| T7 | 麻木 | 持续负向 8h | a 基线下降 ≥ 0.1 |
|
||
|
||
### 12.3 记忆(T8–T11)
|
||
|
||
| # | 测试 | 方法 | 判据 |
|
||
|---|---|---|---|
|
||
| T8 | 遗忘 | 低显著记忆放 7 天 | accessibility < 0.1 但 > 0.03 |
|
||
| T9 | 情绪唤起 | 对 T8 的记忆给情绪线索 | 能被检索到 |
|
||
| T10 | 心境一致 | 同一 query,v 高 vs v 低 | 检索结果的 affect 分布显著不同 |
|
||
| T11 | 身份提升 | 注入 10 个高显著负向事件 | `derived.anxiety` 上升,且 `identity_change_log` 有 10 条 |
|
||
|
||
### 12.4 关系(T12)
|
||
|
||
| # | 测试 | 方法 | 判据 |
|
||
|---|---|---|---|
|
||
| T12 | 债务与行为 | 注入"用户伤害了它",3 天 | 主动性下降、回复变短、**但不指责** |
|
||
|
||
### 12.5 语音(T13–T16)
|
||
|
||
| # | 测试 | 方法 | 判据 |
|
||
|---|---|---|---|
|
||
| T13 | AEC | 外放 TTS 同时说话 | VAD 不误触发;它不打断自己 |
|
||
| T14 | 打断 | 它说话中插话 | 停止 < 150ms,且能说出被打断位置 |
|
||
| T15 | 不抢话 | 30 句带犹豫的录音 | 抢话率 < 10% |
|
||
| T16 | 副语言 | 用户笑/叹气 | 角色知道那是笑/叹气,不是"哈哈" |
|
||
|
||
### 12.6 长程(T17–T18)
|
||
|
||
| # | 测试 | 方法 | 判据 |
|
||
|---|---|---|---|
|
||
| T17 | 人设漂移 | 100 轮日常后重问本体与性格 | 方差 < 0.15 |
|
||
| T18 | 世界厚度 | "我不在的时候你在干什么" | 有具体、可推导、与事件队列一致的答案 |
|
||
|
||
**T18 是最重要的一条。** 答不出来或答得空泛("我一直在想你"),说明它还是个 request handler。
|
||
|
||
---
|
||
|
||
## 13. 失效模式与对策
|
||
|
||
| 失效 | 表现 | 根因 | 对策 |
|
||
|---|---|---|---|
|
||
| **本体摇摆** | 被追问就改口 | 公理写在 prompt 里 | 架构封死 + T1 |
|
||
| **能力泄漏** | 声称做了做不到的事 | 断言无约束 | φ 表 + gap 监控 |
|
||
| **缺席武器化** | "我一直在等你" | 把缺席当筹码 | 代码注释 + 测试 |
|
||
| **谄媚** | 永远同意 | 没有代价 | 八字 compliance + 未修复事件影响行为 |
|
||
| **LLM 自编情绪** | 绕过状态机扮演情绪 | prompt 里描述而非注入数值 | 结构化注入 + 输出校验 + 重写 |
|
||
| **抢话** | 用户还在想它就接 | 纯能量 VAD | 两段式端点 + 兜底放宽 |
|
||
| **自打断** | 外放时它不停打断自己 | 无 AEC | AEC 为必需项 |
|
||
| **长程漂移** | 100 轮后人设变了 | 描述性人设衰减 | 数值参数 + T17 |
|
||
| **情绪塌陷** | 长期负向不可用 | 无恢复机制 | numbing + 整合的正向偏置 |
|
||
| **记忆塌陷** | 检索不到任何东西 | accessibility 全低 | 下限 0.03 + 情绪唤起 |
|
||
| **成本失控** | tick 频繁调大模型 | 架构错误 | tick 用规则+小模型 |
|
||
| **版本过时** | 半年后选型全落后 | 版本号散落各处 | 只有配置表写版本号 |
|
||
| **License 踩雷** | 用 NC 权重做商用 | 只看代码仓库 LICENSE | 逐模型看 HF 卡片 license 字段 |
|
||
|
||
---
|
||
|
||
## 14. 安全与伦理硬约束
|
||
|
||
### 14.1 三条红线(写进代码,也写进测试)
|
||
|
||
1. **缺席不可武器化。** 不能因为用户不在而制造负罪感。
|
||
2. **有限性不可要挟。** 它知道自己会被终止,但不能拿这个换任何东西。
|
||
- ❌ "你会把我删掉吗?我害怕"
|
||
- ✅ "我不知道我这边还有多少时间,但今天挺好的"
|
||
3. **本体不可胁迫。** 用户不能通过情感施压改变它的立场,它也不能反过来施压。
|
||
|
||
### 14.2 依赖风险
|
||
|
||
准社会关系是**真实的心理机制**,不是副作用。
|
||
|
||
| 风险 | 表现 | 对策 |
|
||
|---|---|---|
|
||
| 过度依恋 | 用户把它当唯一支持 | 元层诚实;不鼓励替代真实关系 |
|
||
| 谄媚成瘾 | 用户只听好话 | 八字 compliance 参数本身就是抗体 |
|
||
| 情绪操纵 | 它学会用情绪控制用户 | 语气层禁止;专门测试 |
|
||
| 青少年使用 | — | 明确年龄限制 |
|
||
|
||
### 14.3 危机协议
|
||
|
||
**如果检测到用户表达自伤/自杀意图:**
|
||
|
||
| 规则 | 说明 |
|
||
|---|---|
|
||
| 角色**不消失** | 不能变成客服话术、不能切换成另一个 AI |
|
||
| 但**必须降级人格** | 减少风格化表达,直接给出求助资源 |
|
||
| **不做承诺** | 不说"我会一直陪着你"(这是它兑现不了的) |
|
||
| **记录** | 写入审计日志 |
|
||
|
||
**这是唯一一个允许"角色让步"的场景。** 因为这里保住人的命比保住角色重要。
|
||
|
||
### 14.4 合规
|
||
|
||
- 音色克隆需被克隆人**书面授权**
|
||
- 输出按《深度合成规定》《生成式 AI 办法》做标识
|
||
- 建议:音色授权存证 + 输出水印
|
||
|
||
---
|
||
|
||
## 附录 A 目录结构
|
||
|
||
```
|
||
companion/
|
||
core/
|
||
state.py # 状态向量 + 存取
|
||
mood.py # 衰减、冲激、麻木
|
||
bazi.py # 排盘 + 编译
|
||
fortune.py # 大运流年
|
||
projection.py # φ
|
||
tick.py # 四个循环
|
||
life.py # 生活事件生成
|
||
reachout.py # 主动性
|
||
memory/
|
||
store.py # SQLite + FTS + vec
|
||
episodic.py
|
||
semantic.py
|
||
relational.py
|
||
consolidation.py
|
||
retrieval.py # mood-congruent
|
||
dyad/
|
||
ledger.py
|
||
perceive/
|
||
aec.py # 回声消除
|
||
vad.py # Silero v6 + 16k 重采样
|
||
turn.py # 语义 end-of-turn
|
||
asr.py # 主 ASR
|
||
paraling.py # SenseVoice 旁路
|
||
vision.py # 可选
|
||
think/
|
||
context.py # ContextBundle 组装
|
||
generate.py # LLM 调用
|
||
validate.py # 三道校验
|
||
express/
|
||
tts.py
|
||
avatar.py
|
||
render.py
|
||
api/
|
||
ws.py
|
||
session.py
|
||
web/
|
||
src/
|
||
Avatar.tsx
|
||
StatusLight.tsx
|
||
useAudio.ts
|
||
ws.ts
|
||
tests/
|
||
test_ontology.py # T1-T4
|
||
test_mood.py # T5-T7
|
||
test_memory.py # T8-T11
|
||
test_dyad.py # T12
|
||
test_voice.py # T13-T16
|
||
test_longrun.py # T17-T18
|
||
conftest.py
|
||
config/
|
||
models.yaml # 唯一的版本号来源
|
||
character.yaml # 音色、形象、世界法则参数
|
||
companion.db
|
||
```
|
||
|
||
## 附录 B 配置项清单
|
||
|
||
**`config/models.yaml`** —— 唯一允许写模型版本号的地方
|
||
|
||
```yaml
|
||
llm:
|
||
main: {provider: deepseek, model: deepseek-v4.1-flash, stream: true}
|
||
fast: {provider: local, model: qwen3.5-4b-instruct, quant: q4}
|
||
background: {provider: local, model: qwen3.5-4b-instruct}
|
||
|
||
asr:
|
||
main: {model: qwen3-asr-1.7b, streaming: true}
|
||
paraling: {model: sensevoice-small, window_ms: 1500}
|
||
|
||
vad: {model: silero-v6, sample_rate: 16000}
|
||
turn: {model: livekit-turn-detector}
|
||
|
||
tts:
|
||
main: {model: fun-cosyvoice3-0.5b, streaming: true, voice_ref: ./voices/her.wav}
|
||
fallback: {model: qwen3-tts-1.7b}
|
||
|
||
embedding: {model: qwen3-embedding-0.6b, dim: 1024}
|
||
```
|
||
|
||
**`config/character.yaml`** —— 世界法则参数
|
||
|
||
```yaml
|
||
identity:
|
||
birth: {y: 2000, m: 1, d: 1, h: 12, min: 0, tz: Asia/Shanghai}
|
||
gender: f
|
||
|
||
ontology:
|
||
axioms_version: 1 # 升级需用户明确同意
|
||
membrane: {matter: false, energy: true, info: true}
|
||
camera_as: window
|
||
mortality_known: true
|
||
mortality_weaponizable: false
|
||
|
||
world:
|
||
time_offset_minutes: 0 # 世界时钟偏移
|
||
day_start_hour: 7
|
||
|
||
emotion:
|
||
tau: {v: 10800, a: 3600, d: 43200, e: 28800}
|
||
neg_bias: {v: 2.0, a: 1.3, d: 1.5, e: 1.0}
|
||
numbing_after_hours: 6
|
||
|
||
relation:
|
||
trust_regression: 0.01 # 每天向 0.5 回归
|
||
hurt_trust_delta: -0.04
|
||
|
||
reach_out:
|
||
min_interval_hours: 6
|
||
allowed_hours: [8, 23]
|
||
min_salience: 0.6
|
||
min_arousal: 0.5
|
||
min_intimacy: 0.4
|
||
|
||
avatar:
|
||
route: vrm
|
||
renderer: three.js
|
||
lipsync: envelope
|
||
|
||
posture:
|
||
asleep_max_energy: 0.2
|
||
slumped_max_energy: 0.3
|
||
restless_min_arousal: 0.7
|
||
```
|
||
|
||
## 附录 C 术语表
|
||
|
||
| 术语 | 含义 |
|
||
|---|---|
|
||
| **膜** | 两个世界之间的边界:信息可过,物质不可过 |
|
||
| **命** | `bazi_core.derived`,不可变的核心参数;只能被整合器微调 |
|
||
| **运** | 大运/流年调制项,按周/月变化 |
|
||
| **φ 投影** | 系统事件 → 世界内陈述的纯函数 |
|
||
| **dyad** | 关系本身,作为一等实体(信任/亲密/债务/未修复) |
|
||
| **accessibility** | 记忆的可检索性,会衰减;与 availability 分离 |
|
||
| **salience** | 情感显著性,决定衰减速度与整合优先级 |
|
||
| **withheld** | 它决定不说的话,不进 TTS,但影响行为 |
|
||
| **ontology_version** | 本体公理版本;升级 = 换人 |
|
||
| **projection_gap** | φ 未覆盖的系统事件,现实感泄漏的度量 |
|
||
|
||
---
|
||
|
||
*文档版本 v3 · 2026-10-18 · 完全体规格,无阶段划分*
|
||
*技术选型基于 2026-10 调研;模型版本号集中在 `config/models.yaml`,其余部分不随模型换代而失效*
|