refactor(memory): 裁剪与召回收敛到唯一入口 memoryPass

把「踢出去(prune)」与「取进来(recall)」从两处各写一遍,收敛为
memoryPass(query, trigger, prune, recall) 单一入口,统一:

- 同一份清洗后的 query(避免噪声带偏相关性打分);
- 同一次 token 预算与召回截断;
- 同一条带 trigger 的审计日志(谁、据什么触发了哪种操作)。

落地:
- 新增 memorypass.go:memoryPass + pruneByQuery(原 pruneOnInput 的执行体);
- pruneOnInput 只解析声明,执行委托 memoryPass;
- stepToolAfter 的裁剪/召回改为一次 memoryPass 调用(去掉重复的 topK 逻辑);
- 抽出 recallText,输入侧 buildTaskMemoryContext 与工具侧 recallTextFor 共用;
- 输入侧召回 query 改用 CleanInput(清洗文本),与裁剪侧同一语义;
- QQ qq_get_history 补齐声明 ContextPolicy=prune + RecallPolicy=auto
  (内容类工具:真实聊天正文既当轮用完即裁,又据正文召回)。

测试:新增 memorypass_test.go,锁死 no-op / 两轴同时生效 / 正交不互相触发 /
输入侧用清洗 query。go build/vet 干净,internal/... 全绿,qq 插件模块测试通过。
This commit is contained in:
JianFeeeee
2026-09-14 23:32:41 +08:00
parent b792a94b84
commit 3a780384d0
6 changed files with 217 additions and 34 deletions

View File

@ -44,27 +44,49 @@ func (a *Agent) buildMemoryContext(input string, maxTokens int) string {
//
// 默认 auto(保持“每条输入都召回”的既有行为);输入/注入声明
// recall_policy=none 时返回空串,从而不注入记忆。策略与裁剪(ContextPolicy)正交。
//
// query 取**清洗后**的输入(通道 Cleaner 的输出),与裁剪侧同一套语义:
// 原始输入里的 ANSI/base64/JSON 包装会把相关性打分带偏。清洗为空时回退原文。
func (a *Agent) buildTaskMemoryContext(f *TaskFrame, input string, maxTokens int) string {
if f != nil && !a.recallDeclared(f.Evt) {
if f == nil {
return a.recallText(input, "input", maxTokens)
}
if !a.recallDeclared(f.Evt) {
return ""
}
return a.buildMemoryContext(input, maxTokens)
query := strings.TrimSpace(f.CleanInput)
if query == "" {
query = input
}
trigger := "input"
if f.Evt != nil && f.Evt.Source != "" {
trigger = "input:" + f.Evt.Source
}
return a.recallText(query, trigger, maxTokens)
}
// recallTextFor 以 query 触发一次记忆召回,返回可注入的文本(空串表示无)。
// recallTextFor 以 query 触发一次记忆召回,按当前预算截断,返回可注入的文本。
//
// 这是“召回”侧的单一入口:与 Prune 共用同一份**清洗后**的 query,
// 使“取进来”(召回)与“踢出去”(裁剪)落在同一个相关性过程上。
// trigger 仅用于日志溯源(如 "tool:qq_get_message")。
func (a *Agent) recallTextFor(query, trigger string) string {
if query == "" || a.indexer == nil {
return ""
}
memTokens := 0 // 0 = 不截断
if a.provider != nil {
if a != nil && a.provider != nil {
memTokens = ComputeTokenBudget(a.provider, a.systemPrompt).MemoryTokens
}
text := a.buildMemoryContext(query, memTokens)
return a.recallText(query, trigger, memTokens)
}
// recallText 是召回侧的共同实现:query → 记忆索引文本(空串表示无)。
//
// 输入侧的 buildTaskMemoryContext 与工具侧的 recallTextFor 都收敛到这里,
// 使“同一份 query、同一次预算、同一条审计日志”只写一遍。
func (a *Agent) recallText(query, trigger string, maxTokens int) string {
if a == nil || query == "" || a.indexer == nil {
return ""
}
text := a.buildMemoryContext(query, maxTokens)
if text == "" {
return ""
}