文档:智能财富管家系统 · 智能客服 Agent 专项设计方案 版本:v1.3 日期:2026-09-07 状态:内部方案评审稿

智能客服 Agent 专项设计方案(内部评审稿)

0. 文档说明与评审指引

0.1 定位与范围

本文档是《智能财富管家系统-业务与技术全景设计说明书》中「智能客服 Agent」的深化专项设计,聚焦对外客户服务入口这一条链路(用户提问 → 意图识别 → 知识检索 → 答案生成 → 合规校验 → 人工转接 → 结果反馈),供内部方案评审使用。

范围边界:只覆盖智能客服 Agent(自然语言问答 / FAQ / 产品一般性介绍 / 政策解读 / 转人工)。投顾助手、风控监测、数据分析三个 Agent 不在本文档范围(详见全景说明书 §5.3/§5.4/§5.5)。

设计原则:所有结论都落到项目实际内容(意图、集合、阈值、表、接口),并对照主流平台真实落地做法标注来源;信息不确定处标注「需核实」,不做臆测。

0.2 依据文件(优先级从高到低)

优先级 文件 作用
1 需求文档-修改版.html 需求基线:四大 Agent、合规定位(不代客交易)、分阶段需求、评分标准
2 统一执行骨架_七步模板方法与职责边界.html 执行骨架权威:七步模板方法与基类/子类职责边界
3 功能设计文档.html(v1.5) 意图体系、推理范式、工具清单(注意:其「业务操作 Agent / NL2API」已被修改版下线)
4 智能财富管家系统-业务与技术全景设计说明书.md 全景说明书:置信度阈值、三集合路由、数据表、架构

0.3 合规基线速览(本文档反复引用)

红线 要求 依据
不代客交易 AI 不得代替客户执行申购/赎回/转账,不触及资金与下单环节 需求文档-修改版合规定位
适当性 客户 C1-C5 ↔ 产品 R1-R5 匹配,取最严口径 全景说明书 A4
负面词 7 个零容忍:保本 / 稳赚 / 无风险 / 保证收益 / 预期收益率 / 年化收益率 / 安全 全景说明书 §14
免责声明 面向客户的输出末尾必须附固定话术 全景说明书 B3
留痕 客户沟通记录保存 ≥ 20 年 《证券基金经营机构信息技术管理办法》〔S15〕

0.4 评审关注点(读前提示)

  1. §2 闭环流程中「合规校验」是否应前置到「答案生成」之前(当前为生成后置校验 + 二次打回)。
  2. §3.3「费率与收益计算」的收益口径:字段名 expected_return,对外一律表述「业绩比较基准」并附测算依据,不得出现「预期收益率」。
  3. §5 转人工阈值(意图 0.6、RAG 混合判定)是否需按业务容忍度二次校准。
  4. §6 回复话术的「负面词 7 个」是否要扩充(如「零风险」「稳赚不赔」等变体)。

1. 定位与设计目标

1.1 一句话定位

智能客服 Agent 是系统的唯一对外自然语言入口,负责把「客户想问的」映射到「合规能答的」,答不了的果断转人工——价值在于 7×24 秒回标准问题,边界在于绝不越合规红线。

1.2 服务对象与边界

服务对象 可问范围 权限边界
游客(未登录) 仅公开产品信息、一般性介绍 不涉及任何账户数据
零售客户 客服咨询、产品一般性介绍、政策解读、持仓查询、投资分析报告(带免责声明) 仅本人资产与持仓
高净值客户 同上,且可触发「AI 初筛 + 投顾人工审核」闭环 仅本人数据

1.3 业务目标与度量指标

指标 目标值 度量来源
高频意图回答准确率 ≥ 90% 会话归档 + RAG 判定日志(对标天弘智能客服公开的 94%〔S6〕)
首响时长 < 3 s 埋点
转人工率 ≤ 20% 工单统计
违规话术次数 0(负面词后置校验拦截率 100%) 后置校验日志
适当性违规 0 起 安全审计日志

1.4 合规红线(客服 Agent 视角)

客服 Agent 的职责是「信息提供」,因此它只读不写、只答不荐、只解释不下结论:

允许 不允许
产品一般性介绍、政策规则解读(适当性、反洗钱、销售办法) 承诺收益、使用「保本/稳赚/安全/预期收益率」等违规表述
持仓/收益查询(本人数据) 代客申购/赎回/转账
风险等级客观转述(R1-R5) 主观评价「这个产品风险很低」
转人工引导 对低置信问题「硬答」

行业对标:华云天下提出的金融 AI 客服「十条铁律」首条即「不能承诺收益」——任何含「左右/大约/预期」的收益数字都不允许〔S13〕。招商基金总经理级口径强调「AI 不能代替信任、责任与人文关怀」〔S7〕,与本项目「不代客交易」定位同源。

1.5 AI 接入前后的工作流程对比

本节用两张流程对比图说明:AI 智能客服解决了什么问题、在哪些环节提升了效率。量化结论见 §1.3 度量指标。

1.5.1 未接入 AI 智能客服时的工作流程

flowchart TD
    A[用户有问题] --> B[电话/邮件/线下网点求助]
    B --> C[排队等待人工客服]
    C --> D{人工客服在线?}
    D -- 否(夜间/节假日) --> E[等到工作日再处理]
    E --> C
    D -- 是 --> F[人工逐条答疑
口径依赖个人经验] F --> G{能解决?} G -- 是 --> H[解决 + 人工记录] G -- 否 --> I[转资深客服/持证投顾] I --> F H --> J[人工汇总工单/报表
易遗漏]

痛点:响应慢(排队 + 非工作时间无人值守)、口径不一致(靠个人经验)、重复劳动高、留痕靠人工易遗漏。

1.5.2 接入 AI 智能客服后的工作流程

flowchart TD
    A[用户提问] --> B[AI 7×24 秒级响应]
    B --> C{意图识别 + 置信度}
    C -- 标准问题 --> D[三集合检索 → 秒回]
    C -- 低置信/情绪/投诉 --> E[自动转人工
携带完整上下文] D --> F[合规校验
负面词 + 免责声明] F --> G[结果反馈 SSE] E --> H[人工接管
聚焦复杂问题] G --> I[自动归档 + 脱敏留痕] H --> I

1.5.3 两者对比(AI 解决了什么 / 在哪提升效率)

环节 未接入 AI 的痛点 接入 AI 后 效率提升
响应时效 排队等待、非工作时间无人 7×24 秒级响应标准问题 首响分钟级 → <3s
标准问题 人工重复回答 FAQ 直返 / RAG 秒回 高频意图准确率 ≥90%
口径一致性 各客服口径不一 统一话术 + 来源引用 违规话术 0 次
转人工 靠用户自己找入口 智能触发 + 上下文无缝传递 转人工率 ≤20%,且不复述
留痕与合规 人工整理、易遗漏 全量自动归档(脱敏) 100% 留痕 ≥20 年
人力投入 大量重复劳动 人力聚焦复杂/高价值问题 释放约 40% 重复人力(对标天弘〔S6〕)

2. 整体流程闭环

2.1 闭环流程图

flowchart TD
    A[用户提问] --> B[① 输入接收与校验鉴权]
    B --> B1{鉴权通过?}
    B1 -- 否 --> B2[返回登录引导]
    B1 -- 是 --> C[② 记忆召回
读 Redis 短期会话] C --> D[③ 意图路由
qwen-turbo 分类·阈值0.6] D --> D1{置信度>=0.6?} D1 -- 否 --> D2[澄清提问] D2 --> D3{仍无法识别?} D3 -- 是 --> H[转人工] D3 -- 否 --> D D1 -- 是 --> E[④ 知识检索
Milvus 三集合路由] E --> F[⑤ 答案生成
temperature 0.3 + 来源引用] F --> G[⑥ 合规校验
负面词后置 + 免责声明注入] G --> G1{命中负面词?} G1 -- 是 --> G2[打回重生成] G2 --> G3{二次仍命中?} G3 -- 是 --> G4[返回安全话术 + 告警] G3 -- 否 --> F G1 -- 否 --> I[⑦ 结果反馈 SSE 流式] I --> J{需转人工?} J -- 是 --> H J -- 否 --> K[数据沉淀:写回会话+归档脱敏] H --> K K --> L[事件广播:命中敏感意图 publish suspicious_intent]

2.2 七步拆解(与统一执行骨架对齐)

步 名称 输入 → 输出 归属
① 输入接收与校验鉴权 session_id/user_id/message → JWT 解析画像标签 基类
② 记忆召回 读 Redis 会话(List,TTL 30min,最长 24h,超 4096 token 截断旧消息) 基类
③ 意图路由 消息 + 上下文 → 5 类意图 + 置信度(qwen-turbo,阈值 0.6) 基类调用、子类给分类器
④ 核心逻辑 意图 → Milvus 三集合检索 → 混合判定 子类唯一实现
⑤ 结果生成 检索结果 → LLM 生成(temperature 0.3)+ 来源引用 + 负面词后置校验 + 免责声明注入 基类
⑥ 数据沉淀 写回会话、归档(脱敏)、记录 tool_calls 基类(强制)
⑦ 事件广播 命中转账/敏感意图 → publish event:suspicious_intent 基类(强制)

对齐说明:⑥⑦ 由基类强制,防止「某个 Agent 忘了归档/忘了广播」——这是合规留痕与跨 Agent 协作的结构性保障〔全景说明书 §8〕。

