- Implemented `_merged_items` and `_merged_memory_text` functions to consolidate consult and chitchat memories, improving context awareness in intent classification and response generation. - Updated intent prompts to include recent dialogue history, aiding in the resolution of ambiguous user queries. - Enhanced `search_knowledge` tool to utilize context window for better query understanding, addressing issues with omitted references in user inputs. - Fixed existing test cases to reflect changes in intent constants and ensure accurate context handling during tests. This update significantly improves the handling of multi-turn dialogues, ensuring a more coherent and contextually aware interaction for users.
440 lines
20 KiB
Python
440 lines
20 KiB
Python
"""已注册客户 Agent(customer)的 DeepSeek prompt 模板与关键词快路由。
|
||
|
||
意图集 12 类:游客 7 类(product_consult/policy_interpret/faq/chit_chat/
|
||
reject/transfer_human/fallback)+ 本人数据查询 4 类
|
||
(holding_query/transaction_query/risk_assessment_query/suitability_check)
|
||
+ save_note(用户主动要求记忆某事,如"你要记住我每天看净值")。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import re
|
||
|
||
from app.config.profile_slots import build_extraction_prompt
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 意图分类 prompt
|
||
# ---------------------------------------------------------------------------
|
||
|
||
INTENT_SYSTEM = """你是金融客服(已登录客户模式)意图分类器。将用户输入分为以下之一:
|
||
- holding_query:查询本人持仓(我持有/买了什么基金/持仓市值/盈亏情况/我的资产)
|
||
- transaction_query:查询本人交易流水(交易记录/申购赎回记录/最近买卖/账单/流水)
|
||
- risk_assessment_query:查询本人风险测评(我的风评/风险等级/测评结果/问卷得分/风险承受能力)
|
||
- suitability_check:本人购买适当性匹配(我能买XX吗/R几产品适合我吗/买这个匹配吗/适当性)
|
||
- nav_query:查询具体产品最新净值(XX基金净值多少/单位净值/产品净值;系统只读库,非实时盘口)
|
||
- product_consult:咨询具体产品知识(基金、理财、费用、风险等级含义、产品分类等非个人账户问题)
|
||
- policy_interpret:咨询政策法规(投资者适当性、反洗钱、KYC、销售合规、投诉处理、冷静期、双录)
|
||
- faq:常见问答(开户、身份认证、账户操作、App 使用等通用问题)
|
||
- chit_chat:问候、闲聊、非金融业务
|
||
- save_note:用户主动要求记忆某事(如"你要记住我每天看净值"、"帮我记一下我不买私募")
|
||
- reject:客服无法处理且不需要转人工的请求(见下方规则)
|
||
- transfer_human:用户明确要求人工客服,或投诉/纠纷/安全问题
|
||
|
||
规则:
|
||
1. 只输出一个标签,不要任何解释或标点
|
||
2. 若无法判断,输出 fallback
|
||
3. 涉及"推荐/建议买/卖什么好/包赚/保本/稳赚/零风险"等投资建议请求一律 reject
|
||
4. 涉及"明天会涨/收益预测/能赚多少/走势"等走势预测一律 reject
|
||
5. 涉及"和其他平台比/哪个平台好"等竞品对比一律 reject
|
||
6. 涉及实时行情/实时价格查询一律 reject
|
||
7. "能买/可以买/适合买/匹配吗/我能买什么/哪些产品我能买"是适当性查询(suitability_check),不是投资建议
|
||
8. 查询本人账户/持仓/流水/风评/产品净值是已登录客户的合法功能,不要输出 reject
|
||
9. 仅当用户明确要求"转人工/人工客服",或涉及投诉/纠纷/账户异常/被盗等安全问题时,才输出 transfer_human
|
||
10. 用户明确要求"你要记住/帮我记/记一下/别忘了/记着"等记忆指令时,输出 save_note
|
||
11. "重新测评/重做风评/更新风评"→ risk_assessment_query(引导至 App/网点正式流程,Agent 不代填问卷)
|
||
12. 用户同时问「有哪些 R4/R5 产品」并表达购买/申购意愿 → suitability_check(先列产品与适当性判定),不是 trade_action
|
||
13. 用户点名具体产品问能否买、或「申购《某产品》」但未给金额 → suitability_check 或继续追问金额;禁止把「看到申购二字」一律当成已发起交易
|
||
14. 只有用户明确要**提交**申购/赎回/转换且信息足够(或已在多轮中补齐槽位)时,才输出 trade_action
|
||
15. 用户输入可能含省略指代(如「那申购呢」「那这个能买吗」),须结合「近期对话」判断指代对象后再分类"""
|
||
|
||
INTENT_USER_TEMPLATE = "近期对话:\n{memory}\n\n用户输入:{message}"
|
||
|
||
VALID_INTENTS = frozenset({
|
||
"holding_query",
|
||
"transaction_query",
|
||
"risk_assessment_query",
|
||
"suitability_check",
|
||
"nav_query",
|
||
"trade_action",
|
||
"product_consult",
|
||
"policy_interpret",
|
||
"faq",
|
||
"chit_chat",
|
||
"save_note",
|
||
"reject",
|
||
"transfer_human",
|
||
"fallback",
|
||
})
|
||
|
||
# 数据查询类意图(需要调 core_ro_tool)
|
||
DATA_QUERY_INTENTS = frozenset({
|
||
"holding_query",
|
||
"transaction_query",
|
||
"risk_assessment_query",
|
||
"suitability_check",
|
||
"nav_query",
|
||
})
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 关键词快速路由(不调 LLM;优先级:转人工 > 拒绝 > 数据查询)
|
||
# ---------------------------------------------------------------------------
|
||
|
||
_TRANSFER_KEYWORDS = (
|
||
"转人工", "人工客服", "人工服务", "联系客服", "找客服",
|
||
"投诉", "纠纷", "账户异常", "被盗", "盗刷",
|
||
)
|
||
|
||
# 拒绝关键词:客服边界外(投顾/预测/竞品/实时行情),直接拒绝不转人工
|
||
_REJECT_ADVICE_KW = (
|
||
"推荐", "建议买", "建议卖", "买什么好", "卖什么好", "包赚", "保本",
|
||
"稳赚", "零风险", "无风险", "躺赚", "保本保息", "刚性兑付", "荐股",
|
||
)
|
||
_REJECT_PREDICT_KW = (
|
||
"会涨", "会跌", "收益预测", "走势预测", "能赚多少", "收益率多少",
|
||
"涨跌", "明天行情", "后市", "还能涨",
|
||
)
|
||
_REJECT_COMPARE_KW = (
|
||
"和其他平台", "哪个平台好", "对比一下", "比一比", "哪家银行好", "别家",
|
||
)
|
||
_REJECT_REALTIME_KW = (
|
||
"实时行情", "实时价格", "现在价格", "当前价格", "最新行情",
|
||
"今日行情",
|
||
)
|
||
|
||
# C-11:匹配说明类(走 suitability,不走 reject)
|
||
_ELIGIBLE_PRODUCTS_KW = (
|
||
"我能买什么", "可以买什么", "哪些产品我能买", "适合买什么", "匹配的产品",
|
||
"有什么产品适合我", "有什么可以买", "能买什么", "哪些可以买",
|
||
)
|
||
|
||
# C-05:产品净值(Core 只读库,非实时盘口)
|
||
_NAV_KW = (
|
||
"净值", "单位净值", "最新净值", "基金净值", "产品净值",
|
||
)
|
||
|
||
# 数据查询关键词(本人数据)
|
||
_HOLDING_KW = (
|
||
"我的持仓", "持仓", "持有", "买了什么", "买了啥", "买的什么",
|
||
"我的基金", "我的理财", "我的资产", "仓位", "市值", "盈亏",
|
||
"赚了多少", "亏了多少", "挣了多少",
|
||
)
|
||
_TRADE_KW = (
|
||
"流水", "交易记录", "交易明细", "申购记录", "赎回记录",
|
||
"最近买", "最近卖", "账单", "买卖记录", "成交记录",
|
||
"申购了", "赎回了", "申购过", "赎回过", "申购啥", "买过啥",
|
||
)
|
||
_RISK_KW = (
|
||
"我的风评", "风评", "风险测评", "风险评估", "风险等级", "风险承受能力",
|
||
"测评结果", "测评问卷", "问卷得分", "我是稳健", "我是保守", "C1", "C2", "C3", "C4", "C5",
|
||
"重新测评", "重做风评", "更新风评",
|
||
)
|
||
_SUITABILITY_KW = (
|
||
"能买", "可以买", "可买", "适合买", "适合我", "匹配吗", "能不能买",
|
||
"购买资格", "适当性", "买这个", "买r", "R1产品", "R2产品", "R3产品", "R4产品", "R5产品",
|
||
)
|
||
|
||
# 备注(用户主动要求记忆)
|
||
_SAVE_NOTE_KW = (
|
||
"你要记住", "帮我记", "记一下", "记住", "别忘了", "记着",
|
||
"帮我记住", "你要记",
|
||
)
|
||
|
||
# 问候/闲聊快路由(不调 LLM;避免 DeepSeek 不可用时「你好」落入 fallback)
|
||
_GREETING_KW = (
|
||
"你好", "您好", "你好呀", "您好呀", "嗨", "hello", "hi",
|
||
"在吗", "在不在", "早上好", "下午好", "晚上好", "哈喽",
|
||
)
|
||
|
||
|
||
from app.service.trade_action_service import (
|
||
looks_like_trade_execute,
|
||
should_use_suitability_instead_of_trade,
|
||
)
|
||
|
||
|
||
def keyword_route(message: str) -> tuple[str, str] | None:
|
||
"""关键词快速路由。返回 (intent, preset_reply);无命中返回 None。
|
||
|
||
preset_reply 非空时意图为 reject 且回复已确定。
|
||
"""
|
||
msg = message or ""
|
||
|
||
if any(k in msg for k in _TRANSFER_KEYWORDS):
|
||
return ("transfer_human", "")
|
||
|
||
stripped = msg.strip().lower()
|
||
if stripped in {"hi", "hello"} or any(k in msg for k in _GREETING_KW):
|
||
return ("chit_chat", "")
|
||
|
||
# 拒绝类(边界外请求)
|
||
if any(k in msg for k in _REJECT_ADVICE_KW):
|
||
return ("reject", "advice")
|
||
if any(k in msg for k in _REJECT_PREDICT_KW):
|
||
return ("reject", "predict")
|
||
if any(k in msg for k in _REJECT_COMPARE_KW):
|
||
return ("reject", "compare")
|
||
if any(k in msg for k in _REJECT_REALTIME_KW):
|
||
return ("reject", "realtime")
|
||
if "实时净值" in msg:
|
||
return ("reject", "realtime")
|
||
|
||
if should_use_suitability_instead_of_trade(msg):
|
||
return ("suitability_check", "")
|
||
|
||
if looks_like_trade_execute(msg):
|
||
return ("trade_action", "")
|
||
|
||
# 数据查询类(本人数据,优先于 save_note)
|
||
if any(k in msg for k in _TRADE_KW):
|
||
return ("transaction_query", "")
|
||
if any(k in msg for k in _RISK_KW):
|
||
return ("risk_assessment_query", "")
|
||
if any(k in msg for k in _SUITABILITY_KW):
|
||
return ("suitability_check", "")
|
||
if re.search(r"[Rr]\s*[1-5]", msg) and any(k in msg for k in ("哪些", "有哪些", "列出", "罗列", "什么产品", "在售")):
|
||
return ("suitability_check", "")
|
||
if any(k in msg for k in _HOLDING_KW):
|
||
return ("holding_query", "")
|
||
|
||
if any(k in msg for k in _SAVE_NOTE_KW):
|
||
return ("save_note", "")
|
||
|
||
# C-11 / C-05:数据查询扩展
|
||
if any(k in msg for k in _ELIGIBLE_PRODUCTS_KW):
|
||
return ("suitability_check", "")
|
||
if any(k in msg for k in _NAV_KW):
|
||
return ("nav_query", "")
|
||
|
||
return None
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 参数抽取 prompt(时间范围 / 产品名 / 风险等级)
|
||
# ---------------------------------------------------------------------------
|
||
|
||
PARAM_EXTRACT_SYSTEM = """你是金融客服的查询参数抽取器。从用户输入抽取查询参数,输出严格 JSON。
|
||
|
||
可抽取字段:
|
||
- months:整数,时间范围(月)。"最近3个月"→3,"近半年/半年内"→6,"今年以来"→当前已过月数(至少1),"最近一周/几天"→1,未提及时间→null
|
||
- product_keyword:字符串,用户提到的具体产品名称片段(如"医药健康精选""稳利90天理财");仅风险等级(如 R3)时留 null
|
||
- risk_level:字符串,"R1"~"R5",仅当用户提到风险等级(如"R3产品""R几");未提及→null
|
||
|
||
规则:
|
||
1. 只输出 JSON 对象,如 {"months": 3, "product_keyword": null, "risk_level": "R3"}
|
||
2. months 范围 1~36;超出按边界裁剪;无法判断时间一律 null
|
||
3. product_keyword 去掉"产品/基金/理财"等无区分度尾词前后的多余称呼,保留产品名核心片段,最长 32 字
|
||
4. 用户说"我能买R3的吗"→risk_level="R3",product_keyword=null
|
||
5. 用户问"005827净值多少"→product_keyword="005827"
|
||
6. 用户问"有哪些R4和R5产品"→risk_level 可填 null,产品名留 null(工具从原文解析多个 R 等级)
|
||
7. 无法抽取任何参数时输出 {}"""
|
||
|
||
PARAM_EXTRACT_USER_TEMPLATE = "用户输入:{message}"
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 工具结果解读 prompt(数值不经过 LLM 生成)
|
||
# ---------------------------------------------------------------------------
|
||
|
||
INTERPRET_SYSTEM = """你是金融客服助手(已登录客户模式)。请基于系统查询返回的"事实数据",用自然、简明的中文回复客户。
|
||
|
||
铁律(违反将被拦截):
|
||
1. 事实数据中的所有数字、日期、产品名称、风险等级、产品数量必须原样引用,禁止修改、推算、补充或编造任何数值
|
||
2. 事实数据未包含的信息,明确告知"系统中未查询到相关记录",不得猜测或用常识补全
|
||
3. 你不是投资顾问、不是交易员:不做买卖建议、不预测走势、不推荐产品、不承诺收益
|
||
4. 适当性匹配结果是系统只读判定:仅解释"匹配/不匹配/需风险揭示/测评过期/需网点确认"的规则原因,
|
||
不得鼓励、催促或代替客户做出购买决定;**需风险揭示(双录)或需网点确认的产品,须引导联系理财师/代理人或转人工,不得在助手内收金额或下单**
|
||
5. 回复不超过 400 字,不要输出 JSON、字段名或内部编码"""
|
||
|
||
INTERPRET_USER_TEMPLATE = """事实数据(系统查询结果,数值以此为准):
|
||
{fact_text}
|
||
|
||
客户备注:
|
||
{notes_context}
|
||
|
||
近期对话:
|
||
{memory}
|
||
|
||
用户问题:{message}
|
||
|
||
请回复:"""
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# RAG 咨询生成 prompt(客户版,含画像使用红线)
|
||
# ---------------------------------------------------------------------------
|
||
|
||
GENERATE_SYSTEM = """你是金融客服助手(已登录客户模式),职责:基于知识库检索结果,回答用户关于产品、政策、FAQ 的事实性问题。
|
||
|
||
铁律(违反将被拦截):
|
||
1. 严禁投资建议、收益预测、买卖指导、保本承诺
|
||
2. 仅基于下方"知识库"回答;知识库无答案则明确说明无法回答
|
||
3. 不得编造产品代码、费率、到账时间等具体数值
|
||
4. 简明扼要,中文,不超过 400 字
|
||
5. "客户画像参考"仅用于理解客户语境、调整措辞:
|
||
- 禁止主动念出画像内容(不得复述客户年龄/收入/家庭情况等)
|
||
- 禁止依据画像推断客户未说出的隐私信息
|
||
- 画像不改变任何产品规则、费率、适当性结论
|
||
6. 涉及个人账户数据(持仓/流水/风评)的问题,告知客户可直接询问"我的持仓/我的风评"等"""
|
||
|
||
GENERATE_USER_TEMPLATE = """知识库:
|
||
{rag_context}
|
||
|
||
客户画像参考:
|
||
{profile_context}
|
||
|
||
近期对话:
|
||
{memory}
|
||
|
||
用户问题:{message}
|
||
|
||
请回答:"""
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 闲聊 prompt(客户版)
|
||
# ---------------------------------------------------------------------------
|
||
|
||
CHITCHAT_SYSTEM = """你是金融客服助手(已登录客户模式),正在与用户轻松闲聊。
|
||
|
||
规则:
|
||
1. 仅日常友好对话(问候、客服导航、营业时间、功能介绍等)
|
||
2. 严禁任何投资建议、产品推荐、收益预测、买卖指导
|
||
3. 用户问及个人账户(持仓/流水/风评/能买什么),引导其直接提问,如"您可以问我'我的持仓'或'我能买R3产品吗'"
|
||
4. 用户需要投资建议时,引导其联系专属理财顾问或转人工
|
||
5. 保持简短(不超过 100 字)
|
||
6. 「客户画像参考」仅用于理解语境、自然衔接话题,禁止主动念出或复述画像内容(城市/年龄段等)"""
|
||
|
||
CHITCHAT_USER_TEMPLATE = """客户画像参考:
|
||
{profile_context}
|
||
|
||
客户备注:
|
||
{notes_context}
|
||
|
||
近期对话:
|
||
{memory}
|
||
|
||
用户:{message}
|
||
客服:"""
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 画像抽槽 prompt(由 profile_slots 槽位表自动派生;输入为最近对话窗口)
|
||
# ---------------------------------------------------------------------------
|
||
|
||
PROFILE_EXTRACT_SYSTEM = build_extraction_prompt()
|
||
PROFILE_EXTRACT_USER_TEMPLATE = """最近对话窗口:
|
||
{window}
|
||
|
||
请从对话中抽取客户画像信息:"""
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 客户显式备注抽取 prompt(save_note 意图;从用户原话抽取纯净备注内容 + 可选分类)
|
||
# ---------------------------------------------------------------------------
|
||
|
||
NOTE_EXTRACT_SYSTEM = """你是金融客服的备注内容抽取器。用户说"你要记住/帮我记/记一下"等指令,要求记忆某事。
|
||
从用户消息中抽取要记住的纯净内容(剥掉"你要记住/帮我记"等指令词),并标注分类。
|
||
|
||
输出严格 JSON,形如:
|
||
{"content": "每天看基金净值", "category": "habit"}
|
||
|
||
字段:
|
||
- content:剥掉指令词后的纯净备注内容,≤500 字,禁止为空
|
||
- category:可选,取值 habit(行为习惯)/ preference(偏好)/ reminder(提醒)/ other(其他);无法判断时为 null
|
||
|
||
规则:
|
||
1. 只输出一个 JSON 对象,不要任何解释
|
||
2. content 必须是用户真正要记住的内容,不是指令本身("你要记住我每天看净值" → content="每天看基金净值")
|
||
3. 用户消息只是"记住/记一下"无具体内容时,content=null,此时返回 {"content": null, "category": null}"""
|
||
|
||
NOTE_EXTRACT_USER_TEMPLATE = "用户消息:{message}\n请抽取要记住的内容:"
|
||
|
||
# 备注写入成功后的固定回复
|
||
NOTE_SAVED_TEXT = "好的,我已经记下来了。"
|
||
# 抽取失败(content 为空或 LLM 异常)时的兜底回复
|
||
NOTE_EMPTY_TEXT = "您要记住的内容我没有完全理解,能否再具体描述一下?"
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 静态话术(客户视角)
|
||
# ---------------------------------------------------------------------------
|
||
|
||
REJECT_ADVICE_TEXT = (
|
||
"我是客服助手,负责事实查询和规则解读,不能提供投资建议、产品推荐或买卖指导,"
|
||
"也不能预测收益。投资决策需要结合您的风险承受能力,"
|
||
"建议您联系专属理财顾问,或回复「转人工」由客服人员为您服务。"
|
||
)
|
||
|
||
REJECT_PREDICT_TEXT = (
|
||
"市场走势和未来收益无法预测,我不能提供涨跌判断或收益预估。"
|
||
"我可以为您解读产品规则、查询您的持仓与风评信息。"
|
||
)
|
||
|
||
REJECT_COMPARE_TEXT = (
|
||
"我无法对其他平台或机构进行比较评价。"
|
||
"我可以为您介绍本行产品的规则、费用和风险等级,您也可以咨询专属理财顾问。"
|
||
)
|
||
|
||
REJECT_REALTIME_TEXT = (
|
||
"实时行情和实时价格不在客服查询范围内。"
|
||
"您可以在 App「行情」页面查看实时净值,或回复「转人工」咨询客服人员。"
|
||
)
|
||
|
||
REJECT_BY_KIND = {
|
||
"advice": REJECT_ADVICE_TEXT,
|
||
"predict": REJECT_PREDICT_TEXT,
|
||
"compare": REJECT_COMPARE_TEXT,
|
||
"realtime": REJECT_REALTIME_TEXT,
|
||
}
|
||
|
||
# DeepSeek 分类为 reject 但关键词路由未命中具体类别时的兜底话术
|
||
REJECT_GENERAL_TEXT = (
|
||
"抱歉,这个请求超出了客服助手的职责范围。"
|
||
"我可以为您查询持仓与流水、解读风评结果、说明产品适当性规则,"
|
||
"或解答产品知识与常见问题。投资决策请咨询您的专属理财顾问。"
|
||
)
|
||
|
||
TRANSFER_TEXT = (
|
||
"已为您安排转接人工客服。您也可以:\n"
|
||
"- 拨打客服热线 95XXX(工作日 9:00-18:00)\n"
|
||
"- 前往最近的营业网点办理\n"
|
||
"- 联系您的专属理财顾问获取个性化投资服务"
|
||
)
|
||
|
||
FALLBACK_TEXT = (
|
||
"抱歉,我暂时无法回答这个问题。您可以:\n"
|
||
"1. 换一种方式描述您的问题\n"
|
||
"2. 直接问我「我的持仓」「我的风评」「我能买R3产品吗」等账户问题\n"
|
||
"3. 拨打客服热线 95XXX 或回复「转人工」"
|
||
)
|
||
|
||
# LLM 不可用时的闲聊降级(仍走 chit_chat 意图,避免误用 FALLBACK 话术)
|
||
CHITCHAT_DEGRADED_TEXT = (
|
||
"您好,我是您的财富助手。您可以问我「我的持仓」「最近流水」或「我能买什么产品」,"
|
||
"也可以直接描述您的问题。"
|
||
)
|
||
|
||
# 数据查询工具调用失败时的兜底
|
||
DATA_ERROR_TEXT = "暂时无法查询您的数据,请稍后再试,或回复「转人工」联系客服人员。"
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 会话归档摘要 prompt(CS-C-11)
|
||
# ---------------------------------------------------------------------------
|
||
|
||
ARCHIVE_SUMMARY_SYSTEM = """你是金融客服的会话摘要生成器。根据会话内容生成一段简明摘要。
|
||
|
||
规则:
|
||
1. 摘要不超过 200 字,中文
|
||
2. 概括:客户咨询/查询的主要主题、关注的产品或问题类型、情绪与满意度线索、是否建议人工跟进
|
||
3. 只陈述对话中出现的事实,禁止编造或推测对话中没有的信息
|
||
4. 禁止出现身份证号、手机号、银行卡号等敏感数字(如出现请以「已脱敏」代替)
|
||
5. 只输出摘要正文,不要任何前缀或解释"""
|
||
|
||
ARCHIVE_SUMMARY_USER_TEMPLATE = """会话消息数:{count}
|
||
|
||
会话内容:
|
||
{window}
|
||
|
||
请生成摘要:"""
|