mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-11-02 07:17:34 +00:00
## ① 全面改名 HomeAgent → HologramAgent
仓库已改名,但代码与文档里仍有大量旧名。本次按"用户能看到的优先"全量替换:
- 用户可见:登录页标题/页脚、默认人格第一句、CLI 状态输出、启动日志、
plugin_install 的工具描述、AI 图片插件的 User-Agent
- 文档与脚本:README、assets/docs、docs/zh、deploy/*.sh、.github/workflows
- 鸿蒙工程:cmd/ohos/HomeAgent → cmd/ohos/HologramAgent(git mv)
目录内的 module/app 描述同步;**证书路径原样保留**
(default_HomeAgent_xxx.cer/p7b/p12 是签名材料的实际文件名,改了会找不到证书)
- 插件元数据:plugin.json 的 author
★ 必须保持原样的两处(改了会坏):
· legacySeededPromptSignature = "你是 HomeAgent 的看板娘「小宅」…"
—— 那是识别历史播种人格卡的**原文锚点**,改了就识别不出旧数据
· 公开 SDK 的 ProxyAuthHomeAgent = "homeagent"
—— 那是 SDK 契约里的值,属独立仓库,本仓不改
## ② 修 21 处乱码(U+FFFD)
这些是历史编辑留下的编码破坏,全在注释里,例如:
「赌注重下���遍」→「赌注重下一遍」
「「不��滤」」 →「「不过滤」」
「**待��退场**」→「**待退场**」
★ 保留 providers/chineseclip/tokenizer.go 里的 2 处:那是**故意**用
U+FFFD 演示"非法 UTF-8 字节被归一成 U+FFFD",不是乱码。
## ③ 提示词技术内容下沉(用户:技术性内容应该收敛到 skill 与 -help 中)
实测系统提示词 2309 字,其中两段几乎全是**模型无法据以行动的实现细节**:
【工具执行顺序】1272 字(ParallelSafe 声明、批内降级、batchRunnable 判据…)
【三层记忆】 1069 字(L1/L2/L3 编号、消费式删除、向量空间、指纹…)
收敛后 **2309 → 1723 字(-25%)**,保留的全是"模型据此做什么不同的事":
工具顺序:默认并行 / 不要依赖先后 / 同通道保序 / 写类工具可能串行
三层记忆:时序里能看最近 / doc_query 把历史事件按原始时间戳插回时序 /
关系用 memory_recall / 成文知识用 knowledge_search
判断口径写进判据:新加一段提示词前先问"模型会据此做什么不同的事",
答不上来就该进 skill 或工具描述。
## 判据
prompt_memory_test.go 新增 2 条:
- TestSystemPromptHasNoImplementationJargon
黑名单:ParallelSafe / batchRunnable / 消费式 / DenseCosine / fingerprint /
向量空间 / 批内降级 / 自激
- TestSystemPromptStaysCompact:上限 2000 字(当前 1723)
变异:把 "ParallelSafe 声明、批内降级" 加回工具顺序段 ⇒ 第一条立刻红。
## 验证
- go build ./... / go vet ./... 干净
- go test ./... -count=1:58 包 0 失败
- make check-client-versions:客户端与内核版本一致
223 lines
8.5 KiB
Go
223 lines
8.5 KiB
Go
package distill
|
||
|
||
import (
|
||
"fmt"
|
||
"regexp"
|
||
"sort"
|
||
"strings"
|
||
)
|
||
|
||
// 字段名漂移的自动发现。
|
||
//
|
||
// ★ 为什么不能靠补词表
|
||
// --------------------
|
||
// 实测(真库 188 条里 40 条,真实 qwen3:1.7b)漂移有 8 个未归一名字:
|
||
//
|
||
// 观察比例 观察时长 峰值 峰值范围 批次 批次号 批次范围
|
||
//
|
||
// 而且 drift 不总是「模型叫错了」——**同一份语义,模型在不同记录里给出
|
||
// 不同名字**:
|
||
//
|
||
// 第112批 灰度10%观察48分后放50% → 字段名 "发布窗口"
|
||
// 第113批 灰度5%观察86分后放50% → 字段名 "发布窗口"
|
||
// 第114批 灰度10%观察70分后放50% → 字段名 "观察时长"
|
||
//
|
||
// 三条同构记录、同一语义,两个名字。补词表能补到这一次,补不到下一次。
|
||
// 而且补的时候人在猜 —— 「观察时长」该并到「发布窗口」还是「停机时长」?
|
||
// 猜错就把两个真维度合并了。
|
||
//
|
||
// ★ 判据不靠字段名,靠**值的形态**
|
||
// -------------------------------
|
||
// 「字段名是不是同一个维度」正是要判定的问题,拿字段名去判是循环论证。
|
||
// 这里用的是两条独立信号:
|
||
//
|
||
// 信号1 同 subject —— 同一个实体上出现的属性
|
||
// 信号2 同值形态 —— 值走同一个模板(百分比/时长/版本/日期/区间…)
|
||
//
|
||
// 两条同时成立**只是候选**,不是结论:仍可能同形态却是不同维度
|
||
// (「停机4分」和「观察48分」都是时长,但确实是两个维度)。
|
||
// 所以 Discover 返回的是**候选报告**,由人确认后才写进 dimensionAliases。
|
||
// 自动合并会把真维度错并,代价(信息丢失)远大于收益(少写几行词表)。
|
||
|
||
// valueShape 是值的形态模板。
|
||
//
|
||
// 判据的核心:把具体值抽象成「它长什么样」。百分比和百分比同形,
|
||
// 时长和时长同形,版本和版本同形 —— 跨形态几乎必然是不同维度。
|
||
type valueShape string
|
||
|
||
const (
|
||
shapePercent valueShape = "百分比" // 10% 5%
|
||
shapeDuration valueShape = "时长" // 4分 48分 30分钟
|
||
shapeVersion valueShape = "版本" // v2.28.2 第4版
|
||
shapeRange valueShape = "区间" // 4%~17% 30~84批 56~63批
|
||
shapeNumber valueShape = "数字" // 8861 4324
|
||
shapeDate valueShape = "日期" // 10月 周四
|
||
shapeTime valueShape = "时刻" // 凌晨2点
|
||
shapeRatio valueShape = "比例词" // 均仅 / 全部
|
||
shapeText valueShape = "文本" // 兜底
|
||
)
|
||
|
||
var (
|
||
// reISODate 先匹配 ISO 日期:**年-月**(2026-10)与**年月日**(2026-10-02)。
|
||
//
|
||
// ★ 必须在 reRange 之前判,且必须覆盖年-月:
|
||
// 1. 区间正则收了连字符(13-17%),ISO 日期会被它吃掉。
|
||
// 2. 只匹配年月日会漏掉 "2026-10" —— 它同样被 reRange 命中
|
||
// (实测 reRange("2026-10") = true)。而年-月在迁移数据里很常见:
|
||
// memory_blocks.created_at 就是 "2026-10" 这种形态。
|
||
reISODate = regexp.MustCompile(`^\d{4}-\d{1,2}(-\d{1,2})?$`)
|
||
|
||
// reRange 匹配「A~B」与「A-B」两种区间形态。
|
||
//
|
||
// ★ 三条实测约束,都是踩出来的:
|
||
// 1. 两端要容许单位后缀:初版写成 \d+\s*[~~-]\s*\d+ 时
|
||
// "4%~17%" 不匹配 —— 而它才是真库里最常见的区间形态(错误率峰值)。
|
||
// 只匹配纯数字对会把最该发现的那类漂移(峰值 vs 峰值范围)漏掉。
|
||
// 2. **连字符必须收进来**:修 1 时把字符类写成 [~~](漏了 -),
|
||
// "13-17%" 就退回文本。真库里两种区间写法都有(13~17% 与 13-17%)。
|
||
// 3. 结尾锚定(^…$):不锚定会让 "10月-12月" 之类被判成区间,
|
||
// 而它属于日期。前缀也锚定,否则 "v2-3" 会被当成区间。
|
||
// 4. 结尾要容许**中文单位**尾巴:初版只放行 [%%],结果真库里高频的
|
||
// "30~84批"、"56~63批" 全部退回文本 —— 而那正是批次区间,
|
||
// 正是要靠它发现漂移的形态。
|
||
reRange = regexp.MustCompile(`^\d+(?:\.\d+)?\s*[%%]?\s*[~~-]\s*\d+(?:\.\d+)?\s*[%%]?[\p{Han}A-Za-z]*$`)
|
||
rePercent = regexp.MustCompile(`^\d+(\.\d+)?\s*[%%]$`)
|
||
reDuration = regexp.MustCompile(`^\d+(\.\d+)?\s*(分|分钟|秒|小时|min|s)$`)
|
||
// reVersion 容许点号与连字符两种分隔:真库里 "v2.28.2" 和 "v2-3" 都见过,
|
||
// 且 "v2-3" 含连字符 —— 不认的话它会掉进 reRange(区间)或文本。
|
||
reVersion = regexp.MustCompile(`^[vV]\d+(?:[.\-]\d+)*$|^第\d+版$`)
|
||
reNumber = regexp.MustCompile(`^\d+$`)
|
||
reDate = regexp.MustCompile(`^\d{1,2}月(\d{1,2}日)?$|^周[一二三四五六日]$`)
|
||
reTime = regexp.MustCompile(`(凌晨|上午|下午|晚上|中午)?\d{1,2}[点::]\d{0,2}`)
|
||
)
|
||
|
||
// classifyValue 把一个值归到形态模板。
|
||
func classifyValue(v string) valueShape {
|
||
v = strings.TrimSpace(v)
|
||
switch {
|
||
case v == "":
|
||
return shapeText
|
||
case reISODate.MatchString(v):
|
||
// 必须在区间之前:区间正则含连字符,ISO 日期会被它吃掉。
|
||
return shapeDate
|
||
case reRange.MatchString(v):
|
||
return shapeRange
|
||
case rePercent.MatchString(v):
|
||
return shapePercent
|
||
case reDuration.MatchString(v):
|
||
return shapeDuration
|
||
case reVersion.MatchString(v):
|
||
return shapeVersion
|
||
case reNumber.MatchString(v):
|
||
return shapeNumber
|
||
case reTime.MatchString(v):
|
||
return shapeTime
|
||
case reDate.MatchString(v):
|
||
return shapeDate
|
||
default:
|
||
return shapeText
|
||
}
|
||
}
|
||
|
||
// dimObservation 是一次观测:某 subject 上某字段名出现过某形态的值。
|
||
type dimObservation struct {
|
||
Subject string
|
||
Dim string
|
||
Shape valueShape
|
||
Samples []string
|
||
}
|
||
|
||
// DriftCandidate 是一组疑似同义字段名。
|
||
type DriftCandidate struct {
|
||
// Shape 是这组字段名共有的值形态(判别的依据)。
|
||
Shape valueShape
|
||
// Dims 是疑似同义的字段名(按出现次数降序)。
|
||
Dims []string
|
||
// Subjects 是共同出现过这些字段名的 subject(判为「同一实体」)。
|
||
Subjects []string
|
||
// Evidence 是支持这个候选的观测样例,供人工确认。
|
||
Evidence []string
|
||
}
|
||
|
||
// String 便于报告与日志。
|
||
func (c DriftCandidate) String() string {
|
||
return fmt.Sprintf("[%s] %s(%d 个 subject,%d 条证据)",
|
||
c.Shape, strings.Join(c.Dims, " / "), len(c.Subjects), len(c.Evidence))
|
||
}
|
||
|
||
// DiscoverDimensionDrift 从观测里找出疑似同义的字段名。
|
||
//
|
||
// 返回**候选**而不是归并结果 —— 理由见文件头:同形态不同维度很常见
|
||
// (停机4分 vs 观察48分),自动合并会丢信息。确认后由人写进 dimensionAliases。
|
||
//
|
||
// 判据(三条同时成立才列为候选):
|
||
// 1. 值形态相同(classifyValue 相同)
|
||
// 2. 共同至少一个 subject(同实体上的属性)
|
||
// 3. 字段名不同(否则不是漂移)
|
||
func containsDim(ss []string, s string) bool {
|
||
for _, x := range ss {
|
||
if x == s {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
func DiscoverDimensionDrift(observations []dimObservation) []DriftCandidate {
|
||
// (shape) → (subject) → dim → 样例
|
||
type shapeKey struct {
|
||
shape valueShape
|
||
subject string
|
||
}
|
||
grouped := map[shapeKey]map[string][]string{}
|
||
|
||
for _, o := range observations {
|
||
k := shapeKey{shape: o.Shape, subject: o.Subject}
|
||
if grouped[k] == nil {
|
||
grouped[k] = map[string][]string{}
|
||
}
|
||
grouped[k][o.Dim] = append(grouped[k][o.Dim], o.Samples...)
|
||
}
|
||
|
||
// shape → 候选(跨 subject 聚合)
|
||
byShape := map[valueShape]*DriftCandidate{}
|
||
for k, dims := range grouped {
|
||
if len(dims) < 2 {
|
||
continue // 只有一种叫法,不是漂移
|
||
}
|
||
cand, ok := byShape[k.shape]
|
||
if !ok {
|
||
cand = &DriftCandidate{Shape: k.shape}
|
||
byShape[k.shape] = cand
|
||
}
|
||
for dim, samples := range dims {
|
||
// ★ 去重:byShape 是跨 subject 累积的,同一 subject 里同一个
|
||
// 字段名出现多次会重复进 Dims(实测真库跑出
|
||
// 「发布窗口 / 发布窗口 / 发布窗口 …」五个重复项)。
|
||
if !containsDim(cand.Dims, dim) {
|
||
cand.Dims = append(cand.Dims, dim)
|
||
}
|
||
if len(samples) > 0 {
|
||
cand.Evidence = append(cand.Evidence,
|
||
fmt.Sprintf("%s·%s=%s", k.subject, dim, samples[0]))
|
||
}
|
||
}
|
||
if !containsDim(cand.Subjects, k.subject) {
|
||
cand.Subjects = append(cand.Subjects, k.subject)
|
||
}
|
||
}
|
||
|
||
var out []DriftCandidate
|
||
for _, cand := range byShape {
|
||
sort.Strings(cand.Dims)
|
||
sort.Strings(cand.Subjects)
|
||
sort.Strings(cand.Evidence)
|
||
out = append(out, *cand)
|
||
}
|
||
// 按证据数降序:证据多的先看
|
||
sort.Slice(out, func(i, j int) bool {
|
||
return len(out[i].Evidence) > len(out[j].Evidence)
|
||
})
|
||
return out
|
||
}
|