Files
HomeAgent/internal/agent/core/inputch.go
JianFeeeee b792a94b84 feat(memory): 新增可声明的召回轴 RecallPolicy(与 prune 正交)
问题:召回(把 L2/L3 相关记忆注入本轮)此前不可声明、也不受任何 SDK 字段
控制——它只在任务开始时对 f.Input 无条件跑一次。于是 qq_get_message 取回
真实正文后只触发 Prune(裁剪),从不触发召回;而中断通知的 meta 文本反而
会去召回(词不对题,命中一堆泛实体)。

改动:
- SDK 新增 RecallPolicy(none|auto) 轴,落在 InjectOptions / ChannelDef /
  ToolDef 三个声明面,与 ContextPolicy 正交(裁剪 vs 召回)。默认值与
  prune 刻意相反:输入/注入默认 auto(保持既有「每条输入都召回」),
  工具默认 none(工具输出多为噪声,按需声明)。
- 内核:recallDeclared 按 注入点 > 通道 > 默认auto 解析;输入侧用它决定
  是否注入记忆索引;工具侧 ContextPolicy/RecallPolicy 共用同一份清洗后
  query,一次相关性过程分别 prune / recall;召回以 system 消息挂到消息
  末尾(同任务内替换而非累加)。
- 管线:proc RPC(inject/register + 校验)、lua 键、io payload 全量透传。
- QQ 插件:中断与 qq 通道声明 RecallPolicy=none(meta 不是内容);
  qq_get_message 声明 RecallPolicy=auto(取回正文后据正文召回)。

测试:新增 recallpolicy_test.go(core)与 proc 校验用例;
go build ./... 通过,go test ./internal/... 全通过,SDK 模块与 qq 插件测试通过。
2026-09-14 23:14:38 +08:00

