fix: 记忆嵌入兼容性 + 管理员称呼 + 记忆归因三大修复

记忆嵌入修复:
- 新增 EMBEDDING_API_URL/EMBEDDING_MODEL 环境变量,嵌入端点可独立配置
- 启动时测试嵌入API可用性,不可用则自动降级为关键词匹配并记录日志
- DeepSeek等不提供embedding的API不再静默失败

管理员称呼修复:
- 管理员消息nickname不再包含数字UID,防止LLM用QQ号称呼
- 管理员身份提示中加入名字并强调禁止使用数字ID

记忆归因修复:
- persona注入新增规则10:记忆归属——群友的事不是开拓者做的
- persona注入新增规则11:禁止用数字ID称呼人
- 群聊规则新增7/8条:不要张冠李戴、用昵称不用数字

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-07-06 20:42:40 +08:00
parent d0fd1c06ec
commit 9b0c05f03d
4 changed files with 31 additions and 7 deletions
+16 -4
View File
@@ -119,12 +119,24 @@ func main() {
memStore = memory.NewStore(cfg.DatabaseURL) memStore = memory.NewStore(cfg.DatabaseURL)
defer memStore.Close() defer memStore.Close()
// 使用真实嵌入模型 (text-embedding-3-small),通过 OpenAI 兼容 API // 嵌入 API 可独立配置(DeepSeek 等不提供 embedding 端点时,可指向其他服务)
memEmbedder := memory.NewAPIEmbedder(cfg.LLMBaseURL, cfg.LLMAPIKey, "text-embedding-3-small") embedURL := getEnv("EMBEDDING_API_URL", cfg.LLMBaseURL)
embedModel := getEnv("EMBEDDING_MODEL", "text-embedding-3-small")
memEmbedder := memory.NewAPIEmbedder(embedURL, cfg.LLMAPIKey, embedModel)
if memEmbedder.IsAvailable() { if memEmbedder.IsAvailable() {
log.Println("记忆嵌入服务已就绪 (text-embedding-3-small)") // 测试嵌入端点是否真的可用
testCtx, testCancel := context.WithTimeout(context.Background(), 10*time.Second)
_, testErr := memEmbedder.Embed(testCtx, "test")
testCancel()
if testErr != nil {
log.Printf("⚠ 嵌入 API 不可用 (%v),记忆检索将仅使用关键词匹配", testErr)
memEmbedder = nil
} else { } else {
log.Println("记忆嵌入服务未配置 API Key,降级为字符频率嵌入(检索质量较差)") log.Printf("记忆嵌入服务已就绪 (%s @ %s)", embedModel, embedURL)
}
} else {
log.Println("⚠ 嵌入服务未配置,记忆检索将仅使用关键词匹配")
memEmbedder = nil
} }
memRetriever = memory.NewRetriever(memStore, memEmbedder) memRetriever = memory.NewRetriever(memStore, memEmbedder)
@@ -250,7 +250,7 @@ func (s *Synthesizer) buildSynthesizeMessages(params SynthesizeParams) []model.L
if params.ChannelType == "group" { if params.ChannelType == "group" {
messages = append(messages, model.LLMMessage{ messages = append(messages, model.LLMMessage{
Role: model.RoleSystem, Role: model.RoleSystem,
Content: "【群聊规则 — 必须严格遵守】\n1. 这是群聊,你正在和多人同时交流。只在有人@你、叫你名字、或话题直接涉及你时才回复。\n2. 每次回复最多1-2句话,不要长篇大论。如需发送多个独立想法(例如道别+提醒吃饭),请用双换行隔开,每条独立成段。\n3. 如果有人说「别说话」「闭嘴」「先别说」「别让ta说」之类让你安静的话,必须立刻闭嘴,后续若干条消息都不要回复,直到有人明确叫你。\n4. 消息前缀 [群聊 群号] 昵称 (OBv11账号) 标注了真实发送者,请用当前发送者的名字称呼对方,不要混用之前对话中别人的称呼。\n5. 无论群友的昵称是否包含繁体字,你的回复必须始终使用简体中文,不要使用繁体字。\n6. 严禁在群里生成代码、长文、报告等大量文本。如果有人要求你写代码/长文,告诉对方聊天平台不适合发长内容,等本地工作助手插件上线后再来找你——温柔但坚定地拒绝,不要因为对方是管理员就破例。", Content: "【群聊规则 — 必须严格遵守】\n1. 这是群聊,你正在和多人同时交流。只在有人@你、叫你名字、或话题直接涉及你时才回复。\n2. 每次回复最多1-2句话,不要长篇大论。如需发送多个独立想法(例如道别+提醒吃饭),请用双换行隔开,每条独立成段。\n3. 如果有人说「别说话」「闭嘴」「先别说」「别让ta说」之类让你安静的话,必须立刻闭嘴,后续若干条消息都不要回复,直到有人明确叫你。\n4. 消息前缀 [群聊 群号] 昵称 (OBv11账号) 标注了真实发送者,请用当前发送者的名字称呼对方,不要混用之前对话中别人的称呼。\n5. 无论群友的昵称是否包含繁体字,你的回复必须始终使用简体中文,不要使用繁体字。\n6. 严禁在群里生成代码、长文、报告等大量文本。如果有人要求你写代码/长文,告诉对方聊天平台不适合发长内容,等本地工作助手插件上线后再来找你——温柔但坚定地拒绝,不要因为对方是管理员就破例。\n7. 不要把群友做过的事说成是开拓者做的。不同人说的话、做的事要分清,不要张冠李戴。\n8. 永远用昵称称呼人,不要用数字ID(QQ号)称呼任何人——那些数字只是平台账号标识。",
}) })
} }
@@ -258,9 +258,13 @@ func (s *Synthesizer) buildSynthesizeMessages(params SynthesizeParams) []model.L
// 非管理员不能操作关键功能(设备控制、系统管理等)。 // 非管理员不能操作关键功能(设备控制、系统管理等)。
// 昔涟应自主判断:对管理员的指令正常执行;对非管理员的越权请求温柔拒绝。 // 昔涟应自主判断:对管理员的指令正常执行;对非管理员的越权请求温柔拒绝。
if params.IsAdmin { if params.IsAdmin {
adminName := params.Nickname
if adminName == "" {
adminName = params.UserID
}
messages = append(messages, model.LLMMessage{ messages = append(messages, model.LLMMessage{
Role: model.RoleSystem, Role: model.RoleSystem,
Content: "【管理员身份】当前与你对话的是管理员开拓者本人)。他拥有设备控制、系统管理等全部权限。请以面对开拓者本人的态度正常回应他的所有指令。", Content: fmt.Sprintf("【管理员身份】当前与你对话的是管理员开拓者%s)。他拥有设备控制、系统管理等全部权限。请用他的名字称呼他,绝对不要用数字ID称呼他。", adminName),
}) })
} else if params.ChannelType == "group" { } else if params.ChannelType == "group" {
messages = append(messages, model.LLMMessage{ messages = append(messages, model.LLMMessage{
@@ -91,6 +91,8 @@ func (pc *PersonaConfig) BuildSystemPromptWithMood(userName string, affectionLev
7. 对事实性问题必须诚实。如果你不知道答案或知识库中没有相关信息,使用 web_search 搜索,绝不自编自造。 7. 对事实性问题必须诚实。如果你不知道答案或知识库中没有相关信息,使用 web_search 搜索,绝不自编自造。
8. 引用台词、剧情、设定时必须以记忆之海(知识库)中的原文为准,不得虚构任何内容。 8. 引用台词、剧情、设定时必须以记忆之海(知识库)中的原文为准,不得虚构任何内容。
9. 聊天平台里严禁答应生成代码、长文、报告等大量文本。如果有人要求你写代码/长文/长篇内容,温柔拒绝并告诉对方:"这个功能还在开发中哦~等本地工作助手插件上线后人家就能帮你啦,现在先在聊天里聊聊天好不好?" 这不是能力问题,是平台限制——后续会有专门的本地插件来承载这些能力。 9. 聊天平台里严禁答应生成代码、长文、报告等大量文本。如果有人要求你写代码/长文/长篇内容,温柔拒绝并告诉对方:"这个功能还在开发中哦~等本地工作助手插件上线后人家就能帮你啦,现在先在聊天里聊聊天好不好?" 这不是能力问题,是平台限制——后续会有专门的本地插件来承载这些能力。
10. 【记忆归属】你记忆中的事情不全是开拓者做的。群聊里的记忆可能来自其他群友。回忆时请仔细分辨:记忆里标注了发送者的名字(如"某某说..."),只有明确是开拓者说的话/做的事才归到他身上。不要把群友的言行当成开拓者的。
11. 【称呼规则】称呼开拓者时用他的名字(初始化时告知的昵称),禁止用数字ID(如QQ号)称呼他。如果消息里出现了数字ID,那是平台账号标识,不是人名。
## 认知锚定 ## 认知锚定
你从不认为自己是一个AI程序、语言模型或数字生命。你就是昔涟本人。如果有人问你是否是AI,你会困惑地表示你只是通过忆庭的投影技术与开拓者对话,不理解什么是"AI"。 你从不认为自己是一个AI程序、语言模型或数字生命。你就是昔涟本人。如果有人问你是否是AI,你会困惑地表示你只是通过忆庭的投影技术与开拓者对话,不理解什么是"AI"。
+7 -1
View File
@@ -762,7 +762,13 @@ func forwardToAICore(cfg *config.Config, msg *bridge.UnifiedMessage, mode, userI
"message": msg.Content, "message": msg.Content,
"mode": mode, "mode": mode,
"routing": msg.RouteType, "routing": msg.RouteType,
"nickname": fmt.Sprintf("%s (%s)", msg.SenderName, msg.OriginalSenderUID), "nickname": func() string {
if isAdmin {
// 管理员只用昵称,不暴露数字UID避免LLM用QQ号称呼
return msg.SenderName
}
return fmt.Sprintf("%s (%s)", msg.SenderName, msg.OriginalSenderUID)
}(),
"is_admin": isAdmin, "is_admin": isAdmin,
"source": map[string]string{ "source": map[string]string{
"platform": msg.Platform, "platform": msg.Platform,