package loop import ( "context" "strings" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/model" ) // 本文件拥有模型上下文的发言人归属(DevDocs/design/22 // §7-5)。一个进程现在同时承载多个角色,且会话在切换角色时 // 保留其对话,因此模型必须能分辨上下文中的哪些话 // 出自哪个角色:前一角色的 assistant 消息不得读作 // 「我说过的话」,而当前作答的角色必须能认出 // 自己。 // // 两条硬规则塑造了渲染方式: // - 归属元数据绝不进入消息正文。用户消息是来自外部世界的 // 自由文本,因此让它携带看似系统的标记,就等于 // 给写下它的人一次冒充系统指令的机会 // (design/11 §10)。因此归属以其自身的系统消息传递, // 绝不作为用户或 assistant 文本内部的前缀。 // - 渲染只「新增」系统消息。它绝不重排、改写或 // 丢弃消息,因此组装出的提示词保持既定形态:agent 的 // 指令仍是第一条系统消息,时间 / 工作记忆 / 记忆块保持 // 既有顺序(位置由 assembleContext 拥有;本文件只提供 // 追加的片段)。 // // 每条消息的发言人从结构化的 model.Message.SenderName // 车道读取,因为本批次 model/** 已冻结,Message 没有专门的 // persona 字段。只读 assistant 消息:对 assistant 轮, // 发送者正是说出它的角色,而带标签的用户消息保持其 // 既有含义(design/16 §4.6 的第三方发送者标记), // 本文件刻意不碰它。 // 归属标记。两者都是编译期常量,使运行路径中 // 不出现魔法字符串,测试也能断言确切的线上形态。 const ( // roleContextMarker 开启声明发言角色的前导片段。 roleContextMarker = "[会话角色] " // speakerSwitchMarker 开启插在对话发言人 // 发生变化处的边界说明。 speakerSwitchMarker = "[发言归属] " ) // speakerOf 返回被标注为 m 发言人的角色 id;该消息不带 // 归属时返回 ""。只考虑 assistant 消息:用户 // 消息的 SenderName 是 design/16 §4.6 的第三方显示名车道, // 与人格选择无关。 func speakerOf(m model.Message) string { if m.Role != model.RoleAssistant { return "" } return strings.TrimSpace(m.SenderName) } // anySpeakerLabel 报告对话中是否有任何消息携带 // 发言人标签。都没有时,归属渲染是完全空操作:单角色对话 // (或从不给消息打标签的调用方)保持 // 多角色之前完全一样的提示词。 func anySpeakerLabel(msgs []model.Message) bool { for _, m := range msgs { if speakerOf(m) != "" { return true } } return false } // attributeConversation 为一个模型步渲染角色上下文。它返回 // 前导角色声明(对话完全不带发言人标签时 ok = false), // 以及插入了边界说明的对话 —— 只要带标签的发言人 // 发生变化就插入。 // // 该说明为其后每条带标签的消息宣告发言人,直到 // 下一条说明,这正是模型回答「第 N // 条是谁说的」所需的粒度,且不触碰任何消息正文。渲染发生在请求 // 构建时、基于实时切片,因此中途被截断或压缩的窗口 // 绝不会让某条消息落在错误的发言人之下:被丢弃的 // 说明会重新生成,且没有说明会声称它不拥有的位置。 func (l *Loop) attributeConversation(ctx context.Context, msgs []model.Message) (model.Message, bool, []model.Message) { if !anySpeakerLabel(msgs) { return model.Message{}, false, msgs } current := l.personaFor(ctx) out := make([]model.Message, 0, len(msgs)+2) prev := "" for _, m := range msgs { if s := speakerOf(m); s != "" && s != prev { out = append(out, model.TextMessage(model.RoleSystem, l.speakerSwitchText(s, current))) prev = s } out = append(out, m) } return l.roleContextMessage(current), true, out } // roleContextMessage 构建声明哪个角色在作答的前导片段, // 使刚被切换进来的角色在其指令之上对自己身份有强烈的 // 感知(design/22 §7-5)。它陈述下文各条说明的边界 // 规则:一段带归属的发言属于它被标注的角色, // 而不属于当前作答的角色。 func (l *Loop) roleContextMessage(current string) model.Message { var b strings.Builder b.WriteString(roleContextMarker) if current != "" { b.WriteString("你正以") b.WriteString(l.speakerLabel(current)) b.WriteString("的身份对话。") } b.WriteString("本会话此前由不同角色相继发言:下面凡是标了「发言归属」的发言,都只属于标注的那个角色——" + "不要把别个角色说过的话当成你自己说过的,也不要继承他们的身份、口吻或立场;" + "该标注从其出现处起生效,直到下一条「发言归属」为止。") return model.TextMessage(model.RoleSystem, b.String()) } // speakerSwitchText 为一个发言角色构建边界说明。 // 作答角色自己的一段历史被表述为「来自你自己」(使模型与 // 切换前说过的话保持连续),其他每个角色都被表述为 // 属于该角色、且明确不属于当前作答者。 func (l *Loop) speakerSwitchText(personaID, current string) string { label := l.speakerLabel(personaID) if personaID != "" && personaID == current { return speakerSwitchMarker + "以下发言来自你自己——" + label + "。" } return speakerSwitchMarker + "以下发言来自" + label + ",不是你说的。" } // speakerLabel 为模型渲染一个角色引用:名字解析器认识该角色时 // 用其显示名加 id,否则只用 id。它 // 绝不返回空字符串,因此说明总能指名道姓。 func (l *Loop) speakerLabel(personaID string) string { name := "" if l.personaName != nil { name = strings.TrimSpace(l.personaName(personaID)) } if name == "" || name == personaID { return "「" + personaID + "」" } return "「" + name + "」(" + personaID + ")" }