2026-09-09 23:22:59 +08:00
|
|
|
|
<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">
|
2026-09-10 10:57:40 +08:00
|
|
|
|
客户线不走顾问通用 tool→llm→guard 编排(<code>agent_service.py</code>),而是
|
|
|
|
|
|
登录客户 14 节点 LangGraph 编排(<code>customer_service.py</code>)里一张更大的
|
2026-09-09 23:22:59 +08:00
|
|
|
|
<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>
|
2026-09-10 10:57:40 +08:00
|
|
|
|
<div class="badge-item"><span class="badge-code">5</span><span class="badge-desc">tool_call · Core 只读 Tool(core_ro_tool.py)</span></div>
|
2026-09-09 23:22:59 +08:00
|
|
|
|
<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>。
|
2026-09-10 10:57:40 +08:00
|
|
|
|
<strong>口吻护栏与转人工决策</strong>见模块 4。
|
2026-09-09 23:22:59 +08:00
|
|
|
|
</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">
|
2026-09-10 10:57:40 +08:00
|
|
|
|
<span class="translation-label">图构建 · 登录客户 14 节点 LangGraph 编排(customer_service.py)</span>
|
2026-09-09 23:22:59 +08:00
|
|
|
|
<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>
|