Files
HomeAgent/internal/agent/core/memorypass.go
JianFeeeee 3a780384d0 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 插件模块测试通过。
2026-09-14 23:32:41 +08:00

66 lines
2.8 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
import "log"
// memoryPassOut 是一次记忆操作(取进来 / 踢出去)的结果。
type memoryPassOut struct {
// Archived 是被归档进文档记忆的低相关 L0 事件数prune 的输出)。
Archived int
// RecallText 是可注入 prompt 的记忆索引文本recall 的输出,空串表示无)。
RecallText string
}
// memoryPass 是「取进来(召回)」与「踢出去(裁剪)」的**唯一入口**。
//
// prune 与 recall 是两根正交的声明轴(默认值刻意相反:裁剪是破坏性的、
// 默认关;召回是只读增量、默认开),但两者都建立在**同一份清洗后的 query**
// 之上。调用点只负责解析声明,这里统一做三件各写一遍就会写歪的事:
//
// 1. 同一 queryprune 与 recall 用同一个查询向量来源,避免「裁错事件、
// 召回错记忆」——原始内容里的 ANSI/base64/JSON 噪声会把相关性打分带偏。
// 2. 同一次预算:召回文本按 token 预算截断只做一次(见 recallText
// 3. 同一条审计trigger据什么触发了哪种操作都落一条日志
// 否则又是一个「幕后发生、查不出是谁」的机制(对齐 prune 的设计初衷)。
//
// 边界prune 与 recall 目前仍走**各自的相关性空间**prune 用 L0 事件的
// 稠密/词向量给已有事件打分recall 用图 + TF-IDF 实体索引)。真正的
// 「一次打分」要先统一打分空间(后续步骤);这里统一的是**入口、query、
// 预算与审计**——这已是「一个过程」的可审计外壳,剩下的差在打分空间。
func (a *Agent) memoryPass(query, trigger string, prune, recall bool) memoryPassOut {
var out memoryPassOut
if a == nil || (!prune && !recall) {
return out
}
if prune {
out.Archived = a.pruneByQuery(query)
}
if recall && query != "" {
out.RecallText = a.recallTextFor(query, trigger)
}
if out.Archived > 0 || out.RecallText != "" {
log.Printf("[agent] memory pass (%s): archived=%d recalled=%d chars",
trigger, out.Archived, len(out.RecallText))
}
return out
}
// pruneByQuery 按相关性把低相关 L0 事件归档进文档记忆,返回归档数。
//
// **不做声明判定**——声明已由调用方pruneOnInput / stepToolAfter解析
// 这里只负责执行。放在 memoryPass 内部是为了让裁剪与召回共享入口。
func (a *Agent) pruneByQuery(query string) int {
if a == nil || a.context == nil {
return 0
}
// **动态上下文**是父 agent 专属能力:轻量内核(驻留子)用传统上下文,
// 不做按相关度的裁剪与向 doc 记忆的归档(子也没有 doc 记忆)。
if a.isLightKernel() {
return 0
}
topK := a.maxContextSize - 1
if topK < 1 {
topK = 1
}
return a.context.Prune(query, topK, a.docStore)
}