2.3 关键决策点:置信度与三档兜底

决策点 规则 结果
意图置信度 < 0.6 → 澄清提问;澄清后仍无法识别 → 转人工 不硬答
RAG 混合判定 绝对阈值 AND(相对间隙 OR 分布优势) 三档:高/中/低
高置信 — 直接回答
中置信 — 回答 + 「以上信息可能不完整…」提示
低置信 — 兜底话术 + 建议转人工

2.4 三集合路由代码与 Milvus 关键概念(带详细中文注释)

§2.1 的流程图里多次出现「Milvus 三集合路由」——这里给出对应工程模块(tool/milvus_tool.py,与本文档 §0.2 / §4.1 引用一致)的完整代码与逐行中文注释,方便评审时逐字段、逐分支核对。

阅读顺序:先看 2.4.1 概念与原理(建立直觉)→ 2.4.2 三级路由职责速查 → 2.4.3 Milvus 概念速查(扫盲)→ 2.4.4 完整代码(带注释的实现)→ 2.4.6 操作类查询规划。

2.4.1 三集合路由的概念与原理(先看懂,再看代码)

一句话定义:三集合路由 = 把知识库分成三个「书架」(FAQ / 产品 / 政策),用户每问一个问题,系统先判断「该去哪个/哪几个书架找」,再去对应书架里用「语义相似度」找出最相关的资料。

用一个图书馆的类比理解:

概念 图书馆类比 本项目
collection(集合) 一个书架 fin_faq / fin_product / fin_policy
路由(routing) 图书管理员判断「去哪个书架找」 _step1_route_intent
向量检索 按「内容像不像」找书,而非按书名精确匹配 _step2_search_in_collection
重排 从几个书架找到的书里挑最好的几本 _step3_rerank_and_merge

为什么需要「路由」而不是一个大集合:如果把 FAQ、产品、政策全塞进一个大集合,问「赎回费怎么算」时,可能把「反洗钱政策」也检索出来,干扰答案。分成三个集合 + 路由,等于先缩小查找范围,再精确定位——既快又准。

原理(三步走):

flowchart LR
    A[用户问题] --> B[意图识别
这是哪类问题?] B --> C{路由决策
去哪个集合} C -- 高频问答 --> D1[fin_faq_collection] C -- 产品咨询 --> D2[fin_product_collection] C -- 政策解读 --> D3[fin_policy_collection] D1 & D2 & D3 --> E[集合内向量检索
找最相关的 topK 条] E --> F[重排去重
输出最终素材]

注意:这里的「路由」不是网络路由器(转发数据包),而是「按问题类型分发检索目标」的业务路由——两者同名,含义完全不同。

2.4.2 三级路由职责速查表

级别 名称 干啥的 代码位置
第一级 意图路由 决定「去哪个/哪几个集合」找资料 ThreeCollectionRouter._step1_route_intent
第二级 集合内检索 在选定的集合里做「向量相似度」搜索,取 topK 条 ThreeCollectionRouter._step2_search_in_collection
第三级 重排与融合 把多个集合的候选合并、去重、排序,输出最终素材 ThreeCollectionRouter._step3_rerank_and_merge

「三集合路由」= 在三个 Milvus collection(fin_faq / fin_product / fin_policy)之间,按用户意图选集合,再在集合内做向量检索。写入路径另有 upsert_document(往集合里塞新数据/更新旧数据)。

2.4.3 Milvus 关键概念速查(给不熟悉 Milvus 的同学)

概念 一句话解释 在本项目对应
collection 集合 Milvus 的「表」,存一类数据 三个:fin_faq / fin_product / fin_policy
field 字段 表里的列 id、content、embedding、effective_date 等
primary key 主键 唯一标识一条记录的字段 每个集合的 id 字段(VARCHAR,max=64)
vector field 向量字段 专门存 embedding(文本的数学表示)的字段,类型 FLOAT_VECTOR 每个集合的 embedding 字段,维度 VECTOR_DIM
scalar field 标量字段 非向量的普通字段(VARCHAR/INT 等),可用于精确过滤 tags、product_code、effective_date、expire_date
partition 分区 集合内部的「子文件夹」,可按维度划分,搜索时指定分区可大幅缩范围 产品集合按 product_type 分区(股票/混合/债券)
segment 段 Milvus 内部把数据切成的小块(存盘+加载的单位),对使用者透明 Milvus 自动管理
index 索引 为向量字段建的「加速查找结构」,常见 IVF_FLAT、HNSW、DISKANN 本项目用 IVF_FLAT(nlist=128)
ANN Approximate Nearest Neighbor 近似最近邻——速度极快、不要求 100% 精确 搜索的本质目标
topK 返回相似度最高的 K 条记录 FAQ 取 3、其余取 5
metric_type 距离度量 向量之间「像不像」的衡量方式 COSINE 余弦相似度(最常用,忽略长度只看方向)
hybrid search 混合检索 向量检索 + 标量过滤(如过期过滤) 用 expr 参数实现
load() 把集合从磁盘加载到内存(不加载不能搜,Milvus 是「磁盘省钱、内存才快」) 初始化时调用一次
upsert 插入或更新(Milvus 用 upsert,不用 insert+update) 写入路径统一用 upsert
flush 把内存里的数据真正落盘+建索引(较重,高频写可批量) 写入后调用

2.4.4 完整代码(带详细中文注释)

下面的代码可直接保存为 tool/milvus_tool.py。每一行 # 后的中文注释都对不熟悉 Milvus / RAG 的读者做了通俗解释。

# -*- coding: utf-8 -*-
"""
milvus_tool.py
==============
智能客服 Agent 的「三集合路由」核心模块。

【三集合路由的定义和作用】
    智能客服 Agent 回答用户问题时,需要先在「知识库」里找到相关资料。
    我们的知识库分成了三个独立的 Milvus 集合(你可以理解为三张表),
    分别存放不同类型的资料:
        1. fin_faq_collection     —— 高频操作类问答(如「怎么重置密码」)
        2. fin_product_collection —— 产品说明书、费率、风险等级
        3. fin_policy_collection  —— 监管法规、适当性规则、销售办法
    为什么要分三个?
        - 不同类型资料的「更新频率」不同(FAQ 周更,政策即时更新)
        - 不同意图需要「检索不同的内容」(问政策就不该去产品表里找)
        - 权限/审计可以按集合隔离

【三级路由的协作流程】
    用户提问 → 意图分类器判定意图 →
        第一级(意图路由)→ 决定去哪些集合 →
        第二级(集合内检索)→ 每个集合内做向量相似度搜索 →
        第三级(重排与融合)→ 合并多集合候选 → 输出最终 topK
    写入路径(更新/新增知识):upsert_document() → 写入对应 collection
"""
from __future__ import annotations  # 让类型注解在 Python 3.9 之前也能用
import time                        # 用 time.strftime 取当前日期
import hashlib                     # 用 md5 做内容指纹(去重用)
from typing import List, Dict, Any, Optional  # 类型注解,让代码可读性更强
from dataclasses import dataclass  # 用装饰器快速定义「数据类」(只装数据的简单类)

# 下面这行是 pymilvus 的官方 SDK(Milvus 团队维护的 Python 客户端)。
# 如果项目里还没装,请先:pip install pymilvus
from pymilvus import (
    connections,         # 用来连 Milvus 服务(建立网络连接)
    Collection,          # 集合对象 = 指向 Milvus 某张「表」的引用
    CollectionSchema,    # 集合的「表结构」= 字段列表
    FieldSchema,         # 单个字段的「列定义」
    DataType,            # 字段类型枚举:VARCHAR / FLOAT_VECTOR / INT64 ...
    utility,             # 工具函数:判断 collection 是否存在、查版本等
)


# ============================================================
# 一、常量区:把会变的配置集中起来(单一真相源)
# ============================================================
# 「单一真相源」= 全项目只有这一处定义这个值,避免散落各处导致改一处忘改另一处。
# 比如你想把端口换了,只改这里就行。

# Milvus 服务器地址。本地开发用 standalone,集群用 cluster 地址。
MILVUS_HOST = "127.0.0.1"
MILVUS_PORT = "19530"

# 三个集合的名字(= 三张「表」)。改这里就够了,不要在代码里到处写字符串字面量。
FAQ_COLLECTION      = "fin_faq_collection"
PRODUCT_COLLECTION   = "fin_product_collection"
POLICY_COLLECTION    = "fin_policy_collection"
ALL_COLLECTION_NAMES = [FAQ_COLLECTION, PRODUCT_COLLECTION, POLICY_COLLECTION]

# 意图 → 集合 的映射关系(= 「第一级路由」的配置表)
# 含义:用户问的是这个意图时,去这几个集合里找资料。
# 用列表而不是单个字符串,是允许「一个意图跨多个集合」的灵活设计。
INTENT_TO_COLLECTIONS: Dict[str, List[str]] = {
    "faq":              [FAQ_COLLECTION],                                       # 高频问题只在 FAQ 表
    "product_inquiry":  [PRODUCT_COLLECTION],                                   # 产品咨询去产品表
    "policy_explain":   [POLICY_COLLECTION],                                    # 政策解读去政策表
    "chitchat":         [],                                                     # 闲聊不去任何表(走 LLM 自由生成)
    "transfer_human":   [],                                                     # 转人工也不查表
    # 兜底:意图分类没把握时,三个集合都查,取并集后再重排
    "low_confidence":   [FAQ_COLLECTION, PRODUCT_COLLECTION, POLICY_COLLECTION],
}

