"""记忆语义键的受控词表与显式信号识别。 触发判定(`MemoryService.should_extract_memory`)与模型抽取校验 (`MemoryExtractionService`)共用同一份定义,避免出现"能触发但写不进"或 "写进去但召回时无人认识"的错位。词表只描述持久事实/偏好/约束/目标, 不含会话流水信息,因此不需要新增库表或库列。 """ DEFAULT_MEMORY_KEYS: frozenset[str] = frozenset({ "preference:risk_level", "preference:horizon", "preference:product_type", "preference:communication", "constraint:liquidity", "constraint:loss_tolerance", "constraint:exclusion", "profile:occupation", "profile:family", "profile:income_stability", "profile:investment_experience", "goal:target", "goal:retirement", }) # memory_unit.memory_type 是 String(24),取值必须落在该集合内,与键前缀同构。 MEMORY_TYPES: frozenset[str] = frozenset({ "preference", "constraint", "profile", "goal", "fact", }) # 只有这些业务事件本身构成持久事实,普通运行事件不得触发记忆写入。 BUSINESS_EVENT_TYPES: frozenset[str] = frozenset({ "risk.assessment_completed", "suitability.assessment_completed", "trade.completed", }) # 受控键 -> 用户明确陈述时使用的表述。模式必须足够具体:像"我想买基金"这类 # 意图、以及任何普通问答都不应命中,否则记忆会被会话噪音污染。 SIGNAL_PATTERNS: dict[str, tuple[str, ...]] = { "preference:risk_level": ( "风险偏好", "风险承受", "风险等级", "稳健型", "保守型", "激进型", "平衡型", "低风险", "中风险", "中高风险", "高风险", ), "preference:horizon": ( "投资期限", "投资周期", "期限是", "长期持有", "短期持有", "长期投资", "短期投资", "年内不用", "三年内不用", "不赎回", "打算持有", "计划持有", "持有几年", ), # 不得使用宽泛的"偏好":它会与"风险偏好"重叠,导致从风险偏好陈述里误抽出 # 产品类型偏好。产品类型只认明确列举或"只买/只投"这类排他表述。 "preference:product_type": ( "只买", "只投", "只做", "倾向买", "倾向于买", "更喜欢买", "偏好股票", "偏好债券", "偏好货币", "偏好指数", ), "preference:communication": ( "沟通方式", "联系我", "推送", "短信通知", ), "constraint:liquidity": ( "流动性", "随时赎回", "随时取", "不能锁定", "急用钱", ), "constraint:loss_tolerance": ( "不能亏", "不想亏", "怕亏", "亏损承受", "最大回撤", "回撤", "不接受亏损", "接受不了亏损", "不能接受亏损", "受不了亏损", ), "constraint:exclusion": ( "不买", "不投", "别推荐", "不要推荐", "禁止", ), "profile:occupation": ( "我从事", "我的职业", "职业是", "我是做", ), "profile:family": ( "已婚", "未婚", "结婚", "有孩子", "孩子", "子女", "配偶", "家庭负担", "赡养", "家庭", "养家", ), "profile:income_stability": ( "收入稳定", "月收入", "工资", "收入来源", ), "profile:investment_experience": ( "投资经验", "新手", "第一次买基金", "做过股票", ), # 去掉裸"打算":它在"我打算三年内不赎回"这类期限陈述里不构成目标收益目标。 "goal:target": ( "我的目标是", "计划在", "打算买", "打算投", "攒够", "目标收益", "买房", "留学", ), "goal:retirement": ( "养老", "退休", ), } # 抽取结果必须落在词表内;配置中心可在 default 之外追加受控键。 MEMORY_KEY_NAMESPACE = "memory" MEMORY_KEY_CONFIG_KEY = "vocabulary" MEMORY_KEY_FIELD = "memory_keys" def detect_memory_signals(content: str) -> tuple[str, ...]: """识别消息中明确陈述的持久事实/偏好,返回命中的受控键(按词表顺序稳定输出)。""" normalized = content.strip() if not normalized: return () return tuple( key for key, patterns in SIGNAL_PATTERNS.items() if any(pattern in normalized for pattern in patterns) )