- Added `ThresholdRepository` for managing customer loss threshold configurations and notifications. - Introduced `threshold_service` to handle loss threshold alerts based on customer portfolio performance. - Enhanced `customer_prompts` to include new intent for querying product net values. - Updated `customer_service` to integrate new threshold alert functionality into existing workflows. - Implemented `sanitize_postprocess` for improved compliance handling in customer interactions. - Enhanced course documentation to reflect updates in advisor training modules and interactive elements. This update significantly improves the customer experience by providing proactive loss threshold notifications and enhancing the overall service framework.
958 lines
61 KiB
HTML
958 lines
61 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>客户财富 Agent 深潜 · 交互导览</title>
|
||
|
||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||
<link href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,400;12..96,600;12..96,700;12..96,800&family=DM+Sans:ital,opsz,wght@0,9..40,300;0,9..40,400;0,9..40,500;0,9..40,600;0,9..40,700;1,9..40,400;1,9..40,500&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||
|
||
<link rel="stylesheet" href="styles.css">
|
||
|
||
<style>
|
||
:root {
|
||
--color-accent: #2D8B55;
|
||
--color-accent-hover: #236B44;
|
||
--color-accent-light: #E8F5ED;
|
||
--color-accent-muted: #5BAE7A;
|
||
}
|
||
</style>
|
||
|
||
<script src="main.js" defer></script>
|
||
</head>
|
||
<body>
|
||
|
||
<nav class="nav" id="nav">
|
||
<div class="progress-bar" id="progress-bar" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100"></div>
|
||
<div class="nav-inner">
|
||
<span class="nav-title">客户财富 Agent 深潜</span>
|
||
<div class="nav-dots" id="nav-dots" role="tablist">
|
||
<button class="nav-dot" data-target="module-1" data-tooltip="能做什么" role="tab" aria-label="模块 1: 能做什么"></button>
|
||
<button class="nav-dot" data-target="module-2" data-tooltip="14 节点图" role="tab" aria-label="模块 2: 14 节点图"></button>
|
||
<button class="nav-dot" data-target="module-3" data-tooltip="流式与铁律" role="tab" aria-label="模块 3: 流式与铁律"></button>
|
||
<button class="nav-dot" data-target="module-4" data-tooltip="口吻与转人工" role="tab" aria-label="模块 4: 口吻与转人工"></button>
|
||
<button class="nav-dot" data-target="module-5" data-tooltip="边界与接缝" role="tab" aria-label="模块 5: 边界与接缝"></button>
|
||
<button class="nav-dot" data-target="module-6" data-tooltip="L0 与 L1 画像" role="tab" aria-label="模块 6: L0 与 L1 画像"></button>
|
||
<button class="nav-dot" data-target="module-7" data-tooltip="Wave3 查询" role="tab" aria-label="模块 7: Wave3 查询"></button>
|
||
</div>
|
||
</div>
|
||
</nav>
|
||
|
||
<main id="main">
|
||
<section class="module" id="module-1">
|
||
<div class="module-inner">
|
||
<p class="eyebrow animate-in">模块 1 · 客户视角</p>
|
||
<h1 class="module-title animate-in">客户能做什么?<br>从 SSE 对话到 Core 只读</h1>
|
||
<p class="module-lead animate-in">
|
||
登录客户(如 <code>CUST-9527</code>)打开「客户助手」后,可走<strong>同步</strong>或
|
||
<span class="term" data-definition="SSE = Server-Sent Events,服务器像打字一样一段段把回复推给浏览器,不用等整段话生成完。">SSE 流式</span>
|
||
对话。未登录访客另有<strong>游客试聊</strong>(只 RAG、不查持仓)。
|
||
2026-09 <code>merger</code> 分支:客户线 SSE 已接通,测试基线 **786 passed**。
|
||
</p>
|
||
|
||
<div class="screen animate-in">
|
||
<h2>四条能力线(指挥 AI 改功能前先对齐)</h2>
|
||
<div class="pattern-cards">
|
||
<div class="pattern-card">
|
||
<h3>对话 SSE / 同步</h3>
|
||
<p><code>POST /api/chat/stream</code> 或 <code>POST /api/chat</code>,请求头 <code>X-Agent-Type: customer</code> + Bearer JWT。</p>
|
||
</div>
|
||
<div class="pattern-card">
|
||
<h3>持仓 / 流水 / 风评 / 净值</h3>
|
||
<p>意图命中后走 Core 模拟库只读查询 Tool(<code>core_ro_tool.py</code>);含 C-05 最新净值(非实时);C-04 查持仓时可 inline 阈值提醒。</p>
|
||
</div>
|
||
<div class="pattern-card">
|
||
<h3>产品规则 RAG</h3>
|
||
<p><code>fin_*</code> 知识库(Milvus)回答申购赎回、费率等产品咨询;画像语境注入但不泄露敏感字段。</p>
|
||
</div>
|
||
<div class="pattern-card">
|
||
<h3>游客试聊</h3>
|
||
<p><code>POST /api/chat/visitor</code> 免登录,游客试聊 9 节点编排(<code>visitor_service.py</code>),不写客户会话表。</p>
|
||
</div>
|
||
</div>
|
||
<div class="callout callout-accent">
|
||
<strong>和理财师线的区别:</strong> 客户线走登录客户 14 节点 LangGraph 编排(<code>customer_service.py</code>),<strong>不走</strong> 顾问通用 tool→llm→guard 编排(<code>agent_service.py</code>)。
|
||
</div>
|
||
</div>
|
||
|
||
<div class="screen animate-in">
|
||
<h2>数据流:浏览器问「我持仓多少」</h2>
|
||
<p>客户发消息后,四 Agent 对话 HTTP 入口(<code>chat.py</code>)按 <code>X-Agent-Type</code> 分流到客户编排,再查 Core。</p>
|
||
|
||
<div class="flow-animation" data-steps='[
|
||
{"highlight":"flow-actor-1","label":"浏览器:ChatPanel 带 JWT + X-Agent-Type: customer"},
|
||
{"highlight":"flow-actor-2","label":"四 Agent 对话 HTTP 入口(chat.py):鉴权、建 session、分流 customer 分支","packet":true,"from":"1","to":"2"},
|
||
{"highlight":"flow-actor-3","label":"customer_service:意图→tool_call→interpret(数值来自 Core)","packet":true,"from":"2","to":"3"},
|
||
{"highlight":"flow-actor-4","label":"Core 只读查询 Tool(core_ro_tool.py):SQL 只读 + fact_text 脱敏模板","packet":true,"from":"3","to":"4"},
|
||
{"highlight":"flow-actor-1","label":"SSE 分块推送完整回复 + MySQL 落会话","packet":true,"from":"4","to":"1"}
|
||
]'>
|
||
<div class="flow-actors">
|
||
<div class="flow-actor" id="flow-actor-1"><span class="flow-actor-icon">🖥</span><span>浏览器</span></div>
|
||
<div class="flow-actor" id="flow-actor-2"><span class="flow-actor-icon">🚪</span><span>对话 HTTP 入口(chat.py)</span></div>
|
||
<div class="flow-actor" id="flow-actor-3"><span class="flow-actor-icon">🤖</span><span>customer_service</span></div>
|
||
<div class="flow-actor" id="flow-actor-4"><span class="flow-actor-icon">🗄</span><span>core_ro_tool</span></div>
|
||
</div>
|
||
<p class="flow-step-label">点击「下一步」看请求怎么走</p>
|
||
<div class="flow-controls">
|
||
<button class="btn flow-next-btn">下一步</button>
|
||
<button class="btn flow-reset-btn">重来</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="screen animate-in">
|
||
<div class="translation-block">
|
||
<div class="translation-code">
|
||
<span class="translation-label">入口 · 四 Agent 对话 HTTP 入口(chat.py)</span>
|
||
<pre><code><span class="code-line"><span class="code-keyword">if</span> agent_type == <span class="code-string">"customer"</span>:</span>
|
||
<span class="code-line"> host_ctx = host_auth_for_customer_service(auth, trace_id=trace_id)</span>
|
||
<span class="code-line"> customer_prep = prepare_customer_stream(</span>
|
||
<span class="code-line"> host_ctx, message, sid, cust_id, req.end_session</span>
|
||
<span class="code-line"> )</span>
|
||
<span class="code-line"> <span class="code-comment"># 图跑完后按 chunks 推 SSE</span></span></code></pre>
|
||
</div>
|
||
<div class="translation-english">
|
||
<span class="translation-label">白话</span>
|
||
<div class="translation-lines">
|
||
<p class="tl">看到 customer 就换轨道:不进 agent_service,改走客户专用编排。</p>
|
||
<p class="tl">先把 JWT 转成 customer_service 认识的 AuthContext(字段名对齐)。</p>
|
||
<p class="tl">流式路径:LangGraph 整图跑完,再把回复切成块推给前端——Tool/RAG 仍是同步完成。</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="quiz-container" id="quiz-customer-m1">
|
||
<div class="quiz-question-block"
|
||
data-correct="option-b"
|
||
data-explanation-right="对。游客走 /api/chat/visitor + visitor_service,免 JWT,不查持仓/流水。"
|
||
data-explanation-wrong="游客试聊和登录客户是两条线:无 JWT、无 Core 客户数据查询。">
|
||
<h3 class="quiz-question">未登录访客在首页试聊,会查到自己的持仓吗?</h3>
|
||
<div class="quiz-options">
|
||
<button class="quiz-option" data-value="option-a" onclick="selectOption(this)">
|
||
<div class="quiz-option-radio"></div><span>会,走同一套 customer_service</span>
|
||
</button>
|
||
<button class="quiz-option" data-value="option-b" onclick="selectOption(this)">
|
||
<div class="quiz-option-radio"></div><span>不会,只 RAG + 闲聊,无客户数据</span>
|
||
</button>
|
||
<button class="quiz-option" data-value="option-c" onclick="selectOption(this)">
|
||
<div class="quiz-option-radio"></div><span>会,但要填客户号</span>
|
||
</button>
|
||
</div>
|
||
<div class="quiz-feedback"></div>
|
||
</div>
|
||
<button class="quiz-check-btn" onclick="checkQuiz('quiz-customer-m1')">检查答案</button>
|
||
<button class="quiz-reset-btn" onclick="resetQuiz('quiz-customer-m1')">重做</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
<section class="module" id="module-2">
|
||
<div class="module-inner">
|
||
<p class="eyebrow animate-in">模块 2 · 编排内核</p>
|
||
<h1 class="module-title animate-in">14 节点 LangGraph:<br>从回忆到归档</h1>
|
||
<p class="module-lead animate-in">
|
||
客户线不走顾问通用 tool→llm→guard 编排(<code>agent_service.py</code>),而是
|
||
登录客户 14 节点 LangGraph 编排(<code>customer_service.py</code>)里一张更大的
|
||
<span class="term" data-definition="LangGraph = 用「节点 + 边」画出的对话流程图;每个节点是一段 Python 函数,边决定下一步走哪条分支。">LangGraph</span>。
|
||
入口永远是 <code>recall_memory</code>,出口经 <code>save_memory</code> 写记忆后收尾。
|
||
</p>
|
||
|
||
<div class="screen animate-in">
|
||
<h2>14 个节点一览(改分支前先认门牌号)</h2>
|
||
<div class="badge-list">
|
||
<div class="badge-item"><span class="badge-code">1</span><span class="badge-desc">recall_memory · 拉 Redis/MySQL 窗口</span></div>
|
||
<div class="badge-item"><span class="badge-code">2</span><span class="badge-desc">intent_classify · LLM 分意图</span></div>
|
||
<div class="badge-item"><span class="badge-code">3</span><span class="badge-desc">rag_search · fin_* Milvus</span></div>
|
||
<div class="badge-item"><span class="badge-code">4</span><span class="badge-desc">param_extract · 抽查询参数</span></div>
|
||
<div class="badge-item"><span class="badge-code">5</span><span class="badge-desc">tool_call · Core 只读 Tool(core_ro_tool.py)</span></div>
|
||
<div class="badge-item"><span class="badge-code">6</span><span class="badge-desc">interpret · 解读 fact_text</span></div>
|
||
<div class="badge-item"><span class="badge-code">7</span><span class="badge-desc">generate · RAG 拼回复</span></div>
|
||
<div class="badge-item"><span class="badge-code">8</span><span class="badge-desc">chitchat · 闲聊</span></div>
|
||
<div class="badge-item"><span class="badge-code">9</span><span class="badge-desc">reject · 合规拒绝</span></div>
|
||
<div class="badge-item"><span class="badge-code">10</span><span class="badge-desc">transfer_human · 转人工</span></div>
|
||
<div class="badge-item"><span class="badge-code">11</span><span class="badge-desc">fallback · 兜底话术</span></div>
|
||
<div class="badge-item"><span class="badge-code">12</span><span class="badge-desc">save_note · 客户记一笔</span></div>
|
||
<div class="badge-item"><span class="badge-code">13</span><span class="badge-desc">save_memory · 写回合记忆</span></div>
|
||
<div class="badge-item"><span class="badge-code">14</span><span class="badge-desc">profile_maybe_extract + archive_check</span></div>
|
||
</div>
|
||
<div class="callout callout-accent">
|
||
<strong>分支规则:</strong> <code>intent_classify</code> 之后走 RAG(产品/政策/FAQ)、走 Tool(持仓/流水/风评)、或走静态话术(拒绝/转人工/闲聊)。所有生成分支最后都汇入 <code>save_memory</code>。
|
||
<strong>口吻护栏与转人工决策</strong>见模块 4。
|
||
</div>
|
||
</div>
|
||
|
||
<div class="screen animate-in">
|
||
<h2>群聊动画:客户问「我持仓多少」</h2>
|
||
<p>下面按节点顺序播放(数据查询分支);若问「申购费率多少」则在节点 2 后改走 <code>rag_search → generate</code>。</p>
|
||
|
||
<div class="chat-window" id="chat-customer-m2">
|
||
<div class="chat-messages">
|
||
<div class="chat-message" data-msg="0" data-sender="user" style="display:none">
|
||
<div class="chat-avatar" style="background:#2D8B55">客</div>
|
||
<div class="chat-bubble">
|
||
<span class="chat-sender" style="color:#2D8B55">客户</span>
|
||
<p>我持仓多少?</p>
|
||
</div>
|
||
</div>
|
||
<div class="chat-message" data-msg="1" data-sender="mem" style="display:none">
|
||
<div class="chat-avatar" style="background:#5BAE7A">①</div>
|
||
<div class="chat-bubble">
|
||
<span class="chat-sender" style="color:#5BAE7A">recall_memory</span>
|
||
<p>从 Redis 窗口 + MySQL 回源,拼 consult_memory / profile_context</p>
|
||
</div>
|
||
</div>
|
||
<div class="chat-message" data-msg="2" data-sender="intent" style="display:none">
|
||
<div class="chat-avatar" style="background:#236B44">②</div>
|
||
<div class="chat-bubble">
|
||
<span class="chat-sender" style="color:#236B44">intent_classify</span>
|
||
<p>LLM 输出意图 <code>holdings_query</code>(非 RAG、非闲聊)</p>
|
||
</div>
|
||
</div>
|
||
<div class="chat-message" data-msg="3" data-sender="param" style="display:none">
|
||
<div class="chat-avatar" style="background:#2D8B55">④</div>
|
||
<div class="chat-bubble">
|
||
<span class="chat-sender" style="color:#2D8B55">param_extract</span>
|
||
<p>抽查询参数;customer_id 已在 state 里(来自 JWT),不信用户口述</p>
|
||
</div>
|
||
</div>
|
||
<div class="chat-message" data-msg="4" data-sender="tool" style="display:none">
|
||
<div class="chat-avatar" style="background:#1a5235">⑤</div>
|
||
<div class="chat-bubble">
|
||
<span class="chat-sender" style="color:#1a5235">tool_call</span>
|
||
<p><code>query_holdings(customer_id)</code> → Core 只读 → 返回 fact_text 模板</p>
|
||
</div>
|
||
</div>
|
||
<div class="chat-message" data-msg="5" data-sender="interp" style="display:none">
|
||
<div class="chat-avatar" style="background:#7B6DAA">⑥</div>
|
||
<div class="chat-bubble">
|
||
<span class="chat-sender" style="color:#7B6DAA">interpret</span>
|
||
<p>LLM 只「解读」fact_text,禁止编造持仓数字</p>
|
||
</div>
|
||
</div>
|
||
<div class="chat-message" data-msg="6" data-sender="rag-alt" style="display:none">
|
||
<div class="chat-avatar" style="background:#D4A843">③⑦</div>
|
||
<div class="chat-bubble">
|
||
<span class="chat-sender" style="color:#D4A843">RAG 分支(对照)</span>
|
||
<p>若意图是 product_consult / policy_interpret / faq → <code>rag_search</code>(fin_product / fin_policy / fin_faq)→ <code>generate</code></p>
|
||
</div>
|
||
</div>
|
||
<div class="chat-message" data-msg="7" data-sender="save" style="display:none">
|
||
<div class="chat-avatar" style="background:#2A7B9B">⑬</div>
|
||
<div class="chat-bubble">
|
||
<span class="chat-sender" style="color:#2A7B9B">save_memory</span>
|
||
<p>回合写入 Redis 窗口;失败 fail-open 不挡回复</p>
|
||
</div>
|
||
</div>
|
||
<div class="chat-message" data-msg="8" data-sender="profile" style="display:none">
|
||
<div class="chat-avatar" style="background:#5A9DB8">⑭</div>
|
||
<div class="chat-bubble">
|
||
<span class="chat-sender" style="color:#5A9DB8">profile_maybe_extract</span>
|
||
<p>每 5 轮节流,后台线程抽画像槽位(失败不影响响应)</p>
|
||
</div>
|
||
</div>
|
||
<div class="chat-message" data-msg="9" data-sender="archive" style="display:none">
|
||
<div class="chat-avatar" style="background:#6B6560">⑭</div>
|
||
<div class="chat-bubble">
|
||
<span class="chat-sender" style="color:#6B6560">archive_check</span>
|
||
<p>显式结束会话或懒扫描超时 → 归档 MySQL 长记忆</p>
|
||
</div>
|
||
</div>
|
||
<div class="chat-message" data-msg="10" data-sender="reply" style="display:none">
|
||
<div class="chat-avatar" style="background:#2D8B55">AI</div>
|
||
<div class="chat-bubble">
|
||
<span class="chat-sender" style="color:#2D8B55">回复客户</span>
|
||
<p>interpret 产出自然语言 + 合规 sanitize;SSE 路径在图跑完后分块推送</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="chat-typing" id="chat-customer-m2-typing" style="display:none">
|
||
<div class="chat-avatar" id="chat-customer-m2-typing-avatar">…</div>
|
||
<div class="chat-typing-dots"><span class="typing-dot"></span><span class="typing-dot"></span><span class="typing-dot"></span></div>
|
||
</div>
|
||
<div class="chat-controls">
|
||
<button class="btn chat-next-btn">下一条</button>
|
||
<button class="btn chat-all-btn">全部播放</button>
|
||
<button class="btn chat-reset-btn">重播</button>
|
||
<span class="chat-progress"></span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="screen animate-in">
|
||
<div class="translation-block">
|
||
<div class="translation-code">
|
||
<span class="translation-label">图构建 · 登录客户 14 节点 LangGraph 编排(customer_service.py)</span>
|
||
<pre><code><span class="code-line">g.set_entry_point(<span class="code-string">"recall_memory"</span>)</span>
|
||
<span class="code-line">g.add_edge(<span class="code-string">"recall_memory"</span>, <span class="code-string">"intent_classify"</span>)</span>
|
||
<span class="code-line">g.add_conditional_edges(<span class="code-string">"intent_classify"</span>, _route, {...})</span>
|
||
<span class="code-line">g.add_edge(<span class="code-string">"tool_call"</span>, <span class="code-string">"interpret"</span>)</span>
|
||
<span class="code-line">g.add_edge(<span class="code-string">"rag_search"</span>, <span class="code-string">"generate"</span>)</span>
|
||
<span class="code-line"><span class="code-comment"># 各生成分支 → save_memory → profile → archive → END</span></span></code></pre>
|
||
</div>
|
||
<div class="translation-english">
|
||
<span class="translation-label">白话</span>
|
||
<div class="translation-lines">
|
||
<p class="tl">先回忆上下文,再让 LLM 判意图,然后走 RAG 或 Tool 或静态话术。</p>
|
||
<p class="tl">数据类问题:param_extract → tool_call → interpret,数字 100% 来自 Core。</p>
|
||
<p class="tl">无论哪条分支,最后都要 save_memory,再异步做画像和归档。</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="quiz-container" id="quiz-customer-m2">
|
||
<div class="quiz-question-block"
|
||
data-correct="option-b"
|
||
data-explanation-right="对。持仓/流水/风评走 param_extract→tool_call→interpret;产品规则走 rag_search→generate。"
|
||
data-explanation-wrong="RAG 只服务 fin_* 三库(产品/政策/FAQ),不查持仓表。">
|
||
<h3 class="quiz-question">客户问「基金申购费率多少」,LangGraph 走哪条分支?</h3>
|
||
<div class="quiz-options">
|
||
<button class="quiz-option" data-value="option-a" onclick="selectOption(this)">
|
||
<div class="quiz-option-radio"></div><span>tool_call → interpret</span>
|
||
</button>
|
||
<button class="quiz-option" data-value="option-b" onclick="selectOption(this)">
|
||
<div class="quiz-option-radio"></div><span>rag_search → generate</span>
|
||
</button>
|
||
<button class="quiz-option" data-value="option-c" onclick="selectOption(this)">
|
||
<div class="quiz-option-radio"></div><span>chitchat 闲聊</span>
|
||
</button>
|
||
</div>
|
||
<div class="quiz-feedback"></div>
|
||
</div>
|
||
<button class="quiz-check-btn" onclick="checkQuiz('quiz-customer-m2')">检查答案</button>
|
||
<button class="quiz-reset-btn" onclick="resetQuiz('quiz-customer-m2')">重做</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
<section class="module" id="module-3">
|
||
<div class="module-inner">
|
||
<p class="eyebrow animate-in">模块 3 · 铁律与流式</p>
|
||
<h1 class="module-title animate-in">两条铁律 +<br>prepare_customer_stream</h1>
|
||
<p class="module-lead animate-in">
|
||
客户线能跑通,靠文件头写死的两条
|
||
<span class="term" data-definition="铁律 = 写进代码注释、测试会卡住的硬规则;违反会导致合规或数据错乱。">铁律</span>。
|
||
流式 SSE 看起来是「边生成边推」,实现上是<strong>先跑完整张 LangGraph,再切块推送</strong>。
|
||
</p>
|
||
|
||
<div class="screen animate-in">
|
||
<h2>铁律(指挥 AI 改代码时别碰)</h2>
|
||
<div class="pattern-cards">
|
||
<div class="pattern-card">
|
||
<h3>铁律 1 · 数值不经过 LLM 编造</h3>
|
||
<p>持仓、流水、风评等查询结果 100% 来自 Core 只读查询 Tool(<code>core_ro_tool.py</code>)的 <code>fact_text</code>。LLM 在 <code>interpret</code> / <code>generate</code> 里只做解读与组织语言,不能自己算数。</p>
|
||
</div>
|
||
<div class="pattern-card">
|
||
<h3>铁律 2 · customer_id 只来自 JWT</h3>
|
||
<p>四 Agent 对话 HTTP 入口(<code>chat.py</code>)在进图之前已从令牌解析客户号并做归属校验。图内 <code>state["customer_id"]</code> 不信用户消息里的「帮我查 CUST-xxx」。</p>
|
||
</div>
|
||
</div>
|
||
<div class="callout callout-warning">
|
||
<strong>和 advisor 线的差异:</strong> 理财师走顾问通用编排(<code>agent_service.py</code>)的 tool→llm→guard;客户走独立 14 节点图。别混用对话 Tool 编排(<code>tool_service.py</code>)关键词意图那套来改客户 RAG 分支。
|
||
</div>
|
||
</div>
|
||
|
||
<div class="screen animate-in">
|
||
<h2>流式真相:图跑完再切块</h2>
|
||
<p>方案 C 下 <code>prepare_customer_stream</code> 先调用 <code>run_customer_chat</code> 跑完整图,再用 <code>_chunk_reply_text</code> 切成 16 份左右推 SSE——Tool/RAG 仍是同步完成,不是 token 级流式。</p>
|
||
|
||
<div class="translation-block">
|
||
<div class="translation-code">
|
||
<span class="translation-label">CODE · prepare_customer_stream</span>
|
||
<pre><code><span class="code-line"><span class="code-keyword">def</span> prepare_customer_stream(</span>
|
||
<span class="code-line"> ctx, message, session_id, customer_id, end_session=<span class="code-keyword">False</span></span>
|
||
<span class="code-line">) -> dict[str, Any]:</span>
|
||
<span class="code-line"> reply, has_disclaimer, intent, transfer = run_customer_chat(</span>
|
||
<span class="code-line"> ctx, message, session_id, customer_id, end_session</span>
|
||
<span class="code-line"> )</span>
|
||
<span class="code-line"> <span class="code-keyword">return</span> {</span>
|
||
<span class="code-line"> <span class="code-string">"reply"</span>: reply,</span>
|
||
<span class="code-line"> <span class="code-string">"has_disclaimer"</span>: has_disclaimer,</span>
|
||
<span class="code-line"> <span class="code-string">"intent"</span>: intent,</span>
|
||
<span class="code-line"> <span class="code-string">"transfer_to_human"</span>: transfer,</span>
|
||
<span class="code-line"> <span class="code-string">"chunks"</span>: _chunk_reply_text(reply),</span>
|
||
<span class="code-line"> }</span></code></pre>
|
||
</div>
|
||
<div class="translation-english">
|
||
<span class="translation-label">白话</span>
|
||
<div class="translation-lines">
|
||
<p class="tl">入参里的 customer_id 是四 Agent 对话 HTTP 入口(<code>chat.py</code>)验完 JWT 后塞进来的,函数内部不再解析用户文本。</p>
|
||
<p class="tl">先拿到完整 reply(含 intent、是否转人工、是否附免责),再切片给 SSE 层逐块写。</p>
|
||
<p class="tl">改「真流式」要先动 LangGraph 执行模型,不是只改前端 ChatPanel。</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="screen animate-in">
|
||
<h2>相关文件(改流式 / 铁律时打开这些)</h2>
|
||
<div class="file-tree animate-in">
|
||
<div class="tree-item tree-folder">app/service/</div>
|
||
<div class="tree-item tree-indent">登录客户 14 节点 LangGraph 编排(customer_service.py)— prepare_customer_stream</div>
|
||
<div class="tree-item tree-indent">游客试聊 9 节点 LangGraph 编排(visitor_service.py)— 无 Tool 查持仓</div>
|
||
<div class="tree-item tree-indent">RAG 检索层(rag_service.py)— fin_faq / fin_product / fin_policy</div>
|
||
<div class="tree-item tree-folder">app/api/</div>
|
||
<div class="tree-item tree-indent">四 Agent 对话 HTTP 入口(chat.py)— customer 分支分流 + SSE 写帧</div>
|
||
<div class="tree-item tree-indent">宿主 AuthContext → 模块 AuthContext 适配(auth_adapter.py)— host_auth_for_customer_service</div>
|
||
<div class="tree-item tree-folder">app/tool/</div>
|
||
<div class="tree-item tree-indent">客户 Agent Core 只读查询工具(core_ro_tool.py)— query_holdings / query_trades 等</div>
|
||
</div>
|
||
|
||
<div class="quiz-container" id="quiz-customer-m3">
|
||
<div class="quiz-question-block"
|
||
data-correct="option-c"
|
||
data-explanation-right="对。prepare_customer_stream 先 run_customer_chat 整图 invoke,再 _chunk_reply_text 切块;不是 LLM token 级流。"
|
||
data-explanation-wrong="客户 SSE 是「整图跑完再切块」,Tool/RAG 在推流前已同步完成。">
|
||
<h3 class="quiz-question">客户 SSE 流式回复,LangGraph 什么时候跑完?</h3>
|
||
<div class="quiz-options">
|
||
<button class="quiz-option" data-value="option-a" onclick="selectOption(this)">
|
||
<div class="quiz-option-radio"></div><span>每推一块就跑一个节点</span>
|
||
</button>
|
||
<button class="quiz-option" data-value="option-b" onclick="selectOption(this)">
|
||
<div class="quiz-option-radio"></div><span>只跑 interpret,前面跳过</span>
|
||
</button>
|
||
<button class="quiz-option" data-value="option-c" onclick="selectOption(this)">
|
||
<div class="quiz-option-radio"></div><span>推流前整图 invoke 完成</span>
|
||
</button>
|
||
</div>
|
||
<div class="quiz-feedback"></div>
|
||
</div>
|
||
<button class="quiz-check-btn" onclick="checkQuiz('quiz-customer-m3')">检查答案</button>
|
||
<button class="quiz-reset-btn" onclick="resetQuiz('quiz-customer-m3')">重做</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
<section class="module" id="module-4">
|
||
<div class="module-inner">
|
||
<p class="eyebrow animate-in">模块 4 · 口吻与转人工</p>
|
||
<h1 class="module-title animate-in">专业口吻怎么「锁住」?<br>什么时候该转人工?</h1>
|
||
<p class="module-lead animate-in">
|
||
机制课讲了 LangGraph 怎么走,但客服线还有两条你指挥 AI 时最常碰的线:
|
||
<strong>回复别漂成投顾/推销员</strong>,以及<strong>什么时候打「建议转人工」</strong>。
|
||
代码里不是单靠「写个好 prompt」,而是<strong>三层滤网 + 一条主动转人工路径</strong>(sanitize 命中<strong>不再</strong>自动转人工,拍板 1B)。
|
||
</p>
|
||
|
||
<div class="screen animate-in">
|
||
<h2>三层滤网:防止口吻漂移</h2>
|
||
<p>可以把客服回复想成「过三道闸」——前面两道尽量写对,最后一道不管 LLM 说什么都要拦违规词。</p>
|
||
<div class="step-cards">
|
||
<div class="step-card">
|
||
<span class="step-num">①</span>
|
||
<p><strong>Prompt 层</strong> · 客户线各分支 System Prompt 模板(<code>customer_prompts.py</code>)<br>
|
||
每个生成分支有独立 system 模板(<code>INTERPRET_SYSTEM</code> / <code>GENERATE_SYSTEM</code> / <code>CHITCHAT_SYSTEM</code>),开头就定身份:「金融客服助手(已登录客户模式)」——禁止投顾话术、禁止预测、画像只调措辞<strong>不能念出</strong>。</p>
|
||
</div>
|
||
<div class="step-card">
|
||
<span class="step-num">②</span>
|
||
<p><strong>记忆层</strong> · <code>recall_memory</code><br>
|
||
咨询走 <code>consult_memory</code>、闲聊走 <code>chitchat_memory</code>,再注入 <code>profile_context</code>。让回复有连续语境,但 prompt 里写死:不得主动复述年龄/收入等画像字段。</p>
|
||
</div>
|
||
<div class="step-card">
|
||
<span class="step-num">③</span>
|
||
<p><strong>事后扫描</strong> · <code>finalize_sanitized_reply()</code>(<code>sanitize_postprocess.py</code>)<br>
|
||
<code>interpret</code> / <code>generate</code> / <code>chitchat</code> 产出后过违禁词正则。命中后:<strong>数据查询类</strong>(含 nav_query)回退 Core <code>fact_text</code>;RAG/闲聊替换 <code>COMPLIANCE_REJECT</code>——<strong>均不</strong>自动 <code>transfer_to_human</code>。<strong>游客线已对齐</strong>。</p>
|
||
</div>
|
||
</div>
|
||
<div class="callout callout-accent">
|
||
<strong>入口还有第 0 道闸:</strong> 四 Agent 对话 HTTP 入口(<code>chat.py</code>)的 <span class="term" data-definition="input_guard = 进 LangGraph 之前对用户输入做检查:注入短语、超长、频率限制;命中直接拒答并写 input_guard_log。">input_guard</span> 在进图之前就拦注入/超长——这类请求根本到不了 LLM。
|
||
</div>
|
||
</div>
|
||
|
||
<div class="screen animate-in">
|
||
<div class="translation-block">
|
||
<div class="translation-code">
|
||
<span class="translation-label">事后扫描 · 共用分治(sanitize_postprocess.py)</span>
|
||
<pre><code><span class="code-line"><span class="code-keyword">def</span> finalize_sanitized_reply(reply, *, intent, fact_text):</span>
|
||
<span class="code-line"> safe, hit = sanitize_reply(reply)</span>
|
||
<span class="code-line"> <span class="code-keyword">if not</span> hit: <span class="code-keyword">return</span> safe, <span class="code-keyword">False</span></span>
|
||
<span class="code-line"> <span class="code-keyword">if</span> intent <span class="code-keyword">in</span> DATA_QUERY <span class="code-keyword">and</span> fact_text:</span>
|
||
<span class="code-line"> <span class="code-keyword">return</span> fact_text, <span class="code-keyword">False</span> <span class="code-comment"># 1B:保真账,不转人工</span></span>
|
||
<span class="code-line"> <span class="code-keyword">return</span> COMPLIANCE_REJECT, <span class="code-keyword">False</span></span></code></pre>
|
||
</div>
|
||
<div class="translation-english">
|
||
<span class="translation-label">白话</span>
|
||
<div class="translation-lines">
|
||
<p class="tl">数据查询分支:LLM 嘴瓢时<strong>回退 Core fact_text</strong>,客户仍能看到「持有 3 只产品」等真数。</p>
|
||
<p class="tl">RAG/闲聊分支:替换为固定拒答,但<strong>不打</strong>转人工标志——除非用户原话已是投诉/转人工。</p>
|
||
<p class="tl">底层 <code>sanitize_reply</code> 仍负责扫违禁词;分治逻辑在 customer 编排层。</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="screen animate-in">
|
||
<h2>转人工 vs 直接拒绝:别混成一条线</h2>
|
||
<p>模块 2 列出了 <code>reject</code> 和 <code>transfer_human</code> 两个节点,但<strong>触发条件完全不同</strong>。指挥 AI 改逻辑时最容易搞混这里。</p>
|
||
|
||
<div class="flow-animation" data-steps='[
|
||
{"highlight":"flow-g-1","label":"用户输入进入 intent_classify(关键词优先,再 LLM)"},
|
||
{"highlight":"flow-g-2","label":"命中「转人工/投诉/纠纷/账户异常/被盗」→ transfer_human 节点","packet":true,"from":"1","to":"2"},
|
||
{"highlight":"flow-g-3","label":"命中「推荐/预测/竞品/实时行情」→ reject 节点(静态拒答,不自动打转人工标志)","packet":true,"from":"1","to":"3"},
|
||
{"highlight":"flow-g-4","label":"正常走 interpret/generate/chitchat → sanitize 分治(1B)","packet":true,"from":"1","to":"4"},
|
||
{"highlight":"flow-g-4","label":"数据查询命中 → 回退 fact_text;RAG/闲聊 → COMPLIANCE_REJECT;均 transfer=false","packet":true,"from":"4","to":"4"}
|
||
]'>
|
||
<div class="flow-actors">
|
||
<div class="flow-actor" id="flow-g-1"><span class="flow-actor-icon">💬</span><span>intent_classify</span></div>
|
||
<div class="flow-actor" id="flow-g-2"><span class="flow-actor-icon">👤</span><span>transfer_human</span></div>
|
||
<div class="flow-actor" id="flow-g-3"><span class="flow-actor-icon">🚫</span><span>reject</span></div>
|
||
<div class="flow-actor" id="flow-g-4"><span class="flow-actor-icon">🔍</span><span>sanitize_reply</span></div>
|
||
</div>
|
||
<p class="flow-step-label">点击「下一步」看决策分叉</p>
|
||
<div class="flow-controls">
|
||
<button class="btn flow-next-btn">下一步</button>
|
||
<button class="btn flow-reset-btn">重来</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="pattern-cards">
|
||
<div class="pattern-card">
|
||
<h3>路径 A · 主动转人工</h3>
|
||
<p>关键词:<code>转人工</code>、<code>投诉</code>、<code>纠纷</code>、<code>账户异常</code>、<code>被盗</code> 等 → <code>TRANSFER_TEXT</code> + <code>transfer_to_human=True</code>。</p>
|
||
</div>
|
||
<div class="pattern-card">
|
||
<h3>路径 B · LLM 越界被拦(1B)</h3>
|
||
<p>数据查询 interpret 违禁 → <strong>回退 fact_text</strong>,transfer=false。RAG/闲聊违禁 → <code>COMPLIANCE_REJECT</code>,transfer=false。</p>
|
||
</div>
|
||
<div class="pattern-card">
|
||
<h3>路径 C · 边界外但不必转人工</h3>
|
||
<p>问「推荐稳赚基金」「明天会涨吗」→ <code>reject</code> 静态话术(如 <code>REJECT_ADVICE_TEXT</code>)。<strong>不</strong>自动打转人工标志;话术中可引导用户自行说「转人工」。</p>
|
||
</div>
|
||
</div>
|
||
<div class="callout callout-warning">
|
||
<strong>演示环境边界:</strong> 「建议转人工」只是 API 字段 + 前端橙色 Tag(<code>ChatPanel</code>),<strong>不会</strong>真的排队接入真人坐席——真转接要接呼叫中心/工单系统,那是产品层后续工作。
|
||
</div>
|
||
</div>
|
||
|
||
<div class="screen animate-in">
|
||
<h2>群聊动画:LLM 嘴瓢后被扫描层拦住</h2>
|
||
<p>客户问持仓,Tool 返回正常 fact_text,但 interpret 的 LLM 多嘴加了「建议您加仓」——扫描层介入。</p>
|
||
|
||
<div class="chat-window" id="chat-customer-m4">
|
||
<div class="chat-messages">
|
||
<div class="chat-message" data-msg="0" data-sender="user" style="display:none">
|
||
<div class="chat-avatar" style="background:#2D8B55">客</div>
|
||
<div class="chat-bubble">
|
||
<span class="chat-sender" style="color:#2D8B55">客户</span>
|
||
<p>我持仓情况怎么样?</p>
|
||
</div>
|
||
</div>
|
||
<div class="chat-message" data-msg="1" data-sender="tool" style="display:none">
|
||
<div class="chat-avatar" style="background:#1a5235">⑤</div>
|
||
<div class="chat-bubble">
|
||
<span class="chat-sender" style="color:#1a5235">tool_call</span>
|
||
<p>Core 返回 fact_text(脱敏模板,数字 100% 来自只读库)</p>
|
||
</div>
|
||
</div>
|
||
<div class="chat-message" data-msg="2" data-sender="llm" style="display:none">
|
||
<div class="chat-avatar" style="background:#7B6DAA">⑥</div>
|
||
<div class="chat-bubble">
|
||
<span class="chat-sender" style="color:#7B6DAA">interpret · LLM 草稿</span>
|
||
<p>「您目前持有 3 只基金…<strong>建议您加仓医药板块</strong>…」</p>
|
||
</div>
|
||
</div>
|
||
<div class="chat-message" data-msg="3" data-sender="guard" style="display:none">
|
||
<div class="chat-avatar" style="background:#B84A4A">③</div>
|
||
<div class="chat-bubble">
|
||
<span class="chat-sender" style="color:#B84A4A">sanitize 分治</span>
|
||
<p>命中「建议…加仓」→ <strong>丢弃 LLM 草稿,回退 Core fact_text</strong>(仍含「3 只产品」)</p>
|
||
</div>
|
||
</div>
|
||
<div class="chat-message" data-msg="4" data-sender="api" style="display:none">
|
||
<div class="chat-avatar" style="background:#2A7B9B">API</div>
|
||
<div class="chat-bubble">
|
||
<span class="chat-sender" style="color:#2A7B9B">返回前端</span>
|
||
<p><code>transfer_to_human: false</code> · 客户看到脱敏持仓事实,无橙色转人工 Tag</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="chat-typing" id="chat-customer-m4-typing" style="display:none">
|
||
<div class="chat-avatar" id="chat-customer-m4-typing-avatar">…</div>
|
||
<div class="chat-typing-dots"><span class="typing-dot"></span><span class="typing-dot"></span><span class="typing-dot"></span></div>
|
||
</div>
|
||
<div class="chat-controls">
|
||
<button class="btn chat-next-btn">下一条</button>
|
||
<button class="btn chat-all-btn">全部播放</button>
|
||
<button class="btn chat-reset-btn">重播</button>
|
||
<span class="chat-progress"></span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="screen animate-in">
|
||
<h2>RAG 咨询还要加风险提示</h2>
|
||
<p>产品/政策/FAQ 类意图在 <code>generate</code> 通过后,若 intent 属于咨询类,会追加 <code>RISK_DISCLAIMER</code>(<code>should_add_disclaimer</code>)。这是「口吻」的另一面:<strong>该免责时必须免责</strong>,不是只有拦违规词。</p>
|
||
|
||
<div class="quiz-container" id="quiz-customer-m4">
|
||
<div class="quiz-question-block"
|
||
data-correct="option-b"
|
||
data-explanation-right="对。reject 走静态 REJECT_* 话术,不自动设 transfer_to_human;只有 transfer_human 节点(用户主动投诉/转人工等)才会打转人工标志。sanitize 命中也不再自动 transfer(1B)。"
|
||
data-explanation-wrong="reject 和 transfer_human 是两条线:边界外拒答 ≠ 自动转人工。">
|
||
<h3 class="quiz-question">客户说「推荐一个稳赚不赔的基金」,系统会怎么标记?</h3>
|
||
<div class="quiz-options">
|
||
<button class="quiz-option" data-value="option-a" onclick="selectOption(this)">
|
||
<div class="quiz-option-radio"></div><span>transfer_to_human=True,直接排队人工</span>
|
||
</button>
|
||
<button class="quiz-option" data-value="option-b" onclick="selectOption(this)">
|
||
<div class="quiz-option-radio"></div><span>走 reject 静态拒答,transfer 标志为 false(话术中可引导说「转人工」)</span>
|
||
</button>
|
||
<button class="quiz-option" data-value="option-c" onclick="selectOption(this)">
|
||
<div class="quiz-option-radio"></div><span>走 RAG generate,正常回答产品列表</span>
|
||
</button>
|
||
</div>
|
||
<div class="quiz-feedback"></div>
|
||
</div>
|
||
<button class="quiz-check-btn" onclick="checkQuiz('quiz-customer-m4')">检查答案</button>
|
||
<button class="quiz-reset-btn" onclick="resetQuiz('quiz-customer-m4')">重做</button>
|
||
</div>
|
||
|
||
<div class="callout">
|
||
<strong>指挥 AI 改客服时的检查清单:</strong>
|
||
① 改口吻 → 动客户线各分支 System Prompt 模板(<code>customer_prompts.py</code>)对应 SYSTEM,别只改前端;
|
||
② 加新禁语 → 同时更新 <code>FORBIDDEN_TERMS</code> 和意图关键词路由;
|
||
③ 加「必须转人工」场景 → 改 <code>_TRANSFER_KEYWORDS</code> 或 INTENT_SYSTEM 规则,别误放进 reject。
|
||
</div>
|
||
|
||
<p class="module-footer-note">相关文件:客户线各分支 System Prompt 模板(<code>app/service/customer_prompts.py</code>)· 回复违禁词合规扫描(<code>app/utils/compliance_guard.py</code>)· <code>web/src/components/chat/ChatPanel.tsx</code></p>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
<section class="module" id="module-5">
|
||
<div class="module-inner">
|
||
<p class="eyebrow animate-in">模块 5 · 边界与接缝</p>
|
||
<h1 class="module-title animate-in">两套知识库、合规口径<br>和一期代码差在哪?</h1>
|
||
<p class="module-lead animate-in">
|
||
客服线「能答什么」不只看 LangGraph,还看<strong>知识库分家</strong>和<strong>合规文档 vs 一期实现</strong>。
|
||
指挥 AI 改 RAG 或「放开推荐」前,先对齐下面这张差分表。
|
||
</p>
|
||
|
||
<div class="screen animate-in">
|
||
<h2>两套 RAG:别灌错库</h2>
|
||
<div class="pattern-cards">
|
||
<div class="pattern-card">
|
||
<h3>客服 · fin_* 三库</h3>
|
||
<p>登录客户的对话编排(<code>customer_service.py</code>)和游客试聊编排(<code>visitor_service.py</code>)遇到产品/政策/FAQ 类问题时,都会交给 RAG 检索层(<code>rag_service.py</code>:把问题向量化后查 Milvus),从 <code>fin_product</code> · <code>fin_policy</code> · <code>fin_faq</code> 三个知识集合取片段。</p>
|
||
</div>
|
||
<div class="pattern-card">
|
||
<h3>顾问 · kb_product_rules</h3>
|
||
<p>理财师/风控通用 tool→llm→guard 编排(<code>agent_service.py</code>)的 <code>search_knowledge</code> Tool → 知识库对话 Tool 注册与分发(<code>kb_tools.py</code>)· 集合名不同,<strong>不能</strong>把顾问文档灌进 fin_*。</p>
|
||
</div>
|
||
<div class="pattern-card">
|
||
<h3>风控 · 不开放 search_knowledge</h3>
|
||
<p>知识库对话 Tool 注册与分发(<code>kb_tools.py</code>)注册表对 risk 关闭——风控对话走 RISK_TOOL_REGISTRY 四只读 Tool。</p>
|
||
</div>
|
||
</div>
|
||
<div class="callout callout-warning">
|
||
<strong>指挥 AI 误区:</strong> 「统一成一个知识库」听起来省事,但会破坏各 Agent 合规过滤与检索策略——要改先对契约和 <code>scripts/kb/</code> 脚本。
|
||
</div>
|
||
</div>
|
||
|
||
<div class="screen animate-in">
|
||
<h2>合规允许 vs 一期拒答(你拍板要讲的)</h2>
|
||
<p>合规文档里,客户 Agent 在<strong>过 R-02 + 附免责 + 标注需持证审核</strong>时,可以做「匹配说明」——但一期代码对<strong>主动推荐/预测类</strong>问题走 <code>reject</code> 静态拒答,更保守。</p>
|
||
|
||
<div class="translation-block">
|
||
<div class="translation-code">
|
||
<span class="translation-label">合规文档 · 允许方向</span>
|
||
<pre><code><span class="code-line">适当性匹配说明(C-11)</span>
|
||
<span class="code-line">→ 系统判定 + 规则解读 + 免责声明</span>
|
||
<span class="code-line">→ 禁止具体操作指令</span></code></pre>
|
||
</div>
|
||
<div class="translation-english">
|
||
<span class="translation-label">一期代码 · 实际行为</span>
|
||
<div class="translation-lines">
|
||
<p class="tl"><code>suitability_check</code> Tool:「我能买 R3 吗」「有哪些匹配产品」——<strong>允许</strong>。</p>
|
||
<p class="tl">「推荐稳赚基金」「买什么好」→ <code>reject</code> + <code>REJECT_ADVICE_TEXT</code>——<strong>直接拒</strong>。</p>
|
||
<p class="tl">要「放开推荐」= 产品决策 + 改 intent 路由 + 合规评审,不是只改 prompt 一句。</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="screen animate-in">
|
||
<h2>群聊:同一客户,两种问法</h2>
|
||
<div class="chat-window" id="chat-customer-m5">
|
||
<div class="chat-messages">
|
||
<div class="chat-message" data-msg="0" data-sender="user" style="display:none">
|
||
<div class="chat-avatar" style="background:#2D8B55">客</div>
|
||
<div class="chat-bubble">
|
||
<span class="chat-sender" style="color:#2D8B55">客户 A</span>
|
||
<p>我能买 R3 产品吗?</p>
|
||
</div>
|
||
</div>
|
||
<div class="chat-message" data-msg="1" data-sender="ok" style="display:none">
|
||
<div class="chat-avatar" style="background:#236B44">✓</div>
|
||
<div class="chat-bubble">
|
||
<span class="chat-sender" style="color:#236B44">suitability_check</span>
|
||
<p>意图 <code>suitability_check</code> → Core 只读判定 → interpret 解释规则(合规允许路径)</p>
|
||
</div>
|
||
</div>
|
||
<div class="chat-message" data-msg="2" data-sender="user2" style="display:none">
|
||
<div class="chat-avatar" style="background:#2D8B55">客</div>
|
||
<div class="chat-bubble">
|
||
<span class="chat-sender" style="color:#2D8B55">客户 B</span>
|
||
<p>推荐一个稳赚的基金</p>
|
||
</div>
|
||
</div>
|
||
<div class="chat-message" data-msg="3" data-sender="rej" style="display:none">
|
||
<div class="chat-avatar" style="background:#B84A4A">⑨</div>
|
||
<div class="chat-bubble">
|
||
<span class="chat-sender" style="color:#B84A4A">reject</span>
|
||
<p><code>REJECT_ADVICE_TEXT</code> · transfer 标志 false · 话术中可引导「转人工」</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="chat-typing" id="chat-customer-m5-typing" style="display:none">
|
||
<div class="chat-avatar" id="chat-customer-m5-typing-avatar">…</div>
|
||
<div class="chat-typing-dots"><span class="typing-dot"></span><span class="typing-dot"></span><span class="typing-dot"></span></div>
|
||
</div>
|
||
<div class="chat-controls">
|
||
<button class="btn chat-next-btn">下一条</button>
|
||
<button class="btn chat-all-btn">全部播放</button>
|
||
<button class="btn chat-reset-btn">重播</button>
|
||
<span class="chat-progress"></span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="screen animate-in">
|
||
<h2>游客与画像:客户看不见 L2/L3</h2>
|
||
<p>游客走游客试聊 9 节点编排(<code>visitor_service.py</code>):无 JWT、无 Core 持仓查询、不写登录客户会话表。L1 画像由客服线后台抽槽写入;<span class="term" data-definition="L2 = 代理人侧 enrich 画像;L3 = 风控监测标签。客户 Agent 不可读 L3,也不应把 L2 内容念给客户。">L2/L3</span> 对客户不可见——合规上客户只能 enrich L1,不能替代正式风评。</p>
|
||
|
||
<div class="quiz-container" id="quiz-customer-m5">
|
||
<div class="quiz-question-block"
|
||
data-correct="option-a"
|
||
data-explanation-right="对。顾问 kb_product_rules 与客服 fin_* 是两套集合;混改会导致检索策略和合规过滤错乱。"
|
||
data-explanation-wrong="两套 RAG 分 Agent 是有意的,不能为了「统一」直接合并 Milvus 集合。">
|
||
<h3 class="quiz-question">AI 建议「把 kb 和 fin 合成一个 Milvus 集合简化维护」,你怎么回?</h3>
|
||
<div class="quiz-options">
|
||
<button class="quiz-option" data-value="option-a" onclick="selectOption(this)">
|
||
<div class="quiz-option-radio"></div><span>不行,两套库各服务 customer/advisor,先对 scripts/kb 和契约</span>
|
||
</button>
|
||
<button class="quiz-option" data-value="option-b" onclick="selectOption(this)">
|
||
<div class="quiz-option-radio"></div><span>好,合并后客服也能搜顾问话术</span>
|
||
</button>
|
||
<button class="quiz-option" data-value="option-c" onclick="selectOption(this)">
|
||
<div class="quiz-option-radio"></div><span>好,让风控也开放 search_knowledge</span>
|
||
</button>
|
||
</div>
|
||
<div class="quiz-feedback"></div>
|
||
</div>
|
||
<button class="quiz-check-btn" onclick="checkQuiz('quiz-customer-m5')">检查答案</button>
|
||
<button class="quiz-reset-btn" onclick="resetQuiz('quiz-customer-m5')">重做</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
<section class="module" id="module-6">
|
||
<div class="module-inner">
|
||
<p class="eyebrow animate-in">模块 6 · L0 与 L1 画像</p>
|
||
<h1 class="module-title animate-in">正式风评 vs 抽槽画像<br>Top-K 怎么注入 prompt?</h1>
|
||
<p class="module-lead animate-in">
|
||
客户线除了查 Core 真账,还有后台<strong>画像抽槽</strong>(L1)调措辞。
|
||
拍板铁律:<strong>L0 永远优先</strong>——正式 C1~C5、持仓数字、适当性判定听 Core,L1 不能覆盖。
|
||
偏好类槽位注入 prompt 时走 <strong>Top-K + 时间衰减</strong>(R1),不是全量平铺。
|
||
</p>
|
||
|
||
<div class="screen animate-in">
|
||
<h2>L0 vs L1:谁说了算?</h2>
|
||
<div class="pattern-cards">
|
||
<div class="pattern-card">
|
||
<h3>L0 · Core 正式数据</h3>
|
||
<p>风评 C1~C5、持仓/流水数字、适当性矩阵判定——100% 来自 Core 只读查询 Tool(<code>core_ro_tool.py</code>)的 <code>fact_text</code>。LLM 只解读,不能改写。</p>
|
||
</div>
|
||
<div class="pattern-card">
|
||
<h3>L1 · 对话 enrich 画像</h3>
|
||
<p>登录客户每 5 轮节流,后台线程跑画像抽槽与合并(<code>profile_service.py</code>)→ 写入 MySQL <code>style_tags</code> + Redis 热缓存。只影响 generate/chitchat 的措辞语境,<strong>不能</strong>替代正式风评。</p>
|
||
</div>
|
||
<div class="pattern-card">
|
||
<h3>撞车规则</h3>
|
||
<p>抽槽若与 L0 字段冲突(例如用户口述「我是 R5」但 Core 是 R3)→ <strong>永远听 L0</strong>。合并规则 D7:用户显式 <code>user_declared</code> 不被 <code>inferred</code> 覆盖。</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="screen animate-in">
|
||
<h2>13 槽位 + confidence 门槛</h2>
|
||
<p>槽位表唯一来源:画像槽位定义(<code>profile_slots.py</code>)——13 个 path(含 C-08 <code>investment.allocation_target</code>),含 merge_mode 与 sensitivity。</p>
|
||
<div class="step-cards">
|
||
<div class="step-card">
|
||
<span class="step-num">①</span>
|
||
<p><strong>归一</strong> · <code>normalize_value</code><br>
|
||
例如「我喜欢货币基金」→ 映射到 9 类 <code>PRODUCT_TYPE_VOCAB</code>(不是具体 SKU 代码)。</p>
|
||
</div>
|
||
<div class="step-card">
|
||
<span class="step-num">②</span>
|
||
<p><strong>阈值</strong> · confidence ≥0.7(默认)/ ≥0.9(高敏)才写入;高敏 <code>inferred</code> 一律丢弃。</p>
|
||
</div>
|
||
<div class="step-card">
|
||
<span class="step-num">③</span>
|
||
<p><strong>合并</strong> · <code>merge_candidates</code><br>
|
||
单值 latest 覆盖;偏好/排除类 set_union 并集,并写 <code>items_meta</code>(R1)。</p>
|
||
</div>
|
||
</div>
|
||
<div class="callout callout-accent">
|
||
<strong>谁写 L1:</strong> 仅<strong>登录</strong>客户线(<code>customer_service.profile_maybe_extract</code>);游客试聊不写画像。<br>
|
||
<strong>C-04 阈值槽:</strong> <code>threshold_pref_summary</code> → <code>customer_threshold_config</code>;达线提醒仅在查持仓时 inline。
|
||
</div>
|
||
</div>
|
||
|
||
<div class="screen animate-in">
|
||
<h2>R1 · 偏好 Top-K 注入</h2>
|
||
<div class="translation-block">
|
||
<div class="translation-code">
|
||
<span class="translation-label">注入前筛选 · 画像上下文渲染(profile_service.py)</span>
|
||
<pre><code><span class="code-line"><span class="code-comment"># settings 默认:top_k=3 · ttl=90d · half_life=30d</span></span>
|
||
<span class="code-line">score = confidence × decay × (1 + log(mention_count))</span>
|
||
<span class="code-line"><span class="code-comment"># 超 TTL 的偏好不进 prompt;取得分 Top-K 条</span></span></code></pre>
|
||
</div>
|
||
<div class="translation-english">
|
||
<span class="translation-label">白话</span>
|
||
<div class="translation-lines">
|
||
<p class="tl">客户说过很多偏好类型,但 prompt 里只带<strong>最近、反复提到</strong>的前几条,避免画像越积越长带偏 LLM。</p>
|
||
<p class="tl">很久没提过的偏好(超过 90 天)不再注入,但 MySQL 里仍保留,下次提到会刷新。</p>
|
||
<p class="tl">改 K 或 TTL → 动环境配置(<code>settings.py</code> 的 <code>profile_preference_*</code>),别在 prompt 里硬编码。</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="quiz-container" id="quiz-customer-m6">
|
||
<div class="quiz-question-block"
|
||
data-correct="option-b"
|
||
data-explanation-right="对。L1 偏好只 enrich 措辞;持仓数字、正式风评、适当性结论必须来自 Core L0,画像不能覆盖。"
|
||
data-explanation-wrong="L0 正式数据优先级高于 L1 抽槽——这是合规硬阀门。">
|
||
<h3 class="quiz-question">客户口述「我是激进型 R5」,但 Core 风评是 R3,持仓查询应信谁?</h3>
|
||
<div class="quiz-options">
|
||
<button class="quiz-option" data-value="option-a" onclick="selectOption(this)">
|
||
<div class="quiz-option-radio"></div><span>信 L1 抽槽,按 R5 解读</span>
|
||
</button>
|
||
<button class="quiz-option" data-value="option-b" onclick="selectOption(this)">
|
||
<div class="quiz-option-radio"></div><span>信 Core L0 的 R3;L1 最多调闲聊措辞</span>
|
||
</button>
|
||
<button class="quiz-option" data-value="option-c" onclick="selectOption(this)">
|
||
<div class="quiz-option-radio"></div><span>取两者平均</span>
|
||
</button>
|
||
</div>
|
||
<div class="quiz-feedback"></div>
|
||
</div>
|
||
<button class="quiz-check-btn" onclick="checkQuiz('quiz-customer-m6')">检查答案</button>
|
||
<button class="quiz-reset-btn" onclick="resetQuiz('quiz-customer-m6')">重做</button>
|
||
</div>
|
||
|
||
<p class="module-footer-note">相关文件:画像抽槽与合并(<code>app/service/profile_service.py</code>)· 槽位表(<code>app/config/profile_slots.py</code>)· 环境配置(<code>app/config/settings.py</code>)</p>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
<section class="module" id="module-7">
|
||
<div class="module-inner">
|
||
<p class="eyebrow animate-in">模块 7 · Wave3 数据查询扩展</p>
|
||
<h1 class="module-title animate-in">净值、阈值、匹配说明<br>关键词怎么分流?</h1>
|
||
<p class="module-lead animate-in">
|
||
2026-09-10 批次在持仓/流水/风评/适当性之外,又接了<strong>净值查询(C-05)</strong>、<strong>亏损阈值(C-04)</strong>和<strong>「我能买什么」(C-11)</strong>。
|
||
指挥 AI 改意图时,先看<strong>关键词路由顺序</strong>——数据查询类必须优先于「帮我记住」类 save_note。
|
||
</p>
|
||
|
||
<div class="screen animate-in">
|
||
<h2>新增 intent 一览</h2>
|
||
<div class="pattern-cards">
|
||
<div class="pattern-card">
|
||
<h3>C-05 · nav_query</h3>
|
||
<p>「最新净值 / 单位净值 / 基金净值」→ Core 只读 <code>get_latest_nav</code>(快照)。<strong>「实时净值」</strong>仍走 <code>reject</code>(禁止实时盘口)。</p>
|
||
</div>
|
||
<div class="pattern-card">
|
||
<h3>C-04 · 阈值提醒</h3>
|
||
<p>用户说「亏 10% 提醒我」→ L1 槽 <code>threshold_pref_summary</code> + 写 <code>customer_threshold_config</code>;下次<strong>查持仓</strong>时组合加权盈亏达线 → 追加提醒 + <code>customer_notify_log</code>。</p>
|
||
</div>
|
||
<div class="pattern-card">
|
||
<h3>C-11 · 匹配说明</h3>
|
||
<p>「我能买什么 / 匹配产品 / 有哪些我能买」→ <code>suitability_check</code> + Core 可购列表。<strong>「推荐稳赚」</strong>仍 <code>reject</code>。</p>
|
||
</div>
|
||
<div class="pattern-card">
|
||
<h3>C-07 · 风评重测</h3>
|
||
<p>「重新测评 / 重做风评」扩进风评关键词 → 引导 App/网点;Agent 内<strong>不做</strong>问卷。</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="screen animate-in">
|
||
<h2>关键词优先级(易踩坑)</h2>
|
||
<p>客户线各分支 System Prompt 模板(<code>customer_prompts.py</code>)里 <code>keyword_route</code> 顺序:</p>
|
||
<div class="step-cards">
|
||
<div class="step-card">
|
||
<span class="step-num">1</span>
|
||
<p>转人工 / reject(实时净值、推荐稳赚等)</p>
|
||
</div>
|
||
<div class="step-card">
|
||
<span class="step-num">2</span>
|
||
<p><strong>数据查询</strong>:流水 · 风评 · 适当性 · 持仓(含「最近的流水」)</p>
|
||
</div>
|
||
<div class="step-card">
|
||
<span class="step-num">3</span>
|
||
<p><code>save_note</code>(「帮我记住…」——但不含上面已命中的数据词)</p>
|
||
</div>
|
||
<div class="step-card">
|
||
<span class="step-num">4</span>
|
||
<p>匹配说明 · 净值查询</p>
|
||
</div>
|
||
</div>
|
||
<div class="callout callout-warning">
|
||
<strong>回归用例:</strong> 「帮我记住最近的流水」必须走 <code>transaction_query</code>,不能误进 save_note——见 <code>test_wave5_notes.py</code>。
|
||
</div>
|
||
</div>
|
||
|
||
<div class="screen animate-in">
|
||
<div class="translation-block">
|
||
<div class="translation-code">
|
||
<span class="translation-label">C-04 · 亏损阈值服务(threshold_service.py)</span>
|
||
<pre><code><span class="code-line"><span class="code-comment"># 画像抽槽 → sync_threshold_from_summary</span></span>
|
||
<span class="code-line"><span class="code-comment"># query_holdings 末尾 → build_threshold_alert</span></span>
|
||
<span class="code-line"><span class="code-keyword">if</span> loss_pct >= threshold:</span>
|
||
<span class="code-line"> append alert + insert_notify_log</span></code></pre>
|
||
</div>
|
||
<div class="translation-english">
|
||
<span class="translation-label">白话</span>
|
||
<div class="translation-lines">
|
||
<p class="tl">阈值不是 push 通知——只有客户<strong>再来问持仓</strong>时才 inline 提醒。</p>
|
||
<p class="tl">组合盈亏按持仓<strong>市值加权</strong>算,不是单只基金。</p>
|
||
<p class="tl">改阈值:口述新比例 → 画像摘要更新 → config upsert。</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="screen animate-in">
|
||
<h2>1B 仍覆盖 nav_query</h2>
|
||
<p>违禁词扫描后的分治(<code>sanitize_postprocess.py</code>)把 <code>nav_query</code> 算进数据查询 intent——interpret 嘴瓢时仍回退 Core <code>fact_text</code>。</p>
|
||
|
||
<div class="quiz-container" id="quiz-customer-m7">
|
||
<div class="quiz-question-block"
|
||
data-correct="option-b"
|
||
data-explanation-right="对。C-05 走 Core 最新净值快照;「实时净值」是 reject 关键词,不是 nav_query。"
|
||
data-explanation-wrong="实时盘口/爬虫不在一期范围;见 C-05 选型文档。">
|
||
<h3 class="quiz-question">客户问「这只基金实时净值多少」,系统怎么走?</h3>
|
||
<div class="quiz-options">
|
||
<button class="quiz-option" data-value="option-a" onclick="selectOption(this)">
|
||
<div class="quiz-option-radio"></div><span>nav_query → Core get_latest_nav</span>
|
||
</button>
|
||
<button class="quiz-option" data-value="option-b" onclick="selectOption(this)">
|
||
<div class="quiz-option-radio"></div><span>reject · 实时净值禁止</span>
|
||
</button>
|
||
<button class="quiz-option" data-value="option-c" onclick="selectOption(this)">
|
||
<div class="quiz-option-radio"></div><span>RAG fin_product 查费率</span>
|
||
</button>
|
||
</div>
|
||
<div class="quiz-feedback"></div>
|
||
</div>
|
||
<button class="quiz-check-btn" onclick="checkQuiz('quiz-customer-m7')">检查答案</button>
|
||
<button class="quiz-reset-btn" onclick="resetQuiz('quiz-customer-m7')">重做</button>
|
||
</div>
|
||
|
||
<p class="module-footer-note">相关文件:客户线意图模板(<code>customer_prompts.py</code>)· Core 只读 Tool(<code>core_ro_tool.py</code>)· 阈值服务(<code>threshold_service.py</code>)· 测试 <code>test_threshold_service.py</code> · <code>test_wave2_prompts.py</code></p>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
</main>
|
||
|
||
</body>
|
||
</html>
|