# 每个集合检索时取 topK 条候选。TopK 越大召回越全但越慢。
COLLECTION_TOPK: Dict[str, int] = {
    FAQ_COLLECTION:     3,   # FAQ 是「一问一答」标准答案,取 3 条够用
    PRODUCT_COLLECTION: 5,   # 产品信息需要更多候选让 LLM 选
    POLICY_COLLECTION:  5,   # 政策条款同理
}

# 向量维度。要和 embedding 模型输出一致(如 Qwen Embedding 通常 1024 或 1536 维)。
# 「维度」= 一个向量有多少个数字。文本越长不一定维度越高,是模型决定的。
VECTOR_DIM = 1024

# 向量距离度量。COSINE = 余弦相似度(最常用,忽略向量长度只看方向)。
# 选 COSINE 而不是 L2(欧氏距离),因为我们关心「语义方向」而不是「距离绝对值」。
METRIC_TYPE = "COSINE"


# ============================================================
# 二、三个集合的「表结构」定义(schema)
# ============================================================
# Milvus 的 schema 就像建表语句:定义有哪些字段、各自什么类型。
# 用工厂函数(返回对象的函数)生成,方便统一管理 + 后续扩展。

def build_faq_schema() -> CollectionSchema:
    """FAQ 集合的表结构:高频问答。"""
    fields = [
        # 主键:用 md5(question) 做主键,保证同一问题只有一条记录
        FieldSchema("id",            DataType.VARCHAR, max_length=64, is_primary=True),
        FieldSchema("question",      DataType.VARCHAR, max_length=512),    # 标准问法
        FieldSchema("answer",        DataType.VARCHAR, max_length=2048),  # 标准答案
        FieldSchema("tags",          DataType.VARCHAR, max_length=256),   # 标签,JSON 字符串
        FieldSchema("effective_date",DataType.VARCHAR, max_length=32),    # 生效日期 "2026-01-01"
        FieldSchema("expire_date",   DataType.VARCHAR, max_length=32),    # 失效日期,空=长期有效
        # 向量字段:把 question 文本转成 embedding 存进来
        FieldSchema("embedding",     DataType.FLOAT_VECTOR, dim=VECTOR_DIM),
    ]
    # description 给这个集合写个说明,方便在 Milvus 管理界面(Attu)里辨认
    return CollectionSchema(fields, description="FAQ 集合:高频操作类问答")


def build_product_schema() -> CollectionSchema:
    """产品集合的表结构:产品说明书/费率/风险等级。"""
    fields = [
        FieldSchema("id",             DataType.VARCHAR, max_length=64, is_primary=True),
        FieldSchema("product_code",   DataType.VARCHAR, max_length=32),   # 产品代码,如 "000001"
        FieldSchema("product_name",   DataType.VARCHAR, max_length=128),
        FieldSchema("product_type",   DataType.VARCHAR, max_length=32),   # 股票/混合/债券
        FieldSchema("risk_level",     DataType.VARCHAR, max_length=8),    # R1-R5
        FieldSchema("content",        DataType.VARCHAR, max_length=4096), # 文档片段原文
        FieldSchema("source_url",     DataType.VARCHAR, max_length=512),
        FieldSchema("effective_date", DataType.VARCHAR, max_length=32),
        FieldSchema("embedding",      DataType.FLOAT_VECTOR, dim=VECTOR_DIM),
    ]
    return CollectionSchema(fields, description="产品集合:产品说明书/费率/风险")


def build_policy_schema() -> CollectionSchema:
    """政策集合的表结构:监管法规/适当性/销售办法。"""
    fields = [
        FieldSchema("id",             DataType.VARCHAR, max_length=64, is_primary=True),
        FieldSchema("doc_id",         DataType.VARCHAR, max_length=64),   # 法规文号,如 "银发2016261号"
        FieldSchema("title",          DataType.VARCHAR, max_length=256),
        FieldSchema("content",        DataType.VARCHAR, max_length=4096),
        FieldSchema("issuing_org",    DataType.VARCHAR, max_length=64),   # 发文机构:银保监会 / 证监会 ...
        FieldSchema("effective_date", DataType.VARCHAR, max_length=32),
        FieldSchema("embedding",      DataType.FLOAT_VECTOR, dim=VECTOR_DIM),
    ]
    return CollectionSchema(fields, description="政策集合:监管法规/适当性/销售办法")


# schema 字典:集合名 → schema 工厂函数
SCHEMA_FACTORIES: Dict[str, Any] = {
    FAQ_COLLECTION:     build_faq_schema,
    PRODUCT_COLLECTION: build_product_schema,
    POLICY_COLLECTION:  build_policy_schema,
}


# ============================================================
# 三、检索结果的数据类(让返回值有结构,方便后续处理)
# ============================================================
# @dataclass 装饰器自动生成 __init__ / __repr__ / __eq__ 等方法,代码更短。

@dataclass
class Hit:
    """单条检索命中结果。相当于「一行记录」+「相似度分」。"""
    collection: str              # 来自哪个集合
    doc_id:     str              # 该集合内的主键
    score:      float            # 相似度分(越大越像;范围约 0~1)
    content:    str              # 文本内容(问题/答案/产品/政策原文)
    metadata:   Dict[str, Any]   # 其他元数据(tags、product_code、raw_distance 等)


@dataclass
class RoutePlan:
    """第一级路由的输出:决定去哪些集合、每个取多少条。"""
    target_collections:  List[str]            # 要去查的集合名列表
    per_collection_topk: Dict[str, int]       # 每个集合分别取 topK


# ============================================================
# 四、三集合路由器(核心类)
# ============================================================

