mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-21 17:38:10 +00:00
背景:此前媒体是靠「生成的描述文本」将就进记忆的——写 marker 进正文、 再由正则反解成 media_refs 与图库里的 type=Media 实体。这条链路有三个 致命缺陷:描述由异步模型生成(未生成前媒体等于不存在)、语义检索实质上 只搜描述文字、图库里的「媒体节点」是描述文本的投影而不是媒体本身。 本提交把这条链路整体拆除,媒体改为按自己的原生向量参与记忆: 一、描述链彻底删除(无残留、无兼容分支) - media.Item 去掉 Description/DescribedBy 与对应列; - 删除 Store.Describe / Store.Search / Store.Pending; - 删除 Agent.mediaDescribeLoop / describePendingMedia 与配置项 core.memory.media.describe_on_ingest; - SDK 侧 MediaAttachment 去掉 Description(见 SDK 仓独立提交)。 二、marker 机制删除,媒体归属改为结构化块边 - 删除 mediaMarkerLine/parseMediaMarkers/mediaEntityName/mediaTriplesFromText/ extractMediaDigests/sentenceWithMediaMarkers/docMediaContext; - memory.Triple 新增 MediaDigests 结构化字段;句子文本保持原样, 不再被 marker 污染; - 块以 sentence --contains--> block / document --contains--> block 结构边 挂到承载节点(新增 documents 表与 document 节点种类); - 模型未给原句时用「主谓宾。」拼一句自然语言作落点,不造 marker 文本。 三、旧数据迁移(幂等) - 新增 GraphDB.MigrateLegacyMediaEntities:把 type=Media 的旧实体按短 digest 还原成原生块、挂回原句子、删除旧实体与描述关系;Agent 启动时执行; - CleanupOrphanedSentences 同时看关系引用与块边,避免把只靠块存活的句子 连同块边一起删掉。 四、向量融合:媒体按图本身被召回 - 新增 vector.FuseVectors(逐维求和 + L2 归一化); - Doc.DenseVec = 文本向量 ⊕ 文档块的媒体向量(同 fingerprint 才融合), 新增 Doc.DenseFP,指纹变化触发重算; - ContextEvent.DenseVec 同理融合事件块;事件新增 DenseFP,Prune 只在 同一统一空间内比稠密余弦; - 跨模态视觉路只召回「仍被某层记忆块持有」的媒体,CAS 全库字节不再 直接充当记忆检索结果。 五、同时纳入本分支既有的嵌入基础改造(此前工作区未提交,缺它 HEAD 不可构建) - internal/tfidf 懒回退包、千问三段式多模态 ONNX 空间的 Go 侧 (qwen/embedder.go、image.go、model_input.go)、CLIP 移除、 sdk.NewStore 分词器签名与调用点、embed 侧车 systemd 单元。 验证:go build ./... 、go vet ./...(含 -tags medialive)均通过; 在 HEAD 的独立 worktree 上重放本次暂存集后 go test -short ./internal/... 全部通过(端口冲突类用例在隔离环境中亦通过)。未提交工作区中与本改造 无关的改动(HarmonyOS、waiter、devicebridge、plan.md 等)。
324 lines
8.1 KiB
Go
324 lines
8.1 KiB
Go
package memory
|
||
|
||
import (
|
||
"fmt"
|
||
"log"
|
||
"strings"
|
||
"sync"
|
||
|
||
"gitcode.com/JianFeeeee/HomeAgent/internal/memory/vector"
|
||
)
|
||
|
||
type Indexer struct {
|
||
db *GraphDB
|
||
vec *vector.Store
|
||
veczer *vector.TFIDFVectorizer
|
||
mu sync.RWMutex
|
||
trained bool
|
||
recalled map[string]bool // 已通过工具调用显式召回的实体名,自动注入时跳过
|
||
}
|
||
|
||
func NewIndexer(db *GraphDB) *Indexer {
|
||
return &Indexer{
|
||
db: db,
|
||
vec: vector.NewStore(),
|
||
veczer: vector.NewTFIDFVectorizer(TokenizeWords),
|
||
recalled: make(map[string]bool),
|
||
}
|
||
}
|
||
|
||
// MarkRecalled 标记实体名已被工具调用显式召回,后续自动注入时跳过
|
||
func (idx *Indexer) MarkRecalled(names ...string) {
|
||
idx.mu.Lock()
|
||
defer idx.mu.Unlock()
|
||
for _, name := range names {
|
||
idx.recalled[name] = true
|
||
}
|
||
}
|
||
|
||
// Sync 从图数据库中同步实体名到向量索引
|
||
func (idx *Indexer) Sync() error {
|
||
idx.mu.Lock()
|
||
defer idx.mu.Unlock()
|
||
|
||
if idx.db == nil {
|
||
return nil
|
||
}
|
||
|
||
result, err := idx.db.Recall(nil, nil, 1, "")
|
||
if err != nil || result == nil {
|
||
return err
|
||
}
|
||
|
||
// 收集实体名
|
||
var names []string
|
||
for _, e := range result.Entities {
|
||
names = append(names, e.Name)
|
||
}
|
||
|
||
if len(names) == 0 {
|
||
return nil
|
||
}
|
||
|
||
// 训练向量化器
|
||
idx.veczer.Train(names)
|
||
|
||
// 重建向量索引
|
||
idx.vec = vector.NewStore()
|
||
for _, e := range result.Entities {
|
||
vec := idx.veczer.Vectorize(e.Name)
|
||
idx.vec.Insert(fmt.Sprintf("entity_%d", e.ID), e.Name, vec, map[string]string{
|
||
"type": "entity",
|
||
"name": e.Name,
|
||
})
|
||
}
|
||
|
||
idx.trained = true
|
||
log.Printf("[indexer] synced %d entities to vector index", len(names))
|
||
return nil
|
||
}
|
||
|
||
type InjectedContext struct {
|
||
Entities []Entity `json:"entities"`
|
||
Relations []Relation `json:"relations"`
|
||
Summary string `json:"summary"`
|
||
TokenEstimate int `json:"token_estimate"`
|
||
}
|
||
|
||
func (idx *Indexer) BuildContext(userInput string) *InjectedContext {
|
||
if idx.db == nil {
|
||
return &InjectedContext{Summary: ""}
|
||
}
|
||
|
||
input := CleanText(userInput)
|
||
|
||
// 1. 向量搜索:从实体名向量索引中找到相关实体
|
||
vectorEntities := idx.vectorSearchEntities(input)
|
||
|
||
// 2. 关键词搜索:已有逻辑
|
||
keywords := ExtractKeywords(input)
|
||
if len(keywords) == 0 && len(vectorEntities) == 0 {
|
||
keywords = []string{userInput}
|
||
}
|
||
|
||
// 合并关键词和向量找到的实体名
|
||
seedNames := make([]string, 0, len(vectorEntities))
|
||
for _, e := range vectorEntities {
|
||
seedNames = append(seedNames, e.Name)
|
||
}
|
||
allKeywords := append(keywords, seedNames...)
|
||
|
||
result, err := idx.db.Recall(allKeywords, nil, 2, "")
|
||
if err != nil || result == nil {
|
||
return &InjectedContext{Summary: ""}
|
||
}
|
||
|
||
// 过滤已被工具调用显式召回的实体,避免重复注入
|
||
idx.mu.RLock()
|
||
filtered := result.Entities[:0]
|
||
for _, e := range result.Entities {
|
||
if !idx.recalled[e.Name] {
|
||
filtered = append(filtered, e)
|
||
}
|
||
}
|
||
idx.mu.RUnlock()
|
||
|
||
ctx := &InjectedContext{
|
||
Entities: filtered,
|
||
Relations: nil,
|
||
}
|
||
|
||
if len(filtered) > 0 {
|
||
summary := buildIndexSummary(filtered)
|
||
ctx.Summary = summary
|
||
ctx.TokenEstimate = estimateTokens(summary) + len(filtered)*8
|
||
} else {
|
||
ctx.Summary = ""
|
||
}
|
||
|
||
return ctx
|
||
}
|
||
|
||
// vectorSearchEntities 在实体名向量索引中搜索
|
||
func (idx *Indexer) vectorSearchEntities(query string) []Entity {
|
||
idx.mu.RLock()
|
||
defer idx.mu.RUnlock()
|
||
|
||
if !idx.trained || idx.vec.Size() == 0 {
|
||
return nil
|
||
}
|
||
|
||
queryVec := idx.veczer.Vectorize(query)
|
||
results := idx.vec.Search(queryVec, 5)
|
||
|
||
var entities []Entity
|
||
for _, r := range results {
|
||
if r.Meta != nil && r.Meta["type"] == "entity" {
|
||
entities = append(entities, Entity{Name: r.Meta["name"]})
|
||
}
|
||
}
|
||
return entities
|
||
}
|
||
|
||
func (idx *Indexer) BuildToolPrompt() string {
|
||
return `## 图记忆工具
|
||
|
||
你有以下工具可以操作长期图记忆系统:
|
||
|
||
### memory_recall
|
||
检索与关键词相关的实体和关系。
|
||
参数:
|
||
- query_intent: 查询关键词,逗号分隔
|
||
- depth: 遍历深度(默认2)
|
||
|
||
### memory_commit
|
||
将三元组写入图记忆。
|
||
参数:
|
||
- triples: [{"subject": "实体名", "relation": "关系类型", "object": "目标实体",
|
||
"sentence_text": "原始句子(可选)", "media_digests": ["图片digest(可选)"]}]
|
||
填了 media_digests,日后从这条记忆就能取回当时那张图/那段音频。
|
||
|
||
### memory_introspect
|
||
查看记忆统计信息。
|
||
|
||
### memory_purge
|
||
删除或修正记忆。
|
||
参数:
|
||
- criteria: {"subject_contains": "...", "relation_type": "..."}
|
||
- mode: "soft" | "supersede"
|
||
|
||
使用方法:在推理过程中调用对应的 tool,系统会自动执行并返回结果。`
|
||
}
|
||
|
||
func (idx *Indexer) FormatContext(ctx *InjectedContext) string {
|
||
if ctx == nil || len(ctx.Entities) == 0 {
|
||
return ""
|
||
}
|
||
|
||
var b strings.Builder
|
||
b.WriteString("【记忆索引】")
|
||
|
||
if ctx.Summary != "" {
|
||
b.WriteString(" ")
|
||
b.WriteString(ctx.Summary)
|
||
}
|
||
|
||
b.WriteString(fmt.Sprintf(" 索引: "))
|
||
for i, e := range ctx.Entities {
|
||
if i >= 5 {
|
||
b.WriteString("…")
|
||
break
|
||
}
|
||
if i > 0 {
|
||
b.WriteString(", ")
|
||
}
|
||
b.WriteString(e.Name)
|
||
if e.Type != "Concept" {
|
||
b.WriteString("(" + e.Type + ")")
|
||
}
|
||
}
|
||
|
||
b.WriteString(" | 需更多细节请用 memory_recall 查询")
|
||
return b.String()
|
||
}
|
||
|
||
func (idx *Indexer) GetToolDefinitions() []map[string]interface{} {
|
||
return []map[string]interface{}{
|
||
{
|
||
"type": "function",
|
||
"function": map[string]interface{}{
|
||
"name": "memory_recall",
|
||
"description": "检索图记忆。输入查询意图关键词,返回相关实体和关系。",
|
||
"parameters": map[string]interface{}{
|
||
"type": "object",
|
||
"properties": map[string]interface{}{
|
||
"query_intent": map[string]interface{}{
|
||
"type": "string",
|
||
"description": "查询意图,支持逗号分隔多个关键词",
|
||
},
|
||
"depth": map[string]interface{}{
|
||
"type": "integer",
|
||
"description": "遍历深度,默认2",
|
||
"default": 2,
|
||
},
|
||
},
|
||
"required": []string{"query_intent"},
|
||
},
|
||
},
|
||
},
|
||
{
|
||
"type": "function",
|
||
"function": map[string]interface{}{
|
||
"name": "memory_commit",
|
||
"description": "写入图记忆。将三元组列表写入长期记忆。",
|
||
"parameters": map[string]interface{}{
|
||
"type": "object",
|
||
"properties": map[string]interface{}{
|
||
"triples": map[string]interface{}{
|
||
"type": "array",
|
||
"description": "三元组列表",
|
||
"items": map[string]interface{}{
|
||
"type": "object",
|
||
"properties": map[string]interface{}{
|
||
"subject": map[string]interface{}{"type": "string"},
|
||
"relation": map[string]interface{}{"type": "string"},
|
||
"object": map[string]interface{}{"type": "string"},
|
||
"sentence_text": map[string]interface{}{
|
||
"type": "string",
|
||
"description": "可选:这条三元组的原始句子。填了才能日后从图谱回到原文。",
|
||
},
|
||
"media_digests": map[string]interface{}{
|
||
"type": "array",
|
||
"description": "可选:这条记忆关联的媒体 digest(对话或 memory_recall 的「关联媒体」里显示的十六进制串,短的即可)。填了以后从这条记忆能取回原图/音频。",
|
||
"items": map[string]interface{}{"type": "string"},
|
||
},
|
||
},
|
||
"required": []string{"subject", "relation", "object"},
|
||
},
|
||
},
|
||
},
|
||
"required": []string{"triples"},
|
||
},
|
||
},
|
||
},
|
||
{
|
||
"type": "function",
|
||
"function": map[string]interface{}{
|
||
"name": "memory_introspect",
|
||
"description": "查看图记忆统计信息:实体数量、关系数量、热点实体。",
|
||
"parameters": map[string]interface{}{
|
||
"type": "object",
|
||
"properties": map[string]interface{}{},
|
||
},
|
||
},
|
||
},
|
||
}
|
||
}
|
||
|
||
func buildIndexSummary(entities []Entity) string {
|
||
if len(entities) == 0 {
|
||
return ""
|
||
}
|
||
|
||
var b strings.Builder
|
||
b.WriteString(fmt.Sprintf("关联 %d 个记忆实体", len(entities)))
|
||
|
||
topN := 3
|
||
if len(entities) < topN {
|
||
topN = len(entities)
|
||
}
|
||
b.WriteString(",高频:")
|
||
for i := 0; i < topN; i++ {
|
||
if i > 0 {
|
||
b.WriteString("、")
|
||
}
|
||
b.WriteString(entities[i].Name)
|
||
}
|
||
|
||
return b.String()
|
||
}
|
||
|
||
func estimateTokens(s string) int {
|
||
return len(s) / 2
|
||
}
|