- Added `status` query parameter to `list_sessions_api` for filtering sessions by their status (active/closed). - Introduced `close_all_sessions_api` endpoint to allow users to close all active sessions for the current actor. - Updated `SessionRepository` to support status filtering in session listing and implemented logic for closing active sessions. - Improved Redis connection settings for better performance and reliability. This update enhances the chat functionality by providing more control over session management, improving user experience and system efficiency.
581 lines
32 KiB
HTML
581 lines
32 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>
|
||
<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="D-06 缓存" role="tab" aria-label="模块 5: D-06 缓存"></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 就改 问数 REST(<code>analyst.py</code>) / NL→SQL 编排(<code>analyst_agent.py</code>);
|
||
要聊天体验才碰 <code>analytics/chat</code> 占位页,别和问数混成一个接口。
|
||
</div>
|
||
<div class="callout callout-warning">
|
||
<strong>customer self 域:</strong> 客户 token 进问数 → <code>domain=self</code>,
|
||
SQL 安全校验(<code>sql_guard.py</code>)强制 SQL 带本人 <code>customer_id</code>;免责声明追加 <code>CUSTOMER_AI_RISK_NOTE</code>。
|
||
</div>
|
||
</div>
|
||
|
||
<div class="screen animate-in">
|
||
<h2>后端「四件套」路由</h2>
|
||
<p>全部挂在 问数 REST(<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">问数 REST(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 安全校验(sql_guard.py) → 执行 → 解读</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">
|
||
NL→SQL 编排(<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":"② 模板匹配:published 模板填参 → 可跳过 LLM","packet":true,"from":"1","to":"2"},
|
||
{"highlight":"flow-actor-2","label":"②b 未命中 → DeepSeek + SCHEMA_PROMPT 生成 SQL","packet":true,"from":"1","to":"2"},
|
||
{"highlight":"flow-actor-3","label":"③ SQL 安全校验(sql_guard.py)五层校验:只读 / 表白名单 / 行级归属…","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 安全校验</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 安全校验(sql_guard.py)在拦什么</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 · NL→SQL 编排(analyst_agent.py).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 安全校验(sql_guard.py),执行只走只读连接。</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> → clarify;否则 <strong>模板填参</strong> 或 <code>_generate_sql</code>。</p>
|
||
</div>
|
||
<div class="pattern-card">
|
||
<h3>执行与缓存</h3>
|
||
<p><code>validate</code> → <code>execute_readonly</code>;Redis <strong>权限指纹 + SQL + 表世代</strong>;写交易/预警/L3 后 bump。</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, <code>template_hit</code>, 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 在 问数 REST(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>
|
||
<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">
|
||
问数 Agent 能出表格和解读,但<strong>不能关预警、不能当投顾、不能改画像</strong>。
|
||
另外「消歧总空」「改占位 Chat 没用」是 vibe coder 高频踩坑——这模块专门讲清。
|
||
</p>
|
||
|
||
<div class="screen animate-in">
|
||
<h2>问数三条红线(合规 §4.3)</h2>
|
||
<div class="step-cards">
|
||
<div class="step-card">
|
||
<span class="step-num">①</span>
|
||
<p><strong>不处置预警</strong> — 不能调用 <code>POST /api/risk/alerts/{id}/handle</code>;台账只读统计。</p>
|
||
</div>
|
||
<div class="step-card">
|
||
<span class="step-num">②</span>
|
||
<p><strong>不当投顾</strong> — 输出带免责声明;禁止生成可执行买卖/处置指令。</p>
|
||
</div>
|
||
<div class="step-card">
|
||
<span class="step-num">③</span>
|
||
<p><strong>不改 L0/L1 画像</strong> — SQL 只读;customer 域仅 <code>self</code> 趋势统计,不下钻他人。</p>
|
||
</div>
|
||
</div>
|
||
<div class="callout callout-warning">
|
||
<strong>和风控对话线的区别:</strong> 风控 Chat 用 RISK_TOOL_REGISTRY 只读查台账;问数走 <code>/api/analyst/chat</code> + SQL 安全校验(sql_guard.py),<strong>不要</strong>混接口。
|
||
</div>
|
||
</div>
|
||
|
||
<div class="screen animate-in">
|
||
<h2>排障:消歧为什么总空?</h2>
|
||
<p>口径字典在 MySQL,<strong>本机须执行一次</strong> <code>scripts/agent/seed-analyst-metric-dict.sql</code>。没灌种子 → NL2SQL 消歧永远不命中,不是 LLM 「坏了」。</p>
|
||
|
||
<div class="translation-block">
|
||
<div class="translation-code">
|
||
<span class="translation-label">占位页 vs 真问数</span>
|
||
<pre><code><span class="code-line">AnalystChatShell → 占位 UI,未接问数 API</span>
|
||
<span class="code-line">AnalystQueryPage → POST /api/analyst/chat</span>
|
||
<span class="code-line">改 Shell 不会出 SQL 表格</span></code></pre>
|
||
</div>
|
||
<div class="translation-english">
|
||
<span class="translation-label">白话</span>
|
||
<div class="translation-lines">
|
||
<p class="tl">菜单里两个「分析」入口可能让人改错文件——问数逻辑在 NL→SQL 编排(<code>analyst_agent.py</code>) + 问数页。</p>
|
||
<p class="tl">Redis 问数缓存键含<strong>权限指纹</strong>,别只 hash SQL 字符串,否则可能串权。</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="screen animate-in">
|
||
<div class="quiz-container" id="quiz-analyst-m4">
|
||
<div class="quiz-question-block"
|
||
data-correct="option-b"
|
||
data-explanation-right="对。问数线无 X-Agent-Type,走 get_platform_auth_context;加 analyst 头会走错栈。"
|
||
data-explanation-wrong="问数和 Chat 是两套鉴权,见总览模块 7 速查表。">
|
||
<h3 class="quiz-question">问数 401,AI 建议给 analyst.ts 加 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>不对,问数不要 Agent 头,查 Bearer + 平台鉴权</span>
|
||
</button>
|
||
<button class="quiz-option" data-value="option-c" onclick="selectOption(this)">
|
||
<div class="quiz-option-radio"></div><span>去掉 JWT,全用 debug 头</span>
|
||
</button>
|
||
</div>
|
||
<div class="quiz-feedback"></div>
|
||
</div>
|
||
<button class="quiz-check-btn" onclick="checkQuiz('quiz-analyst-m4')">检查答案</button>
|
||
<button class="quiz-reset-btn" onclick="resetQuiz('quiz-analyst-m4')">重做</button>
|
||
</div>
|
||
<p class="module-footer-note">相关:NL→SQL 编排(<code>app/service/analyst_agent.py</code>) · SQL 安全校验(<code>app/service/analyst/sql_guard.py</code>)</p>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
<section class="module" id="module-5">
|
||
<div class="module-inner">
|
||
<p class="eyebrow animate-in">模块 5 · D-06</p>
|
||
<h1 class="module-title animate-in">模板填参 + 结果缓存<br>写侧 bump 失效</h1>
|
||
<p class="module-lead animate-in">
|
||
问数两层「快路径」:① <strong>同形态问题</strong>命中 published 模板,跳过 LLM 写 SQL(<code>template_service.py</code>)
|
||
② 执行结果进 Redis,键里带<strong>表世代</strong>——交易/预警/L3 写后 bump,不等 TTL 过期。
|
||
</p>
|
||
|
||
<div class="screen animate-in">
|
||
<h2>两层 D-06 各管什么</h2>
|
||
<table style="width:100%; border-collapse:collapse; margin:1.5rem 0; font-size:0.95rem;">
|
||
<thead>
|
||
<tr><th style="text-align:left;padding:0.5rem;">层</th><th style="text-align:left;padding:0.5rem;">入口</th><th style="text-align:left;padding:0.5rem;">答辩句</th></tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr>
|
||
<td style="padding:0.5rem;">模板缓存</td>
|
||
<td style="padding:0.5rem;"><code>match_template</code> → 填 <code>:days</code> 等</td>
|
||
<td style="padding:0.5rem;">「客户+总数」→ <code>template_hit: true</code>,省 LLM 写 SQL</td>
|
||
</tr>
|
||
<tr>
|
||
<td style="padding:0.5rem;">结果缓存</td>
|
||
<td style="padding:0.5rem;"><code>cache_service.py</code> 权限指纹 + SQL hash + 世代</td>
|
||
<td style="padding:0.5rem;">「同 SQL 同权限」秒回;写侧 bump 后旧键自然 miss</td>
|
||
</tr>
|
||
<tr>
|
||
<td style="padding:0.5rem;">写侧失效</td>
|
||
<td style="padding:0.5rem;"><code>analyst_cache_invalidate</code> 交易/预警/L3 后</td>
|
||
<td style="padding:0.5rem;">「演示里刚模拟交易,问数不会还显示旧汇总」</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
<div class="callout callout-accent">
|
||
前端问数页会展示 <strong>模板命中</strong> / <strong>结果缓存</strong> 标签(读 <code>meta.template_hit</code> · <code>cache_hit</code>)。
|
||
</div>
|
||
</div>
|
||
|
||
<div class="screen animate-in">
|
||
<div class="translation-block">
|
||
<div class="translation-code">
|
||
<span class="translation-label">analyst_agent 决策顺序(简化)</span>
|
||
<pre><code><span class="code-line">clarify? → return suggestions</span>
|
||
<span class="code-line">template match? → fill SQL → validate → execute</span>
|
||
<span class="code-line">else LLM generate_sql → validate → execute (cache?)</span>
|
||
<span class="code-line">guardrail → persist analytics_query_log</span></code></pre>
|
||
</div>
|
||
<div class="translation-english">
|
||
<span class="translation-label">白话</span>
|
||
<div class="translation-lines">
|
||
<p class="tl">像<strong>预制菜 + 冰箱贴保质期</strong>:常问题型直接拿模板填天数;跑完的结果贴冰箱,但 Core 有新交易就撕掉旧标签(bump)。</p>
|
||
<p class="tl">种子:<code>scripts/agent/seed-analyst-query-templates.sql</code> · 一键演示:<code>prepare_all.ps1</code>。</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="screen animate-in">
|
||
<h2>群聊:模板 vs LLM vs 缓存</h2>
|
||
<div class="chat-window" id="chat-analyst-m5">
|
||
<div class="chat-messages">
|
||
<div class="chat-message" data-msg="0" data-sender="page" 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「近7天申购金额」</p>
|
||
</div>
|
||
</div>
|
||
<div class="chat-message" data-msg="1" data-sender="tpl" style="display:none">
|
||
<div class="chat-avatar" style="background:#6B5B95">template</div>
|
||
<div class="chat-bubble">
|
||
<span class="chat-sender" style="color:#6B5B95">template_service</span>
|
||
<p>关键词 + schema 命中 → 填 <code>:days=7</code> → 跳过 LLM</p>
|
||
</div>
|
||
</div>
|
||
<div class="chat-message" data-msg="2" data-sender="cache" style="display:none">
|
||
<div class="chat-avatar" style="background:#5A9DB8">cache</div>
|
||
<div class="chat-bubble">
|
||
<span class="chat-sender" style="color:#5A9DB8">cache_service</span>
|
||
<p>世代未变 → <code>cache_hit</code>;刚 bump → 重算再写 Redis</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<button class="btn chat-play-btn" data-chat="chat-analyst-m5">播放对话</button>
|
||
</div>
|
||
|
||
<div class="quiz-container" id="quiz-analyst-m5">
|
||
<div class="quiz-question-block"
|
||
data-correct="option-c"
|
||
data-explanation-right="对。模板是「同形态填参」省 LLM;结果缓存是「同 SQL 同权限」省 DB。两层独立,meta 可同时看 template_hit 与 cache_hit。"
|
||
data-explanation-wrong="D-06 是两层:不要混成「只有 Redis 缓存」。">
|
||
<h3 class="quiz-question">AI 说「问数缓存就是 Redis TTL 10 分钟」,你怎么纠正?</h3>
|
||
<div class="quiz-options">
|
||
<button class="quiz-option" data-value="option-a" onclick="selectOption(this)">
|
||
<div class="quiz-option-radio"></div><span>对,只靠 TTL</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>还有表世代 bump + 模板填参两层</span>
|
||
</button>
|
||
</div>
|
||
<div class="quiz-feedback"></div>
|
||
</div>
|
||
<button class="quiz-check-btn" onclick="checkQuiz('quiz-analyst-m5')">检查答案</button>
|
||
<button class="quiz-reset-btn" onclick="resetQuiz('quiz-analyst-m5')">重做</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
</main>
|
||
|
||
</body>
|
||
</html>
|