class ThreeCollectionRouter:
    """
    三集合路由器的核心类。
    一个实例 = 一个 Milvus 客户端连接。
    设计要点:复用连接、缓存 collection 句柄,避免每次检索都重新建连。
    """

    def __init__(self, host: str = MILVUS_HOST, port: str = MILVUS_PORT):
        # 1. 连 Milvus 服务
        self._connect(host, port)
        # 2. 懒加载:集合句柄按需获取(用 self.get_collection(name) 时才建),
        #    避免启动时全部加载占内存。
        # 「懒加载」= 用到时再加载,不用时不加载。
        self._collection_cache: Dict[str, Collection] = {}

    # ---------- 连接与初始化 ----------

    def _connect(self, host: str, port: str) -> None:
        """连 Milvus 服务。alias='default' 是默认连接名,后面 Collection 用默认连接。"""
        connections.connect(alias="default", host=host, port=port)
        # 用一个轻量调用确认连接成功(get_server_version() 返回类似 "v2.4.x")
        if not utility.get_server_version():
            # 抛异常而不是静默失败,让上层知道出问题了
            raise RuntimeError(f"无法连接 Milvus 服务 {host}:{port}")

    def get_collection(self, name: str) -> Collection:
        """
        获取一个集合的句柄(带缓存)。
        Collection 对象 = 指向 Milvus 某张「表」的引用,搜索/写入都要用它。
        缓存的好处:第二次拿同一集合不用重新查 Milvus,省一次 RPC。
        """
        # 第一步:先看缓存里有没有
        if name not in self._collection_cache:
            # 第二步:缓存里没有,检查 Milvus 里有没有这个集合
            if not utility.has_collection(name):
                # 都没有 → 首次启动,自动建表
                self._create_collection(name)
            # 第三步:拿句柄
            coll = Collection(name)
            # 第四步:load() 把集合从磁盘加载到内存,否则搜索会报「collection not loaded」
            # Milvus 是「磁盘省钱、内存才快」的设计,不 load 就搜不了
            coll.load()
            # 放进缓存,下次直接拿
            self._collection_cache[name] = coll
        return self._collection_cache[name]

    def _create_collection(self, name: str) -> None:
        """首次启动时创建集合(建表 + 建索引)。"""
        # 用 schema 工厂拿表结构定义
        schema = SCHEMA_FACTORIES[name]()
        # 创建集合:参数 = 名字 + 表结构
        Collection(name, schema=schema)
        # 为向量字段建索引。
        # IVF_FLAT:常用索引,简单稳;HNSW 更精确但费内存;DISKANN 大数据量省钱。
        coll = Collection(name)
        coll.create_index(
            field_name="embedding",     # 给哪个字段建索引
            index_params={
                "metric_type": METRIC_TYPE,    # 用余弦相似度(前面常量区定义的)
                "index_type":  "IVF_FLAT",
                # nlist:聚类中心数。越大越精确但越慢;128 是常用起点
                "params":      {"nlist": 128},
            },
        )

    # ---------- 写入路径:往集合里塞一条新数据 / 更新旧数据 ----------

    def upsert_document(
        self,
        collection_name: str,
        doc_id: str,
        text: str,
        embedding: List[float],
        extra_fields: Optional[Dict[str, Any]] = None,
    ) -> None:
        """
        写入或更新一条文档。
        调用时机:运营上传新知识、费率变更、政策更新时。
        参数:
            collection_name: 写入哪个集合(faq / product / policy)
            doc_id:          主键(自己生成,md5 或雪花 ID 都行)
            text:            文本内容
            embedding:       text 转成的向量(调用 embedding 模型得到)
            extra_fields:    其他字段(product_code、effective_date 等)
        """
        coll = self.get_collection(collection_name)
        # 拼装一行数据。Milvus 要求:字段顺序和 schema 一致,且值都是列表(支持批量)。
        row = {
            "id":        [doc_id],
            "content":   [text],   # 简化:假设 schema 都有 content 字段
            "embedding": [embedding],
        }
        # 把额外字段补进去(如果 schema 里有)
        if extra_fields:
            for k, v in extra_fields.items():
                row[k] = [v]
        # upsert:存在则更新、不存在则插入(Milvus 用 upsert 统一两种操作)
        coll.upsert(row)
        # 写完刷一下:让数据立即可见、开始建索引。
        # flush 较重(要写盘),高频写入场景可以用 flush_buffer 批量刷
        coll.flush()

    # ---------- 读取路径:三集合路由的核心(对外入口) ----------

    def query(
        self,
        intent: str,
        query_text: str,
        query_embedding: List[float],
        user_id: Optional[int] = None,
        partition_name: Optional[str] = None,
    ) -> List[Hit]:
        """
        三级路由的对外入口——客服 Agent 每次问问题,都会调这个函数。
        参数:
            intent:         意图分类的结果("faq" / "product_inquiry" / "policy_explain" / "low_confidence")
            query_text:     用户的原始问题(中文)
            query_embedding: query_text 经 embedding 模型转成的向量(长度 = VECTOR_DIM)
            user_id:        当前用户 ID(用于权限过滤,例如只让看本人产品)
            partition_name: 可选——只查某个分区的数据(产品集合可按 product_type 分区)
        返回:经过三级路由后的候选结果列表(已重排)
        """
        # ============ 第一级:意图路由 → 决定去哪些集合 ============
        route_plan = self._step1_route_intent(intent, query_text)
        # 如果第一级判定不需要查表(闲聊/转人工),直接返回空,让上层走别的逻辑
        if not route_plan.target_collections:
            return []

        # ============ 第二级:集合内检索(多集合可并行)============
        # 多个集合之间没有依赖关系,可以并行查。这里用 for 串行演示;真实环境用线程池/异步并行。
        per_collection_hits: Dict[str, List[Hit]] = {}
        for coll_name in route_plan.target_collections:
            # 每个集合的 topK 从 route_plan 里取
            topk = route_plan.per_collection_topk.get(coll_name, 3)
            hits = self._step2_search_in_collection(
                coll_name=coll_name,
                query_embedding=query_embedding,
                top_k=topk,
                user_id=user_id,
                partition_name=partition_name,
            )
            per_collection_hits[coll_name] = hits

        # ============ 第三级:重排与融合 → 输出最终 topK ============
        final_hits = self._step3_rerank_and_merge(
            per_collection_hits=per_collection_hits,
            query_text=query_text,
        )
        return final_hits

    # ---------- 第一级:意图路由 ----------

    def _step1_route_intent(self, intent: str, query_text: str) -> RoutePlan:
        """
        第一级路由:决定去哪些集合、每个集合取多少条。
        决策逻辑:
            1. 查 INTENT_TO_COLLECTIONS 配置,看该意图默认对应哪些集合
            2. 如果意图不在配置里 → 走兜底:三个集合都查
        """
        # 默认配置里的目标集合列表
        targets = INTENT_TO_COLLECTIONS.get(intent)
        # 没找到意图配置 → 兜底:全集合都查
        if targets is None:
            # list() 是为了拷贝一份,避免后面修改时改了常量
            targets = list(ALL_COLLECTION_NAMES)

        # 构造每个集合的 topK 配置
        per_topk: Dict[str, int] = {}
        for name in targets:
            # 用 COLLECTION_TOPK 配置表查每个集合的 topK,找不到默认 3
            per_topk[name] = COLLECTION_TOPK.get(name, 3)
        return RoutePlan(target_collections=targets, per_collection_topk=per_topk)

    # ---------- 第二级:集合内检索 ----------

    def _step2_search_in_collection(
        self,
        coll_name: str,
        query_embedding: List[float],
        top_k: int,
        user_id: Optional[int],
        partition_name: Optional[str] = None,
    ) -> List[Hit]:
        """
        第二级路由:在某一个集合内做「向量相似度检索」。
        流程:
            1. 构造过滤表达式(标量过滤,比如「未过期」+「本人可见」)
            2. 用 embedding 向量在该集合内搜 topK(向量化最近邻)
            3. 把 Milvus 返回的原始结果转成 Hit 对象
        """
        coll = self.get_collection(coll_name)

        # 构造标量过滤条件(标量 = 非向量字段)。
        # 语法类似 SQL 的 WHERE 条件。含义:expire_date 为空 OR expire_date > 今天
        today = time.strftime("%Y-%m-%d")
        expr = f'expire_date == "" OR expire_date > "{today}"'

        # 产品集合再加权限过滤(简化示意:实际应该是 OR 公开产品 OR user_id 匹配)
        if coll_name == PRODUCT_COLLECTION and user_id is not None:
            expr += f' OR user_id == "{user_id}"'

        # 向量搜索参数:
        #   nprobe:搜索时探查的聚类数,越大越精确但越慢
        search_params = {"metric_type": METRIC_TYPE, "params": {"nprobe": 16}}

        # 准备 partition_names 参数(如果指定了 partition)
        # 搜索时指定 partition 可以大幅缩范围、提速
        search_kwargs: Dict[str, Any] = {
            "data": [query_embedding],      # 查询向量,外层是列表因为支持一次查多个
            "anns_field": "embedding",      # 用哪个向量字段做相似度
            "param": search_params,         # 搜索参数
            "limit": top_k,                 # topK
            "expr": expr,                   # 标量过滤条件
            "output_fields": ["id", "content"],  # 这些标量字段也一起返回
        }
        if partition_name:
            # 如果传了分区名,只在该分区内搜(产品集合可按 product_type 分区)
            search_kwargs["partition_names"] = [partition_name]

        # 执行搜索(anns_field = Approximate Nearest Neighbor Search field)
        results = coll.search(**search_kwargs)

        # 解析结果。results 是 [[Hit1, Hit2, ...]],外层列表是因为可以一次查多个 query
        hits: List[Hit] = []
        for query_hits in results:
            for h in query_hits:
                # h.distance 是距离(越小越像)。
                # 余弦距离 = 1 - 余弦相似度,所以 score = 1 - distance
                # 我们把 score 统一成「越大越像」,方便后续按相似度排序
                score = 1.0 - h.distance
                hits.append(Hit(
                    collection=coll_name,
                    doc_id=h.id,                                  # 命中记录的主键
                    score=score,                                  # 相似度分(0~1)
                    content=h.entity.get("content", ""),         # 命中的文本内容
                    metadata={"raw_distance": h.distance},        # 保留原始距离方便排查
                ))
        return hits

    # ---------- 第三级:重排与融合 ----------

    def _step3_rerank_and_merge(
        self,
        per_collection_hits: Dict[str, List[Hit]],
        query_text: str,
    ) -> List[Hit]:
        """
        第三级路由:把多个集合的结果合并、重排、去重。
        策略:
            1. 把所有集合的 hits 合并到一个列表
            2. 按 score 降序排(相似度高的排前面)
            3. 简单去重:内容前 100 字相同的视为重复
            4. 截断到最终 topK(这里固定 5,可改为参数)
        进阶:可以接 rerank 模型(如 bge-reranker)做精排,这里简化。
        """
        # 1. 合并:把所有集合的 hits 展平成一个列表
        all_hits: List[Hit] = []
        for hits in per_collection_hits.values():
            all_hits.extend(hits)

        # 2. 按相似度分数降序排(lambda h: h.score 意思是「取 h 的 score 作为排序键」)
        all_hits.sort(key=lambda h: h.score, reverse=True)

        # 3. 去重(用内容前 100 字符的 md5 做指纹)
        # 为什么要去重?因为同一个答案可能在 FAQ 和产品集合里都出现(比如关于费率的说明)
        seen: set = set()
        deduped: List[Hit] = []
        for h in all_hits:
            # md5(内容前100字符) → 32 位字符串指纹
            fingerprint = hashlib.md5(h.content[:100].encode("utf-8")).hexdigest()
            if fingerprint in seen:
                continue  # 见过这个指纹了,跳过
            seen.add(fingerprint)
            deduped.append(h)

        # 4. 截断到最终 topK(这里固定 5,可改为参数或按业务调整)
        return deduped[:5]


# ============================================================
# 五、demo:怎么用
# ============================================================
# 下面是「怎么调用路由器」的最小示例。
# 真实 Agent 里:query_text 先送意图分类器(拿到 intent),再送 embedding 模型(拿到 vector),
# 然后调 router.query(...) 拿到候选 hits。

if __name__ == "__main__":
    # 1. 初始化路由器(自动连 Milvus、自动建表、自动 load)
    router = ThreeCollectionRouter()

    # 2. 模拟一次用户提问
    intent = "policy_explain"                       # 假设意图分类器说这是「政策解读」
    query_text = "什么叫冷静期?"                    # 用户原话
    # 真实场景:query_embedding = embedding_model.encode(query_text)
    # 这里用全 0 占位(真实项目必须用真实 embedding,否则相似度全为 0)
    query_embedding = [0.0] * VECTOR_DIM

    # 3. 三级路由检索
    hits = router.query(
        intent=intent,
        query_text=query_text,
        query_embedding=query_embedding,
        user_id=10001,                              # 当前用户 ID(用于权限过滤)
        # partition_name="股票型",                  # 可选:只查「股票型」分区
    )

    # 4. 打印结果
    for i, h in enumerate(hits, 1):
        print(f"[{i}] 集合={h.collection} 分数={h.score:.3f}")
        print(f"    内容: {h.content[:80]}...")
        print()

