Files
Cyrene_v2/cmd/cyrene/persona_prompt.go
T
AskaEth 52f08dc343
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
docs(cyrene): cmd/cyrene 存量英文注释中文化(22 个非测试文件,仅注释)
- 四组并行完成:main/wire*.go、wire_capacity|channels|runtime、channels|persona_*|emotion|observability、
  voice|plugins|stream|skills|vision|subagent_tool|extractor|mcp|sessionread_tool|terminal_tool。
- 机械校验(git diff -U0 去 // 行与空行)仅剩 wire_frontend.go:84 一处行尾注释:代码前缀
  「\ta.gw = gw 」经 cat -A 与 HEAD 逐字节比对一致(allow 清单已声明)。
- 验收:隔离 worktree(HEAD + 本批 patch)go build/vet 绿、全树 58 包 ok、
  linux/amd64 + darwin/arm64 交叉编译绿;gofmt -l 干净。
- 备注:整树两次红均为负载敏感 flake(backthink / orchestration 的 timeout 类用例单独 -count=20 全过)。
2026-10-04 16:58:06 +08:00

168 lines
6.2 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 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
}