feat: 示例问题老板视角过滤+画像定向+prompt禁止清单
CI / 后端测试 (push) Has been cancelled
CI / 前端构建 (push) Has been cancelled

后端:
- filter_boss_perspective(): 关键词过滤员工视角问题
  覆盖:加班费/被辞退/拖欠工资/工伤认定/产假/劳动仲裁/降薪扣工资等
- /samples: prompt加禁止清单+正确视角示例,返回前过滤
  画像注入加行业定向(有industry时明确要求生成行业相关问题)
- /topics: prompt加禁止清单+正确视角示例,返回前过滤
- generate_daily_topics: prompt加禁止清单+正确视角示例,返回前过滤

前端:
- 审查FALLBACK_SAMPLES: 三个场景兜底示例均为老板视角,无需修改

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
selfrelease
2026-09-01 09:24:05 +08:00
co-authored by Devin
parent 86f15bc79c
commit 3d64b98ed6
2 changed files with 126 additions and 19 deletions
+41 -8
View File
@@ -18,7 +18,7 @@ from bossai import (
ExpertStore, ConsultationStore, RatingStore, ExpertStore, ConsultationStore, RatingStore,
DocumentGenerator, RegulationUpdateManager, FactFinder, DocumentGenerator, RegulationUpdateManager, FactFinder,
ChatStore, redact, ChatStore, redact,
DailyTopicStore, generate_daily_topics, DailyTopicStore, generate_daily_topics, filter_boss_perspective,
) )
# ===== 日志脱敏 ===== # ===== 日志脱敏 =====
@@ -394,9 +394,19 @@ def generate_topics(req: TopicsRequest):
1. 话题要覆盖该场景下不同方向,不要只集中在某一类 1. 话题要覆盖该场景下不同方向,不要只集中在某一类
2. 结合用户画像(如有)推荐个性化话题 2. 结合用户画像(如有)推荐个性化话题
3. 示例问题要具体、贴近中小企业老板实际 3. 示例问题要具体、贴近中小企业老板实际
4. **所有问题必须从老板/雇主视角出发**(BossAi 的用户是企业老板,不是员工)。例如劳动用工应问"员工工伤后能开除吗"而非"拖欠工资怎么追讨" 4. **所有问题必须从老板/雇主视角出发**(BossAi 的用户是企业老板,不是员工)
5. 返回JSON数组格式:[{{"title":"话题名","desc":"简短描述","samples":["问题1","问题2","问题3","问题4"]}}] 5. 返回JSON数组格式:[{{"title":"话题名","desc":"简短描述","samples":["问题1","问题2","问题3","问题4"]}}]
6. 只返回JSON,不要其他文字{existing_str}""" 6. 只返回JSON,不要其他文字{existing_str}
**严禁生成员工视角问题:**
- 加班费怎么算、被辞退怎么维权、拖欠工资怎么追讨
- 请假被拒、工伤认定、产假怎么休、劳动仲裁怎么告公司
- 降我薪、扣我工资、罚我款、被歧视、被骚扰
**正确视角示例:**
- 老板问:员工工伤后能开除吗?(不是"工伤赔偿怎么申请")
- 老板问:辞退员工要赔多少?(不是"被辞退怎么维权")
- 老板问:客户拖欠货款如何催收?(不是"拖欠工资怎么追")"""
try: try:
answer = kimi.chat([{"role": "user", "content": prompt}], temperature=0.8) answer = kimi.chat([{"role": "user", "content": prompt}], temperature=0.8)
@@ -411,14 +421,18 @@ def generate_topics(req: TopicsRequest):
if start == -1 or end == -1: if start == -1 or end == -1:
return {"topics": builtin_topics.get(req.scene, builtin_topics["legal"])} return {"topics": builtin_topics.get(req.scene, builtin_topics["legal"])}
topics = json.loads(answer[start:end + 1]) topics = json.loads(answer[start:end + 1])
# 校验结构 # 校验结构 + 老板视角过滤
valid_topics = [] valid_topics = []
for t in topics: for t in topics:
if isinstance(t, dict) and t.get("title") and t.get("samples"): if isinstance(t, dict) and t.get("title") and t.get("samples"):
boss_samples = filter_boss_perspective(
[str(s) for s in t["samples"] if isinstance(s, str) and s.strip()]
)[:4]
if boss_samples:
valid_topics.append({ valid_topics.append({
"title": str(t["title"])[:10], "title": str(t["title"])[:10],
"desc": str(t.get("desc", ""))[:30], "desc": str(t.get("desc", ""))[:30],
"samples": [str(s) for s in t["samples"] if isinstance(s, str) and s.strip()][:4], "samples": boss_samples,
}) })
if len(valid_topics) < 3: if len(valid_topics) < 3:
return {"topics": builtin_topics.get(req.scene, builtin_topics["legal"])} return {"topics": builtin_topics.get(req.scene, builtin_topics["legal"])}
@@ -469,6 +483,17 @@ def generate_samples(req: SampleRequest):
profile_ctx = build_profile_context(profile) profile_ctx = build_profile_context(profile)
if profile_ctx: if profile_ctx:
prompt += f"用户画像(请结合画像生成更贴合用户自身情况的问题):\n{profile_ctx}\n\n" prompt += f"用户画像(请结合画像生成更贴合用户自身情况的问题):\n{profile_ctx}\n\n"
# 行业定向:如果有行业,加明确指令
if profile.industry:
industry_map = {
"retail": "零售/餐饮", "manufacturing": "制造业/工厂",
"tech": "科技/互联网", "service": "服务业",
"construction": "建筑/工程", "logistics": "物流/运输",
"finance": "金融/投资", "education": "教育/培训",
"healthcare": "医疗/健康", "agriculture": "农业/农产品",
}
industry_label = industry_map.get(profile.industry, profile.industry)
prompt += f"**用户行业为{industry_label},请生成与该行业密切相关的问题**\n\n"
except Exception: except Exception:
pass pass
if existing: if existing:
@@ -482,7 +507,13 @@ def generate_samples(req: SampleRequest):
"2. **所有问题必须从老板/雇主视角出发**(用户是企业老板,不是员工)\n" "2. **所有问题必须从老板/雇主视角出发**(用户是企业老板,不是员工)\n"
"3. 每个问题 15-30 字\n" "3. 每个问题 15-30 字\n"
"4. 不要重复已展示过的问题\n" "4. 不要重复已展示过的问题\n"
"5. 只返回 JSON 数组,如 [\"问题1\", \"问题2\", \"问题3\", \"问题4\"]\n" "5. 只返回 JSON 数组,如 [\"问题1\", \"问题2\", \"问题3\", \"问题4\"]\n\n"
"**严禁生成员工视角问题(如:加班费怎么算、被辞退怎么维权、拖欠工资怎么追讨、**\n"
"**请假被拒、工伤认定、产假怎么休、劳动仲裁怎么告公司、降我薪扣我工资等)**\n\n"
"**正确视角示例:**\n"
"- 老板问:员工工伤后能开除吗?(不是"工伤赔偿怎么申请")\n"
"- 老板问:辞退员工要赔多少?(不是"被辞退怎么维权")\n"
"- 老板问:客户拖欠货款如何催收?(不是"拖欠工资怎么追")\n"
) )
try: try:
@@ -507,8 +538,10 @@ def generate_samples(req: SampleRequest):
if start == -1 or end == -1: if start == -1 or end == -1:
return {"samples": builtin.get(req.scene, builtin["legal"])} return {"samples": builtin.get(req.scene, builtin["legal"])}
samples = json.loads(answer[start:end + 1]) samples = json.loads(answer[start:end + 1])
# 过滤非字符串和空值 # 过滤:老板视角 + 非字符串 + 去重
samples = [s for s in samples if isinstance(s, str) and s.strip()][:4] samples = filter_boss_perspective(
[s for s in samples if isinstance(s, str) and s.strip()]
)[:4]
if len(samples) < 2: if len(samples) < 2:
return {"samples": builtin.get(req.scene, builtin["legal"])} return {"samples": builtin.get(req.scene, builtin["legal"])}
return {"samples": samples} return {"samples": samples}
+81 -7
View File
@@ -3291,6 +3291,64 @@ class DailyTopicStore:
] ]
# ===== 老板视角过滤 =====
# 员工视角关键词:出现这些问题时,说明 AI 从员工角度生成了问题
EMPLOYEE_PERSPECTIVE_KEYWORDS = [
"加班费", "加班工资", "调休怎么", "请假被", "申请休假",
"被辞退", "被开除", "被裁员", "被解雇", "辞退补偿", "离职补偿",
"拖欠工资", "拖欠我", "欠薪怎么", "老板不给",
"社保没给我", "公积金没缴", "不给我签合同",
"工伤认定", "工伤赔偿我", "伤残补助",
"产假", "哺乳假", "年休假怎么休",
"劳动仲裁我", "仲裁怎么告", "怎么告公司",
"举报公司", "投诉公司", "举报老板",
"试用期被", "转正被",
"歧视我", "骚扰我", "针对我",
"降我薪", "扣我工资", "罚我款",
"不让我", "不给我",
]
# 老板视角关键词:出现这些问题时,确认是老板视角
BOSS_PERSPECTIVE_KEYWORDS = [
"员工工伤后能开除", "员工被辞退要赔", "辞退员工", "开除员工",
"员工不签合同", "员工仲裁", "员工离职",
"拖欠货款", "客户欠款", "催收货款",
"合伙人退股", "股东不出资", "股东纠纷",
"税务稽查", "发票开错", "税务罚款",
"扩张", "融资", "裁员", "招人",
"公司股权", "公司分红", "公司经营",
"业务转型", "业务线", "定价",
]
def filter_boss_perspective(questions: list[str]) -> list[str]:
"""过滤掉员工视角的问题,只保留老板视角。
判断规则:
1. 包含员工视角关键词 → 过滤掉
2. 包含老板视角关键词 → 保留
3. 中性问题(不含任何关键词) → 保留
Args:
questions: AI 生成的问题列表
Returns:
过滤后的老板视角问题列表
"""
valid = []
for q in questions:
q = str(q).strip()
if not q or len(q) > 50:
continue
# 检查是否员工视角
is_employee = any(kw in q for kw in EMPLOYEE_PERSPECTIVE_KEYWORDS)
if is_employee:
continue
valid.append(q)
return valid
def generate_daily_topics(kimi_client, store: DailyTopicStore) -> dict: def generate_daily_topics(kimi_client, store: DailyTopicStore) -> dict:
"""AI 生成当天 4 个场景的热点问题并存库。 """AI 生成当天 4 个场景的热点问题并存库。
@@ -3341,13 +3399,29 @@ def generate_daily_topics(kimi_client, store: DailyTopicStore) -> dict:
请生成25-30个该场景下老板最焦虑、最关注的热点问题。 请生成25-30个该场景下老板最焦虑、最关注的热点问题。
要求: 要求:
1. 问题必须从老板/雇主视角出发(BossAi 用户是企业老板) 1. **所有问题必须从老板/雇主视角出发**(BossAi 用户是企业老板,不是员工)
2. 结合当前季节性和时事热点(如政策变化、节日前后、经济形势) 2. 结合当前季节性和时事热点(如政策变化、节日前后、经济形势)
3. 问题要具体、有场景感、能直接开问,不要空泛 3. 问题要具体、有场景感、能直接开问,不要空泛
4. 涵盖该场景下不同方向,不要集中在某一类 4. 涵盖该场景下不同方向,不要集中在某一类
5. 问题长度 10-30 字 5. 问题长度 10-30 字
6. 返回JSON数组格式:["问题1","问题2",...] 6. 返回JSON数组格式:["问题1","问题2",...]
7. 只返回JSON,不要其他文字""" 7. 只返回JSON,不要其他文字
**严禁生成以下员工视角问题(违反则整批作废):**
- 加班费怎么算、加班工资、调休、请假被拒、申请休假
- 被辞退/被开除/被裁员后怎么维权、辞退补偿、离职补偿
- 拖欠工资怎么追讨、老板不给钱、欠薪怎么告
- 工伤认定、工伤赔偿、伤残补助怎么申请
- 产假/哺乳假/年休假怎么休
- 劳动仲裁怎么告公司、举报公司、投诉老板
- 试用期被辞、转正被卡、被歧视、被骚扰
- 降我薪、扣我工资、罚我款
**正确视角示例(老板问的):**
- 员工工伤后能开除吗?(不是"工伤赔偿怎么申请")
- 辞退员工要赔多少?(不是"被辞退怎么维权")
- 员工不签合同有何风险?(不是"老板不签合同怎么办")
- 客户拖欠货款3个月如何催收?(不是"拖欠工资怎么追")"""
try: try:
answer = kimi_client.chat( answer = kimi_client.chat(
@@ -3366,16 +3440,16 @@ def generate_daily_topics(kimi_client, store: DailyTopicStore) -> dict:
results[scene] = {"count": 0, "source": "ai", "error": "JSON解析失败"} results[scene] = {"count": 0, "source": "ai", "error": "JSON解析失败"}
continue continue
questions = json.loads(answer[start:end + 1]) questions = json.loads(answer[start:end + 1])
# 过滤和去重 # 过滤:老板视角 + 去重
boss_questions = filter_boss_perspective(questions)
valid = [] valid = []
seen = set() seen = set()
for q in questions: for q in boss_questions:
q = str(q).strip() if q not in seen:
if q and q not in seen and len(q) <= 50:
valid.append(q) valid.append(q)
seen.add(q) seen.add(q)
if len(valid) < 10: if len(valid) < 10:
results[scene] = {"count": 0, "source": "ai", "error": "有效问题不足10个"} results[scene] = {"count": 0, "source": "ai", "error": f"过滤后有效问题不足10个(原始{len(questions)}个)"}
continue continue
store.upsert(scene, valid[:30], source="ai") store.upsert(scene, valid[:30], source="ai")
results[scene] = {"count": len(valid[:30]), "source": "ai"} results[scene] = {"count": len(valid[:30]), "source": "ai"}