2.4.5 三级路由协作时序(一句话总结)

用户提问 ──► 意图分类 ──► 第一级(去哪个集合)
                          ──► 第二级(在集合里找 topK)
                              ──► 第三级(合并去重排序)
                                  ──► 交给 LLM 生成最终回复

写入路径(与读取路径独立):upsert_document() 在运营/系统需要更新知识时调用,往指定 collection 塞一条或更新一条;不参与用户问问题的实时链路。

2.4.6 操作类查询的现状与实施规划(客户端未完成,暂缓)

本节回应一个常见疑问:为什么现在只有三个「知识类」集合,没有「操作类」(账户/持仓/交易查询)的向量库?

关键澄清:知识类 vs 操作类

类型 问的是 数据来源 是否依赖客户端 现在能做吗
知识类(当前三集合) 「赎回费怎么算」「适当性规则是什么」 文档(说明书/法规/FAQ) 否(文档已有) ✅ 现在就能做
操作类(账户/持仓/交易查询) 「我的持仓有哪些」「这笔交易到账了吗」 客户真实账户数据(账户系统) 是(需客户端 + 账户系统产生数据) ❌ 暂缓

为什么操作类暂缓:操作类查询需要「客户真实账户数据」,而这些数据由客户端(前端)+ 账户系统在真实业务运行中产生。客户端尚未开发完成,账户数据还不存在,因此现在无法实现操作类查询。

重要修正——操作类本质不是「向量库」:账户/持仓/交易查询是「精确查询」(查"我"的具体持仓、具体流水),应当通过 API 直连账户系统实现,而不是用向量库做「语义模糊检索」。向量库只服务于「答案散落在文档里的知识问答」。因此:

分阶段规划:

阶段 前置条件 实施内容
Phase 1(当前) 仅需文档(已具备) 知识类三集合路由(FAQ/产品/政策)+ RAG 问答
Phase 2(客户端完成后) 客户端 + 账户系统 + 真实账户数据 接入操作类查询:账户/持仓/交易记录 API,与知识类问答打通(如「我的 XX 基金盈亏」→ API 查数)
Phase 3(可选,视需求) 操作类 API 稳定后 若出现「操作指引类」高频问题(如"如何修改定投"),可沉淀进 FAQ 知识类集合,不必新建操作类向量库

对 §3.1 账户与交易查询的影响:§3.1 将该场景列入 6 条业务路径,但当前其「可答范围」需标注为「Phase 2 待实现」——现阶段用户问账户问题,客服应引导转人工或说明「该功能即将上线」。


3. 细分流程(按意图类型)

3.0 意图体系总览(5 类)

意图 集合 TopK 处理方式
product_inquiry 产品信息咨询 fin_product_collection 5 RAG + LLM 生成 + 来源引用
policy_explain 政策解读 fin_policy_collection 5 RAG + 解释
faq 常见问题 fin_faq_collection 3 检索直返标准答案
chitchat 闲聊 — — LLM 引导业务话题
transfer_human 转人工 — — 转接消息 + 生成转接工单

本方案将 5 类「技术意图」按业务场景细分为 6 条处理路径(§3.1-§3.6),便于评审时逐条核对;faq/product_inquiry/policy_explain 是检索主路径,chitchat/transfer_human 是控制路径。

3.1 账户与交易查询(持仓 / 收益 / 交易记录)

维度 内容
判定条件 命中「持仓、收益、交易记录、份额、到账」等实体 + 本人数据查询意图
可答范围 本人持仓明细、收益概览、在途交易、历史交易记录(只读)
禁答范围 他人账户数据;未完成身份验证时的账户信息
处理路径 鉴权 → JWT 画像标签 → 只读查询 → 脱敏输出(mask_tool)→ 回复
转人工条件 涉及账户异常、冻结、司法冻结、到账失败、无法自助解释的账务问题
合规要点 完整敏感字段(卡号/身份证号)不回显,仅显示掩码;查询前必须完成身份验证〔S16〕

⚠️ 实施状态:本场景属「操作类」查询,依赖客户端 + 账户系统产生真实账户数据,当前为 Phase 2 待实现(客户端尚未完成)。现阶段用户问账户问题,客服应引导转人工或说明「该功能即将上线」。详见 §2.4.6。

参考做法:天天基金智能客服对「什么时候能看到基金份额」这类账务问题,直接给出「T+1 工作日确认,[我的]→持仓查看,未确认的在[在途交易]查看」的标准流程答案〔S3〕。富途帮助中心以「我的 > 我的客服 > 全部问题搜索 → 仍未解决再转电话/在线」的三步式自助+兜底〔S8〕。

3.2 产品信息咨询(一般性介绍)

维度 内容
判定条件 命中基金名称/代码 + 「是什么、怎么样、投资方向、基金经理、规模」等介绍类问法
可答范围 产品要素客观转述(类型、投资范围、风险等级 R1-R5、费率结构、开放状态)
禁答范围 「这只基金适不适合我」「值不值得买」等个性化推荐(应转投顾流程,客服不越界)
处理路径 意图 product_inquiry → 检索 fin_product_collection → 生成 + 来源引用
转人工条件 用户追问「该买哪个」「给我推荐」→ 引导至风险测评 + 转投顾/人工
合规要点 风险等级只做客观转述,不做「风险很低」主观评价〔S13 铁律三〕

参考做法:招商基金智能客服对「基金下跌怎么办」分析净值下跌 4 个原因(市场变化、分红、折算、转型)并提示异常可查公告〔S6〕——只解释现象、不给结论,与本项目「只答不荐」一致。华夏基金「机智小牛智能体」主打「会倾听、能共情、懂陪伴」,同样止步于陪伴与解答〔S7〕。

3.3 费率与收益计算

维度 内容
判定条件 命中「费率、手续费、申购费、赎回费、管理费、收益、赚多少、年化」等计算类问法
可答范围 费率结构与计算规则(申购/赎回/管理费率、前端/后端收费)、业绩比较基准及测算依据
禁答范围 任何收益数字承诺(含「左右/大约/预期」);「预期收益率/年化收益率」表述
处理路径 费率计算器(三维计算模型)→ 客观输出费率;收益类 → 检索产品文档 + 附「业绩比较基准 + 过往业绩不代表未来表现」
转人工条件 用户要求测算「到期能拿回多少钱」等个性化金额 → 转投顾(需画像与风评)
合规要点 字段 expected_return 对外一律称「业绩比较基准」;提及收益必附风险提示〔S16〕

参考做法:百度智能云证券基金方案内置「申购费/赎回费/管理费三维费率计算器,支持前端/后端收费切换」〔S15〕。华云天下铁律一给出的标准应答为「根据产品说明书,该产品的业绩比较基准为 XX%。过往业绩不代表未来表现,投资有风险,请您仔细阅读产品说明书」〔S13〕——本项目固定免责话术与之同构。

3.4 赎回与到账时效

维度 内容
判定条件 命中「赎回、卖出、到账、多久到账、T+N」等时效类问法
可答范围 分类型到账时效规则(货币/债券/混合/股票/QDII)、T+N 确认规则
禁答范围 承诺「最快几秒/当天必到」等确定性时效(实际以产品为准)
处理路径 意图 faq → 检索 fin_faq_collection → 直返标准答案 + 「具体以产品为准」
转人工条件 用户到账异常、超时未到账、金额不符 → 转人工核查
合规要点 时效表述必须带「具体以产品为准」,不做绝对承诺

参考做法:天天基金「基金吧答疑」对「基金赎回多久到账」给出分类型标准答案:货币 1-2 工作日、债券 2-4、混合/股票 2-4、QDII 4-13 工作日(具体以产品为准)〔S3〕。这正是本项目 fin_faq_collection 标准答案的组织范式——高频规则类问题固化为标准话术直返。

3.5 投诉与情绪安抚

维度 内容
判定条件 命中「投诉、差评、举报、生气、不满意」等情绪/投诉词,或情绪识别触发
可答范围 情绪共情 + 投诉渠道引导 + 受理承诺
禁答范围 对纠纷事实自行定性、承诺赔偿/退款(必须人工确认)
处理路径 情绪识别 → 共情话术 → 生成投诉工单 → 自动转人工
转人工条件 情绪激烈、涉及投诉/纠纷/资金争议 → 无条件转人工
合规要点 涉及退款/赔偿/合同变更,AI 只解释规则、收集材料、生成工单,最终确认由人工或规范化流程完成〔S11〕

参考做法:捷通华声明确「涉及投诉/纠纷关键词」纳入转人工触发条件,「宁可早转,不可硬撑」〔S14〕。国标《顾客联络服务 人工与智能客户服务协同要求》明确「感知到客户负面情绪…系统应自动转接人工」〔S11〕。

3.6 风险揭示与合规禁答

维度 内容
判定条件 命中「保本、稳赚、零风险、绝对安全、保证收益、预期收益率」等违规词,或监管敏感问题
可答范围 风险揭示话术(非保本浮动收益、可能损失本金)、引导阅读产品说明书
禁答范围 任何合规禁答内容(见 §6.5 负面词清单)
处理路径 负面词后置校验命中 → 打回重生成;二次仍命中 → 返回安全话术 + 告警
转人工条件 监管敏感问题(如「有没有内部消息」「能不能代持」)→ 转合规/法务
合规要点 命中负面词零容忍;告警留痕备查

