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
**安装依赖**
pip install -r requirements.txt**快速启动**
本地
python main.py start -config_center d19f7b0a-2b8a-4503-8c0d-1a587b90eb69 #使用公共资源,速度非常慢,建议更换成自己的key镜像
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 - 提供记忆机制及skills设计的参考
- OpenAI Codex - 提供稳定的工具调用能力的参考
- FunASR - 提供语音识别(ASR)能力
Fay侧标准动作改造说明 (`docs/Fay侧标准动作改造说明.md`)
Fay 侧通用动作语义改造说明
目标
让 Fay 输出“通用动作语义”,而不是输出某个渲染引擎或某个模型的具体动作编号。
这样 Fay 可以同时服务于:
- Live2D 数字人
- 3D 数字人
- Unity / Unreal 角色
- 机器人 / 硬件角色
- 仅语音客户端
原则:
- 保持原版 Fay WebSocket 音频接口结构不变。
- 在原版
Data中增加两个可选字段:Action和Sentiment。 - Fay 不输出
TapBody、MotionNo、F01这类具体实现细节。 - 各前端或驱动端通过自己的配置表完成映射。
原版接口
原版接口如下:
{
"Topic": "human",
"Data": {
"Key": "audio",
"Text": "这边请",
"IsFirst": 1,
"IsEnd": 0,
"Lips": []
}
}建议改造方式
在原版 Data 中增加可选字段 Action 和 Sentiment:
{
"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 侧都可以归一成:
{
"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() 方法,逻辑如下:
维护 4 个关键词列表,每个对应一个权重:
列表 示例关键词 权重 非常积极 开心、太好了、完美、激动 +2 轻微积极 好、可以、没问题、欢迎 +1 轻微消极 不好、失望、烦、担心 -1 非常消极 痛苦、绝望、崩溃、气死 -2 统计文本中匹配的关键词数量,加权求和
标点修正:
?/!/~加 0.3,.../。。.减 0.3最终 clamp 到
[-2, +2]范围
注意:此计算与 Action 中的 sentimentHint 字段完全独立。sentimentHint 是动作规则自身的情感标注,用于辅助关键词匹配;Sentiment 是对整句话的情感极性评估。
`code`
用于表达“这次动作在业务上的语义是什么”。
例如:
greeting.helloguidance.invitedialogue.explainsocial.thanks
`behavior`
用于表达“身体动作属于哪一类”。
例如:
nodinvitewavethinkwarn
它是动作语义锚点。
`affect`
用于表达“整体表现风格是什么”。
例如:
smilewarmneutralserioussadexcited
这个字段比 expression 更通用,因为它不只适用于 Live2D 表情,也可以映射到:
- 3D 面部表情
- TTS 语气风格
- 机器人灯光/屏幕表情
`intensity`
用于表达“动作做得轻一点还是重一点”。
例如:
0.2轻微点头0.8明显邀请0.95强烈庆祝
Fay 侧只负责什么
Fay 侧只负责:
文本 / 意图 / 场景 -> Action
文本 / 情绪分析 -> SentimentFay 不负责:
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 打开编辑:
关键词/意图 -> code/behavior/affect/intensity/priority/sentimentHintCSV 格式说明:
- 第一行为表头:
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
behavior -> motion
affect -> expression
intensity -> 候选动作强度选择
sentiment -> 表情/动作幅度微调或回退3D 数字人
behavior -> 动画状态机
affect -> 面部表情 / blendshape
intensity -> 动作幅度 / 播放权重
sentiment -> 情绪层混合权重机器人
behavior -> 舵机动作模板
affect -> 灯光 / 屏幕表情 / 语音风格
intensity -> 动作幅度 / 执行力度
sentiment -> 情绪表现微调纯语音客户端
behavior -> 可忽略
affect -> TTS 风格 / 音色 / 情绪
intensity -> 情绪强度
sentiment -> 连续情绪控制最小结论
Fay 侧建议从:
{
"Topic": "human",
"Data": {
"Key": "audio",
"Text": "这边请",
"IsFirst": 1,
"IsEnd": 0,
"Lips": []
}
}升级为:
{
"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独立调用,方便集成到其他系统:
调用接口:
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
}
}返回示例:
{
"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:
{
"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. 参数配置
配置预启动参数
- 在工具列表中找到
query_yueshen工具 - 点击工具右侧的"预启动?"标签
- 在弹出的对话框中配置参数:
{
"query": "{{question}}",
"top_k": 5,
"where": {}
}参数说明:
| 参数 | 类型 | 说明 |
|---|---|---|
query |
string | 查询文本,{{question}}会被替换为用户实际问题 |
top_k |
int | 返回最相关的文档数量 |
where |
object | 可选的元数据过滤条件 |
配置选项
- 结果保存到记忆:勾选后,检索结果会保存到对话历史中
- 允许函数调用:是否允许LLM在推理时再次调用此工具
点击"保存预启动"完成配置。
3. 启动方式
方式一:随Fay自动启动
配置完成后,知识库服务会在Fay启动时自动连接。确保:
- MCP服务器已添加并保存
- 在服务器卡片上启用了自动连接
方式二:手动连接
在MCP管理页面:
- 找到目标服务器卡片
- 点击"连接"按钮
- 等待状态变为"在线"
方式三:通过API连接
POST http://127.0.0.1:5010/api/mcp/servers/{server_id}/connect验证配置
连接成功后,可以通过以下方式验证:
- 查看工具列表:确认
query_yueshen等工具已显示 - 测试查询:在工具面板中手动调用工具测试
- 对话测试:向Fay提问知识库相关问题,观察是否返回准确答案
三、怎么做到的
Fay MCP工具的预启动功能
Fay实现了一套独特的**预启动(Prestart)**机制,使知识库查询能够无缝集成到对话流程中。
核心设计
/* Detailed source-code truncated for AI context efficiency. */技术实现要点
1. 预启动配置独立于工具启用状态
预启动功能与工具的启用/禁用状态相互独立:
- 启用/禁用:控制LLM是否可以主动调用该工具
- 预启动:控制工具是否在每次对话前自动执行
这意味着你可以:
- 禁用工具的LLM调用权限,但保留预启动功能
- 让知识库查询仅在对话开始时执行,避免LLM重复调用
2. 参数模板与占位符替换
预启动支持{{question}}占位符,在执行时自动替换为用户的实际问题:
# 参数模板
{"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: 知识库服务连接失败?
检查以下几点:
- Python环境是否正确配置
- 依赖包是否安装完整(chromadb, pdfplumber等)
- 环境变量中的API密钥是否正确
- 知识库文档目录是否存在
Q2: 检索结果不准确?
优化建议:
- 调整
top_k参数,增加返回文档数量 - 检查文档分块策略是否合理
- 尝试更换Embedding模型
- 优化知识库文档的质量和格式
Q3: 如何添加新文档到知识库?
将文档(支持PDF、DOCX、TXT)放入YUESHEN_CORPUS_DIR配置的目录中:
- 如果启用了
YUESHEN_AUTO_INGEST,系统会自动索引 - 也可以手动调用
ingest_yueshen工具触发索引
Q4: 预启动工具执行超时?
检查:
- Embedding API服务是否正常
- 网络连接是否稳定
- 可适当增加超时时间配置
总结
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 组:
{
"Target": "Parameter",
"Name": "LipSync",
"Ids": ["ParamMouthOpenY"]
}眨眼(必需)
| 参数 ID | 用途 |
|---|---|
ParamEyeLOpen |
左眼开合 |
ParamEyeROpen |
右眼开合 |
程序会在 Groups 中查找 EyeBlink 组:
{
"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 为例):
{
"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) | 推荐 | 方便后续调整动作 |
九、重要注意事项
- 口型参数独占:
ParamMouthOpenY由程序实时驱动口型同步,动作和表情中都不要对此参数设置关键帧 - 表情命名必须匹配:表情名称必须是
F01、F02... 格式,程序按此名称调用 - 动作组命名必须匹配:待机组必须叫
Idle,语义动作组必须叫TapBody - TapBody 的顺序很重要:程序按数组下标索引动作,请务必按对照表排列顺序
- 动作不宜过长:对话场景中动作切换频繁,单个动作建议 2~4 秒
- 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 为例:
{
"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) | 推荐 |
八、注意事项
ParamMouthOpenY不要动:动作中不要对嘴巴参数设关键帧,程序实时驱动口型- 动作组必须叫
Idle和TapBody:程序按这两个名字查找 - TapBody 顺序不能变:程序按下标 0~4 调用,顺序错了动作就对不上
EyeBlink和LipSyncGroups 不能漏:没有这两个声明,自动眨眼和口型同步不会生效
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
请求体示例:
{
"method": "tool_name",
"params": {
"key": "value"
},
"is_prestart": false
}说明:
is_prestart=true会跳过工具启用状态检查(用于预启用调用)。
调用工具(自动选择在线服务器)
POST /api/mcp/tools/{tool_name}
请求体示例:
{
"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
请求体示例:
{
"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
请求体示例:
{
"params": {
"query": "{{question}}"
},
"question": "用户问题",
"keep_connection": true
}批量调用所有预启用工具(自动连接服务器)
POST /api/mcp/prestart/call
请求体示例:
{
"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 + 关联记忆 + 可用工具名 + 知识库主题 + 预启动工具结果 + 历史 + 对话历史
输出示例:
{"action": "finish", "message": "你好呀!有什么我可以帮你的吗?"}{"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 查看所有课程列表附加内容:人设 + 关联记忆 + 观察 + 预启动工具结果 + 已完成工具摘要 + 对话历史
输出示例:
{"action": "tool", "tool": "kb_search", "args": {"query": "课程A"}}{"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 `` 标签
| 属性 | 说明 | 在 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 包含:
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)
{
"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)
{
"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)
{
"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)
{
"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)
{
"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,跨场景示例)
{
"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)
{
"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 |
写入的统一管线:
- 锁外 算 importance(LLM 评分 0–10)和 embedding(API 向量)
- 持锁
agent_lock,把节点 append 到memory_stream.seq_nodes,并写embeddings[content] - 内部 4 条只更新内存;只有
core.memory_service.remember会立即落盘 - 每天 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 行附近):
- 用当前用户输入做 query
- 一次 retrieve 拉 30 条候选
- 按
node_type分成三段:观察记忆 / 对话记忆 / 反思记忆,每段最多 10 条 - 拼成 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— 修复维度不一致的旧 embeddingcreate_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:truememory_get_reflections— 最近反思memory_get_user_profile— portrait + extra_infomemory_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 自动从源节点继承+覆盖
结果如事例 G7.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 问题。
八、还没解决的事
- 对话/观察线程没打 tag:反思继承不到东西,需在
remember_conversation_thread/remember_observation_thread里加默认 tag(如source:fay_conversation、source:fay_observation) - 冷热分层缺失:节点数大了之后 retrieve 全扫,没有按 importance + 年龄做分层或聚类收敛
- 上下文压缩缺失:retrieve 回来的节点直接全文进 prompt,没有 summary 压缩层
- 端到端联调未跑: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 ---
- Files Extracted: 10
- Estimated Token Budget: ~13632 tokens
- Canonical Reference: https://codewiki.google/github.com/xszyou/Fay