WIP: memory system improvements (in progress)
Interview/chat prompt layers, reply planner, style profiles, memory injection, interview meta store, and related tests. Work not finished. Made-with: Cursor
This commit is contained in:
@@ -1,6 +1,11 @@
|
||||
"""
|
||||
访谈轮次编排(方案 A):由服务端显式给出 turn_mode / 主槽 / 挂钩摘录,
|
||||
减少仅靠长 prompt 软约束时模型「随便问、不往回忆录引」的漂移。
|
||||
访谈轮次编排(Option B 定位:InterviewTurnPlan 是**本轮行为模式的唯一决策源**)。
|
||||
|
||||
约束说明:
|
||||
- 主 prompt(prompt_layers)只提供跨轮通用的承接-深挖-串联节奏与身份守则;
|
||||
- 本轮是否「情绪优先」「跟话头」「回忆推进」完全由 `plan_interview_turn()` 决定;
|
||||
- 对 LLM 的硬指令由 `InterviewTurnPlan.render_system_directive()` 输出,主 prompt **不得**再
|
||||
针对具体模式另立软约束。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -11,17 +16,173 @@ from typing import Literal
|
||||
from app.agents.chat.prompts_conversation import SLOT_NAME_MAP
|
||||
from app.agents.stage_constants import STAGE_SLOT_KEYS
|
||||
|
||||
InterviewTurnMode = Literal["emotion_first", "memoir_push", "follow_user_only"]
|
||||
InterviewTurnMode = Literal[
|
||||
"emotion_first",
|
||||
"clarify_first",
|
||||
"memoir_push",
|
||||
"follow_user_only",
|
||||
]
|
||||
SubjectOwner = Literal["user_only"]
|
||||
MemoryUsage = Literal["none", "allowed_with_attribution"]
|
||||
ReplyShape = Literal["flexible", "ack_only", "ack_then_question"]
|
||||
AnchorSourceKind = Literal["user_message", "memory", "none"]
|
||||
|
||||
# 本轮承接焦点(规则基线或 focus planner LLM 输出;供 directive 与日志)
|
||||
FocusPrimary = Literal[
|
||||
"emotion",
|
||||
"relationship",
|
||||
"identity",
|
||||
"scene",
|
||||
"memoir_gap",
|
||||
"follow_user",
|
||||
]
|
||||
FocusSource = Literal["rule", "llm", "fallback"]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class InterviewTurnPlan:
|
||||
"""单轮访谈的硬目标(供注入 system prompt 顶部,优先级高于一般性建议)。"""
|
||||
"""单轮访谈的硬目标(优先级高于主 prompt 的一般性建议)。
|
||||
|
||||
作为唯一决策源,对 LLM 的落地由 `render_system_directive()` 输出;其它调用方应通过
|
||||
`is_emotion_first` / `is_clarify_first` / `is_follow_user_only` / `is_memoir_push` /
|
||||
`requires_explicit_question` 等语义属性读取决策,避免在主 prompt 里重复立法。
|
||||
|
||||
扩展字段承载「主语归属 / 记忆引用策略 / 回复形状」,供防上下文污染与双阶段 planner 合并。
|
||||
"""
|
||||
|
||||
mode: InterviewTurnMode
|
||||
anchor_slot_key: str | None
|
||||
anchor_slot_readable: str
|
||||
anchor_snippet: str
|
||||
anchor_source_kind: AnchorSourceKind = "none"
|
||||
assistant_identity_question: bool = False
|
||||
subject_owner: SubjectOwner = "user_only"
|
||||
memory_usage: MemoryUsage = "none"
|
||||
memory_reference_style: str = "你之前提过"
|
||||
forbid_first_person_experience: bool = True
|
||||
reply_shape: ReplyShape = "flexible"
|
||||
# 本轮承接重点(单一事实源的一部分;与 mode 配合,由 directive 落地)
|
||||
primary_focus: FocusPrimary = "memoir_gap"
|
||||
secondary_focus: FocusPrimary | None = None
|
||||
focus_summary: str = ""
|
||||
focus_source: FocusSource = "rule"
|
||||
|
||||
# ---- 语义属性:供 prompt_layers / interview_agent 等调用方消费,禁止重复立法 ----
|
||||
|
||||
@property
|
||||
def is_emotion_first(self) -> bool:
|
||||
return self.mode == "emotion_first"
|
||||
|
||||
@property
|
||||
def is_clarify_first(self) -> bool:
|
||||
return self.mode == "clarify_first"
|
||||
|
||||
@property
|
||||
def is_follow_user_only(self) -> bool:
|
||||
return self.mode == "follow_user_only"
|
||||
|
||||
@property
|
||||
def is_memoir_push(self) -> bool:
|
||||
return self.mode == "memoir_push"
|
||||
|
||||
@property
|
||||
def requires_explicit_question(self) -> bool:
|
||||
"""memoir_push 要求恰好一个开放式追问;其它模式允许不问或尽量不问。"""
|
||||
return self.mode == "memoir_push"
|
||||
|
||||
@property
|
||||
def allows_zero_question(self) -> bool:
|
||||
"""emotion_first / clarify_first 整轮允许不问;follow_user_only 仅允许最多一个轻问。"""
|
||||
return self.mode in ("emotion_first", "clarify_first", "follow_user_only")
|
||||
|
||||
def render_system_directive(self) -> str:
|
||||
"""对 LLM 的硬指令(供注入 system prompt 顶部)。"""
|
||||
return format_interview_turn_directive_block(self)
|
||||
|
||||
|
||||
def _focus_primary_for_mode(mode: InterviewTurnMode) -> FocusPrimary:
|
||||
if mode == "emotion_first":
|
||||
return "emotion"
|
||||
if mode == "clarify_first":
|
||||
return "follow_user"
|
||||
if mode == "follow_user_only":
|
||||
return "follow_user"
|
||||
return "memoir_gap"
|
||||
|
||||
|
||||
def apply_safe_mode_override(
|
||||
baseline: InterviewTurnMode,
|
||||
override: str | None,
|
||||
*,
|
||||
primary_focus: str | None,
|
||||
) -> InterviewTurnMode | None:
|
||||
"""若 planner 建议覆盖 mode,在此做安全收敛;返回 None 表示保持 baseline。"""
|
||||
if not override or override == baseline:
|
||||
return None
|
||||
if override not in (
|
||||
"emotion_first",
|
||||
"clarify_first",
|
||||
"memoir_push",
|
||||
"follow_user_only",
|
||||
):
|
||||
return None
|
||||
# 情绪优先一旦成立,不因 planner 改回强推素材
|
||||
if baseline == "emotion_first" and override != "emotion_first":
|
||||
return None
|
||||
# 跟话头 / 模糊先澄清:不允许改回问卷式 memoir_push
|
||||
if baseline in ("follow_user_only", "clarify_first") and override == "memoir_push":
|
||||
return None
|
||||
# memoir_push -> emotion_first:仅当焦点偏情绪/关系/身份(或空,由模型判断)
|
||||
if baseline == "memoir_push" and override == "emotion_first":
|
||||
pf = (primary_focus or "").strip()
|
||||
if pf in ("", "emotion", "relationship", "identity"):
|
||||
return "emotion_first"
|
||||
return None
|
||||
if baseline == "memoir_push" and override == "follow_user_only":
|
||||
return "follow_user_only"
|
||||
if baseline == "follow_user_only" and override == "emotion_first":
|
||||
return "emotion_first"
|
||||
if baseline == "follow_user_only" and override == "clarify_first":
|
||||
return "clarify_first"
|
||||
if baseline == "memoir_push" and override == "clarify_first":
|
||||
pf = (primary_focus or "").strip()
|
||||
if pf in ("", "emotion", "relationship", "identity", "follow_user"):
|
||||
return "clarify_first"
|
||||
return None
|
||||
if baseline == "clarify_first" and override == "emotion_first":
|
||||
return "emotion_first"
|
||||
if baseline == "clarify_first" and override == "follow_user_only":
|
||||
return "follow_user_only"
|
||||
return None
|
||||
|
||||
|
||||
def _focus_directive_lines(plan: InterviewTurnPlan) -> str:
|
||||
"""本轮承接重点:focus_summary 仅作追问角度,不支配正文措辞。"""
|
||||
if (plan.focus_summary or "").strip():
|
||||
summary = (plan.focus_summary or "").strip()
|
||||
if len(summary) > 200:
|
||||
summary = summary[:199].rstrip() + "…"
|
||||
return (
|
||||
"- **本轮追问/承接角度(非正文提纲;主回复仍须从用户本轮原话长出来)**:"
|
||||
+ summary
|
||||
+ "\n"
|
||||
)
|
||||
labels: dict[FocusPrimary, str] = {
|
||||
"emotion": "情绪与感受",
|
||||
"relationship": "关系与他人(谁在场、谁在看、和谁较劲)",
|
||||
"identity": "身份与面子(自我形象、怕丢脸、要强)",
|
||||
"scene": "现场与感官(画面、光线、身体感受)",
|
||||
"memoir_gap": "叙述槽与成稿素材(当前主追问方向)",
|
||||
"follow_user": "顺着用户话头自然展开",
|
||||
}
|
||||
pri = labels.get(plan.primary_focus, labels["memoir_gap"])
|
||||
if plan.secondary_focus and plan.secondary_focus != plan.primary_focus:
|
||||
sec = labels.get(plan.secondary_focus, "")
|
||||
if sec:
|
||||
return (
|
||||
f"- **本轮承接重点**:先锚定「{pri}」,必要时再轻点「{sec}」作辅助。\n"
|
||||
)
|
||||
return f"- **本轮承接重点**:优先锚定「{pri}」。\n"
|
||||
|
||||
|
||||
def primary_empty_slot(stage: str, empty_slots: list[str]) -> str | None:
|
||||
@@ -48,16 +209,51 @@ def extract_anchor_snippet(
|
||||
user_message: str,
|
||||
max_chars: int = 180,
|
||||
) -> str:
|
||||
"""优先记忆摘录,其次用户原话(用于追问挂钩,非事实断言)。"""
|
||||
mem = _strip_scene_hint(memory_evidence_text)
|
||||
if mem and len(mem) >= 4:
|
||||
return mem[:max_chars].strip()
|
||||
"""优先用户本轮原话,其次极短记忆线索(用于追问挂钩,非事实断言)。
|
||||
|
||||
旧记忆不得压过用户当前话头;仅当本轮原话过短时再借 `memory_anchor_source` 挂钩。
|
||||
`memory_evidence_text` 此处仅为**一条短线索**(非整段 `[M…]` 摘录块)。
|
||||
"""
|
||||
um = (user_message or "").strip()
|
||||
if len(um) >= 10:
|
||||
return um[:max_chars].strip()
|
||||
mem = _strip_scene_hint(memory_evidence_text)
|
||||
if mem and len(mem) >= 4:
|
||||
for line in mem.splitlines():
|
||||
s = line.strip()
|
||||
if s.startswith("[M") and "]" in s:
|
||||
rest = s.split("]", 1)[1].strip()
|
||||
if rest and len(rest) >= 4:
|
||||
return rest[:max_chars].strip()
|
||||
return mem[:max_chars].strip()
|
||||
if len(um) >= 4:
|
||||
return um[:max_chars].strip()
|
||||
return ""
|
||||
|
||||
|
||||
def determine_anchor_source_kind(
|
||||
*,
|
||||
memory_evidence_text: str,
|
||||
user_message: str,
|
||||
) -> AnchorSourceKind:
|
||||
"""标记挂钩线索来源,避免把用户本轮原话误标成旧记忆。"""
|
||||
um = (user_message or "").strip()
|
||||
if len(um) >= 10:
|
||||
return "user_message"
|
||||
mem = _strip_scene_hint(memory_evidence_text)
|
||||
if mem and len(mem) >= 4:
|
||||
for line in mem.splitlines():
|
||||
s = line.strip()
|
||||
if s.startswith("[M") and "]" in s:
|
||||
rest = s.split("]", 1)[1].strip()
|
||||
if rest and len(rest) >= 4:
|
||||
return "memory"
|
||||
return "memory"
|
||||
if len(um) >= 4:
|
||||
return "user_message"
|
||||
return "none"
|
||||
|
||||
|
||||
_EMOTION_MARKERS: tuple[str, ...] = (
|
||||
"哭",
|
||||
"难受",
|
||||
@@ -79,6 +275,52 @@ _EMOTION_MARKERS: tuple[str, ...] = (
|
||||
"扛不住",
|
||||
"放不下",
|
||||
"意难平",
|
||||
"难堪",
|
||||
"丢脸",
|
||||
"怕丢人",
|
||||
"臊得慌",
|
||||
)
|
||||
|
||||
# 说不清 / 暧昧放慢:不触发 memoir_push 式强推槽位
|
||||
_AMBIGUITY_MARKERS: tuple[str, ...] = (
|
||||
"说不清",
|
||||
"说不明白",
|
||||
"说不上来",
|
||||
"说不上",
|
||||
"说不好",
|
||||
"不知道怎么说",
|
||||
"不知道咋说",
|
||||
"好像",
|
||||
"也不是",
|
||||
"不完全是",
|
||||
"不太确定",
|
||||
"不确定",
|
||||
"有点模糊",
|
||||
"很模糊",
|
||||
"模模糊糊",
|
||||
"难说",
|
||||
"有一点,但我不确定",
|
||||
)
|
||||
_SOFT_SLOW_MARKERS: tuple[str, ...] = (
|
||||
"害羞",
|
||||
"羞涩",
|
||||
"不好意思",
|
||||
"脸红",
|
||||
"暧昧",
|
||||
)
|
||||
|
||||
_ASSISTANT_IDENTITY_QUESTION_MARKERS: tuple[str, ...] = (
|
||||
"你是哪里人",
|
||||
"你哪的人",
|
||||
"你是哪里长大的",
|
||||
"你在哪长大",
|
||||
"你的童年",
|
||||
"你小时候",
|
||||
"你家里",
|
||||
"你爸妈",
|
||||
"你父母",
|
||||
"你也有过",
|
||||
"你也是",
|
||||
)
|
||||
|
||||
|
||||
@@ -97,6 +339,33 @@ def _is_emotion_heavy(text: str) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def _is_ambiguous_or_needs_slow_pace(text: str) -> bool:
|
||||
"""模糊表达、暧昧/羞涩等:先澄清或放慢,不强推叙述槽。"""
|
||||
t = (text or "").strip()
|
||||
if not t:
|
||||
return True
|
||||
if any(m in t for m in _AMBIGUITY_MARKERS):
|
||||
return True
|
||||
if any(m in t for m in _SOFT_SLOW_MARKERS):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _is_too_vague_for_memoir_push(text: str) -> bool:
|
||||
"""过短或仍含糊时,不进入 memoir_push。"""
|
||||
t = (text or "").strip()
|
||||
if len(t) < 12:
|
||||
return True
|
||||
return _is_ambiguous_or_needs_slow_pace(t)
|
||||
|
||||
|
||||
def _is_asking_assistant_identity_or_life(text: str) -> bool:
|
||||
t = (text or "").strip()
|
||||
if not t:
|
||||
return False
|
||||
return any(marker in t for marker in _ASSISTANT_IDENTITY_QUESTION_MARKERS)
|
||||
|
||||
|
||||
def plan_interview_turn(
|
||||
*,
|
||||
current_stage: str,
|
||||
@@ -108,14 +377,31 @@ def plan_interview_turn(
|
||||
"""
|
||||
粗规则(可迭代):
|
||||
- 情绪浓:先共情,不强推叙述槽搜集问。
|
||||
- 说不清/暧昧羞涩等:模糊先澄清,禁止替用户下结论或强推槽位。
|
||||
- 刚切换人生阶段:跟着用户节奏,不做「新阶段问卷首开」。
|
||||
- 当前阶段无空槽:深度跟进,不重启盘点。
|
||||
- 默认:memoir_push,锁一个主槽 + 挂钩摘录。
|
||||
- 默认:memoir_push(仅当本轮话头足够具体、非含糊短答时)。
|
||||
"""
|
||||
snippet = extract_anchor_snippet(
|
||||
memory_evidence_text=memory_evidence_text,
|
||||
user_message=normalized_user_message,
|
||||
)
|
||||
anchor_source_kind = determine_anchor_source_kind(
|
||||
memory_evidence_text=memory_evidence_text,
|
||||
user_message=normalized_user_message,
|
||||
)
|
||||
mem_trim = _strip_scene_hint(memory_evidence_text).strip()
|
||||
mem_use: MemoryUsage = (
|
||||
"allowed_with_attribution" if mem_trim else "none"
|
||||
)
|
||||
um = (normalized_user_message or "").strip()
|
||||
asks_assistant_identity = _is_asking_assistant_identity_or_life(um)
|
||||
reply_shape: ReplyShape = "flexible"
|
||||
if any(
|
||||
k in um
|
||||
for k in ("讲讲", "说说", "她的故事", "他的故事", "后来呢", "然后呢")
|
||||
):
|
||||
reply_shape = "ack_then_question"
|
||||
|
||||
if _is_emotion_heavy(normalized_user_message):
|
||||
slot = primary_empty_slot(current_stage, empty_slots)
|
||||
@@ -129,6 +415,12 @@ def plan_interview_turn(
|
||||
anchor_slot_key=slot,
|
||||
anchor_slot_readable=readable,
|
||||
anchor_snippet=snippet,
|
||||
anchor_source_kind=anchor_source_kind,
|
||||
assistant_identity_question=asks_assistant_identity,
|
||||
memory_usage=mem_use,
|
||||
reply_shape=reply_shape,
|
||||
primary_focus=_focus_primary_for_mode("emotion_first"),
|
||||
focus_source="rule",
|
||||
)
|
||||
|
||||
if stage_switched_this_turn:
|
||||
@@ -137,6 +429,12 @@ def plan_interview_turn(
|
||||
anchor_slot_key=None,
|
||||
anchor_slot_readable="(刚自然谈到本阶段,先顺着对方语势,勿问卷式首开)",
|
||||
anchor_snippet=snippet,
|
||||
anchor_source_kind=anchor_source_kind,
|
||||
assistant_identity_question=asks_assistant_identity,
|
||||
memory_usage=mem_use,
|
||||
reply_shape=reply_shape,
|
||||
primary_focus=_focus_primary_for_mode("follow_user_only"),
|
||||
focus_source="rule",
|
||||
)
|
||||
|
||||
if not empty_slots:
|
||||
@@ -145,6 +443,32 @@ def plan_interview_turn(
|
||||
anchor_slot_key=None,
|
||||
anchor_slot_readable="(本阶段主要叙述槽已有素材)请 depth-first:接续画面或情绪线,别重启童年在哪长大式盘点",
|
||||
anchor_snippet=snippet,
|
||||
anchor_source_kind=anchor_source_kind,
|
||||
assistant_identity_question=asks_assistant_identity,
|
||||
memory_usage=mem_use,
|
||||
reply_shape=reply_shape,
|
||||
primary_focus=_focus_primary_for_mode("follow_user_only"),
|
||||
focus_source="rule",
|
||||
)
|
||||
|
||||
if _is_too_vague_for_memoir_push(um):
|
||||
slot = primary_empty_slot(current_stage, empty_slots)
|
||||
readable = (
|
||||
SLOT_NAME_MAP.get(slot, slot or "")
|
||||
if slot
|
||||
else "(先澄清感受时可暂不强绑某一槽位)"
|
||||
)
|
||||
return InterviewTurnPlan(
|
||||
mode="clarify_first",
|
||||
anchor_slot_key=slot,
|
||||
anchor_slot_readable=readable,
|
||||
anchor_snippet=snippet,
|
||||
anchor_source_kind=anchor_source_kind,
|
||||
assistant_identity_question=asks_assistant_identity,
|
||||
memory_usage=mem_use,
|
||||
reply_shape=reply_shape,
|
||||
primary_focus=_focus_primary_for_mode("clarify_first"),
|
||||
focus_source="rule",
|
||||
)
|
||||
|
||||
slot = primary_empty_slot(current_stage, empty_slots)
|
||||
@@ -154,6 +478,12 @@ def plan_interview_turn(
|
||||
anchor_slot_key=slot,
|
||||
anchor_slot_readable=SLOT_NAME_MAP.get(slot, slot),
|
||||
anchor_snippet=snippet,
|
||||
anchor_source_kind=anchor_source_kind,
|
||||
assistant_identity_question=asks_assistant_identity,
|
||||
memory_usage=mem_use,
|
||||
reply_shape=reply_shape,
|
||||
primary_focus=_focus_primary_for_mode("memoir_push"),
|
||||
focus_source="rule",
|
||||
)
|
||||
|
||||
|
||||
@@ -165,6 +495,45 @@ def format_interview_turn_directive_block(plan: InterviewTurnPlan) -> str:
|
||||
else "(无可用摘录时,必须从用户本轮原话里抽词作挂钩,禁止编造)"
|
||||
)
|
||||
|
||||
if plan.memory_usage == "allowed_with_attribution":
|
||||
mem_block = (
|
||||
"- **过往记忆线索**:主 Context 里若有一条极短线索,**只**用于帮你选追问角度;"
|
||||
"**不是**正文提纲,**不是**长素材。\n"
|
||||
"- **禁止**用「"
|
||||
+ plan.memory_reference_style
|
||||
+ "…」开场或整段复述旧记忆;若轻勾连,**最多半句**,且须让本轮原话占主位。\n"
|
||||
"- **禁止**把线索写成助手亲历;**禁止**把线索当「咱俩共同回忆」。\n"
|
||||
)
|
||||
else:
|
||||
mem_block = (
|
||||
"- **过往记忆线索**:本轮无可用线索(或已策略性不注入);"
|
||||
"**不要**编造「你说过…」式假归因。\n"
|
||||
)
|
||||
|
||||
perspective_block = (
|
||||
"- **主语与视角**:用户可见回复主线必须是**用户**的经历与感受;"
|
||||
"助手**没有**真实人生传记。\n"
|
||||
)
|
||||
if plan.forbid_first_person_experience:
|
||||
perspective_block += (
|
||||
"- **禁止助手自传式措辞**:不得使用「我小时候」「我演过/我扮演」「我当时暗恋」"
|
||||
"「我爸妈」等自称亲历表述。\n"
|
||||
)
|
||||
if plan.assistant_identity_question:
|
||||
perspective_block += (
|
||||
"- **本轮用户在问助手本人**:必须明确守住边界,不能假装自己有籍贯、童年、家庭或恋爱经历;"
|
||||
"若要借记忆继续聊,只能明确归因到用户,例如「你刚提到上海」「你前面说过……」。\n"
|
||||
)
|
||||
|
||||
if plan.reply_shape == "ack_only":
|
||||
shape_block = "- **本轮回复形状**:以短承接为主;若无必要可整轮不问。\n"
|
||||
elif plan.reply_shape == "ack_then_question":
|
||||
shape_block = (
|
||||
"- **本轮回复形状**:先短承接,再尽量带**一条**贴用户话头的开放式问(仍守全篇最多一个问句)。\n"
|
||||
)
|
||||
else:
|
||||
shape_block = ""
|
||||
|
||||
if plan.mode == "emotion_first":
|
||||
mode_rules = (
|
||||
"- **情绪优先**:本轮以承接、并肩与安全感为主,**不要**为推进「叙述槽大纲」而追加信息搜集型追问。\n"
|
||||
@@ -173,6 +542,16 @@ def format_interview_turn_directive_block(plan: InterviewTurnPlan) -> str:
|
||||
+ plan.anchor_slot_readable
|
||||
+ "」仅供你心里知道后续方向,**不要**在本轮用问卷口吻硬推该槽。"
|
||||
)
|
||||
elif plan.mode == "clarify_first":
|
||||
mode_rules = (
|
||||
"- **模糊先澄清**:用户正在表达不确定、说不清,或暧昧/羞涩等未命名感受;"
|
||||
"以承接、并列为先,**禁止**把模糊感受改写成确定关系、命运、动机或人生结论。\n"
|
||||
"- 若带问句,最多**一个**,且须邀请对方**用自己的词**继续摸索或澄清(例如更接近哪一种、还是也不完全是),"
|
||||
"禁止封闭式逼认、禁止替用户命名关系或事件结局。\n"
|
||||
"- 允许整轮**只承接不问**;参考主槽「"
|
||||
+ plan.anchor_slot_readable
|
||||
+ "」仅供你心里知道后续方向,**不要**问卷式硬推该槽。"
|
||||
)
|
||||
elif plan.mode == "follow_user_only":
|
||||
mode_rules = (
|
||||
"- **跟话头**:本轮禁止问卷式首开、禁止重启式盘点;顺着用户刚展开的画面、人物或情绪自然往下。\n"
|
||||
@@ -181,20 +560,38 @@ def format_interview_turn_directive_block(plan: InterviewTurnPlan) -> str:
|
||||
else:
|
||||
mode_rules = (
|
||||
"- **回忆推进(memoir_push)**:对用户可见回复中须有**恰好一个**开放式回忆追问,\n"
|
||||
" 且意图明显在补足下面「主追问方向」;问句必须挂住**挂钩摘录**或**用户本轮原词**(二者至少其一)。\n"
|
||||
"- 禁止用日常寒暄替代该追问;仍遵守全篇「最多一个问句」「禁止晚会硬切」。"
|
||||
" 且意图明显在补足下面「主追问方向」;问句必须挂住**用户本轮原词**或**挂钩摘录**(二者至少其一,且**优先原词**)。\n"
|
||||
"- **禁止**把用户仍含糊的表达改写成确定事实或关系;禁止用日常寒暄替代该追问;"
|
||||
"仍遵守全篇「最多一个问句」「禁止晚会硬切」。"
|
||||
)
|
||||
|
||||
focus_block = _focus_directive_lines(plan)
|
||||
|
||||
if plan.anchor_source_kind == "user_message":
|
||||
anchor_label = "(仅作追问挂钩,来自用户本轮原话摘录;优先把这句里的原词接住)"
|
||||
elif plan.anchor_source_kind == "memory":
|
||||
anchor_label = "(仅作追问挂钩,来自检索到的用户过往口述/摘要;不是用户本轮新说的内容)"
|
||||
else:
|
||||
anchor_label = "(无可用挂钩时,必须从用户本轮原话里抽词承接,禁止编造)"
|
||||
|
||||
return f"""## 本轮编排指令(硬规则,优先于后文一般性建议)
|
||||
{mode_rules}
|
||||
- **主追问方向(叙述槽)**:{plan.anchor_slot_readable}
|
||||
- **挂钩摘录**(仅作衔接线索,**不是**用户本轮新说的内容;禁止写成就等于用户刚讲的原话):{snippet_line}
|
||||
{focus_block}{mem_block}{perspective_block}{shape_block}- **主追问方向(叙述槽)**:{plan.anchor_slot_readable}
|
||||
- **挂钩线索**{anchor_label}:{snippet_line}
|
||||
"""
|
||||
|
||||
|
||||
__all__ = [
|
||||
"FocusPrimary",
|
||||
"FocusSource",
|
||||
"InterviewTurnMode",
|
||||
"InterviewTurnPlan",
|
||||
"MemoryUsage",
|
||||
"ReplyShape",
|
||||
"SubjectOwner",
|
||||
"AnchorSourceKind",
|
||||
"apply_safe_mode_override",
|
||||
"determine_anchor_source_kind",
|
||||
"extract_anchor_snippet",
|
||||
"format_interview_turn_directive_block",
|
||||
"plan_interview_turn",
|
||||
|
||||
Reference in New Issue
Block a user