feat(persona): 人格设定配置项化 + 默认模板契约测试 + 腐坏告警

起因(v1.2.0 压测):线上实例内核日志/接口都报 1.2.0,agent 被问版本时却按人格卡
自述 v0.9.0 + C ABI v2(该机制 v1.0.0 已删除)。根因是人格只有「文件」一个来源且无人
维护——写死的版本号必然随发版腐坏。

改动:
1. 新增配置项 core.agent.personal_prompt(多行文本),默认值为内置模板
   config.DefaultPersonaPrompt,随其它默认值同批播种(老安装不注入,语义不变)
2. 默认模板**不含任何版本号字面量**,并显式要求「被问到版本/构建信息时以运行时快照
   (healthcheck_kernel)为准」——从根上消掉这类腐坏
3. 人格来源优先级:personal/personal.md(存在且非空)> 配置项 > 无
   启动日志明确打印来源;文件含腐坏内容(版本号字面量 / 已删除机制的说法)时告警并
   建议迁移到配置项
4. internal/agent.PersonaStaleHints:腐坏检测(版本号正则 + 已删除机制词表)

契约测试(防复发):
- TestDefaultPersonaPromptHasNoVersionLiterals:默认模板不得含 v?\d+\.\d+\.\d+,
  且必须含「运行时快照」要求
- TestPersonaPromptRegisteredWithDefault:注册存在、默认值一致、播种真的写入
- TestPersonaStaleHints:线上人格卡原文必须被识别(v0.9.0 / C ABI v2),干净文本不误报

验证:go build ./cmd/homed ok;go vet 三个包 ok;go test ./internal/config ./internal/agent ok;
端到端两场景(无文件→来源=配置项 1307 字节;有旧文件→来源=文件 + 告警列出 v0.9.0 与 C ABI v2)。
This commit is contained in:
JianFeeeee
2026-09-12 12:38:52 +08:00
parent b15d0bd114
commit 7318a10828
5 changed files with 192 additions and 1 deletions

View File

@ -4,6 +4,8 @@ import (
"fmt"
"os"
"path/filepath"
"regexp"
"strings"
)
type Personality struct {
@ -39,3 +41,25 @@ func (p *Personality) InjectPrompt() string {
}
return fmt.Sprintf("【人格设定】\n%s\n", p.Content)
}
// 会随时间腐坏的人格内容特征。现场:人格卡写死 v0.9.0 与早已删除的 C ABI v2
// 实例被问版本时自述错误v1.2.0 压测发现)。
var (
personaVersionRe = regexp.MustCompile(`\bv?\d+\.\d+\.\d+\b`)
personaStalePhrases = []string{"C ABI v2", "plugin.so", "c-shared", "描述式索引", "引用计数式"}
)
// PersonaStaleHints 返回人格文本里会腐坏的内容(空 = 干净)。
// 供启动时告警:引导改用配置项 core.agent.personal_prompt默认模板不含这些
func PersonaStaleHints(content string) []string {
var out []string
if m := personaVersionRe.FindAllString(content, -1); len(m) > 0 {
out = append(out, fmt.Sprintf("版本号字面量 %v版本应来自运行时快照", m))
}
for _, p := range personaStalePhrases {
if strings.Contains(content, p) {
out = append(out, "可能已过期的说法: "+p)
}
}
return out
}

View File

@ -0,0 +1,62 @@
package agent
import (
"os"
"path/filepath"
"strings"
"testing"
)
// PersonaStaleHints 必须能认出会腐坏的人格内容——版本号字面量与已删除机制。
func TestPersonaStaleHints(t *testing.T) {
// 现场真实文本(线上人格卡的原文)
stale := "你是 HomeAgent内核代号 HΔ-Kernel当前版本 v0.9.0)。\n当前运行的二进制是 v0.9.0C ABI v2构建于 2026-08-15。"
hints := PersonaStaleHints(stale)
if len(hints) == 0 {
t.Fatal("未识别出写死版本号与 C ABI 的人格文本")
}
joined := strings.Join(hints, " | ")
if !strings.Contains(joined, "版本号字面量") {
t.Fatalf("应报出版本号字面量,实际: %s", joined)
}
if !strings.Contains(joined, "C ABI v2") {
t.Fatalf("应报出已删除机制的残留说法,实际: %s", joined)
}
// 干净文本(配置项默认模板)不应误报
if h := PersonaStaleHints(DefaultPersonaProbeClean()); len(h) != 0 {
t.Fatalf("干净人格被误报: %v", h)
}
}
// DefaultPersonaProbeClean 由 config 包的默认模板等价物构成——
// 这里不复用 config 包以避免 import cycle只断言「不含版本号与旧机制」的文本不被误报。
func DefaultPersonaProbeClean() string {
return "你是 HomeAgent内核代号 HΔ-Kernel。外部插件是独立子进程经 stdio JSON-RPC 通信;" +
"被问到版本时以运行时快照为准。"
}
func TestLoadAndSavePersonality(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "personal", "personal.md")
// 文件不存在时返回空(不报错、不创建)
p, err := LoadPersonality(path)
if err != nil || p == nil || p.Content != "" {
t.Fatalf("缺文件时应返回空人格,实际 %+v err=%v", p, err)
}
if err := SavePersonality(path, "人格内容"); err != nil {
t.Fatal(err)
}
if _, err := os.Stat(path); err != nil {
t.Fatalf("SavePersonality 未落盘: %v", err)
}
p, err = LoadPersonality(path)
if err != nil || p.Content != "人格内容" {
t.Fatalf("读回失败: %+v err=%v", p, err)
}
if got := p.InjectPrompt(); !strings.Contains(got, "【人格设定】") || !strings.Contains(got, "人格内容") {
t.Fatalf("InjectPrompt 形状不对: %q", got)
}
}