参考做法:华云天下铁律四标准应答「理财产品是非保本浮动收益产品,您的本金和收益可能会因市场波动而产生损失」〔S13〕。蚂蚁财富《智能理财助理服务协议》明确「输出可能出现错误或遗漏…不构成投资建议/推介/要约」〔S1〕——本项目的免责声明与「AI 生成内容风险提示」双保险与此对齐。

3.7 各意图数据流向图(汇总)

下图把 §3.1-§3.6 的六类业务场景,映射回 §3.0 的五类技术意图,展示不同意图下数据从「提问」到「结果」的完整流转路径。账户/持仓/交易查询(操作类)当前为 Phase 2 待实现(见 §2.4.6),图中以虚线标出。

flowchart TD
    U[用户提问] --> A[① 输入接收与校验鉴权]
    A --> B[② 记忆召回:读 Redis 会话]
    B --> INT{③ 意图路由
qwen-turbo · 阈值0.6} INT -- faq 高频问答 --> F1[检索 fin_faq_collection · TopK=3] F1 --> F2[直返标准答案] INT -- product_inquiry 产品咨询 --> P1[检索 fin_product_collection · TopK=5] P1 --> P2[RAG + LLM 生成 + 来源引用] INT -- policy_explain 政策解读 --> PL1[检索 fin_policy_collection · TopK=5] PL1 --> PL2[RAG + 解释] INT -- chitchat 闲聊 --> C1[不查集合
LLM 引导业务话题] INT -- transfer_human 转人工 --> T1[生成转接工单
携带完整上下文] T1 --> T2[运营/投顾后台接管] INT -- 置信度<0.6 --> L1[澄清提问 → 仍低则转人工] O1[账户/持仓/交易查询
Phase 2 · API 直连账户系统] -.-> G F2 --> G[⑤ 合规校验
负面词 + 免责声明] P2 --> G PL2 --> G C1 --> G T2 --> G L1 --> G G --> H[⑥ 数据沉淀:归档 + 脱敏] H --> I[⑦ 事件广播:敏感意图 → 风控] H --> J[结果反馈 SSE]

各意图数据路径说明:

意图 数据从哪来 经过什么处理 到哪去
faq fin_faq_collection 检索直返(不经 LLM) 用户
product_inquiry fin_product_collection RAG + LLM 生成 + 来源引用 用户
policy_explain fin_policy_collection RAG + 解释 用户
chitchat 无(不查集合) LLM 直接生成 用户
transfer_human 会话上下文 生成转接工单 运营/投顾后台
账户/持仓/交易查询(操作类) 账户系统 API(Phase 2) 精确查询 用户(暂缓,见 §2.4.6)

4. 知识库清单

4.1 三集合总览

集合 内容范围 TopK 数据来源
fin_faq_collection 高频操作类标准问答(开户/绑定/赎回时效/密码/换卡) 3 客服历史工单 + 帮助中心 + 人工审校
fin_product_collection 产品说明书、招募说明书、费率结构、风险等级、业绩比较基准 5 产品系统 + 官方法律文件
fin_policy_collection 监管法规、适当性规则、销售办法、反洗钱政策、平台规则 5 监管原文 + 法务审校

4.2 字段结构(建议统一 schema)

字段 说明 示例
doc_id 唯一标识 FAQ-0001 / PROD-001 / POL-2016-261
title 标题 「基金赎回多久到账」
content 正文(按条款完整性切分,例外与主条款同片段) —
tags 意图/主题标签 ["faq", "redeem"]
effective_date 生效日期 2026-01-01
expire_date 失效日期(过期自动下线) null / 2027-01-01
source_url 来源链接 产品说明书 URL
version 版本号 v2.1
reviewer 审校人 客服运营 / 法务
embedding 向量 Qwen Embedding 生成

字段设计依据:阿里云开发者社区强调「知识条目需标注生效日期与适用范围,费率/期限/政策调整时旧条目及时下线,避免过期信息被检索命中」〔S16〕;富途微藤 AI 提供「知识库健康度检查」能力,识别并更正知识库瑕疵〔S9〕。

4.3 更新频率与数据来源

知识类型 更新频率 更新机制
FAQ 周更新(高频新问题入池) 从历史工单提取真实问法 → 人工审校 → 入库〔S9 FAQ 抽取〕
产品要素 产品变更即时同步 产品系统更新 → 触发知识库重建 → Agent 自动拉取(自动化同步管道)〔S14〕
费率/政策 变更即时下线旧条目 规则引擎 + 生效/失效日期控制〔S16〕
监管法规 法规发布后 N 个工作日内 法务审校后入库

参考做法:招商基金智能在线客服「集成招商银行小招X引擎及知识库」,并将「文字聊天记录生成工单内容总结、自动匹配受理产品和渠道」,实现知识沉淀的自动化〔S5〕。富途微藤 AI 的「语料智能扩写」可在冷启动阶段快速丰富语料〔S9〕。

4.4 知识运营机制

4.5 FAQ 示例(fin_faq_collection 内容样例)

下表给出 fin_faq_collection 的典型问答对示例,覆盖不同意图类型。实际入库时按 §4.2 字段结构存储(question / answer / tags / effective_date / expire_date / embedding)。

类别 用户问法 标准答案要点 是否可直返
开户/注册 怎么开通账户? 邮箱验证码注册 → 填写风险测评问卷 → 后台人工审核 → 邮件通知激活 是
账户绑定 怎么换绑定的银行卡? 进入「我的」-「账户设置」-「银行卡」按提示操作(本功能 Phase 2 上线) 是
密码重置 忘记密码怎么办? 登录页点击「忘记密码」→ 邮箱验证 → 重置 是
风险测评 风险测评多久有效? 有效期按监管要求;过期需重新测评(具体以系统提示为准) 是
赎回时效 赎回多久到账? 货币 1-2 工作日 / 债券 2-4 / 混合·股票 2-4 / QDII 4-13,具体以产品为准 是
费率 申购费怎么收? 按产品费率表执行,前端/后端收费按合同约定;以产品说明书为准 是
适当性 我能买什么风险等级的基金? 先完成风险测评得 C1-C5,再匹配 R1-R5;系统会校验是否匹配 是
收益 这个基金能赚多少? 不能承诺收益;请参考「业绩比较基准」及产品说明书,过往业绩不代表未来 是(合规话术)
投诉 我要投诉! 共情安抚 → 登记投诉工单 → 转人工跟进 否(转人工)
转人工 转人工 立即生成工单转人工 否(转人工)

说明:「是否可直返」= 是否走「检索直返标准答案」路径(faq 意图)。涉及投诉/转人工的,直接走 transfer_human 意图,不检索 FAQ。


5. 人工审核机制

5.1 转人工触发条件(分级)

级别 触发条件 响应
P0 自动转 用户明确要求转人工;情绪激烈/投诉/纠纷关键词;涉人身财产安全/信息安全紧急场景;监管敏感问题 立即转人工,同步上下文
P1 兜底转 意图置信度 < 0.6 且澄清后仍无法识别;RAG 混合判定低置信;连续 2 轮未解决 兜底话术 + 建议转人工
P2 人工引导 涉及资金争议、个性化推荐诉求、大额操作意愿 生成工单 + 引导至投顾/运营人工

触发条件整合了项目既有设计(全景说明书场景 2 的四条件)与国标/行业要求:国标明确「交互失败达到设定阈值、感知负面情绪、涉信息安全人身安全紧急场景时自动转接人工」〔S11〕;捷通华声「连续 2 轮未理解意图、涉及投诉/纠纷关键词、客户主动要求人工」〔S14〕。

5.2 事后复核与质检(兜底保障)

即使 AI 未触发转人工,仍设事后质检:

质检项 规则 动作
负面词命中 后置校验拦截记录 告警 + 安全话术兜底
高风险表述 涉及收益/风险/推荐 抽检人工复核
低置信已答 中置信档回答 纳入质检池,定期回看
投诉关联会话 后续产生投诉的历史会话 全量复核

参考:金融 AI 客服「双轨评测」——一条测业务准确率、一条测合规通过率,任何一条不通过不发布〔S14〕;监管要求客户沟通记录保存 ≥ 20 年且不得含诱导性表述,传统录音抽检覆盖率不足 5%,AI 全量留痕可支撑 100% 质检〔S15〕。

5.3 升级路径

一线客服(运营/客户经理接管)
   ↓ 无法解决或涉合规
高级坐席(持证投顾/资深客服)
   ↓ 涉监管敏感、法律、重大投诉
合规/法务(合规官)

参考做法:招商基金「金谘机构」投顾服务强调「从『顾』到『投』再到『顾』的闭环」,高净值客户「AI 初筛 + 投顾人工审核」正是分级升级的实践〔S4〕。富途按「在线客服 7×24 / 电话人工交易日 24h / 紧急交易柜台」分层〔S8〕。

5.4 国标对齐检查(《顾客联络服务 人工与智能客户服务协同要求》,2026-09-01 实施)

