Files
HomeAgent/internal/agent/core/prompt_parallel_test.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

131 lines
5.0 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 (
"strings"
"testing"
)
// 阶段 2.5:提示词改为「默认并行」。
//
// ⚠️ 本阶段有一条**硬性顺序约束**:必须在并行执行(阶段 2)落地**之后**。
// 反序(先改提示词说"并发"、内核仍串行)会让提示词**对模型说谎** ——
// 模型据"并发执行"推断安全性,写出真正依赖顺序的调用。宁可晚改,不可错改。
//
// 判据的负向部分(串行阶段不得出现"默认并行"字样)已在阶段 2 完成后
// 才补写,故此处只断言**正向**内容:四要点齐全,且与 batchRunnable 的
// 真实判据**一致**——提示词若与实现不符,比不说更坏。
// ① 四要点齐全:默认并行 / 不可依赖顺序 / 同通道保序 / 写类工具不并发。
func TestPromptDeclaresParallelContract(t *testing.T) {
a := newPreemptAgent(t, newPreemptProvider())
p := a.buildSystemPrompt("", "hi")
required := []struct {
key string
words []string
}{
{"默认并行", []string{"并行"}},
{"不可依赖顺序", []string{"顺序"}},
{"同通道保序", []string{"保序"}},
{"并发安全声明", []string{"并发安全", "ParallelSafe", "声明"}},
}
for _, r := range required {
found := false
for _, w := range r.words {
if strings.Contains(p, w) {
found = true
break
}
}
if !found {
t.Errorf("提示词缺少要点「%s」(期望含 %v 之一)", r.key, r.words)
}
}
}
// ② ★ 提示词必须与实现**一致**,不能只说一半。
//
// batchRunnable 的真实规则是「全批都 ParallelSafe 才并发,且同通道
// 多次发送不并发」。若提示词只说"会并行"而不说例外,模型会在
// 「同通道连发」时误以为顺序无关 —— 而实现恰恰保证了保序(安全但
// 模型不知情);反过来若说"永远串行"则与实现矛盾。
//
// 本判据钉死:提示词里必须同时出现"例外/不并发"的限定语。
func TestPromptStatesExceptionsNotJustParallelism(t *testing.T) {
a := newPreemptAgent(t, newPreemptProvider())
p := a.buildSystemPrompt("", "hi")
for _, w := range []string{"例外", "不会并发", "不并发"} {
if strings.Contains(p, w) {
return
}
}
t.Error("提示词只讲并行、不讲例外 —— 与 batchRunnable 的实际规则不符,模型会误判")
}
// ③ output_send__ 同通道保序这一条必须**显式**告诉模型。
//
// 理由:保序是内核替模型兜住的行为,模型不知道就可能依赖"反正并发"
// 来发多条消息,从而写出让保序失去意义的东西(如把"重试"和"确认"
// 并发发出)。显式说明能让模型据此主动选择分轮。
func TestPromptExplainsSameChannelOrdering(t *testing.T) {
a := newPreemptAgent(t, newPreemptProvider())
p := a.buildSystemPrompt("", "hi")
if !strings.Contains(p, "output_send__") {
t.Fatal("提示词未提及 output_send__,无法说明同通道保序")
}
if !strings.Contains(p, "保序") {
t.Error("提示词未说明同通道多次发送会保序")
}
}
// ④ spawn_child 的旧表述必须改掉。
//
// 原文是「应并行 spawn 多个子 Agent,不要自己串行逐个执行」——它在并行化
// 之前是**落空**的(模型照做,内核仍串行)。改造后应改为机制性表述。
func TestPromptSpawnChildNoLongerOverpromises(t *testing.T) {
a := newPreemptAgent(t, newPreemptProvider())
desc := toolDefDescription(a, "spawn_child")
if desc == "" {
t.Fatal("buildToolDefs 里没有 spawn_child")
}
// 若保留「并行 spawn」的建议,必须同时说明它现在真的并发
// (否则又是一句落空的建议)。这里只要求不出现旧的绝对化措辞。
if strings.Contains(desc, "不要自己串行逐个执行") {
t.Error("spawn_child 描述仍含旧的「不要自己串行逐个执行」——该建议在并行化前是落空的")
}
}
// ⑤ 提示词改动不得破坏既有要点(回归防护)。
func TestPromptKeepsExistingContract(t *testing.T) {
a := newPreemptAgent(t, newPreemptProvider())
p := a.buildSystemPrompt("", "hi")
for _, want := range []string{
"【输出规则】",
"output_list_channels",
"【可用工具能力】",
// 记忆维护块:标题从【记忆清理指令】改为【记忆维护】——具体工具选择
// 下沉到 memory_* 的工具描述,提示词只保留「必须真做、不许只描述计划」。
"【记忆维护】",
} {
if !strings.Contains(p, want) {
t.Errorf("提示词丢失既有要点 %q", want)
}
}
}
// toolDefDescription 从 buildToolDefs 的产物里取某工具的 description。
// 直接查真实产物,而不是另建一套注册表——判据必须对着**代码真实输出**。
func toolDefDescription(a *Agent, name string) string {
for _, t := range a.buildToolDefs() {
fn, ok := t.(map[string]interface{})["function"].(map[string]interface{})
if !ok {
continue
}
if n, _ := fn["name"].(string); n == name {
d, _ := fn["description"].(string)
return d
}
}
return ""
}