mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-11-01 14:57:38 +00:00
用户指出(2026-10-22):doc_query 的结果走 NoMemory 路径,否则会对记忆块
产生自激震荡。核实后确认**两个入口都开着**。
## 回路
doc_query
├─ ① 工具返回值含被召回事件的正文片段 → 作为 ToolResults 进 context
│ → 参与向量化/关键词/蒸馏 → 同一内容被**重新注入**记忆
├─ ② 回填事件本身(带原 Source、完整 Blocks)进 L1
│ → Prune 因相关而保留它们(挤掉真正的新事件)
│ → 下一轮 Prune 又把它们归档回 L2
└─ ⇒ 下次 doc_query 再召回同一批:块在 L1↔L2 之间反复搬迁
## ① 工具输出:doc_query 未声明 NoMemory
基础设施早就有(textForVector 里按 ToolDef.NoMemory 跳过工具输出),
但 doc_query 是**内置工具**,它的声明无处安放:
StageHost.ToolDef 只覆盖插件工具,内置工具由 executeToolCallInner 的
switch 分派,不经过注册。
修法:
- internal/sdk/tool.go:BuiltinToolDef 加 NoMemory 字段(与 ToolDef.NoMemory
同义,两条路必须能声明同一件事)
- internal/agent/core/tooldefs.go:declareNoMemoryTool + noMemoryOf,
由 init() 里**显式名单**登记 doc_query
- internal/agent/core/context.go:textForVector 同时查插件 ToolDef 与
内置声明表(原来只查前者)
★ 为什么在 init 显式登记、不靠 toolDefWith 构造时登记:
工具表是按需构造的(受 `if a.docStore != nil` 之类条件控制),
而 textForVector 可能在构造之前被调用 ⇒ 声明会"时有时无"。
实测踩到:构造前查 noMemoryOf("doc_query") 得到 false。
★ declareParallelTool 顺带改为**按字段合并**(原来整体覆盖):
同一工具可能同时声明 parallel 与 noMemory,覆盖会让后登记的那项
静默丢失 —— 声明丢一个是最难察觉的一类问题。
## ② 回填事件:加 ContextEvent.NoMemory
ContextEvent 新增 NoMemory 字段(此前只有 ToolDef/ChannelDef 有)。
restoreDocEvents 回填时置位(含存量降级路径),于是:
- textForVector 直接返回 "" ⇒ 不向量化、不参与关键词/蒸馏
- PruneWithProtected 归档前过滤掉它们 ⇒ 不再产生新的 L2 副本
语义与 ToolDef.NoMemory 一致:**原文仍在上下文可读**,只是不进计算、
不再产生新记忆。这是「回填是为了让模型读到原文」的本意。
## 判据(3 条,钉住两个入口 + 一个后果)
selfexcite_test.go:
- DocQueryDeclaresNoMemory:声明可查 + 工具输出不进 textForVector
- RestoredEventsNotReArchived:回填事件带 NoMemory 标记 + Prune 后
L2 里不出现含回填内容的新文档(真跑 Prune 验证回路)
- RestoredEventsDontCrowdOutNewOnes:回填事件不被向量化(否则会
参与相关性打分、挤掉新事件)
变异全部被抓住:
- Prune 不过滤 NoMemory → 「回填事件被 Prune 又归档回了 L2」
- restoreDocEvents 不置 NoMemory → 「没有 NoMemory 标记」+ 回路成立
- init 不登记 doc_query → 「未声明 NoMemory」
## 验证
- go build ./... / go vet ./... 干净
- go test ./... -count=1:58 包 0 失败
- 公开 SDK(third_party/homeagent-sdk)未改动 —— 改的是 internal/sdk
133 lines
6.2 KiB
Go
133 lines
6.2 KiB
Go
package sdk
|
||
|
||
// ToolSource 描述内核工具注册表/执行器的只读视图(由 StageHost 实现)。
|
||
// 用接口而非具体类型,避免 sdk 依赖内核包(内核包反向依赖 sdk)。
|
||
type ToolSource interface {
|
||
GetToolDefs() []ToolDef
|
||
ToolDef(name string) *ToolDef
|
||
ExecuteTool(name string, args map[string]interface{}) (interface{}, error)
|
||
}
|
||
|
||
// ToolAPI exposes the kernel tool registry and executor
|
||
// (StageHost for plugin tools + IOManager for device/channel tools).
|
||
type ToolAPI interface {
|
||
// GetToolDefs returns all tools registered on the stage host (plugin tools).
|
||
GetToolDefs() []ToolDef
|
||
// GetAllTools returns all tools exposed by IO devices/channels.
|
||
GetAllTools() []ToolDef
|
||
// CanUse 报告「执行该工具是否被授权」。
|
||
//
|
||
// 存在的理由(D4):设备类工具的授权闸原本只存在于 core 的
|
||
// executeToolCallInner,即**「agent 收到模型 tool_call」那条路径**。
|
||
// 而 ExecuteTool 是**另一条**独立入口,不经那道闸 —— 于是凡是走
|
||
// ToolAPI 的调用都能绕过 AllowedOutputs。实测范围不止序列:
|
||
// cli 插件的 /terminal 就直接经 ToolAPI 调 agentcli 的终端工具
|
||
// (cli/plugin.go:1038 自陈"ToolAPI 已允许跨插件调用工具")。
|
||
//
|
||
// 语义与内核那道闸**必须一致**(core 的 TestCanUseAgreesWithInnerPath
|
||
// 钉住这一点),否则两条路径判定不同同样是漏洞。
|
||
//
|
||
// 零值实现返回 true:未实现者行为不变(存量插件与测试替身不受影响)。
|
||
CanUse(toolName string, args map[string]interface{}) bool
|
||
// ToolDefByName 按名字查任一来源(StageHost 插件工具 / IO 设备工具 /
|
||
// 内核内置工具)的声明。
|
||
// 插件需要它来在**运行前**判断目标是否存在、是否并发安全 ——
|
||
// 而工具是动态注册的,"不存在"是常态(见 seq 包的 missing 策略)。
|
||
// 查不到返回 nil(调用方按"不存在"处理,不得 panic)。
|
||
ToolDefByName(name string) *ToolDef
|
||
// ExecuteTool executes a tool by name, resolving across the stage host first.
|
||
ExecuteTool(name string, args map[string]interface{}) (interface{}, error)
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// 内核内置工具的注册面(方案 B)
|
||
// ---------------------------------------------------------------------------
|
||
//
|
||
// 背景:`memory_*` / `knowledge_*` / `doc_*` / `person_*` 这 20+ 个工具是
|
||
// **内核内置**的,在 core.executeToolCallInner 里按**前缀分派**,
|
||
// 从不进 StageHost / IOManager ⇒ 插件经 ToolAPI 既查不到也调不了。
|
||
// 真机实跑实证:seq_run 报「工具 knowledge_list 不存在或未注册」,
|
||
// 而同一轮模型直接调 knowledge_list 是成功的。
|
||
//
|
||
// 为什么用**晚绑定注入**而不是让 toolImpl 直接依赖 core:
|
||
// toolImpl 在 internal/sdk,而内置工具的执行依赖 core 的 *Agent
|
||
// (它持有 memory/knowledge/store 等状态,且这些状态**逐 agent 不同**
|
||
// —— 驻留子是轻量内核,memory 为 nil)。sdk 不能依赖 core(方向反了),
|
||
// 且 ToolAPI 是**全局单例**,无法天然携带 per-agent 状态。
|
||
//
|
||
// 注入方(core/bootstrap)负责提供两个函数;未注入时行为与今天完全一致
|
||
// (内置工具在 ToolAPI 上不可见),故存量插件与测试替身不受影响。
|
||
|
||
// BuiltinToolDef 是内核内置工具的声明。
|
||
type BuiltinToolDef struct {
|
||
Name string
|
||
Description string
|
||
Parameters map[string]interface{}
|
||
// ParallelSafe 与 ToolDef.ParallelSafe 同义:声明此工具可被并发执行。
|
||
// 零值 false = 默认串行(保守)。
|
||
//
|
||
// ⚠️ 内核曾经没有这个字段,于是内置工具的并发声明无处安放 ——
|
||
// 先后试过"内核里一张硬编码 map"和"往 required 变参塞字符串"两种错做法。
|
||
// 声明项必须落在**工具自己的结构体**上,与 NoMemory 同构。
|
||
ParallelSafe bool
|
||
// Serial 声明此工具必须串行,优先于 ParallelSafe。
|
||
Serial bool
|
||
// Timeout 声明本工具单次执行允许的最长时间(如 "3m")。
|
||
//
|
||
// 与 ToolDef.Timeout 同义(对外插件 / 内置插件两条路都要能声明)。
|
||
// 留空 = 内核默认 60s。
|
||
Timeout string
|
||
// NoMemory 声明本工具的**输出**不参与记忆计算(向量化/关键词/蒸馏),
|
||
// 但原文仍留在上下文的 ToolResults 里可读。
|
||
//
|
||
// 与 ToolDef.NoMemory 同义(对外插件经 proc 桥、内置工具经这里,
|
||
// 两条路必须能声明同一件事 —— 否则内置工具的输出无法抑制)。
|
||
//
|
||
// ★ 典型用例:doc_query 的返回值里含被召回事件的正文片段。
|
||
// 那些正文**已经在记忆体系里**(来自 L2/L3),若工具的返回文本再
|
||
// 参与一次向量化与蒸馏,就等于把同一内容重新注入记忆 ——
|
||
// 与「回填事件不再归档」是同一个自激回路的两个入口。
|
||
NoMemory bool
|
||
}
|
||
|
||
// ToSchema 转成下发给模型的 function schema。
|
||
func (d BuiltinToolDef) ToSchema() map[string]interface{} {
|
||
return map[string]interface{}{
|
||
"type": "function",
|
||
"function": map[string]interface{}{
|
||
"name": d.Name,
|
||
"description": d.Description,
|
||
"parameters": d.Parameters,
|
||
},
|
||
}
|
||
}
|
||
|
||
// ConcurrencySafe 报告该工具是否可并发执行。
|
||
// Serial 优先:显式声明"必须串行"不允许被 ParallelSafe 或默认值覆盖。
|
||
func (d BuiltinToolDef) ConcurrencySafe() bool {
|
||
return d.ParallelSafe && !d.Serial
|
||
}
|
||
|
||
// BuiltinProvider 由内核注入(导出:core 需实现它)。
|
||
type BuiltinProvider interface {
|
||
// Defs 返回当前 agent 可见的内置工具声明(**已按运行期状态门控**)。
|
||
Defs() []BuiltinToolDef
|
||
// Exec 执行一个内置工具,返回给模型看的文本。
|
||
Exec(name string, args map[string]interface{}) (string, error)
|
||
}
|
||
|
||
var builtinProv BuiltinProvider
|
||
|
||
// SetBuiltinProvider 注入内核内置工具的查询/执行面。
|
||
//
|
||
// 传 nil 表示撤销注入(测试隔离用)。注入后,内置工具对插件可见可调。
|
||
func SetBuiltinProvider(p BuiltinProvider) { builtinProv = p }
|
||
|
||
// builtinDefs 返回注入方提供的内置工具声明(未注入时为空)。
|
||
func builtinDefs() []BuiltinToolDef {
|
||
if builtinProv == nil {
|
||
return nil
|
||
}
|
||
return builtinProv.Defs()
|
||
}
|