diff --git a/api.py b/api.py index 21b8124..ab505c2 100644 --- a/api.py +++ b/api.py @@ -18,7 +18,7 @@ from bossai import ( ExpertStore, ConsultationStore, RatingStore, DocumentGenerator, RegulationUpdateManager, FactFinder, ChatStore, redact, - DailyTopicStore, generate_daily_topics, filter_boss_perspective, + DailyTopicStore, generate_daily_topics, filter_boss_perspective, filter_by_scene, ) # ===== 日志脱敏 ===== @@ -461,10 +461,14 @@ def generate_samples(req: SampleRequest): return {"samples": builtin.get(req.scene, builtin["legal"])} scene_config = { - "legal": {"desc": "法律/法务", "focus": "劳动用工、合同审查、债权债务、股东纠纷、税务风险、诉讼仲裁等法律问题"}, - "decision": {"desc": "经营决策", "focus": "扩张收缩、定价策略、招人裁员、融资合伙、项目承接、业务转型等经营决策"}, - "life": {"desc": "生活助理", "focus": "家庭沟通、子女教育、父母养老、睡眠压力、出差日程、健康习惯等个人生活问题,不是企业经营问题"}, - "emotion": {"desc": "情绪疏导", "focus": "压力、焦虑、愤怒、拖延、疲惫等情绪状态,老板可能倾诉的心理感受"}, + "legal": {"desc": "法律/法务", "focus": "劳动用工、合同审查、债权债务、股东纠纷、税务风险、诉讼仲裁等法律问题", + "boundary": "只关注法律/法务问题,不要生成纯经营策略、纯情绪疏导、纯生活问题"}, + "decision": {"desc": "经营决策", "focus": "扩张收缩、定价策略、招人裁员、融资合伙、项目承接、业务转型等经营决策", + "boundary": "只关注经营决策问题。不要生成纯法律问题(如合同违约金怎么约定)、纯情绪问题(如焦虑失眠、自我怀疑、凌晨怀疑人生)、纯生活问题。情绪相关的决策问题可以,但必须是'该怎么做'而非'我很难受'"}, + "life": {"desc": "生活助理", "focus": "家庭沟通、子女教育、父母养老、睡眠压力、出差日程、健康习惯等个人生活问题,不是企业经营问题", + "boundary": "只关注个人生活问题,不要生成纯法律问题、纯经营决策问题、纯情绪疏导问题"}, + "emotion": {"desc": "情绪疏导", "focus": "压力、焦虑、愤怒、拖延、疲惫等情绪状态,老板可能倾诉的心理感受", + "boundary": "关注情绪与心理疏导,核心是情绪感受和心理状态,不是经营策略或法律方案"}, } config = scene_config.get(req.scene, scene_config["legal"]) @@ -473,6 +477,7 @@ def generate_samples(req: SampleRequest): f"你是{config['desc']}领域的专业顾问。请生成 4 个该领域的典型问题," f"用户(中小企业老板)可能会问的问题。\n\n" f"问题方向:{config['focus']}\n\n" + f"场景边界:{config['boundary']}\n\n" ) if req.topic: prompt += f"具体话题:{req.topic}\n\n" @@ -538,9 +543,11 @@ def generate_samples(req: SampleRequest): if start == -1 or end == -1: return {"samples": builtin.get(req.scene, builtin["legal"])} samples = json.loads(answer[start:end + 1]) - # 过滤:老板视角 + 非字符串 + 去重 - samples = filter_boss_perspective( - [s for s in samples if isinstance(s, str) and s.strip()] + # 过滤:老板视角 + 场景归属 + 非字符串 + samples = filter_by_scene( + filter_boss_perspective( + [s for s in samples if isinstance(s, str) and s.strip()] + ), req.scene )[:4] if len(samples) < 2: return {"samples": builtin.get(req.scene, builtin["legal"])} diff --git a/bossai.py b/bossai.py index d6d6c47..1d44be1 100644 --- a/bossai.py +++ b/bossai.py @@ -3349,6 +3349,87 @@ def filter_boss_perspective(questions: list[str]) -> list[str]: return valid +# ===== 场景过滤:防止问题跨场景混淆 ===== + +# 各场景的归属关键词:问题包含这些关键词时,应归属对应场景 +SCENE_KEYWORDS = { + "emotion": [ + "睡不着", "失眠", "焦虑", "抑郁", "崩溃", "自我怀疑", "自我贬低", + "撑不下去", "孤独", "愤怒", "委屈", "难受", "压力很大", "心力交瘁", + "怀疑人生", "怀疑自己", "不想干了", "想放弃", "绝望", "恐惧", + "情绪", "心态", "心理", "疏导", "陪伴", "倾诉", + "凌晨三点怀疑", "破PPT", "怀疑这", "贬低自己", + "吵架后", "吵了一架", "背叛了我", "背叛感", + "害怕做决定", "下不了手", "拖延症", "就是拖延", + ], + "legal": [ + "仲裁", "诉讼", "起诉", "合同", "违约", "欠款", "催收", + "税务", "发票", "社保", "工伤", "辞退", "开除员工", + "股东", "股权", "分红", "退股", "合伙人撤资", + "法规", "法律", "合规", "稽查", "罚款", + "知识产权", "商标", "专利", "侵权", + "借条", "担保", "抵押", "保全", "执行", + ], + "decision": [ + "扩张", "开分店", "开第二家", "裁员", "招人", "融资", + "定价", "涨价", "降价", "促销", "备货", + "业务线", "转型", "外包", "预算", "现金流", + "投资", "回报率", "盈亏", "亏损业务", + "接单", "大单", "账期", "框架合同", + "竞争", "竞品", "市场份额", + "Q1", "Q2", "Q3", "Q4", "季度", "年度规划", + "客户流失", "复购率", "转化率", + ], + "life": [ + "孩子", "子女", "父母", "养老", "家庭", "夫妻", + "亲子", "青春期", "兴趣班", "留学", "接班", + "体检", "健康", "运动", "饮食", "睡眠", + "出差", "行程", "时间管理", + "应酬", "酒", "体重", "颈椎", + ], +} + + +def filter_by_scene(questions: list[str], target_scene: str) -> list[str]: + """过滤掉不属于目标场景的问题。 + + 判断规则: + 1. 问题包含其他场景的归属关键词 → 过滤掉(归属错误) + 2. 问题包含目标场景的归属关键词 → 保留 + 3. 中性问题(不含任何场景关键词) → 保留(可能确实是该场景的问题) + + 特殊处理: + - emotion 场景较宽泛,只过滤明显属于 legal/decision/life 的问题 + - legal 和 decision 有交叉(如"合伙人退股"),归 legal + + Args: + questions: 问题列表 + target_scene: 目标场景 legal/decision/life/emotion + + Returns: + 过滤后的属于目标场景的问题列表 + """ + valid = [] + for q in questions: + q = str(q).strip() + if not q: + continue + # 检查是否包含其他场景的归属关键词 + wrong_scene = False + for scene, keywords in SCENE_KEYWORDS.items(): + if scene == target_scene: + continue + # emotion 场景不拦截(情绪问题可能出现在任何场景的焦虑中) + if target_scene == "emotion": + continue + if any(kw in q for kw in keywords): + wrong_scene = True + break + if not wrong_scene: + valid.append(q) + return valid + + def generate_daily_topics(kimi_client, store: DailyTopicStore) -> dict: """AI 生成当天 4 个场景的热点问题并存库。 @@ -3367,6 +3448,14 @@ def generate_daily_topics(kimi_client, store: DailyTopicStore) -> dict: "emotion": "情绪与心理问题,聚焦中小企业老板的焦虑:创业压力、合伙人冲突、孤独感、决策焦虑、拖延症、自我怀疑、危机应对等", } + # 场景边界:明确告诉AI什么问题不该出现在当前场景 + scene_boundaries = { + "legal": "本场景只关注法律/法务问题。不要生成纯经营策略问题(如'该不该扩张')、纯情绪问题(如'压力很大怎么办')、纯生活问题(如'孩子教育')。", + "decision": "本场景只关注经营决策问题。不要生成纯法律问题(如'合同违约金怎么约定')、纯情绪问题(如'焦虑失眠怎么办'、'自我怀疑'、'凌晨怀疑人生')、纯生活问题(如'父母养老')。情绪相关的决策问题可以,但必须是'该怎么做'而非'我很难受'。", + "life": "本场景只关注个人生活问题。不要生成纯法律问题、纯经营决策问题、纯情绪疏导问题。问题应围绕家庭、健康、教育、养老、时间管理等生活领域。", + "emotion": "本场景关注情绪与心理疏导。可以涉及经营中的情绪反应,但核心是'情绪感受和心理状态',不是'经营策略'或'法律方案'。", + } + # 获取当前日期用于 prompt today_str = datetime.now().strftime("%Y年%m月%d日") # 获取当前热点事件提示(简单版:用日期季节推断) @@ -3389,6 +3478,7 @@ def generate_daily_topics(kimi_client, store: DailyTopicStore) -> dict: results = {} for scene, desc in scene_desc.items(): + boundary = scene_boundaries.get(scene, "") prompt = f"""你是左右(BossAi)的内容编辑,负责为中小企业老板生成今日热点问题。 当前日期:{today_str} @@ -3396,6 +3486,8 @@ def generate_daily_topics(kimi_client, store: DailyTopicStore) -> dict: 场景:{desc} +场景边界:{boundary} + 请生成25-30个该场景下老板最焦虑、最关注的热点问题。 要求: @@ -3440,16 +3532,17 @@ def generate_daily_topics(kimi_client, store: DailyTopicStore) -> dict: results[scene] = {"count": 0, "source": "ai", "error": "JSON解析失败"} continue questions = json.loads(answer[start:end + 1]) - # 过滤:老板视角 + 去重 + # 过滤:老板视角 + 场景归属 + 去重 boss_questions = filter_boss_perspective(questions) + scene_questions = filter_by_scene(boss_questions, scene) valid = [] seen = set() - for q in boss_questions: + for q in scene_questions: if q not in seen: valid.append(q) seen.add(q) if len(valid) < 10: - results[scene] = {"count": 0, "source": "ai", "error": f"过滤后有效问题不足10个(原始{len(questions)}个)"} + results[scene] = {"count": 0, "source": "ai", "error": f"过滤后有效问题不足10个(原始{len(questions)}个,老板视角{len(boss_questions)}个,场景匹配{len(scene_questions)}个)"} continue store.upsert(scene, valid[:30], source="ai") results[scene] = {"count": len(valid[:30]), "source": "ai"}