# Technical Documentation: xszyou/Fay

> ℹ️ **Provenance:** Multi-Tier Ingestion · [CodeWiki Reference](https://codewiki.google/github.com/xszyou/Fay)

### Overview (README.md)

<div align="center">
    <br>
    <img src="readme/icon.png" alt="Fay">
    <h1>FAY</h1>
	<h3>Fay数字人框架</h3>
</div>

！！重要通知：我们已经把Fay的三个版本合并成1个，并致力提供更稳定更全面的功能。

我们致力于思考面向终端的数字人落地应用，并通过完整代码把思考结果呈现给大家。Fay数字人框架，向上适配各种数字人模型技术，向下接入各式大语言模型，并且便于更换诸如TTS、ASR等模型，为单片机、app、网站提供全面的数字人应用接口。      
更新日志：https://qqk9ntwbcit.feishu.cn/wiki/UlbZwfAXgiKSquk52AkcibhHngg
文档：https://qqk9ntwbcit.feishu.cn/wiki/JzMJw7AghiO8eHktMwlcxznenIg


## **功能特点**



- 完全开源，商用免责
- 支持全离线使用
- 全时流式的支持
- 自由匹配数字人模型、大语言模型（openai 兼容接口）、ASR、TTS模型
- 支持数字人自动播报模式（虚拟教师、虚拟主播、新闻播报）
- 支持任意终端使用：单片机、app、网站、大屏、三方业务系统接入等
- 支持多用户多路并发
- 提供文字交互接口、语音交互接口、数字人驱动接口、管理控制接口、自动播报接口、意图接口
- 支持语音指令灵活配置执行（qa.csv）
- 支持自定义知识库、自定义问答对、自定义人设信息
- 支持唤醒及打断对话
- 支持服务器及单机模式
- 支持机器人表情输出
- 支持agent自主决策工具调用
- 基于日程式数字人主动对话
- 支持后台静默启动
- 支持deepseek等thinking llm
- 自我认知提高
- 仿生记忆
- 支持MCP工具管理（sse、studio）
- 提供配置管理中心
- 全链路交互互通

###               

## **Fay数字人框架**












## **源码启动**


### **环境** 
- Python 3.12

- Windows、macos、ubuntu

- 注：ubuntu需要先安装gcc及portaudio

- ````bash
  sudo apt update
  sudo apt install build-essential
  sudo apt install portaudio19-dev
  ````

  

### **安装依赖**

```shell
pip install -r requirements.txt
```


### **快速启动**
本地
```shell
python main.py start -config_center d19f7b0a-2b8a-4503-8c0d-1a587b90eb69  #使用公共资源，速度非常慢，建议更换成自己的key
```
镜像
```shell
https://www.compshare.cn/images/compshareImage-1cft3sk9gvta?ytag=GPU_fay
```

### **个性化配置**
+ 根目录system.conf.bak 重命名为system.conf，并配置里面的内容

### **管理页面**
+ 浏览器访问 http://127.0.0.1:5000

## **高级玩法**





### ***使用数字人（非必须）***
https://qqk9ntwbcit.feishu.cn/wiki/GHevwqxwfiX4hCk8yJCcoJ54nqg




### ***集成到自家产品（非必须）***
https://qqk9ntwbcit.feishu.cn/wiki/Mcw3wbA3RiNZzwkexz6cnKCsnhh



### **联系**

**交流群及资料教程**关注公众号 **fay数字人**（**请先star本仓库**）


**商务联系**

qq467665317


## **致谢**

感谢以下开源项目为 Fay 提供的技术支持：

- [openclaw](https://github.com/openclaw/openclaw) - 提供记忆机制及skills设计的参考
- [OpenAI Codex](https://github.com/openai/codex) - 提供稳定的工具调用能力的参考
- [FunASR](https://github.com/modelscope/FunASR) - 提供语音识别（ASR）能力

---

### Fay侧标准动作改造说明 (`docs/Fay侧标准动作改造说明.md`)

# Fay 侧通用动作语义改造说明

## 目标

让 Fay 输出“通用动作语义”，而不是输出某个渲染引擎或某个模型的具体动作编号。

这样 Fay 可以同时服务于：

- Live2D 数字人
- 3D 数字人
- Unity / Unreal 角色
- 机器人 / 硬件角色
- 仅语音客户端

原则：

1. 保持原版 Fay WebSocket 音频接口结构不变。
2. 在原版 `Data` 中增加两个可选字段：`Action` 和 `Sentiment`。
3. Fay 不输出 `TapBody`、`MotionNo`、`F01` 这类具体实现细节。
4. 各前端或驱动端通过自己的配置表完成映射。

---

## 原版接口

原版接口如下：

```json
{
  "Topic": "human",
  "Data": {
    "Key": "audio",
    "Text": "这边请",
    "IsFirst": 1,
    "IsEnd": 0,
    "Lips": []
  }
}
```

---

## 建议改造方式

在原版 `Data` 中增加可选字段 `Action` 和 `Sentiment`：

```json
{
  "Topic": "human",
  "Data": {
    "Key": "audio",
    "Text": "这边请",
    "IsFirst": 1,
    "IsEnd": 0,
    "Lips": [],
    "Sentiment": 0.7,
    "Action": {
      "code": "guidance.invite",
      "behavior": "invite",
      "affect": "warm",
      "intensity": 0.74,
      "priority": 82
    }
  }
}
```

说明：

- 不强制增加 `ActionSchema`
- 第一版先保证字段语义稳定即可
- 如果未来需要版本控制，可以后续再补 `Action.version`
- `Action` 和 `Sentiment` 建议同时存在，但都应为可选字段

---

## 为什么需要“动作语义锚点”

这里的“动作语义锚点”，建议用 `behavior` 表示。

它的作用不是描述某个具体模型动作，而是给所有前端一个统一的“动作类别坐标”。

### 它解决的问题

| 问题 | 没有语义锚点时 | 有语义锚点时 |
| --- | --- | --- |
| Fay 与模型耦合 | Fay 需要知道 `TapBody`、`MotionNo=21` | Fay 只说 `behavior=invite` |
| 多端复用困难 | Live2D、3D、机器人都要单独写逻辑 | 各端都只做 `behavior -> 本地动作` 映射 |
| 话术很多但动作有限 | 每种话术都要单独适配 | 多个话术可归一到同一个锚点 |
| 后续维护成本高 | 每换模型都要改 Fay | 只改前端配置表 |

### 一个简单例子

这些话术：

- “这边请”
- “请进”
- “跟我来”
- “请往这边走”

在 Fay 侧都可以归一成：

```json
{
  "code": "guidance.invite",
  "behavior": "invite",
  "affect": "warm"
}
```

然后不同终端各自解释：

- Live2D：映射到某个引导动作
- 3D：映射到某个招手/邀请动画
- 机器人：映射到某个手臂引导动作
- 纯语音：忽略动作，只保留语气

所以：

- `code` 更偏业务/场景语义
- `behavior` 更偏“身体动作类别”
- `affect` 更偏“表现气质/情绪风格”

---

## 推荐字段定义

| 字段 | 类型 | 是否必填 | 含义 |
| --- | --- | --- | --- |
| `Sentiment` | number | 否 | 连续情绪值，建议范围 `-2 ~ 2` 或 `-1 ~ 1`，由接收端自行约定 |
| `Action.code` | string | 是 | 标准动作语义 ID，稳定主键 |
| `Action.behavior` | string | 否 | 动作语义锚点，描述身体动作类别 |
| `Action.affect` | string | 否 | 表现语义锚点，描述表情/语气/情绪风格 |
| `Action.intensity` | number | 否 | 强度，建议范围 `0 ~ 1` |
| `Action.priority` | number | 否 | 优先级，冲突时用于决策 |

---

## 字段职责建议

### `Sentiment`

用于表达”这段话整体情绪倾向”。

它和 `Action` 不冲突，职责不同：

- `Action` 决定”做什么”
- `Sentiment` 决定”情绪偏什么方向”

推荐用途：

- 当 `Action` 存在时，作为微调信号
- 当 `Action` 不存在时，作为回退信号
- 可同时用于表情、语气、动作幅度、TTS 风格

#### `Sentiment` 计算方式

Fay 当前有两种计算方式，按优先级依次尝试：

**方式一：百度情感分析 API（优先）**

如果配置了 `baidu_emotion_api_key` 和 `baidu_emotion_secret_key`，调用百度 NLP 的 `sentiment_classify` 接口（`ai_module/baidu_emotion.py`），返回百度原始 sentiment 值（0=消极 / 1=中性 / 2=积极）。

**方式二：本地关键词匹配（兜底）**

未配置百度 API 或调用失败时，走 `fay_core.py` 中的 `__analyze_sentiment_by_keywords()` 方法，逻辑如下：

1. 维护 4 个关键词列表，每个对应一个权重：

   | 列表 | 示例关键词 | 权重 |
   | --- | --- | --- |
   | 非常积极 | 开心、太好了、完美、激动 | +2 |
   | 轻微积极 | 好、可以、没问题、欢迎 | +1 |
   | 轻微消极 | 不好、失望、烦、担心 | -1 |
   | 非常消极 | 痛苦、绝望、崩溃、气死 | -2 |

2. 统计文本中匹配的关键词数量，加权求和
3. 标点修正：`？`/`!`/`~` 加 0.3，`...`/`。。.` 减 0.3
4. 最终 clamp 到 `[-2, +2]` 范围

注意：此计算与 `Action` 中的 `sentimentHint` 字段完全独立。`sentimentHint` 是动作规则自身的情感标注，用于辅助关键词匹配；`Sentiment` 是对整句话的情感极性评估。

### `code`

用于表达“这次动作在业务上的语义是什么”。

例如：

- `greeting.hello`
- `guidance.invite`
- `dialogue.explain`
- `social.thanks`

### `behavior`

用于表达“身体动作属于哪一类”。

例如：

- `nod`
- `invite`
- `wave`
- `think`
- `warn`

它是动作语义锚点。

### `affect`

用于表达“整体表现风格是什么”。

例如：

- `smile`
- `warm`
- `neutral`
- `serious`
- `sad`
- `excited`

这个字段比 `expression` 更通用，因为它不只适用于 Live2D 表情，也可以映射到：

- 3D 面部表情
- TTS 语气风格
- 机器人灯光/屏幕表情

### `intensity`

用于表达“动作做得轻一点还是重一点”。

例如：

- `0.2` 轻微点头
- `0.8` 明显邀请
- `0.95` 强烈庆祝

---

## Fay 侧只负责什么

Fay 侧只负责：

```text
文本 / 意图 / 场景 -> Action
文本 / 情绪分析 -> Sentiment
```

Fay 不负责：

```text
Action -> Live2D动作编号
Action -> 3D动画名
Action -> 机器人舵机动作
```

---

## 不建议 Fay 输出的字段

| 不建议由 Fay 输出 | 原因 |
| --- | --- |
| `MotionGroup` | 属于 конкретe 模型动作组 |
| `MotionNo` | 属于 конкретe 模型动作编号 |
| `TapBody` | 属于 конкретe Live2D 工程命名 |
| `F01/F02/F03` | 属于 конкретe 模型表情资源名 |

这些都应该由前端或驱动端自行配置。

---

## 推荐可配置映射表

Fay 侧维护一张 CSV 配置表（`config/action_rules.csv`），可直接用 Excel 打开编辑：

```text
关键词/意图 -> code/behavior/affect/intensity/priority/sentimentHint
```

CSV 格式说明：
- 第一行为表头：`code,behavior,affect,intensity,priority,sentimentHint,keywords`
- `keywords` 列中多个关键词用 `|` 分隔（避免与 CSV 逗号冲突）
- 文件编码为 UTF-8

不要把这张表写死在代码里。

### 当前规则示例

| 关键词或意图示例 | `code` | `behavior` | `affect` | `intensity` | `priority` |
| --- | --- | --- | --- | --- | --- |
| 你好、您好、hello、hi | `greeting.hello` | `nod` | `smile` | `0.35` | `60` |
| 欢迎、欢迎光临、欢迎来到 | `greeting.welcome` | `invite` | `warm` | `0.78` | `80` |
| 再见、拜拜、goodbye | `farewell.goodbye` | `wave` | `smile` | `0.62` | `72` |
| 是的、没错、收到、明白 | `dialogue.confirm` | `nod` | `smile` | `0.42` | `58` |
| 不是、不行、不能、no | `dialogue.reject` | `reject` | `serious` | `0.58` | `70` |
| 让我想想、考虑一下 | `dialogue.think` | `think` | `neutral` | `0.44` | `74` |
| 为什么、怎么回事、真的吗 | `dialogue.question` | `question` | `curious` | `0.52` | `55` |
| 也就是说、其实、比如 | `dialogue.explain` | `explain` | `neutral` | `0.56` | `63` |
| 建议、推荐、你可以试试 | `dialogue.recommend` | `recommend` | `smile` | `0.63` | `66` |
| 总之、综上、最后总结 | `dialogue.summary` | `summary` | `neutral` | `0.40` | `57` |
| 请进、这边请、跟我来 | `guidance.invite` | `invite` | `warm` | `0.74` | `82` |
| 稍等、等一下、请稍等 | `guidance.wait` | `wait` | `neutral` | `0.26` | `65` |
| 提醒、别忘了、记得 | `guidance.remind` | `remind` | `neutral` | `0.48` | `62` |
| 注意、小心、警告 | `guidance.warn` | `warn` | `serious` | `0.80` | `84` |
| 谢谢、感谢、多谢 | `social.thanks` | `thanks` | `warm` | `0.46` | `61` |
| 抱歉、对不起、不好意思 | `social.apology` | `apology` | `sorry` | `0.60` | `71` |
| 还好吗、没事吧、别担心 | `social.care` | `care` | `warm` | `0.50` | `64` |
| 太好了、成功了、真棒 | `emotion.celebrate` | `celebrate` | `excited` | `0.95` | `92` |
| 天啊、竟然、不会吧 | `emotion.surprise` | `surprise` | `surprised` | `0.88` | `86` |
| 难过、可惜、遗憾、糟糕 | `emotion.sad` | `sad` | `sad` | `0.82` | `83` |

说明：

- 这张表本身就建议“带上表情风格”，也就是 `affect`
- 这样前端不需要只靠 `Sentiment` 猜表情
- `Sentiment` 仍然保留，用于微调和回退

---

## 各端如何消费 `Action`

### Live2D

```text
behavior -> motion
affect -> expression
intensity -> 候选动作强度选择
sentiment -> 表情/动作幅度微调或回退
```

### 3D 数字人

```text
behavior -> 动画状态机
affect -> 面部表情 / blendshape
intensity -> 动作幅度 / 播放权重
sentiment -> 情绪层混合权重
```

### 机器人

```text
behavior -> 舵机动作模板
affect -> 灯光 / 屏幕表情 / 语音风格
intensity -> 动作幅度 / 执行力度
sentiment -> 情绪表现微调
```

### 纯语音客户端

```text
behavior -> 可忽略
affect -> TTS 风格 / 音色 / 情绪
intensity -> 情绪强度
sentiment -> 连续情绪控制
```

---

## 最小结论

Fay 侧建议从：

```json
{
  "Topic": "human",
  "Data": {
    "Key": "audio",
    "Text": "这边请",
    "IsFirst": 1,
    "IsEnd": 0,
    "Lips": []
  }
}
```

升级为：

```json
{
  "Topic": "human",
  "Data": {
    "Key": "audio",
    "Text": "这边请",
    "IsFirst": 1,
    "IsEnd": 0,
    "Lips": [],
    "Sentiment": 0.7,
    "Action": {
      "code": "guidance.invite",
      "behavior": "invite",
      "affect": "warm",
      "intensity": 0.74,
      "priority": 82
    }
  }
}
```

即可。

后续具体怎么动，由各前端或驱动端通过配置表自己决定。

---

### Fay数字人MCP知识库配置指南 (`docs/Fay数字人MCP知识库配置指南.md`)

# Fay数字人MCP知识库配置指南

> 本文介绍如何在Fay数字人中通过MCP工具配置知识库，实现智能问答增强。

## 一、实现效果

### 1. 通过MCP工具灵活配置

Fay采用MCP（Model Context Protocol）协议实现知识库集成，具有以下优势：

- **模块化设计**：知识库作为独立的MCP服务器运行，与Fay主程序解耦
- **灵活扩展**：支持同时接入多个知识库服务，按需启用
- **标准协议**：遵循MCP标准，可复用社区现有的知识库工具
- **可视化管理**：通过Web界面完成全部配置，无需修改代码



### 2. 1次LLM请求即可输出

通过**预启动（Prestart）**机制，知识库查询在LLM推理前自动执行：

```
用户提问 → 预启动工具执行(知识库检索) → 检索结果注入上下文 → LLM生成回答
```

整个流程只需**1次LLM调用**，相比传统RAG方案减少了额外的意图判断步骤，响应更快、成本更低。

**工作原理**：
- 预启动工具在每次对话时自动触发
- 用户问题通过`{{question}}`占位符传递给知识库查询
- 检索结果作为背景知识注入到LLM的系统提示词中
- LLM基于检索到的知识生成准确回答

### 3. 提供接口可以单独调用

知识库工具支持通过REST API独立调用，方便集成到其他系统：

**调用接口**：
```http
POST http://127.0.0.1:5010/api/mcp/servers/{server_id}/call
Content-Type: application/json

{
    "method": "query_yueshen",
    "params": {
        "query": "你的查询问题",
        "top_k": 5
    }
}
```

**返回示例**：
```json
{
    "success": true,
    "result": {
        "documents": [...],
        "distances": [...]
    }
}
```

---

## 二、如何配置

### 1. MCP工具配置

#### 步骤一：访问MCP管理页面

启动Fay后，访问MCP管理界面：
```
http://127.0.0.1:5010/Page3
```

#### 步骤二：添加知识库服务器

点击"添加MCP服务器"按钮，配置以下信息：

| 配置项 | 说明 | 示例值 |
|--------|------|--------|
| 服务器名称 | 自定义名称 | `yueshen rag` |
| 传输方式 | 选择STDIO | `stdio` |
| 启动命令 | Python解释器路径 | `python` |
| 启动参数 | 服务器脚本路径 | `mcp_servers/yueshen_rag/server.py` |
| 环境变量 | 配置API密钥等 | 见下方 |

#### 步骤三：配置环境变量

知识库服务需要配置embedding模型的API：

```json
{
    "YUESHEN_CORPUS_DIR": "新知识库",
    "YUESHEN_PERSIST_DIR": "cache_data/chromadb_yueshen",
    "YUESHEN_AUTO_INGEST": "1",
    "YUESHEN_AUTO_INTERVAL": "300",
    "YUESHEN_EMBED_API_KEY": "sk-your-api-key",
    "YUESHEN_EMBED_BASE_URL": "https://api.siliconflow.cn/v1",
    "YUESHEN_EMBED_MODEL": "Qwen/Qwen3-Embedding-8B"
}
```

**环境变量说明**：

| 变量名 | 说明 | 默认值 |
|--------|------|--------|
| `YUESHEN_CORPUS_DIR` | 知识库文档目录 | `新知识库` |
| `YUESHEN_PERSIST_DIR` | 向量数据库存储路径 | `cache_data/chromadb_yueshen` |
| `YUESHEN_AUTO_INGEST` | 是否自动索引新文档 | `1`（启用） |
| `YUESHEN_AUTO_INTERVAL` | 自动索引检查间隔（秒） | `300` |
| `YUESHEN_EMBED_API_KEY` | Embedding API密钥 | - |
| `YUESHEN_EMBED_BASE_URL` | Embedding API地址 | - |
| `YUESHEN_EMBED_MODEL` | Embedding模型名称 | - |

#### 步骤四：连接服务器

保存配置后，点击"连接"按钮启动知识库服务。连接成功后：
- 服务器状态变为绿色"在线"
- 下方显示可用工具列表：`ingest_yueshen`、`query_yueshen`、`yueshen_stats`

### 2. 参数配置

#### 配置预启动参数

1. 在工具列表中找到 `query_yueshen` 工具
2. 点击工具右侧的"预启动?"标签
3. 在弹出的对话框中配置参数：

```json
{
    "query": "{{question}}",
    "top_k": 5,
    "where": {}
}
```

**参数说明**：

| 参数 | 类型 | 说明 |
|------|------|------|
| `query` | string | 查询文本，`{{question}}`会被替换为用户实际问题 |
| `top_k` | int | 返回最相关的文档数量 |
| `where` | object | 可选的元数据过滤条件 |

#### 配置选项

- **结果保存到记忆**：勾选后，检索结果会保存到对话历史中
- **允许函数调用**：是否允许LLM在推理时再次调用此工具

点击"保存预启动"完成配置。

### 3. 启动方式

#### 方式一：随Fay自动启动

配置完成后，知识库服务会在Fay启动时自动连接。确保：
1. MCP服务器已添加并保存
2. 在服务器卡片上启用了自动连接

#### 方式二：手动连接

在MCP管理页面：
1. 找到目标服务器卡片
2. 点击"连接"按钮
3. 等待状态变为"在线"

#### 方式三：通过API连接

```http
POST http://127.0.0.1:5010/api/mcp/servers/{server_id}/connect
```

#### 验证配置

连接成功后，可以通过以下方式验证：

1. **查看工具列表**：确认`query_yueshen`等工具已显示
2. **测试查询**：在工具面板中手动调用工具测试
3. **对话测试**：向Fay提问知识库相关问题，观察是否返回准确答案

---

## 三、怎么做到的

### Fay MCP工具的预启动功能

Fay实现了一套独特的**预启动（Prestart）**机制，使知识库查询能够无缝集成到对话流程中。

#### 核心设计

```
/* Detailed source-code truncated for AI context efficiency. */
```

#### 技术实现要点

**1. 预启动配置独立于工具启用状态**

预启动功能与工具的启用/禁用状态相互独立：
- **启用/禁用**：控制LLM是否可以主动调用该工具
- **预启动**：控制工具是否在每次对话前自动执行

这意味着你可以：
- 禁用工具的LLM调用权限，但保留预启动功能
- 让知识库查询仅在对话开始时执行，避免LLM重复调用

**2. 参数模板与占位符替换**

预启动支持`{{question}}`占位符，在执行时自动替换为用户的实际问题：

```python
# 参数模板
{"query": "{{question}}", "top_k": 5}

# 用户问题："Fay如何配置语音识别？"
# 实际执行参数
{"query": "Fay如何配置语音识别？", "top_k": 5}
```

**3. 结果处理与记忆管理**

预启动结果可以选择是否保存到对话记忆：
- `include_history: true` - 结果保存到历史，后续对话可引用
- `include_history: false` - 仅用于当前对话，不污染历史记录

**4. 多知识库并行查询**

支持配置多个预启动工具，它们会并行执行：

```
预启动工具1: 产品知识库查询
预启动工具2: FAQ知识库查询
预启动工具3: 用户手册查询
         │
         ▼
    结果合并后一起注入LLM上下文
```

#### 相关代码位置

| 模块 | 文件 | 说明 |
|------|------|------|
| 预启动注册表 | `faymcp/prestart_registry.py` | 预启动配置的存储与管理 |
| 预启动执行 | `llm/nlp_cognitive_stream.py` | `_run_prestart_tools()` 函数 |
| API路由 | `faymcp/mcp_service.py` | `/api/mcp/prestart/runnable` 接口 |
| 前端配置 | `faymcp/templates/Page3.html` | 预启动配置对话框 |

---

## 常见问题

### Q1: 知识库服务连接失败？

检查以下几点：
1. Python环境是否正确配置
2. 依赖包是否安装完整（chromadb, pdfplumber等）
3. 环境变量中的API密钥是否正确
4. 知识库文档目录是否存在

### Q2: 检索结果不准确？

优化建议：
1. 调整`top_k`参数，增加返回文档数量
2. 检查文档分块策略是否合理
3. 尝试更换Embedding模型
4. 优化知识库文档的质量和格式

### Q3: 如何添加新文档到知识库？

将文档（支持PDF、DOCX、TXT）放入`YUESHEN_CORPUS_DIR`配置的目录中：
- 如果启用了`YUESHEN_AUTO_INGEST`，系统会自动索引
- 也可以手动调用`ingest_yueshen`工具触发索引

### Q4: 预启动工具执行超时？

检查：
1. Embedding API服务是否正常
2. 网络连接是否稳定
3. 可适当增加超时时间配置

---

## 总结

Fay数字人通过MCP协议和预启动机制，实现了：

- **零代码配置**：可视化界面完成全部设置
- **高效查询**：1次LLM请求完成知识增强回答
- **灵活扩展**：支持多知识库、多工具并行
- **接口开放**：REST API支持外部系统集成

这套方案既保持了系统的简洁性，又提供了强大的知识库集成能力，是构建智能数字人的理想选择。

---

### Live2D模型制作要求 (`docs/Live2D模型制作要求.md`)

# Live2D 模型制作要求

本文档用于指导外包人员制作 Live2D 模型，确保交付后能直接对接我方的数字人系统。

---

## 一、技术规格

| 项目 | 要求 |
| --- | --- |
| Cubism 版本 | **Cubism 5**（SDK for Web 5-r.4） |
| model3.json 版本 | Version: 3 |
| 贴图尺寸 | 2048×2048，张数不限 |
| 交付格式 | 完整的 Cubism 导出目录（含 .moc3、贴图、motion3.json、exp3.json、physics3.json、pose3.json 等） |
| 渲染目标 | 浏览器端 WebGL（Cubism SDK for Web） |

---

## 二、必需参数（Parameters）

模型必须包含以下标准参数，参数 ID 请严格使用 Cubism 默认命名：

### 口型同步（必需）

| 参数 ID | 用途 | 说明 |
| --- | --- | --- |
| `ParamMouthOpenY` | 嘴巴张合 | **最关键参数**，程序通过此参数驱动实时口型同步 |

程序会在 `model3.json` 的 `Groups` 中查找 `LipSync` 组：

```json
{
  "Target": "Parameter",
  "Name": "LipSync",
  "Ids": ["ParamMouthOpenY"]
}
```

### 眨眼（必需）

| 参数 ID | 用途 |
| --- | --- |
| `ParamEyeLOpen` | 左眼开合 |
| `ParamEyeROpen` | 右眼开合 |

程序会在 `Groups` 中查找 `EyeBlink` 组：

```json
{
  "Target": "Parameter",
  "Name": "EyeBlink",
  "Ids": ["ParamEyeLOpen", "ParamEyeROpen"]
}
```

### 头部与身体跟随（必需）

| 参数 ID | 用途 |
| --- | --- |
| `ParamAngleX` | 头部左右旋转 |
| `ParamAngleY` | 头部上下旋转 |
| `ParamAngleZ` | 头部倾斜 |
| `ParamBodyAngleX` | 身体左右摇摆 |
| `ParamEyeBallX` | 眼球水平跟随 |
| `ParamEyeBallY` | 眼球垂直跟随 |

这些参数用于呼吸动效和鼠标/视线跟随，程序会自动驱动。

---

## 三、口型同步（LipSync）要求

程序会通过 `ParamMouthOpenY` 参数实时驱动嘴巴开合，模拟说话口型。该参数的值范围是 **0（闭嘴）~ 1（张嘴最大）**。

### 对嘴部建模的要求

- **`ParamMouthOpenY` = 0 时**：嘴巴完全闭合，自然状态
- **`ParamMouthOpenY` = 1 时**：嘴巴张到最大
- 中间值（0.2~0.8）应有平滑的过渡，不能出现跳变或形变异常
- 嘴部形变需要在 0~1 全范围内看起来自然，因为程序会输出各种中间值

### 建模细节建议

程序内部使用 OVR LipSync 的 15 种 viseme（口型音素），每种映射到不同的开合度：

| 开合度范围 | 对应口型场景 | 对应 viseme 示例 |
| --- | --- | --- |
| 0.0 | 静音 / 闭嘴 | sil |
| 0.2~0.3 | 轻微张嘴（双唇音、唇齿音） | PP, FF, nn |
| 0.4~0.5 | 中度张嘴（齿音、舌音） | TH, DD, CH, SS, ih |
| 0.6~0.7 | 较大张嘴（元音、圆唇音） | kk, E, oh |
| 0.8~0.9 | 大张嘴（低元音、圆唇突出） | aa, ou |

请确保这些开合度值下嘴型看起来合理自然。

### 重要约束

- 动作（motion）和表情（expression）中**都不要**对 `ParamMouthOpenY` 设置关键帧，否则会与程序的实时口型驱动冲突
- 如果嘴部有多个参数（如 `ParamMouthForm` 控制嘴型宽窄），可以保留，但 `ParamMouthOpenY` 必须留给程序独占

---

## 四、动作（Motions）要求

### 动作组结构

模型必须包含以下两个动作组：

| 动作组名称 | 用途 | 说明 |
| --- | --- | --- |
| `Idle` | 待机动作 | 无对话时循环播放，至少 1 个 |
| `TapBody` | 语义动作 | 对话时由程序调用，按编号索引触发 |

### TapBody 动作清单

`TapBody` 组中的动作按**数组下标**（从 0 开始）索引触发。程序会根据后端传来的语义信号选择对应编号的动作。

需要制作以下 **18 类语义动作**，每类至少 1 个，推荐关键类别提供 2 个变体（用于强度区分）：

| 编号 | 语义（behavior） | 动作描述 | 建议变体数 |
| --- | --- | --- | --- |
| 1 | nod（点头） | 轻微点头表示肯定 | 2（轻/重） |
| 2 | invite（邀请） | 伸手引导方向 | 1 |
| 3 | wave（挥手） | 挥手打招呼或告别 | 1 |
| 4 | reject（拒绝） | 摇头或摆手表示否定 | 2（轻/重） |
| 5 | think（思考） | 手托下巴、目光偏移 | 2（轻/重） |
| 6 | question（疑问） | 歪头、挑眉表示好奇 | 1 |
| 7 | explain（解释） | 双手摊开或单手比划说明 | 2（简述/详述） |
| 8 | recommend（推荐） | 单手指向或展示推荐姿态 | 1 |
| 9 | summary（总结） | 双手合拢或归拢手势 | 1 |
| 10 | wait（等待） | 双手交叠、微微前倾 | 1 |
| 11 | remind（提醒） | 食指轻点或举手提示 | 1 |
| 12 | warn（警告） | 严肃摆手或交叉手势 | 1 |
| 13 | thanks（感谢） | 鞠躬或双手合十 | 2（轻/重） |
| 14 | apology（道歉） | 鞠躬或低头表示歉意 | 1 |
| 15 | care（关心） | 微微前倾、温和手势 | 1 |
| 16 | celebrate（庆祝） | 举手欢呼、拍手 | 2（轻/重） |
| 17 | surprise（惊讶） | 后仰、手捂嘴或张大眼 | 1 |
| 18 | sad（悲伤） | 低头、肩膀下沉 | 2（轻/重） |

**关于编号与变体的说明：**

- TapBody 组中的动作按数组顺序排列，程序通过下标调用
- 有 2 个变体的类别：下标靠前的是**轻度版本**（intensity 低时使用），下标靠后的是**强度版本**（intensity 高时使用）
- 最终动作总数取决于变体数量，预计 **18~26 个**
- 交付时请提供一份**动作编号与语义的对照表**，方便我方配置映射

### 动作制作要求

- 每个动作 `FadeInTime` 和 `FadeOutTime` 建议设为 `0.5` 秒
- 动作时长建议 2~4 秒，不要过长（对话过程中会频繁切换）
- 动作结束后应能自然过渡回待机姿态
- **动作中不要包含嘴部参数的关键帧**，口型由程序实时驱动，动作中写入嘴部数据会产生冲突

---

## 五、表情（Expressions）要求

表情用于控制面部细节（眉毛、眼睛、嘴角等），与动作独立叠加。

### 必需表情清单

至少制作以下 **6 个表情**，表情名称请严格按下表命名：

| 表情名称 | 语义（affect） | 面部特征描述 |
| --- | --- | --- |
| `F01` | 默认 / 微笑（smile, warm, neutral） | 自然微笑，嘴角上扬，眉毛放松。这是最常用的表情 |
| `F02` | 严肃（serious） | 眉毛微蹙，嘴角平或略向下，目光坚定 |
| `F03` | 悲伤 / 歉意（sad, sorry） | 眉毛下垂，眼睛微眯，嘴角下拉 |
| `F04` | 好奇 / 惊讶 / 兴奋（curious, surprised, excited） | 眉毛上挑，眼睛睁大 |
| `F05` | 害羞 / 不好意思 | 脸红、目光偏移、嘴角含笑（可选，备用） |
| `F06` | 得意 / 自信 | 眉毛上扬、嘴角上翘、目光明亮（可选，备用） |

**说明：**

- `F01`~`F04` 为**必需**，程序核心逻辑依赖这 4 个表情
- `F05`~`F06` 及更多为**可选**，交付后我方可扩展映射
- 如果能做更多差异化表情更好，但命名请保持 `F01`、`F02`... 的编号格式
- 表情文件格式为 `.exp3.json`，放在 `expressions/` 目录下

### 表情制作要求

- **表情中不要包含 `ParamMouthOpenY` 参数**，口型由程序实时驱动，表情中写入会产生冲突
- 表情是对基础状态的差值叠加，请确保各表情之间切换自然
- `F01`（微笑）是默认表情，对话结束后会自动恢复到此表情

---

## 六、model3.json 结构要求

最终交付的 `model3.json` 需要包含以下结构（以示例名 `MyModel` 为例）：

```json
{
  "Version": 3,
  "FileReferences": {
    "Moc": "MyModel.moc3",
    "Textures": ["MyModel.2048/texture_00.png"],
    "Physics": "MyModel.physics3.json",
    "Pose": "MyModel.pose3.json",
    "Expressions": [
      { "Name": "F01", "File": "expressions/F01.exp3.json" },
      { "Name": "F02", "File": "expressions/F02.exp3.json" },
      { "Name": "F03", "File": "expressions/F03.exp3.json" },
      { "Name": "F04", "File": "expressions/F04.exp3.json" }
    ],
    "Motions": {
      "Idle": [
        { "File": "motions/idle.motion3.json", "FadeInTime": 0.5, "FadeOutTime": 0.5 }
      ],
      "TapBody": [
        { "File": "motions/m01_nod_light.motion3.json", "FadeInTime": 0.5, "FadeOutTime": 0.5 },
        { "File": "motions/m02_nod_heavy.motion3.json", "FadeInTime": 0.5, "FadeOutTime": 0.5 },
        "... 按照对照表顺序排列所有动作"
      ]
    }
  },
  "Groups": [
    {
      "Target": "Parameter",
      "Name": "EyeBlink",
      "Ids": ["ParamEyeLOpen", "ParamEyeROpen"]
    },
    {
      "Target": "Parameter",
      "Name": "LipSync",
      "Ids": ["ParamMouthOpenY"]
    }
  ],
  "HitAreas": [
    { "Id": "HitArea", "Name": "Head" },
    { "Id": "HitArea2", "Name": "Body" }
  ]
}
```

---

## 七、目录结构示例

```
MyModel/
├── MyModel.model3.json          # 模型配置（入口文件）
├── MyModel.moc3                 # 模型数据
├── MyModel.physics3.json        # 物理演算
├── MyModel.pose3.json           # 姿态配置（可选）
├── MyModel.cdi3.json            # 显示信息（可选）
├── MyModel.2048/                # 贴图目录
│   ├── texture_00.png
│   └── texture_01.png           # 如有多张
├── expressions/                 # 表情文件
│   ├── F01.exp3.json
│   ├── F02.exp3.json
│   ├── F03.exp3.json
│   └── F04.exp3.json
└── motions/                     # 动作文件
    ├── idle.motion3.json
    ├── m01_nod_light.motion3.json
    ├── m02_nod_heavy.motion3.json
    └── ...
```

---

## 八、交付清单

| 交付物 | 必需 | 说明 |
| --- | --- | --- |
| 完整模型导出目录 | 是 | 包含上述所有文件 |
| 动作编号对照表 | 是 | 说明 TapBody 中每个下标对应的语义和动作描述 |
| Cubism Editor 工程源文件（.cmo3） | 推荐 | 方便后续调整 |
| 动作源文件（.can3） | 推荐 | 方便后续调整动作 |

---

## 九、重要注意事项

1. **口型参数独占**：`ParamMouthOpenY` 由程序实时驱动口型同步，动作和表情中**都不要**对此参数设置关键帧
2. **表情命名必须匹配**：表情名称必须是 `F01`、`F02`... 格式，程序按此名称调用
3. **动作组命名必须匹配**：待机组必须叫 `Idle`，语义动作组必须叫 `TapBody`
4. **TapBody 的顺序很重要**：程序按数组下标索引动作，请务必按对照表排列顺序
5. **动作不宜过长**：对话场景中动作切换频繁，单个动作建议 2~4 秒
6. **Groups 配置不能遗漏**：`EyeBlink` 和 `LipSync` 两个 Group 必须在 model3.json 中正确声明

---

### Live2D模型制作要求（简化版） (`docs/Live2D模型制作要求（简化版）.md`)

# Live2D 模型制作要求（简化版）

本文档用于指导外包制作 Live2D 数字人模型。本版为最小可用版本，仅满足基础对话场景。

---

## 一、技术规格

| 项目             | 要求                                                  |
| -------------- | --------------------------------------------------- |
| Cubism 版本      | **Cubism 5**                                        |
| model3.json 版本 | Version: 3                                          |
| 贴图尺寸           | 2048×2048                                           |
| 渲染目标           | 浏览器端 WebGL                                          |
| 交付格式           | 完整 Cubism 导出目录（.moc3、贴图、motion3.json、physics3.json） |

---

## 二、必需参数（Parameters）

参数 ID 请严格使用 Cubism 默认命名。

| 参数 ID             | 用途   | 说明               |
| ----------------- | ---- | ---------------- |
| `ParamMouthOpenY` | 嘴巴张合 | 程序实时驱动口型，**最关键** |
| `ParamEyeLOpen`   | 左眼开合 | 程序驱动自动眨眼         |
| `ParamEyeROpen`   | 右眼开合 | 程序驱动自动眨眼         |
| `ParamAngleX`     | 头部左右 | 呼吸动效 + 视线跟随      |
| `ParamAngleY`     | 头部上下 | 同上               |
| `ParamAngleZ`     | 头部倾斜 | 同上               |
| `ParamBodyAngleX` | 身体摇摆 | 呼吸动效             |
| `ParamEyeBallX`   | 眼球水平 | 视线跟随             |
| `ParamEyeBallY`   | 眼球垂直 | 视线跟随             |

---

## 三、口型同步（LipSync）要求

程序会通过 `ParamMouthOpenY` 参数实时驱动嘴巴开合，模拟说话口型。该参数的值范围是 **0（闭嘴）~ 1（张嘴最大）**。

### 对嘴部建模的要求

- **`ParamMouthOpenY` = 0 时**：嘴巴完全闭合，自然状态
- **`ParamMouthOpenY` = 1 时**：嘴巴张到最大
- 中间值（0.2~0.8）应有平滑的过渡，不能出现跳变或形变异常
- 嘴部形变需要在 0~1 全范围内看起来自然，因为程序会输出各种中间值

### 建模细节建议

程序内部会产生以下几种典型开合度，请确保这些值下嘴型看起来合理：

| 开合度范围 | 对应口型场景 |
| --- | --- |
| 0.0 | 静音 / 闭嘴 |
| 0.2~0.3 | 轻微张嘴（双唇音 p/b、唇齿音 f） |
| 0.4~0.5 | 中度张嘴（齿音 d/t、舌音 ch/s） |
| 0.6~0.7 | 较大张嘴（元音 e、圆唇音 o） |
| 0.8~0.9 | 大张嘴（低元音 a、圆唇突出 ou） |

### 重要约束

- 动作（motion）和表情（expression）中**都不要**对 `ParamMouthOpenY` 设置关键帧，否则会与程序的实时口型驱动冲突
- 如果嘴部有多个参数（如 `ParamMouthForm` 控制嘴型宽窄），可以保留，但 `ParamMouthOpenY` 必须留给程序独占

---

## 四、动作（Motions）要求

不需要表情文件，不需要情绪系统。只需要以下动作：

### 动作组结构

| 动作组名称     | 用途             |
| --------- | -------------- |
| `Idle`    | 待机，无对话时循环播放    |
| `TapBody` | 对话动作，程序按数组下标调用 |

### Idle 组（至少 1 个）

自然站立的待机循环动画，有轻微呼吸感。

### TapBody 组（共 5 个动作）

程序按 **数组下标（从 0 开始）** 调用，顺序必须严格按下表排列：

| 下标  | 动作名称 | 动作描述                   | 使用场景        |
| --- | ---- | ---------------------- | ----------- |
| 0   | 聆听   | 微微前倾、略点头，表现出认真听对方说话的姿态 | 用户正在输入/说话时  |
| 1   | 说话   | 自然的说话姿态，可有轻微手势，身体略有律动  | AI 回复时的默认动作 |
| 2   | 思考   | 手托下巴或目光微偏上方，表现在想问题     | AI 正在生成回复时  |
| 3   | 等待   | 双手自然交叠或轻微晃动，耐心等待的姿态    | 空闲等待用户操作时   |
| 4   | 打招呼  | 挥手或点头致意                | 对话开始时的问候    |

### 动作制作要求

- `FadeInTime` 和 `FadeOutTime` 设为 `0.5` 秒
- 动作时长 **2~4 秒**
- 动作结束后能自然过渡回待机姿态
- **动作中不要包含 `ParamMouthOpenY` 的关键帧**，口型由程序实时驱动，写入会冲突

---

## 五、model3.json 结构要求

以 `MyModel` 为例：

```json
{
  "Version": 3,
  "FileReferences": {
    "Moc": "MyModel.moc3",
    "Textures": ["MyModel.2048/texture_00.png"],
    "Physics": "MyModel.physics3.json",
    "Motions": {
      "Idle": [
        { "File": "motions/idle.motion3.json", "FadeInTime": 0.5, "FadeOutTime": 0.5 }
      ],
      "TapBody": [
        { "File": "motions/listen.motion3.json", "FadeInTime": 0.5, "FadeOutTime": 0.5 },
        { "File": "motions/speak.motion3.json", "FadeInTime": 0.5, "FadeOutTime": 0.5 },
        { "File": "motions/think.motion3.json", "FadeInTime": 0.5, "FadeOutTime": 0.5 },
        { "File": "motions/wait.motion3.json", "FadeInTime": 0.5, "FadeOutTime": 0.5 },
        { "File": "motions/greet.motion3.json", "FadeInTime": 0.5, "FadeOutTime": 0.5 }
      ]
    }
  },
  "Groups": [
    {
      "Target": "Parameter",
      "Name": "EyeBlink",
      "Ids": ["ParamEyeLOpen", "ParamEyeROpen"]
    },
    {
      "Target": "Parameter",
      "Name": "LipSync",
      "Ids": ["ParamMouthOpenY"]
    }
  ],
  "HitAreas": [
    { "Id": "HitArea", "Name": "Body" }
  ]
}
```

**关键点：**

- `Groups` 中 `EyeBlink` 和 `LipSync` 必须声明，程序依赖这两个组实现自动眨眼和口型同步
- `TapBody` 数组中的顺序必须是：聆听、说话、思考、等待、打招呼

---

## 六、目录结构

```
MyModel/
├── MyModel.model3.json
├── MyModel.moc3
├── MyModel.physics3.json
├── MyModel.2048/
│   └── texture_00.png
└── motions/
    ├── idle.motion3.json
    ├── listen.motion3.json      # 下标 0 - 聆听
    ├── speak.motion3.json       # 下标 1 - 说话
    ├── think.motion3.json       # 下标 2 - 思考
    ├── wait.motion3.json        # 下标 3 - 等待
    └── greet.motion3.json       # 下标 4 - 打招呼
```

---

## 七、交付清单

| 交付物                        | 必需  |
| -------------------------- | --- |
| 完整模型导出目录（上述所有文件）           | 是   |
| Cubism Editor 工程源文件（.cmo3） | 推荐  |
| 动作源文件（.can3）               | 推荐  |

---

## 八、注意事项

1. **`ParamMouthOpenY` 不要动**：动作中不要对嘴巴参数设关键帧，程序实时驱动口型
2. **动作组必须叫 `Idle` 和 `TapBody`**：程序按这两个名字查找
3. **TapBody 顺序不能变**：程序按下标 0~4 调用，顺序错了动作就对不上
4. **`EyeBlink` 和 `LipSync` Groups 不能漏**：没有这两个声明，自动眨眼和口型同步不会生效

---

### MCP外部调用接口 (`docs/MCP外部调用接口.md`)

# MCP 工具外部调用接口

本文整理 Fay 内置 MCP 管理服务（默认端口 5010）的外部调用接口，覆盖 MCP 工具调用与预启用工具调用。

## 基础信息

- Base URL: `http://127.0.0.1:5010`
- Content-Type: `application/json`

## MCP 工具调用接口

### 调用指定服务器工具

`POST /api/mcp/servers/{server_id}/call`

请求体示例：
```json
{
  "method": "tool_name",
  "params": {
    "key": "value"
  },
  "is_prestart": false
}
```

说明：
- `is_prestart=true` 会跳过工具启用状态检查（用于预启用调用）。

### 调用工具（自动选择在线服务器）

`POST /api/mcp/tools/{tool_name}`

请求体示例：
```json
{
  "key": "value"
}
```

### 获取指定服务器工具列表

`GET /api/mcp/servers/{server_id}/tools`

### 获取在线服务器工具列表（聚合）

`GET /api/mcp/servers/online/tools`

## 预启用工具调用接口

### 配置预启用工具

`POST /api/mcp/servers/{server_id}/tools/{tool_name}/prestart`

请求体示例：
```json
{
  "enabled": true,
  "params": {
    "query": "{{question}}"
  },
  "include_history": true,
  "allow_function_call": false
}
```

### 获取可运行的预启用工具列表

`GET /api/mcp/prestart/runnable`

### 调用单个预启用工具（自动连接服务器）

`POST /api/mcp/servers/{server_id}/prestart/{tool_name}/call`

请求体示例：
```json
{
  "params": {
    "query": "{{question}}"
  },
  "question": "用户问题",
  "keep_connection": true
}
```

### 批量调用所有预启用工具（自动连接服务器）

`POST /api/mcp/prestart/call`

请求体示例：
```json
{
  "question": "用户问题",
  "keep_connection": true,
  "server_ids": [1, 2],
  "tool_names": ["tool_a", "tool_b"]
}
```

说明：
- `question` 必填，用于替换 `params` 中的 `{{question}}` 占位符。
- `server_ids` 与 `tool_names` 为空时表示对全部预启用工具生效。

---

### Prompt设计文档 (`docs/Prompt设计文档.md`)

# Fay Prompt 设计文档

> **提示**：使用 AI IDE 对 Fay 进行 Prompt 改造时，可以让 AI 参考 `docs/Prompt设计文档.md`

本文档说明 Fay 数字人系统中各类 Prompt 的设计与大小模型协作机制。

## 1. 架构总览

Fay 采用 **闲聊判断器 + 工具循环 + 最终回复** 的三段式架构。根据是否配置大模型，自动在双模型模式和单模型模式之间切换：

| 模式 | 触发条件 | 闲聊判断器 | 工具循环 | 最终回复 |
|---|---|---|---|---|
| **双模型模式** | `system.conf` 配置了 `big_model_engine` | 小模型（流式） | 大模型（后台线程） | 小模型（流式） |
| **单模型模式** | `big_model_engine` 留空 | 小模型（流式） | 小模型（同步） | 小模型（流式） |

> 所有面向用户的文本始终由小模型流式输出。大模型只在双模型模式的工具循环中工作，从不直接面向用户。

### 1.1 Prompt 类型与功能

| Prompt 类型 | 功能定位 | 输出格式 | 执行模型 | 触发时机 |
|---|---|---|---|---|
| **系统 Prompt** | 定义 AI 人设和行为规范 | 自然语言 | - | 贯穿整个对话流程 |
| **闲聊判断器 Prompt** | 判断是闲聊（finish）还是需要工具（tool） | 严格 JSON | 小模型 | 每次 `question()` 调用 |
| **工具循环 Prompt** | 决策下一步：调哪个工具 / 是否完成 | 严格 JSON | 大模型（双）/ 小模型（单） | 工具循环的每一步 |
| **最终回复 Prompt** | 基于工具结果生成自然语言回复 | 口语化文本 | 小模型 | 工具循环完成后 |
| **意图分类 Prompt** | 后台任务运行中判断用户新消息意图 | 标签字符串 | 小模型 | 后台任务运行中用户又发消息 |
| **记忆重要性 Prompt** | 评估对话内容的记忆价值 | 0-10 数值 | 小模型 | 每轮对话后异步 |
| **用户画像 Prompt** | 从对话中提取用户特征 | 结构化文本 | 小模型 | 每天 11:30 定时 |
| **记忆反思 Prompt** | 对历史记忆进行归纳总结 | 高层次洞察 | 小模型 | 每天 23:00 定时 |

### 1.2 调用次数对比

| 场景 | 双模型模式 | 单模型模式 |
|---|---|---|
| 闲聊"你好" | 小模型 × 1 | 小模型 × 1 |
| 工具请求"查课程 A" | 小模型 × 2（判断 + 最终回复）+ 大模型 × N（工具循环） | 小模型 × (2 + N)（判断 + 工具循环 + 最终回复） |
| 后台任务运行中"进度?" | 小模型 × 1（意图分类） | 同左 |

---

## 2. 工作流程

### 2.1 整体流程

```
用户输入（文字/语音）
    ↓
预处理：加载系统 Prompt、检索关联记忆、执行预启动工具、加载对话历史、加载 MCP 工具
    ↓
┌─────────────────────────────────────────────────────────┐
│  检查 ExecutionManager 状态                              │
│  ├─ 情况1：有已完成的后台结果 -> 小模型基于结果生成最终回复  │
│  ├─ 情况2：有 RUNNING 后台任务 -> 小模型做意图分类         │
│  └─ 情况3：无后台任务 -> 走正常流程                       │
└─────────────────────────────────────────────────────────┘
    ↓ (情况3)
┌─────────────────────────────────────────────────────────┐
│  闲聊判断器（小模型，流式 JSON）                          │
│  ├─ {"action": "finish", "message": "..."}  → 流式输出   │
│  └─ {"action": "tool", "keyword": "..."}    → 提交工具执行 │
└─────────────────────────────────────────────────────────┘
    ↓ (action == "tool")
┌─────────────────────────────────────────────────────────┐
│  工具循环（双模型：大模型后台 / 单模型：小模型同步）       │
│  while tool_results < max_steps:                         │
│    1. 执行工具 → spec.executor(args)                     │
│    2. 决策下一步 → LLM.invoke(next_step_messages)        │
│    3. 解析 JSON：tool → 继续 / finish → 跳出              │
└─────────────────────────────────────────────────────────┘
    ↓
┌─────────────────────────────────────────────────────────┐
│  最终回复（小模型，流式）                                 │
│  基于工具结果生成自然语言回复，写入原始会话流              │
└─────────────────────────────────────────────────────────┘
    ↓
异步后处理：记忆重要性评估、对话记忆存储
    ↓
定时任务：用户画像分析（11:30）、每日反思（23:00）
```

### 2.2 关键文件:行号

| 功能 | 位置 |
|---|---|
| 配置加载 | `utils/config_util.py:543-563` |
| 单模型模式判断 | `llm/nlp_cognitive_stream.py` `_is_single_model_mode()` |
| 模型实例工厂 | `llm/execution_manager.py:172-205` `_get_llm_instance(role, streaming)` |
| 工具循环入口 | `llm/execution_manager.py:415` `_big_model_execute(state, llm_role)` |
| `question()` 主入口 | `llm/nlp_cognitive_stream.py` `question()` |
| 闲聊判断器 | `llm/nlp_cognitive_stream.py` `_call_judge_llm()` |
| 闲聊判断器 Prompt 构建 | `llm/nlp_cognitive_stream.py` `_build_judge_messages()` |
| 工具循环 Prompt 构建 | `llm/execution_manager.py:226-330` `_build_execution_next_step_messages()` |
| 最终回复 Prompt 构建 | `llm/nlp_cognitive_stream.py` `_build_final_messages()` |
| 后台完成自动回复 | `llm/nlp_cognitive_stream.py` `_auto_reply_after_execution()` |
| 单模型同步分支 | `llm/nlp_cognitive_stream.py` `_submit_tool_execution()` 内 |

---

## 3. Prompt 示例

### 3.1 系统 Prompt 示例

系统 Prompt 由用户在配置文件中定义（GUI 可以管理），用于设定 AI 的人设：

```
**系统设定**
你是一个名叫 Feifei 的数字人助手。你性格温柔、善解人意，说话简洁明了。
你会用亲切的语气与用户交流。
---
```

### 3.2 闲聊判断器 Prompt

**System Message 核心**（`_build_judge_messages`）：

```
你是一个闲聊判断器。判断用户的话是不是闲聊，请严格输出合法 JSON，不要输出其他内容。

输出格式只有两种：
1. 是闲聊: {"action": "finish", "message": "你的回复内容"}
2. 不是闲聊: {"action": "tool", "keyword": "提取的搜索关键词"}

什么是闲聊（输出 finish）：
- 打招呼：你好、hi、早上好
- 情绪表达：我好开心、今天好累
- 感谢道别：谢谢、再见、拜拜
- 简单确认：好的、收到、明白了

什么不是闲聊（输出 tool）：
- 问任何具体事物/概念：XX是什么、你知道XX吗
- 要求查询/获取/阅读内容
- 提到任何产品名、项目名、专有名词
- 任何你需要查资料才能准确回答的问题

不确定时 -> 输出 tool
```

**附加内容**：系统 Prompt + 关联记忆 + 可用工具名 + 知识库主题 + 预启动工具结果 + 历史 + 对话历史

**输出示例**：

```json
{"action": "finish", "message": "你好呀！有什么我可以帮你的吗？"}
```

```json
{"action": "tool", "keyword": "课程A"}
```

### 3.3 工具循环 Prompt

**System Message 核心**（`_build_execution_next_step_messages`）：

```
你是一个任务执行器，负责逐步调用工具完成用户请求。请严格输出合法 JSON。

输出格式只有两种：
1. 还需要调用工具: {"action": "tool", "tool": "工具名", "args": {...}}
2. 任务已完成: {"action": "finish", "message": "简短总结执行结果"}

判断规则：
- 对照用户的原始请求，检查是否所有要求都已满足
- 【严格遵守】调用前先看【已完成的工具调用】列表，凡是已经成功执行过的"同名+同参数"调用，绝对不要再调一次

搜索策略：
- 如果 kb_search 的结果不够相关，换关键词重试
- 可以用 kb_list_sources 查看所有课程列表
```

**附加内容**：人设 + 关联记忆 + 观察 + 预启动工具结果 + 已完成工具摘要 + 对话历史

**输出示例**：

```json
{"action": "tool", "tool": "kb_search", "args": {"query": "课程A"}}
```

```json
{"action": "finish", "message": "已找到课程A的内容：..."}
```

### 3.4 最终回复 Prompt

**System Message 核心**（`_auto_reply_after_execution` 内的精简 prompt）：

```
你是一个友好的助手。你已经告诉用户"我来帮你查一下，稍等…"，现在工具执行完毕。
请基于以下工具执行结果回答用户，不要再重复"我来查一下"之类的过渡语，直接给出答案。

**用户消息**: {user_request}
**实际执行的操作**: {tool_call_details}
**工具执行结果**: {tool_context}

请基于工具结果直接回答，用日常口语，不要说'工具调用'等技术术语。回复简洁，3-5句话即可。
```

**输出示例**：

```
课程A的内容是……（基于工具结果的口语化总结）
```

---

## 4. 单模型模式 vs 双模型模式

### 4.1 单模型模式（`big_model_engine` 留空）

```
用户输入
    ↓
闲聊判断器（小模型，流式）→ finish 直接回复
    ↓ (tool)
同步工具循环（小模型，最多 30 步）
    ↓
最终回复（小模型，流式）
```

**特点**：
- 不开后台线程，工具循环在 `question()` 调用栈内同步执行
- 用户会等待工具循环完成才看到最终回复
- 不走 `ExecutionManager` 状态机，无 `on_complete` 回调
- 适合只有一个模型实例的部署（如本地小模型）

### 4.2 双模型模式（配置了 `big_model_engine`）

```
用户输入
    ↓
闲聊判断器（小模型，流式）→ finish 直接回复
    ↓ (tool)
exec_mgr.submit() → 后台线程启动
    ↓
question() 立即返回 ""（前端通过 stream 接收异步回复）
    ↓ (后台线程)
工具循环（大模型，最多 30 步）
    ↓
on_complete 回调 → _auto_reply_after_execution
    ↓
最终回复（小模型，流式，写入原始会话流）
```

**特点**：
- 工具循环在后台线程执行，不阻塞 `question()`
- 大模型负责工具决策，小模型负责最终回复
- 每用户最多 1 个 RUNNING 任务（`ExecutionManager.submit` 拒绝重复）
- 后台任务运行中用户又发消息 → 意图分类（5 类：update_task / query_progress / cancel_task / new_task / normal_chat）

---

## 5. 特殊标签处理

### 5.1 `<prestart>` 标签

| 属性 | 说明 | 在 Prompt 中的表现 |
|---|---|---|
| 无属性 | 临时预启动结果 | 仅在当前轮对话的 Prompt 中出现，下一轮自动移除 |
| `keep="true"` | 持久预启动结果 | 保留在对话历史中，后续轮次的最近对话模块中持续可见 |

### 5.2 `<think>` 标签

| 场景 | 说明 | 处理逻辑 |
|---|---|---|
| 工具执行日志 | 包裹工具调用过程（工具名、参数、结果摘要、耗时） | 由 `_auto_reply_after_execution` 生成，写入流让用户看到执行过程 |
| 模型思考内容 | 兼容带思考标签的模型（如 DeepSeek R1） | 解析 JSON 前自动移除 |

---

## 6. 最佳实践

### 6.1 系统 Prompt 编写建议

- 明确定义 AI 的名称、性格特点
- 设定说话风格和语气
- 指定对用户的称呼方式
- 避免过长，控制在 200 字以内

### 6.2 工具设计建议

- 工具描述要清晰，让 LLM 能准确判断何时使用
- 参数说明要完整，包含类型和示例
- 返回结果要简洁，便于 LLM 理解和转述

### 6.3 预启动工具使用建议

- 用于获取实时上下文（如时间、天气、用户状态）
- 配置 `include_history: true` 可让结果在多轮对话中持续可见
- 避免预启动工具执行时间过长，影响响应速度

### 6.4 单模型 vs 双模型选择

- **单模型模式**：本地部署小模型、低成本场景、不需要复杂工具循环
- **双模型模式**：云服务部署、需要强工具调用能力（如 MiniMax-M2.7、gpt-4o）、希望工具循环不阻塞用户

---

### Fay Vs Harness Memory (`docs/fay_vs_harness_memory.md`)

# Fay 与 Harness Agent 的记忆机制对比

> 写作时间：2026-04-27。"Harness agent" 指 Anthropic Claude Code 这一类"为开发者准备的通用代理脚手架"，下文以 Claude Code 为代表。**Fay 是为数字人场景设计的开源 agent 框架——它的记忆机制不是给开发者用的工程辅助工具，而是支撑一个长期陪伴主体的认知底座。**

**核心结论先放上：**
- Harness agent 把"开发者协作记忆"做透了，Fay 学了它的元数据组织、来源归因、规则通道、自描述这套"记忆治理"经验；
- 但**数字人场景的诉求与 IDE 场景根本不同**——长期陪伴、多用户隔离、跨会话语义检索、自动反思、自动画像、多模态状态、跨领域外部协作；
- 这些诉求 harness agent 并不打算解决，**Fay 才是数字人场景下记忆机制的正确形态**。

---

## 一、Fay 从 harness agent 学到了什么

Fay 的记忆底座来自斯坦福 Generative Agents 的 memory_stream：三种节点 + 三因子打分 + 反思机制。但 Claude Code 这一年的演进里把"长期记忆怎么对外协作"这件事打磨得很到位，Fay 这次记忆改造里就直接搬了几条它的设计经验：

### 1.1 用前缀命名空间组织 tag，而不是新增枚举字段

Claude Code 的 feedback 记忆文件里，每条记忆带 YAML frontmatter——`name / description / type: feedback / originSessionId`。它没有为"feedback 是不是规则、是不是偏好、属于哪个项目"再单独建字段，而是用"key：value"风格把维度收敛在元数据里。

Fay 这次没有给 ConceptNode 加 `is_rule / is_persistent / domain` 一堆布尔字段，而是加了一个 `tags: list[str]`，用 `kind:rule / persistent:true / domain:quant` 这种命名空间式的前缀承载所有维度。新增维度不改 schema，反思继承 tag 也是按前缀过滤——这套思路直接对标了 harness 的做法。

### 1.2 来源归因（source attribution）

Claude Code 的每条 feedback 都记 `originSessionId`，让"这条规则是哪次对话留下的"可追溯。Fay 这次在 tag 里固定预留了 `source:` 命名空间——`source:claude_code / source:cursor / source:fay_self / source:fay_reflection / source:user`。同一条记忆在反思、检索、调试时都能立刻看出来源，不会出现"这条规则是谁写的"这种盲区。

### 1.3 persistent 标志与"长期规则"通道

Claude Code 的 MEMORY.md 是 auto-loaded 的——每次会话开始自动注入。这种"无条件加载"的语义让"长期约束"和"具体事件"在物理上分开。

Fay 借鉴了这个区分：tag 里专门有 `persistent:true`，并给 service 层加了 `get_active_rules()` 函数，专门返回 `kind:rule + persistent:true` 的节点。外部 agent 在每次开新任务前调一下，就拿到一份等效于 MEMORY.md 的"必须遵守的清单"。

### 1.4 schema 自描述工具

Claude Code 的 memory 系统有一个隐含但关键的体验：约定是公开的、可发现的。第三方工具看到一个 feedback 文件就知道它的字段是什么含义。

Fay 把这条做成了显式接口——`memory_get_schema()` MCP 工具会返回 `kind` 枚举与 tag 命名空间约定。任何外部 agent（Claude Code、Cursor、自定义工具）在不熟悉参数时调用一次，就能拿到"该填什么、有什么约束"的完整说明。这是 Fay 不让外部 agent 乱写 tag 的主要保险。

### 1.5 append-only + 周期性 consolidation

Claude Code 的实践：会话过程中一直 append（消息日志、TodoWrite、feedback 文件），到容量上限时由 compaction 把历史压缩成 summary 注入下一段。

Fay 这次确认了同样的节奏：写入路径只 append 不修改（4 条写入路径都走 `append_prepared_node`），每天 23:00 由 `perform_daily_reflection` 把零散事件 consolidate 成 reflection 节点。reflection 节点继承业务 tag 但覆盖打 `kind:insight + source:fay_reflection`，相当于一次结构化的 compaction。

### 1.6 index/detail 分层

Claude Code 的 MEMORY.md 是个 index，里面是指向 `feedback_xxx.md` 等详细文件的链接，主索引短，详情分文件。

Fay 在用户档案上对应同样的模式：`T_Member.user_portrait` 是简短画像（1000 字以内的 LLM 总结），而 `memory_stream` 里 nodes.json 是完整明细。检索时先看 portrait 拿"这个人是谁"，再用语义检索拿具体事件。

---

## 二、数字人场景下，Fay 是更合适的形态

**这一节是本文的重点。** Harness agent 的目标是辅助开发者写代码——它面对的是一个键盘前的工程师、一个仓库、一段会话；记忆机制只要让"上下文不丢"就够了。

Fay 的目标完全不同：承载一个**会陪伴用户多年、会跨多种场景工作、会随时间成长**的数字人主体。它面对的是一个老人、一个家庭、一个学生、一支量化团队——记忆要做的不是"帮 agent 别忘事"，而是**让数字人成为一个有人格、有关系、有时间纵深的存在**。

下面这张对比表，**第三列是关键**——它直接说明每一项设计差异背后，数字人场景为什么必须选 Fay 这条路：

| 维度 | Harness Agent（如 Claude Code） | Fay | 数字人场景为什么需要 Fay 的设计 |
|---|---|---|---|
| **存储介质** | 主要是文件（CLAUDE.md / MEMORY.md / feedback md） | sqlite（用户档案）+ JSON（节点流）+ embedding（向量） | 数字人要支持多用户、按用户隔离、跨会话语义检索，纯文件不够 |
| **节点类型** | 单一文件粒度，无类型语义 | observation / conversation / reflection 三种 | 对话、观察、反思在数字人交互里需要分别召回（比如检索时按"近期发生过什么"vs"我们聊过什么"分段） |
| **检索方式** | 全量文件加载到 prompt（命中即注入） | 三因子打分（recency + relevance + importance）+ tag 过滤 | 数字人的对话会话短、节点数量多，必须按相关度+时间打分挑出 top-N，否则 prompt 直接撑爆 |
| **重要度评估** | 无，由人/agent 自己取舍内容 | LLM 在写入时自动给 0–100 重要度分数 | 数字人写入是高频自动行为（每句话/每次观察都写），必须有自动评分才能在检索时分层 |
| **时间维度** | 文件 mtime（粗粒度） | 每个节点带 `datetime` + `created` time_step + `last_retrieved`，retrieve 用时间衰减打分 | 陪伴型数字人需要回答"上周我们聊过什么"、"昨天血压是多少"，时间是一等公民 |
| **反思机制** | 无内建反思（用户主动整理 MEMORY.md） | 每天 23:00 LLM 自动反思，把零散事件提炼成 insight 节点，自动继承业务 tag | 数字人作为长期陪伴对象，需要主动从昨日发现规律（"用户连续 3 天血压偏高"），不能等用户来整理 |
| **画像生成** | 无 | 每天 22:35 LLM 重写 `T_Member.user_portrait`，含"与 Fay 的关系"维度（亲密度、情感基调、共同经历） | 数字人需要稳定人格 + 关系记忆，让"昨天对你说过的话"塑造"今天的语气" |
| **多用户隔离** | 单一用户视角 | `memory.isolate_by_user=true` 时按用户名分目录，T_Member 表按 username 索引 | 数字人产品经常一对多（养老院、家教平台、客服），必须支持每个用户独立记忆 |
| **写入并发模型** | 串行（agent 顺序写文件） | 锁外算 importance/embedding，锁内 append；落盘异步 | 对话延迟敏感，不能因为 LLM 评分卡住就让下一句话排队（曾经因此挂死过 5 小时） |
| **多模态状态** | 纯文本 | 与 voice / avatar / live2d / 设备状态联动（scratch 角色卡 + observation 节点） | 数字人是有"身体"的——表情、语音、设备事件都是记忆材料 |
| **实时对话注入** | 一次性加载，不每轮变化 | 每轮对话 retrieve 30 条候选 → 按 type 分三段拼进 system prompt | 数字人对话每一轮上下文都不同，需要动态更新"该想起来什么" |
| **外部 agent 协作写入** | 主要是被 harness 写，没有标准化外部写入接口 | core.memory_service + 7 个 MCP 工具，外部 agent（Claude Code/Cursor）可以反向回写 | 数字人是被多个工具/服务共同塑造的——量化策略写交易事件、监测脚本写血压、家教 agent 写学员进度 |
| **schema 自描述** | 约定式（看示例就懂） | `memory_get_schema()` MCP 工具显式返回 kind 枚举与命名空间 | 数字人对接的外部 agent 异质化高，必须能让外部"运行时发现规约" |
| **场景普适性** | 紧贴 IDE / coding 场景 | 量化交易、居家养老、教育辅导、生活助理、智能家居……同一套机制 | 数字人的产品形态本身就是跨领域的，记忆机制必须场景无关 |

### 2.1 一个具象场景：harness agent 撑不起，Fay 撑得起

想象一个真实数字人产品——居家养老陪护机器人 "小慧"，部署在一个家庭里 365 天。

| 时间 | 发生的事 | 用 harness agent 的记忆能不能扛？ | Fay 怎么扛？ |
|---|---|---|---|
| Day 1 上午 | 老人第一次和小慧聊天，告诉她"我不喜欢被叫'老人家'，叫我老张" | ❌ 没有 user 概念，下次会话不会记得 | conversation 节点 + 偏好节点 `kind:preference, persistent:true`，写入 `T_Member.user_portrait` |
| Day 1 下午 | 血压计自动上传：158/95 | ❌ 不是文本对话，无写入接口 | MCP 工具 `memory_remember(kind=event, source=elderly_monitor, domain:homecare)` 直接写入 |
| Day 1 晚 23:00 | 系统主动反思 | ❌ 不会主动反思 | `perform_daily_reflection` 跑一次，提炼"老张今天血压偏高"insight |
| Day 7 早晨 | 老张说"最近怎么有点累" | ❌ 没有时间维度的检索 | retrieve 带 recency 权重 + tag 过滤，捞到这周 3 条血压偏高的 event 与 1 条反思 insight |
| Day 30 | 家属在 app 设规则"每天早上必须测血压" | ❌ 没有外部 agent 写入通道 | 家属 agent 通过 MCP 调 `memory_remember(kind=rule, persistent=true)`，小慧每天自动 `get_active_rules()` 读到 |
| Day 90 | 小慧和老张已经熟络 | ❌ 没有人格演化 | 22:35 画像分析持续重写 `user_portrait`，含"与 Fay 的关系（亲密度、称呼偏好、共同经历）"维度 |

这是 harness agent 与 Fay 的根本分野——**前者活在一次会话里，后者活在一段关系里。**

### 2.2 一句话定位

> **Harness agent 的记忆，是为了让 agent 不忘事；Fay 的记忆，是为了让数字人成为一个"人"。**

---

## 三、两个仓库

| 项目 | 仓库地址 | 简介 |
|---|---|---|
| **Fay** | https://gitee.com/xszyou/fay | 开源数字人 agent 框架。本文记忆机制的全部代码都在这个仓库里。 |
| **Claude Code（harness agent）** | https://github.com/anthropics/claude-code | Anthropic 官方为开发者打造的通用代理脚手架，本文的"harness agent"参照对象。 |

> 想动手验证：克隆 Fay，启动 `faymcp/mcp_server.py`，用 Claude Code 通过 SSE 连上端口 8765，就能体会"外部 harness agent 通过 MCP 把记忆回写到 Fay"这条新通道。
>
> **两个仓库各司其职**：Claude Code 让开发者更高效地写代码，Fay 让你的数字人活得更长、更真。

---

### Memory Mechanism (`docs/memory_mechanism.md`)

# Fay 记忆机制总览（2026-04-27）

> 本文档描述 Fay 当前默认的认知记忆机制。涉及代码：
> `llm/nlp_cognitive_stream.py` + `genagents/modules/memory_stream.py`
> + `core/memory_service.py` + `core/member_db.py` + `faymcp/mcp_server.py`。
>
> 旧版精简说明见 [`memory_module.md`](./memory_module.md)，本文为增补与扩展。

---

## 一、三层存储

| 存储 | 路径 | 内容 | 隔离方式 |
|---|---|---|---|
| **memory_stream**（核心） | `memory/[<username>/]memory_stream/nodes.json` + `embeddings.json` | 所有事件、对话、反思节点 | `config.memory.isolate_by_user=true` 时按 username 分目录，否则全局共享 |
| **scratch**（人物属性） | 内存里的 `agent.scratch` dict，启动时从 `config.json` 的 `attribute` 段加载 | 姓名、年龄、性别、性格、目标等 Fay 自身的"角色卡" | 不持久化，每次启动从 config 重新装填 |
| **T_Member**（用户档案） | `memory/user_profiles.db` 的 sqlite 表 | `username`、`extra_info`（人工补充）、`user_portrait`（每天 22:35 自动生成的画像） | 一行一个用户 |

ChromaDB 仅用于本地知识库（doc 检索），不参与记忆流。

---

## 二、节点的数据结构

每个 `ConceptNode` 包含：

```
node_id, node_type, content, importance, datetime,
created (time_step), last_retrieved, pointer_id,
tags  ← 业务标签列表
```

`node_type` 只有三种：`observation` / `conversation` / `reflection`。

`tags` 走命名空间约定：

```
kind:<decision|event|fact|rule|error|insight|preference|observation>
source:<fay_self|claude_code|cursor|user|fay_reflection|...>
persistent:true              ← 长期保留标志，rule 必带
domain:<quant|homecare|education|life_assistant|home_automation|companion|...>
strategy:<策略名>  symbol:<标的>  session:<会话ID>  schedule:<表达式>  date:<YYYY-MM-DD>
```

### 2.1 节点完整事例

#### 事例 A：普通对话（conversation，无 tag）

```json
{
  "node_id": 142,
  "node_type": "conversation",
  "content": "主人：今天股市怎么样？\nFay：A股震荡走低，沪指跌 0.8%。",
  "importance": 4,
  "datetime": "2026/04/27 10:23:15",
  "created": 142,
  "last_retrieved": 142,
  "pointer_id": null,
  "tags": []
}
```

> 当前对话路径暂未打 tag，反思阶段无法从中继承业务标签。

#### 事例 B：外部 agent 写入的事件（event）

```json
{
  "node_id": 156,
  "node_type": "observation",
  "content": "上午 10:30 AAPL 突破日内高点 187.4，breakout 策略买入信号触发",
  "importance": 7,
  "datetime": "2026/04/27 10:30:42",
  "created": 156,
  "last_retrieved": 156,
  "pointer_id": null,
  "tags": [
    "domain:quant",
    "kind:event",
    "session:2026-04-27-am",
    "source:claude_code",
    "strategy:breakout",
    "symbol:AAPL"
  ]
}
```

#### 事例 C：长期规则（rule，必带 persistent:true）

```json
{
  "node_id": 88,
  "node_type": "observation",
  "content": "每小时整点检查所有持仓策略是否有未触发的止损单",
  "importance": 9,
  "datetime": "2026/04/26 09:00:00",
  "created": 88,
  "last_retrieved": 130,
  "pointer_id": null,
  "tags": [
    "domain:quant",
    "kind:rule",
    "persistent:true",
    "schedule:hourly",
    "source:user"
  ]
}
```

#### 事例 D：用户偏好（preference）

```json
{
  "node_id": 31,
  "node_type": "observation",
  "content": "用户希望晚上 22:00 之后不要主动发起对话",
  "importance": 8,
  "datetime": "2026/04/15 22:13:05",
  "created": 31,
  "last_retrieved": 105,
  "pointer_id": null,
  "tags": [
    "domain:life_assistant",
    "kind:preference",
    "persistent:true",
    "source:user"
  ]
}
```

#### 事例 E：失败事件（error）

```json
{
  "node_id": 173,
  "node_type": "observation",
  "content": "买入信号在 AAPL 涨停封板后仍触发，导致 0 成交但占用了风控额度",
  "importance": 8,
  "datetime": "2026/04/27 14:02:11",
  "created": 173,
  "last_retrieved": 173,
  "pointer_id": null,
  "tags": [
    "domain:quant",
    "kind:error",
    "session:2026-04-27-pm",
    "source:claude_code",
    "strategy:breakout",
    "symbol:AAPL"
  ]
}
```

#### 事例 F：居家养老观察（event，跨场景示例）

```json
{
  "node_id": 412,
  "node_type": "observation",
  "content": "老人晨起血压 158/95，比上周平均高 12 个点",
  "importance": 7,
  "datetime": "2026/04/27 07:15:00",
  "created": 412,
  "last_retrieved": 412,
  "pointer_id": null,
  "tags": [
    "domain:homecare",
    "kind:event",
    "source:elderly_monitor",
    "date:2026-04-27"
  ]
}
```

#### 事例 G：Fay 自动生成的反思（reflection，继承+追加 tag）

```json
{
  "node_id": 201,
  "node_type": "reflection",
  "content": "用户在 AAPL 上的 breakout 策略本周 3 次假突破亏损，可能需要加 ATR 滤波或限制涨停后入场",
  "importance": 8,
  "datetime": "2026/04/27 23:00:14",
  "created": 201,
  "last_retrieved": 201,
  "pointer_id": [156, 162, 173],
  "tags": [
    "domain:quant",
    "kind:insight",
    "source:fay_reflection",
    "strategy:breakout",
    "symbol:AAPL"
  ]
}
```

> 反思节点的 tag 由 `MemoryStream.reflect()` 自动从 `pointer_id` 指向的源节点继承（去掉 `session:`、`date:`、`schedule:` 前缀），再覆盖为 `kind:insight` + `source:fay_reflection`。

---

## 三、写入路径（共 4 条）

| 触发 | 函数 | 节点类型 | 是否打 tag |
|---|---|---|---|
| 用户每说一句话 | `remember_conversation_thread` | conversation | ❌ 暂未打 |
| 主动观察（API/前端调） | `record_observation` → `remember_observation_thread` | observation | ❌ 暂未打 |
| 外部 agent / Fay 自身 | `core.memory_service.remember()` | observation（默认） | ✅ 自动 normalize |
| 每晚 23:00 反思 | `perform_daily_reflection` → `MemoryStream.reflect()` | reflection | ✅ 继承源节点 tag + 自动加 `kind:insight`、`source:fay_reflection` |

写入的统一管线：
1. **锁外** 算 importance（LLM 评分 0–10）和 embedding（API 向量）
2. **持锁** `agent_lock`，把节点 append 到 `memory_stream.seq_nodes`，并写 `embeddings[content]`
3. 内部 4 条只更新内存；只有 `core.memory_service.remember` 会立即落盘
4. 每天 00:00 `save_agent_memory` 全量 dump 一次 nodes.json + embeddings.json

---

## 四、检索路径

唯一入口：`MemoryStream.retrieve(focal_points, time_step, ...)`。

打分公式：
```
score = recency_w · 衰减(last_retrieved)
      + relevance_w · cosine(query_embedding, node_embedding)
      + importance_w · normalized(importance)
```

默认权重 `[0, 1, 0.5]`（纯相关度+重要度），但**对话流**用的是 `[0.8, 0.5, 0.5]`（加重时间权重）。

新增 tag 过滤：`filter_tags_all`（AND）/ `filter_tags_any`（OR）。

**对话时拼提示词的过程**（见 `nlp_cognitive_stream.py` 2329 行附近）：
1. 用当前用户输入做 query
2. 一次 retrieve 拉 30 条候选
3. 按 `node_type` 分成三段：观察记忆 / 对话记忆 / 反思记忆，每段最多 10 条
4. 拼成 markdown，塞进系统 prompt 的 `memory_context`

---

## 五、定时任务（`init_memory_scheduler`）

| 时间 | 任务 | 作用 |
|---|---|---|
| 00:00 | `save_agent_memory` | 把内存里的 nodes/embeddings/scratch 全量落盘 |
| 11:30（注释说正式应改 22:35） | `perform_user_portrait_analysis` | LLM 读最近对话，更新 `T_Member.user_portrait`（含"与 Fay 的关系"维度） |
| 23:00 | `perform_daily_reflection` | 抽取热门主题做反思，生成 reflection 节点 |

启动时还会：
- `precheck_embedding_dimensions` — 修复维度不一致的旧 embedding
- `create_agent` 默认 username 创建主 agent

---

## 六、对外接口

| 调用方 | 接口 | 用途 |
|---|---|---|
| Fay 内部对话流 | 直接调 `agent.memory_stream` + `remember_*_thread` | 高频路径，绕过 service 层 |
| Flask `/api/observation` 等 | `record_observation` | 外部 HTTP 写入观察 |
| **MCP** | `faymcp/mcp_server.py`（SSE，端口 8765） | 暴露 7 个 `memory_*` 工具，进程内直调 `core.memory_service` |
| `core.memory_service` | 7 个函数：`remember / search / get_recent / get_active_rules / get_reflections / get_user_profile / get_schema` | 唯一权威 API，统一 tag 规范 + 立即落盘 |

MCP 的 7 个工具：
- `memory_remember` — 写入（含 kind 枚举 + persistent + extra_tags）
- `memory_search` — 语义检索 + tag 过滤
- `memory_get_recent` — 时间倒序最近 N 条
- `memory_get_active_rules` — 所有 `kind:rule + persistent:true`
- `memory_get_reflections` — 最近反思
- `memory_get_user_profile` — portrait + extra_info
- `memory_get_schema` — kind 枚举与 tag 命名空间，外部 agent 拿来对齐参数

---

## 七、典型流程示例

### 7.1 内部场景（Fay 主进程内）

#### 例 1：普通用户聊天
```
用户："今天天气真不错"
└─→ Fay 主流程: question() 拿到内容
    ├─→ 锁外算 importance/embedding
    ├─→ remember_conversation_thread 异步写一条 conversation 节点（content="主人：...\nFay：..."）
    └─→ 当前提示词组装时调 memory_stream.retrieve(content)
         ├─→ 取回 30 条候选，分三段塞进 system prompt 的 memory_context
         └─→ LLM 据此生成回复
```
当前 conversation 节点不带 tag，反思阶段无法继承。

#### 例 2：用户主动表达偏好（理想路径）
```
用户："以后晚上 10 点之后别叫我"
└─→ Fay 听到后做出回应（写 conversation 节点）
    └─→ Fay 应该 同时调 core.memory_service.remember(
            content="用户希望晚上 22:00 之后不要主动发起对话",
            kind="preference",
            persistent=True,
            source="fay_self",
            extra_tags=["domain:life_assistant"]
        )
        └─→ 写入一条 observation 节点（事例 D）
```
> 当前 Fay 还没自动做这件事，需要后续在对话流里加"指令识别 → 调 service" 的钩子。

#### 例 3：每天 22:35 用户画像分析
```
schedule.run_pending() 触发 perform_user_portrait_analysis()
└─→ 读 T_Member.user_portrait（旧画像）
└─→ 抽取最近 N 条 conversation 节点
└─→ 调 LLM，按 6 个维度（含"与 Fay 的关系"）生成新画像，1000 字以内
└─→ 写回 T_Member.user_portrait
```

#### 例 4：每晚 23:00 反思
```
schedule.run_pending() 触发 perform_daily_reflection()
└─→ 选若干"主题锚点"（anchor）
    └─→ 对每个 anchor 调 memory_stream.reflect(anchor)
         ├─→ 内部 retrieve 出 120 条相关节点
         ├─→ LLM 提炼为 5 条 reflection 文本
         ├─→ 算 importance + embedding
         └─→ 调 _add_node 写入，tags 自动从源节点继承+覆盖
              结果如事例 G
```

---

### 7.2 外部场景（外部 agent 通过 MCP 调用）

> 外部 agent 通过 SSE 连接 `http://<fay_host>:8765/sse`，按 MCP 协议调用 `memory_*` 工具。

#### 例 5：Claude Code 开新策略任务前先拉规则 & 写 session 开始
```
[Claude Code 启动新会话]
1) call memory_get_active_rules(username="trader_zhang")
   ← 返回 [事例 C, 例 8 的 homecare 规则等]
   → Claude Code 把规则塞进自己的 system prompt

2) call memory_remember(
      content="开始 2026-04-27 上午盘量化执行任务",
      kind="event",
      source="claude_code",
      extra_tags=["domain:quant", "session:2026-04-27-am"],
      username="trader_zhang"
   )
   ← {"ok": true, "node_id": 155, ...}
```

#### 例 6：执行过程中实时回写事件
```
[策略触发 → Claude Code 调 broker API → 成交]
3) call memory_remember(
      content="上午 10:30 AAPL 突破日内高点 187.4，breakout 策略买入信号触发",
      kind="event",
      source="claude_code",
      extra_tags=["domain:quant", "strategy:breakout", "symbol:AAPL", "session:2026-04-27-am"],
      username="trader_zhang"
   )
   → 节点 156 落地（事例 B）

[发现策略 bug]
4) call memory_remember(
      content="买入信号在 AAPL 涨停封板后仍触发，导致 0 成交但占用了风控额度",
      kind="error",
      source="claude_code",
      extra_tags=["domain:quant", "strategy:breakout", "symbol:AAPL", "session:2026-04-27-pm"],
      username="trader_zhang"
   )
   → 节点 173 落地（事例 E）
```

#### 例 7：第二天另一个会话回顾
```
[次日 Claude Code 新会话开启]
1) call memory_search(
      query="breakout 策略最近问题",
      filter_tags_all=["domain:quant", "strategy:breakout"],
      n=10,
      username="trader_zhang"
   )
   ← 返回:
      - 节点 173 (kind:error 假突破)
      - 节点 201 (kind:insight 反思——"3 次假突破，建议加 ATR 滤波")
      - 节点 156 (kind:event 当时的买入)

2) Claude Code 据此调整策略实现，再写一条:
   call memory_remember(
      content="已在 breakout 策略加 ATR(14)>1.5 滤波，规避涨停后假突破",
      kind="decision",
      source="claude_code",
      extra_tags=["domain:quant", "strategy:breakout"],
      username="trader_zhang"
   )
```

#### 例 8：居家养老监测脚本（非交易场景）
```
[ESP32 血压计 → 中转脚本 → MCP]
call memory_remember(
   content="老人晨起血压 158/95，比上周平均高 12 个点",
   kind="event",
   source="elderly_monitor",
   extra_tags=["domain:homecare", "date:2026-04-27"],
   username="grandpa_li"
)
→ 节点 412 落地（事例 F）

[家属在 app 设规则]
call memory_remember(
   content="爷爷每天早晨必须测血压，9 点前没数据要报警",
   kind="rule",
   persistent=True,
   source="family_app",
   extra_tags=["domain:homecare", "schedule:daily"],
   username="grandpa_li"
)
→ 一条 persistent rule 入库

[当晚反思]
Fay 23:00 reflect → 检索 grandpa_li 节点
→ 生成 insight: "老人本周血压偏高 3 次，建议家属安排复查" (kind:insight, domain:homecare)
```

#### 例 9：Cursor 修 bug 后回写
```
[Cursor 修了一个内存泄漏]
call memory_remember(
   content="发现 stream_manager 在 ws 异常断开时未释放 buffer，已改为 try/finally 关闭",
   kind="fact",
   source="cursor",
   extra_tags=["domain:engineering", "module:stream_manager", "session:fix-memleak-1"]
)

[同时记录决策]
call memory_remember(
   content="Fay 项目里所有 ws 路径都要在 finally 里 close buffer",
   kind="rule",
   persistent=True,
   source="cursor",
   extra_tags=["domain:engineering"]
)
```

#### 例 10：教育辅导 agent
```
[家教 agent 完成一节课]
call memory_remember(
   content="小明今天独立完成 5 道一元二次方程，全部正确",
   kind="fact",
   source="tutor_bot",
   extra_tags=["domain:education", "subject:math", "topic:quadratic_eq"],
   username="xiaoming"
)

[下次开课前]
call memory_search(
   query="小明数学进度",
   filter_tags_all=["domain:education", "subject:math"],
   username="xiaoming"
)
→ 拉到上一次的"已掌握一元二次方程" → 这次直接进二次函数
```

#### 例 11：跨 agent 协作 — Claude Code 写规则、Fay 自身遵守
```
1) Claude Code 调:
   memory_remember(
      content="每小时整点检查所有持仓策略是否有未触发的止损单",
      kind="rule",
      persistent=True,
      source="user",
      extra_tags=["domain:quant", "schedule:hourly"]
   )

2) Fay 主进程的某个内置 agent（如调度器）
   每小时启动时调:
   core.memory_service.get_active_rules(username)
   ← 返回所有 persistent rules，包含上面这条
   → 调度器据此触发巡检流程
```
> 这是"MCP 工具同时向 Fay 自身暴露"的核心价值：写入与读取走同一条 service 层，外部 agent 写、内部 agent 读，无 sync 问题。

---

## 八、还没解决的事

1. **对话/观察线程没打 tag**：反思继承不到东西，需在 `remember_conversation_thread` / `remember_observation_thread` 里加默认 tag（如 `source:fay_conversation`、`source:fay_observation`）
2. **冷热分层缺失**：节点数大了之后 retrieve 全扫，没有按 importance + 年龄做分层或聚类收敛
3. **上下文压缩缺失**：retrieve 回来的节点直接全文进 prompt，没有 summary 压缩层
4. **端到端联调未跑**：MCP 工具刚加，需要：启动 Fay → Claude Code 连 SSE → 调 `memory_get_schema` / `memory_remember` / `memory_search`，确认节点正确落盘

---

## 九、文件位置速查

| 用途 | 文件 |
|---|---|
| 节点结构 + memory_stream 主体 | `genagents/modules/memory_stream.py` |
| 对话流 + 写线程 + 定时任务 | `llm/nlp_cognitive_stream.py` |
| 唯一权威 service | `core/memory_service.py` |
| 用户档案 sqlite | `core/member_db.py` |
| MCP SSE 服务（含记忆工具） | `faymcp/mcp_server.py` |
| 记忆数据 | `memory/[<username>/]memory_stream/nodes.json` + `embeddings.json` |
| 用户档案 DB | `memory/user_profiles.db` |

---

### Memory Module (`docs/memory_module.md`)

# Fay Memory Module

本说明描述当前默认的认知记忆实现。适用于 `llm/nlp_cognitive_stream.py` + `genagents/modules/memory_stream.py` 这条路径。

## 记忆节点
- 节点类型：`observation`（观察）、`conversation`（对话）、`reflection`（反思）
- 字段：
  - `node_id`: 递增整数
  - `node_type`: 节点类型
  - `content`: 记忆内容（纯文本，已去除 `<think>` / `<prestart>`）
  - `importance`: 整数重要度
  - `datetime`: 节点创建时间，格式 `YYYY/MM/DD HH:MM:SS`；旧节点可能为空
  - `created` / `last_retrieved`: 时间步（整型）
  - `pointer_id`: 反思节点指向的源节点列表 ID
- 创建：
  - 观察：`remember_observation_thread` / `record_observation`
  - 对话：`remember_conversation_thread`（问答格式：`{user}：{问}\n{agent}：{答}`，其中 `user/user` 会写成“主人”）
  - 反思：`MemoryStream.reflect` 生成
  - 写盘策略：即时不落盘，按定时/退出在 `llm/nlp_cognitive_stream.py::save_agent_memory` 才写入
- Embedding：`memory_stream._add_node` 创建时生成；维度不符时检索阶段会临时重算（内存中，不立即落盘）

## 检索与提示词
- 检索：`MemoryStream.retrieve` 按 recency/relevance/importance 组合权重取回；关联记忆一步检索 `curr_filter="all"` 后按类型分段展示。
- 展示格式：检索结果中若节点有 `datetime`，前缀 `"[{datetime}] "`；无时间则不展示时间。
- 关联记忆段落无“关联记忆”标题，直接列出各类型小节（观察、对话、反思）。

## 配置与隔离
- `memory.isolate_by_user`: 打开后记忆目录按用户名隔离。

## 运行时要点
- 文字接口 `no_reply=true` 且有 `observation` 时：只记观察，不回复；无 `messages` 且有 `observation` 会强制 `no_reply=true`。
- `no_reply=false` 的普通对话：问题/回答会写入对话记忆；`observation` 若存在也会写入观察记忆。
- Prompt 打印：已关闭（`_log_prompt` 是空操作）。

## 文件位置
- 核心逻辑：`llm/nlp_cognitive_stream.py`
- 记忆结构：`genagents/modules/memory_stream.py`
- 定时保存：`llm/nlp_cognitive_stream.py::save_agent_memory`
- 记忆数据：`memory/memory_stream/nodes.json`, `embeddings.json`, `meta.json`

---

--- METRICS ---
- Files Extracted: 10
- Estimated Token Budget: ~13632 tokens
- Canonical Reference: https://codewiki.google/github.com/xszyou/Fay
