Files
Cyrene_v2/internal/runtime/loop/attribution.go
T
AskaEth 91e8d7a029 docs(runtime): internal/runtime/loop 44 个文件注释改中文(逐行 1:1,代码零字节改动)
主人 2026-10-03 拍板「注释一律中文」;本模块按长期分批协议(一个模块一次派工)执行。
注释行数 1329 → 1329 逐行 1:1(含中文 33 → 1304);行尾注释 5 条一并译。
校验:机械校验(非注释改动行)= 空;独立 Go AST 程序(剥注释复渲)证明 44/44 文件代码全等;
全树 go test ./... 58 包 ok / 0 FAIL;交叉编译绿。
gofmt:capacity_loop_test.go / flow.go 改动前即未格式化,按铁律保留原样未动。
2026-10-03 22:28:21 +08:00

142 lines
6.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 + ")"
}