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.
This commit is contained in:
2026-09-09 23:22:59 +08:00
parent 3ad244add0
commit 6f222f1c56
78 changed files with 20237 additions and 1 deletions
@@ -0,0 +1,146 @@
<section class="module" id="module-2">
<div class="module-inner">
<p class="eyebrow animate-in">模块 2 · 三张牌编排</p>
<h1 class="module-title animate-in">tool → llm → guard<br>群聊走一遍</h1>
<p class="module-lead animate-in">
顾问线 LangGraph 只有三个业务节点(加 START/END):
<span class="term" data-definition="tool 节点 = 根据用户消息关键词匹配 Tool 名,调 run_tool 查库并落 agent_tool_call 留痕。">tool</span>
→ llm → guard。比客户 14 节点图短,但<strong>归属校验在 tool_service.run_tool 里</strong>,和客户线铁律口径一致。
</p>
<div class="screen animate-in">
<h2>节点职责(背下来少踩坑)</h2>
<div class="pattern-cards">
<div class="pattern-card">
<h3>tool_node</h3>
<p><code>match_intent("advisor", message)</code> 命中关键词 → <code>run_tool</code> 查 Core / 知识库 → 结果写入 <code>tool_results</code>。</p>
</div>
<div class="pattern-card">
<h3>llm_node</h3>
<p>System prompt(顾问边界)+ Tool 摘要注入 + 历史窗口 + 本轮用户话 → DeepSeek 组织自然语言。</p>
</div>
<div class="pattern-card">
<h3>guard_node</h3>
<p>顾问线<strong>不附</strong>客户/风控那种尾部免责声明(<code>needs_disclaimer("advisor")</code> 为 false)。</p>
</div>
</div>
</div>
<div class="screen animate-in">
<h2>群聊:理财师问「客户 CUST-9527 持仓如何」</h2>
<p>简化播放 tool → llm → guard 全链路(前面 chat.py 已验 JWT + G-01)。</p>
<div class="chat-window" id="chat-advisor-m2">
<div class="chat-messages">
<div class="chat-message" data-msg="0" data-sender="browser" style="display:none">
<div class="chat-avatar" style="background:#D4A843">前</div>
<div class="chat-bubble">
<span class="chat-sender" style="color:#D4A843">浏览器</span>
<p>POST /api/chat/stream · X-Agent-Type: advisor · customer_id: CUST-9527</p>
</div>
</div>
<div class="chat-message" data-msg="1" data-sender="chat" style="display:none">
<div class="chat-avatar" style="background:#B8922F">API</div>
<div class="chat-bubble">
<span class="chat-sender" style="color:#B8922F">chat.py</span>
<p>验 JWT → assert_customer_access(G-01:名下客户)→ 建/续 session</p>
</div>
</div>
<div class="chat-message" data-msg="2" data-sender="tool" style="display:none">
<div class="chat-avatar" style="background:#E0C06A">Tool</div>
<div class="chat-bubble">
<span class="chat-sender" style="color:#E0C06A">tool_node</span>
<p>关键词「持仓」→ <code>query_holdings</code> → tool_service.run_tool</p>
</div>
</div>
<div class="chat-message" data-msg="3" data-sender="core" style="display:none">
<div class="chat-avatar" style="background:#2D8B55">DB</div>
<div class="chat-bubble">
<span class="chat-sender" style="color:#2D8B55">core_ro</span>
<p>只读 SQL 查持仓 → fact_text 摘要写入 tool_results;agent_tool_call 落库 success</p>
</div>
</div>
<div class="chat-message" data-msg="4" data-sender="llm" style="display:none">
<div class="chat-avatar" style="background:#7B6DAA">AI</div>
<div class="chat-bubble">
<span class="chat-sender" style="color:#7B6DAA">llm_node</span>
<p>Tool 摘要注入 system 上下文 → DeepSeek 生成顾问口吻回复(数据引用 fact_text)</p>
</div>
</div>
<div class="chat-message" data-msg="5" data-sender="guard" style="display:none">
<div class="chat-avatar" style="background:#2A7B9B">盾</div>
<div class="chat-bubble">
<span class="chat-sender" style="color:#2A7B9B">guard_node</span>
<p>advisor 不拼 CHAT_DISCLAIMER;reply 原样传出</p>
</div>
</div>
<div class="chat-message" data-msg="6" data-sender="sse" style="display:none">
<div class="chat-avatar" style="background:#D4A843">流</div>
<div class="chat-bubble">
<span class="chat-sender" style="color:#D4A843">stream_chat</span>
<p>Tool 同步跑完后逐块推 LLM 文本;收完 done 后 api 层统一落 MySQL 回合</p>
</div>
</div>
</div>
<div class="chat-typing" id="chat-advisor-m2-typing" style="display:none">
<div class="chat-avatar" id="chat-advisor-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">CODE · agent_service.tool_node</span>
<pre><code><span class="code-line">tool_name = tool_service.match_intent(</span>
<span class="code-line"> state[<span class="code-string">"agent_type"</span>], state[<span class="code-string">"user_message"</span>]</span>
<span class="code-line">)</span>
<span class="code-line">record = tool_service.run_tool(</span>
<span class="code-line"> tool_name=tool_name,</span>
<span class="code-line"> agent_type=state[<span class="code-string">"agent_type"</span>],</span>
<span class="code-line"> customer_id=state.get(<span class="code-string">"customer_id"</span>) or <span class="code-string">""</span>,</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">先靠关键词判断要不要查库;闲聊不命中就 tool_results 为空,LLM 纯聊。</p>
<p class="tl">customer_id 来自会话绑定(chat.py 归属校验后的值),不是 LLM 生成的。</p>
<p class="tl">query_holdings 和 customer 线共用 core_tools 定义,但跑在 agent_service 图里。</p>
</div>
</div>
</div>
<div class="quiz-container" id="quiz-advisor-m2">
<div class="quiz-question-block"
data-correct="option-a"
data-explanation-right="对。一期用关键词 match_intent;命中「持仓」等词才调 query_holdings。"
data-explanation-wrong="顾问线 Tool 意图是一期关键词规则,不是 LLM 自由选函数名。">
<h3 class="quiz-question">理财师问「这位客户仓位怎么样」,tool_node 如何选中 query_holdings?</h3>
<div class="quiz-options">
<button class="quiz-option" data-value="option-a" onclick="selectOption(this)">
<div class="quiz-option-radio"></div><span>match_intent 关键词命中「仓位」</span>
</button>
<button class="quiz-option" data-value="option-b" onclick="selectOption(this)">
<div class="quiz-option-radio"></div><span>LLM 在回复里输出函数名</span>
</button>
<button class="quiz-option" data-value="option-c" onclick="selectOption(this)">
<div class="quiz-option-radio"></div><span>前端在 body 里传 tool_name</span>
</button>
</div>
<div class="quiz-feedback"></div>
</div>
<button class="quiz-check-btn" onclick="checkQuiz('quiz-advisor-m2')">检查答案</button>
<button class="quiz-reset-btn" onclick="resetQuiz('quiz-advisor-m2')">重做</button>
</div>
</div>
</div>
</section>