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
This commit is contained in:
qzl
2026-04-14 11:18:59 +08:00
parent 6bc9c88ce8
commit dc66afb5a8
6 changed files with 227 additions and 73 deletions
@@ -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,
),
@@ -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(
[
"[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.",
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(
[
"- 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,
@@ -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)
+16 -5
View File
@@ -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
@@ -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
@@ -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