Files
HomeAgent/internal/memory/synth_model_test.go
JianFeeeee 147d0baaf9 fix: LLM 工具循环 400、中断消息注入、ConPTY 终端支持
- agent: 工具轮请求尾部补 user 占位(zen 网关强制),tool 消息正确配对
- agent: 工具提醒/中断以 system 角色注入并带 [中断消息] 前缀,不进用户履历;系统提示词说明中断消息格式
- agentcli: 基于 ConPTY 的交互式终端(ptywin fork),terminal_create/read/write/resize/close/watch
- webui: server 输出通道适配器(保留 reasoning_content/disable_thinking)
- GUI: 沉浸式标题栏、icon 圆角重制、mascot 等打磨
2026-08-14 00:48:40 +08:00

131 lines
4.6 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
// 共享测试工具:在 t.TempDir() 中生成小型合成 word2vec 文本模型,
// 替代曾硬编码在 /tmp 的真实 fastText 模型(依赖网络下载与全局文件)。
// 各领域词簇落在正交维度上,语义测试断言即可稳定复现。
import (
"fmt"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
"testing"
)
// synthClusters 领域词簇:簇索引即向量维度,同簇词共享同一维度。
var synthClusters = map[int][]string{
0: {"天气", "下雨", "明天", "今天", "台风", "降温", "气象", "预报", "雨"},
1: {"股票", "基金", "投资", "定投", "收益", "行情", "理财", "风险", "策略", "市场", "stock", "涨"},
2: {"微积分", "导数", "数学", "作业", "公式", "求解", "计算", "题目"},
3: {"大学", "招生", "录取", "分数", "医学", "医药", "专业", "分数线", "志愿", "高考", "升学", "排名", "咨询", "河南", "university"},
4: {"老大", "私聊", "消息", "回复", "汇报", "任务", "安排", "收到", "boss"},
5: {"图片", "转换", "工具", "图标", "画布", "svg"},
6: {"南航", "航空", "航天", "电气", "院校", "民航"},
7: {"前端", "组件", "封装", "布局", "页面", "路由", "交互", "调试", "优化", "代码", "开发", "逻辑", "react", "javascript"},
8: {"服务器", "配置", "部署", "容器", "代理", "证书", "备份", "恢复", "监控", "告警", "数据库", "反向", "续期", "nginx", "docker", "server", "computer", "电脑"},
}
// synthNeutral 通用词:落在最后一个维度,不参与领域区分。
var synthNeutral = []string{
"会", "不会", "帮", "查", "看", "最近", "晚上", "随便", "推荐", "电影",
"注意", "安全", "可以", "说", "事情", "要求", "检查", "状态", "获取",
"实现", "测试", "结果", "问题", "处理", "已经", "相关", "需要", "使用",
"方法", "信息", "好的", "内容", "发送", "询问", "朋友",
}
// writeSynthModel 生成合成 word2vec 文本模型文件并返回路径。
func writeSynthModel(t testing.TB, dim int) string {
t.Helper()
words := make(map[string][]float64)
var clusterDims []int
for c := range synthClusters {
clusterDims = append(clusterDims, c)
}
sort.Ints(clusterDims)
for _, c := range clusterDims {
for _, w := range synthClusters[c] {
vec := make([]float64, dim)
vec[c] = 1.0
words[w] = vec
}
}
neutralDim := len(synthClusters)
for _, w := range synthNeutral {
vec := make([]float64, dim)
vec[neutralDim] = 1.0
words[w] = vec
}
path := filepath.Join(t.TempDir(), "synth.vec")
var sb strings.Builder
sb.WriteString(fmt.Sprintf("%d %d\n", len(words), dim))
for w, vec := range words {
sb.WriteString(w)
for _, v := range vec {
sb.WriteString(" ")
sb.WriteString(strconv.FormatFloat(v, 'f', 4, 64))
}
sb.WriteString("\n")
}
if err := os.WriteFile(path, []byte(sb.String()), 0o644); err != nil {
t.Fatalf("writeSynthModel: %v", err)
}
return path
}
// newSynthEmbedder 返回加载了合成模型的 StaticEmbedder。
func newSynthEmbedder(t testing.TB, dim int) *StaticEmbedder {
t.Helper()
e := NewStaticEmbedder(writeSynthModel(t, dim))
if !e.Loaded() {
t.Fatal("synthetic embedder should be loaded")
}
return e
}
// Phase 5: #topN 规格裁剪加载——只加载前 N 个词向量,控制常驻内存
func TestStaticEmbedderTopNSpec(t *testing.T) {
path := writeSynthModel(t, 300)
// 解析规格
cleanPath, topN := parseModelSpec(path + "#top5")
if cleanPath != path || topN != 5 {
t.Fatalf("parseModelSpec(#top5) = (%q, %d), want (%q, 5)", cleanPath, topN, path)
}
cleanPath2, topN2 := parseModelSpec(path)
if cleanPath2 != path || topN2 != 0 {
t.Fatalf("parseModelSpec(plain) = (%q, %d), want (%q, 0)", cleanPath2, topN2, path)
}
cleanPath3, topN3 := parseModelSpec(path + "#abc")
if cleanPath3 != path || topN3 != 0 {
t.Fatalf("parseModelSpec(#abc) = (%q, %d), want (%q, 0)", cleanPath3, topN3, path)
}
// 裁剪加载
e := NewStaticEmbedder(path + "#top5")
if !e.Loaded() {
t.Fatal("topN embedder should be loaded")
}
if len(e.words) != 5 {
t.Errorf("expected 5 words loaded with #top5, got %d", len(e.words))
}
}
// Phase 5: 裁剪后向量化仍可用(未命中词走 unkVec 兜底)
func TestStaticEmbedderTopNVectorize(t *testing.T) {
path := writeSynthModel(t, 300)
e := NewStaticEmbedder(path + "#top1")
if !e.Loaded() {
t.Fatal("embedder should be loaded")
}
v := e.Vectorize("天气怎么样")
// 未命中词不应产生空向量unkVec 兜底)
if len(v) == 0 {
t.Error("vectorize with topN=1 should still produce a vector (unkVec fallback)")
}
}