mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-11-05 16:57:32 +00:00
staticcheck 门接入时把存量设为基线 24 条,本次全部清掉,基线改为 0。
## 生产代码里的真实缺陷(4 个)
### ① memory/fusion.go —— BFS 的 depth 参数形同虚设
for d := 1; d <= depth; d++ {
neighbors, _ := g.NeighbourBlocks(seed.BlockID) // ← 永远是 seed
...
break // ← 只跑一轮
}
三处叠加:循环体末尾无条件 `break`;每层传的还是 `seed.BlockID`;
于是 depth=2 与 depth=3 召回结果**完全一样**。
staticcheck SA4008("变量在循环条件里从不改变")正是从这里报出来的 ——
它看出来 `d` 除了打分没有参与寻址。
改成真正的逐层 BFS(frontier 推进到上一层新发现的节点)。实测:
depth=1 → 2 个 [节点0 节点1]
depth=2 → 3 个 [节点0 节点1 节点2] ← 修复前停在这里
depth=3 → 4 个 [节点0 节点1 节点2 节点3]
补判据 `TestBFS召回_层深真实生效`:必须用**链**(n0-n1-n2-n3),
星型在 depth=1 就能召回全部叶子、断不出有没有走深。
变异(改回单层)⇒ 实测红:`★ depth=2 召回 2 个,期望 3`。
### ② nlp/extractor.go —— subjRels/objRels 只写不读
两个切片被 append 了 4 处,但**从未被读取**。三元组的关系类型来自
`relLabel := result.Tokens[vi]`,置信度来自 `result.DepRels[si]`,
与这两个切片无关。删除。
### ③ plugins/cmd/plugin.go —— 配置解析失败会把上限写成 0
if n, err := fmt.Sscanf(s, "%d", &p.maxOutput); err == nil && n > 0 {
} // ← 空分支
直接 Sscanf 到 &p.maxOutput:解析失败时它可能已被写成 0 或部分值,
而空分支无法回滚 ⇒ 配置写错一个字符就把输出上限变成 0
(下游 `maxLen := p.maxOutput` 直接拿它算截断长度)。
改为解析到临时变量、失败时保持默认并记日志。
### ④ cmd/homed/bootstrap.go —— 空的 else 分支
`if err != nil { log } else { }` —— 删掉 else。
## 测试判据里的真实缺陷(3 个)
### ⑤ stop_semantics_test —— 短路求值让第二次调用从不执行
if !a.sched.consumeCancel() || !a.sched.consumeCancel()
第一次返回 true 时第二次**根本不会执行**,于是"两条配额都可消费"
这条断言实际只测了一次。拆成两次独立调用(两处同型)。
### ⑥ seq/store_test —— 永假断言
if maxCallDepth != maxCallDepth { t.Fatal("maxCallDepth 不稳定") }
自己跟自己比,从诞生起就不可能失败。改成显式读两次比较。
### ⑦ pluginhost/catalog_test —— typed-nil 守卫永假
var nb *kstate.SyncBus
var iface kstate.SyncState = nb
if iface == nil { t.Skip(...) } // ← 恒 false
把 typed-nil 装进接口后,接口的 (type, value) 二元组都不为 nil。
这道前置守卫**从未生效** —— 而这正是该判据要演示的陷阱本身。
改用 `reflect.ValueOf(iface).IsNil()`。
## 其余(写法/冗余,无行为变化)
agent.go / context.go / toolresult_store_test / apply_regression_test
空 if 块(注释移到真实分支上方)
codec_chunkfast_golden_test 未使用的三个返回值 + 空 if
scheduler_critical/stack_test `if _, _ = f(); true { }` → 直接调用
overflow_test / upgrade_test 变量被整体覆盖,首次赋值未使用
dense_test m[string(x)] 替代临时变量
lex_rules_test range over string 而非 []rune(string)
## 验证
staticcheck -checks 'SA4*,SA5*,SA6*,SA9*' 0 条(基线 24 → 0)
go build ./... PASS
go vet ./... PASS
gofmt -l 干净
go test ./... -count=1 65 包全过,0 FAIL
干净检出(worktree) build + memory/nlp/core 全绿
320 lines
11 KiB
Go
320 lines
11 KiB
Go
package memory
|
||
|
||
import (
|
||
"fmt"
|
||
"testing"
|
||
)
|
||
|
||
// ═══════════════════════════════════════════════════════════════
|
||
// BFS 接入召回链 —— 判据
|
||
//
|
||
// ★ 真实缺口(生产探针 coexist 维度 0/1)
|
||
//
|
||
// 查询「本机服务监听哪些端口」召不回 13010。
|
||
// 原因:库里含「端口」二字的块是 14010端口 / 14011端口 那批,
|
||
// 而真正的本机端口块文本是「http://127.0.0.1:13010」——
|
||
// **不含「端口」二字**。
|
||
//
|
||
// ⇒ 纯向量/符号召回召不回它。
|
||
// ⇒ 而图上是连着的:billing服务 --监听--> 13010
|
||
// 只要从「监听」或端口号出发 BFS,就能找回服务名。
|
||
//
|
||
// ★ 所以这一格测的不是「关键词召回能不能更强」,
|
||
// 而是「召回**是否即联想**」—— 孤立命中之外,
|
||
// 还要沿边找回上下文。
|
||
// ═══════════════════════════════════════════════════════════════
|
||
|
||
func seedCoexistGraph(t *testing.T) *GraphDB {
|
||
t.Helper()
|
||
g := newTestGraph(t)
|
||
// 织一张真实形态的网:服务 --监听--> 端口号
|
||
blocks := []MemoryBlock{
|
||
{ID: "b_svc", Modality: BlockText, Text: "billing服务", Vector: []float64{1, 0}, Fingerprint: "fp1"},
|
||
{ID: "b_port", Modality: BlockText, Text: "13010", Vector: []float64{0.9, 0.1}, Fingerprint: "fp1"},
|
||
{ID: "b_noise1", Modality: BlockText, Text: "14010端口", Vector: []float64{0, 1}, Fingerprint: "fp1"},
|
||
{ID: "b_noise2", Modality: BlockText, Text: "14011端口", Vector: []float64{0, 1}, Fingerprint: "fp1"},
|
||
{ID: "b_noise3", Modality: BlockText, Text: "CPU总线与MAR/MDR/ALU连接问题", Vector: []float64{0, 1}, Fingerprint: "fp1"},
|
||
}
|
||
for i := range blocks {
|
||
if err := g.PutMemoryBlocks([]MemoryBlock{blocks[i]}); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
}
|
||
for _, e := range []struct{ from, to, typ string }{
|
||
{"b_svc", "b_port", "监听"},
|
||
{"b_noise1", "b_port", "监听"},
|
||
{"b_noise3", "b_svc", "涉及"},
|
||
} {
|
||
if err := g.AddRelationBlockEdge(e.from, e.to, e.typ,
|
||
RelationEdgeData{SessionID: "s1", Confidence: 0.9, Status: EdgeActive}); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
}
|
||
return g
|
||
}
|
||
|
||
// TestBFS召回_从服务找回端口:图上连着的就该召回
|
||
func TestBFS召回_从服务找回端口(t *testing.T) {
|
||
g := seedCoexistGraph(t)
|
||
defer func() { _ = g.Close() }()
|
||
|
||
// 查询直接命中「billing服务」——一个**孤立的语义节点**,
|
||
// 但图上它连着 13010。
|
||
// 带上查询向量:RecallBlocks 要求非空向量(符号路也依赖融合入口)
|
||
q := BlockRecallQuery{Vector: []float64{1, 0}, TopK: 5, MinScore: -1}
|
||
hits, _, err := g.RecallBlocksFusedBFS(q, "billing服务", 2)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
fmt.Printf(" 命中 %d 个: ", len(hits))
|
||
for _, h := range hits {
|
||
fmt.Printf("%q ", h.Text)
|
||
}
|
||
fmt.Println()
|
||
|
||
if !containsText(hits, "13010") {
|
||
t.Errorf("★ 召回应沿边找回端口号 13010(召回即联想),实际 %v", textsOfFused(hits))
|
||
}
|
||
}
|
||
|
||
// TestBFS召回_顺序稳定:map 遍历会让输出顺序随机
|
||
func TestBFS召回_顺序稳定(t *testing.T) {
|
||
g := seedCoexistGraph(t)
|
||
defer func() { _ = g.Close() }()
|
||
|
||
// 带上查询向量:RecallBlocks 要求非空向量(符号路也依赖融合入口)
|
||
q := BlockRecallQuery{Vector: []float64{1, 0}, TopK: 5, MinScore: -1}
|
||
var first []string
|
||
for run := 0; run < 5; run++ {
|
||
hits, _, err := g.RecallBlocksFusedBFS(q, "billing服务", 2)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
got := textsOfFused(hits)
|
||
if run == 0 {
|
||
first = got
|
||
continue
|
||
}
|
||
if len(got) != len(first) {
|
||
t.Fatalf("第 %d 次长度不同: %v vs %v", run, got, first)
|
||
}
|
||
for i := range got {
|
||
if got[i] != first[i] {
|
||
t.Fatalf("★ 第 %d 次顺序不同: %v vs %v(map 遍历导致输出不稳定)", run, got, first)
|
||
}
|
||
}
|
||
}
|
||
fmt.Printf(" 5 次顺序一致: %v\n", first)
|
||
}
|
||
|
||
// TestBFS召回_预算有界:不能因为加 BFS 就无限膨胀
|
||
func TestBFS召回_预算有界(t *testing.T) {
|
||
g := newTestGraph(t)
|
||
defer func() { _ = g.Close() }()
|
||
|
||
// 织一个 200 节点的链:加 BFS 若无预算,depth=3 会拉进一大片
|
||
for i := 0; i < 200; i++ {
|
||
id := fmt.Sprintf("n%03d", i)
|
||
if err := g.PutMemoryBlocks([]MemoryBlock{
|
||
{ID: id, Modality: BlockText, Text: fmt.Sprintf("节点%03d", i),
|
||
Vector: []float64{1, 0}, Fingerprint: "fp1"},
|
||
}); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if i > 0 {
|
||
if err := g.AddRelationBlockEdge(fmt.Sprintf("n%03d", i-1), id, "next",
|
||
RelationEdgeData{SessionID: "s", Status: EdgeActive}); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
}
|
||
}
|
||
|
||
// 带上查询向量:RecallBlocks 要求非空向量(符号路也依赖融合入口)
|
||
q := BlockRecallQuery{Vector: []float64{1, 0}, TopK: 5, MinScore: -1}
|
||
hits, _, err := g.RecallBlocksFusedBFS(q, "节点000", 3)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
fmt.Printf(" 200 节点链,depth=3 → 召回 %d 个\n", len(hits))
|
||
if len(hits) > 60 {
|
||
t.Errorf("★ BFS 应受预算约束(200 节点链 depth=3 召回 %d 个,过多)", len(hits))
|
||
}
|
||
}
|
||
|
||
func containsText(hits []FusedHit, s string) bool {
|
||
for _, h := range hits {
|
||
if h.Text == s {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
func textsOfFused(hits []FusedHit) []string {
|
||
out := make([]string, 0, len(hits))
|
||
for _, h := range hits {
|
||
out = append(out, h.Text)
|
||
}
|
||
return out
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════════════
|
||
// 中文字符切分 —— 判据(2026-10-04)
|
||
//
|
||
// ★ 这是生产探针 coexist 0/1 的**真正根因**
|
||
//
|
||
// 滑窗「每位置只取最长窗口」把目标词吃掉了:
|
||
//
|
||
// 「本机服务监听哪些端口」
|
||
// 滑窗 → ["本机服务", "监听哪些"] 全是跨词伪词
|
||
// jieba → [本机 服务 监听 哪些 端口] 「端口」独立成词
|
||
//
|
||
// ⇒ 「端口」从未单独成窗 ⇒ 符号路对「14010端口」全打 0 分
|
||
// ⇒ 直接命中全是噪音 ⇒ 图联想也无从谈起
|
||
// ═══════════════════════════════════════════════════════════════
|
||
|
||
// TestQuerySymbols_目标词必须独立成词
|
||
func TestQuerySymbols_目标词必须独立成词(t *testing.T) {
|
||
for _, tc := range []struct {
|
||
query string
|
||
want []string // 必须出现的词
|
||
notWant []string // 跨词伪词,出现即失败
|
||
}{
|
||
{
|
||
query: "本机服务监听哪些端口",
|
||
want: []string{"端口", "服务", "监听"},
|
||
notWant: []string{"听哪些", "些端口", "务监听哪"},
|
||
},
|
||
{
|
||
query: "grafana 监控面板的端口是多少",
|
||
want: []string{"端口", "面板", "grafana"},
|
||
notWant: []string{"控面板的", "板的端口"},
|
||
},
|
||
{
|
||
query: "本机 13010 端口对应什么服务",
|
||
want: []string{"13010", "端口", "服务"},
|
||
},
|
||
} {
|
||
syms := QuerySymbols(tc.query)
|
||
set := make(map[string]bool, len(syms))
|
||
for _, s := range syms {
|
||
set[s] = true
|
||
}
|
||
fmt.Printf(" %q → %v\n", tc.query, syms)
|
||
for _, w := range tc.want {
|
||
if !set[w] {
|
||
t.Errorf("★ %q 的符号应含 %q(目标词被切分吃掉),实际 %v", tc.query, w, syms)
|
||
}
|
||
}
|
||
for _, w := range tc.notWant {
|
||
if set[w] {
|
||
t.Errorf("★ %q 的符号不该含跨词伪词 %q,实际 %v", tc.query, w, syms)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// TestQuerySymbols_数字与英文精确串不受分词影响
|
||
func TestQuerySymbols_数字与英文精确串(t *testing.T) {
|
||
syms := QuerySymbols("本机 13010 端口对应 grafana 9.99.99 吗")
|
||
set := map[string]bool{}
|
||
for _, s := range syms {
|
||
set[s] = true
|
||
}
|
||
for _, want := range []string{"13010", "grafana", "9.99.99"} {
|
||
if !set[want] {
|
||
t.Errorf("★ 精确串 %q 必须原样保留,实际 %v", want, syms)
|
||
}
|
||
}
|
||
}
|
||
|
||
// TestSymbolScore_目标词能匹配到对应块:端到端的一环
|
||
func TestSymbolScore_目标词能匹配到对应块(t *testing.T) {
|
||
syms := QuerySymbols("本机服务监听哪些端口")
|
||
for _, tc := range []struct {
|
||
text string
|
||
shouldMatch bool
|
||
}{
|
||
{"14010端口", true},
|
||
{"端口", true},
|
||
{"13010/13011 而非 12011", false}, // 不含目标词
|
||
} {
|
||
_, matched, _ := SymbolScore(syms, tc.text)
|
||
got := len(matched) > 0
|
||
if got != tc.shouldMatch {
|
||
t.Errorf("★ %q 是否匹配 %q:期望 %v,实际 %v(matched=%v)",
|
||
tc.text, "端口", tc.shouldMatch, got, matched)
|
||
}
|
||
}
|
||
}
|
||
|
||
// TestBFS召回_层深真实生效 钉住 depth 参数真的有语义。
|
||
//
|
||
// ## 守的是什么(2026-11-04 修的实测缺陷)
|
||
//
|
||
// 原实现:
|
||
//
|
||
// for d := 1; d <= depth; d++ {
|
||
// neighbors, _ := g.NeighbourBlocks(seed.BlockID) // ← 永远是 seed
|
||
// ...
|
||
// break // ← 只跑一轮
|
||
// }
|
||
//
|
||
// 三个问题叠在一起:
|
||
//
|
||
// ① 循环体末尾无条件 break ⇒ d 循环只执行一次
|
||
// ② NeighbourBlocks 每层传的还是 seed.BlockID ⇒ 第 2 层取的是同一批邻居
|
||
// ③ 于是 depth 参数**形同虚设**:传 2 和传 3 召回结果完全一样
|
||
//
|
||
// staticcheck 的 SA4008("variable in loop condition never changes")
|
||
// 正是从这里报出来的 —— 它看出来 `d` 除了打分没有参与寻址。
|
||
//
|
||
// ## 判据为什么必须是"链"而不是"星型"
|
||
//
|
||
// 星型(一个中心连 N 个叶子)在 depth=1 就能召回全部叶子,第 2 层没有新节点
|
||
// ⇒ 断不出"有没有真的走深"。必须是链:n0-n1-n2-n3,depth=d 恰好召回 d+1 个。
|
||
func TestBFS召回_层深真实生效(t *testing.T) {
|
||
g := newTestGraph(t)
|
||
defer func() { _ = g.Close() }()
|
||
|
||
// 造一条 4 节点链:n0 → n1 → n2 → n3
|
||
for i := 0; i < 4; i++ {
|
||
id := fmt.Sprintf("n%d", i)
|
||
if err := g.PutMemoryBlocks([]MemoryBlock{{
|
||
ID: id, Modality: BlockText, Text: fmt.Sprintf("链节点%d", i),
|
||
Vector: []float64{1, 0}, Fingerprint: "fp1",
|
||
}}); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if i > 0 {
|
||
if err := g.AddRelationBlockEdge(fmt.Sprintf("n%d", i-1), id, "next",
|
||
RelationEdgeData{SessionID: "s", Status: EdgeActive}); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
}
|
||
}
|
||
|
||
q := BlockRecallQuery{Vector: []float64{1, 0}, TopK: 1, MinScore: -1}
|
||
got := map[int]int{}
|
||
for _, d := range []int{1, 2, 3} {
|
||
hits, _, err := g.RecallBlocksFusedBFS(q, "链节点0", d)
|
||
if err != nil {
|
||
t.Fatalf("depth=%d: %v", d, err)
|
||
}
|
||
got[d] = len(hits)
|
||
}
|
||
|
||
// depth=d 应召回命中(1) + d 层邻居 = d+1 个。
|
||
for _, d := range []int{1, 2, 3} {
|
||
if got[d] != d+1 {
|
||
t.Fatalf("★ depth=%d 召回 %d 个,期望 %d ——\n"+
|
||
"说明 BFS 没有真的逐层推进(depth 参数形同虚设)。\n"+
|
||
"实测各层: %v", d, got[d], d+1, got)
|
||
}
|
||
}
|
||
// 交叉验证:不同 depth 必须给出**不同**结果,否则参数无意义。
|
||
if got[1] == got[3] {
|
||
t.Fatalf("★ depth=1 与 depth=3 召回数相同(%d)—— depth 没生效", got[1])
|
||
}
|
||
}
|