Files
HomeAgent/internal/agent/core/promptdump.go
root b9097986c8 fix(prompt): 拼装时空行翻倍 + 人格支持版本占位符 + 配置项说明对齐分层原则
## 1. 提示词里的空行翻倍(用户实测发现)

抓包看实际 payload,system 里出现 3 处连续 3~4 个换行:

    …别包装成"稍等我看一下"。\n\n\n【人格设定】
    …/mascot.webp 直接能看。\n\n\n\n【场景记忆】
    …能否同轮发出。\n\n\n【输出规则】

根因:拼装统一用 `prompt += "\n\n" + block`,而块自己常以换行结尾
(人格块是 "【人格设定】\n%s\n",自定义 system_prompt 也常带尾部换行)。
空行不致命但推高 token,也让「提示词到底长什么样」难以肉眼核对。

修法:新增 appendPromptBlock(trim 块两端 + 只拼一个 "\n\n"),所有拼装点改用它。
重新抓包:连续 3+ 换行 0 处。

## 2. 人格块支持版本占位符

buildSystemPrompt 只对 systemPrompt 调 expandPromptVars,人格块没展开 ——
人格里写死版本号仍会随发版说谎(实测线上人格卡写 v1.0.3)。现在人格同样展开
{{kernel_version}}/{{kernel_commit}}/{{sdk_version}}。

## 3. 人格/系统提示词两个配置项的说明重写

- core.agent.personal_prompt:DisplayName 改为「人格设定(称呼/语气/说话方式/形象)」,
  说明只写人设;工具用法写工具描述、多步流程写技能;支持占位符;文件优先。
- core.agent.system_prompt:DisplayName 改为「系统提示词(身份与行为规则)」,
  说明只写跨工具/跨通道成立的内容;运行时通道投递/并行/时序语义由内核注入无需重复;
  留在提示词里的工具用法会随插件外置而失真。

## 4. 【记忆清理指令】→【记忆维护】

具体选哪个 memory_* 工具、各自副作用早已写在工具描述里(【记忆清理】前缀),
提示词里那份逐条列举属于重复。只保留「必须真做、不能只描述计划」这条
不看工具描述也会犯的错,并要求用一句人话说结果。

## 5. 新增 opt-in 提示词抓包(排查用)

HOMED_PROMPT_DUMP=<path> 时把每轮实际发给 LLM 的 messages 落 jsonl(0600)。
环境变量而非常规配置项:内容是用户隐私,只该在诊断时临时开。
本次排查即由它拿到证据;诊断后已移除临时 drop-in。

验证:go test(agent core / config / sdk / healthcheck)全绿;
实例重启后抓包复核空行归零;对话口吻仍为「短、口语、不列清单」。
2026-10-20 09:16:07 +08:00

69 lines
1.9 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 core
import (
"encoding/json"
"os"
"sync"
"time"
agentAPI "github.com/JianFeeeee/HomeAgent/internal/agent/api"
)
// 提示词抓包(opt-in 诊断)。
//
// 为什么需要:系统提示词是**每轮动态拼装**的(身份 + 人格 + 记忆 + 时序 +
// 工具顺序 + 输出规则 + 技能索引 + 首启门禁…),只看代码永远说不清模型
// 这一轮到底收到了什么。排查「它怎么又列清单了」「为什么它说有某能力」
// 这类问题时,唯一可靠的事实是**实际发出去的 payload**。
//
// 开关是环境变量而不是配置项:它会把记忆与用户输入原样落盘,属于
// 临时诊断手段,不该在界面上有一个能被随手打开的开关。
//
// HOMED_PROMPT_DUMP=/var/log/homeagent/prompt.jsonl
//
// 每行一条 JSON:时间、通道、模型、完整 messages(role + content)。
// 文件权限 0600 —— 内容是用户隐私。
const promptDumpEnv = "HOMED_PROMPT_DUMP"
var promptDumpMu sync.Mutex
func dumpPrompt(channel string, req *agentAPI.CompletionRequest) {
path := os.Getenv(promptDumpEnv)
if path == "" || req == nil {
return
}
entry := map[string]interface{}{
"at": time.Now().Format(time.RFC3339Nano),
"channel": channel,
"model": req.Model,
}
msgs := make([]map[string]interface{}, 0, len(req.Messages))
for _, m := range req.Messages {
item := map[string]interface{}{"role": m.Role, "content": m.Content}
if len(m.Blocks) > 0 {
item["blocks"] = len(m.Blocks)
}
if len(m.ToolCalls) > 0 {
item["tool_calls"] = len(m.ToolCalls)
}
msgs = append(msgs, item)
}
entry["messages"] = msgs
entry["tool_count"] = len(req.Tools)
line, err := json.Marshal(entry)
if err != nil {
return
}
line = append(line, '\n')
promptDumpMu.Lock()
defer promptDumpMu.Unlock()
f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600)
if err != nil {
return
}
defer f.Close()
f.Write(line)
}