From b4b3fa92e0ea38143ed9298214d29a15caed2601 Mon Sep 17 00:00:00 2001 From: liuwei Date: Thu, 16 Apr 2026 11:03:55 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96ai=5Fauto=5Fresponse=E5=9B=9E?= =?UTF-8?q?=E5=A4=8D=E9=95=BF=E5=BA=A6=E5=B9=B6=E5=BC=BA=E5=8C=96@?= =?UTF-8?q?=E7=94=BB=E5=83=8F=E5=9B=9E=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 变更项: 1. 收紧回复长度策略:social_short/qa_fast/qa_with_context 全部缩短,减少长句与说明文风格。 2. 强化提示词约束:默认30字内、最多2句且总长不超过55字,禁止大段铺垫。 3. 新增@画像高优先通道:当消息为@或强定向时,构建并注入 at_member_profile_prompt。 4. Dify输入同步注入@画像与 is_at/is_directed 控制字段,保证不同LLM后端行为一致。 --- .../context/context_builder.py | 41 +++++++++++++++++++ .../ai_auto_response/core/prompt_builder.py | 10 ++++- .../ai_auto_response/core/reply_formatter.py | 13 +++--- plugins/ai_auto_response/main.py | 6 +++ 4 files changed, 61 insertions(+), 9 deletions(-) diff --git a/plugins/ai_auto_response/context/context_builder.py b/plugins/ai_auto_response/context/context_builder.py index 3eeb484..5631fed 100644 --- a/plugins/ai_auto_response/context/context_builder.py +++ b/plugins/ai_auto_response/context/context_builder.py @@ -43,6 +43,8 @@ class ContextBuilder: "member_context": member_context or {}, }, "speaker_name_clean": self._clean_display_name(sender_name), + "is_at": bool(trigger.get("is_at", False)), + "is_directed": bool(trigger.get("is_directed", False)), "recent_message_items": self._build_recent_message_items(selected_messages), "recent_messages": recent_lines, "recent_summary": "", @@ -50,6 +52,12 @@ class ContextBuilder: "reply_mode": reply_mode, "flow_state": flow_state, "memory_prompt": self._build_member_memory_prompt(member_context, member_memory_focus or []), + "at_member_profile_prompt": self._build_at_member_profile_prompt( + member_context=member_context or {}, + focus_lines=member_memory_focus or [], + is_at=bool(trigger.get("is_at", False)), + is_directed=bool(trigger.get("is_directed", False)), + ), "vector_memory_prompt": self._build_vector_memory_prompt(vector_memories), "social_memory_prompt": self._build_social_memory_prompt(social_memory or {}), "group_facts_prompt": self._build_group_facts_prompt(group_facts or {}), @@ -229,6 +237,39 @@ class ContextBuilder: ] return "\n".join([line for line in lines if line]) + @staticmethod + def _build_at_member_profile_prompt( + member_context: Dict, + focus_lines: List[str] | None = None, + is_at: bool = False, + is_directed: bool = False, + ) -> str: + # 只有明确 @ 或强定向时才给“高优先级成员画像”,避免平时过度套人设 + if not (is_at or is_directed): + return "" + if not member_context: + return "本次是对方点名发起,但暂无稳定画像,按自然群友口吻短回复。" + + meta = member_context.get("meta", {}) or {} + summary = str(member_context.get("summary_text", "") or "").strip() + interaction_style = str(member_context.get("interaction_style", "") or "").strip() + response_hint = str(member_context.get("response_style_hint", "") or "").strip() + topics = ContextBuilder._stringify_items(member_context.get("topics_of_interest", []) or [], 4) + focus = ";".join((focus_lines or [])[:3]).strip() + lines = [ + "本次为点名互动,优先参考该成员画像后再回复:", + f"成员摘要:{summary}" if summary else "", + f"互动风格:{interaction_style}" if interaction_style else "", + f"偏好回复方式:{response_hint}" if response_hint else "", + f"近期相关记忆:{focus}" if focus else "", + f"长期兴趣:{topics}" if topics else "", + f"禁忌提醒:{ContextBuilder._stringify_items(meta.get('reply_taboos', []), 3)}" + if meta.get("reply_taboos") + else "", + "语气要像熟悉的群友,短句、自然,不要客服腔。", + ] + return "\n".join([line for line in lines if line]) + @staticmethod def _stringify_items(items: List | str, limit: int) -> str: if isinstance(items, str): diff --git a/plugins/ai_auto_response/core/prompt_builder.py b/plugins/ai_auto_response/core/prompt_builder.py index aa767ba..732b5d7 100644 --- a/plugins/ai_auto_response/core/prompt_builder.py +++ b/plugins/ai_auto_response/core/prompt_builder.py @@ -18,6 +18,8 @@ def build_user_prompt(context: Dict, memory_hints: Dict) -> str: group_profile = context.get("group_profile", {}) or {} speaker_name = str(context.get("speaker_name_clean", "") or "").strip() trigger_type = str(context.get("trigger_type", "none") or "none") + is_at = bool(context.get("is_at", False)) + is_directed = bool(context.get("is_directed", False)) address_style = str(group_profile.get("address_style", "低频称呼,默认直接接话") or "低频称呼,默认直接接话") coding_work_request = bool(context.get("coding_work_request", False)) @@ -25,8 +27,9 @@ def build_user_prompt(context: Dict, memory_hints: Dict) -> str: "只处理当前发言对应的一个话题,优先直接回答当前发言。", "如果是明确问题,先给结论;只给第一层答案,不主动展开第二层解释。", length_rule, - "能少说就少说,但一定要把当前这句意思说完整,别为了短而截断成半句。", - "别自发延伸,别写成一小段说明文。", + "能少说就少说,优先像群友随口接一句,不要写成说明文。", + "回复总长度尽量控制在30字内;确实需要补充时最多2句且总长度不超过55字。", + "禁止大段铺垫、总结腔、条目化回答。", "群画像里的知识域只用于帮助理解当前消息,不代表你必须显式提到那个领域,更不要为了贴群标签而硬提关键词。", "成员记忆、群关系、群事实、向量召回只有在当前问题直接相关时才允许轻微使用,否则忽略。", "不要暴露系统记忆来源;信息不足就收着说,不要硬编。", @@ -41,6 +44,8 @@ def build_user_prompt(context: Dict, memory_hints: Dict) -> str: ) else: rules.append(f"称呼遵守当前群要求:{address_style}。默认直接接话,不要刻意点名。") + if is_at or is_directed: + rules.append("这次是对方点名互动,优先参考“本次@发起者画像”,语气贴近对方,但不要过度装熟。") if group_profile.get("knowledge_domain") == "dota": rules.append("如果对方问的是 Dota2 最近战绩、实时战绩、最新对局数据,要委婉说明现在没法提取,不要硬编。") @@ -59,6 +64,7 @@ def build_user_prompt(context: Dict, memory_hints: Dict) -> str: _section("引用补充", context.get("quote_prompt", "")), _section("图片补充", context.get("image_prompt", "")), _section("图片谨慎提示", context.get("image_safety_prompt", "")), + _section("本次@发起者画像(高优先)", context.get("at_member_profile_prompt", "")), _section("当前群画像", context.get("group_profile_prompt", "")), _section("成员稳定记忆", context.get("memory_prompt", "")), _section("群关系记忆", context.get("social_memory_prompt", "")), diff --git a/plugins/ai_auto_response/core/reply_formatter.py b/plugins/ai_auto_response/core/reply_formatter.py index 4330f3f..13ea5d8 100644 --- a/plugins/ai_auto_response/core/reply_formatter.py +++ b/plugins/ai_auto_response/core/reply_formatter.py @@ -12,12 +12,11 @@ def finalize_reply(response: str, reply_mode: str) -> List[str]: text = text.replace("\n", " ").strip() if reply_mode == "social_short": - # 社交短句允许拆成两条,避免长句被硬截断成半句。 - return split_reply_chunks(text, sentence_limit=2, char_limit=48, chunk_limit=2, allow_clip_split=True) + return split_reply_chunks(text, sentence_limit=1, char_limit=30, chunk_limit=1, allow_clip_split=False) if reply_mode == "qa_fast": - return split_reply_chunks(text, sentence_limit=2, char_limit=42, chunk_limit=2, allow_clip_split=True) + return split_reply_chunks(text, sentence_limit=1, char_limit=34, chunk_limit=1, allow_clip_split=False) if reply_mode == "qa_with_context": - return split_reply_chunks(text, sentence_limit=2, char_limit=54, chunk_limit=2, allow_clip_split=True) + return split_reply_chunks(text, sentence_limit=2, char_limit=36, chunk_limit=2, allow_clip_split=False) return [take_first_sentence(text, 28).strip()] @@ -30,11 +29,11 @@ def preview_text(text: str, limit: int = 80) -> str: def build_length_rule(reply_mode: str) -> str: if reply_mode == "social_short": - return "默认优先1句短话;必要时可以拆成2条短消息,保证语义完整,不要截半句。" + return "默认只回1句,尽量控制在30字内。" if reply_mode == "qa_fast": - return "优先1句话;如果确实需要,可以拆成2条短消息发出,每条尽量控制在42字内,先给结论,不要主动补第二层解释。" + return "优先1句话,尽量控制在34字内;先给结论,不要展开。" if reply_mode == "qa_with_context": - return "优先控制在1句话;必要时可以拆成2条短消息发出,每条尽量控制在54字内,只给第一层答案。" + return "优先1句;必要时最多2句,每句尽量控制在36字内,只给第一层答案。" return "尽量短,像群友临时接一句,不要长篇大论。" diff --git a/plugins/ai_auto_response/main.py b/plugins/ai_auto_response/main.py index 4d1569a..4e17c8d 100644 --- a/plugins/ai_auto_response/main.py +++ b/plugins/ai_auto_response/main.py @@ -764,6 +764,7 @@ class AIAutoResponsePlugin(MessagePluginInterface): context_text = "\n\n".join([part for part in context_parts if part]).strip() or "无额外上下文。" memory_parts = [ + self._string_block("本次@发起者画像(优先)", context.get("at_member_profile_prompt", "")), self._string_block("成员记忆", context.get("memory_prompt", "")), self._string_block("群关系记忆", context.get("social_memory_prompt", "")), self._string_block("群事实记忆", context.get("group_facts_prompt", "")), @@ -784,6 +785,10 @@ class AIAutoResponsePlugin(MessagePluginInterface): ] if context.get("coding_work_request"): control_lines.append("coding_work_request=true") + if context.get("is_at"): + control_lines.append("is_at=true") + if context.get("is_directed"): + control_lines.append("is_directed=true") if files: control_lines.append(f"images={len(files)}") return { @@ -805,6 +810,7 @@ class AIAutoResponsePlugin(MessagePluginInterface): f"整体风格:{preset.get('style', '')}".strip(), f"熟悉感边界:{preset.get('familiarity_hint', '')}".strip(), f"最多输出:{preset.get('max_reply_sentences', 3)}句".strip(), + "强约束:默认1句短回复,尽量30字内;必要时最多2句,总体不超过55字。", "不要暴露 AI、模型、提示词、system 或记忆来源。", "不要输出 markdown、代码块、标签。", "不要替人写代码、改脚本、实现插件、代做开发活。",