|
|
5836c2ce5c
|
refactor(memory): 拆除描述式媒体索引,媒体成为一等块并按原生向量融合
背景:此前媒体是靠「生成的描述文本」将就进记忆的——写 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 等)。
|
2026-09-11 11:45:24 +08:00 |
|
|
|
dae01f9c06
|
refactor(memory): 移除 media_refs/引用计数,媒体成为一等记忆块
媒体此前是"文本块 + digest 引用 + owner 账本 + 独立 GC":ContextEvent.Media
记 digest,media_refs 表用 owner_kind/owner_id 保活,ref_count 决定 GC 能否清。
这与文本记忆块的管理方式不一致,也是本次一并纠正的核心偏差。
改为与文本块完全一致的生命周期:
1. 一等记忆块直接由所在层持有
- ContextEvent.Blocks / Doc.Blocks / GraphDB memory_blocks
- 块带 modality/digest/MIME/size/vector/fingerprint,文本、图片、视频同构
- Context→Document→Graph 迁移的是块本身(ID 不变),迁移后清空源容器,
同一块不同时存在于两层
2. 删除平行生命周期账本
- media.Store 去掉 media_refs 表、OwnerKind 常量、RefCount 字段、
AddRef/DropRef/DropOwner/Refs、ref_count 列与索引
- 删除 mediaGCLoop、GC(keep,minAge)、容量上限与 media.gc_* / media.max_mb 配置
- 媒体内容在块被永久删除时一并删除(media.Store.Delete + forgetPayloads),
与"删除文本块即删除内容"同一语义
3. L3 原生结构
- memory_blocks / memory_block_edges(contains/depicts/derived_from)
- 边端点必须是真实图节点,不再用 owner 字符串伪装关系
- BlocksForNode 支持 sentence --contains--> block 反查
4. SDK 与检索同步
- 插件附件/标记直接变成块,不再 AddRef
- 跨模态检索改用 QueryMediaScored(CAS 内不再有孤儿缓存需要过滤)
测试全部改写为块语义:删除 refcount/media_refs/GC 断言,新增块迁移、
单层不变量、Delete 语义与并发删除回归。
注:cmd/homed/main.go 同时携带工作区中既有的 CLIP→Qwen 模型目录接线改动。
|
2026-09-11 10:57:22 +08:00 |
|
|
|
687e5655bc
|
feat(sdk): 多模态贯通插件边界——公开接口、内核桥接与统一输入主干
记忆系统在 1.1.0 支持了二进制多媒体节点,但那条链路只对**内核自己**开放:
用户在 qq 发图能落进 CAS、能被记忆引用,而插件调 Commit / DocMemory().Insert
交进来的媒体一律无处安放。原因是三层都断着,且**每一层都不报错**。
## 一、公开 SDK:补上媒体的表达能力(全部新增,无签名变更)
- `Triple` += `SentenceText`、`MediaDigests`
- `Doc` += `MediaDigests`、`Attachments`;新增 `MediaAttachment`
- `TextEvent` += `Attachments`
- `DocMemoryAPI` += `InsertWithMedia`
- `IOInjector` += `InjectInputMedia` / `InjectInputMediaSync` / `InjectInterruptMedia`
- `PluginSDK` 补上一直缺失的 `SetToolBlocks` 包装(接口里有、便捷方法里没有)
`MediaAttachment` 一个类型服务两个方向:给 `Data`+`MIME` 是新内容(CAS 按字节
去重),只给 `Digest` 是引用已有内容。读路径**只回元数据不回字节**——一次检索
可能命中几十份媒体,全塞回去会把跨进程消息撑爆。
媒体注入不能搭 `SetToolBlocks` 的车:那个方法只在工具处理函数内部可用,且媒体
要等下一条 tool message 才到模型手上。插件主动发起一轮带媒体的对话、以及中断
注入,需要自己的签名,且媒体在**本轮**就送到模型。
## 二、内核桥接层:原先在静默裁字段
`internal/sdk/memory_impl.go` 此前只搬自己认识的几个字段,其余丢弃且返回 nil:
- 图记忆丢 `Confidence`/`SubjectType`/`ObjectType`/`SentenceText`,又走 `Commit`
而非 `CommitWithMedia`(不回 sentenceIDs)→ 媒体绑定链 `SentenceText → sentences
→ sentence_id → media_refs` 一步都走不通,插件即便按格式写好标记也永远挂不上;
- 知识库 `Query` 只回 ID/Title/Content,`Insert` 只写这三个;`Remove` 不解引用,
于是那些媒体永久处于「被引用」状态,GC 收不掉、磁盘只增不减
(内核的归档路径 `releaseDocMedia` 做了这一步,插件路径漏了同一步)。
规则改为:**内部结构有的字段一律透传**。标记格式处理作为包级私有辅助留在桥接
层自己手里,但必须与内核 `mediaSummaryForEvent` 字节兼容——两边要能互读对方
写下的标记。
标记插入必须在 `ds.Insert` **之前**(向量索引取 `Summary + " " + Content`,
之后补的标记检索不到),引用绑定必须在**之后**(owner_id 是 Insert 生成的 ID)。
## 三、跨进程链路:不接线就是全体外部插件编译失败
`go test` 直接把这一层拍出来了——`procIO does not implement sdk.IOInjector`。
公开接口加方法后,生成模板不跟上,**每个外部插件都编不过**,是硬失败不是软降级。
六处接线:`protocol.go` 四个 method 常量、`capability.go` 能力归属、
`corehandler.go` 四个分派分支、`proc_core.go` 委托、`proc_main.go.tmpl` 模板侧
实现、以及三个测试替身。
## 四、统一输入主干:把模态从「函数选择」降级为「字段」
`processTextInput` / `processMediaInput` 合并为 `processInput`。这个分叉是历史
产物而非设计:`processTextInput` 本来就处理媒体(`bindEventMedia` +
`mediaSummaryForEvent`,与媒体路径尾部完全相同),`process()` 只看
`stageCtx.Extra["media_blocks"]`、根本不认识 `evt.Type`。模态是输入的**属性**,
不是输入的**种类**。
媒体路径由此获得它一直缺的六项:去重、`no_memory`、通道 `Cleaner`、中断语义、
`_consolidation_` 路由、正确的 `EventRawInput`。
最后一项是个真 bug:媒体路径发布 `"content": evt.Payload`(一个 map),而
`webui/handler.go` 断言 `.(string)` → 断言失败、`content == ""`、提前返回。
**用户发的图从来没出现在 WebUI 聊天记录里。**
`media_blocks` 同时接受 `[]agentAPI.ContentBlock` 与 `[]pubsdk.ContentBlock`:
字段一致但 Go 不自动转换,只认一种的后果是另一种被静默丢弃。
## 五、模型可调用的三个工具
`memory_commit` 的 `sentence_text` **从未暴露给模型**,而它是绑定链上的必经环节;
连同 `media_digests` 一起补进 JSON schema 与工具文档。`doc_commit` 加
`media_digests`。`doc_query` 把关联媒体单独一行附在结果末尾(正文按 2000 字截断,
标记通常就在尾部)。
标记由**内核**生成而非插件/模型拼装:要求调用方知道格式,等于让一个拼写错误
静默切断引用绑定,而全链路无人报错。
## 六、WebUI 上传走真实媒体链路
图片/音频读回字节拼 data URL 注入 `media_blocks`(8MB 上限,超限退回按路径处理)。
此前只注入一句「文件已保存到 <路径>」,指望模型自己调 `files_read`——但那返回
文本,图片字节对模型永远不可见。附件类型识别扩展到 audio 并在缺 Content-Type
时按扩展名兜底(判错不只是卡片样式问题,图片被当普通文件就进不了视觉链路)。
## 测试
- `internal/sdk/memory_impl_test.go`(12 例,此前该包**没有任何测试文件**)
- `internal/agent/core/inputunify_test.go`(统一主干 + 双静态类型 + 三工具媒体)
- `third_party/homeagent-sdk/sdk/stress_test.go`(13 例并发压测)
压测抓到两处**真**竞态(不是理论风险):`PluginSDK` 的 API 字段与 `autoRestart`
无锁,而写方(内核注入 API、插件 `SetAutoRestart`)与读方(插件后台 goroutine
注入、内核 registry 读 `AutoRestart`)天然跨 goroutine。加 `apiMu` 修掉;约定
只在持锁期间取字段值,取完即释放再调用——持锁调用会把 `InjectInputSync` 这类
阻塞到 agent 回复(可达数分钟)的方法与 `SetIOInjector` 串起来,让插件重载卡死。
测试还抓出两个自身缺陷:`bindDocMedia` 把同一份媒体数两次(`AddRef` 幂等所以表
是对的,但日志说「绑定 2 个」而实际 1 条——误导后续排查),以及用单字符实体名
时 `validEntityName` 静默跳过、`Commit` 返回 nil 却什么都没写。
存量插件不需要改一行也不需要重编:新增方法由插件调用、内核实现,不调就不受影响。
17 个 example 插件源码零改动通过类型检查。
|
2026-09-06 09:51:31 +08:00 |
|