Files
group_xinghuo_jinrong/docs/course/jinrong-module-analyst/index.html
T
zhanghongyu_0626 6f222f1c56 feat(course): Add course assembly script and module structure for advisor training
- 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.
2026-09-09 23:22:59 +08:00

389 lines
21 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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>