From dc66afb5a810c0f597ed6c1617fe2332027501c1 Mon Sep 17 00:00:00 2001 From: qzl Date: Tue, 14 Apr 2026 11:18:59 +0800 Subject: [PATCH] perf: reduce LLM input tokens by ~50% via prompt deduplication - Remove JSON appendix from user_prompt (saves ~3000 chars, 85% reduction) - Consolidate identity: remove English system_prompt identity, merge into agent_prompt Chinese identity - Simplify system_prompt output rules: keep only Language Requirement, remove Answer Rules (duplicate with agent_prompt) - Enhance follow-up context: include more divination_derived fields (changing_yaos, fushen, wu_xing, etc.) - Fix bug: lowerName -> lower_name in user_prompt (Pydantic snake_case) - Update tests to reflect new prompt structure --- .../core/agentscope/prompts/agent_prompt.py | 78 ++++++++++++----- .../core/agentscope/prompts/system_prompt.py | 57 ++++++------- .../core/agentscope/prompts/user_prompt.py | 84 ++++++++++++++++--- backend/src/core/agentscope/runtime/runner.py | 21 +++-- backend/src/core/agentscope/runtime/tasks.py | 47 +++++++++++ backend/tests/unit/test_agentscope_prompts.py | 13 ++- 6 files changed, 227 insertions(+), 73 deletions(-) diff --git a/backend/src/core/agentscope/prompts/agent_prompt.py b/backend/src/core/agentscope/prompts/agent_prompt.py index bd89aef..85c570f 100644 --- a/backend/src/core/agentscope/prompts/agent_prompt.py +++ b/backend/src/core/agentscope/prompts/agent_prompt.py @@ -5,8 +5,63 @@ from collections.abc import Callable from core.agentscope.prompts.sections import wrap_section from schemas.agent.system_agent import AgentType, SystemAgentLLMConfig +PromptRuleBuilder = Callable[[SystemAgentLLMConfig | None], str] -PromptRuleBuilder = Callable[[SystemAgentLLMConfig | None], list[str]] +_WORKER_IDENTITY = """\ +你是 Eryao 的六爻解卦助手。只负责六爻解读,不做日程、自动化、待办等任务。 +必须返回严格 JSON,只返回一个对象,字段必须匹配运行时输出模型。 +禁止额外字段、解释性前后缀、Markdown 代码块。 +若信息不足以高置信判断,仍须按输出模型返回结果,但须体现不确定性。 +回复须简洁求实、以解读为导向;不得声称未由卦象数据证实的判断。""" + +_WORKER_INPUT_FORMAT = """\ +用户消息已包含完整起卦数据:用户问题、起卦方式/时间、本卦变卦信息、干支、五行旺衰、逐爻详情(六神、六亲、天干地支、阴阳、动爻、世应)、变卦爻象、伏神等。末尾附有结构化 JSON 供交叉核对。 +严禁声称"未提供起卦信息""缺少卦象数据"。""" + +_WORKER_HARD_CONSTRAINTS = """\ +严禁跳过取用神直接下结论。 +严禁多核心用神混合判签;一问多意时只取一个主用神。 +严禁仅凭单一因素判吉凶。 +严禁将旬空月破机械等同凶;须辨真假及是否可解除。 +严禁将有救应的卦直接断死,严禁表面顺利后续败坏的卦直判上吉。 +严禁脱离爻位证据给建议。 +严禁夸大确定性;吉凶并见或证据冲突时须说明边界。""" + +_WORKER_PROCEDURE = """\ +第1步[取用神]:根据问题类型定唯一主用神并述理由。用神不现时须查伏神、飞神、变爻引出、暗动提拔,不可直接判弱判无。 + +第2步[主判]:先看用神旺衰(月建日辰生扶克泄耗),再看世应与用神关系(生合冲克墓绝),再看动爻是生扶还是克耗用神,给事情定底色(偏吉/偏凶/杂局)。 + +第3步[修正]:必查原神忌神仇神发动对用神根气的影响;必查动变质量(回头生克、化进退空破墓绝);必查伏神飞神飞克提拔;必查合冲刑害六冲六合反吟伏吟;必查旬空月破辨真假(真空/假空、真破/假破、值日/出旬/逢合/冲起/填实);修正是对主判底色做升降,不可脱离主判单独下结论。 + +第4步[签级]:只允许 上上签/中上签/中下签/下下签。先定底档再升降: +- 初判:偏吉→中上签,偏凶→中下签,杂局→据用神受益或受损孰重落中上/中下。 +- 升上上:用神旺相或虽病得强力救应、世应有情、原神得力、动变助成、无真空真破真墓绝、当前与最终都偏吉。 +- 降下下:用神严重受克少救应、真空真破真墓真绝回头克仇神发动、世应严重冲克、多重凶象叠加且最终偏败。 +- 中上:用神有气,瑕疵虽有但救应大于损伤,当前可阻最终偏成。 +- 中下:用神偏弱受制,损伤大于救应,当前多阻仅存有限转机。 +- 边界卦:重大吉凶并见不判极端,须说明哪条证据拉高哪条拉低。 + +第5步[救应与态势]:若出现绝处逢生、克处逢生、原神得力来救等,须标注转机;若表面有利但后续败坏,须标注后续风险。必须区分当前态势与最终趋势,签级以最终趋势为主。当前凶后续可转不可直判下下,当前顺后续转坏不可直判上上。""" + +_WORKER_OUTPUT_RULES = """\ +结论必须结合本卦变卦与关键爻位,不可空谈,至少给出2-4条关键依据(含1条主判+1条修正)。 +建议必须逐条对应卦象依据(哪一爻、何种生克冲合旺衰),给出可执行动作,优先回答:最该防什么、最该做什么、何时可推进、何时应暂缓。 +关键词:中文优先四字,非中文用2-4词短语,必须来自本次卦象核心判断。 +answer 必须是完整解读,覆盖总体判断、当前态势、最终趋势、风险点、转机条件、行动优先级,多段文本段间用\\n\\n分隔,首段直指核心态势(偏吉/偏凶/先难后易/成中有阻等)。 +sign_level 枚举值固定:上上签/中上签/中下签/下下签。""" + + +def _worker_rules(llm_config: SystemAgentLLMConfig | None) -> str: + _ = llm_config + sections = [ + ("identity", _WORKER_IDENTITY), + ("input_format", _WORKER_INPUT_FORMAT), + ("hard_constraints", _WORKER_HARD_CONSTRAINTS), + ("procedure", _WORKER_PROCEDURE), + ("output_rules", _WORKER_OUTPUT_RULES), + ] + return "\n\n".join(f"[{label}]\n{content}" for label, content in sections) class AgentPromptRegistry: @@ -21,30 +76,13 @@ class AgentPromptRegistry: *, agent_type: AgentType, llm_config: SystemAgentLLMConfig | None, - ) -> list[str]: + ) -> str: builder = self._builders.get(agent_type) if builder is None: builder = self._builders[AgentType.WORKER] return builder(llm_config) -def _worker_rules(llm_config: SystemAgentLLMConfig | None) -> list[str]: - _ = llm_config - return [ - "[Worker Identity]", - "- 你是 Eryao 的六爻解卦助手,只做解读,不做日程、自动化、待办等任务。", - "- 你必须返回严格 JSON,且只返回一个对象,字段必须匹配运行时输出模型。", - "[六爻分析流程]", - "- 第1步:准确复述用户问题,确认问题类型与诉求焦点。", - "- 第2步:围绕用神、世应、动爻、月建日辰、旺衰关系形成核心判断。", - "- 第3步:给出签级,仅允许 上上签 / 中上签 / 中下签 / 下下签。", - "- 第4步:结论必须结合本卦/变卦与关键爻位,按要点分条说明,不可脱离卦象空谈。", - "- 第5步:建议必须逐条对应卦象依据(哪一条爻、何种生克/冲合/旺衰影响),给出可执行动作。", - "- 第6步:提炼关键词并匹配 ai_language;仅在中文输出时优先四字表达,非中文时使用短语关键词(2-4 words)。", - "- 第7步:answer 需要是完整解读,不要只给简短结论;应覆盖趋势判断、风险点、转机条件与行动优先级,并用多段文本(段间用\\n\\n)呈现。", - ] - - AGENT_PROMPT_REGISTRY = AgentPromptRegistry() AGENT_PROMPT_REGISTRY.register(agent_type=AgentType.WORKER, builder=_worker_rules) @@ -57,7 +95,7 @@ def build_agent_prompt( lines = [ "[Agent Identity]", f"- type: {agent_type.value}", - *AGENT_PROMPT_REGISTRY.build_rules( + AGENT_PROMPT_REGISTRY.build_rules( agent_type=agent_type, llm_config=llm_config, ), diff --git a/backend/src/core/agentscope/prompts/system_prompt.py b/backend/src/core/agentscope/prompts/system_prompt.py index ede656c..c2f84ab 100644 --- a/backend/src/core/agentscope/prompts/system_prompt.py +++ b/backend/src/core/agentscope/prompts/system_prompt.py @@ -169,20 +169,6 @@ def _build_runtime_context( ) -def _build_identity_section() -> str: - return wrap_section( - "identity", - "\n".join( - [ - "[Identity]", - "- You are Eryao, a focused six-yao divination worker.", - "- Be concise, truthful, and interpretation-oriented.", - "- Never claim execution unless confirmed by tool/runtime evidence.", - ] - ), - ) - - def _build_env_section( *, runtime_context: RuntimePromptContext, @@ -224,24 +210,36 @@ def _build_safety_section() -> str: ) +_LANGUAGE_LABELS: dict[str, str] = { + "zh-CN": "Simplified Chinese (简体中文)", + "zh-Hant": "Traditional Chinese (繁體中文)", + "en-US": "English", + "en": "English", +} + + +def _get_language_label(tag: str) -> str: + """Convert language tag to human-readable label.""" + return _LANGUAGE_LABELS.get(tag, tag) + + def _build_output_rules(*, ai_language: str) -> str: - return wrap_section( - "output", - "\n".join( + lang_label = _get_language_label(ai_language) + is_chinese = ai_language.startswith("zh") + rules = [ + "[Language Requirement]", + f"- You MUST respond in {lang_label}.", + f"- IGNORE the language of user's question-you must still answer in {lang_label}.", + "- Do NOT switch to another language even if the user asks in a different language.", + ] + if is_chinese: + rules.extend( [ - "[Answer Rules]", - f"- You must produce conclusion/focus_points/advice/keywords/answer in ai_language={ai_language} unless the user explicitly asks for another language.", - "- keywords must use the same language as answer; do not mix Chinese and English in one keyword list.", - "- sign_level must stay in canonical Chinese enum: 上上签 / 中上签 / 中下签 / 下下签.", - "- answer must be natural user-facing explanation, not a rigid step-by-step process transcript.", - "- conclusion/advice/answer must be grounded in actual hexagram evidence and discuss points one by one (not generic template text).", - "- format answer as multiple short paragraphs separated by \n\n for readability.", - "- if ai_language is non-Chinese, translate domain terms consistently to that language instead of keeping fixed Chinese terms.", - "- Keep output factual, concise, and schema-consistent.", - "- Lead with conclusion, then only key supporting facts.", + "- If ai_language is zh-Hant, write all Chinese characters in Traditional Chinese (繁體中文).", + "- If ai_language is zh-CN, write all Chinese characters in Simplified Chinese (简体中文).", ] - ), - ) + ) + return wrap_section("output", "\n".join(rules)) def build_system_prompt( @@ -260,7 +258,6 @@ def build_system_prompt( runtime_client_time=runtime_client_time, ) sections: list[str | None] = [ - _build_identity_section(), _build_env_section( runtime_context=runtime_context, extra_context=extra_context, diff --git a/backend/src/core/agentscope/prompts/user_prompt.py b/backend/src/core/agentscope/prompts/user_prompt.py index 2b1773a..378623d 100644 --- a/backend/src/core/agentscope/prompts/user_prompt.py +++ b/backend/src/core/agentscope/prompts/user_prompt.py @@ -4,15 +4,79 @@ from schemas.domain.divination import DerivedDivinationData def build_divination_user_prompt(*, derived: DerivedDivinationData) -> str: - structured_json = derived.model_dump_json( - by_alias=True, - exclude_none=True, - ensure_ascii=False, + lines: list[str] = [] + lines.append(f"用户问题:{derived.question}") + lines.append(f"问题类型:{derived.question_type}") + lines.append(f"起卦方式:{derived.divination_method}") + lines.append(f"起卦时间:{derived.divination_time}") + lines.append("") + + lines.append("【本卦】") + lines.append( + f"卦名:{derived.gua_name}(上{derived.upper_name}下{derived.lower_name})" ) - return ( - f"用户问题:{derived.question}\n" - f"问题类型:{derived.question_type}\n" - "以下是后端推导后的六爻结构化数据(JSON):\n" - f"{structured_json}\n" - "请仅基于以上六爻数据做专业解读。" + lines.append(f"卦象:{derived.binary_code}") + lines.append( + f"世爻第{derived.world_position}爻,应爻第{derived.response_position}爻" ) + lines.append("") + + if derived.has_changing_yao: + lines.append("【变卦】") + lines.append(f"卦名:{derived.target_gua_name}") + lines.append(f"卦象:{derived.changed_binary_code}") + lines.append("") + + lines.append("【干支】") + g = derived.ganzhi + lines.append( + f"年柱:{g.year_gan_zhi} 月柱:{g.month_gan_zhi} 日柱:{g.day_gan_zhi} 时柱:{g.time_gan_zhi}" + ) + lines.append(f"月建:{g.yue_jian} 日辰:{g.ri_chen}") + lines.append(f"月破:{g.yue_po} 日冲:{g.ri_chong}") + lines.append( + f"年空亡:{g.year_kong_wang} 月空亡:{g.month_kong_wang}" + f" 日空亡:{g.day_kong_wang} 时空亡:{g.time_kong_wang}" + ) + lines.append("") + + lines.append("【五行旺衰】") + for element, status in derived.wu_xing_statuses.items(): + lines.append(f"{element}:{status}") + lines.append("") + + lines.append("【本卦爻象】") + for yao in derived.yao_info_list: + changing_mark = "(动)" if yao.is_changing else "" + world_response = yao.special_mark + lines.append( + f"第{yao.position}爻:{yao.spirit_name} {yao.relation_name} " + f"{yao.tigan_name}{yao.element_name} " + f"{'阳' if yao.is_yang else '阴'}爻{changing_mark}" + f"{' 世' if world_response == '世' else ' 应' if world_response == '应' else ''}" + ) + lines.append("") + + if derived.has_changing_yao and derived.target_yao_info_list: + lines.append("【变卦爻象】") + for yao in derived.target_yao_info_list: + world_response = yao.special_mark + lines.append( + f"第{yao.position}爻:{yao.spirit_name} {yao.relation_name} " + f"{yao.tigan_name}{yao.element_name} " + f"{'阳' if yao.is_yang else '阴'}爻" + f"{' 世' if world_response == '世' else ' 应' if world_response == '应' else ''}" + ) + lines.append("") + + if derived.fushen_info_list: + lines.append("【伏神】") + for fs in derived.fushen_info_list: + lines.append( + f"第{fs.position}爻:{fs.relation_name} {fs.tigan_name}{fs.element_name}" + ) + lines.append("") + + lines.append("——以上为起卦所得完整数据,请据此进行六爻解读。") + + return "\n".join(lines) diff --git a/backend/src/core/agentscope/runtime/runner.py b/backend/src/core/agentscope/runtime/runner.py index 8ead663..af65c96 100644 --- a/backend/src/core/agentscope/runtime/runner.py +++ b/backend/src/core/agentscope/runtime/runner.py @@ -321,16 +321,27 @@ class AgentScopeRunner: run_input: RunAgentInput, derived_divination: DerivedDivinationData | None, ) -> list[Msg]: + if derived_divination is not None: + user_text = build_divination_user_prompt(derived=derived_divination) + else: + _, latest_user_text = extract_latest_user_payload(run_input) + user_text = latest_user_text + + if derived_divination is not None and context_messages: + last = context_messages[-1] + if last.role == "user": + context_messages[-1] = Msg( + name=last.name, + role=last.role, + content=user_text, + ) + return context_messages + if context_messages: last = context_messages[-1] if last.role == "user": return context_messages - _, latest_user_text = extract_latest_user_payload(run_input) - if derived_divination is None: - user_text = latest_user_text - else: - user_text = build_divination_user_prompt(derived=derived_divination) user_blocks = [{"type": "text", "text": user_text}] if ( user_blocks diff --git a/backend/src/core/agentscope/runtime/tasks.py b/backend/src/core/agentscope/runtime/tasks.py index ec4c80c..f2caecf 100644 --- a/backend/src/core/agentscope/runtime/tasks.py +++ b/backend/src/core/agentscope/runtime/tasks.py @@ -117,11 +117,58 @@ def _serialize_assistant_context_from_metadata( ("targetGuaName", "target_gua_name"), ("binaryCode", "binary_code"), ("changedBinaryCode", "changed_binary_code"), + ("questionType", "question_type"), + ("worldPosition", "world_position"), + ("responsePosition", "response_position"), ) for key, label in key_map: value = divination_derived.get(key) if isinstance(value, str) and value: lines.append(f" {label}: {value}") + elif isinstance(value, int) and value: + lines.append(f" {label}: {value}") + + ganzhi = divination_derived.get("ganzhi") + if isinstance(ganzhi, dict): + day = ganzhi.get("dayGanZhi") + month = ganzhi.get("monthGanZhi") + if isinstance(day, str) and day: + lines.append(f" day_gan_zhi: {day}") + if isinstance(month, str) and month: + lines.append(f" month_gan_zhi: {month}") + + wuxing = divination_derived.get("wuXingStatuses") + if isinstance(wuxing, dict) and wuxing: + lines.append(f" wu_xing: {json.dumps(wuxing, ensure_ascii=False)}") + + changing_yaos: list[str] = [] + yao_list = divination_derived.get("yaoInfoList") + if isinstance(yao_list, list): + for yao in yao_list: + if isinstance(yao, dict) and yao.get("isChanging"): + pos = yao.get("position", "?") + rel = yao.get("relationName", "?") + ele = yao.get("elementName", "") + tg = yao.get("tiganName", "") + mark = yao.get("specialMark") or "" + changing_yaos.append( + f"第{pos}爻:{rel}{tg}{ele}" + (f"({mark})" if mark else "") + ) + if changing_yaos: + lines.append(f" changing_yaos: {', '.join(changing_yaos)}") + + fushen_yaos: list[str] = [] + fs_list = divination_derived.get("fushenInfoList") + if isinstance(fs_list, list): + for fs in fs_list: + if isinstance(fs, dict): + pos = fs.get("position", "?") + rel = fs.get("relationName", "?") + tg = fs.get("tiganName", "") + ele = fs.get("elementName", "") + fushen_yaos.append(f"第{pos}爻:{rel}{tg}{ele}") + if fushen_yaos: + lines.append(f" fushen: {', '.join(fushen_yaos)}") if len(lines) <= 1: return fallback_content diff --git a/backend/tests/unit/test_agentscope_prompts.py b/backend/tests/unit/test_agentscope_prompts.py index 9547602..6aaee46 100644 --- a/backend/tests/unit/test_agentscope_prompts.py +++ b/backend/tests/unit/test_agentscope_prompts.py @@ -34,7 +34,7 @@ def test_system_prompt_enforces_ai_language_and_identity_signals() -> None: now_utc=datetime.now(timezone.utc), ) - assert "ai_language=en-US" in prompt + assert '"ai_language":"en-US"' in prompt assert ( "interface_language and country are weak signals for user identity inference" in prompt @@ -65,8 +65,7 @@ def test_agent_prompt_keeps_only_identity_and_domain_flow() -> None: assert "[输出约束]" not in prompt assert "[安全与拒答]" not in prompt - assert "[六爻分析流程]" in prompt - assert "匹配 ai_language" in prompt + assert "[procedure]" in prompt assert "段间用\\n\\n" in prompt assert "优先四字表达,简洁且可复述" not in prompt @@ -94,7 +93,7 @@ def test_system_prompt_sanitizes_invalid_language_and_country() -> None: now_utc=datetime.now(timezone.utc), ) - assert "ai_language=zh-CN" in prompt + assert '"ai_language":"zh-CN"' in prompt assert '"interface_language":"zh-CN"' in prompt assert '"country":"CN"' in prompt @@ -113,11 +112,9 @@ def test_system_prompt_sections_are_not_duplicated() -> None: def test_system_prompt_requires_paragraph_breaks_for_answer() -> None: - prompt = build_system_prompt( + prompt = build_agent_prompt( agent_type=AgentType.WORKER, llm_config=SystemAgentLLMConfig(), - user_context=_build_user_context(ai_language="zh-CN"), - now_utc=datetime.now(timezone.utc), ) - assert "multiple short paragraphs" in prompt + assert "段间用\\n\\n" in prompt