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 6547506704
commit fd03e8b905
6 changed files with 217 additions and 34 deletions

View File

@ -395,10 +395,8 @@ func (a *Agent) prepareInputTask(evt *agentIO.InputEvent) (*TaskFrame, taskTermi
}
a.publishEvent(events.EventRawInput, rawPayload)
archived := a.pruneOnInput(evt, cleanInput)
if archived > 0 {
log.Printf("[agent] pruned %d low-relevance events to document memory", archived)
}
// 裁剪与审计统一在 memoryPass 内(日志已带 trigger)。
a.pruneOnInput(evt, cleanInput)
// 本轮 inputch(处理表按它记账)+ contextfull 检测(只有驻留子设了钩子)。
a.tableMu.Lock()
@ -746,7 +744,8 @@ func (a *Agent) stepToolAfter(f *TaskFrame) stepOutcome {
// 工具后处理:一次相关性过程,两个**正交**声明——
// ContextPolicy=prune → 裁剪(踢出去,归档低相关 L0 事件)
// RecallPolicy=auto → 召回(取进来,注入 L2/L3 相关记忆)
// 两者共用同一份**清洗后**的 query:查询向量取清洗后的有效内容,否则噪声
// 两者共用同一份**清洗后**的 query,并统一走 memoryPass(同一入口、
// 同一次预算与审计)。查询向量取清洗后的有效内容,否则噪声
// (ANSI/base64/JSON 包装)会把相关性打分带偏,裁错事件、召回错记忆。
var recallText string
if def := a.stageHost.ToolDef(tc.Name); def != nil {
@ -754,16 +753,7 @@ func (a *Agent) stepToolAfter(f *TaskFrame) stepOutcome {
needRecall := def.RecallPolicy == sdk.RecallPolicyAuto
if needPrune || needRecall {
query := a.toolOutputForQuery(tc.Name, result)
if needPrune && a.context != nil {
topK := a.maxContextSize - 1
if topK < 1 {
topK = 1
}
a.context.Prune(query, topK, a.docStore)
}
if needRecall {
recallText = a.recallTextFor(query, "tool:"+tc.Name)
}
recallText = a.memoryPass(query, "tool:"+tc.Name, needPrune, needRecall).RecallText
}
}