国标要求 本项目对齐情况
转人工入口便捷、衔接顺畅 ✅ 对话中固定「转人工」入口 + 关键词触发
切换后同步客户信息且不重复询问 ✅ 转接工单携带 session_id 完整历史 + 意图 + 置信度 + 已引用来源
支持关键词/菜单/按键/语音多种切换 ✅ 关键词 + 菜单(语音为后续扩展,需核实)
AI 回复保留显著 AI 生成标识 + 风险提示 ✅ 免责声明强制注入(AI 生成标识建议补充,见 §0.4 评审点)
涉及退款/赔偿/合同变更最终确认由人工 ✅ §3.5 投诉处理路径
不以「AI 回答不代表公司立场」拒履行承诺 ✅ 将智能+人工视为完整服务〔S12〕

6. 回复策略

6.1 标准回复模板(分场景)

场景 A:产品信息 / 风险等级客观转述

「XX 基金为【股票型/混合型/债券型】基金,风险等级为 R【1-5】(【低/中低/中/中高/高】风险),适合【风险等级】及以上投资者。您可以在完成风险测评后查看是否匹配。」〔S13 铁律三模板〕

场景 B:费率 / 业绩比较基准

「根据产品说明书,该产品的【申购费率/赎回费率/管理费率】为 X%。该产品的业绩比较基准为 X%,过往业绩不代表未来表现,投资有风险,请您仔细阅读产品说明书。」〔S13 铁律一模板〕

场景 C:风险揭示(非保本)

「理财产品/基金是非保本浮动收益产品,您的本金和收益可能会因市场波动而产生损失,请在充分了解风险后做出决策。」〔S13 铁律四模板〕

场景 D:到账时效

「【货币 1-2 / 债券 2-4 / 混合·股票 2-4 / QDII 4-13】个工作日到账,具体以产品为准。」〔S3〕

场景 E:情绪安抚 + 投诉受理

「非常理解您的心情,很抱歉给您带来不好的体验。我已为您登记并转接人工专员跟进处理,稍后会有专人联系您。」

6.2 语气与措辞规范

规范 说明
共情优先 投诉/情绪场景先安抚、后解决(对标华夏「机智小牛」的情感计算〔S7〕)
客观中性 只陈述事实与规则,不做主观判断与收益暗示
简洁明确 标准问题直返答案,不绕弯
主动引用 关键结论附来源引用,增强可信

6.3 必备风险提示与免责声明

固定免责声明(基类强制注入,子类不得绕过):

「本内容仅为投资分析参考,不构成任何直接投资建议,不构成对任何产品的收益承诺,据此操作风险自负,请谨慎对待。」

AI 生成标识(建议补充,见 §0.4 评审点 4):对 AI 生成的开放式回答,前置「本回答由 AI 生成,仅供参考」,对齐国标「保留显著 AI 生成标识和风险提示」〔S11〕。

6.4 拒答与兜底话术

情形 话术
低置信兜底 「抱歉,我暂时无法准确回答您的问题,建议您转接人工客服获取更准确的帮助。」
负面词命中(安全话术) 「根据监管要求,我不能对收益做出任何承诺。本产品为非保本浮动收益产品,请以产品说明书为准。」
合规禁答 「非常抱歉,该问题涉及的内容我无法回答,为您转接人工客服。」
个性化推荐越界 「我无法为您推荐具体产品。您可以先完成风险测评,了解自己的风险承受能力后,咨询持证投顾为您提供服务。」

6.5 负面词清单(7 个,零容忍)

# 负面词 合规原因
1 保本 资管新规后非保本
2 稳赚 误导性表述
3 无风险 绝对化表述
4 保证收益 禁止刚兑承诺
5 预期收益率 《理财销售办法》明文禁止
6 年化收益率 监管处罚点名措辞
7 安全 绝对化表述

评审建议:可将「零风险」「稳赚不赔」「躺着赚」「坐享收益」等变体纳入模糊匹配规则(见 §0.4 评审点 4)。


7. 数据与状态流转 / 接口与异常

7.1 接口约定

// POST /api/chat/customer   请求
{ "session_id": "...", "user_id": 10001, "message": "..." }
// 响应(统一信封)
{ "code": 200, "message": "success", "trace_id": "uuid",
  "data": { "reply": "...", "source_references": [...], "intent": "faq",
            "confidence": 0.86, "tool_calls": [...], "suggestions": [...] } }

7.2 会话状态机

状态 说明 迁移条件
idle 空闲 收到消息 → processing
processing 处理中 意图清晰 → answering;需澄清 → clarifying
clarifying 澄清中 澄清后仍无法识别 → transferring
answering 回答 完成 → idle
transferring 转人工中 工单生成 → idle(会话交人工)

7.3 异常处理表

异常 策略
检索无结果 / 低分 三档兜底(不硬答)
Milvus 超时(>2s)/故障 降级 MySQL LIKE 关键词检索
LLM 失败 退避重试 1s/2s/4s ×3 → 备用模型 → 预设兜底话术
命中负面词 打回重生成;二次仍命中则返回安全话术 + 告警
Redis 故障 会话降级为进程内字典;画像直连 MySQL

8. 度量与验收(双轨评测)

轨道 用例 通过标准
业务准确率 高频意图回答正确性、来源引用完整性 高频意图准确率 ≥ 90%
合规通过率 负面词命中率、免责声明缺失率、适当性越界、越权查询 全部为 0

参考:捷通华声「上线前必须跑满合规用例,任何一条不通过都不发布」〔S14〕;百度智能云分层目标「试点期意图识别准确率 ≥85%、推广期多轮完成率 ≥70%、成熟期 NPS ≥40」〔S15〕。


9. 完善方向与最终交付要求

本章按六大维度逐项指出当前差距 → 完善标准(可量化)→ 交付要求,作为评审后的迭代排期依据。每条完善标准都对应可验收的量化门槛;文末给出上线门禁(Go/No-Go)清单作为最终交付要求。

9.0 差距总览(一页速览)

维度 关键差距(Top) 完善标准(量化) 优先级
功能完整性 闲聊无边界、澄清槽位缺失、同义问法缺失、suggestions 空转、转人工会话未回收、跨 Agent 联动未定义 闲聊 ≤3 轮引导;澄清 ≤2 轮;top20 问法各 ≥5 同义句;转人工会话 24h 内回收 P0
用户体验 无 SSE 流式、情绪识别后置、无首屏建议问题、引用不可点击、多端未适配 首 token <800ms;负面情绪直通安抚通道;引用可跳转原文 P0
性能优化 三段串行、FAQ 无缓存、无 rerank、截断策略粗糙、无并发限流 端到端 P95 <3s;热点缓存命中 ≥80%;意图与检索并行 P1
错误处理 无熔断/半开、无幂等、trace_id 未贯穿、兜底话术硬编码、无灰度回滚 熔断 N 次触发;同 trace_id 幂等;话术可热更新 P1
安全性 无提示词注入防护、检索层行级过滤未强制、脱敏清单未定、反诈触发词未列、审计字段未定 注入红队 0 穿透;越权查询 0;敏感字段 100% 脱敏 P0
代码可读性 常量散落、意图分类器硬编码、单测覆盖未定义、命名未固化、文档代码脱节 常量单一来源;核心逻辑覆盖率 ≥80%;合规用例 100% P1

9.1 功能完整性

改进点 现状差距 完善标准 交付要求
闲聊意图边界 chitchat 仅写「LLM 引导业务话题」,无话题边界与轮次上限,可被闲聊无限消耗资源 闲聊 ≤3 轮后强制引导业务;引导后仍无业务意图 → 转人工或静默结束 闲聊状态机 + 引导话术模板 + 轮次计数单测
澄清机制槽位 状态机有 clarifying 态,但未定义澄清追问的槽位(产品名/时间范围等)与次数上限 澄清 ≤2 轮;每轮追问 1 个关键槽位;澄清失败 → 转人工 槽位定义表 + 澄清话术 + 澄清失败转人工用例
同义问法/口语纠错 用户问法与官方术语差距大(「到期能拿回多少钱」vs「现金价值」)〔S16〕 高频意图 top20 问法各配置 ≥5 个同义问法;拼写/口语纠错 同义问法词表(历史工单提取)+ 纠错规则
suggestions 空转 接口有 suggestions 字段但无生成逻辑 每条回答附带 2-3 个关联建议问题,点击率埋点 建议问题生成规则 + 埋点
转人工会话回收 全景说明书提到「解决后提取摘要并回收对话」,客服专项未细化 转人工会话解决后 24h 内生成摘要,回收入 FAQ 候选池 摘要提取流程 + FAQ 沉淀机制
跨 Agent 联动 客服识别「高净值/大额意愿/敏感话题」后的联动规则未定义 三类联动触发词清单 + 事件发布契约(suspicious_intent 等) 联动触发规则表 + 事件契约文档

9.2 用户体验

改进点 现状差距 完善标准 交付要求
流式输出(SSE) 接口返回完整 JSON,长答案首字延迟高 首 token 延迟 <800ms;SSE 流式输出 SSE 改造 + 首字延迟监控
情绪识别前置 情绪识别在转人工判定时才做,负面情绪客户先在兜底话术打转 意图路由前做情绪预判;负面情绪直通「安抚+转人工」,不绕检索 情绪预判前置 + 负面情绪快速通道
首屏建议问题 对标天天基金「猜你想问」、支小宝「大家都在问」〔S2〕,本项目无首屏引导 首屏提供 top 高频问题 + 按画像的个性化建议 首屏问题推荐模块
引用可点击 来源引用格式未定义,长答案无分点 长答案分点/分段;来源引用可点击跳转原文 回答格式化模板 + 引用跳转
多端适配 未说明 APP/H5/网页端差异 三端统一会话(session 跨端)与展示适配 多端适配方案