171 lines
4.9 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 core
// inputch 总览:**单工具多视图**。
//
// inputch 是**最基本的输入路由单位**(由插件注册,一个插件可注册多个)。
// 父 agent 需要能看清两件事:
// 1. 有哪些 inputch 已注册(谁注册的);
// 2. 它们是怎么划分的(各自划给了哪个 agent、容量多少
//
// 按用户要求做成**单工具多视图**(一个 `input_channels` 工具 + `view` 参数),
// 而不是一堆小工具 —— 视图切换比工具增殖更好用,也更省提示词预算。
import (
"fmt"
"sort"
"strings"
agentAPI "gitcode.com/JianFeeeee/HomeAgent/internal/agent/api"
agentIO "gitcode.com/JianFeeeee/HomeAgent/internal/agent/io"
)
func (a *Agent) executeInputChannels(tc agentAPI.ToolCall) string {
view, _ := tc.Arguments["view"].(string)
view = strings.TrimSpace(view)
if view == "" {
view = "all"
}
name, _ := tc.Arguments["name"].(string)
all := a.io.InputChannels()
if len(all) == 0 {
return "没有任何已注册的 inputch。"
}
switch view {
case "all":
return a.renderInputChannels(all, "全部已注册 inputch")
case "mine":
return a.renderInputChannels(a.channelRegistry().ListByOwner(string(a.id)),
"划给本 agent"+string(a.id)+")的 inputch")
case "unassigned":
return a.renderInputChannels(a.channelRegistry().ListByOwner(""),
"尚未划出的 inputch可按需分配")
case "by_agent":
return a.renderInputChannelsByAgent(all)
case "detail":
if name == "" {
return "view=detail 需要 name 参数inputch 名)"
}
ch, ok := a.io.LookupInputChannel(name)
if !ok {
return fmt.Sprintf("inputch %q 未注册", name)
}
return renderInputChannelDetail(ch)
default:
return fmt.Sprintf("未知 view=%q可用all | mine | unassigned | by_agent | detail", view)
}
}
func (a *Agent) channelRegistry() *agentIO.ChannelRegistry { return a.io.ChannelRegistry() }
// renderInputChannels 渲染一组 inputch 的一行式概览。
func (a *Agent) renderInputChannels(list []agentIO.InputChannel, title string) string {
if len(list) == 0 {
return title + ":无"
}
var b strings.Builder
fmt.Fprintf(&b, "%s%d 个):", title, len(list))
for _, ch := range list {
fmt.Fprintf(&b, "\n - %s%s%s", ch.Name, pluginSuffix(ch), policySuffix(ch))
fmt.Fprintf(&b, "\n 归属: %s", ownerLabel(ch.Owner))
if ch.Capacity > 0 {
fmt.Fprintf(&b, " | 容量: %d", ch.Capacity)
}
if ch.Output != "" {
fmt.Fprintf(&b, " | 默认回程: %s", ch.Output)
}
}
return b.String()
}
// renderInputChannelsByAgent 按归属分组("划分情况"总览)。
func (a *Agent) renderInputChannelsByAgent(all []agentIO.InputChannel) string {
byOwner := map[string][]agentIO.InputChannel{}
for _, ch := range all {
byOwner[ch.Owner] = append(byOwner[ch.Owner], ch)
}
owners := make([]string, 0, len(byOwner))
for o := range byOwner {
owners = append(owners, o)
}
sort.Strings(owners)
var b strings.Builder
fmt.Fprintf(&b, "inputch 划分情况(共 %d 个):", len(all))
for _, o := range owners {
names := make([]string, 0, len(byOwner[o]))
for _, ch := range byOwner[o] {
names = append(names, ch.Name)
}
sort.Strings(names)
fmt.Fprintf(&b, "\n - %s: %s", ownerLabel(o), strings.Join(names, ", "))
}
return b.String()
}
// renderInputChannelDetail 渲染单个 inputch 的全部字段。
func renderInputChannelDetail(ch agentIO.InputChannel) string {
var b strings.Builder
fmt.Fprintf(&b, "inputch: %s\n", ch.Name)
fmt.Fprintf(&b, " 注册插件: %s\n", orDash(ch.Plugin))
fmt.Fprintf(&b, " 归属 agent: %s\n", ownerLabel(ch.Owner))
if ch.Capacity > 0 {
fmt.Fprintf(&b, " 容量: %d\n", ch.Capacity)
} else {
fmt.Fprintf(&b, " 容量: 内核默认\n")
}
fmt.Fprintf(&b, " 默认回程输出通道: %s\n", orDash(ch.Output))
fmt.Fprintf(&b, " 记忆策略: %s\n", policyLabel(ch))
return b.String()
}
func pluginSuffix(ch agentIO.InputChannel) string {
if ch.Plugin == "" {
return ""
}
return "(插件 " + ch.Plugin + ""
}
// policySuffix 用短标记提示策略(详见 view=detail
func policySuffix(ch agentIO.InputChannel) string {
var m []string
if ch.Def.NoMemory {
m = append(m, "无记忆")
}
if ch.Def.Cleaner != nil {
m = append(m, "清洗")
}
if ch.Def.ContextPolicy != "" && ch.Def.ContextPolicy != "none" {
m = append(m, "裁剪:"+ch.Def.ContextPolicy)
}
if ch.Def.RecallPolicy != "" && ch.Def.RecallPolicy != "auto" {
m = append(m, "召回:"+ch.Def.RecallPolicy)
}
if len(m) == 0 {
return ""
}
return " [" + strings.Join(m, "/") + "]"
}
func policyLabel(ch agentIO.InputChannel) string {
if s := policySuffix(ch); s != "" {
return strings.Trim(s, " []")
}
return "默认(记入记忆、不裁剪)"
}
func ownerLabel(owner string) string {
if owner == "" {
return "未分配(根 agent/内核默认)"
}
return owner
}
func orDash(s string) string {
if s == "" {
return "-"
}
return s
}