package engine // norm_consecutive_role.go -- 合并连续的同角色消息. // // 来源:早期方案第 3 步(mergeConsecutiveSameRole). // 场景:API 要求消息交替出现(user/assistant/user/assistant), // 如果出现连续的同角色消息,将它们的内容块合并到一条消息中. // // 精妙之处(CLEVER): 即使 Anthropic 1P API 允许连续 user 消息, // 合并后对模型注意力更好(一个 user turn 内容集中). // 跨场景通用:任何 provider 都受益.Bedrock 不支持连续同角色, // 这个步骤保证了 provider 兼容性. import ( "git.flytoex.net/yuanwei/flyto-agent/pkg/query" ) // ConsecutiveRoleMerger 合并连续的同角色消息. type ConsecutiveRoleMerger struct{} func (m *ConsecutiveRoleMerger) Name() string { return "consecutive_role" } func (m *ConsecutiveRoleMerger) Priority() int { return 30 } func (m *ConsecutiveRoleMerger) Normalize(messages []query.Message) []query.Message { if len(messages) <= 1 { return messages } result := make([]query.Message, 0, len(messages)) result = append(result, cloneMessage(messages[0])) for i := 1; i < len(messages); i++ { last := &result[len(result)-1] if last.Role == messages[i].Role { // 同角色,合并内容块 last.Content = append(last.Content, messages[i].Content...) // 合并 metadata(后者覆盖前者) if messages[i].Metadata != nil { if last.Metadata == nil { last.Metadata = make(map[string]any) } for k, v := range messages[i].Metadata { last.Metadata[k] = v } } } else { result = append(result, cloneMessage(messages[i])) } } return result } // cloneMessage 深拷贝消息,防止调用者修改 Content.Input 时污染原始数据. // // 精妙之处(CLEVER): Content slice 做元素级拷贝,对含 Input map 的 tool_use // 块单独深拷贝一层--tool input 是 JSON 解析结果,值本身是基础类型(string/ // number/bool/nil),不会出现嵌套 map,一层深拷贝足够. // 替代方案:完全浅拷贝(早期方案做法)--合并后两条消息共享同一个 Input map // 引用,下游若修改 Input(如 tool input 脱敏)会静默污染历史消息. func cloneMessage(msg query.Message) query.Message { clone := query.Message{ Role: msg.Role, Content: make([]query.Content, len(msg.Content)), Time: msg.Time, } for i, c := range msg.Content { clone.Content[i] = c // tool_use 的 Input 是 map,需要深拷贝一层,防止共享引用被下游修改. // LEGACY: 早期方案直接 copy(clone.Content, msg.Content),Input map 浅拷贝共享. if c.Input != nil { deep := make(map[string]any, len(c.Input)) for k, v := range c.Input { deep[k] = v } clone.Content[i].Input = deep } } if msg.Metadata != nil { clone.Metadata = make(map[string]any, len(msg.Metadata)) for k, v := range msg.Metadata { clone.Metadata[k] = v } } return clone }