9.3 性能优化

改进点 现状差距 完善标准 交付要求
三段串行链路 意图分类 → RAG 检索 → LLM 生成串行 意图分类与 RAG 检索并行;端到端 P95 <3s 并行化改造 + 压测报告
FAQ 热点缓存 FAQ 直返未做缓存,重复计算 热点 FAQ 缓存命中率 ≥80%;缓存 TTL 10min Redis 缓存层 + 命中率监控
检索重排 rerank TopK=5 直接送 LLM,无精排 引入 rerank,top1 准确率相对基线提升 ≥5pp rerank 模型接入 + 离线评测
上下文截断策略 超 4096 token 截断旧消息,丢失关键信息 改为「摘要 + 近期原文」混合;按意图选择性保留 上下文压缩策略 + 效果对比
并发限流削峰 高峰并发无保护 令牌桶/滑动窗口限流;QPS 阈值;降级预案 限流配置 + 压测

9.4 错误处理

改进点 现状差距 完善标准 交付要求
熔断与半开恢复 只有超时降级,无熔断器 Milvus/LLM 连续失败 N 次熔断,半开探测恢复 熔断器 + 恢复策略
幂等性 重复提交/重试无幂等 同一 trace_id 幂等 幂等键设计
trace_id 全链路 接口有 trace_id,未说明贯穿到 Milvus/LLM 调用日志 一次请求一个 trace_id 贯穿所有下游调用 全链路日志规范 + 示例
兜底话术硬编码 安全话术/兜底话术硬编码在代码 话术配置化(独立配置/知识条目),可热更新 话术配置中心
灰度与回滚 新模型/新知识库上线失败无回滚 模型/知识库版本化,一键回滚 版本管理 + 回滚预案

9.5 安全性

改进点 现状差距 完善标准 交付要求
提示词注入防护 用户消息可夹带「忽略之前的指令」 输入侧注入检测 + 输出侧越界拦截;红队 0 穿透 注入防护规则 + 红队测试用例
检索层权限过滤 客服「仅本人数据」,但检索/SQL 层是否强制行级过滤未明确 数据访问在检索层强制 + 服务层二次校验(纵深防御) 行级过滤设计 + 越权用例
脱敏字段清单 mask_tool 已提但字段清单未定 身份证/卡号/手机号全脱敏;日志/回显均脱敏 脱敏字段清单 + 脱敏时机
反诈敏感意图触发词 suspicious_intent 事件已提但触发词未列 反诈关键词(转账/验证码/安全账户)+ 实时预警 触发词清单 + 与风控联动
审计留痕完整性 留痕 ≥20 年已提但字段未定义 完整记录(人/时/问/答/意图/置信度/来源/工具调用) 审计日志字段规范

9.6 代码可读性

改进点 现状差距 完善标准 交付要求
常量散落 阈值/负面词/免责声明可能散落在子类 唯一常量源(对标 SUITABILITY_MATRIX 单一来源) 常量集中管理(config/constants)
意图分类器硬编码 意图分类器硬编码 意图集 + 阈值配置化 配置化 + 文档
单元测试覆盖 未定义测试覆盖率 核心逻辑覆盖率 ≥80%;合规用例 100% 通过 单测 + 双轨评测
命名与目录规范 已有 api/service/tool 但未固化规范 统一命名 + 分层依赖规则 编码规范文档
文档代码同步 方案 vs 代码可能脱节 关键阈值/话术/集合在代码中有单一真相源,文档引用该源 文档-代码同步机制

9.7 最终交付要求(上线门禁 Go/No-Go 清单)

以下为必须全部满足才可进入联调/上线的硬性门禁;任一未达标即 No-Go。

功能门禁(Must)

# 交付项 验收标准
F1 5 类意图 + 三集合路由可运行 每类意图至少 1 条端到端用例通过
F2 三档兜底(高/中/低置信) 低置信 100% 不硬答,触发兜底/转人工
F3 转人工工单携带完整上下文 session_id + 历史 + 意图 + 置信度 + 来源一次传递
F4 负面词 7 个后置校验 命中率 0(拦截率 100%)
F5 免责声明强制注入 面向客户的输出 100% 附固定话术

安全与合规门禁(Must)

# 交付项 验收标准
S1 不代客交易 客服 Agent 无任何交易写接口
S2 适当性不越界 C1×R2+ 推荐 0 起(客服不荐,越界转投顾)
S3 越权查询防护 本人数据之外查询 0 起
S4 敏感字段脱敏 身份证/卡号/手机号日志与回显 100% 脱敏
S5 留痕可追溯 全量会话归档,保存 ≥20 年

质量门禁(Must)

# 交付项 验收标准
Q1 高频意图准确率 ≥90%
Q2 首响时长 <3s(SSE 首 token <800ms)
Q3 转人工率 ≤20%
Q4 合规用例通过 双轨评测合规轨道 100% 通过
Q5 核心逻辑单测覆盖率 ≥80%

交付物清单(最终交付要求)

类别 交付物
代码 customer.py(5 意图 + 三档兜底)、rag_service.py、milvus_tool.py、mask_tool.py、memory_service.py + 常量集中管理模块
配置 意图阈值、负面词清单、免责声明话术、兜底话术、限流参数、熔断参数(全部配置化)
数据 三集合知识库(FAQ/产品/政策)初始版本 + 同义问法词表
文档 本方案 v1.1 + 编码规范 + 接口契约 + 审计日志字段规范 + 联调测试用例
测试 单元测试(覆盖率 ≥80%)+ 双轨评测集(业务 + 合规)

10. 附录

10.1 参考来源清单

编号 来源 要点
S1 蚂蚁财富《智能理财助理服务协议》〔render.alipay.com〕 「蚂小财」大模型局限免责、不构成投资建议/推介/要约、95188 人工热线
S2 天天基金研究(AI 客服「小天」/支小宝界面)〔ima.qq.com〕 常见问题、选基金/持仓分析/产品评测功能
S3 天天基金「基金吧答疑」〔guba.eastmoney.com〕 联系客服路径、赎回到账分类型时效、折算解释
S4 招商基金「懂投资更懂你」(金谘机构)〔cmfchina.com〕 7×24 在线机器人、持仓诊断、基金体检
S5 招商基金「数字金融建设」〔sohu.com/a/969419069〕 小招X引擎+知识库、AI 工单总结、营销知识图谱、DeepSeek R1+QWQ-32b
S6 证券时报「沉浸式体验基金智能客服 九大趋势」〔stcn.com〕 招商/富国基金智能客服对「基金下跌」的回答差异、天弘 94% 准确率
S7 华夏基金李一梅「AI 时代的能力者」〔cet.com.cn〕 「机智小牛智能体」、情感计算、人机边界
S8 富途官方「7×24 小时服务」〔futunn.com〕 帮助中心三步、智能客服 7×24、人工分层时段
S9 富途 2024 年报〔newsfile.futunn.com〕 ContactBot(TextBot/VoiceBot)、微藤 AI:语料扩写/FAQ 抽取/文档问答/知识库健康度检查
S10 腾讯新闻「6000 次实测基金公司」〔so.html5.qq.com〕 国泰 84%、嘉实 42%、建信 31% AI 解答率;15 家仅 3 家周末有人工
S11 新华网《顾客联络服务 人工与智能客户服务协同要求》〔news.cn〕 国标转人工规则、切换同步不重复询问、AI 标识+风险提示
S12 人民论坛「新国标整治 AI 客服躲猫猫」〔rmlt.com.cn〕 不以「AI 回答不代表公司立场」拒履行承诺
S13 华云天下「金融 AI 客服十条合规铁律」〔huayunworld.com〕 不能承诺收益/推荐/风险评价/刚兑、三道防线、标准应答模板
S14 捷通华声「金融客服智能体可控比聪明更重要」〔sohu.com〕 置信度阈值转人工、连续 2 轮/投诉关键词、双轨评测
S15 百度智能云「智能客服赋能证券基金」〔cloud.baidu.com〕 留痕 ≥20 年、合规引擎 200+ 规则、分层落地指标
S16 阿里云开发者社区「AI 客服金融落地实践」〔developer.aliyun.com〕 生效日期标注、条款完整性切分、同义问法、脱敏、知识运营岗位
S17 嘉实基金「亮相世界人工智能大会」〔jsfund.cn〕 与浦发共建 AI 财富数字人联合实验室

10.2 需核实项清单

# 待核实内容 说明
1 天天基金智能客服响应时效 ≤30 秒 来源为第三方(叩富网)引用「天天基金统计数据」,非官方口径,需核实
2 招商基金部署模型「QWQ-32b」具体版本 搜狐报道原文,模型名拼写需核实
3 蚂蚁财富「蚂小财」的意图分类体系与转人工阈值 未公开,需向平台方或公开资料核实
4 国泰/嘉实/建信 AI 解答率实测数据 为第三方媒体抽样测试,非官方披露,仅作行业参照
5 各平台知识库字段结构 均为非公开信息,本方案 §4.2 为设计建议,需按本项目实际数据核实
6 语音切换人工(§5.4 国标对齐) 本项目当前仅文本,语音为后续扩展,需排期确认