Files
tw/archive/test_battle.py
2026-02-27 16:03:04 +08:00

299 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
AI 左右互搏测试工具
客服AI真实 vs 买家AI模拟
用来调试客服AI的回复质量不需要真实客户
"""
import asyncio
import os
import random
import httpx
from openai import AsyncOpenAI
from dotenv import load_dotenv
from pydantic_ai_agent import CustomerServiceAgent, CustomerMessage
load_dotenv()
WECHAT_WEBHOOK = "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=cc88bdef-a13f-4d7e-bdb6-ee51b68b8205"
async def notify_wechat(content: str):
"""发送企业微信机器人通知"""
try:
async with httpx.AsyncClient(timeout=10) as client:
await client.post(WECHAT_WEBHOOK, json={
"msgtype": "text",
"text": {"content": content}
})
except Exception as e:
print(f"[通知] 企业微信发送失败: {e}")
# ========== 颜色输出 ==========
RESET = "\033[0m"
BLUE = "\033[94m" # 买家
GREEN = "\033[92m" # 客服
YELLOW = "\033[93m" # 系统提示
GRAY = "\033[90m" # 分隔线
def print_buyer(msg):
print(f"{BLUE}【买家】{msg}{RESET}")
def print_shop(msg):
print(f"{GREEN}【客服】{msg}{RESET}")
def print_system(msg):
print(f"{YELLOW}{msg}{RESET}")
def print_sep():
print(f"{GRAY}{'' * 50}{RESET}")
# ========== 买家人设配置 ==========
BUYER_PERSONAS = {
"普通买家": """你是一个普通淘宝买家,想找一张图的高清版本。
行为:先打招呼,发图问价,可能小砍一次价,觉得合适就说要拍了。
说话简短自然像手机上发消息1-2句话。不要太正式。""",
"砍价客": """你是个爱砍价的淘宝买家,总想便宜一点。
行为问价后嫌贵砍价2-3次最后可能接受也可能走人。
说话直接,喜欢说"能不能便宜点""太贵了""别家更便宜"""",
"爱问细节": """你是个很谨慎的买家,买东西前喜欢问清楚。
行为:先问在不在,问能不能找到,问格式,问效果,问不满意能退吗,最后才考虑下单。
说话有点啰嗦,喜欢多问几个问题。""",
"快节奏": """你是个很忙的买家,说话简短,不废话。
行为:直接发图问多少钱,价格合适立刻说拍了,不合适直接走。
每次只说3-5个字极度简短。""",
"售后投诉": """你是个已经付款的买家,但对收到的图片不满意,想要退款或重做。
行为:先说拿到图了但效果不行,描述哪里不满意(模糊/颜色不对/尺寸不够),
要求重做或者退款,态度有点不耐烦,反复追问进度。
说话带点情绪,但不至于骂人,就是那种"花钱了结果不行"的委屈感。""",
"多图打包": """你是个需要批量处理图片的买家,手头有好几张图要找高清版。
行为:先问在不在,然后说有多张图要处理,询问能不能打包便宜点,
逐步发图或询问价格,跟客服商量总价,最终决定下单还是再想想。
说话随意,像在商量事情,不太在意每张的价格,更关注总价合不合适。""",
"大批量砍价": """你是个手头有十几张图要处理的买家,量大,觉得自己有底气砍价。
行为:上来就说"我有很多图,你们量大能便宜吗"
告知大概数量10-20张用量大作为筹码反复压价
"做完这批还有下一批,能不能给个长期价""别家给我打6折了"
如果客服给的总价还算合理就下单,否则拉锯几个来回。
说话有点强势,觉得自己是大客户,应该享受优惠。""",
"要分层PSD": """你是个做设计的买家需要带分层的PSD格式不是普通jpg。
行为:先问在不在,发图后问"这个能出PSD吗""要分层的那种"
追问能不能保留图层、格式是不是真的PSD听到价格后可能嫌贵问能不能便宜
最终决定下单或者放弃。
说话带点专业感,会说"图层""分层""PSD""源文件"之类的词。""",
"问尺寸分辨率": """你是个有印刷需求的买家,非常在意图片的尺寸和分辨率。
行为:发图后先不问价,反复问"这个能做多大""分辨率能到300dpi吗""印出来会不会模糊"
问完尺寸再问价格,如果客服说能满足需求就考虑下单,否则犹豫很久。
说话里经常出现"厘米""像素""dpi""印刷""大图"""",
"问颜色效果": """你是个对颜色很挑剔的买家,担心高清版颜色和原图有色差。
行为:发图后问"颜色会变吗""饱和度能保持吗""跟原图一样的色调吧"
让客服保证颜色效果,追问不满意能不能改,反复确认才肯下单。
说话谨慎,总要客服给"保证",但说话不凶,就是很在意品质。""",
"来源质疑": """你是个对店铺服务有疑虑的买家,总觉得有猫腻,想搞清楚原理。
行为:问"你们是怎么找的""原图是从哪里来的""是AI弄的吗还是真的原图"
追问来源和方法,对客服的回答将信将疑,继续追问,
最终可能被说服下单,也可能觉得不靠谱走人。
说话带点怀疑,喜欢反问,但不是来找茬的,是真的想搞清楚。""",
}
# 用于测试的图片URL
TEST_IMAGE_URLS = [
"https://img.alicdn.com/imgextra/i1/O1CN01OilxfD1kr8tM4Ugg2_!!4611686018427387680-0-amp.jpg",
]
class BuyerAgent:
"""模拟买家的AI"""
def __init__(self, persona_name: str = "普通买家"):
self.client = AsyncOpenAI(
api_key=os.getenv("OPENAI_API_KEY"),
base_url=os.getenv("OPENAI_BASE_URL"),
)
self.model = os.getenv("OPENAI_MODEL", "gpt-4o-mini")
self.persona_name = persona_name
self.persona = BUYER_PERSONAS.get(persona_name, BUYER_PERSONAS["普通买家"])
self.history = []
self.image_sent = False
self.rounds = 0
async def next_message(self, shop_reply: str = None) -> str:
"""根据客服回复,生成下一条买家消息"""
self.rounds += 1
if shop_reply:
self.history.append({"role": "assistant", "content": f"客服说:{shop_reply}"})
# 第一轮:打招呼或直接发问
if self.rounds == 1:
first_msgs = ["在不在", "在吗", "你好", "有人吗", "亲在吗"]
msg = random.choice(first_msgs)
self.history.append({"role": "user", "content": msg})
return msg
# 第二轮:发图+问有没有
if self.rounds == 2 and not self.image_sent:
self.image_sent = True
url = random.choice(TEST_IMAGE_URLS)
msg = f"有吗#*#{url}"
self.history.append({"role": "user", "content": msg})
return msg
# 后续让AI根据对话历史自由生成
system = f"""{self.persona}
你正在和一家找图店的客服聊天,对话历史如下。
请根据你的人设生成下一条消息,简短自然,像真人发消息。
如果对话已经快结束超过10轮可以说"好的拍了""算了不要了"结束对话。
只输出你要发的消息内容,不要加任何前缀说明。"""
resp = await self.client.chat.completions.create(
model=self.model,
messages=[
{"role": "system", "content": system},
*self.history,
{"role": "user", "content": "(请生成你的下一条消息)"}
],
max_tokens=80,
temperature=0.9,
)
msg = resp.choices[0].message.content.strip()
self.history.append({"role": "user", "content": msg})
return msg
async def run_battle(persona_name: str = "普通买家", max_rounds: int = 8):
"""运行一场对话模拟"""
print_sep()
print_system(f" 开始模拟 | 买家人设:{persona_name} | 最多 {max_rounds}")
print_sep()
# 初始化双方
buyer = BuyerAgent(persona_name)
shop = CustomerServiceAgent()
buyer_id = "test_buyer_001"
shop_id = "test_shop_001"
shop_reply = None
for i in range(max_rounds):
# 买家发消息
buyer_msg = await buyer.next_message(shop_reply)
print_buyer(buyer_msg)
# 判断对话是否结束
end_words = ["算了", "不要了", "不买了", "拍了", "好的拍了", "下单了", "谢谢"]
if any(w in buyer_msg for w in end_words):
print_system(f"\n 对话结束(第 {i+1} 轮)")
break
# 构建消息对象
customer_msg = CustomerMessage(
msg_id=f"test_{i}",
acc_id=shop_id,
msg=buyer_msg,
from_id=buyer_id,
from_name="测试买家",
cy_id=buyer_id,
acc_type="AliWorkbench",
msg_type=0,
cy_name="测试买家",
goods_name="模糊图清晰处理专业代找原图素材淘宝图片找图修复服务",
)
# 含图片URL时先打印等待语模拟真实客服的即时回应
if "#*#" in buyer_msg or (buyer_msg.startswith(("http://", "https://")) and any(
h in buyer_msg for h in ("alicdn.com", "imgextra", ".jpg", ".jpeg", ".png")
)):
print_shop("我找一下看看")
print()
# 客服AI处理
response = await shop.process_message(customer_msg)
if response.need_transfer:
print_shop("[转人工]")
print_system("\n 客服决定转人工,对话结束")
break
shop_reply = response.reply if response.should_reply else None
# 过滤 AI 误输出的内部独白(与生产环境保持一致)
nonsense_patterns = [
"无需", "流程已完成", "不需要回复", "无需额外", "已完成",
"无需回复", "不需要额外", "已经完成", "无需再", "操作已完成",
"任务完成", "流程完成", "记录完成", "报价已",
]
if shop_reply and any(p in shop_reply for p in nonsense_patterns):
print_system(f" [已拦截无效回复: {shop_reply}]")
shop_reply = None
if shop_reply:
print_shop(shop_reply)
else:
print_system(" (客服决定不回复)")
shop_reply = ""
print()
await asyncio.sleep(0.3)
print_sep()
print_system(" 模拟结束")
print_sep()
async def main():
import sys
personas = list(BUYER_PERSONAS.keys())
print(f"\n{YELLOW}{'=' * 50}")
print(" AI 左右互搏测试工具")
print(f"{'=' * 50}{RESET}")
print(f"{GRAY}用法: python test_battle.py [人设编号|all]")
print(f" 1=普通买家 2=砍价客 3=爱问细节 4=快节奏")
print(f" 5=售后投诉 6=多图打包 7=大批量砍价 8=要分层PSD")
print(f" 9=问尺寸分辨率 10=问颜色效果 11=来源质疑 0=全部{RESET}\n")
arg = sys.argv[1] if len(sys.argv) > 1 else "1"
if arg == "0" or arg == "all":
for persona in personas:
await run_battle(persona, max_rounds=14)
print()
await asyncio.sleep(1)
else:
try:
idx = int(arg) - 1
persona = personas[idx] if 0 <= idx < len(personas) else personas[0]
except ValueError:
persona = personas[0]
await run_battle(persona, max_rounds=14)
if __name__ == "__main__":
try:
asyncio.run(main())
except Exception as e:
err_str = str(e)
if "AccountOverdueError" in err_str or "overdue" in err_str.lower():
msg = "⚠️ 火山引擎 API 欠费,请立即充值!\n地址https://console.volcengine.com/ark"
else:
msg = f"⚠️ 测试脚本异常退出:{err_str[:200]}"
print(f"\n[通知] {msg}")
asyncio.run(notify_wechat(msg))
raise