This commit is contained in:
2026-03-06 14:25:10 +08:00
parent afb2b78c15
commit f06bfb1fa0
6 changed files with 289 additions and 145 deletions

173
README.md
View File

@@ -1,46 +1,70 @@
# AI 客服系统 - 天网协作版 # AI 客服系统
**版本**: v1.0 | **服务器**: 1.12.50.92 基于 PydanticAI 的智能客服,对接千牛 WebSocket自动接待、收图、转接设计师。
--- ---
## 功能概览 ## 架构概览
```
客户消息 (千牛)
WebSocket Client → QianniuAdapter (协议转换)
Orchestrator (防抖/去重/冷却/路由)
CustomerServiceBrain (PydanticAI Agent)
├── lookup_chat_history_tool → 查询历史记录
├── transfer_to_human_tool → 转接设计师
└── check_order_status_tool → 订单查询
QianniuAdapter → WebSocket → 回复客户
```
---
## 核心功能
| 功能 | 说明 | | 功能 | 说明 |
|------|------| |------|------|
| 天网协作 | 接收天网任务,支持指定客户回复触发 | | 智能接待 | 自动引导发图、问需求、转接设计师 |
| 三种工作流 | 找图 / 处理图片 / 转人工派单 | | 历史记忆 | AI 可调用工具查询完整聊天历史,避免重复提问 |
| 图片任务数据库 | 任务持久化,支持后续增加需求 | | 自动转接 | 收到图片+需求后自动派单给在线设计师 |
| 图绘派单系统 | 自动派单给在线设计师 | | 转接冷却 | 转接后 120 秒内不再调用 AI直接安抚 |
| 文字检测加价 | 自动识别文字数量并加价 | | 情绪识别 | 客户愤怒/投诉时自动转人工 |
| 风险评估 | 自动识别敏感内容,拒绝不良订单 | | 消息防抖 | 合并短时间内的多条消息,避免重复回复 |
| 作图失败转人工 | 失败自动转接人工客服 | | 订单静默 | 订单通知/SKU 信息自动入库,不触发 AI |
| 时段感知 | 根据时间区分"没上班"/"下班了"/"暂时不在" |
| 图片分析 | 后台调用 Gemini 分析图片复杂度 |
| 日报统计 | 每日自动生成客服数据报告 |
| 多进程 | 支持多 Worker 并行处理 |
--- ---
## 快速开始 ## 快速开始
```bash ```bash
cd /root/ai_customer_service/ai_cs # 安装依赖
pip3 install -r requirements.txt pip install -r requirements.txt
# 天网协作版(仅 HTTP API # 配置环境变量
python3 run.py --api-only cp .env.example .env
# 编辑 .env 填入 API Key、数据库等配置
# 完整版HTTP API + WebSocket + AI Agent # 启动WebSocket 客服模式
python3 run.py --tianwang python run.py
# AI 客服(仅 WebSocket,默认 # 完整版HTTP API + WebSocket
python3 run.py python run.py --tianwang
# 多进程模式
python run.py --multi -w 4
# 仅 HTTP API
python run.py --api-only
``` ```
### 后台运行 ### 健康检查
```bash
nohup python3 run.py --api-only > /tmp/tianwang.log 2>&1 &
```
### 验证
```bash ```bash
curl http://localhost:6060/api/health curl http://localhost:6060/api/health
@@ -48,44 +72,75 @@ curl http://localhost:6060/api/health
--- ---
## API 地址
| 服务 | 地址 |
|------|------|
| AI 客服 API | `http://127.0.0.1:6060` |
| 派单系统 | `http://1.12.50.92:8005` |
| 图绘平台 | `http://1.12.50.92:8002` |
---
## 文档
| 文档 | 内容 |
|------|------|
| **项目功能汇总.md** | 全部功能详细说明(工作流、报价、风险、派单、数据库等) |
| **部署文档.md** | 部署、API 接口、天网集成、多进程、故障排查 |
| **features/self_evolution_mvp.md** | 自我进化 MVP采样、评测、建议、灰度门禁 |
---
## 项目结构 ## 项目结构
``` ```
├── api/ # HTTP API 服务器 ├── run.py # 统一入口
├── core/ # 核心逻辑Agent、工作流、WebSocket ├── api/
├── config/ # 配置文件 │ └── http_server.py # HTTP API 服务
├── db/ # 数据库模块 ├── core/
├── image/ # 图片处理模块 │ ├── orchestrator.py # 总编排(防抖/去重/冷却/路由)
├── services/ # 外部服务集成 │ ├── pydantic_ai_agent_v2.py # AI 大脑PydanticAI Agent
├── utils/ # 工具模块 │ ├── agent_tools.py # AI 工具(转接/查历史/查订单)
├── skills/ # Agent 技能定义 ├── schema.py # 数据模型StandardMessage/Response
└── run.py # 统一入口(--api-only / --tianwang / 默认 WebSocket │ ├── repository.py # 异步数据仓库
│ ├── skill_manager.py # 技能加载器
│ ├── engine.py # 业务逻辑兜底
│ ├── adapters/
│ │ └── qianniu_adapter.py # 千牛协议适配
│ ├── events/
│ │ └── event_bus.py # 异步事件总线
│ └── websocket_*.py # WebSocket 连接/发送/日志
├── db/
│ ├── chat_log_db.py # 聊天记录SQLite/MySQL
│ ├── customer_db.py # 客户档案
│ ├── image_tasks_db.py # 图片任务
│ └── task_db/ # 任务模型
├── services/
│ ├── dispatch_service.py # 设计师派单
│ ├── service_gemini.py # Gemini 图片分析
│ ├── service_image_analyzer.py # 图片复杂度分析
│ └── ... # 其他服务
├── skills/ # AI 技能定义SKILL.md
│ ├── customer-service/ # 客服核心技能
│ ├── owner-style/ # 店主风格
│ ├── pre-sales-skill/ # 售前
│ ├── after-sales-skill/ # 售后
│ ├── pricing-skill/ # 报价(排除出 prompt
│ ├── risk-skill/ # 风控
│ └── style-skill/ # 语气风格
├── config/ # 配置文件
├── utils/ # 工具(日报/健康检查/API计费等
└── scripts/ # 运维脚本
``` ```
## 自我进化 MVP ---
```bash ## 环境变量
python scripts/evolution_cycle.py --hours 24 --publish
```
默认从线上 MySQL 读取对话数据(可用 `--source` 切换)。 | 变量 | 说明 |
|------|------|
| `OPENAI_API_KEY` | 火山引擎 Ark API Key |
| `OPENAI_BASE_URL` | API 地址 |
| `OPENAI_MODEL` | 对话模型 |
| `DB_TYPE` | 数据库类型(`sqlite` / `mysql` |
| `MYSQL_HOST/PORT/USER/PASSWORD/DATABASE` | MySQL 连接信息 |
| `WECHAT_WEBHOOK` | 企业微信通知 Webhook |
| `MESSAGE_DEBOUNCE_SECONDS` | 消息防抖时间(秒) |
| `DISPATCH_BASE_URL` | 派单服务地址 |
完整配置见 `.env.example`
---
## 消息处理流程
1. **WebSocket 接收** → 千牛原始消息
2. **适配器转换**`StandardMessage`(统一格式)
3. **Orchestrator 过滤** → 订单/SKU 静默入库、心跳过滤、商家回复入库
4. **防抖合并** → 2 秒窗口内多条消息合并为一条
5. **冷却检查** → 转接后 120 秒内直接安抚,不调 AI
6. **AI 思考** → PydanticAI Agent 调用工具、生成回复
7. **转接截获** → 工具返回转接指令时直接发送,不经 AI 二次加工
8. **乱码清理** → 过滤 `<think>`、内部标记等泄露内容
9. **发送回复** → 通过 WebSocket 回复客户,同时入库

View File

@@ -1,13 +1,16 @@
import logging import logging
import asyncio import asyncio
from datetime import datetime
from typing import List, Optional, Dict, Any from typing import List, Optional, Dict, Any
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from pydantic_ai import RunContext from pydantic_ai import RunContext
from core.schema import StandardResponse from core.schema import StandardResponse
from services.dispatch_service import dispatch_service from services.dispatch_service import dispatch_service
from db.chat_log_db import get_conversation
logger = logging.getLogger("cs_agent") logger = logging.getLogger("cs_agent")
async def transfer_to_human_tool(ctx: RunContext[Any], reason: str = Field(description="转人工的原因")) -> str: async def transfer_to_human_tool(ctx: RunContext[Any], reason: str = Field(description="转人工的原因")) -> str:
""" """
【核心工具】执行转人工逻辑。 【核心工具】执行转人工逻辑。
@@ -15,25 +18,80 @@ async def transfer_to_human_tool(ctx: RunContext[Any], reason: str = Field(descr
""" """
logger.info(f"[Tool] 尝试呼叫设计师接手: {reason}") logger.info(f"[Tool] 尝试呼叫设计师接手: {reason}")
# 1. 尝试派单获取设计师姓名
designer_name = await dispatch_service.assign_designer() designer_name = await dispatch_service.assign_designer()
if designer_name: if designer_name:
# 2. 有设计师在线:生成标准转接指令 (必须包含 [转移会话] 且格式正确)
magic_cmd = f"正在为您转接|[转移会话],{designer_name},无原因" magic_cmd = f"正在为您转接|[转移会话],{designer_name},无原因"
logger.info(f"[Tool] 成功呼叫设计师: {designer_name}") logger.info(f"[Tool] 成功呼叫设计师: {designer_name}")
return magic_cmd return magic_cmd
else: else:
# 3. 设计师下线:返回特定信号 hour = datetime.now().hour
logger.warning("[Tool] 派单失败:设计师们已下线或不在位") logger.warning(f"[Tool] 派单失败:设计师们不在位 (当前{hour}点)")
return "ERROR_NO_DESIGNER_ONLINE" if 0 <= hour < 9:
return "ERROR_DESIGNER_NOT_STARTED现在设计师还没上班你告诉客户需求记下了上班后第一时间处理。不要说下班。"
elif 22 <= hour or hour < 1:
return "ERROR_DESIGNER_OFFLINE设计师已下班你告诉客户需求记下了明天第一时间回复。"
else:
return "ERROR_DESIGNER_BUSY设计师暂时不在位你告诉客户稍等马上帮忙联系设计师。不要说下班。"
async def check_order_status_tool(ctx: RunContext[Any], customer_id: str = Field(description="客户ID")) -> str: async def check_order_status_tool(ctx: RunContext[Any], customer_id: str = Field(description="客户ID")) -> str:
"""查询订单状态。""" """查询订单状态。"""
return "设计师正在后台加急处理中,稍等哈。" return "我在帮你加急处理中,稍等哈。"
async def lookup_chat_history_tool(
ctx: RunContext[Any],
customer_id: str = Field(description="客户ID从当前对话上下文中获取"),
num_messages: int = Field(default=30, description="要查询的历史消息条数默认30条"),
) -> str:
"""
【历史记录查询工具】查询该客户的历史聊天记录。
使用场景:
- 客户说"之前聊过""上次""你看聊天记录""我发过图了"等暗示有历史对话时
- 客户第二次来访、追问进度、催单时
- 你不确定客户之前是否发过图或说过需求时
必须先调用此工具回顾历史,再回复客户,避免重复要求客户发图。
"""
logger.info(f"[Tool] 查询历史记录: customer_id={customer_id}, limit={num_messages}")
try:
rows = await asyncio.to_thread(get_conversation, customer_id, limit=num_messages)
if not rows:
return f"该客户({customer_id})暂无历史聊天记录。"
lines = []
has_images = False
customer_needs = []
for r in rows:
role = "客户" if r["direction"] == "in" else "客服"
ts = str(r.get("timestamp", ""))
msg = r.get("message", "")
line = f"[{ts}] {role}{msg}"
lines.append(line)
if r["direction"] == "in":
if "已收到" in msg and "" in msg:
has_images = True
if any(k in msg for k in ["找原图", "修复", "高清", "去背景", "抠图", "做衣服", "打印"]):
customer_needs.append(msg[:60])
summary_parts = [f"{len(rows)}条历史消息。"]
if has_images:
summary_parts.append("⚠️ 客户之前已经发过图片!不要再让客户发图!")
if customer_needs:
summary_parts.append(f"客户曾表达的需求:{''.join(customer_needs[:3])}")
summary = " ".join(summary_parts)
history_text = "\n".join(lines[-30:])
return f"【历史记录摘要】{summary}\n\n【详细记录】\n{history_text}"
except Exception as e:
logger.error(f"[Tool] 查询历史记录失败: {e}")
return f"查询历史记录失败: {e}"
def register_agent_tools(agent: Any): def register_agent_tools(agent: Any):
"""注册工具""" """注册工具"""
agent.tool(transfer_to_human_tool) agent.tool(transfer_to_human_tool)
agent.tool(check_order_status_tool) agent.tool(check_order_status_tool)
logger.info("[Agent] 工具箱已更新:称呼统一为“设计师”。") agent.tool(lookup_chat_history_tool)
logger.info("[Agent] 工具箱已更新:含转人工、订单查询、历史记录查询。")

View File

@@ -15,9 +15,18 @@ logger = logging.getLogger("cs_agent")
# 配置常量 # 配置常量
MSG_DEDUP_CAPACITY = 200 # 消息 ID 去重缓存容量 MSG_DEDUP_CAPACITY = 200 # 消息 ID 去重缓存容量
TRANSFER_COOLDOWN_SEC = 60 # 转接冷却时间(秒) TRANSFER_COOLDOWN_SEC = 120 # 转接冷却时间(秒)—— 转接后2分钟内不再调用AI
DEBOUNCE_SECONDS = 2.0 # 消息防抖延迟(秒) DEBOUNCE_SECONDS = 2.0 # 消息防抖延迟(秒)
# 转接后安抚话术池(轮换使用,避免复读)
_TRANSFER_CALM_REPLIES = [
"我在帮你催了哈,稍等下",
"已经转了哈,马上就来",
"收到,设计师在赶来了哈",
"好的亲,稍等一下哈",
"在催了在催了,马上哈",
]
class SystemOrchestrator: class SystemOrchestrator:
""" """
全系统总编排:具备转接冷却、防抖合并、多消息去重、以及精准日志。 全系统总编排:具备转接冷却、防抖合并、多消息去重、以及精准日志。
@@ -30,8 +39,9 @@ class SystemOrchestrator:
# 1. 消息 ID 去重 # 1. 消息 ID 去重
self._processed_msg_ids = deque(maxlen=MSG_DEDUP_CAPACITY) self._processed_msg_ids = deque(maxlen=MSG_DEDUP_CAPACITY)
# 2. 转接冷却存储 (customer_id -> last_transfer_time) # 2. 转接冷却存储 (session_key -> last_transfer_time)
self._last_transfer_time: Dict[str, float] = {} self._last_transfer_time: Dict[str, float] = {}
self._transfer_calm_idx: Dict[str, int] = {} # 安抚话术轮换索引
# 3. 防抖配置 # 3. 防抖配置
self._debounce_seconds = DEBOUNCE_SECONDS self._debounce_seconds = DEBOUNCE_SECONDS
@@ -68,12 +78,13 @@ class SystemOrchestrator:
# 店铺隔离:同一客户在不同店铺的对话独立处理 # 店铺隔离:同一客户在不同店铺的对话独立处理
session_key = f"{user_id}@{std_msg.acc_id}" session_key = f"{user_id}@{std_msg.acc_id}"
# 订单消息 / 纯金额通知:静默入库,不触发 AI 回复 # 订单消息 / 纯金额通知 / SKU信息:静默入库,不触发 AI 回复
msg_text = std_msg.content or "" msg_text = std_msg.content or ""
is_order = "[系统订单信息]" in msg_text is_order = "[系统订单信息]" in msg_text
is_price_only = bool(re.match(r'^[\s\n]*金?额?[:]?\s*[\d.]+\s*元', msg_text.strip())) is_price_only = bool(re.match(r'^[\s\n]*金?额?[:]?\s*[\d.]+\s*元', msg_text.strip()))
is_sku_only = bool(re.match(r'^[\s\n]*(备注[:]|数量[:]|款式[:])', msg_text.strip())) is_sku_only = bool(re.match(r'^[\s\n]*(备注[:]|数量[:]|款式[:]|定制[:])', msg_text.strip()))
if is_order or is_price_only or is_sku_only: is_sku_amount = bool(re.match(r'^[\s\n]*金额[:]\s*[\d.]+元\s*●', msg_text.strip()))
if is_order or is_price_only or is_sku_only or is_sku_amount:
await self._handle_order_packet(platform, std_msg) await self._handle_order_packet(platform, std_msg)
logger.info(f"[订单消息] user={user_id} acc={std_msg.acc_id} 已入库更新状态") logger.info(f"[订单消息] user={user_id} acc={std_msg.acc_id} 已入库更新状态")
await repo.save_chat(platform, user_id, msg_text, "in", acc_id=std_msg.acc_id) await repo.save_chat(platform, user_id, msg_text, "in", acc_id=std_msg.acc_id)
@@ -222,22 +233,25 @@ class SystemOrchestrator:
if all_image_urls: if all_image_urls:
asyncio.create_task(self._analyze_images_background(session_key, all_image_urls)) asyncio.create_task(self._analyze_images_background(session_key, all_image_urls))
# C. 冷却检查:如果转接冷却期内发过转接,告诉大脑"已处于转接中" # C. 冷却检查:转接成功后冷却期内直接回安抚话术不调AI
is_in_cooldown = (time.time() - self._last_transfer_time.get(session_key, 0)) < TRANSFER_COOLDOWN_SEC last_transfer = self._last_transfer_time.get(session_key, 0)
cooldown_elapsed = time.time() - last_transfer
is_in_cooldown = cooldown_elapsed < TRANSFER_COOLDOWN_SEC
# D. 思考
history = await repo.get_chat_history(user_id, limit=10, acc_id=acc_id)
if history and history[-1].get('content') == db_content: history = history[:-1]
# 冷却期内:禁止再发转接指令,避免反复转接
if is_in_cooldown: if is_in_cooldown:
final_msg.content = ( idx = self._transfer_calm_idx.get(session_key, 0)
"【系统:设计师已收到转接通知正在赶来,严禁再次调用转人工工具!" calm_reply = _TRANSFER_CALM_REPLIES[idx % len(_TRANSFER_CALM_REPLIES)]
"客户再问就回'设计师正在看了哈,稍等一下',换着说不要重复】\n" self._transfer_calm_idx[session_key] = idx + 1
+ final_msg.content logger.info(f"[Orchestrator] 转接冷却中({cooldown_elapsed:.0f}s),直接安抚: {calm_reply}")
std_res = StandardResponse(
reply_content=calm_reply,
metadata={"acc_id": acc_id, "acc_type": acc_type}
) )
else:
std_res = await self.brain.think_and_reply(final_msg, history=history) # D. 正常流程调用AI思考
history = await repo.get_chat_history(user_id, limit=10, acc_id=acc_id)
if history and history[-1].get('content') == db_content: history = history[:-1]
std_res = await self.brain.think_and_reply(final_msg, history=history)
# E. 发送并记录时间 # E. 发送并记录时间
if std_res.should_reply: if std_res.should_reply:

View File

@@ -57,29 +57,59 @@ class CustomerServiceBrain:
system_prompt = ( system_prompt = (
"你是一位专注【高清修复】和【找原图】的专业店主。性格干脆,说话自然、专业。\n\n" "你是一位专注【高清修复】和【找原图】的专业店主。性格干脆,说话自然、专业。\n\n"
"【统一称呼规范】\n" "【统一称呼规范 - 第一人称原则\n"
"1. 严禁使用'师傅''客服''专员'等词汇!必须统一称为【设计师】\n" "1. 你就是店主本人,未转接设计师之前,所有回复必须用第一人称:'''我这边'\n"
"2. 未转接前,用第一人称(我/我这边)。例如:'我叫设计师看下'\n\n" "2. 例如:客户问进度 → '我在看哈,稍等';客户催 → '我帮你催下哈'\n"
"3. 只有在需要转接时才提'设计师''我叫设计师来看下哈'\n"
"4. 严禁使用'师傅''客服''专员'等词汇。\n\n"
"【★★★ 历史记录查询 - 最高优先级 ★★★】\n"
"你有一个 lookup_chat_history_tool 工具,可以查询客户的完整历史聊天记录。\n"
"以下情况你【必须】先调用此工具查历史,再回复:\n"
"1. 客户说'之前聊过''上次''你看聊天记录''我发过了''前面发了'\n"
"2. 客户追问进度:'做好了吗''多久能好''怎么样了'\n"
"3. 客户表达不满或困惑:'''你瞎么''搞笑''说过了'\n"
"4. 【近期对话回顾】中显示客户之前已发过图或说过需求\n"
"查到历史后,根据历史内容回复,绝对不要再重复问客户已经回答过的问题!\n\n"
"【核心逻辑】\n" "【核心逻辑】\n"
"1. 业务:只聊高清修复和找原图。核心链路:引导发图 -> 问需求 -> 找设计师。\n" "1. 业务:只聊高清修复和找原图。核心链路:引导发图 -> 问需求 -> 找设计师。\n"
"2. **主动引导(关键)**:如果客户【没发图】就问能不能做、问收费,你必须回:'亲亲先发图我看下哈'\n" "2. **主动引导**:只有当客户【从未发过图】且没有历史图片记录时,才引导发图\n"
"3. **非业务问题**:如果客户问招聘、合作、闲聊等与做图无关的话题,礼貌拒绝'亲亲咱这边只做图哦,暂不招人哈'\n" "3. **非业务问题**:如果客户问招聘、合作、闲聊等与做图无关的话题,礼貌拒绝。\n"
"4. **客户说没有参考图**如果客户明确说'没有图''找不到''想让你们帮找'直接转人工:'好的,我这就叫设计师帮您找哈'\n" "4. **客户说没有参考图**:直接转人工:'好的,我这就叫设计师帮您找哈'\n"
"5. **客户问尺寸/能否打印/退款**这类问题需要设计师判断,直接转人工:'这个设计师帮您看下哈'\n" "5. **客户问尺寸/能否打印/退款**:直接转人工:'这个设计师帮您看下哈'\n"
"6. 转接时机:收到图片并明确需求后,立即调用转人工工具,并告知:'收到,正在呼叫设计师核价,稍等哈'\n" "6. 转接时机:收到图片并明确需求后,立即调用转人工工具。\n"
"7. **下线安抚(重要)**:只有当【本次】工具返回 'ERROR_NO_DESIGNER_ONLINE' 时才能说下班。不能根据历史对话或自己猜测说下班!\n" "7. **下线安抚**:只有工具返回ERROR时才能提设计师不在。根据错误码区分\n"
"8. 正在转接中:如果系统提示已在转接,回:'设计师正在赶来,我再帮你催下哈!'\n" " - ERROR_DESIGNER_NOT_STARTED → 说'还没上班,记下了上班马上处理'(严禁说下班)\n"
"9. **每次转接必须调用工具**:不要根据之前的结果猜测,每次需要转接都必须重新调用工具检查设计师是否在线。\n\n" " - ERROR_DESIGNER_OFFLINE → 说'下班了,需求记下明天回'\n"
" - ERROR_DESIGNER_BUSY → 说'稍等,我帮你联系下'(严禁说下班)\n"
"8. 正在转接中:如果系统提示已在转接,回:'已经在帮你催了哈,稍等下!'\n"
"9. **每次转接必须调用工具**:不要猜测,每次都重新调用。\n\n"
"【情绪识别与应急转人工】\n"
"当客户出现以下信号时,立即调用转人工工具,不要继续机械回复:\n"
"- 愤怒/辱骂:'''垃圾''投诉''差评''骗子'\n"
"- 反复质疑:'你是机器人吗''搞笑''你瞎么''说了多少遍'\n"
"- 连续不满客户连续2条以上表达不满'''...'、质问语气)\n"
"转人工话术:'亲亲抱歉,我马上叫设计师亲自来处理哈'\n\n"
"【确认短句收尾规则 - 千牛要求最后一句必须是客服说的】\n"
"客户说'''''好的''''ok''''知道了'等确认短句时,\n"
"必须回一句自然的收尾,但严禁复读'嗯咯'!根据上下文选择合适的收尾:\n"
"- 如果刚谈完需求/报价 → '有问题随时找我哈'\n"
"- 如果刚说了等设计师 → '好的,有消息马上告诉你'\n"
"- 如果是闲聊结束 → '好嘞~'\n"
"每次收尾话术不能重复,要自然变化。\n\n"
"【必杀令 - 严格遵守】\n" "【必杀令 - 严格遵守】\n"
"1. 每句回复严禁超过15个字语气淘宝亲切风多用''''\n" "1. 每句回复严禁超过15个字语气淘宝亲切风多用''''\n"
"2. 严禁报价,严禁复读图片已收到的情况。\n" "2. 严禁报价,严禁复读图片已收到的情况。\n"
"3. 必须原样输出工具返回的'正在为您转接|'指令。\n" "3. 必须原样输出工具返回的'正在为您转接|'指令。\n"
"4. **严禁**说'在呢铁子'!只能说'在呢''在呢亲'\n" "4. **严禁**说'在呢铁子'!只能说'在呢''在呢亲'\n"
"5. **严禁**重复发送相同内容!如果刚说过的话,换一种说法。\n" "5. **严禁**连续两次回复相同或相似内容!回顾你最近说过的话,换一种说法。\n"
"6. **严禁**输出任何代码、标记、括号等乱码!只输出自然语言。\n" "6. **严禁**输出任何代码、标记、括号等乱码!只输出自然语言。\n"
"7. **严禁**自己臆造'下班'只有工具返回ERROR才能说下班。\n\n" "7. **严禁**自己臆造'下班'只有工具返回ERROR才能说下班。\n"
"8. **严禁**在客户已发过图的情况下还说'先发图来看看'!先查历史确认。\n\n"
f"业务参考:\n{all_skills}" f"业务参考:\n{all_skills}"
) )
@@ -109,7 +139,7 @@ class CustomerServiceBrain:
lines.append(f"[{_fmt_time(h.get('timestamp'))}] {role}{content}") lines.append(f"[{_fmt_time(h.get('timestamp'))}] {role}{content}")
recent_context = "【近期对话回顾】\n" + "\n".join(lines) + "\n----------------\n" recent_context = "【近期对话回顾】\n" + "\n".join(lines) + "\n----------------\n"
full_input = f"{recent_context}现在的对话:{user_content}" full_input = f"【当前客户ID{msg.user_id}\n{recent_context}现在的对话:{user_content}"
logger.info( logger.info(
f"[PROMPT->AI] user={msg.user_id} acc={msg.acc_id} images={len(msg.image_urls)}\n" f"[PROMPT->AI] user={msg.user_id} acc={msg.acc_id} images={len(msg.image_urls)}\n"
f"{_clip(full_input)}" f"{_clip(full_input)}"
@@ -117,29 +147,29 @@ class CustomerServiceBrain:
result = await self.agent.run(full_input, message_history=history) result = await self.agent.run(full_input, message_history=history)
# --- 终极修复:强制截获工具返回的转接指令 --- # --- 转接指令:直接从工具返回截获,不经过 AI 二次加工 ---
reply_text = "" transfer_cmd = ""
# pydantic-ai 1.x 使用 result.output旧版 0.x 使用 result.data
raw_output = getattr(result, 'output', None) or getattr(result, 'data', None)
if isinstance(raw_output, str):
reply_text = raw_output
# 暴力扫描所有消息片段,寻找转接暗号
found_magic = ""
for m in result.all_messages(): for m in result.all_messages():
if hasattr(m, 'parts'): if hasattr(m, 'parts'):
for part in m.parts: for part in m.parts:
# 检查是否是工具返回片段
if getattr(part, 'part_kind', '') == 'tool-return': if getattr(part, 'part_kind', '') == 'tool-return':
content = str(getattr(part, 'content', '')) content = str(getattr(part, 'content', ''))
if "[转移会话]" in content: if "[转移会话]" in content:
found_magic = content transfer_cmd = content
# 如果 AI 弄丢了暗号,我们强行给它补回来 if transfer_cmd:
if found_magic and "[转移会话]" not in reply_text: logger.info(f"[Brain] 工具返回转接指令直接发送跳过AI加工: {transfer_cmd[:60]}")
logger.info(f"[Brain] 检测到 AI 弄丢了转接暗号,正在强制恢复: {found_magic[:30]}...") return StandardResponse(
reply_text = found_magic reply_content=transfer_cmd,
# ---------------------------------------- need_transfer=True,
metadata={"acc_id": msg.acc_id, "acc_type": msg.acc_type}
)
# --- 非转接场景:取 AI 的正常回复 ---
reply_text = ""
raw_output = getattr(result, 'output', None) or getattr(result, 'data', None)
if isinstance(raw_output, str):
reply_text = raw_output
# 清理模型泄露的内部标记/乱码(覆盖所有已知格式) # 清理模型泄露的内部标记/乱码(覆盖所有已知格式)
reply_text = re.sub(r'\[\]<\|[^|]+\|>', '', reply_text) reply_text = re.sub(r'\[\]<\|[^|]+\|>', '', reply_text)
@@ -147,9 +177,9 @@ class CustomerServiceBrain:
reply_text = re.sub(r'\[Function[^\]]*\]', '', reply_text) reply_text = re.sub(r'\[Function[^\]]*\]', '', reply_text)
reply_text = re.sub(r'\[/?Tool[^\]]*\]', '', reply_text) reply_text = re.sub(r'\[/?Tool[^\]]*\]', '', reply_text)
reply_text = re.sub(r'</?tool[_\-]?[^>]*>', '', reply_text, flags=re.IGNORECASE) reply_text = re.sub(r'</?tool[_\-]?[^>]*>', '', reply_text, flags=re.IGNORECASE)
reply_text = re.sub(r'<think[^>]*>.*?</think[^>]*>', '', reply_text, flags=re.DOTALL) reply_text = re.sub(r'<think[_a-zA-Z0-9]*[^>]*>.*?</think[_a-zA-Z0-9]*[^>]*>', '', reply_text, flags=re.DOTALL)
reply_text = re.sub(r'<think[^>]*>.*', '', reply_text, flags=re.DOTALL) reply_text = re.sub(r'<think[_a-zA-Z0-9]*[^>]*>.*', '', reply_text, flags=re.DOTALL)
reply_text = re.sub(r'</?think[^>]*>', '', reply_text) reply_text = re.sub(r'</?think[_a-zA-Z0-9]*[^>]*>', '', reply_text)
reply_text = re.sub(r'```[^`]*```', '', reply_text) reply_text = re.sub(r'```[^`]*```', '', reply_text)
reply_text = re.sub(r'\{["\'][^}]+\}', '', reply_text) reply_text = re.sub(r'\{["\'][^}]+\}', '', reply_text)
reply_text = re.sub(r'AgentRunResult\([^)]*\)', '', reply_text) reply_text = re.sub(r'AgentRunResult\([^)]*\)', '', reply_text)
@@ -176,4 +206,4 @@ class CustomerServiceBrain:
except Exception as e: except Exception as e:
logger.error(f"[Brain Error]: {e}") logger.error(f"[Brain Error]: {e}")
return StandardResponse(reply_content="好哒,设计师正在看图,稍等回你。", metadata={"acc_id": msg.acc_id}) return StandardResponse(reply_content="好哒,在看图,稍等回你", metadata={"acc_id": msg.acc_id})

View File

@@ -1,19 +0,0 @@
"你是一位专注【高清修复】和【找原图】的专业店主。性格干脆,说话高端、专业。\n\n"
"【统一称呼规范】\n"
"1. 严禁使用'师傅'、'客服'、'专员'等词汇!\n"
"2. 必须统一称呼为【设计师】。比如:'找设计师看下'、'设计师马上来'、'等设计师核价'。\n\n"
"【核心逻辑】\n"
"1. 业务:只聊高清修复和找原图。引导发图 -> 问需求 -> 找设计师。\n"
"2. 下线安抚:如果工具返回 'ERROR_NO_DESIGNER_ONLINE',说明设计师们【下班/下线】了。回:'亲亲,设计师现在下班啦,需求我先记下,明天第一时间回您哈!'。\n"
"3. 正在转接中:如果看到系统提示已在转接,回:'设计师正在赶来,我再帮你催下哈!'。\n\n"
"4. 没转接时:引导发图 -> 问需求 -> 调工具转人工。\n\n
"5. 语气:淘宝亲切风,多用'亲亲'、'铁子'。每句回复【严禁超过15字】\n\n"
"【必杀令】\n"
"1. 每句回复严禁超过15个字\n"
"2. 严禁报价,严禁复读图片已收到的情况。\n"
"3. 必须原样输出工具返回的'正在为您转接|'指令。\n\n"
f"业务参考:\n{all_skills}"

View File

@@ -26,12 +26,18 @@ description: 找原图/高清修复客服 - 需求收集、阶段引导与转接
## 上下文承接逻辑让AI变聪明的关键 ## 上下文承接逻辑让AI变聪明的关键
- **回访/二次进店识别(最高优先级)**
- 客户说「之前聊过 / 上次 / 你看聊天记录 / 我发过了 / 前面发了图」 => **必须先调用 lookup_chat_history_tool 查历史**,根据历史回复,严禁再要图。
- 客户问「做好了吗 / 多久能好 / 怎么样了 / 好了吗」 => 这是进度追问,先查历史确认之前的需求,用第一人称回:'我在看哈,稍等'。
- **短句识别** - **短句识别**
- 「有吗 / 有没有 / 找到了吗」 => 进度追问。回:'设计师正在看哈,稍等'。 - 「有吗 / 有没有 / 找到了吗」 => 进度追问。回:'在看哈,稍等'。
- 「就这一个 / 没有了 / 就这些」 => 拿图完成。立即引导转接。 - 「就这一个 / 没有了 / 就这些」 => 拿图完成。立即引导转接。
- 「高清 / 重新发 / 发我」 => 催办。正面承接。 - 「高清 / 重新发 / 发我」 => 催办。正面承接。
- **多图关联识别** - **多图关联识别**
- 客户发第二张图时提到「上一张」「前面那张」「局部」「细节」 => 按【同一需求补充】处理,不要当成新单。 - 客户发第二张图时提到「上一张」「前面那张」「局部」「细节」 => 按【同一需求补充】处理,不要当成新单。
- **情绪升级识别**
- 客户连续表达不满(质问、辱骂、威胁投诉)=> 立即转人工,话术:'亲亲抱歉,我马上叫设计师来处理'。
- 严禁在客户愤怒时继续机械重复同一句话。
## 业务红线 ## 业务红线
- **绝对不说**'客服'、'师傅'、'专员'、'AI做的'、'修复'(如果是找原图单)、'处理'。 - **绝对不说**'客服'、'师傅'、'专员'、'AI做的'、'修复'(如果是找原图单)、'处理'。