- Introduced `build_all.py` script to automate the assembly of course modules into a single `index.html` file. - Created `index.html` for the main course overview, featuring a structured layout and navigation for various modules. - Developed `_base.html` and `_footer.html` templates for the advisor module, ensuring consistent styling and structure. - Added `build.sh` script for individual module assembly, enhancing modularity and ease of updates. - Implemented multiple module HTML files detailing specific training scenarios and functionalities for advisors, including interactive elements and quizzes. This update significantly enhances the course delivery framework, providing a comprehensive and interactive learning experience for advisors.
389 lines
21 KiB
HTML
389 lines
21 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: #2A7B9B;
|
||
--color-accent-hover: #1F6280;
|
||
--color-accent-light: #E4F2F7;
|
||
--color-accent-muted: #5A9DB8;
|
||
}
|
||
</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="NL→SQL" role="tab" aria-label="模块 2: NL→SQL"></button>
|
||
<button class="nav-dot" data-target="module-3" data-tooltip="Agent 与页面" role="tab" aria-label="模块 3: Agent 与页面"></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">问数工作台 vs 分析对话<br>两套门,别走错</h1>
|
||
<p class="module-lead animate-in">
|
||
数据分析 Agent 走<strong>平台鉴权</strong>(<code>get_platform_auth_context</code>),
|
||
和对话线不同——<strong>不要</strong>带 <span class="term" data-definition="HTTP 请求头,声明本次请求走哪条 Agent 业务线(customer/advisor/risk/analyst)。问数 API 故意不需要它。">X-Agent-Type</span>。
|
||
像进档案室查账:只出示工牌(JWT),不用说明「我是来聊天还是来问数」。
|
||
</p>
|
||
|
||
<div class="screen animate-in">
|
||
<h2>前端两条菜单</h2>
|
||
<div class="pattern-cards">
|
||
<div class="pattern-card">
|
||
<h3>问数工作台</h3>
|
||
<p><code>/app/analytics/query</code> → <code>AnalystQueryPage</code><br>
|
||
真·NL→SQL:表格 + SQL + 解读,调 <code>POST /api/analyst/chat</code></p>
|
||
</div>
|
||
<div class="pattern-card">
|
||
<h3>分析对话(占位)</h3>
|
||
<p><code>/app/analytics/chat</code> → <code>AnalystChatShell</code><br>
|
||
横幅写明「轻量对话占位;查数请用问数工作台」——还没接独立问数图</p>
|
||
</div>
|
||
</div>
|
||
<div class="callout callout-accent">
|
||
<strong>指挥 AI 改功能时:</strong> 要表格和 SQL 就改 <code>analyst.py</code> / <code>analyst_agent.py</code>;
|
||
要聊天体验才碰 <code>analytics/chat</code> 占位页,别和问数混成一个接口。
|
||
</div>
|
||
<div class="callout callout-warning">
|
||
<strong>customer self 域:</strong> 客户 token 进问数 → <code>domain=self</code>,
|
||
<code>sql_guard</code> 强制 SQL 带本人 <code>customer_id</code>;免责声明追加 <code>CUSTOMER_AI_RISK_NOTE</code>。
|
||
</div>
|
||
</div>
|
||
|
||
<div class="screen animate-in">
|
||
<h2>后端「四件套」路由</h2>
|
||
<p>全部挂在 <code>app/api/analyst.py</code>,前缀 <code>/api/analyst</code>:</p>
|
||
<div class="file-tree animate-in">
|
||
<div class="tree-item tree-folder">POST /chat</div>
|
||
<div class="tree-item tree-indent">问数主入口 → AnalystAgent.run()</div>
|
||
<div class="tree-item tree-folder">GET /dashboard</div>
|
||
<div class="tree-item tree-indent">智能看板卡片(按角色返回 metrics)</div>
|
||
<div class="tree-item tree-folder">POST /assets</div>
|
||
<div class="tree-item tree-indent">口径/样例沉淀(仅 analyst 角色可写)</div>
|
||
<div class="tree-item tree-folder">GET /ops/metrics</div>
|
||
<div class="tree-item tree-indent">运营指标:查询总量 / 被拦截次数</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="screen animate-in">
|
||
<h2>群聊:分析员点「提问」之后</h2>
|
||
<p>STAFF-20001 在问数工作台输入「客户总数是多少」——简化版:</p>
|
||
|
||
<div class="chat-window" id="chat-analyst-m1">
|
||
<div class="chat-messages">
|
||
<div class="chat-message" data-msg="0" data-sender="browser" style="display:none">
|
||
<div class="chat-avatar" style="background:#2A7B9B">前</div>
|
||
<div class="chat-bubble">
|
||
<span class="chat-sender" style="color:#2A7B9B">AnalystQueryPage</span>
|
||
<p>POST /api/analyst/chat · 只带 Bearer JWT(无 X-Agent-Type)</p>
|
||
</div>
|
||
</div>
|
||
<div class="chat-message" data-msg="1" data-sender="api" style="display:none">
|
||
<div class="chat-avatar" style="background:#5A9DB8">API</div>
|
||
<div class="chat-bubble">
|
||
<span class="chat-sender" style="color:#5A9DB8">analyst.py</span>
|
||
<p>get_platform_auth_context → analyst_auth_from_deps</p>
|
||
</div>
|
||
</div>
|
||
<div class="chat-message" data-msg="2" data-sender="agent" style="display:none">
|
||
<div class="chat-avatar" style="background:#1F6280">Agent</div>
|
||
<div class="chat-bubble">
|
||
<span class="chat-sender" style="color:#1F6280">AnalystAgent</span>
|
||
<p>消歧 → 生成 SQL → sql_guard → 执行 → 解读</p>
|
||
</div>
|
||
</div>
|
||
<div class="chat-message" data-msg="3" data-sender="browser" style="display:none">
|
||
<div class="chat-avatar" style="background:#2A7B9B">前</div>
|
||
<div class="chat-bubble">
|
||
<span class="chat-sender" style="color:#2A7B9B">表格 + answer</span>
|
||
<p>Ant Design Table 渲染 columns/rows;折叠面板展示 SQL</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="chat-typing" id="chat-analyst-m1-typing" style="display:none">
|
||
<div class="chat-avatar" id="chat-analyst-m1-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">前端 · analyst.ts</span>
|
||
<pre><code><span class="code-line"><span class="code-keyword">await</span> apiFetch(<span class="code-string">'/api/analyst/chat'</span>, {</span>
|
||
<span class="code-line"> method: <span class="code-string">'POST'</span>,</span>
|
||
<span class="code-line"> token, <span class="code-comment">// Bearer JWT only</span></span>
|
||
<span class="code-line"> body: JSON.stringify({ question, session_id }),</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">问数和读持仓一样:登录令牌够了,不用声明 Agent 类型。</p>
|
||
<p class="tl">后端用角色(analyst/advisor/customer…)决定能查哪片数据域,不靠 X-Agent-Type。</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="quiz-container" id="quiz-analyst-m1">
|
||
<div class="quiz-question-block"
|
||
data-correct="option-b"
|
||
data-explanation-right="对。/api/analyst/* 走平台鉴权,apiFetch 只传 token,不传 agentType。"
|
||
data-explanation-wrong="X-Agent-Type 是对话线和部分风控 REST 用的。问数故意不走那条门禁。">
|
||
<h3 class="quiz-question">问数工作台调 POST /api/analyst/chat,请求头要带 X-Agent-Type: analyst 吗?</h3>
|
||
<div class="quiz-options">
|
||
<button class="quiz-option" data-value="option-a" onclick="selectOption(this)">
|
||
<div class="quiz-option-radio"></div><span>要,和 chat 线一样</span>
|
||
</button>
|
||
<button class="quiz-option" data-value="option-b" onclick="selectOption(this)">
|
||
<div class="quiz-option-radio"></div><span>不要,只带 Bearer JWT</span>
|
||
</button>
|
||
<button class="quiz-option" data-value="option-c" onclick="selectOption(this)">
|
||
<div class="quiz-option-radio"></div><span>只有 dashboard 不要,chat 要</span>
|
||
</button>
|
||
</div>
|
||
<div class="quiz-feedback"></div>
|
||
</div>
|
||
<button class="quiz-check-btn" onclick="checkQuiz('quiz-analyst-m1')">检查答案</button>
|
||
<button class="quiz-reset-btn" onclick="resetQuiz('quiz-analyst-m1')">重做</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
<section class="module" id="module-2">
|
||
<div class="module-inner">
|
||
<p class="eyebrow animate-in">模块 2 · NL→SQL 管线</p>
|
||
<h1 class="module-title animate-in">一句话怎么变成<br>可审计的表格?</h1>
|
||
<p class="module-lead animate-in">
|
||
<code>app/service/analyst_agent.py</code> 把用户问题拆成可测试步骤。
|
||
像<strong>翻译官 + 安检 + 记账员</strong>:先弄清指标含义,再生成 SQL,层层校验后才执行,最后解读并留痕。
|
||
</p>
|
||
|
||
<div class="screen animate-in">
|
||
<h2>七步流水线(动画)</h2>
|
||
<p>点击「下一步」看 <span class="term" data-definition="Natural Language to SQL:把自然语言问题翻译成数据库查询语句。">NL→SQL</span> 在代码里怎么走:</p>
|
||
|
||
<div class="flow-animation" data-steps='[
|
||
{"highlight":"flow-actor-1","label":"① 消歧:口径字典命中多义指标 → 返回 suggestions 让用户选"},
|
||
{"highlight":"flow-actor-2","label":"② 生成 SQL:DeepSeek + SCHEMA_PROMPT + 权限域提示","packet":true,"from":"1","to":"2"},
|
||
{"highlight":"flow-actor-3","label":"③ sql_guard 五层校验:只读 / 表白名单 / 行级归属…","packet":true,"from":"2","to":"3"},
|
||
{"highlight":"flow-actor-4","label":"④ 执行:AnalyticsRepo 只读查询(Redis 缓存可命中)","packet":true,"from":"3","to":"4"},
|
||
{"highlight":"flow-actor-5","label":"⑤ guardrail:解读数字必须与表格一致,不过则降级","packet":true,"from":"4","to":"5"},
|
||
{"highlight":"flow-actor-1","label":"⑥ 留痕:analytics_query_log 写入 SQL/耗时/护栏结果","packet":true,"from":"5","to":"1"}
|
||
]'>
|
||
<div class="flow-actors">
|
||
<div class="flow-actor" id="flow-actor-1"><span class="flow-actor-icon">📖</span><span>消歧 dict</span></div>
|
||
<div class="flow-actor" id="flow-actor-2"><span class="flow-actor-icon">🤖</span><span>LLM 生成</span></div>
|
||
<div class="flow-actor" id="flow-actor-3"><span class="flow-actor-icon">🛡</span><span>sql_guard</span></div>
|
||
<div class="flow-actor" id="flow-actor-4"><span class="flow-actor-icon">🗄</span><span>execute</span></div>
|
||
<div class="flow-actor" id="flow-actor-5"><span class="flow-actor-icon">✓</span><span>guardrail</span></div>
|
||
</div>
|
||
<p class="flow-step-label">校验失败 → 403 或 degrade(只给表格不给瞎编解读)</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">
|
||
<h2>sql_guard 在拦什么</h2>
|
||
<div class="pattern-cards">
|
||
<div class="pattern-card">
|
||
<h3>只读白名单</h3>
|
||
<p>拒绝 INSERT/UPDATE/DELETE 等写操作关键字</p>
|
||
</div>
|
||
<div class="pattern-card">
|
||
<h3>表白名单</h3>
|
||
<p><code>core_*</code> + <code>risk_alert</code> 等,超出即 403</p>
|
||
</div>
|
||
<div class="pattern-card">
|
||
<h3>行级归属</h3>
|
||
<p>customer 域强制 <code>customer_id = 本人</code>;advisor 只能名下客户</p>
|
||
</div>
|
||
<div class="pattern-card">
|
||
<h3>粒度控制</h3>
|
||
<p>ops 聚合域禁止下钻到单个客户明细</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="screen animate-in">
|
||
<div class="translation-block">
|
||
<div class="translation-code">
|
||
<span class="translation-label">CODE · analyst_agent.run</span>
|
||
<pre><code><span class="code-line">amb = self._detect_ambiguity(question)</span>
|
||
<span class="code-line"><span class="code-keyword">if</span> amb: <span class="code-keyword">return</span> self._clarify(amb, trace_id)</span>
|
||
<span class="code-line">sql_text, usage = self._generate_sql(...)</span>
|
||
<span class="code-line">vres = validate(sql_text, domain, scope) <span class="code-comment"># sql_guard</span></span>
|
||
<span class="code-line">exec_result = self.repo.execute_readonly(sql_text)</span>
|
||
<span class="code-line">answer, guard = self._generate_verified_answer(...)</span>
|
||
<span class="code-line">self._persist(...) <span class="code-comment"># analytics_query_log</span></span></code></pre>
|
||
</div>
|
||
<div class="translation-english">
|
||
<span class="translation-label">白话</span>
|
||
<div class="translation-lines">
|
||
<p class="tl">指标说不清就先反问,不瞎猜 SQL。</p>
|
||
<p class="tl">生成后必须过 sql_guard,执行只走只读连接。</p>
|
||
<p class="tl">AI 解读若数字对不上表,降级为「请看表格」;全程写入查询日志备查。</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="quiz-container" id="quiz-analyst-m2">
|
||
<div class="quiz-question-block"
|
||
data-correct="option-c"
|
||
data-explanation-right="guardrail 不通过时 status=degrade:保留 table/sql,answer 换成安全文案。"
|
||
data-explanation-wrong="不会静默丢数据;表格是 ground truth,解读可被降级。">
|
||
<h3 class="quiz-question">数字护栏(guardrail)校验失败时,用户还能看到查询结果表格吗?</h3>
|
||
<div class="quiz-options">
|
||
<button class="quiz-option" data-value="option-a" onclick="selectOption(this)">
|
||
<div class="quiz-option-radio"></div><span>不能,整单 500 错误</span>
|
||
</button>
|
||
<button class="quiz-option" data-value="option-b" onclick="selectOption(this)">
|
||
<div class="quiz-option-radio"></div><span>能,但 SQL 会被隐藏</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-analyst-m2')">检查答案</button>
|
||
<button class="quiz-reset-btn" onclick="resetQuiz('quiz-analyst-m2')">重做</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
<section class="module" id="module-3">
|
||
<div class="module-inner">
|
||
<p class="eyebrow animate-in">模块 3 · Agent 与页面</p>
|
||
<h1 class="module-title animate-in">AnalystAgent.run<br>+ 看板与资产 API</h1>
|
||
<p class="module-lead animate-in">
|
||
问数主脑是 <code>AnalystAgent.run</code>;页面侧 <code>AnalystQueryPage</code>(P2)
|
||
同时拉 <code>GET /dashboard</code> 卡片和 <code>POST /chat</code> 结果。
|
||
口径沉淀走 <code>POST /assets</code>(仅 analyst 角色可写)。
|
||
</p>
|
||
|
||
<div class="screen animate-in">
|
||
<h2>AnalystAgent.run 要点</h2>
|
||
<div class="pattern-cards">
|
||
<div class="pattern-card">
|
||
<h3>鉴权与域</h3>
|
||
<p><code>assert_analyst_query_access</code> → domain;<code>resolve_analyst_scope</code> → customer_id 白名单。</p>
|
||
</div>
|
||
<div class="pattern-card">
|
||
<h3>消歧与生成</h3>
|
||
<p><code>_detect_ambiguity</code> 命中则 <code>_clarify</code> 返回 suggestions;否则 <code>_generate_sql</code>。</p>
|
||
</div>
|
||
<div class="pattern-card">
|
||
<h3>执行与缓存</h3>
|
||
<p><code>validate</code> → <code>execute_readonly</code>;Redis 按权限指纹 + SQL hash 缓存结果。</p>
|
||
</div>
|
||
<div class="pattern-card">
|
||
<h3>解读与留痕</h3>
|
||
<p><code>_generate_verified_answer</code> + guardrail;<code>_persist</code> 写 analytics_query_log。</p>
|
||
</div>
|
||
</div>
|
||
<div class="callout callout-accent">
|
||
<strong>返回体 AnalystResponse:</strong> answer + table + sql + meta(exec_ms, cache_hit, cost_est)+ disclaimer + status(success / degrade / clarify)。
|
||
</div>
|
||
</div>
|
||
|
||
<div class="screen animate-in">
|
||
<h2>dashboard / assets API</h2>
|
||
<div class="pattern-cards">
|
||
<div class="pattern-card">
|
||
<h3>GET /dashboard</h3>
|
||
<p>按角色返回 cards 文案 + metrics:analyst 全库统计、customer 本人持仓、advisor 名下 AUM 等。</p>
|
||
</div>
|
||
<div class="pattern-card">
|
||
<h3>POST /assets</h3>
|
||
<p>kind=dict/few_shot/template;仅 <code>analyst</code> 角色,写入口径资产表。</p>
|
||
</div>
|
||
<div class="pattern-card">
|
||
<h3>GET /ops/metrics</h3>
|
||
<p>运营侧查询总量与被拦截次数,与 dashboard 卡片分离。</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="screen animate-in">
|
||
<div class="translation-block">
|
||
<div class="translation-code">
|
||
<span class="translation-label">CODE · AnalystQueryPage.tsx(P2)</span>
|
||
<pre><code><span class="code-line">useEffect(() => getAnalystDashboard(token))</span>
|
||
<span class="code-line">setDashMetrics(d.metrics)</span>
|
||
<span class="code-line">setDashCards(d.cards)</span>
|
||
<span class="code-line"><span class="code-comment">// 用户点查询</span></span>
|
||
<span class="code-line">const resp = await postAnalystChat(token, question)</span>
|
||
<span class="code-line">setResult(resp) <span class="code-comment">// Table + Collapse(SQL)</span></span></code></pre>
|
||
</div>
|
||
<div class="translation-english">
|
||
<span class="translation-label">白话</span>
|
||
<div class="translation-lines">
|
||
<p class="tl">进页面先拉看板 MetricCard,数字来自 /dashboard 不是 chat。</p>
|
||
<p class="tl">提问才走 AnalystAgent.run,结果区展示解读、表格、可折叠 SQL。</p>
|
||
<p class="tl">apiFetch 只带 token,与 risk.ts 的 X-Agent-Type 头无关。</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="quiz-container" id="quiz-analyst-m3">
|
||
<div class="quiz-question-block"
|
||
data-correct="option-a"
|
||
data-explanation-right="POST /assets 在 analyst.py 硬校验 analyst 角色,advisor 或 customer 会 403。"
|
||
data-explanation-wrong="资产沉淀是分析员专属写操作,其他角色只能问数或看 dashboard。">
|
||
<h3 class="quiz-question">理财师 STAFF-10086 能调用 POST /api/analyst/assets 沉淀口径吗?</h3>
|
||
<div class="quiz-options">
|
||
<button class="quiz-option" data-value="option-a" onclick="selectOption(this)">
|
||
<div class="quiz-option-radio"></div><span>不能,仅 analyst 角色</span>
|
||
</button>
|
||
<button class="quiz-option" data-value="option-b" onclick="selectOption(this)">
|
||
<div class="quiz-option-radio"></div><span>能,advisor 也有 assigned 域</span>
|
||
</button>
|
||
<button class="quiz-option" data-value="option-c" onclick="selectOption(this)">
|
||
<div class="quiz-option-radio"></div><span>能,只要带 Bearer 就行</span>
|
||
</button>
|
||
</div>
|
||
<div class="quiz-feedback"></div>
|
||
</div>
|
||
<button class="quiz-check-btn" onclick="checkQuiz('quiz-analyst-m3')">检查答案</button>
|
||
<button class="quiz-reset-btn" onclick="resetQuiz('quiz-analyst-m3')">重做</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
</main>
|
||
|
||
</body>
|
||
</html>
|