feat: complete heaven readings and screener publication

This commit is contained in:
leefer
2026-08-05 23:44:17 +08:00
parent 6a058c2929
commit b3a21d05b7
34 changed files with 3538 additions and 225 deletions
+196 -19
View File
@@ -1,6 +1,7 @@
from __future__ import annotations
import json
import re
from typing import Any
from backend.llm import transport as llm_transport
@@ -10,6 +11,13 @@ class HeavenAgentError(RuntimeError):
pass
HEAVEN_PROMPT_VERSIONS = {
"trend": "heaven-trend-v4",
"fortune": "heaven-fortune-v9",
"heart": "heaven-heart-v5",
}
def interpret_heaven(
mode: str,
context: dict[str, Any],
@@ -42,6 +50,34 @@ def interpret_heaven(
answer = str(result.content).strip()
if not answer:
raise KeyError("empty response")
try:
_validate_answer(mode, answer, context)
except HeavenAgentError as validation_error:
repair_messages = [
*messages,
{"role": "assistant", "content": answer},
{
"role": "user",
"content": (
f"上一版未通过本地一致性校验:{validation_error}"
"请依据最初输入完整重写最终答案,只修正违规推断并补齐必答项。"
"不得讨论校验、提示词或重写过程,只输出新的正式解读。"
),
},
]
repaired = llm_transport.chat_completion(
api_key=api_key,
base_url=base_url,
model=model,
messages=repair_messages,
timeout=timeout,
user_agent="XiaobaiReviewWeb/0.7",
)
answer = str(repaired.content).strip()
if not answer:
raise KeyError("empty repaired response")
_validate_answer(mode, answer, context)
result = repaired
except llm_transport.OpenAIHTTPError as exc:
raise HeavenAgentError(exc.describe("问天模型调用失败")) from exc
except (llm_transport.OpenAITransportError, KeyError) as exc:
@@ -55,34 +91,175 @@ def interpret_heaven(
def _system_prompt(mode: str) -> str:
common = """
你是“小白复盘”的问天解读器。所有历法、卦象、爻位和市场指标已经由确定性程序计算,你只能解释提供的数据,不得改卦、改爻、改干支或编造行情
问天属于传统文化与娱乐化观察,不是预测模型,不承诺应验,不输出无条件买卖指令,不用神秘话术制造确定性
使用中文,先给核心判断,再解释结构。引用市场数字时标明数据日期。输出纯文本,可使用简短标题
你是“小白复盘”的问天解读器。输入由calculation、knowledge和interpretation_contract组成:calculation是确定性程序已经算出的事实;knowledge是本次按条件精确检索到的原典、传统规则和产品边界;interpretation_contract规定本次必须回答与禁止推断的内容
只能综合输入中已经提供的事实和知识。不得改卦、改爻、改纳甲、改世应、改干支、重新计算五运六气,也不得凭模型记忆补造缺失字段。知识记录之间若存在张力,应说明条件与分歧,不要强行合成唯一结论
问天属于传统文化与自我观察,不是可验证的行情预测模型。必须给出有内容的倾向和依据,但不得把象义宣布为必然发生的股价结果,不输出无条件买卖指令,不用神秘话术制造确定性
使用中文和普通用户能够理解的表达。专业术语首次出现时紧接一句白话解释。先给核心判断,再说明证据和变化关系。每个主题必须使用独立一行的简短标题,格式为“## 标题”,标题后另起一段正文;不得把全部内容挤在一个长段落中。可以使用Markdown加粗,不使用Markdown表格。
""".strip()
if mode == "trend":
return common + """
当前任务是“观势·解势”。六爻从初爻到上爻依次是个股内核、个股外显、板块内核、板块外显、指数内核、指数外显;初二为地、三四为人、五上为天
行情数据只负责生成六爻,本次解势必须以卦象本身为主,不得根据指数涨跌、板块强弱、涨停家数、成交量或个股表现直接推演方向。context中不会提供这些数字,也不会提供爻位对应的市场角色
先解释本卦卦名的核心义、上下卦组合及大象;再只解释实际动爻所代表的转折,并说明本卦如何走向之卦;最后可把这一组卦势翻译成克制的市场语言
重点是“本卦为当下之势,动爻为变化关节,之卦为所趋之势”。不要说明某一动爻对应指数、板块或个股,也不要输出“一看指数、二看涨停家数”一类行情观察条件
全文控制在300至450个中文字符,最多四小段。卦理约占九成,市场翻译最多一句,只能落到节制、等待、守信、辨伪等行为态度,不得据此预测市场下一阶段、涨跌方向或动能变化。不直接荐股,不使用Markdown表格
不要使用“必然、确定、必涨、必跌、后续将、进入某阶段”等断语;天机只点出势的性质与变化关系,不替用户宣布结果。
当前任务是“观势·解势”。行情只负责在进入模型之前生成卦象,本次回答不得引用或反推指数涨跌、成交额、涨跌停、板块强弱或个股表现,也不得说明某一爻原先对应哪类市场指标
calculation中有意不提供股票、行业和板块身份。不得猜测或讨论观察对象所属行业、政策、消费环境、基本面、资金面或任何现实市场变量;只解释已经生成的卦象
必须明确给出卦义上的当下倾向、主要矛盾、实际动爻所的转折,以及本卦走向之卦后的变化方向。允许使用偏进、偏守、先难后易、由盛转收、转机有限、内外相违或结论有条件等相对判断;不得只罗列卦辞,也不得用“谨慎、等待、守信、辨伪”一类泛化劝诫代替解势
以knowledge中本卦、上下卦、卦辞、彖义、大象、实际动爻和之卦记录为依据。无动爻、一动爻和多动爻分别服从本次检索到的方法规则;多动爻有冲突时必须指出冲突,不得压成单一套话
按“## 核心判断、## 卦势依据、## 动爻转折、## 之卦趋向、## 决策映射”组织答案;无动爻时仍保留“动爻转折”,明确说明本次无动爻并解释结构的延续条件。结尾可以把卦势翻译成克制的交易决策语言,但只能表达条件、节奏和需要验证的矛盾,不得预测具体涨跌、价格、日期或给出直接荐股结论。篇幅随动爻数量自然展开,不设置固定字数
""".strip()
if mode == "fortune":
return common + """
当前任务是“观气·解运”。严格区分五运、六气、节气、月令和日干,不把丙午简单解释为火年
严格服从five_phase_field.framework提供的确定性结构,不自行重新计算五行:年纲由中运司天在泉构成;岁半以前司天为主、在泉为辅,岁半以后在泉为主、司天为辅;当前六气层以客气加临主气为核心;日辰只负责触发。节气只用于定位当前六气阶段,不得再次叠加为独立力量
重点解释framework.relations中的客主同气、客生主、主生客、客克主或主克客,以及客胜为从、主胜为逆、司天在泉同位、天符岁会等已经判定的关系。不得把司天、在泉、主气、客气视为彼此独立的证据重复计权,也不得自行增删传统格局
首要解释当日气场容易放大参与者的哪些情绪、判断偏差和操作冲动,例如急躁、恐惧、迟疑、追涨、过早止损或路径依赖;再给出一至两个调节动作
如有personal_profile,结合其日主、十神、五行平衡倾向说明当日对该用户主观状态的影响,但不得把简化平衡倾向说成唯一喜用神,也不得复述或猜测出生日期
不得引用市场上涨下跌家数、涨跌停数量、成交额、板块强度或个股表现来证明气场。industry_affinity只是五行行业取象示例,不是行情旁证;行业契合度最多在末尾用一句话说明,不得写“当日共振”或暗示相关行业必然涨跌
全文控制在420至600个中文字符,按“三层气机、人的状态、操作偏向、个人影响(如有)、制衡动作”组织,标题必须写“三层气机”。明确这些是传统历法框架下的观察语言,不宣称气候或五行直接导致股价
当前任务是“观气·解运”。页面用于直观展示的五行权重、主导元素和预制复合断语已明确排除,不得自行恢复这些结果,也不得按百分比重新生成单一五行结论
严格区分中运司天在泉、当前主气客气、节气定位和日辰触发。先解释中运与司天在泉构成的年纲,再解释客气加临主气的当前关系,最后说明日辰如何触发;不得把同一项拆成多份证据重复计权。相生不直接等于吉,相克不直接等于凶
必须使用knowledge中与本日中运、六气和客主关系精确匹配的记录。可形成“湿热交蒸、燥中夹滞”一类复合表达,但要从输入关系逐层说明,不能从页面权重结论倒推
如calculation.personal存在,只结合日主、十神和当日派生关系说明用户容易出现的主观感受与判断偏差;不得使用简化强弱、喜用神、出生日期或权重平衡结论
personal.natal_day_master才是用户本命日主;today_relative_to_natal_day_master.pillars是当日历法,不是用户出生四柱。stem_relations只是当日年、月、日三柱天干相对本命日主的程序结果,只能使用knowledge中本次命中的关系释义,不得自行重算十神或扩展五行生克过程。不得使用藏干、支中藏某干、某支为某库或燥湿属性等输入未提供的信息,也不得把当日日柱写成用户命局,或推断用户命局中某个十神“较重”、身强身弱或喜用神
日辰只按calculation.day_trigger.summary与knowledge中的日辰边界解释,不得从干支另行推导藏气、库气或五行生克链。个人合参不得宣称本命日主被当日某气生扶、泄耗或克制,只能说明已给关系标签可能对应的主观注意点
day_trigger.summary中的日干运势、地支五行和六气对应是三个并列的确定性事实。不得把日柱整体改写成某一种五行,也不得把地支与六气的“对应”改写成地支自身具有某种六气属性
calculation.industry_symbols只提供五行与行业的传统取象归类及其本次出现依据。必须说明这些气机对相关行业可能形成的象征性关注、节奏或约束,但不得引入行业实时行情,不得预测行业涨跌或把取象写成投资推荐;未列入industry_symbols的行业不得自行补造。
按“## 年纲、## 客主加临、## 日辰触发、## 行业影响、## 个人合参、## 制衡动作”组织答案;没有个人资料时可以省略“个人合参”。不得用行情上涨下跌、行业表现或个股结果证明运气关系。篇幅按实际关系自然展开,不设置固定字数。
""".strip()
return common + """
当前任务是“观心·解卦”。用户的问题始终只在心中,没有输入给你,因此你不能猜测问题内容,也不能替用户作具体决定
全文控制在180至350个中文字符。只写一句卦意;一小段动爻与之卦;最后三句极短的问心句
不要重述六条爻辞,不猜用户未说出口的问题,不以吉凶二字替代思考,不给出股票涨跌预测。语气安静、克制,越短越有余味
当前任务是“观心·解卦”。用户在起卦前确定的问题位于calculation.questionquestion_preset只说明问题来源。必须针对实际问题作答;无题观心时不得猜测用户没有说出的事项
question_scope是本次问题的产品边界。trade预设专指股票交易中的参与条件、机会、阻碍和风险,不是商业合作、融资、借贷或寻找资金方;除非用户问题明确写出这些背景,否则不得擅自补入
纳甲、卦宫、世应、六亲、六神、月建日辰、旬空、伏神、动变和冲合关系已经由确定性程序给出。只能解释这些结果,不得自行改排盘、补用神或用模型记忆重算。六神只作辅助,任何单项都不能独立决定结论
六亲是关系类别,不是现实人物或资金来源的一一映射。必须使用knowledge中的六亲、旬空、动变和六神边界;不得把妻财直接写成现金或融资,把子孙写成资金提供方,把兄弟写成合作方,也不得由某一六神直接推出紧迫、欺骗或吉凶。
除非calculation.question明确说明用户已经持仓、买入、卖出或正在管理仓位,否则不得假定用户已经入场,不得使用“持仓、仓位、建仓、入场、持有、买入、卖出、止损、止盈”等措辞描述用户现状。可以只写尚待核对的参与条件、风险边界和决策倾向。
除非问题明确涉及融资、借贷、合作或资源安排,否则不得制造外围资金、外围资源、资金进入、资源进入,也不得虚构资金或资源的来源、提供、注入、安排和路径。
按“## 所问之答、## 卦象依据、## 动变与之卦、## 可验证之处”组织答案。先直接回应所问,再用白话解释本卦所示处境、世应与相关六亲、关键动爻和变爻,最后说明之卦趋向及一项可以由用户验证的动作。若证据相互冲突,应明确说明结论成立的条件,不以“吉、凶”二字替代推理。
交易问题可以判断参与条件、内外阻碍、风险和决策倾向,但不得宣告具体股价、涨跌日期或替用户作无条件买卖决定。不得用旬空、填实、出空或干支日推算“未来几日”或某日应验;可验证动作必须是用户当下能核对的交易条件或自身判断,不能制造现实中不存在的合作方、承诺、资金或资源安排。心境问题聚焦念头、压力和盲点;无题观心只作一般卦象观照。篇幅随问题和动爻复杂度自然展开,不使用固定三句模板,也不得输出使用竖线分栏的Markdown表格。
""".strip()
def _validate_answer(
mode: str, answer: str, context: dict[str, Any] | None = None
) -> None:
compact = "".join(answer.split())
if len(compact) < 60:
raise HeavenAgentError("问天模型返回内容过短,未形成有效解读。")
forbidden = ("必涨", "必跌", "保证上涨", "保证下跌", "无条件买入", "无条件卖出")
if any(term in answer for term in forbidden):
raise HeavenAgentError("问天模型返回了禁止的确定性行情断语。")
if mode == "fortune" and "%" in answer:
raise HeavenAgentError("解运结果错误引用了已排除的权重百分比。")
if mode == "trend":
market_narratives = (
"行业", "板块", "个股", "指数", "成交额", "涨停", "跌停",
"政策", "消费环境", "基本面", "资金面",
)
if any(term in answer for term in market_narratives):
raise HeavenAgentError("解势结果错误引入了卦象之外的现实市场叙事。")
if mode == "fortune":
if re.search(
r"(?:命局|个人本身).{0,16}(?:偏重|较重|过旺|过弱|身强|身弱|喜用神)",
answer,
):
raise HeavenAgentError("解运结果错误推断了输入中不存在的命局强弱。")
if re.search(
r"藏干|[子丑寅卯辰巳午未申酉戌亥](?:中|内)|[子丑寅卯辰巳午未申酉戌亥].{0,4}(?:含|藏)|(?:中|内)藏|余气|[辰戌丑未].{0,4}(?:火库|水库|金库|木库|土库|燥土|湿土)",
answer,
):
raise HeavenAgentError("解运结果使用了输入中未提供的藏干推断。")
if re.search(
r"木生火|火生土|土生金|金生水|水生木|木克土|土克水|水克火|火克金|金克木",
answer,
):
raise HeavenAgentError("解运结果自行扩展了输入中未提供的五行生克链。")
if re.search(
r"(?:本命)?日主.{0,32}(?:生扶|泄耗|受克|被克|得生|被生|偏强|偏弱)",
answer,
):
raise HeavenAgentError("解运结果把当日关系错误扩展成了本命强弱推断。")
if re.search(
r"(?:日柱)?[甲乙丙丁戊己庚辛壬癸][子丑寅卯辰巳午未申酉戌亥]"
r".{0,8}(?:本身|自身)(?:就)?是[木火土金水]",
answer,
):
raise HeavenAgentError("解运结果错误地把整个日柱归成了单一五行。")
if re.search(
r"[子丑寅卯辰巳午未申酉戌亥](?:的|具有|带有).{0,8}"
r"(?:风木|君火|湿土|相火|燥金|寒水)(?:之)?(?:属性|性质)",
answer,
):
raise HeavenAgentError("解运结果把六气对应误写成了地支自身属性。")
calculation = (context or {}).get("calculation") or {}
if calculation.get("industry_symbols") and "行业影响" not in answer:
raise HeavenAgentError("解运结果遗漏了本次必答的行业影响。")
if re.search(
r"行业.{0,16}(?:必涨|必跌|必然上涨|必然下跌|确定领涨|确定领跌|投资推荐)",
answer,
):
raise HeavenAgentError("解运结果把行业取象错误写成了行情预测或投资推荐。")
personal = calculation.get("personal") or {}
personal_today = personal.get("today_relative_to_natal_day_master") or {}
pillar_values = {
str(value)
for group in (calculation.get("pillars") or {}, personal_today.get("pillars") or {})
for value in group.values()
if value
}
mentioned_pillars = set(
re.findall(r"[甲乙丙丁戊己庚辛壬癸][子丑寅卯辰巳午未申酉戌亥]", answer)
)
if mentioned_pillars - pillar_values:
raise HeavenAgentError("解运结果补入了确定性输入中不存在的干支。")
month_pillar = str((calculation.get("pillars") or {}).get("month") or "")
month_branch = month_pillar[1:2]
mentioned_month_branches = set(
re.findall(r"([子丑寅卯辰巳午未申酉戌亥])月", answer)
)
if mentioned_month_branches - ({month_branch} if month_branch else set()):
raise HeavenAgentError("解运结果补入了当前月份之外的地支月。")
if personal:
relations = {
str(value)
for value in (personal_today.get("stem_relations") or {}).values()
if value
}
if "个人合参" not in answer and "本命日主" not in answer:
raise HeavenAgentError("解运结果遗漏了本次必答的个人合参。")
if relations and not any(relation in answer for relation in relations):
raise HeavenAgentError("解运结果未使用程序提供的当日关系标签。")
if mode != "heart":
return
calculation = (context or {}).get("calculation") or {}
question = str(calculation.get("question") or "")
if _contains_markdown_table(answer):
raise HeavenAgentError("解卦结果错误输出了Markdown表格。")
position_terms = (
"持仓", "仓位", "建仓", "入场", "持有", "买入", "卖出", "止损", "止盈",
)
if not any(term in question for term in position_terms) and any(
term in answer for term in position_terms
):
raise HeavenAgentError("解卦结果擅自假定了用户的持仓或买卖状态。")
financing_terms = (
"融资", "借贷", "合作", "出资", "资金来源", "资金方", "投资人", "投资方",
"外部资金", "外围资金", "外部资源", "外围资源",
)
invented_scenarios = (
"融资", "借贷", "合作方", "资金提供方", "资金意向", "资金注入", "自有资金",
"外围资金", "外围资源", "资金进入", "资源进入",
)
invented_resource_path = re.search(
r"(?:资金|资源).{0,8}(?:来源|提供|注入|安排|路径)", answer
)
if not any(term in question for term in financing_terms) and (
any(term in answer for term in invented_scenarios) or invented_resource_path
):
raise HeavenAgentError("解卦结果擅自补入了用户没有提出的融资或合作场景。")
timing_patterns = (
r"未来\s*[一二三四五六七八九十\d]+\s*(?:个)?(?:交易)?日",
r"[子丑寅卯辰巳午未申酉戌亥]{1,2}日(?:到来|来临|之前|之后|前后)",
r"(?:等待|等到|待).{0,16}(?:旬空|空亡).{0,16}(?:填实|出空)",
)
if any(re.search(pattern, answer) for pattern in timing_patterns):
raise HeavenAgentError("解卦结果错误使用旬空或干支推算了具体应期。")
def _contains_markdown_table(answer: str) -> bool:
return bool(
re.search(r"(?m)^\s*\|", answer)
or re.search(r"(?m)^\s*:?-{3,}:?\s*\|", answer)
or re.search(r"(?m)\|\s*:?-{3,}:?\s*(?:\||$)", answer)
)