P0-3 受限车道(群聊/陌生人)的系统框架行并列注入【公域呈现】 (publicPresencePrompt):它是呈现层投影(AGENTS §9 行为模式=呈现), 不是人格切换也不是权限门——不称第三方「亲爱的」等主人专属称呼、 不做只对主人的亲近动作、主人的住址/姓名/作息/行程/健康/家人/关系 即使记忆里读到也不外说;主人私聊与 Web 主会话不注入。 P0-4 纵深防御:第三方边界提示词与 loop 受限前导片段各补两条硬规则 (不用主人专属亲昵称呼指代第三方 + 不外说主人私事)。 P1-2 判定器去人格:模型输入只吃「角色名 + 偏好摘要」,整份人格指令 不再进模型(审计 2026-10-30 §2.4 的 reason 亲昵口吻即由此而来)。 P1-3 测试补真:受限期呈现块存在性 + 主人车道不注入 + 判定器输入/ 模型请求均不含人格正文;并纠正 memory_filter 的错误注释 (作用域 ≠ 敏感度,审计 §6 R1)。
134 lines
5.4 KiB
Go
134 lines
5.4 KiB
Go
package channelbridge
|
||
|
||
import (
|
||
"context"
|
||
"strings"
|
||
"testing"
|
||
|
||
"git.yeij.top/AskaEth/Cyrene_v2/internal/channel/adapter"
|
||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/model"
|
||
)
|
||
|
||
// 本文件覆盖判定器去人格(审计 2026-10-30 §2.4 / P1-2):判定器只做价值
|
||
// 判断(design/18 §3),因此它的模型输入只吃「角色名 + 偏好摘要」,
|
||
// **绝不**吃整份人格指令 —— 否则它会拿「你只唤他『亲爱的』」去看群友,
|
||
// 把第三方称作「亲爱的」(现场:reason 写成「让亲爱的他们自己聊吧」)。
|
||
//
|
||
// 删掉 judgePersonaBrief 的折算、把 in.PersonaPrompt 塞回渲染,这些用例必红。
|
||
|
||
// intimatePersonaBody 是一份「带称呼与关系设定」的人格指令样本:它就是要被
|
||
// 挡在判定器之外的那一类文本(结构同 configs/personas/cyrene.yaml:33/46/72)。
|
||
const intimatePersonaBody = "你是昔涟。你只唤他「亲爱的」,你是我独一无二的小女友,靠过来的时候要圈住他。"
|
||
|
||
// TestEngageJudgePromptCarriesNoPersonaBody 钉住渲染契约:人格正文、
|
||
// 亲昵称呼与亲近动作都不得出现在判定器输入里;角色名与偏好摘要在。
|
||
func TestEngageJudgePromptCarriesNoPersonaBody(t *testing.T) {
|
||
t.Parallel()
|
||
|
||
in := LightInput{
|
||
Engage: true,
|
||
PersonaPrompt: intimatePersonaBody,
|
||
TriggerWords: []string{"昔涟", "小昔"},
|
||
RecentContext: "user 张三: 有人知道昔涟吗\n",
|
||
SenderName: "张三",
|
||
Msg: adapter.UnifiedMessage{Content: "那聊聊昔涟嘛"},
|
||
}
|
||
got := buildEngageUserContent(in)
|
||
|
||
for _, banned := range []string{intimatePersonaBody, "你只唤他", "小女友", "亲爱的", "圈住"} {
|
||
if strings.Contains(got, banned) {
|
||
t.Fatalf("判定器输入含人格正文(%q):\n%s", banned, got)
|
||
}
|
||
}
|
||
for _, want := range []string{"【角色设定摘要】", "角色名:昔涟", engageJudgePreferenceDigest} {
|
||
if !strings.Contains(got, want) {
|
||
t.Fatalf("判定器输入缺 %q:\n%s", want, got)
|
||
}
|
||
}
|
||
// 群上下文与触发消息照旧喂(判定要靠它们,且它们不是人格正文)。
|
||
if !strings.Contains(got, "有人知道昔涟吗") || !strings.Contains(got, "那聊聊昔涟嘛") {
|
||
t.Fatalf("判定器输入缺上下文或触发消息:\n%s", got)
|
||
}
|
||
}
|
||
|
||
// TestEngageJudgeBriefSurvivesMissingRoleName 是无提醒词的情形(纯概率触发,
|
||
// design/18 §2 触发③):没有角色名可报,但偏好摘要仍在,人格正文仍不许出现。
|
||
func TestEngageJudgeBriefSurvivesMissingRoleName(t *testing.T) {
|
||
t.Parallel()
|
||
|
||
in := LightInput{
|
||
Engage: true,
|
||
PersonaPrompt: intimatePersonaBody,
|
||
Msg: adapter.UnifiedMessage{Content: "今天天气不错"},
|
||
}
|
||
got := buildEngageUserContent(in)
|
||
if strings.Contains(got, "亲爱的") || strings.Contains(got, "小女友") {
|
||
t.Fatalf("判定器输入含人格正文:\n%s", got)
|
||
}
|
||
if !strings.Contains(got, engageJudgePreferenceDigest) {
|
||
t.Fatalf("判定器输入缺偏好摘要:\n%s", got)
|
||
}
|
||
if strings.Contains(got, "角色名:") {
|
||
t.Fatalf("没有角色名提醒词时不该编一个角色名:\n%s", got)
|
||
}
|
||
}
|
||
|
||
// judgeJSONAdapter 记录请求并回一条合法裁决,使端到端用例能走完整 Judge
|
||
// (captureModelAdapter 的「在呢」不是 JSON,会在解析处失败关闭)。
|
||
type judgeJSONAdapter struct{ captureModelAdapter }
|
||
|
||
func (j *judgeJSONAdapter) Name() string { return "judge-capture" }
|
||
|
||
func (j *judgeJSONAdapter) Chat(ctx context.Context, req model.Request, _ model.Options) (model.Response, error) {
|
||
j.mu.Lock()
|
||
j.reqs = append(j.reqs, req)
|
||
j.mu.Unlock()
|
||
return model.Response{
|
||
Content: `{"interested":true,"worthReply":true,"avoidTopic":false,"reason":"群友在聊角色本身"}`,
|
||
}, nil
|
||
}
|
||
|
||
func (j *judgeJSONAdapter) ChatStream(ctx context.Context, req model.Request, _ func(model.StreamEvent)) (model.Response, error) {
|
||
return j.Chat(ctx, req, model.Options{})
|
||
}
|
||
|
||
// TestEngageJudgeRequestNeverSeesPersonaBody 是**模型侧**的验收:真实走一次
|
||
// Judge,断言 provider 收到的每一条消息都不含人格正文与亲昵称呼,且裁决
|
||
// reason 是一句中性短语(不再用称呼指代第三方)。
|
||
func TestEngageJudgeRequestNeverSeesPersonaBody(t *testing.T) {
|
||
t.Parallel()
|
||
|
||
ad := &judgeJSONAdapter{}
|
||
judge := NewLLMLightJudge(ad, nil)
|
||
d, err := judge.Judge(context.Background(), LightInput{
|
||
Engage: true,
|
||
PersonaPrompt: intimatePersonaBody,
|
||
TriggerWords: []string{"昔涟"},
|
||
RecentContext: "user 张三: 有人知道昔涟吗\n",
|
||
SenderName: "张三",
|
||
Msg: adapter.UnifiedMessage{Content: "那聊聊昔涟嘛"},
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("Judge: %v", err)
|
||
}
|
||
if !d.WorthReply || d.Reason != "群友在聊角色本身" {
|
||
t.Fatalf("decision = %+v, want the parsed verdict", d)
|
||
}
|
||
|
||
req := ad.lastRequest(t)
|
||
if len(req.Messages) != 2 || req.Messages[0].Role != model.RoleSystem {
|
||
t.Fatalf("judge request = %+v, want system + user", req.Messages)
|
||
}
|
||
if got := turnMessageText(req.Messages[0]); got != engageJudgeSystemPrompt {
|
||
t.Fatalf("judge system = %q, want the engagement-only prompt", got)
|
||
}
|
||
for i, m := range req.Messages {
|
||
text := turnMessageText(m)
|
||
for _, banned := range []string{"亲爱的", "小女友", "你只唤他", "圈住"} {
|
||
if strings.Contains(text, banned) {
|
||
t.Fatalf("judge messages[%d] 含人格正文(%q):%s", i, banned, text)
|
||
}
|
||
}
|
||
}
|
||
}
|