mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-10-03 15:53:56 +00:00
fix(rel): 场景贯穿流水线到块层 + 编辑不再丢置信度/场景 + 构建默认带 onnxruntime
三件事,前两件是上一轮热部署暴露/遗留的真缺陷。
1) 热部署差点静默降级(已修)
`make build` 之前**不带任何 tags**,而发行构建(deploy/packaging/build.sh)
默认 HOMED_TAGS=onnxruntime,package-linux.sh 还会直接拒收非 onnxruntime 二进制。
实测差异:33MB vs 84MB;启动日志里
「multimodal space active: provider=chineseclip dim=512」整行消失、
少加载一个插件(chinese-clip/qwen3vl provider 降级)、
静态词向量退回 fallback。即「随手 make build」与「发行构建」不是同一个东西,
而部署时无从察觉。
修:Makefile 的 build 默认 HOMED_TAGS ?= onnxruntime(与打包脚本一致),
构建后自动校验二进制里有没有 onnxruntime,缺了就打 WARN。
生产已按此重新构建部署(v1.4.0+hotfix.59a4e1a,已核实 provider 行回归)。
2) memory_edit 每跑一次就静默降级一次(新)
memory_edit 是「按包含匹配 Purge + 写新三元组」,中间那一步把旧关系的
置信度、原句、**场景引用**全丢了:置信度被重置成默认 1.0,场景钉死的记忆
被打散成无场景。而关系复审心跳(reviewLoop)走的正是这条路——每轮复审都
在无声地削记忆质量。
修:编辑前用 FindRelations 精确取回旧关系,把置信度/原句/场景带到新三元组;
新增 ScenesOfRelation。Purge(hard/soft)与 PurgeNoise/PurgeOrphans 之后
统一清理悬空 scene_refs,SceneStats 不再说谎。
3) 场景贯穿流水线到块层(按「rel 应贯穿整条流水线」的设计)
此前场景只到 relation/entity:块(L0/L3 一等记忆块)没有场景,于是
「那场 QQ 对话里发过来的那张图」在场面重现时永远取不回来。
- MemoryBlock.Scene + memory_blocks.scene 列(幂等 ALTER 迁移)。
- scene_refs 增加 ref_text 承载字符串主键(块/文档 id 不是数值)。
**不能只 ALTER ADD COLUMN**:唯一约束要从 (scene_id,kind,ref_id) 变成
含 ref_text 的四元组,而 ALTER 改不了约束——旧约束会让「同场景第 2 个块」
直接冲突(只在多块场景暴露)。改为按列探测后整表重建并搬运旧数据。
- PutMemoryBlocks 同事务挂 scene_refs(kind='block');无场景重写不覆盖已有场景
(否则一次无场景重写就静默抹掉挂载)。
- RecallByScene 返回块;FormatContext 增「场景素材」段(模态 + 文本/短 digest),
上限 3 条。
- 生产者接线:attachBlocksToSentence 让块继承承载它的三元组的场景;
linkBlocksToDocument 让文档的块继承文档来源场景(QQ 归档的图挂 chan:qq)。
验证:go build/vet 干净,go test -count=1 ./... 全绿。
新增用例:场景块(取回/同场景多块/无场景重写不抹场景/悬空引用清理)、
**旧表结构迁移**(降级成旧 scene_refs 后重开,旧数据保留且多块可写)、
场景素材注入、FindRelations+ScenesOfRelation 编辑搬运闭环。
生产:已重建(-tags onnxruntime)并原子替换 /usr/local/bin/homed + 重启,
35 插件全加载、panic/fatal=0、chineseclip 空间 active。
This commit is contained in:
@ -207,7 +207,7 @@ func (a *Agent) archiveColdDocs() {
|
||||
// 文档持有的一等块写入 L3,并以 document --contains--> block 边关联;
|
||||
// 块 ID 原样保留(迁移而非重建)。块迁走后删除文档即完成迁移。
|
||||
if len(doc.Blocks) > 0 {
|
||||
if bound := a.linkBlocksToDocument(doc.ID, doc.Blocks); bound != len(doc.Blocks) {
|
||||
if bound := a.linkBlocksToDocument(doc.ID, doc.Blocks, memory.ChannelScene(doc.Source)); bound != len(doc.Blocks) {
|
||||
log.Printf("[agent] doc→graph: %s 块迁移不完整 (%d/%d),保留文档待下轮重试",
|
||||
doc.ID, bound, len(doc.Blocks))
|
||||
continue
|
||||
|
||||
@ -47,7 +47,7 @@ func (a *Agent) migrateLegacyGraphMedia() {
|
||||
|
||||
// attachBlocksToSentence 把一组 digest 变成 L3 一等块并挂到句子上。
|
||||
// seed 允许复用已持有块的 ID(L2→L3 迁移保持块身份不变)。
|
||||
func (a *Agent) attachBlocksToSentence(sentenceID int64, digests []string, seed map[string]memory.MemoryBlock) int {
|
||||
func (a *Agent) attachBlocksToSentence(sentenceID int64, digests []string, seed map[string]memory.MemoryBlock, scene string) int {
|
||||
if a.mediaStore == nil || a.memory == nil || sentenceID == 0 {
|
||||
return 0
|
||||
}
|
||||
@ -64,6 +64,11 @@ func (a *Agent) attachBlocksToSentence(sentenceID int64, digests []string, seed
|
||||
continue
|
||||
}
|
||||
}
|
||||
// 块继承承载它的三元组的场景:块是流水线里最细的子项目,场景要落到它身上,
|
||||
// 否则「那场对话里发过来的那张图」在场面重现时永远取不回来。
|
||||
if b.Scene == "" {
|
||||
b.Scene = scene
|
||||
}
|
||||
if err := a.memory.PutMemoryBlocks([]memory.MemoryBlock{b}); err != nil {
|
||||
log.Printf("[media] L3 块写入失败 (%s): %v", shortDigest(full), err)
|
||||
continue
|
||||
@ -79,7 +84,7 @@ func (a *Agent) attachBlocksToSentence(sentenceID int64, digests []string, seed
|
||||
|
||||
// linkBlocksToDocument 把文档持有的块写入 L3,并建立
|
||||
// document --contains--> block 边。块的 ID 原样保留(迁移而非重建)。
|
||||
func (a *Agent) linkBlocksToDocument(docID string, blocks []memory.MemoryBlock) int {
|
||||
func (a *Agent) linkBlocksToDocument(docID string, blocks []memory.MemoryBlock, scene string) int {
|
||||
if a.memory == nil || docID == "" || len(blocks) == 0 {
|
||||
return 0
|
||||
}
|
||||
@ -87,6 +92,13 @@ func (a *Agent) linkBlocksToDocument(docID string, blocks []memory.MemoryBlock)
|
||||
log.Printf("[media] 写入 L3 文档节点失败 (%s): %v", docID, err)
|
||||
return 0
|
||||
}
|
||||
// 文档层把场景传给块:归档进图库的块属于该文档的来源场面(QQ 归档的图
|
||||
// 就该挂在 chan:qq 上),否则 L3 里这批块在场景召回中不可见。
|
||||
for i := range blocks {
|
||||
if blocks[i].Scene == "" {
|
||||
blocks[i].Scene = scene
|
||||
}
|
||||
}
|
||||
if err := a.memory.PutMemoryBlocks(blocks); err != nil {
|
||||
log.Printf("[media] 写入 L3 记忆块失败 (doc %s): %v", docID, err)
|
||||
return 0
|
||||
@ -135,7 +147,7 @@ func (a *Agent) commitTriplesWithMedia(triples []memory.Triple, sessionID string
|
||||
if sid == 0 {
|
||||
continue
|
||||
}
|
||||
blocks += a.attachBlocksToSentence(sid, t.MediaDigests, byDigest)
|
||||
blocks += a.attachBlocksToSentence(sid, t.MediaDigests, byDigest, t.Scene)
|
||||
}
|
||||
return ec, rc, blocks, nil
|
||||
}
|
||||
|
||||
@ -194,7 +194,7 @@ func TestCommitTriplesWithMedia_RoundTrip(t *testing.T) {
|
||||
func TestAttachBlocksToSentence_SkipsUnresolvable(t *testing.T) {
|
||||
// digest 在库里不存在时必须跳过,不能建一条指向虚无的块边。
|
||||
a, g, _ := newGraphMediaAgent(t)
|
||||
if n := a.attachBlocksToSentence(42, []string{"deadbeefdead"}, nil); n != 0 {
|
||||
if n := a.attachBlocksToSentence(42, []string{"deadbeefdead"}, nil, ""); n != 0 {
|
||||
t.Fatalf("无法补全的 digest 不该建块,实际绑定 %d", n)
|
||||
}
|
||||
blocks, err := g.BlocksForNode("sentence", "42")
|
||||
@ -208,7 +208,7 @@ func TestAttachBlocksToSentence_SkipsUnresolvable(t *testing.T) {
|
||||
|
||||
func TestAttachBlocksToSentence_NilStoreNoop(t *testing.T) {
|
||||
a := &Agent{}
|
||||
if n := a.attachBlocksToSentence(1, []string{"aaaaaaaaaaaa"}, nil); n != 0 {
|
||||
if n := a.attachBlocksToSentence(1, []string{"aaaaaaaaaaaa"}, nil, ""); n != 0 {
|
||||
t.Fatalf("媒体关闭时应静默无操作,实际 %d", n)
|
||||
}
|
||||
if got, err := a.RecallBlocksForSentence(1); err != nil || got != nil {
|
||||
@ -234,7 +234,7 @@ func TestAttachBlocksToSentence_ReusesSeedIdentity(t *testing.T) {
|
||||
sid := ids["迁移测试句。"]
|
||||
|
||||
byDigest := map[string]memory.MemoryBlock{digest: seedBlock}
|
||||
if n := a.attachBlocksToSentence(sid, []string{digest}, byDigest); n != 1 {
|
||||
if n := a.attachBlocksToSentence(sid, []string{digest}, byDigest, ""); n != 1 {
|
||||
t.Fatalf("应绑定 1 个块,实际 %d", n)
|
||||
}
|
||||
blocks, err := g.BlocksForNode("sentence", strconv.FormatInt(sid, 10))
|
||||
@ -256,7 +256,7 @@ func TestLinkBlocksToDocument_CreatesDocumentNodeEdge(t *testing.T) {
|
||||
t.Fatal("blockFromDigest 失败")
|
||||
}
|
||||
|
||||
if n := a.linkBlocksToDocument("doc_42", []memory.MemoryBlock{b}); n != 1 {
|
||||
if n := a.linkBlocksToDocument("doc_42", []memory.MemoryBlock{b}, ""); n != 1 {
|
||||
t.Fatalf("应建立 1 条文档→块边,实际 %d", n)
|
||||
}
|
||||
blocks, err := g.BlocksForNode("document", "doc_42")
|
||||
|
||||
@ -360,6 +360,20 @@ func (a *Agent) executeMemoryTool(tc agentAPI.ToolCall) string {
|
||||
if newObject == "" {
|
||||
newObject = oldObject
|
||||
}
|
||||
// 编辑前先精确取回旧关系:Purge 是「删旧写新」,中间那一步会把
|
||||
// 置信度、场景引用、原句一起丢掉。复审心跳(reviewLoop)正是走这条路,
|
||||
// 于是每次复审都把置信度重置成默认 1.0、把场景钉死的记忆打散成无场景,
|
||||
// 而且没有任何日志——这类「静默降级」比报错难查得多。
|
||||
var carriedConf float64
|
||||
var carriedSentence, carriedScene string
|
||||
if olds, ferr := a.memory.FindRelations(oldSubject, oldRelation, oldObject); ferr == nil && len(olds) > 0 {
|
||||
carriedConf = olds[0].Confidence
|
||||
carriedSentence = olds[0].SentenceText
|
||||
if keys, serr := a.memory.ScenesOfRelation(olds[0].ID); serr == nil && len(keys) > 0 {
|
||||
carriedScene = keys[0]
|
||||
}
|
||||
}
|
||||
|
||||
n, err := a.memory.Purge(map[string]string{
|
||||
"subject_contains": oldSubject,
|
||||
"relation_type": oldRelation,
|
||||
@ -369,9 +383,12 @@ func (a *Agent) executeMemoryTool(tc agentAPI.ToolCall) string {
|
||||
return fmt.Sprintf("编辑图记忆失败(删除旧记录): %v", err)
|
||||
}
|
||||
triples := []memory.Triple{{
|
||||
Subject: newSubject,
|
||||
Relation: newRelation,
|
||||
Object: newObject,
|
||||
Subject: newSubject,
|
||||
Relation: newRelation,
|
||||
Object: newObject,
|
||||
Confidence: carriedConf,
|
||||
SentenceText: carriedSentence,
|
||||
Scene: carriedScene,
|
||||
}}
|
||||
ec, rc, err := a.memory.Commit(triples, string(a.id), 0)
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user