Fay Documentation

Fay是一个帮助数字人(2.5d、3d、移动、pc、网页)或大语言模型(openai兼容、deepseek)连通业务系统的agent框架。

13,577 stars Python Python 1 file · ~13,707 tokens #ai#android#api#python#ue5#unity3d#wav2lip
RAW Doc

Technical Documentation: xszyou/Fay

ℹ️ Provenance: Multi-Tier Ingestion · CodeWiki Reference

Overview (README.md)

!!重要通知:我们已经把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,并配置里面的内容

**管理页面**

**高级玩法**

***使用数字人(非必须)***

https://qqk9ntwbcit.feishu.cn/wiki/GHevwqxwfiX4hCk8yJCcoJ54nqg

***集成到自家产品(非必须)***

https://qqk9ntwbcit.feishu.cn/wiki/Mcw3wbA3RiNZzwkexz6cnKCsnhh

**联系**

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

商务联系

qq467665317

**致谢**

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

  • openclaw - 提供记忆机制及skills设计的参考
  • OpenAI Codex - 提供稳定的工具调用能力的参考
  • 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推理前自动执行:

text
用户提问 → 预启动工具执行(知识库检索) → 检索结果注入上下文 → 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管理界面:

text
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)**机制,使知识库查询能够无缝集成到对话流程中。

核心设计

text
/* 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. 多知识库并行查询

支持配置多个预启动工具,它们会并行执行:

text
预启动工具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" }
  ]
}

七、目录结构示例

text
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 数组中的顺序必须是:聆听、说话、思考、等待、打招呼

六、目录结构

text
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 整体流程

text
用户输入(文字/语音)
    ↓
预处理:加载系统 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 的人设:

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

3.2 闲聊判断器 Prompt

System Message 核心(_build_judge_messages):

text
你是一个闲聊判断器。判断用户的话是不是闲聊,请严格输出合法 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):

text
你是一个任务执行器,负责逐步调用工具完成用户请求。请严格输出合法 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):

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

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

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

输出示例:

text
课程A的内容是……(基于工具结果的口语化总结)

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

4.1 单模型模式(`big_model_engine` 留空)

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

特点:

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

4.2 双模型模式(配置了 `big_model_engine`)

text
用户输入
    ↓
闲聊判断器(小模型,流式)→ 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 `` 标签

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

5.2 `` 标签

场景 说明 处理逻辑
工具执行日志 包裹工具调用过程(工具名、参数、结果摘要、耗时) 由 _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_stream(核心) memory/[/]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 包含:

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

node_type 只有三种:observation / conversation / reflection。

tags 走命名空间约定:

text
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, ...)。

打分公式:

text
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:普通用户聊天

text
用户:"今天天气真不错"
└─→ Fay 主流程: question() 拿到内容
    ├─→ 锁外算 importance/embedding
    ├─→ remember_conversation_thread 异步写一条 conversation 节点(content="主人:...\nFay:...")
    └─→ 当前提示词组装时调 memory_stream.retrieve(content)
         ├─→ 取回 30 条候选,分三段塞进 system prompt 的 memory_context
         └─→ LLM 据此生成回复

当前 conversation 节点不带 tag,反思阶段无法继承。

例 2:用户主动表达偏好(理想路径)

text
用户:"以后晚上 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 用户画像分析

text
schedule.run_pending() 触发 perform_user_portrait_analysis()
└─→ 读 T_Member.user_portrait(旧画像)
└─→ 抽取最近 N 条 conversation 节点
└─→ 调 LLM,按 6 个维度(含"与 Fay 的关系")生成新画像,1000 字以内
└─→ 写回 T_Member.user_portrait

例 4:每晚 23:00 反思

text
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 开始

text
[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:执行过程中实时回写事件

text
[策略触发 → 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:第二天另一个会话回顾

text
[次日 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:居家养老监测脚本(非交易场景)

text
[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 后回写

text
[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

text
[家教 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 自身遵守

text
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/[/]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: 记忆内容(纯文本,已去除 /)
    • 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 ---