package main import ( "strings" "git.yeij.top/AskaEth/Cyrene_v2/internal/persona/loader" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/loop" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/model" ) // 以下段头对应 design/01 §2.2 中此前未被消费的人格字段。它们作为独立的 system // 提示段追加在身份块之后,使每一段都能单独调优而不必改动身份正文。 const ( personalityHeader = "【性格】" valuesHeader = "【价值观】" speechStyleHeader = "【说话风格】" catchphraseHeader = "【口头禅】" backgroundHeader = "【背景】" ) // personaSection 是一个追加块:方括号段头 + 正文。 type personaSection struct { header string body string } // defaultPersona 返回默认角色的 persona;没有注册角色/persona 时返回 false。 func (a *app) defaultPersona() (*loader.Persona, bool) { c, ok := a.personas.DefaultCharacter() if !ok { return nil, false } p, ok := a.personas.Persona(c.PersonaID) if !ok || p == nil { return nil, false } return p, true } // identityInstructionFor 返回 persona 的身份块 —— design/01 §2.2 之前唯一注入的 // 部分:优先 identity.systemPrompt,其次 identity.role,再退回最小兜底文本。 func identityInstructionFor(p *loader.Persona) string { if p.Identity.SystemPrompt != "" { return p.Identity.SystemPrompt } if p.Identity.Role != "" { return p.Identity.Role } return fallbackInstruction } // instructionsFromPersona 构建 persona 的 system 指令:身份块,后接此前无人消费的 // personality/values/speechStyle/catchphrase/background 各段(design/01 §2.2)。 // 空白字段会被跳过,因此不会产生空段;这些字段全空的 persona 产出与身份块完全一致 // —— 与既有输出逐字节相同。 func instructionsFromPersona(p *loader.Persona) string { base := identityInstructionFor(p) sections := personaSections(p) if len(sections) == 0 { return base } var b strings.Builder b.WriteString(base) for _, s := range sections { b.WriteString("\n\n") b.WriteString(s.header) b.WriteString("\n") b.WriteString(s.body) } return b.String() } // personaSections 按固定顺序渲染五个可选段(personality → values → speechStyle // → catchphrase → background),跳过其中为空的。 func personaSections(p *loader.Persona) []personaSection { out := make([]personaSection, 0, 5) appendSection := func(header, body string) { if body = strings.TrimSpace(body); body == "" { return } out = append(out, personaSection{header: header, body: body}) } appendSection(personalityHeader, p.Personality) appendSection(valuesHeader, bulletList(p.Values)) appendSection(speechStyleHeader, p.SpeechStyle) appendSection(catchphraseHeader, p.Catchphrase) appendSection(backgroundHeader, p.Background) return out } // bulletList 把字符串项渲染成 markdown 无序列表,跳过空白项;全空的列表产出 "" // 使对应段彻底消失。 func bulletList(items []string) string { lines := make([]string, 0, len(items)) for _, item := range items { if item = strings.TrimSpace(item); item != "" { lines = append(lines, "- "+item) } } return strings.Join(lines, "\n") } // defaultThinkingOptions 返回循环应为默认 persona 注入的推理开关:thinking 指令、 // reasoning_effort 取值与 thinking 广播开关。它是循环每轮所做的按角色解析的默认 // 一半(Options.PersonaKnobs,见下方 personaKnobs),因此配置的默认值与每轮查询 // 永远不会漂移。 // // 只有当 persona 配置了会影响请求的内容(预算、effort,或显式的开关模式)时 // thinking 指令才非 nil。showToUser 刻意不产生指令:它只是呈现开关,而一个没有 // 开关的非 nil 指令会顺带打开供应商侧 thinking。未注册 persona 时三者均为零值, // 循环既有行为不受影响。 func (a *app) defaultThinkingOptions() (*model.Thinking, string, *bool) { knobs, ok := a.personaKnobs(a.defaultPersonaID()) if !ok { return nil, "", nil } return knobs.Thinking, knobs.ReasoningEffort, knobs.ThinkingShowToUser } // personaKnobs 为循环解析某个角色的推理开关(loop.Options.PersonaKnobs, // design/22 §4.2):循环会为即将构建请求的那一轮询问其角色,因此绑定角色 Y 的 // 会话运行时用的是 Y 的 thinking 预算 / effort / 广播开关,而不是默认角色的。 // 从角色 id 解析,而不是把取值推入每个调用方的 SessionEnv,意味所有路径 —— Web // REST/WS/语音、IM、backthink、子代理 —— 都由同一个输入拿到正确的开关。 // ok=false 表示「角色未知」,循环沿用其配置的默认值。 func (a *app) personaKnobs(personaID string) (loop.LoopKnobs, bool) { if personaID == "" { return loop.LoopKnobs{}, false } p, ok := a.personas.Persona(personaID) if !ok || p == nil { return loop.LoopKnobs{}, false } thinking, effort := personaThinking(p) return loop.LoopKnobs{ Thinking: thinking, ReasoningEffort: effort, ThinkingShowToUser: p.Thinking.ShowToUser, }, true } // personaName 把角色 id 解析成其展示名,供模型上下文的说话人归属使用 // (loop.Options.PersonaName,design/22 §7-5)。循环把发话角色写成 // 「「昔涟」(xilian)」而不是裸 id,使模型在会话切换角色后仍能跟上谁说了什么。 // id 为空或未知时解析为 "",循环退回只写 id —— 归属不会因为名字解析不出就消失, // 且这里没有任何身份或权限输入。 func (a *app) personaName(personaID string) string { if personaID == "" { return "" } p, ok := a.personas.Persona(personaID) if !ok || p == nil { return "" } return strings.TrimSpace(p.Name) } // personaThinking 把 persona 的 `thinking` 段映射到模型层开关(design/01 §2.1 / // design/04 §3.2)。没有任何内容影响请求时返回 (nil, "")。 func personaThinking(p *loader.Persona) (*model.Thinking, string) { effort := strings.TrimSpace(p.Thinking.Effort) enable := p.Thinking.Enable() if p.Thinking.BudgetTokens == 0 && effort == "" && enable == nil { return nil, "" } return &model.Thinking{BudgetTokens: p.Thinking.BudgetTokens, Enable: enable}, effort }