# 主题相关性与对话连续性逻辑说明

## 一、现有主题相关性逻辑

### 1. 关键词提取 (`extract_keywords`)

- **输入**：当前轮次对话文本。
- **步骤**：
  1. 用正则提取「2 个及以上连续中文」或「3 个及以上连续英文」为候选词。
  2. 去掉常见停用词（的、了、是、在、和、有、我、你…）。
  3. 去重后按词长降序，取前 `keyword_limit`（默认 5）个作为关键词。
- **作用**：得到当前句子的主题「指纹」，用于和历史上各轮比较。

### 2. 主题描述 (`generate_topic_description`)

- **输入**：原文 + 上面得到的关键词。
- **逻辑**：
  - 若有关键词命中预置模式（如含「记忆」「技能」「Token」等），则返回固定描述（如「记忆管理相关」「Token优化相关」）。
  - 否则用「前 3 个关键词 + “相关”」拼成主题描述（如「美伊战争, 股票, 影响相关」）。
- **作用**：给当前轮一个可读的「主题标签」，并影响后续相似度（因为关键词会参与相似度计算）。

### 3. 主题相似度 (`calculate_topic_similarity`)

- **输入**：两个 `TopicSummary`（各自含 topic、keywords、timestamp 等）。
- **公式**：
  - **关键词重叠度**：  
    `keyword_similarity = |keywords₁ ∩ keywords₂| / max(|keywords₁|, |keywords₂|)`  
    即两轮关键词集合的交集大小，除以两者中较大的集合大小。
  - **时间衰减**：  
    `time_decay = time_decay_factor ^ (时间差/3600)`  
    时间差单位是秒，折算成小时后作为指数；默认 `time_decay_factor=0.95`，越久远的轮次权重越低。
  - **最终相似度**：  
    `similarity = keyword_similarity * time_decay`
- **特点**：只看「关键词重叠 + 时间」，不看语义或因果；因此「美伊战争」和「股票基本面」关键词几乎不重叠时，相似度会很低。

### 4. 是否切换上下文 (`should_switch_context`)

- **输入**：当前轮的 `TopicSummary`。
- **逻辑**：
  - 若尚未有当前主题（首轮或刚清空），不切换。
  - 否则取**上一轮**的 `TopicSummary`（`topic_history[-1]`），计算与当前轮的相似度。
  - **仅与上一轮比较**：  
    `similarity >= similarity_threshold`（默认 0.7）→ 不切换，走「连续上下文」；  
    `similarity < 0.7` → 切换，走「主题切换」并可能换记忆。
- **局限**：
  - 只比较「当前 vs 上一轮」，不承认「战争 → 股票影响 → 基本面」这种多步渐变。
  - 一旦当前轮与上一轮相似度 < 0.7 就整段当新话题切换，可能丢弃仍有用的上游上下文（如战争背景）。

---

## 二、对话连续性 vs 主题相关性

- **主题相关性**：当前这句话和**上一句**在「关键词/标签」上有多像（同上）。
- **对话连续性**：整段对话是否在逻辑上**一环扣一环**，即使话题已经漂移。

**例子**：  
美伊战争 → 对股票的影响 → 股票基本面  

- 若只比「战争」和「基本面」，关键词重叠少，会被判为「不相关」并触发**整段切换**，战争部分被丢弃。
- 实际上：战争 → 股票影响（有「影响」「市场」等桥接）→ 基本面（有「股票」等桥接），**对话是连续的**，只是主题在渐变。
- 期望行为：**仍视为同一段对话**，但对「离当前较远且与当前主题弱相关」的轮次（如战争）做**压缩**（保留简短摘要），而不是整段丢弃。

---

## 三、新增：连续性校验与渐变压缩

### 1. 三档判断（用两个阈值）

- **similarity_threshold**（默认 0.7）：当前与上一轮相似度 ≥ 此值 → **连续**，不压缩、不切换。
- **continuity_threshold**（默认 0.35）：当前与上一轮相似度 < 此值 → **硬切换**，按新话题处理。
- **中间区间 [0.35, 0.7)**：当前与上一轮仍有一定关联 → **渐变漂移**（`drift_compress`）：
  - 对话仍视为**连续**（不增加 switch_count，不换记忆）。
  - 对历史中「与当前主题相关性低」的轮次做**压缩**（只留短摘要，减少 token），**不整段丢弃**。

这样「战争 → 股票影响 → 基本面」会落在中间档：保持连续，同时把「战争」等旧段压缩。

### 2. 哪些历史轮次被压缩

- 对 `topic_history` 里**除最近 1～2 轮以外**的每条，计算其与**当前轮**的相似度（同上 `calculate_topic_similarity`）。
- 若某轮与当前的相似度 < **compress_relevance_threshold**（默认 0.3），且该轮尚未被压缩过，则：
  - 将该轮的 `content_snippet` 改为短摘要（如「主题标签 + [已压缩]」）。
  - 将该轮的 `tokens_used` 改为估算的摘要 token 数。
  - 标记 `is_compressed=True`，后续不再重复压缩。

结果是：战争段会被压缩成一句摘要保留在链里，股票影响和基本面保留较完整，既省 token 又保留「从战争聊到基本面」的脉络。

### 3. 弱关联识别（子串与首字）

为把「战争→股市→基本面」这类自然延续判成连续而非硬切换，相似度计算会做：

- **2 字子串**：从每个关键词中取所有 2 字片段，两边有交集则记为弱关联（如「局势」同时出现在「中东局势」「这些局势」）。
- **2 字片段首字**：再取每个 2 字片段的第一个字参与交集（如「股」在「股票」「股市」里），便于「股票」与「股市」建立弱关联；**虚字**（的、了、是、在、和、有、与、这、那、什么、怎么、为什么、一、不）不参与，避免无关话题被误判为相关。
- **连续性加成与上限**：若存在弱关联，在相似度上加成（默认 0.3），但**上限**为 `drift_similarity_cap`（默认 0.65），使「仅弱关联」时落在 [0.35, 0.7)，走 drift_compress 并有机会压缩旧段。

### 4. 配置项（config.json）

- `continuity_threshold`：低于此值判为硬切换（默认 0.35）。
- `compress_relevance_threshold`：历史轮与当前相似度低于此值则压缩该轮（默认 0.3）。
- `continuity_bonus_for_partial`：存在弱关联时的相似度加成（默认 0.3）。
- `drift_similarity_cap`：弱关联时相似度上限，便于判为 drift_compress（默认 0.65）。

---

## 四、行为对照简表

| 当前与上一轮相似度     | 判定         | 行为 |
|------------------------|--------------|------|
| ≥ 0.7                  | 同一主题     | 连续上下文，不压缩 |
| ∈ [0.35, 0.7)          | 渐变漂移     | 连续但压缩「与当前弱相关」的旧轮 |
| < 0.35                 | 无关新话题   | 主题切换，换记忆、清旧上下文 |
