模块 1 · 客户视角
客户能做什么?
从 SSE 对话到 Core 只读
登录客户(如 CUST-9527)打开「客户助手」后,可走同步或
SSE 流式
对话。未登录访客另有游客试聊(只 RAG、不查持仓)。
2026-09 merger 分支:客户线 SSE 已接通,测试基线 **786 passed**。
四条能力线(指挥 AI 改功能前先对齐)
对话 SSE / 同步
POST /api/chat/stream 或 POST /api/chat,请求头 X-Agent-Type: customer + Bearer JWT。
持仓 / 流水 / 风评 / 净值
意图命中后走 Core 模拟库只读查询 Tool(core_ro_tool.py);含 C-05 最新净值(非实时);C-04 查持仓时可 inline 阈值提醒。
产品规则 RAG
fin_* 知识库(Milvus)回答申购赎回、费率等产品咨询;画像语境注入但不泄露敏感字段。
游客试聊
POST /api/chat/visitor 免登录,游客试聊 9 节点编排(visitor_service.py),不写客户会话表。
customer_service.py),不走 顾问通用 tool→llm→guard 编排(agent_service.py)。
数据流:浏览器问「我持仓多少」
客户发消息后,四 Agent 对话 HTTP 入口(chat.py)按 X-Agent-Type 分流到客户编排,再查 Core。
点击「下一步」看请求怎么走
if agent_type == "customer":
host_ctx = host_auth_for_customer_service(auth, trace_id=trace_id)
customer_prep = prepare_customer_stream(
host_ctx, message, sid, cust_id, req.end_session
)
# 图跑完后按 chunks 推 SSE
看到 customer 就换轨道:不进 agent_service,改走客户专用编排。
先把 JWT 转成 customer_service 认识的 AuthContext(字段名对齐)。
流式路径:LangGraph 整图跑完,再把回复切成块推给前端——Tool/RAG 仍是同步完成。
未登录访客在首页试聊,会查到自己的持仓吗?
模块 2 · 编排内核
14 节点 LangGraph:
从回忆到归档
客户线不走顾问通用 tool→llm→guard 编排(agent_service.py),而是
登录客户 14 节点 LangGraph 编排(customer_service.py)里一张更大的
LangGraph。
入口永远是 recall_memory,出口经 save_memory 写记忆后收尾。
14 个节点一览(改分支前先认门牌号)
intent_classify 之后走 RAG(产品/政策/FAQ)、走 Tool(持仓/流水/风评)、或走静态话术(拒绝/转人工/闲聊)。所有生成分支最后都汇入 save_memory。
口吻护栏与转人工决策见模块 4。
群聊动画:客户问「我持仓多少」
下面按节点顺序播放(数据查询分支);若问「申购费率多少」则在节点 2 后改走 rag_search → generate。
g.set_entry_point("recall_memory")
g.add_edge("recall_memory", "intent_classify")
g.add_conditional_edges("intent_classify", _route, {...})
g.add_edge("tool_call", "interpret")
g.add_edge("rag_search", "generate")
# 各生成分支 → save_memory → profile → archive → END
先回忆上下文,再让 LLM 判意图,然后走 RAG 或 Tool 或静态话术。
数据类问题:param_extract → tool_call → interpret,数字 100% 来自 Core。
无论哪条分支,最后都要 save_memory,再异步做画像和归档。
客户问「基金申购费率多少」,LangGraph 走哪条分支?
模块 3 · 铁律与流式
两条铁律 +
prepare_customer_stream
客户线能跑通,靠文件头写死的两条 铁律。 流式 SSE 看起来是「边生成边推」,实现上是先跑完整张 LangGraph,再切块推送。
铁律(指挥 AI 改代码时别碰)
铁律 1 · 数值不经过 LLM 编造
持仓、流水、风评等查询结果 100% 来自 Core 只读查询 Tool(core_ro_tool.py)的 fact_text。LLM 在 interpret / generate 里只做解读与组织语言,不能自己算数。
铁律 2 · customer_id 只来自 JWT
四 Agent 对话 HTTP 入口(chat.py)在进图之前已从令牌解析客户号并做归属校验。图内 state["customer_id"] 不信用户消息里的「帮我查 CUST-xxx」。
agent_service.py)的 tool→llm→guard;客户走独立 14 节点图。别混用对话 Tool 编排(tool_service.py)关键词意图那套来改客户 RAG 分支。
流式真相:图跑完再切块
方案 C 下 prepare_customer_stream 先调用 run_customer_chat 跑完整图,再用 _chunk_reply_text 切成 16 份左右推 SSE——Tool/RAG 仍是同步完成,不是 token 级流式。
def prepare_customer_stream(
ctx, message, session_id, customer_id, end_session=False
) -> dict[str, Any]:
reply, has_disclaimer, intent, transfer = run_customer_chat(
ctx, message, session_id, customer_id, end_session
)
return {
"reply": reply,
"has_disclaimer": has_disclaimer,
"intent": intent,
"transfer_to_human": transfer,
"chunks": _chunk_reply_text(reply),
}
入参里的 customer_id 是四 Agent 对话 HTTP 入口(chat.py)验完 JWT 后塞进来的,函数内部不再解析用户文本。
先拿到完整 reply(含 intent、是否转人工、是否附免责),再切片给 SSE 层逐块写。
改「真流式」要先动 LangGraph 执行模型,不是只改前端 ChatPanel。
相关文件(改流式 / 铁律时打开这些)
客户 SSE 流式回复,LangGraph 什么时候跑完?
模块 4 · 口吻与转人工
专业口吻怎么「锁住」?
什么时候该转人工?
机制课讲了 LangGraph 怎么走,但客服线还有两条你指挥 AI 时最常碰的线: 回复别漂成投顾/推销员,以及什么时候打「建议转人工」。 代码里不是单靠「写个好 prompt」,而是三层滤网 + 一条主动转人工路径(sanitize 命中不再自动转人工,拍板 1B)。
三层滤网:防止口吻漂移
可以把客服回复想成「过三道闸」——前面两道尽量写对,最后一道不管 LLM 说什么都要拦违规词。
Prompt 层 · 客户线各分支 System Prompt 模板(customer_prompts.py)
每个生成分支有独立 system 模板(INTERPRET_SYSTEM / GENERATE_SYSTEM / CHITCHAT_SYSTEM),开头就定身份:「金融客服助手(已登录客户模式)」——禁止投顾话术、禁止预测、画像只调措辞不能念出。
记忆层 · recall_memory
咨询走 consult_memory、闲聊走 chitchat_memory,再注入 profile_context。让回复有连续语境,但 prompt 里写死:不得主动复述年龄/收入等画像字段。
事后扫描 · finalize_sanitized_reply()(sanitize_postprocess.py)
interpret / generate / chitchat 产出后过违禁词正则。命中后:数据查询类(含 nav_query)回退 Core fact_text;RAG/闲聊替换 COMPLIANCE_REJECT——均不自动 transfer_to_human。游客线已对齐。
chat.py)的 input_guard 在进图之前就拦注入/超长——这类请求根本到不了 LLM。
def finalize_sanitized_reply(reply, *, intent, fact_text):
safe, hit = sanitize_reply(reply)
if not hit: return safe, False
if intent in DATA_QUERY and fact_text:
return fact_text, False # 1B:保真账,不转人工
return COMPLIANCE_REJECT, False
数据查询分支:LLM 嘴瓢时回退 Core fact_text,客户仍能看到「持有 3 只产品」等真数。
RAG/闲聊分支:替换为固定拒答,但不打转人工标志——除非用户原话已是投诉/转人工。
底层 sanitize_reply 仍负责扫违禁词;分治逻辑在 customer 编排层。
转人工 vs 直接拒绝:别混成一条线
模块 2 列出了 reject 和 transfer_human 两个节点,但触发条件完全不同。指挥 AI 改逻辑时最容易搞混这里。
点击「下一步」看决策分叉
路径 A · 主动转人工
关键词:转人工、投诉、纠纷、账户异常、被盗 等 → TRANSFER_TEXT + transfer_to_human=True。
路径 B · LLM 越界被拦(1B)
数据查询 interpret 违禁 → 回退 fact_text,transfer=false。RAG/闲聊违禁 → COMPLIANCE_REJECT,transfer=false。
路径 C · 边界外但不必转人工
问「推荐稳赚基金」「明天会涨吗」→ reject 静态话术(如 REJECT_ADVICE_TEXT)。不自动打转人工标志;话术中可引导用户自行说「转人工」。
ChatPanel),不会真的排队接入真人坐席——真转接要接呼叫中心/工单系统,那是产品层后续工作。
群聊动画:LLM 嘴瓢后被扫描层拦住
客户问持仓,Tool 返回正常 fact_text,但 interpret 的 LLM 多嘴加了「建议您加仓」——扫描层介入。
RAG 咨询还要加风险提示
产品/政策/FAQ 类意图在 generate 通过后,若 intent 属于咨询类,会追加 RISK_DISCLAIMER(should_add_disclaimer)。这是「口吻」的另一面:该免责时必须免责,不是只有拦违规词。
客户说「推荐一个稳赚不赔的基金」,系统会怎么标记?
customer_prompts.py)对应 SYSTEM,别只改前端;
② 加新禁语 → 同时更新 FORBIDDEN_TERMS 和意图关键词路由;
③ 加「必须转人工」场景 → 改 _TRANSFER_KEYWORDS 或 INTENT_SYSTEM 规则,别误放进 reject。
模块 5 · 边界与接缝
两套知识库、合规口径
和一期代码差在哪?
客服线「能答什么」不只看 LangGraph,还看知识库分家和合规文档 vs 一期实现。 指挥 AI 改 RAG 或「放开推荐」前,先对齐下面这张差分表。
两套 RAG:别灌错库
客服 · fin_* 三库
登录客户的对话编排(customer_service.py)和游客试聊编排(visitor_service.py)遇到产品/政策/FAQ 类问题时,都会交给 RAG 检索层(rag_service.py:把问题向量化后查 Milvus),从 fin_product · fin_policy · fin_faq 三个知识集合取片段。
顾问 · kb_product_rules
理财师/风控通用 tool→llm→guard 编排(agent_service.py)的 search_knowledge Tool → 知识库对话 Tool 注册与分发(kb_tools.py)· 集合名不同,不能把顾问文档灌进 fin_*。
风控 · 不开放 search_knowledge
知识库对话 Tool 注册与分发(kb_tools.py)注册表对 risk 关闭——风控对话走 RISK_TOOL_REGISTRY 四只读 Tool。
scripts/kb/ 脚本。
合规允许 vs 一期拒答(你拍板要讲的)
合规文档里,客户 Agent 在过 R-02 + 附免责 + 标注需持证审核时,可以做「匹配说明」——但一期代码对主动推荐/预测类问题走 reject 静态拒答,更保守。
适当性匹配说明(C-11)
→ 系统判定 + 规则解读 + 免责声明
→ 禁止具体操作指令
suitability_check Tool:「我能买 R3 吗」「有哪些匹配产品」——允许。
「推荐稳赚基金」「买什么好」→ reject + REJECT_ADVICE_TEXT——直接拒。
要「放开推荐」= 产品决策 + 改 intent 路由 + 合规评审,不是只改 prompt 一句。
群聊:同一客户,两种问法
游客与画像:客户看不见 L2/L3
游客走游客试聊 9 节点编排(visitor_service.py):无 JWT、无 Core 持仓查询、不写登录客户会话表。L1 画像由客服线后台抽槽写入;L2/L3 对客户不可见——合规上客户只能 enrich L1,不能替代正式风评。
AI 建议「把 kb 和 fin 合成一个 Milvus 集合简化维护」,你怎么回?
模块 6 · L0 与 L1 画像
正式风评 vs 抽槽画像
Top-K 怎么注入 prompt?
客户线除了查 Core 真账,还有后台画像抽槽(L1)调措辞。 拍板铁律:L0 永远优先——正式 C1~C5、持仓数字、适当性判定听 Core,L1 不能覆盖。 偏好类槽位注入 prompt 时走 Top-K + 时间衰减(R1),不是全量平铺。
L0 vs L1:谁说了算?
L0 · Core 正式数据
风评 C1~C5、持仓/流水数字、适当性矩阵判定——100% 来自 Core 只读查询 Tool(core_ro_tool.py)的 fact_text。LLM 只解读,不能改写。
L1 · 对话 enrich 画像
登录客户每 5 轮节流,后台线程跑画像抽槽与合并(profile_service.py)→ 写入 MySQL style_tags + Redis 热缓存。只影响 generate/chitchat 的措辞语境,不能替代正式风评。
撞车规则
抽槽若与 L0 字段冲突(例如用户口述「我是 R5」但 Core 是 R3)→ 永远听 L0。合并规则 D7:用户显式 user_declared 不被 inferred 覆盖。
13 槽位 + confidence 门槛
槽位表唯一来源:画像槽位定义(profile_slots.py)——13 个 path(含 C-08 investment.allocation_target),含 merge_mode 与 sensitivity。
归一 · normalize_value
例如「我喜欢货币基金」→ 映射到 9 类 PRODUCT_TYPE_VOCAB(不是具体 SKU 代码)。
阈值 · confidence ≥0.7(默认)/ ≥0.9(高敏)才写入;高敏 inferred 一律丢弃。
合并 · merge_candidates
单值 latest 覆盖;偏好/排除类 set_union 并集,并写 items_meta(R1)。
customer_service.profile_maybe_extract);游客试聊不写画像。C-04 阈值槽:
threshold_pref_summary → customer_threshold_config;达线提醒仅在查持仓时 inline。
R1 · 偏好 Top-K 注入
# settings 默认:top_k=3 · ttl=90d · half_life=30d
score = confidence × decay × (1 + log(mention_count))
# 超 TTL 的偏好不进 prompt;取得分 Top-K 条
客户说过很多偏好类型,但 prompt 里只带最近、反复提到的前几条,避免画像越积越长带偏 LLM。
很久没提过的偏好(超过 90 天)不再注入,但 MySQL 里仍保留,下次提到会刷新。
改 K 或 TTL → 动环境配置(settings.py 的 profile_preference_*),别在 prompt 里硬编码。
客户口述「我是激进型 R5」,但 Core 风评是 R3,持仓查询应信谁?
模块 7 · Wave3 数据查询扩展
净值、阈值、匹配说明
关键词怎么分流?
2026-09-10 批次在持仓/流水/风评/适当性之外,又接了净值查询(C-05)、亏损阈值(C-04)和「我能买什么」(C-11)。 指挥 AI 改意图时,先看关键词路由顺序——数据查询类必须优先于「帮我记住」类 save_note。
新增 intent 一览
C-05 · nav_query
「最新净值 / 单位净值 / 基金净值」→ Core 只读 get_latest_nav(快照)。「实时净值」仍走 reject(禁止实时盘口)。
C-04 · 阈值提醒
用户说「亏 10% 提醒我」→ L1 槽 threshold_pref_summary + 写 customer_threshold_config;下次查持仓时组合加权盈亏达线 → 追加提醒 + customer_notify_log。
C-11 · 匹配说明
「我能买什么 / 匹配产品 / 有哪些我能买」→ suitability_check + Core 可购列表。「推荐稳赚」仍 reject。
C-07 · 风评重测
「重新测评 / 重做风评」扩进风评关键词 → 引导 App/网点;Agent 内不做问卷。
关键词优先级(易踩坑)
客户线各分支 System Prompt 模板(customer_prompts.py)里 keyword_route 顺序:
转人工 / reject(实时净值、推荐稳赚等)
数据查询:流水 · 风评 · 适当性 · 持仓(含「最近的流水」)
save_note(「帮我记住…」——但不含上面已命中的数据词)
匹配说明 · 净值查询
transaction_query,不能误进 save_note——见 test_wave5_notes.py。
# 画像抽槽 → sync_threshold_from_summary
# query_holdings 末尾 → build_threshold_alert
if loss_pct >= threshold:
append alert + insert_notify_log
阈值不是 push 通知——只有客户再来问持仓时才 inline 提醒。
组合盈亏按持仓市值加权算,不是单只基金。
改阈值:口述新比例 → 画像摘要更新 → config upsert。
1B 仍覆盖 nav_query
违禁词扫描后的分治(sanitize_postprocess.py)把 nav_query 算进数据查询 intent——interpret 嘴瓢时仍回退 Core fact_text。