mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-10-15 12:15:16 +00:00
feat(memory): 图数据库自报真实读写事件,取代「按工具名猜节点」
## 问题(生产实证)
星图的「跟随 agent 活动」此前靠**工具名词元猜节点**:把 `memory_recall`
拆成 [memory, recall] 去匹配实体名。而图谱里的实体名是**中文概念**
(小宅/对话/待命),工具名是英文 —— 两套命名空间永不交集。
实测(生产库)每个工具名命中 **0** 个节点:
hint=memory_recall → memory/recall → 0
hint=files_read → files/read → 0
hint=knowledge_search → knowledge/search → 0
匹配失败后走「补齐」分支,按 mention_count 取前 8 个 ⇒ **无论 agent 在干什么,
点亮的都是同一批无关节点**(小宅/对话/待命/投递/图片/文档/插件/老大)。
这就是「星图没有读写反馈」的真身。
## 改法:让图数据库在真正读写处上报
新增 `events.EventMemoryAccess` + `GraphDB.AccessHook`,在读写入口埋点上报
**真实块 ID**(不再猜):
| 操作 | 埋点 | 上报 |
|---|---|---|
| 读 | RecallSorted | blocks |
| 写 | commit | blocks + **created**(真正新建的) |
| 删 | DeleteEntity | removed |
| 合 | MergeBlocks | merged(from→to) + removed |
三处关键设计:
1. **钩子用独立锁** `hookMu`。用 `g.mu` 会**自死锁** —— 埋点方法已持该锁,
而 Go 的 RWMutex 不可重入。
2. **上报在锁外**。拆 `commit`→`commitLocked` 两层解决 `defer Unlock` 的时序:
钩子内部可能要回调内存系统(如 SSE 订阅者补数据),持锁调用必死锁。
3. **区分「新建」与「更新」**(`putBlockTx` 返回 isNew)。用 SELECT 判存在性
而非 `RETURNING (xmax = 0)` —— 后者在**事务 + ON CONFLICT** 下报
`no such column: xmax`(实测:autocommit 可用、包进 tx 就不可用),
而本函数只在事务里被调用。重复提交同一事实不会重复播「生长」动画。
接线:`bootstrap.newMainAgent` 里 `memDB.SetAccessHook(...)` → `events.Bus`。
只在 `evBus != nil` 时接,把「内核进程」与「独立 CLI 工具」
(homed-graph-migrate / memgc 也用 NewGraphDB)区分开 —— 后者不该为一次
批量迁移向不存在的订阅者发几万条事件。
## 验证
- `access_hook_test.go`:钉住 commit/recall/delete 三类埋点真的触发、
且重复提交不误报新建
- `access_hook_race_test.go`:4 写 + 4 读 + 钩子热替换并发,`-race` 下无竞态、
无死锁;**断言事件数 > 0**(否则「钩子从未触发」也会假通过)
- 该测试起初是 flaky 的(读者可能在写者落库前查询 ⇒ 空结果不报事件 ⇒
断言随机失败)。修法是先播种确定数据再并发读,已连跑 3 次稳定
- 生产实例端到端:CLI 注入一句话 → SSE 收到 6 条 memory_access,
带真实块 ID 与 op/created
This commit is contained in:
@ -13,33 +13,33 @@ import (
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
agentPkg "gitcode.com/JianFeeeee/HomeAgent/internal/agent"
|
||||
agentAPI "gitcode.com/JianFeeeee/HomeAgent/internal/agent/api"
|
||||
agentCore "gitcode.com/JianFeeeee/HomeAgent/internal/agent/core"
|
||||
agentIO "gitcode.com/JianFeeeee/HomeAgent/internal/agent/io"
|
||||
internalConfig "gitcode.com/JianFeeeee/HomeAgent/internal/config"
|
||||
"gitcode.com/JianFeeeee/HomeAgent/internal/events"
|
||||
"gitcode.com/JianFeeeee/HomeAgent/internal/ipc"
|
||||
"gitcode.com/JianFeeeee/HomeAgent/internal/knowledge"
|
||||
logpkg "gitcode.com/JianFeeeee/HomeAgent/internal/log"
|
||||
luapkg "gitcode.com/JianFeeeee/HomeAgent/internal/lua"
|
||||
"gitcode.com/JianFeeeee/HomeAgent/internal/memory"
|
||||
"gitcode.com/JianFeeeee/HomeAgent/internal/memory/distill"
|
||||
"gitcode.com/JianFeeeee/HomeAgent/internal/memory/document"
|
||||
"gitcode.com/JianFeeeee/HomeAgent/internal/memory/media"
|
||||
"gitcode.com/JianFeeeee/HomeAgent/internal/memory/pipeline"
|
||||
"gitcode.com/JianFeeeee/HomeAgent/internal/memory/social"
|
||||
"gitcode.com/JianFeeeee/HomeAgent/internal/memory/text"
|
||||
"gitcode.com/JianFeeeee/HomeAgent/internal/memory/vector"
|
||||
"gitcode.com/JianFeeeee/HomeAgent/internal/nlp"
|
||||
"gitcode.com/JianFeeeee/HomeAgent/internal/plugin"
|
||||
"gitcode.com/JianFeeeee/HomeAgent/internal/recovery"
|
||||
sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk"
|
||||
"gitcode.com/JianFeeeee/HomeAgent/internal/supervisor"
|
||||
"gitcode.com/JianFeeeee/HomeAgent/internal/tracker"
|
||||
"gitcode.com/JianFeeeee/HomeAgent/pkg/embedding"
|
||||
"gitcode.com/JianFeeeee/HomeAgent/pkg/generation"
|
||||
"gitcode.com/JianFeeeee/HomeAgent/pkg/types"
|
||||
agentPkg "github.com/JianFeeeee/HomeAgent/internal/agent"
|
||||
agentAPI "github.com/JianFeeeee/HomeAgent/internal/agent/api"
|
||||
agentCore "github.com/JianFeeeee/HomeAgent/internal/agent/core"
|
||||
agentIO "github.com/JianFeeeee/HomeAgent/internal/agent/io"
|
||||
internalConfig "github.com/JianFeeeee/HomeAgent/internal/config"
|
||||
"github.com/JianFeeeee/HomeAgent/internal/events"
|
||||
"github.com/JianFeeeee/HomeAgent/internal/ipc"
|
||||
"github.com/JianFeeeee/HomeAgent/internal/knowledge"
|
||||
logpkg "github.com/JianFeeeee/HomeAgent/internal/log"
|
||||
luapkg "github.com/JianFeeeee/HomeAgent/internal/lua"
|
||||
"github.com/JianFeeeee/HomeAgent/internal/memory"
|
||||
"github.com/JianFeeeee/HomeAgent/internal/memory/distill"
|
||||
"github.com/JianFeeeee/HomeAgent/internal/memory/document"
|
||||
"github.com/JianFeeeee/HomeAgent/internal/memory/media"
|
||||
"github.com/JianFeeeee/HomeAgent/internal/memory/pipeline"
|
||||
"github.com/JianFeeeee/HomeAgent/internal/memory/social"
|
||||
"github.com/JianFeeeee/HomeAgent/internal/memory/text"
|
||||
"github.com/JianFeeeee/HomeAgent/internal/memory/vector"
|
||||
"github.com/JianFeeeee/HomeAgent/internal/nlp"
|
||||
"github.com/JianFeeeee/HomeAgent/internal/plugin"
|
||||
"github.com/JianFeeeee/HomeAgent/internal/recovery"
|
||||
sdk "github.com/JianFeeeee/HomeAgent/internal/sdk"
|
||||
"github.com/JianFeeeee/HomeAgent/internal/supervisor"
|
||||
"github.com/JianFeeeee/HomeAgent/internal/tracker"
|
||||
"github.com/JianFeeeee/HomeAgent/pkg/embedding"
|
||||
"github.com/JianFeeeee/HomeAgent/pkg/generation"
|
||||
"github.com/JianFeeeee/HomeAgent/pkg/types"
|
||||
)
|
||||
|
||||
// defaultSystemPrompt 是内置默认人格模板:不含版本号字面量,
|
||||
@ -629,6 +629,54 @@ func newMainAgent(cfg *types.Config, cfgReg *internalConfig.ConfigRegistry, prov
|
||||
InputProcessing: cfg.InputProcessing,
|
||||
})
|
||||
|
||||
// 图记忆读写钩子 → 事件总线。
|
||||
//
|
||||
// ★ 为什么在这里接(而不是让各调用方自己发事件):
|
||||
// 图读写散在 26 个方法与 180+ 个 db 调用点上,各调用方自报必漏发;
|
||||
// 而**图库自己就知道它碰了哪些块 ID**。星图此前只能靠工具名猜
|
||||
// 节点(把 "memory_recall" 拆词去匹配中文实体名,实测命中 0),
|
||||
// 于是每次都点亮同一批无关节点——这正是「星图没有读写反馈」的真身。
|
||||
//
|
||||
// ★ 只在 evBus 非 nil 时接:它把「内核进程」与「独立 CLI 工具」区分开
|
||||
// (homed-graph-migrate / memgc 等也用 NewGraphDB,但没有事件总线,
|
||||
// 也不该为一次批量迁移向不存在的订阅者发几万条事件)。
|
||||
//
|
||||
// ★ 钩子**必须非阻塞**:它在记忆读写的同步路径上,发布到总线
|
||||
// 是内存操作(各订阅者回调也都很短),实测开销可忽略。
|
||||
if memDB != nil && evBus != nil {
|
||||
memDB.SetAccessHook(func(ev memory.AccessEvent) {
|
||||
payload := map[string]interface{}{
|
||||
"op": ev.Op,
|
||||
}
|
||||
// 只在非空时带上字段:空数组会让前端白白重算一次。
|
||||
if len(ev.Blocks) > 0 {
|
||||
payload["blocks"] = ev.Blocks
|
||||
}
|
||||
if len(ev.Created) > 0 {
|
||||
payload["created"] = ev.Created
|
||||
}
|
||||
if len(ev.Removed) > 0 {
|
||||
payload["removed"] = ev.Removed
|
||||
}
|
||||
if len(ev.Merged) > 0 {
|
||||
merged := make([]map[string]string, 0, len(ev.Merged))
|
||||
for _, m := range ev.Merged {
|
||||
merged = append(merged, map[string]string{"from": m.From, "to": m.To})
|
||||
}
|
||||
payload["merged"] = merged
|
||||
}
|
||||
if ev.Tool != "" {
|
||||
payload["tool"] = ev.Tool
|
||||
}
|
||||
evBus.Publish(&events.Event{
|
||||
Type: events.EventMemoryAccess,
|
||||
Source: "main",
|
||||
Payload: payload,
|
||||
Timestamp: time.Now().Unix(),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// D4:把「设备是否已授权」的判据注入插件侧(toolImpl.CanUse 消费它)。
|
||||
//
|
||||
// 为什么必须在这里:判据要用 agent 自己的 allowedOutputs,而
|
||||
|
||||
Reference in New Issue
Block a user