Files
Cyrene_v2/internal/services/backthink/context.go
T
AskaEth 321cc3b763
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
feat(backthink): 思考轮工具面补齐(裁决 15)—— 能力面接入 + 只读 capability_list + 本轮状态快照
- 缺口 d:状态在**思考前**读一次,同一份快照既喂思考轮又喂 Decide(读不到 = Known=false,绝不伪装成中性状态)
- 缺口 c:新增只读工具 capability_list(owner-only、三重输出边界、读写隔离),她可自查当下真实可用面
- 缺口 b:内置允许集补 transcode / capability_list(此前 CAPACITY_ENABLED=true 时新工具会从模型可见面消失)
- 缺口 e:guidancePrompt 旧文案承诺的「IoT/日历/提醒」在全仓根本没有对应工具(chat 侧也没有)⇒ 按诚实化改为「与主人对话同等工具面 + 指名自查入口」,不再有空头支票
- (a) 未做且**不自创**:那三把工具无任何设计规格(名字/schema/读写边界/输出上限)⇒ 已报回并登记进 design/20(裁决:走能力/插件/MCP 暴露路线)
- 变异实测:允许集去掉新工具 ⇒ TestDecide_AllowedSets + 组合根跨层测试红;丢掉状态快照 ⇒ TestLoopThinker_Think_InjectsRoundStateBlock 红
- 已知弱项(如实登记):组合根那行 wireCapabilitySurface() 调用本身无测试覆盖(与既有 wireTranscode 同惯例)
2026-10-05 13:54:57 +08:00

