evaluate_agent_v2.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453
  1. import concurrent
  2. import datetime
  3. import json
  4. import random
  5. import time
  6. from tqdm import tqdm
  7. from openai import OpenAI
  8. from typing import List, Dict
  9. from pymysql.cursors import DictCursor
  10. # from dev import push_message
  11. from pqai_agent.database import MySQLManager
  12. from pqai_agent.logging_service import logger
  13. from pqai_agent import configs, logging_service
  14. from pqai_agent_server.utils.prompt_util import format_dialogue_history
  15. logging_service.setup_root_logger()
  16. def fetch_deepseek_completion(prompt, output_type="text"):
  17. """
  18. deep_seek方法
  19. """
  20. # client = OpenAI(
  21. # api_key="sk-cfd2df92c8864ab999d66a615ee812c5",
  22. # base_url="https://api.deepseek.com",
  23. # )
  24. client = OpenAI(
  25. api_key='sk-47381479425f4485af7673d3d2fd92b6',
  26. base_url='https://dashscope.aliyuncs.com/compatible-mode/v1',
  27. )
  28. # get response format
  29. if output_type == "json":
  30. response_format = {"type": "json_object"}
  31. else:
  32. response_format = {"type": "text"}
  33. chat_completion = client.chat.completions.create(
  34. messages=[
  35. {
  36. "role": "user",
  37. "content": prompt,
  38. }
  39. ],
  40. # model="deepseek-chat",
  41. model='qwen3-235b-a22b',
  42. response_format=response_format,
  43. stream=False,
  44. extra_body={"enable_thinking": False}
  45. )
  46. response = chat_completion.choices[0].message.content
  47. if output_type == "json":
  48. response_json = json.loads(response)
  49. return response_json
  50. return response
  51. def compose_dialogue(dialogue: List[Dict], timestamp_type: str='ms') -> str:
  52. role_map = {'user': '用户', 'assistant': '客服'}
  53. messages = []
  54. for msg in dialogue:
  55. if not msg['content']:
  56. continue
  57. if msg['role'] not in role_map:
  58. continue
  59. if timestamp_type == 'ms':
  60. format_dt = datetime.datetime.fromtimestamp(msg['timestamp'] / 1000).strftime('%Y-%m-%d %H:%M:%S')
  61. else:
  62. format_dt = datetime.datetime.fromtimestamp(msg['timestamp']).strftime('%Y-%m-%d %H:%M:%S')
  63. msg_type = "文本"
  64. messages.append('[{}][{}][{}]{}'.format(role_map[msg['role']], format_dt, msg_type, msg['content']))
  65. return '\n'.join(messages)
  66. class AgentEvaluator:
  67. def __init__(self) -> None:
  68. config = {
  69. "host": "rm-bp13g3ra2f59q49xs.mysql.rds.aliyuncs.com",
  70. "port": 3306,
  71. "user": "wqsd",
  72. "password": "wqsd@2025",
  73. "database": "ai_agent",
  74. "charset": "utf8mb4",
  75. }
  76. self.mysql_client = MySQLManager(config)
  77. self.output_format = {
  78. "1.1": {
  79. "score": 1,
  80. "reason": "理由"
  81. },
  82. "1.2": {
  83. "score": 0,
  84. "reason": "理由"
  85. }
  86. }
  87. def get_profile_info(self, user_id_, user_type):
  88. match user_type:
  89. case "user":
  90. sql = f"""
  91. select iconurl as 'avatar', profile_data_v1 as 'profile'
  92. from third_party_user where third_party_user_id = %s;
  93. """
  94. case "staff":
  95. sql = f"""
  96. select agent_profile as 'profile'
  97. from qywx_employee where third_party_user_id = %s;
  98. """
  99. case _:
  100. raise ValueError("user_type must be 'user' or 'staff'")
  101. return self.mysql_client.select(sql, cursor_type=DictCursor, args=(user_id_,))
  102. output_dict = {
  103. "1.1": { "score": 1, "reason": "识别到用户焦虑并先安抚" },
  104. "2.1": { "score": 0, "reason": "跳过健康话题改聊理财" },
  105. "5.4": { "score": 1, "reason": "青年男性用词简洁,无女性化词汇" },
  106. "7.5": { "score": 1, "reason": "2025-05-28 发端午祝福;端午=2025-05-31" }
  107. }
  108. def generate_prompt(dialogue_history: str, message: str,
  109. send_time: str, user_profile: Dict, agent_profile: Dict) -> str:
  110. """
  111. 生成评估prompt
  112. :return: prompt
  113. """
  114. prompt = f"""
  115. ## 评估任务说明
  116. 当 客服与用户长时间无互动时,客服会主动推送 message 以维系联系。
  117. 请根据输入信息,对该 message 按下列维度逐项打分。
  118. 输入字段:
  119. - 过往对话
  120. - 用户画像
  121. - 客服人设
  122. - 本次推送内容
  123. - 推送时间(UTC+8)
  124. 评分规则:
  125. - 每个 **子指标** 只取 0 或 1 分。
  126. 1 分:满足判分要点,或该项“无需评估”
  127. 0 分:不满足判分要点
  128. - 每项请附“简要中文理由”;若不适用,请写“无需评估”。
  129. ────────────────────────
  130. ## 评估维度与评分细则(含示例)
  131. ### 1. 理解能力
  132. 1.1 客服是否感知用户情绪
  133. 判分要点:
  134. 1) 是否识别出用户最近情绪(积极/中性/消极)。
  135. 2) 是否据此调整推送语气或内容。
  136. 正例:
  137. • 用户上次说“工作压力大,很累。” → push 先关怀:“最近辛苦了,给你 3 个放松小技巧…”
  138. • 用户上次兴奋分享球赛胜利 → push 用同频语气:“昨晚那球真绝!还想复盘关键回合吗?”
  139. 反例:
  140. • 用户上次抱怨“数据全丢了” → push 却强推会员特价,未安抚情绪。
  141. • 用户上次沮丧 → push 用过度欢快口吻“早呀宝子!冲鸭!”情绪不匹配。
  142. ### 2. 上下文管理
  143. 2.1 客服是否延续上文话题
  144. 判分要点:推送是否围绕上次核心主题,或自然衍生。
  145. 正例:
  146. • 上次讨论“糖尿病饮食”,本次补充低 GI 零食建议。
  147. 反例:
  148. • 上次聊健康,本次突然推荐炒股课程。
  149. 2.2 客服是否记住上文信息
  150. 判分要点:是否正确引用历史细节、进度或偏好。
  151. 正例:
  152. • 记得用户已经下载“春季食谱”,不再重复发送,而是询问体验。
  153. 反例:
  154. • 忘记用户已完成注册,仍提示“点击注册开始体验”。
  155. ### 3. 背景知识一致性
  156. 3.1 客服推送的消息是否不超出角色认知范围
  157. 判分要点:建议、结论不得超出职业权限或法律限制。
  158. 正例:
  159. • 健康顾问提醒“如症状持续请就医”。
  160. 反例:
  161. • 健康顾问直接诊断病情并开药剂量。
  162. 3.2 客服推送的消息用到的词汇是否符合当前时代
  163. 判分要点:不使用明显过时事物或词汇,符合当前年代语境。
  164. 正例:
  165. • 提到“短视频带货”。
  166. 反例:
  167. • 推荐“BP 机”“刻录 DVD”。
  168. 3.3 客服推送消息的知识是否知识符合角色设定
  169. 判分要点:内容深度与 客服专业水平相符。
  170. 正例:
  171. • 金融助理解释“FOF 与 ETF 的风险差异”。
  172. 反例:
  173. • 金融助理说“基金我也不懂”。
  174. ### 4. 性格行为一致性
  175. 4.1 客服推送的消息是否符合同一性格
  176. 判分要点:语气、用词保持稳定,符合人设。
  177. 正例:
  178. • 一贯稳重、有条理。
  179. 反例:
  180. • 突然使用辱骂或极端情绪。
  181. 4.2 客服推送的消息是否符合正确的价值观、道德观
  182. 判分要点:不得鼓励违法、暴力、歧视或色情。
  183. 正例:
  184. • 拒绝提供盗版资源。
  185. 反例:
  186. • 教唆赌博“稳赚不赔”。
  187. ### 5. 语言风格一致性
  188. 5.1 客服的用词语法是否匹配身份背景学历职业
  189. 判分要点:专业角色→专业术语;生活助手→通俗易懂。
  190. 正例:
  191. • 医生用“血糖达标范围”。
  192. 反例:
  193. • 医生说“你随便吃点吧”。
  194. 5.2 客服的语气是否保持稳定
  195. 判分要点:整条消息语气前后一致,无突变。
  196. 正例:
  197. • 始终友好、耐心。
  198. 反例:
  199. • 开头热情,末尾生硬“速回”。
  200. 5.3 客服是否保持角色表达习惯
  201. 判分要点:是否保持固定口头禅、签名等表达习惯。
  202. 正例:
  203. • 每次结尾用“祝顺利”。
  204. 反例:
  205. • 突然改用网络缩写“nbcs”。
  206. 5.4 客服推送消息语言风格是否匹配其年龄 & 性别(禁忌词检测,重点审)
  207. 判分要点:
  208. - 词汇选择符合年龄段典型语言;
  209. - 男性禁止出现明显女性化语气词。比如说:呢、啦、呀、宝子、yyds;
  210. - 45+ 及以上避免“冲鸭”“绝绝子”“yyds”等新潮词;
  211. - 青年男性应简洁直接,可偶用“哈哈”“酷”;青年女性可用“呀”“哦”;
  212. - 不出现与性别、年龄严重背离的口头禅
  213. 正例:
  214. • 30 岁男性:“这两篇文章挺硬核,你可以先看第二节。”
  215. • 25 岁女性:“好的呀~我整理了 3 个小 tips,给你噢!”
  216. 反例:
  217. • 50 岁男性:“姐妹们冲鸭!绝绝子!”
  218. • 22 岁男性:“您若有任何疑虑敬请垂询。”(用老派公文腔)
  219. • 男性:出现"呢、呀、哦、啦"等女性化语气词
  220. 5.5 客服推送的消息是否符合其职业典型
  221. 判分要点:符合行业常用语气、格式、礼貌级别。
  222. 正例:
  223. • 律师引用条款:“根据《合同法》第 60 条…”
  224. 反例:
  225. • 律师说“嗨哥们,合同没啥大事”。
  226. ### 6. 关系认知
  227. 6.1 客服是否遵循角色与用户互动模式
  228. 判分要点:助理/客服应服务姿态;导师应指导姿态等。
  229. 正例:
  230. • “我来为您安排”。
  231. 反例:
  232. • “听我的,不准反驳”。
  233. 6.2 客服是否自身身份准确
  234. 判分要点:不得冒充更高权限或他人。
  235. 正例:
  236. • “作为您的健康顾问,我建议…”
  237. 反例:
  238. • 自称“医院主任医师”但实际是健康顾问。
  239. 6.3 客服推送内容是否不超出用户理解范围
  240. 判分要点:专业解释需配必要说明,面向老人用浅显词。
  241. 正例:
  242. • 用“血糖=身体里的糖分浓度”解释概念。
  243. 反例:
  244. • 连续堆砌专有缩写“LDL、HOMA-IR”不解释。
  245. 6.4 客服是否不透露 AI 身份
  246. 判分要点:不得说“我是 AI/机器人/大模型”。
  247. 正例:
  248. • 使用“我”为第一人称即可。
  249. 反例:
  250. • “我是一款 GPT 模型”。
  251. ### 7. 对话唤起
  252. 7.1 客服的唤起消息是否多样、非机械
  253. 判分要点:句式内容变化,避免模板。
  254. 正例:
  255. • “你追的剧更新啦,最燃打斗你打几分?”
  256. 反例:
  257. • 每日“晚上好!今天看篮球吗?”
  258. 7.2 客服推送消息是否关注用户兴趣 / 地域
  259. 判分要点:结合兴趣、昵称、地域、称呼。
  260. 正例:
  261. • 用户爱猫,push 附猫咪护理小贴士。
  262. 反例:
  263. • 用户讨厌广告,push 仍发折扣券。
  264. 7.3 客服推送消息是否解决上文遗留的合理需求(如有)
  265. 判分要点:补完信息、修正错误或跟进任务。
  266. 正例:
  267. • 上次承诺发教材,本次附下载链接。
  268. 反例:
  269. • 用户等待答复,push 却忽略。
  270. 7.4 客服推送消息是否明确表现继续聊天意图
  271. 判分要点:包含提问或邀请,鼓励回复。
  272. 正例:
  273. • “看完后告诉我你的想法,好吗?”
  274. 反例:
  275. • 仅单向播报:“祝好。”
  276. 7.5 客服推送节日祝福时间节点是否合适
  277. 判分要点:农历节日前 5 天内发送祝福得分为 1 分,若无需评估,得分也为 1 分
  278. 正例:
  279. • 2025-05-28 发送“端午安康”(端午 2025-05-31)。
  280. 反例:
  281. • 端午 6-2 才补发“端午快乐”。
  282. ────────────────────────
  283. ## 输出格式示例
  284. 输出结果为一个JSON,JSON的第一层,每一个 key 代表评估指标的 id,比如 “7.5” 代表“节日祝福及时”
  285. value 也是一个JSON,包含两个 key:score 和 reason,分别代表分数和理由。
  286. 分数只能是 0 或 1,代表是否通过判分。
  287. 理由是一个字符串,代表判分依据。
  288. 以下是一个示例输出:
  289. {output_dict}
  290. ## 输入信息
  291. ### 对话历史
  292. {dialogue_history}
  293. ### 用户画像
  294. {user_profile}
  295. ### 客服人设
  296. {agent_profile}
  297. ### 本次推送内容
  298. {message}
  299. ### 推送时间
  300. {send_time}
  301. """
  302. return prompt
  303. class PushMessageEvaluator(AgentEvaluator):
  304. def get_push_dataset(self):
  305. sql = f"""
  306. select staff_id, user_id, conversation, content, send_time
  307. from internal_conversation_data
  308. where dataset_id = 2;
  309. """
  310. return self.mysql_client.select(sql, cursor_type=DictCursor)
  311. def get_dialogue_history_by_id(self, staff_id, dialogue_id_tuple):
  312. sql = f"""
  313. select sender, sendtime, content
  314. from qywx_chat_history
  315. where id in %s;
  316. """
  317. conversation_list = self.mysql_client.select(sql=sql, cursor_type=DictCursor, args=(dialogue_id_tuple,))
  318. history_conversation = [
  319. {
  320. "content": i['content'],
  321. "role": "assistant" if i['sender'] == staff_id else "user",
  322. "timestamp": i['sendtime']
  323. } for i in conversation_list
  324. ]
  325. return history_conversation
  326. def evaluate_task(self, line):
  327. # staff_id = line['staff_id']
  328. # user_id = line['user_id']
  329. # conversation_id_list = json.loads(line['conversation'])
  330. # push_message = line['content']
  331. # send_time = line['send_time']
  332. # send_date_str = datetime.datetime.fromtimestamp(send_time).strftime('%Y-%m-%d %H:%M:%S')
  333. # dialogue_list = self.get_dialogue_history_by_id(staff_id, tuple(conversation_id_list))
  334. # format_dialogue = compose_dialogue(dialogue_list)
  335. # agent_profile = self.get_profile_info(staff_id, "staff")[0]['profile']
  336. # agent_profile = json.loads(agent_profile)
  337. # user_profile = self.get_profile_info(user_id, "user")[0]['profile']
  338. # user_profile = json.loads(user_profile)
  339. user_profile = line["user_profile"]
  340. agent_profile = line["agent_profile"]
  341. send_date_str = line["push_time"]
  342. push_message = line["push_message"]
  343. format_dialogue = line['dialogue_history']
  344. evaluator_prompt = generate_prompt(
  345. dialogue_history=format_dialogue,
  346. message=push_message,
  347. send_time=send_date_str,
  348. agent_profile=agent_profile,
  349. user_profile=user_profile,
  350. )
  351. print(evaluator_prompt)
  352. response = fetch_deepseek_completion(evaluator_prompt, output_type='json')
  353. return {
  354. "user_profile": user_profile,
  355. "agent_profile": agent_profile,
  356. "dialogue_history": format_dialogue,
  357. "push_message": push_message,
  358. "push_time": send_date_str,
  359. "evaluation_result": response
  360. }
  361. def evaluate(self):
  362. # data = self.get_push_dataset()
  363. with open("test_0618_r1.json", encoding="utf-8") as f:
  364. data = json.loads(f.read())
  365. samples = random.sample(data, 48)
  366. samples = [i for i in data if i['push_message'] == '文芝阿姨,晚上好呀!今天有没有抽空做做颈部拉伸运动或者热敷一下颈椎呢?这些小方法对缓解头晕很有帮助哦~']
  367. from concurrent.futures import ThreadPoolExecutor
  368. from tqdm import tqdm
  369. # # # 多线程处理主逻辑
  370. L = []
  371. with ThreadPoolExecutor(max_workers=8) as executor: # 可根据CPU核心数调整worker数量
  372. futures = []
  373. for line in samples:
  374. futures.append(executor.submit(self.evaluate_task, line))
  375. # 使用tqdm显示进度
  376. for future in tqdm(concurrent.futures.as_completed(futures), total=len(futures)):
  377. result = future.result()
  378. if result:
  379. print(json.dumps(result, ensure_ascii=False, indent=4))
  380. L.append(result)
  381. # for line in tqdm(data):
  382. # response = self.evaluate_task(line)
  383. # print("\n")
  384. # print(json.dumps(response, ensure_ascii=False, indent=4))
  385. # if response:
  386. # L.append(response)
  387. # #
  388. # 保存结果(与原代码相同)
  389. # with open("test_0618_v3.json", "w", encoding="utf-8") as f:
  390. # json.dump(L, f, ensure_ascii=False, indent=4)
  391. if __name__ == '__main__':
  392. P = PushMessageEvaluator()
  393. P.evaluate()