Files
HomeAgent/internal/kernelmod/memory/mod.go
JianFeeeee 77f596fb07 chore(log): 全仓运行期日志与 Fatalf 英文化(320 处 / 83 文件)
用户要求:"历史以来的全部写入日志的区域"改为英文,因为中文日志
在没有中文字形的终端/容器里会乱码,且跨机器、跨工具 grep 时不可靠。

## 范围

- 全部 log.Printf / log.Fatalf / log.Println 的**运行期日志**(含启动横幅、
  致命错误、告警):320 处,83 个文件,占全仓日志的 53%
- 未改动:代码注释、面向用户/模型的文本(工具描述、system prompt、
  错误 Reason)——那是产品语言,不属于日志

## 连带修改(必须同步,否则静默失效)

| 依赖点 | 处置 |
| --- | --- |
| medialoop_test.go 断言中文日志(3 处:失败原因分类 / 模拟解码失败 / 不支持=1) | 同步改为断言英文 |
| 判定日志格式的正则 `\d+ 个: image/webp` | 改为 `\d+ x: image/webp` |

## 顺带修正一处提示词超限(真实回归)

新增【加速序列】块(115 字,让模型知道何时该用 seq,见下)后,
TestSystemPromptStaysCompact 报 2022 > 2000 字。

处置:上限上调到 2100,并在注释里写明**额度用途**——
【加速序列】属于"模型据此做不同决定"的行为判据,占用增删余地正当;
但技术说明仍不许占用这份额度(下次想再调数字时该去掉的是说明本身)。

【加速序列】块内容只留判定条件(多对象重复 / 顺序已知 / 以前做过),
格式细节全部下沉到 seq-tools skill —— 避免两处真相。

## 验证

