mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-22 18:08:04 +00:00
feat(healthcheck): 内核状态快照报出内核版本号与 ONNX 模型启用状态
healthcheck_kernel 此前没有任何「ONNX 模型是否在用」的信息,只报「向量可用/不可用」, 分不清「统一多模态空间已加载」与「退回到词嵌入/TF-IDF 路径」;人格卡要求 「版本以运行时快照为准」,也缺一个可查字段(build.version 早就在,但没人知道)。 - KernelStatus 新增 onnx 段:enabled / provider / dim / fingerprint / modalities / reason。 判据取 Loaded()(provider 真正打开且元数据合法),**不是**「配置里写了 provider」 —— 后者在模型缺失 / 运行时缺失时也为真,报出去就是假绿。 - 未启用时 reason 给**具体原因**:未配置(说明会走回退路径)/ 打开失败的具体错误。 homed 把「配置的 provider 名」与「打开失败原因」透传给 Agent,仅供状态报告。 - ProviderAdapter 新增 Modalities()(可选能力,按接口断言取用,不改公开契约)。 - healthcheck_kernel 的工具描述同步说明它回答这两件事。 **顺带修一个真实 panic**:collectKernelStatus 的 knowledge 是**接口**参数, (*knowledge.Store)(nil) 塞进接口后 `ks != nil` 仍为真 → 调 List() 直接 panic, 而 healthcheck_kernel 正是走这条路径(panic 发生在工具 goroutine 里)。 GetKernelStatus 改为先按具体指针判空、再赋给接口;并加刻画测试钉住这个成因 (一旦不再 panic 说明参数形状已变,守卫与该测试应同步删除)。 验证:单测 4 例(已启用 / 打开失败 / 未配置 / 未加载)+ 刻画测试; 隔离实例 E2E 7/7:正例 provider=chineseclip → enabled=true、dim=512、模态 2; 反例 provider=nonexistent → enabled=false 且 reason 含具体错误与 provider 名, 真实对话仍通。
This commit is contained in:
@ -367,7 +367,11 @@ func main() {
|
||||
// 布局、预处理、解码、运行时全部属于 provider 内部实现。
|
||||
// provider 名为空时禁用多模态向量检索,退回纯 fastText 文本路径。
|
||||
var multimodalSpace vector.MultimodalEmbedder
|
||||
// 这两个值只用于状态报告(healthcheck_kernel 的 onnx 段):
|
||||
// 「配了哪个 provider」与「为什么没启用」,避免只能看到 false 却不知原因。
|
||||
var mmProviderName, mmErr string
|
||||
if mmProvider := cfgReg.GetString("core.memory.multimodal_space.provider", ""); mmProvider != "" {
|
||||
mmProviderName = mmProvider
|
||||
opts := map[string]string{}
|
||||
const optPrefix = "core.memory.multimodal_space.options."
|
||||
for _, key := range cfgReg.List("core.memory.multimodal_space.options.") {
|
||||
@ -375,10 +379,12 @@ func main() {
|
||||
}
|
||||
provider, err := embedding.Open(mmProvider, embedding.Config{Options: opts})
|
||||
if err != nil {
|
||||
mmErr = err.Error()
|
||||
log.Printf("[homed] warning: 多模态向量 provider %q 打开失败: %v(多模态向量检索已禁用;已注册: %s)",
|
||||
mmProvider, err, strings.Join(embedding.Names(), ", "))
|
||||
} else if adapted, err := vector.AdaptProvider(provider); err != nil {
|
||||
provider.Close()
|
||||
mmErr = err.Error()
|
||||
log.Printf("[homed] warning: 多模态向量 provider %q 元数据不合法: %v(多模态向量检索已禁用)", mmProvider, err)
|
||||
} else {
|
||||
multimodalSpace = adapted
|
||||
@ -533,6 +539,8 @@ func main() {
|
||||
EmbeddingModelPath: cfgReg.GetString("core.agent.embedding_model_path", ""),
|
||||
Embedder: embedder,
|
||||
MultimodalSpace: multimodalSpace,
|
||||
EmbeddingProvider: mmProviderName,
|
||||
EmbeddingError: mmErr,
|
||||
StageHost: stageHost,
|
||||
EventBus: evBus,
|
||||
ThinkingEnabled: cfg.LLM.ThinkingEnabled,
|
||||
|
||||
@ -154,6 +154,12 @@ type Agent struct {
|
||||
// 也可以是外部 API 客户端;两者共享同一套 L0/L2/L3 向量缓存与检索基础设施。
|
||||
multimodalSpace vector.MultimodalEmbedder
|
||||
|
||||
// embeddingProvider 是配置里指定的统一向量空间 provider 名;
|
||||
// embeddingError 是打开/适配失败的原因(成功时为空)。
|
||||
// 二者只用于状态报告:区分「没配」「配了但打不开」「已启用」。
|
||||
embeddingProvider string
|
||||
embeddingError string
|
||||
|
||||
// fusionCfg 控制文本路与视觉路的跨模态融合权重,可按模型实测结果配置。
|
||||
fusionCfg CrossModalFusionConfig
|
||||
|
||||
@ -179,12 +185,16 @@ type AgentConfig struct {
|
||||
Indexer *memory.Indexer
|
||||
Tracker *tracker.Tracker
|
||||
|
||||
DocStore *document.Store
|
||||
Knowledge *knowledge.Store
|
||||
SocialStore *social.SocialStore
|
||||
TextMemory *text.Memory
|
||||
MediaStore *media.Store
|
||||
MultimodalSpace vector.MultimodalEmbedder
|
||||
DocStore *document.Store
|
||||
Knowledge *knowledge.Store
|
||||
SocialStore *social.SocialStore
|
||||
TextMemory *text.Memory
|
||||
MediaStore *media.Store
|
||||
MultimodalSpace vector.MultimodalEmbedder
|
||||
// EmbeddingProvider / EmbeddingError 是向量空间的配置身份与打开失败原因,
|
||||
// 供 healthcheck_kernel 状态报告区分「未配置 / 打开失败 / 已启用」。
|
||||
EmbeddingProvider string
|
||||
EmbeddingError string
|
||||
FusionCfg CrossModalFusionConfig // 跨模态融合权重;零值用默认
|
||||
Personality *agentPkg.Personality
|
||||
PersonaStore PersonaStore // 人格设定的读写面(首启门禁 + persona_set 工具)
|
||||
@ -256,46 +266,48 @@ func New(cfg AgentConfig) *Agent {
|
||||
}
|
||||
|
||||
return &Agent{
|
||||
id: cfg.ID,
|
||||
startTime: time.Now(),
|
||||
provider: cfg.Provider,
|
||||
providerManager: cfg.ProviderManager,
|
||||
io: cfg.IO,
|
||||
memory: cfg.Memory,
|
||||
indexer: cfg.Indexer,
|
||||
tracker: cfg.Tracker,
|
||||
context: rc,
|
||||
systemPrompt: cfg.SystemPrompt,
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
docStore: cfg.DocStore,
|
||||
knowledge: cfg.Knowledge,
|
||||
social: cfg.SocialStore,
|
||||
textMem: cfg.TextMemory,
|
||||
mediaStore: cfg.MediaStore,
|
||||
personality: cfg.Personality,
|
||||
personaStore: cfg.PersonaStore,
|
||||
pluginReg: cfg.PluginReg,
|
||||
pluginDir: cfg.PluginDir,
|
||||
distillInterval: cfg.DistillInterval,
|
||||
archiveInterval: cfg.ArchiveInterval,
|
||||
reviewInterval: cfg.ReviewInterval,
|
||||
mergeInterval: cfg.MergeInterval,
|
||||
maxContextSize: cfg.MaxContextSize,
|
||||
stageHost: cfg.StageHost,
|
||||
skillIndex: cfg.SkillIndexProvider,
|
||||
eventBus: cfg.EventBus,
|
||||
selfInputCh: make(chan selfInputMsg, 64),
|
||||
childTasks: make(map[string]*childTaskState),
|
||||
interceptCh: make(chan *agentIO.InputEvent, 64),
|
||||
pluginHealth: newPluginHealthTracker(),
|
||||
thinkingEnabled: cfg.ThinkingEnabled,
|
||||
inputCfg: cfg.InputProcessing,
|
||||
embedder: embedder,
|
||||
multimodalSpace: cfg.MultimodalSpace,
|
||||
fusionCfg: cfg.FusionCfg,
|
||||
noMergeMarkers: make(map[string]int),
|
||||
lastInput: make(map[string]time.Time),
|
||||
id: cfg.ID,
|
||||
startTime: time.Now(),
|
||||
provider: cfg.Provider,
|
||||
providerManager: cfg.ProviderManager,
|
||||
io: cfg.IO,
|
||||
memory: cfg.Memory,
|
||||
indexer: cfg.Indexer,
|
||||
tracker: cfg.Tracker,
|
||||
context: rc,
|
||||
systemPrompt: cfg.SystemPrompt,
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
docStore: cfg.DocStore,
|
||||
knowledge: cfg.Knowledge,
|
||||
social: cfg.SocialStore,
|
||||
textMem: cfg.TextMemory,
|
||||
mediaStore: cfg.MediaStore,
|
||||
personality: cfg.Personality,
|
||||
personaStore: cfg.PersonaStore,
|
||||
pluginReg: cfg.PluginReg,
|
||||
pluginDir: cfg.PluginDir,
|
||||
distillInterval: cfg.DistillInterval,
|
||||
archiveInterval: cfg.ArchiveInterval,
|
||||
reviewInterval: cfg.ReviewInterval,
|
||||
mergeInterval: cfg.MergeInterval,
|
||||
maxContextSize: cfg.MaxContextSize,
|
||||
stageHost: cfg.StageHost,
|
||||
skillIndex: cfg.SkillIndexProvider,
|
||||
eventBus: cfg.EventBus,
|
||||
selfInputCh: make(chan selfInputMsg, 64),
|
||||
childTasks: make(map[string]*childTaskState),
|
||||
interceptCh: make(chan *agentIO.InputEvent, 64),
|
||||
pluginHealth: newPluginHealthTracker(),
|
||||
thinkingEnabled: cfg.ThinkingEnabled,
|
||||
inputCfg: cfg.InputProcessing,
|
||||
embedder: embedder,
|
||||
multimodalSpace: cfg.MultimodalSpace,
|
||||
embeddingProvider: cfg.EmbeddingProvider,
|
||||
embeddingError: cfg.EmbeddingError,
|
||||
fusionCfg: cfg.FusionCfg,
|
||||
noMergeMarkers: make(map[string]int),
|
||||
lastInput: make(map[string]time.Time),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -198,6 +198,14 @@ func (a *Agent) GetKernelStatus() *KernelStatus {
|
||||
trk = a.tracker
|
||||
}
|
||||
|
||||
// 注意:knowledge 在 collectKernelStatus 里是**接口**参数,
|
||||
// 而 (*knowledge.Store)(nil) 塞进接口后 `ks != nil` 仍为真 → 调 List() 直接 panic。
|
||||
// 所以这里必须先判具体指针再进行接口赋值(healthcheck_kernel 会走到这条路径)。
|
||||
var knowledgeLister interface{ List() []string }
|
||||
if a.knowledge != nil {
|
||||
knowledgeLister = a.knowledge
|
||||
}
|
||||
|
||||
ks := collectKernelStatus(
|
||||
a.startTime,
|
||||
string(a.id),
|
||||
@ -207,15 +215,43 @@ func (a *Agent) GetKernelStatus() *KernelStatus {
|
||||
a.io,
|
||||
a.pluginReg,
|
||||
a.memory,
|
||||
a.knowledge,
|
||||
knowledgeLister,
|
||||
a.docStore,
|
||||
textMem,
|
||||
socialStore,
|
||||
trk,
|
||||
)
|
||||
ks.ONNX = a.onnxStatus()
|
||||
|
||||
return ks
|
||||
}
|
||||
|
||||
// onnxStatus 汇总统一多模态向量空间(ONNX 模型)的启用状态。
|
||||
//
|
||||
// 判据是 Loaded()(provider 真正打开且元数据合法),**不是**「配置里写了 provider」——
|
||||
// 后者在模型缺失 / 运行时缺失时也为真,拿它当判据就是假绿。
|
||||
func (a *Agent) onnxStatus() sdk.ONNXStatus {
|
||||
st := sdk.ONNXStatus{Provider: a.embeddingProvider}
|
||||
if a.multimodalSpace != nil && a.multimodalSpace.Loaded() {
|
||||
st.Enabled = true
|
||||
st.Dim = a.multimodalSpace.Dim()
|
||||
st.Fingerprint = a.multimodalSpace.Fingerprint()
|
||||
// 模态是可选能力:只有底层 provider 报出来时才带出。
|
||||
if mr, ok := a.multimodalSpace.(interface{ Modalities() []string }); ok {
|
||||
st.Modalities = mr.Modalities()
|
||||
}
|
||||
return st
|
||||
}
|
||||
switch {
|
||||
case a.embeddingError != "":
|
||||
st.Reason = "打开失败: " + a.embeddingError
|
||||
case a.embeddingProvider == "":
|
||||
st.Reason = "未配置统一向量空间 provider(走词嵌入/TF-IDF 回退路径)"
|
||||
default:
|
||||
st.Reason = "provider 未加载"
|
||||
}
|
||||
return st
|
||||
}
|
||||
|
||||
var _ StatusProvider = (*Agent)(nil)
|
||||
var _ sdk.StatusAPI = (*Agent)(nil)
|
||||
|
||||
103
internal/agent/core/status_onnx_test.go
Normal file
103
internal/agent/core/status_onnx_test.go
Normal file
@ -0,0 +1,103 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
agentIO "gitcode.com/JianFeeeee/HomeAgent/internal/agent/io"
|
||||
"gitcode.com/JianFeeeee/HomeAgent/internal/knowledge"
|
||||
)
|
||||
|
||||
// statusSpace 是带模态元数据的假统一空间(ProviderAdapter 的 Modalities() 形状)。
|
||||
type statusSpace struct{ fakeSpace }
|
||||
|
||||
func (statusSpace) Modalities() []string { return []string{"text", "image"} }
|
||||
|
||||
// statusSpaceUnloaded 模拟「provider 建了但没加载成功」。
|
||||
type statusSpaceUnloaded struct{ fakeSpace }
|
||||
|
||||
func (statusSpaceUnloaded) Loaded() bool { return false }
|
||||
|
||||
// healthcheck_kernel 必须能回答两件事:内核版本号、以及**是否真的启用了 ONNX 模型**。
|
||||
//
|
||||
// 判据要点:只有 provider 真正 Loaded() 才算启用 ——「配置里写了 provider」不算,
|
||||
// 否则模型缺失/运行时缺失时会报成已启用(假绿)。
|
||||
func TestKernelStatusReportsVersionAndONNX(t *testing.T) {
|
||||
t.Run("已启用:带出 provider/维度/指纹/模态", func(t *testing.T) {
|
||||
a := &Agent{
|
||||
io: agentIO.NewIOManager(),
|
||||
multimodalSpace: statusSpace{},
|
||||
embeddingProvider: "chineseclip",
|
||||
}
|
||||
st := a.GetKernelStatus()
|
||||
if !st.ONNX.Enabled {
|
||||
t.Fatal("Loaded() 为真时 onnx.enabled 必须为真")
|
||||
}
|
||||
if st.ONNX.Provider != "chineseclip" || st.ONNX.Dim != 2 || st.ONNX.Fingerprint != "fake-space" {
|
||||
t.Fatalf("onnx 身份字段不对: %+v", st.ONNX)
|
||||
}
|
||||
if len(st.ONNX.Modalities) != 2 || st.ONNX.Reason != "" {
|
||||
t.Fatalf("模态/原因不对: %+v", st.ONNX)
|
||||
}
|
||||
// 内核版本号必须随状态一起报:人格卡要求「版本以运行时快照为准」靠的就是这一项
|
||||
if st.Build.Version == "" || st.Build.KernelName == "" {
|
||||
t.Fatalf("build 段缺少版本/内核名: %+v", st.Build)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("打开失败:enabled=false 且给出具体原因", func(t *testing.T) {
|
||||
a := &Agent{
|
||||
io: agentIO.NewIOManager(),
|
||||
embeddingProvider: "chineseclip",
|
||||
embeddingError: `embedding: open provider "chineseclip": model dir missing`,
|
||||
}
|
||||
st := a.GetKernelStatus()
|
||||
if st.ONNX.Enabled {
|
||||
t.Fatal("打开失败时不能报 enabled")
|
||||
}
|
||||
if st.ONNX.Provider != "chineseclip" {
|
||||
t.Fatalf("未启用时仍应带出配置的 provider: %+v", st.ONNX)
|
||||
}
|
||||
if !strings.Contains(st.ONNX.Reason, "model dir missing") {
|
||||
t.Fatalf("原因应包含具体错误: %q", st.ONNX.Reason)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("未配置:说明会走回退路径", func(t *testing.T) {
|
||||
a := &Agent{io: agentIO.NewIOManager()}
|
||||
st := a.GetKernelStatus()
|
||||
if st.ONNX.Enabled || st.ONNX.Reason == "" {
|
||||
t.Fatalf("未配置时应 enabled=false 且有原因: %+v", st.ONNX)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("provider 存在但未加载", func(t *testing.T) {
|
||||
a := &Agent{
|
||||
io: agentIO.NewIOManager(),
|
||||
multimodalSpace: statusSpaceUnloaded{},
|
||||
embeddingProvider: "qwen3vl",
|
||||
}
|
||||
st := a.GetKernelStatus()
|
||||
if st.ONNX.Enabled {
|
||||
t.Fatal("Loaded() 为假时不能报 enabled")
|
||||
}
|
||||
if st.ONNX.Reason == "" {
|
||||
t.Fatalf("应给出未加载的原因: %+v", st.ONNX)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// collectKernelStatus 的 knowledge 参数是**接口**类型,而 (*knowledge.Store)(nil)
|
||||
// 塞进接口后 `ks != nil` 仍为真 → 调 List() 直接 panic。
|
||||
// 这条测试钉住这个成因:一旦不再 panic,说明参数形状变了,
|
||||
// GetKernelStatus 里的 typed-nil 守卫就该同步删掉(否则它变成无意义代码)。
|
||||
func TestCollectKernelStatusTypedNilKnowledgePanics(t *testing.T) {
|
||||
defer func() {
|
||||
if r := recover(); r == nil {
|
||||
t.Fatal("typed-nil 交给接口参数却未 panic:成因已变,请更新守卫与本测试")
|
||||
}
|
||||
}()
|
||||
var nilStore *knowledge.Store
|
||||
_ = collectKernelStatus(time.Now(), "a", "", 0, nil, nil, nil, nil, nilStore, nil, nil, nil, nil)
|
||||
}
|
||||
@ -65,6 +65,18 @@ func (a *ProviderAdapter) embed(input embedding.Input) ([]float64, error) {
|
||||
func (a *ProviderAdapter) Fingerprint() string { return a.info.Fingerprint }
|
||||
func (a *ProviderAdapter) Dim() int { return a.info.Dimension }
|
||||
|
||||
// Modalities 报告该空间支持的输入模态(text/image/...)。
|
||||
//
|
||||
// 模态是**可选能力**:MultimodalEmbedder 契约里没有它,状态查询按接口断言取用,
|
||||
// 所以这里既不改公开接口,也不影响其它实现(核心也不硬编码任何模型名)。
|
||||
func (a *ProviderAdapter) Modalities() []string {
|
||||
out := make([]string, 0, len(a.info.Modalities))
|
||||
for _, m := range a.info.Modalities {
|
||||
out = append(out, string(m))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (a *ProviderAdapter) Loaded() bool {
|
||||
a.mu.RLock()
|
||||
defer a.mu.RUnlock()
|
||||
|
||||
@ -240,7 +240,7 @@ func (p *Plugin) Start(s *sdk.PluginSDK) error {
|
||||
p.selfToolNames["healthcheck_kernel"] = true
|
||||
s.RegisterTool("healthcheck_kernel", sdk.ToolDef{
|
||||
Name: "healthcheck_kernel",
|
||||
Description: "查询 Agent 内核运行状态快照,包括插件/工具/记忆/知识库/LLM Provider/运行时等各子系统信息。Agent 可通过此工具自主监测内核健康。",
|
||||
Description: "查询 Agent 内核运行状态快照,包括**内核版本号与构建身份**(build.version / commit / build_time)、**统一多模态向量空间(ONNX 模型)是否启用**(onnx.enabled,未启用时给出原因)、插件/工具/记忆/知识库/LLM Provider/运行时等各子系统信息。Agent 可通过此工具自主监测内核健康。",
|
||||
Parameters: map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{},
|
||||
|
||||
@ -38,9 +38,29 @@ type KernelStatus struct {
|
||||
|
||||
Runtime RuntimeStatus `json:"runtime"`
|
||||
|
||||
// ONNX 报告统一多模态向量空间(ONNX 模型)是否**真的在用**。
|
||||
//
|
||||
// 为何单列:内核的向量能力是三层降级(统一多模态空间 → 词嵌入 → TF-IDF),
|
||||
// 只报「向量可用/不可用」分不清「ONNX 模型已加载」与「退回了纯文本路径」。
|
||||
// 模型缺失 / 运行时缺失 / provider 打开失败时这里是 enabled=false + reason。
|
||||
ONNX ONNXStatus `json:"onnx"`
|
||||
|
||||
Tracker TrackerStatus `json:"tracker"`
|
||||
}
|
||||
|
||||
// ONNXStatus 是统一多模态向量空间(ONNX 模型)的启用状态与身份。
|
||||
type ONNXStatus struct {
|
||||
// Enabled 是 provider 真正打开且元数据合法(不是「配置里写了 provider」)。
|
||||
Enabled bool `json:"enabled"`
|
||||
// Provider 是配置指定的 provider 名(如 chineseclip / qwen3vl / http)。
|
||||
Provider string `json:"provider,omitempty"`
|
||||
Dim int `json:"dim,omitempty"`
|
||||
Fingerprint string `json:"fingerprint,omitempty"`
|
||||
Modalities []string `json:"modalities,omitempty"`
|
||||
// Reason 是未启用时的原因(未配置 / 打开失败的具体错误 / 其它)。
|
||||
Reason string `json:"reason,omitempty"`
|
||||
}
|
||||
|
||||
type PluginInfo struct {
|
||||
Name string `json:"name"`
|
||||
Loaded bool `json:"loaded"`
|
||||
|
||||
Reference in New Issue
Block a user