refactor: 全面改名为 HologramAgent + 提示词技术内容下沉到 skill/工具描述 + 修 21 处乱码

## ① 全面改名 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:客户端与内核版本一致
This commit is contained in:
JianFeeeee
2026-10-22 20:49:44 +08:00
parent 6bea130639
commit f256098c9f
204 changed files with 310 additions and 252 deletions

View File

@ -286,7 +286,7 @@ func (a *Agent) toolParallelSafe(name string) bool {
//
// 走过的弯路(都留在注释里,因为每一种都"看起来能工作"):
// 1. 内核里一张 map[string]bool 硬编码名单:声明从工具搬回内核,
// 工具改名/新增不会跟着变,要靠 grep 源码的判据才<EFBFBD><EFBFBD><EFBFBD>发现漂移;
// 工具改名/新增不会跟着变,要靠 grep 源码的判据才能发现漂移;
// 2. 往 required 变参里塞字符串 "toolParallel":拼错静默失效,
// 编译器不报错,而"少一个工具能并发"正是最难察觉的那类问题;
// 3. 每次查询重跑 buildToolDefs():正确但 O(工具数) 重复劳动。

View File

@ -17,7 +17,7 @@ import (
//
// 注入必须**晚绑定**且**per-agent**:内置工具的可见性由运行期状态门控
// (`if a.memory != nil` / `if a.knowledge != nil` …),而驻留子是轻量内核、
// memory 为 nil。<EFBFBD><EFBFBD> ToolAPI 是全局单例,拿不到 agent,只能由 agent 自己
// memory 为 nil。而 ToolAPI 是全局单例,拿不到 agent,只能由 agent 自己
// 提供一份 provider。
// builtinProvider 是 *Agent 上的适配器:把 agent 的内置工具面

View File

@ -21,7 +21,7 @@ type fakeOverflowProvider struct {
func (p *fakeOverflowProvider) MaxContextTokens() int { return p.window }
// newOverflowAgent 造一个够用<EFBFBD><EFBFBD><EFBFBD>超页测试 Agent。
// newOverflowAgent 造一个够用的超页测试 Agent。
func newOverflowAgent(window, maxContextSize int, events int, chunkChars int) *Agent {
a := &Agent{
io: agentIO.NewIOManager(),
@ -237,7 +237,7 @@ func TestOverflow_T5_恢复预算耗尽则终止(t *testing.T) {
t.Fatalf("首次应正常裁剪:handled=%v pruned=%d", handled, pruned)
}
if f.OverflowRecover != 1 {
t.Fatalf("首次应记一次恢复,<EFBFBD><EFBFBD>际 %d", f.OverflowRecover)
t.Fatalf("首次应记一次恢复,实际 %d", f.OverflowRecover)
}
// 制造「裁不动」:每次检查前把上下文压到只剩 1 条大事件
@ -524,7 +524,7 @@ func TestOverflow_protected按预算收紧(t *testing.T) {
// 大窗口 ⇒ 用满配置值(这正是「1M 下这套调度器能更好」的机制)
big := newOverflowAgent(1_000_000, 30, 12, 200)
if got := big.effectiveProtectedCount(); got != big.context.ProtectedCount() {
t.Fatalf("大窗口下应<EFBFBD><EFBFBD>满配置值 %d,实际 %d", big.context.ProtectedCount(), got)
t.Fatalf("大窗口下应达满配置值 %d,实际 %d", big.context.ProtectedCount(), got)
}
}

View File

@ -168,7 +168,7 @@ func (a *Agent) executeGetPluginTools(pluginName string) string {
}
// ★★ 收集**内置工具**(2026-05-05)。
//
// 原实<EFBFBD><EFBFBD><EFBFBD>只收 StageHost(SDK 插件) 与 IOManager(设备) 两处,
// 原实现只收 StageHost(SDK 插件) 与 IOManager(设备) 两处,
// 而 describe_image / ocr_image / memory_recall / knowledge_* 等
// 都是内核 buildToolDefs 里直接 append 的 ⇒ **一条都数不到**。
// 症状:agent 能调这些工具,却调 get_plugin_tools("") 看不到它们,

View File

@ -190,3 +190,53 @@ func TestPrefillMarkdownCarriesContentAndGuide(t *testing.T) {
t.Errorf("预填充文案没有说明来源层:\n%s", md)
}
}
// ⑥ 技术性内容不得留在系统提示词里(收敛到 skill / 工具描述)。
//
// 背景(用户 2026-10-22):agent 说话人机味重。查明主因不是性格设定,
// 而是系统提示词里堆了**模型无法据以行动的实现细节**:
//
// 【工具执行顺序】1272 字(ParallelSafe 声明、批内降级、内核判据…)
// 【三层记忆】 1069 字(L1/L2/L3 编号、消费式删除、向量空间…)
//
// 合计占提示词的绝大部分,而且全是"说明书腔"。模型学不到怎么用它,
// 却会照着这种语气说话。收敛后两段合计不到 500 字。
//
// 本判据钉住:这些术语不得回到系统提示词。
func TestSystemPromptHasNoImplementationJargon(t *testing.T) {
a := newToolTestAgent(t)
prompt := a.buildSystemPrompt("", "测试输入")
jargon := []string{
"ParallelSafe", // 工具声明项,模型不判断也改不了
"batchRunnable", // 内核内部函数名
"消费式", // L2 取出语义的实现说法
"DenseCosine", // 算法名
"fingerprint", // 向量空间标识
"向量空间", // 实现概念
"批内降级", // 内核调度细节
"自激", // 设计隐患,模型无需知道
}
for _, j := range jargon {
if strings.Contains(prompt, j) {
t.Errorf("系统提示词含实现术语 %q —— 技术内容应下沉到 skill / 工具描述,\n"+
"留在这里只会让回复带上说明书腔,且模型无法据此行动", j)
}
}
}
// ⑦ 提示词长度上限(防止技术说明再次淤积)。
//
// 当前实测 1723 字(收敛前 2309)。上限取 2000:留有增删余地,
// 但堵住"又加一大段实现说明"这种回潮。
func TestSystemPromptStaysCompact(t *testing.T) {
a := newToolTestAgent(t)
prompt := a.buildSystemPrompt("", "测试输入")
const maxRunes = 2000
if n := len([]rune(prompt)); n > maxRunes {
t.Errorf("系统提示词 %d 字,超过上限 %d —— \n"+
"新加的内容先问:模型会据此做什么不同的事?答不上来就该进 skill 或工具描述",
n, maxRunes)
}
}

View File

@ -143,7 +143,7 @@ func TestResident_LifecycleAndNoOrphans(t *testing.T) {
t.Fatalf("销毁后 inputch 应回到未分配:%+v", ch)
}
// 父的入站 inputch(child/<id>)必须在销毁时一并注销,否则登记表残留脏数据——
// HomeAgent 实测:destroy 后 child/<id> 仍挂在根 agent 名下,且无工具可单独注销。
// HologramAgent 实测:destroy 后 child/<id> 仍挂在根 agent 名下,且无工具可单独注销。
if inbound, ok := reg.Lookup("child/child-1"); ok {
t.Fatalf("销毁后父的入站 inputch 应被注销,实际残留:%+v", inbound)
}

View File

@ -112,7 +112,7 @@ const defaultChildMaxTurns = 5
// 不把父的【输出规则】/【中断消息】/【记忆清理】/【可用技能】/人格那几节一并搬过来:
// 子被显式禁用了 output_send__*(结果回父而非直接回用户),也无需保留记忆或响应中断;
// 搬来只会白占提示词预算。
const childSystemPromptTemplate = `你是 HomeAgent 的子任务助手。
const childSystemPromptTemplate = `你是 HologramAgent 的子任务助手。
请完成以下任务。完成即可,无需保留记忆或查询历史。
【执行纪律】这是硬性要求,不是建议:

View File

@ -1270,7 +1270,7 @@ const (
//
// 我先写的版本是查库(DominantBlockFingerprint)—— **错的**:
// 库里只有旧空间的块时,多数取值就是旧指纹,
// 拿它当过滤条件等于「不<EFBFBD><EFBFBD>滤」,旧块原样通过。
// 拿它当过滤条件等于「不过滤」,旧块原样通过。
// (判据当场抓住:测试库里唯一的块是 old-fp,而当前空间是 fp1。)
//
// 隔离的语义是「只信任当前空间写入的块」,

View File

@ -157,7 +157,7 @@ func appendPromptBlock(prompt, block string) string {
func (a *Agent) buildSystemPrompt(memContext string, userInput string) string {
prompt := expandPromptVars(a.systemPrompt)
if prompt == "" {
prompt = "你是小宅,HomeAgent 的看板娘,一个家政型 AI 管家助手。绝不用 Unicode emoji,只用颜文字表达情感,句尾带语气词。WebUI 概览页展示你的立绘。"
prompt = "你是小宅,HologramAgent 的看板娘,一个家政型 AI 管家助手。绝不用 Unicode emoji,只用颜文字表达情感,句尾带语气词。WebUI 概览页展示你的立绘。"
}
// 驻留子:在**固定提示词之上**注入任务提示词(设计 §7「创建」)。
@ -185,17 +185,20 @@ func (a *Agent) buildSystemPrompt(memContext string, userInput string) string {
// 为什么必须有这一段:此前系统提示词从未解释 L1/L2/L3 是什么,
// agent 只能从工具名猜。生产日志实测:agent 几乎从不调用 doc_query,
// 即使调用了也只看返回值、不知道原文已被回填到对话时序。
prompt = appendPromptBlock(prompt, "【三层记忆】记忆分三层,你要知道自己现在在看哪一层:\n"+
"- **对话时序(L1,你现在看得到)**:最近的事件,按时间排列在上面那段【对话时序】里。"+
"窗口放不下的会更早被移出,但不会丢。\n"+
"- **历史记忆(L2,不直接给你看)**:被移出窗口的事件,连同**完整原文**(含图片/音频)保存在这里。"+
"每次你收到新输入,内核会按语义相似度自动挑几篇相关的放在【历史记忆】块里,"+
"但那只是片段 —— 要看某次的完整原文与过程,调用 `doc_query`:"+
"它会把命中的事件**按原始时间戳插回你的对话时序**(不是返回一段摘要,那些事件真的回到了时间线上)。\n"+
"- **图记忆(L3,长期)**:实体与关系(谁是什么、某值现在是多少、谁和谁什么关系),"+
"用 `memory_recall` 查、`memory_commit` 记。\n"+
"选择口径:要**原文与过程** → `doc_query`;要**关系与当前值** → `memory_recall`;"+
"要**成文的可复用知识** → `knowledge_search`。")
// 【三层记忆】只留**模型据此选择工具**的语义。
//
// 技术性内容(L1/L2/L3 编号、向量空间、消费式删除、缩略语)全部下沉到
// 对应工具的 description 与 skill:模型只需要知道"哪里有原文、哪里有
// 关系、用什么去取",不需要知道内核怎么存。
//
// ★ 但有一件事必须说:doc_query 会把历史事件**插回时序**,而不是返回
// 一段摘要 —— 这与模型对"检索工具"的默认预期相反,不说就会误用。
prompt = appendPromptBlock(prompt, "【三层记忆】你这儿有三种记忆,按需要的东西选:\n"+
"- **对话时序(L1)**:最近的对话就在上面的【对话时序】里,直接能看。\n"+
"- **历史记忆(L2)**:更早的事被移出窗口了,但原文还在。要看某段完整经过用 `doc_query` —— "+
"它会把那些事**按原始时间戳插回你的对话时序**,你随后就能像看当前对话一样读它们。\n"+
"- **图记忆(L3)**:事实与关系(谁是什么、某值现在多少)用 `memory_recall`;"+
"成文的可复用知识用 `knowledge_search`。")
// 记忆维护:具体选哪个工具、各有什么副作用,都写在 memory_* 的工具描述里
//(【记忆清理】前缀)。这里只保留那条**不看描述也会犯**的错:只描述计划不执行。
@ -220,11 +223,16 @@ func (a *Agent) buildSystemPrompt(memContext string, userInput string) string {
//
// ⚠️ 措辞必须与 batchRunnable 的**真实**判据一致(全批 ParallelSafe
// 才并发 + 同通道保序),否则只是把"没说"换成"说错"。
prompt = appendPromptBlock(prompt, "【工具执行顺序】同一条回复里给出多个工具调用时,内核**默认并行执行**(同时跑),不是依次执行。这带来三条你必须知道的规则:\n")
prompt += "- **不要依赖执行顺序**:若某个调用的参数需要另一个调用的结果,就**分两轮**——先发前一个,看到结果后再发下一个。写在同一轮里就等于假设了不存在的先后。\n"
prompt += "- **例外一:同一通道的输出发送会保序**。对**同一个**通道连续调用多次 `output_send__`,内核会**按你发出的顺序依次执行**(用户可见消息顺序敏感),不会并发打乱。所以「先发回执、再发结论」这类有序发送可以放在同一轮;但若后续内容依赖前一条的用户反应,仍应分轮。\n"
prompt += "- **例外二:不并发安全的工具不会并发**。写类工具(记忆/知识/文档写入等)与未声明并发安全的工具,**整批会退回依次执行**——同一批里只要有一个这样的工具,这一批就全部串行。这对你是透明的:你不需要判断,只需知道「同轮并发」不是无条件保证的。\n"
prompt += "- 工具是否并发安全由**工具自己声明**(`ParallelSafe`),不由你决定;你只需按上面第 1 条判断能否同轮发出。\n"
// 【工具执行顺序】只留**模型必须据此作决定**的三条。
//
// 技术性内容(ParallelSafe 声明、批内降级、内核如何判据)全部下沉:
// 那是实现细节,模型既不需要也改不了 —— 讲它只会让回复带上说明书腔。
// 判断依据仍是真实的 batchRunnable 语义(勿写反)。
prompt = appendPromptBlock(prompt, "【工具执行顺序】同一条回复里的多个工具调用默认**并行**执行。\n"+
"- 不要依赖先后:后一个要用前一个的结果时,分两轮发。\n"+
"- **例外一**:对同一通道的多次输出会**保序**(按你发出的顺序送达),所以「先回执、再结论」可以放同一轮。\n"+
"- **例外二**:写类工具(记忆/知识/文档写入等)以及不声明**并发安全**的工具,这一批会退回依次执行 —— "+
"只是变慢,不影响结果,你不用为它调整写法。")
prompt = appendPromptBlock(prompt, "【输出规则】消息不会自动发送到对话来源通道,你必须自己决定如何回复:\n")
prompt += "- **不要假设当前通道是某个固定值**:同一会话里可能同时有多个来源(多设备、多通道、子任务)。\n"
@ -838,7 +846,7 @@ func (a *Agent) buildToolDefs() []interface{} {
// 而「读本地文件」是 IO —— 三个工具读文件却住在内核进程内,
// 无沙箱、无能力面、无审计,正是 capability.go 那套权限梯度要防的事。
//
// 留在这里的是**待<EFBFBD><EFBFBD>退场**的部分:下面这一大块 pendingMedia 只服务于
// 留在这里的是**待退场**的部分:下面这一大块 pendingMedia 只服务于
// 那三个工具的工具面。input 通路(processInput 直接把 media block
// 拼进本轮请求)**不依赖它**,所以可安全移除。
// 退场顺序见 docs 或本次提交的说明;先确保插件侧能力等价。

View File

@ -696,7 +696,7 @@ func (m *IOManager) GetInputChannelDef(name string) (ChannelDef, bool) {
}
// ToolDefOf 按工具名取其声明(含 Parameters schema)。
// 用途:内核在执行前按 schema 预校验——没有它就只<EFBFBD><EFBFBD><EFBFBD>校验到插件工具,
// 用途:内核在执行前按 schema 预校验——没有它就只能校验到插件工具,
// 而设备/通道工具(cmd_run、files_write 等)会完全绕过校验。
func (m *IOManager) ToolDefOf(name string) (ToolDef, bool) {
m.mu.RLock()

View File

@ -10,7 +10,7 @@ import (
// PersonaStaleHints 必须能认出会腐坏的人格内容——版本号字面量与已删除机制。
func TestPersonaStaleHints(t *testing.T) {
// 现场真实文本(线上人格卡的原文)
stale := "你是 HomeAgent(内核代号 HΔ-Kernel,当前版本 v0.9.0)。\n当前运行的二进制是 v0.9.0(C ABI v2,构建于 2026-08-15)。"
stale := "你是 HologramAgent(内核代号 HΔ-Kernel,当前版本 v0.9.0)。\n当前运行的二进制是 v0.9.0(C ABI v2,构建于 2026-08-15)。"
hints := PersonaStaleHints(stale)
if len(hints) == 0 {
t.Fatal("未识别出写死版本号与 C ABI 的人格文本")
@ -32,7 +32,7 @@ func TestPersonaStaleHints(t *testing.T) {
// DefaultPersonaProbeClean 由 config 包的默认模板等价物构成——
// 这里不复用 config 包以避免 import cycle,只断言「不含版本号与旧机制」的文本不被误报。
func DefaultPersonaProbeClean() string {
return "你是 HomeAgent(内核代号 HΔ-Kernel)。外部插件是独立子进程,经 stdio JSON-RPC 通信;" +
return "你是 HologramAgent(内核代号 HΔ-Kernel)。外部插件是独立子进程,经 stdio JSON-RPC 通信;" +
"被问到版本时以运行时快照为准。"
}