Skip to content

Commit 7831326

Browse files
wehosHongzhi Wenclaude
authored
fix(proactive): Phase 2 无合法来源标签时 drop,杜绝脚手架泄漏 (Project-N-E-K-O#1512)
* fix(proactive): Phase 2 输出无合法来源标签时 drop,杜绝脚手架泄漏被念出 线上复现:free-model 初稿直接把人设 Format 块当正文吐出来("No Markdown: Yes." "* No stage directions/parentheses"),tag 解析为空却仍被投递。根因是 tag_match 未命中时 source_tag 留空但下游照样 emit。 - 初稿:结果处理前加一道闸——非 abort、有正文、但 source_tag 为空(未解析到 [CHAT]/[WEB]/[MUSIC]/[MEME])即判格式泄漏,drop 整轮。合法搭话必以 tag 起头, 缺 tag 一律不投递。TTS 在该点之后才真正 feed,abort 安全。 - regen:同款防护——regen 产出无 tag 不再默认当 CHAT 投递,并入 empty/PASS 的 drop 分支。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(prompts): 内心活动 header 前加空行 + 输出格式标题改"最终输出格式" - INNER_THOUGHTS_HEADER 每个 locale 前缀 \n → \n\n,与上方长期记忆隔开一空行。 - proactive 输出格式段标题 "输出格式(严格遵守)" → "最终输出格式(严格遵守)" (7 locale 同步:Final output format / 最終出力形式 / 최종 출력 형식 / Окончательный формат / Formato de salida final / Formato de saída final), 强调这是最终格式要求,降低模型把人设约束当正文照抄的概率。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(proactive): 搭话改用 conversation tier + 无 tag 时格式自救 regen 根因:搭话用 correction tier,不开思考时较难稳定遵循"第一行写来源标签"的 格式,弱化时会把人设 Format 约束块当正文吐出来。 - tier:proactive 全链路(Phase1 筛选 / Phase2 生成 / regen)从 correction 改用 conversation tier(主对话主力模型,格式遵循更稳);变量 correction_* → conversation_*。仍保持 disable_thinking=True(vision+思考必超时)。 break reminder 不属搭话,保持 correction tier 不动。 - ① 自救:初稿无合法 source_tag 时不直接 drop,先用格式纠正指令 regen 一次 (重建 Human turn:fix 指令 + 原 human_text,末尾仍是 BEGIN 触发句),重跑 解析 tag;拿到合法非 PASS tag 就接回主流程(下游 is_duplicate / BM25 照常 生效),仍无 tag 才 drop。preempt 时放弃。 - ② 强化:新增 render_format_fix_instruction(7 locale),强调第一行写来源 标签、否则只 [PASS]、禁止复述规则/输出清单。配合上一提交的"最终输出格式"标题。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(proactive): tag 解析前 lstrip,容忍 [CHAT] 前的换行/空格(Codex P2) 流式解析与流后兜底都用 ^\[...\] 匹配来源标签,但未去前导空白。模型偶尔先吐 一个换行/空格 token 再吐 [CHAT] 时,^\[ 匹配失败、source_tag 误留空,合法回复 被当成无 tag。两处 tag_match 前补 cleaned.lstrip()。 (format-regen 自救本可救回此类,但根因在解析,lstrip 后省掉一次重生成。) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(proactive): 无 tag gate 仅在启用标签系统时生效 + format-fix 补 WEB - P1:_of_none 模式(无 web/music/meme 通道)下输出格式段明确要求纯文本无 tag, 下游靠 source_tag='CHAT' 兜底投递。原 no-tag gate 会把这类合法纯文本误判为 格式泄漏、强制自救后 drop(screen-only / 纯聊天轮 false pass)。给 gate 加 _expects_source_tag 守卫(external/music/meme 任一通道存在才要求 tag), 纯文本模式跳过 gate 走原 CHAT 兜底。 - P2:format-fix 自救指令的标签白名单补上 [WEB](原只列 CHAT/MUSIC/MEME), 并改为"按上面输出格式段列出的标签选",避免修复 web 话题时丢掉 WEB 标签 导致下游不挂链接卡片。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(proactive): 删除 _llm_call_with_retry 中未使用的 actual_model 该局部变量赋值后从未被引用(_make_llm 内部自行解析 model),删除不影响 重试逻辑 / 消息构建 / 模型调用(github-code-quality)。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(proactive): BM25 regen 的无 tag drop 也加 _expects_source_tag 守卫 与初稿 gate 对齐:BM25 regen 产出无 tag 时,仅在本轮启用标签系统 (_expects_source_tag) 才判泄漏 drop;_of_none 纯文本模式下无 tag 合法, 留空交给下游 source_tag='CHAT' 兜底(regen 成功已设 full_text=_cleaned, 落到 6715 的 CHAT 兜底)。PASS / 空 / 内嵌[PASS] 仍照常 drop(Codex P2)。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Hongzhi Wen <cartabio.coder1@gmail.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 126f7e9 commit 7831326

4 files changed

Lines changed: 193 additions & 39 deletions

File tree

config/prompts/prompts_directives.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -566,6 +566,81 @@ def render_regen_avoid_instruction(terms: List[str], lang: str, master_name: str
566566
)
567567

568568

569+
# ---------------------------------------------------------------------------
570+
# Proactive 格式纠正指令 — 初稿没按格式输出时自救用
571+
# ---------------------------------------------------------------------------
572+
# 初稿没解析到合法来源标签时(弱化模型常把人设 Format/约束块当正文吐出来,
573+
# 如 "No Markdown: Yes."),system_router 注入这段再生成一次,把模型拽回
574+
# "第一行写来源标签、其后正文" 的格式;与 BEGIN 触发句一起放进 Human turn
575+
# (末尾仍是中性触发句)。占位符:{master_name} 搭话对象。
576+
577+
PROACTIVE_FORMAT_FIX_INSTRUCTION = {
578+
'zh': (
579+
"【格式纠正】上一次的输出没有按规定格式,把格式要求当成正文吐了出来。"
580+
"请重写:第一行只写一个来源标签(按上面输出格式段列出的来源标签选,"
581+
"如 [CHAT]、[WEB]、[MUSIC]、[MEME]),第二行起只写要对{master_name}说的话本身;"
582+
"没什么新鲜的可说就只输出 [PASS]。"
583+
"不要复述或解释任何规则,不要输出清单或思考过程,标签和正文以外的内容一律不要输出。"
584+
),
585+
'en': (
586+
"[Format fix] Your last output didn't follow the required format — it spat out the "
587+
"rules as if they were the message. Rewrite it: the first line is a single source tag "
588+
"(choose from the source tags listed in the output-format section above, e.g. [CHAT], "
589+
"[WEB], [MUSIC], [MEME]), then from the next line write only the actual words you'd say "
590+
"to {master_name}; if you have nothing fresh to say, output only [PASS]. Do NOT restate "
591+
"or explain any rule, do NOT output lists or reasoning, and output nothing other than "
592+
"the tag and the message."
593+
),
594+
'ja': (
595+
"【書式修正】前回の出力は指定の書式に従わず、ルールをそのまま本文として出してしまいました。"
596+
"書き直してください:1行目に来源タグを1つだけ(上の出力形式に挙げられたタグから選ぶ。"
597+
"例:[CHAT]・[WEB]・[MUSIC]・[MEME])、2行目以降は{master_name}に実際に言う言葉だけ。"
598+
"新しく言うことがなければ [PASS] だけを出力。"
599+
"ルールを復唱・説明せず、リストや思考過程を出さず、タグと本文以外は何も出力しないこと。"
600+
),
601+
'ko': (
602+
"【형식 교정】지난 출력이 규정된 형식을 따르지 않고 규칙을 본문처럼 뱉어냈습니다. "
603+
"다시 쓰세요: 첫 줄에는 출처 태그 하나만(위 출력 형식에 나열된 태그 중 선택, 예: [CHAT]·"
604+
"[WEB]·[MUSIC]·[MEME]), 이후 줄부터는 {master_name}에게 실제로 할 말만. 새로 할 말이 "
605+
"없으면 [PASS]만 출력. 규칙을 되풀이하거나 설명하지 말고, 목록·사고 과정을 출력하지 "
606+
"말며, 태그와 본문 외에는 아무것도 출력하지 마세요."
607+
),
608+
'ru': (
609+
"[Исправь формат] Прошлый вывод не соответствовал формату — ты выдал правила, как "
610+
"будто это сообщение. Перепиши: первая строка — один тег источника (выбери из тегов, "
611+
"перечисленных в разделе формата вывода выше, напр. [CHAT], [WEB], [MUSIC], [MEME]), "
612+
"далее со следующей строки — только сами слова, которые ты скажешь {master_name}; если "
613+
"нового сказать нечего, выведи только [PASS]. Не пересказывай и не объясняй правила, не "
614+
"выводи списки или рассуждения и не выводи ничего, кроме тега и сообщения."
615+
),
616+
'es': (
617+
"[Corrige el formato] Tu última salida no siguió el formato requerido: soltó las reglas "
618+
"como si fueran el mensaje. Reescríbela: la primera línea es una sola etiqueta de fuente "
619+
"(elige entre las etiquetas listadas en la sección de formato de salida de arriba, p. ej. "
620+
"[CHAT], [WEB], [MUSIC], [MEME]), luego desde la línea siguiente escribe solo lo que le "
621+
"dirías a {master_name}; si no tienes nada nuevo que decir, responde solo [PASS]. No "
622+
"repitas ni expliques ninguna regla, no muestres listas ni razonamientos, y no muestres "
623+
"nada más que la etiqueta y el mensaje."
624+
),
625+
'pt': (
626+
"[Corrija o formato] Sua última saída não seguiu o formato exigido — cuspiu as regras "
627+
"como se fossem a mensagem. Reescreva: a primeira linha é uma única etiqueta de fonte "
628+
"(escolha entre as etiquetas listadas na seção de formato de saída acima, p. ex. [CHAT], "
629+
"[WEB], [MUSIC], [MEME]), depois, a partir da linha seguinte, escreva apenas o que você "
630+
"diria a {master_name}; se não tiver nada novo a dizer, responda apenas [PASS]. Não "
631+
"repita nem explique nenhuma regra, não exiba listas ou raciocínio, e não exiba nada "
632+
"além da etiqueta e da mensagem."
633+
),
634+
}
635+
636+
637+
def render_format_fix_instruction(lang: str, master_name: str = "") -> str:
638+
"""渲染"格式纠正"自救指令。``master_name`` 缺省退化为中性占位符。"""
639+
short = _norm_lang(lang)
640+
template = PROACTIVE_FORMAT_FIX_INSTRUCTION.get(short) or PROACTIVE_FORMAT_FIX_INSTRUCTION['en']
641+
return template.format(master_name=master_name or _DEFAULT_ADDRESSEE.get(short, "them"))
642+
643+
569644
# =====================================================================
570645
# ======= Negative-keyword target check (RFC §3.4.5 Layer 2) ==========
571646
# =====================================================================

config/prompts/prompts_memory.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -842,13 +842,13 @@ def get_emotion_analysis_prompt(lang: str = "zh") -> str:
842842

843843
# ---------- Inner thoughts block header ----------
844844
INNER_THOUGHTS_HEADER = {
845-
"zh": "\n======以下是{name}的内心活动======\n",
846-
"en": "\n======{name}'s Inner Thoughts======\n",
847-
"ja": "\n======{name}の心の声======\n",
848-
"ko": "\n======{name}의 내면 활동======\n",
849-
"ru": "\n======Внутренние мысли {name}======\n",
850-
"es": "\n======Pensamientos internos de {name}======\n",
851-
"pt": "\n======Pensamentos internos de {name}======\n",
845+
"zh": "\n\n======以下是{name}的内心活动======\n",
846+
"en": "\n\n======{name}'s Inner Thoughts======\n",
847+
"ja": "\n\n======{name}の心の声======\n",
848+
"ko": "\n\n======{name}의 내면 활동======\n",
849+
"ru": "\n\n======Внутренние мысли {name}======\n",
850+
"es": "\n\n======Pensamientos internos de {name}======\n",
851+
"pt": "\n\n======Pensamentos internos de {name}======\n",
852852
}
853853

854854
INNER_THOUGHTS_BODY = {

config/prompts/prompts_proactive.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2919,13 +2919,13 @@ def get_proactive_format_sections(
29192919
}
29202920

29212921
_of_header = {
2922-
"zh": "输出格式(严格遵守):\n- 放弃搭话 → 只输出 [PASS]\n- 否则第一行写来源标签,第二行起写你要说的话:",
2923-
"en": "Output format (strict):\n- To skip → reply only [PASS]\n- Otherwise, first line = source tag, then your message on the next line(s):",
2924-
"ja": "出力形式(厳守):\n- パス → [PASS] のみ\n- それ以外 → 1行目にソースタグ、2行目以降にメッセージ:",
2925-
"ko": "출력 형식 (엄격 준수):\n- 패스 → [PASS]만\n- 그 외 → 첫 줄에 소스 태그, 다음 줄부터 메시지:",
2926-
"ru": "Формат ответа (строго):\n- Пропустить → ответьте только [PASS]\n- Иначе первая строка = тег источника, далее со следующей строки ваше сообщение:",
2927-
"es": "Formato de salida (estricto):\n- Para omitir → responde solo [PASS]\n- Si no, primera línea = tag de fuente, luego tu mensaje en la(s) línea(s) siguiente(s):",
2928-
"pt": "Formato de saída (estrito):\n- Para pular → responda apenas [PASS]\n- Caso contrário, primeira linha = tag de fonte, depois sua mensagem na(s) linha(s) seguinte(s):",
2922+
"zh": "最终输出格式(严格遵守):\n- 放弃搭话 → 只输出 [PASS]\n- 否则第一行写来源标签,第二行起写你要说的话:",
2923+
"en": "Final output format (strict):\n- To skip → reply only [PASS]\n- Otherwise, first line = source tag, then your message on the next line(s):",
2924+
"ja": "最終出力形式(厳守):\n- パス → [PASS] のみ\n- それ以外 → 1行目にソースタグ、2行目以降にメッセージ:",
2925+
"ko": "최종 출력 형식 (엄격 준수):\n- 패스 → [PASS]만\n- 그 외 → 첫 줄에 소스 태그, 다음 줄부터 메시지:",
2926+
"ru": "Окончательный формат ответа (строго):\n- Пропустить → ответьте только [PASS]\n- Иначе первая строка = тег источника, далее со следующей строки ваше сообщение:",
2927+
"es": "Formato de salida final (estricto):\n- Para omitir → responde solo [PASS]\n- Si no, primera línea = tag de fuente, luego tu mensaje en la(s) línea(s) siguiente(s):",
2928+
"pt": "Formato de saída final (estrito):\n- Para pular → responda apenas [PASS]\n- Caso contrário, primeira linha = tag de fonte, depois sua mensagem na(s) linha(s) seguinte(s):",
29292929
}
29302930

29312931
_of_example = {

0 commit comments

Comments
 (0)