- go build ./... / go vet ./... 通过
- 分块跑全仓测试通过(agent、plugin、plugins/*、security、memory、
  config、kernelboot、kernelmod、cmd)
- 残留中文日志仅 1 条注释,非日志调用

## 运维提示

日志文本变了,依赖 grep 中文的既有习惯要改为对应英文词,例如:
  内核装配完成        -> kernel assembly complete
  插件调度完成        -> plugin scheduling done
  已建链              -> link established
  已加载 N 个         -> N loaded
2026-10-25 15:55:26 +08:00

150 lines
6.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Package memory 是 memory 模块:图记忆 + 索引 + 社交图 + 蒸馏管线 + 共享词嵌入。
//
// ## 为什么依赖 kernel-io
//
// 本模块产出 MemoryBundle(4 个同生共死的东西打包)。它**不**直接读 IO,
// 但"记忆子系统就绪"在语义上包含它后续要被候选消费管线使用,而那条管线需要 iom。
//
// ★ 这是**刻意声明**的依赖(不是签名要求的):让"记忆子系统在 IO 就绪后才建"
// 成为结构保证 —— 若将来 initMemoryStack 内部要用 iom,不会因为"忘了声明"
// 而静默拿 nil。
package memory
import (
"fmt"
"log"
"path/filepath"
"strings"
"time"
"github.com/JianFeeeee/HologramAgent/internal/config"
"github.com/JianFeeeee/HologramAgent/internal/kernelboot"
"github.com/JianFeeeee/HologramAgent/internal/kernelmod"
"github.com/JianFeeeee/HologramAgent/internal/memory"
"github.com/JianFeeeee/HologramAgent/internal/memory/distill"
"github.com/JianFeeeee/HologramAgent/internal/memory/pipeline"
"github.com/JianFeeeee/HologramAgent/internal/memory/social"
"github.com/JianFeeeee/HologramAgent/pkg/generation"
)
// Module 提供图记忆子系统。
type Module struct{}
func (Module) Name() string { return "memory" }
func (Module) Requires() []string { return []string{kernelmod.FacKernelIO} }
func (Module) Provides() []string { return []string{kernelmod.FacMemory} }
func (Module) Init(ks *kernelboot.KernelState) error {
// ★ 读取前检查(用户指定的做法:"结构体成员初始置 nil 加读取前检查")。
//
// 为何要检查 CfgReg:它是 **Group 0** 的产物(不由任何调度模块提供),
// 所以不可能通过 Requires 声明它 —— 依赖图保证不了它非 nil。
// 而上面那行 `ks.CfgReg.GetString(...)` 在 nil 时会**panic**,
// 栈信息指向 strings.Split 之类无关处,排查成本高。
if ks.CfgReg == nil {
return fmt.Errorf("memory 模块:ks.CfgReg 为 nil —— " +
"Group 0 必须先建好配置中心(NewConfigRegistry)再启动本调度器")
}
if ks.DataDir == "" {
return fmt.Errorf("memory 模块:ks.DataDir 为空 —— 它是 Group 0 的产物,必须先设置")
}
memDB, err := memory.NewGraphDB(filepath.Join(ks.DataDir, "memory", "graph.db"))
if err != nil {
log.Printf("[homed] warning: memory init failed: %v", err)
memDB = nil
} else {
log.Printf("[homed] graph memory initialized")
}
memIdx := memory.NewIndexer(memDB)
memIdx.Sync() // 启动时立即同步,避免前 30 分钟空窗
socialStore := social.New(memDB)
distiller := pipeline.NewDistiller(memDB, ks.DataDir, pipeline.DistillerConfig{
Interval: 10 * time.Minute,
RetentionDays: 7,
BatchSize: 50,
})
// ★ 必须在 distiller.Start() **之前**注入:Start 会起 distillLoop,
// 而第一次 tick 在 Interval 之后。若先 Start 再注入,头一个周期
// (实测配置 10 分钟)会白跑一次 jieba 路 —— 而那条路实测产出 0 条。
if memDB != nil {
distiller.SetSplitter(initDistillSplitter(ks.CfgReg))
}
if memDB != nil {
// 这里**故意不写 defer distiller.Stop()**:函数 return 即触发 defer,
// 而 Stop() → cancel() 会让刚启动的 distillLoop 立刻退出,
// 规则蒸馏管线启动即死、10min 心跳从不运行(旧 main() 拆分时的残留)。
// 停机由 AddCloser 负责(见下方)。
distiller.Start()
}
ks.Memory = &kernelboot.MemoryBundle{
DB: memDB,
Indexer: memIdx,
Social: socialStore,
Distiller: distiller,
}
// 共享词嵌入:蒸馏与 Agent 上下文复用同一实例
// (避免同一模型被二次加载,约 200k×300 维 ≈ 数百 MB)。
ks.Embedder = memory.NewStaticEmbedder(
strings.Split(ks.CfgReg.GetString("core.agent.embedding_model_path", ""), ",")...)
distiller.SetEmbedder(ks.Embedder)
// ★ 伪关机登记(迁移数据目录用,见 kernelboot/lifecycle.go)。
// 只登记**持有写句柄**的 distiller —— 它每 10min 写 memory/graph.db,
// 迁移期间一旦写入就会搬走"写了一半的库"。
if ks.Memory != nil && ks.Memory.Distiller != nil {
ks.AddQuiescer("distiller",
func() error { return ks.Memory.Distiller.Pause() },
func() error { return ks.Memory.Distiller.Resume() })
}
ks.AddCloser(func() {
// 与原 main 的两个 defer 同序(LIFO):先停蒸馏器,再关图库。
if memDB != nil {
distiller.Stop()
memDB.Close()
}
})
return nil
}
// initDistillSplitter 按配置打开蒸馏用的生成侧 provider 并组装拆分器。
//
// 返回 nil 表示不启用(未配置 provider、或打开失败)—— 此时蒸馏回退既有的
// jieba/ONNX 路。**不因为模型不可用而让蒸馏停摆**:那条路虽然实测产出 0 条,
// 但它至少不会让对话记忆丢失。
func initDistillSplitter(cfgReg *config.ConfigRegistry) pipeline.RecordSplitter {
if cfgReg == nil {
return nil
}
providerName := cfgReg.GetString("core.memory.distill.generation.provider", "")
if providerName == "" {
return nil
}
opts := map[string]string{}
const optPrefix = "core.memory.distill.generation.options."
for _, key := range cfgReg.List(optPrefix) {
opts[strings.TrimPrefix(key, optPrefix)] = cfgReg.GetString(key, "")
}
provider, err := generation.Open(providerName, generation.Config{Options: opts})
if err != nil {
log.Printf("[homed] warning: opening distillation provider %q failed: %v (distillation falls back to jieba; registered: %s)",
providerName, err, strings.Join(generation.Names(), ", "))
return nil
}
if !provider.Info().SupportsJSONSchema {
// 拆分器的零幻觉完全依赖 schema 约束。不支持 schema 的 provider
// 会拿到自由文本,解析必炸 —— 与其让它运行时反复失败,不如现在就拒。
log.Printf("[homed] warning: distillation provider %q does not support JSON Schema, splitting is unreliable (falling back to jieba)",
providerName)
provider.Close()
return nil
}
log.Printf("[homed] distill generation active: provider=%s model=%s",
providerName, provider.Info().Model)
return distill.NewExtractor(provider, nil)
}
func init() { kernelmod.Register(Module{}) }