feat: 示例问题老板视角过滤+画像定向+prompt禁止清单
后端: - 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:
@@ -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,15 +421,19 @@ 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"):
|
||||||
valid_topics.append({
|
boss_samples = filter_boss_perspective(
|
||||||
"title": str(t["title"])[:10],
|
[str(s) for s in t["samples"] if isinstance(s, str) and s.strip()]
|
||||||
"desc": str(t.get("desc", ""))[:30],
|
)[:4]
|
||||||
"samples": [str(s) for s in t["samples"] if isinstance(s, str) and s.strip()][:4],
|
if boss_samples:
|
||||||
})
|
valid_topics.append({
|
||||||
|
"title": str(t["title"])[:10],
|
||||||
|
"desc": str(t.get("desc", ""))[:30],
|
||||||
|
"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"])}
|
||||||
return {"topics": valid_topics[:8]}
|
return {"topics": valid_topics[:8]}
|
||||||
@@ -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}
|
||||||
|
|||||||
@@ -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"}
|
||||||
|
|||||||
Reference in New Issue
Block a user