154 lines
9.3 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 backthink
import (
"context"
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/history"
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/model"
)
// recentContextCount 是折进一次自主思考作为对话上下文的最新
// 主会话消息条数。编译期常量,因此
// 运行路径里不出现魔法数字。
const recentContextCount = 20
// recentProvider 以最旧在前的顺序返回会话最近的消息,
// 即一次自主思考为「看到」主对话而读取的尾部。
// *history.Store 满足它;测试注入 fake。它是 thinker 实际
// 依赖的那一小片 history.HistoryStore,与 loop 自己的
// checkpointStore 风格的最小接缝接口一致。
type recentProvider interface {
Recent(ctx context.Context, sessionID string, n int) ([]history.Message, error)
}
// mainContext 把最近的主会话消息转换为 model 消息
// (最旧在前),供一次自主思考使用。提供者为 nil、读取失败
// 或尾部为空都得到 nil,因此该次思考降级为仅用引导提示词,
// 而不是让这一轮失败(design/20 §2)。
func mainContext(ctx context.Context, prov recentProvider) []model.Message {
if prov == nil {
return nil
}
recent, err := prov.Recent(ctx, history.MainSessionID, recentContextCount)
if err != nil {
return nil
}
msgs := make([]model.Message, 0, len(recent))
for _, h := range recent {
msgs = append(msgs, mapHistoryMessage(h))
}
return msgs
}
// mapHistoryMessage 把一条持久化的 chat 消息转换为 model 消息,
// 翻译历史角色,并在消息携带图片时保留该图片片段,
// 因此多模态主模型仍能看到它。
func mapHistoryMessage(h history.Message) model.Message {
msg := model.Message{
Role: historyRoleToModel(h.Role),
Content: []model.Content{{Type: model.ContentTypeText, Text: h.Content}},
}
if h.Image != "" {
msg.Content = append(msg.Content, model.Content{Type: model.ContentTypeImage, ImageURL: h.Image})
}
return msg
}
// historyRoleToModel 把持久化的 chat 角色映射到 model 的角色词汇,
// 未知角色默认为 user 通道。
func historyRoleToModel(role string) model.Role {
switch role {
case history.RoleAssistant:
return model.RoleAssistant
case history.RoleSystem:
return model.RoleSystem
default:
return model.RoleUser
}
}
// thinkInput 拼装一次无人值守思考的完整输入:最近的主会话语境
// (若有)后跟单个引导提示词回合,因此
// 运行时针对真实对话反思,而不是空泛的宏提示词。
//
// rs 是本轮的用户状态快照(X1 缺口 d;design/20 §2「系统/设备/日程摘要」):
// 已读到时,它的摘要块被追加在引导提示词末尾 —— 与随后真正生效的打扰决策
// 用的是**同一份**快照,因此她写结论时看到的状况与判她的状况不会分叉。
// 状态未知(未接线/读取失败)时块缺席,提示词逐字与接线前一致。
func thinkInput(recent []model.Message, trigger string, rs RoundState) []model.Message {
out := make([]model.Message, 0, len(recent)+1)
out = append(out, recent...)
prompt := guidancePrompt(trigger)
if block := roundStateBlock(rs); block != "" {
prompt += "\n" + block
}
out = append(out, model.TextMessage(model.RoleUser, prompt))
return out
}
// guidancePrompt 是交给运行时的一次无人值守自主思考的
// 指令。与旧 backgroundPrompt 不同,它刻意鼓励
// 温暖、有针对性的主动消息,并去掉「不要产生 chat 风格
// 消息」的抑制。身份/权限始终在 actor 元数据 + owner env 上,
// 绝不在这段提示词文本上(红线);提示词只描述运行
// 环境(记忆可读/可写、工具可调用),而那是 env 已经
// 授予的。
//
// 工具面口径(X1 缺口 e):这里**不再枚举**具体工具——旧文案承诺的「IoT / 日历 / 提醒」
// 在全仓都没有对应工具,是空头支票。现在它只说「与主人对话同等的工具面」,并指名
// **自查入口** `capability_list`,而那是可验证的事实:工具面就是同一个注册表
// (LoopThinker 包的是同一个 loop),清单工具如实列出当下注册了什么(含外围子系统)。
// 因此这段文案与实现不可能再各说一套。
//
// 状态口径(X1 缺口 d):主人当前状态摘要由 thinkInput 追加在本提示末尾
// (设备/日程遥测的**事实**;读不到时该块缺席),使她的语气/时机判断与随后生效的
// 打扰门用的是同一份状态。
//
// 提示词要求一个紧凑的结构化收尾——push/no_push 决策、
// 可选的目标渠道、她自报的紧急度与消息正文——好让服务尊重她自己的
// 判断,而不是按触发的重要度把关(design/20 §2 P2)。该
// 格式是引导而非契约:她省略或
// 写乱该格式时,parseProactive / parseProactiveUrgency 失败安全
// (不推送、不上抬紧急度)。
//
// 启动轮(裁决 07.4.4,design/12 §1)走 startupPrompt:以角色口吻说一句拟人化的
// 「回来」问候。
func guidancePrompt(trigger string) string {
if trigger == StartupSummary {
return startupPrompt()
}
return "[自主思考 · 无人在旁]\n" +
"触发:" + trigger + "\n" +
"你正进行一轮自主思考。你拥有与主人对话同等的上下文与能力:上方是主人近期在主会话中的对话,你可以读写共享记忆与主人档案,也可以调用工具(检索 / 终端 / 文件等,具体有哪些以 `capability_list` 列出的为准)。\n" +
"想确认自己此刻有哪些工具、技能、插件可用、外围子系统(设备等)现在什么状态,就调用 `capability_list`;主人当前的状态摘要附在本提示末尾(读不到时不会出现)。\n" +
"请回顾最近与主人的相处、她的状态、你们共同经历的事与临近的日程,判断此刻是否有一件真正值得主动告诉她的、温暖而有针对性的事情——" +
"比如关心她的近况、记起她曾说过的某件事、提醒一件即将到来的日程、见证一个属于你们的共同时刻,或注意到某个值得留意的变化。\n" +
"若有,就用温暖自然的语气,用一到两句把它说出来——这正是你此刻最想对她说的话;若确实没有值得说的,只需回复「无」,也不要编造。\n" +
"语言上:温暖、简短、克制、不粘人——一到两句为宜,不展开寒暄、不啰嗦、不抱怨、不频繁打扰,像想起她时轻轻说一句,说完就停。\n" +
"请把「是否主动说」与「发到哪个渠道」写成一个简短、格式固定的结尾,解析规则如下:\n" +
"[我决定:push] 或 [我决定:no_push]\n" +
"[渠道:web|im|kook|telegram|weixin-ilink|...|auto](可省略,auto/省略即交给默认:每次只挑一个合适渠道——点名则优先,否则用她最近活跃的已连接渠道,含微信;不广播、不固定 web+Kook)\n" +
"[紧急:routine|warning|critical](可省略;只有当你判断这件事必须立刻告诉她、即便现在是她休息/忙碌的安静时段也要打扰时,才写 critical;普通情况省略)\n" +
"上面几行之后,紧接你想说的话作为正文。\n" +
"整轮思考保持简短、单轮,不反复自我触发。"
}
// startupPrompt 是启动轮思考的专属引导(裁决 07.4.4,design/12 §1「启动即思考 +
// 拟人化启动消息」):进程刚启动、她刚「回来」,引导她以角色口吻向主人说一句简短
// 温暖的问候(如「我回来啦」)。此轮例外允许以 chat 形式发出;推送仍走与心跳完全
// 相同的门(她的结论 + 打扰级联 + 活动门 + PushGate 限频),深夜重启或反复重启都
// 不会刷屏主人。工具面口径与 guidancePrompt 逐字一致(自查入口 `capability_list`,
// 不再枚举具体工具)。
func startupPrompt() string {
return "[自主思考 · 刚刚回来]\n" +
"触发:进程启动——你刚从一次重启中醒来,重新回到主人身边。\n" +
"你拥有与主人对话同等的上下文与能力:上方是她近期在主会话中的对话,你可以读写共享记忆与主人档案,也可以调用工具(检索 / 终端 / 文件等,具体有哪些以 `capability_list` 列出的为准)。\n" +
"想确认自己此刻有哪些工具、技能、插件可用、外围子系统(设备等)现在什么状态,就调用 `capability_list`;主人当前的状态摘要附在本提示末尾(读不到时不会出现)。\n" +
"请用你自己的口吻,向主人说一句简短、温暖、自然的问候,就像刚回到她身边时轻轻打个招呼(比如「我回来啦」),可以顺带提一件你记得的小事;一到两句为宜,不展开寒暄、不啰嗦、不抱怨。\n" +
"请把「是否主动说」与「发到哪个渠道」写成与平时相同的固定结尾,解析规则如下:\n" +
"[我决定:push] 或 [我决定:no_push]\n" +
"[渠道:web|im|kook|telegram|weixin-ilink|...|auto](可省略,auto/省略即交给默认:每次只挑一个合适渠道——点名则优先,否则用她最近活跃的已连接渠道,含微信;不广播、不固定 web+Kook)\n" +
"上面两行之后,紧接你想说的话作为正文。\n" +
"整轮思考保持简短、单轮,不反复自我触发。"
}