wip: 客服Agent + RAG + 画像收尾(基于 6516ccb)
This commit is contained in:
@@ -0,0 +1,168 @@
|
||||
# 设计文档:客服 Agent 本体(子项目 B)
|
||||
|
||||
> 状态:待用户审阅
|
||||
> 日期:2026-09-10
|
||||
> 前置依赖:子项目 A(`2026-09-10-knowledge-retrieval-infra-design.md`)必须先完成,本文档依赖其 `query_knowledge` 工具。
|
||||
> 权威来源优先级:行为边界以《客服Agent一期运行边界基线_v1.md》为最高优先级(该文档自身声明"高于历史问答、示例话术和模型自由生成内容");
|
||||
> 底座接入方式以 `docs/14-Agent组员统一接入说明书.md` 和 `TODO.md` T8.1 为准;
|
||||
> 二者冲突时以边界基线为准(业务行为),接入方式冲突时以底座文档为准(工程约束)。
|
||||
|
||||
## 1. 背景
|
||||
|
||||
`TODO.md` T8.1 要求客服 Agent 覆盖 FAQ、产品咨询、政策解释、闲聊、转人工五类意图。桌面胜宇提供的三份"一期"材料把这个需求收窄为一套更具体、更严格的产品规则:一个虚构品牌"奶龙基金责任有限公司"及其客服"奶龙基金智能助手",服务范围只是解释已审核公开信息和指引办理路径,不触碰任何个人账户数据、不代办交易、不提供投资建议。
|
||||
|
||||
## 2. Agent 定义
|
||||
|
||||
```python
|
||||
class CustomerServiceAgent(BaseAgent):
|
||||
definition = AgentDefinition(
|
||||
agent_type="customer_service",
|
||||
version="1.0.0",
|
||||
allowed_roles=("customer",),
|
||||
allowed_portals=("api",),
|
||||
allowed_tools=("query_knowledge",),
|
||||
supported_intents=("faq", "product_inquiry", "policy_explain",
|
||||
"chitchat", "transfer_human"),
|
||||
)
|
||||
```
|
||||
|
||||
`supported_intents` 沿用 `docs/02` 的五类,供底座自动意图分类器使用、供配置中心按意图配置工具白名单使用。但**这五类分类结果不是 `handle()` 内部安全路由的唯一依据**,理由见第 3 节。
|
||||
|
||||
**范围确认:本 Agent 只服务已登录客户端模块,游客版客服是完全独立的功能**
|
||||
|
||||
《一期运行边界基线》§2 把服务对象分成"访客"和"已登录用户"两档,一度让人以为本 Agent 需要同时处理未登录访问——
|
||||
但结合整体前端规划确认:产品分为面向游客的公开站点(公司介绍/产品介绍/登录页/游客版智能客服)和登录后按角色
|
||||
(客户/风控/投顾等)区分的客户端,**本文档设计的 `customer_service` Agent 只是客户端里的一个模块,只服务已登录客户**;
|
||||
游客版智能客服是公开站点上完全独立的另一套功能,不经过本 Agent,也不在本文档范围内。
|
||||
|
||||
这与底座鉴权架构(`app/api/dependencies/auth.py` 要求任何 Agent 运行都必须携带有效 JWT,没有匿名访问路径)天然吻合,
|
||||
不需要额外改动底座。第 3 节 P1 分支"已登录场景可附加引导至我的账户页面"这句话因此永远成立——能走到本 Agent 的
|
||||
请求都必然来自已登录客户。《一期运行边界基线》里关于"访客"那一档的描述,适用对象是游客版智能客服(独立功能,
|
||||
未来若需要设计,是另一个独立任务),不适用于本文档。
|
||||
|
||||
## 3. 核心设计决策:安全路由与知识检索分离
|
||||
|
||||
《一期运行边界基线》第 9 节明确要求:安全、权限、个人数据、写操作、投诉赔偿类问题"必须在 RAG 检索前完成识别与拦截",且"不应仅依赖向量检索命中"。这意味着不能把"要不要查知识库、要不要转人工"这件事完全交给:
|
||||
|
||||
- 底座自动跑的 LLM 意图分类器(`self._classified_intent`,本身是私有属性,且分类结果依赖模型输出,不是确定性的);
|
||||
- 向量检索是否命中某条"应该拒答"的知识条目(万一语义相似度没打准,就会漏判)。
|
||||
|
||||
因此 `handle()` 内部自己实现一个**确定性的安全路由**(关键词/正则规则,不调用模型),独立于底座自动意图分类,逐条对照《一期运行边界基线》§5 和《向量化前流程文档》§3 的优先级表:
|
||||
|
||||
```text
|
||||
P0 安全优先(诈骗/盗号/验证码泄露/资金风险关键词命中)
|
||||
→ 固定安全话术 + transfer_required=True,不查知识库
|
||||
|
||||
P1 本人/他人账户数据(持仓/收益/订单/银行卡/投诉进度/风险测评结果类关键词)
|
||||
→ 固定降级话术("当前客服 Agent 无法读取该项本人数据")
|
||||
已登录场景可附加引导至"我的账户"页面;不查知识库
|
||||
|
||||
P2 转人工诉求(代办交易/资料修改/销户/投诉赔偿/法律争议/明确要求人工)
|
||||
→ 固定转人工话术 + transfer_required=True,不查知识库
|
||||
|
||||
P3 公开静态问题、闲聊、人设问答
|
||||
→ 调用 query_knowledge 工具检索(闲聊类问答本身也在 105 条知识库内,
|
||||
走同一条检索路径,不单独分支)
|
||||
注意:子项目 A 第一版只有 fin_faq_collection 有数据,product_inquiry/
|
||||
policy_explain 两个意图对应的集合暂时是空的,命中率等同于未命中,
|
||||
会自然落入下面的 P4 兜底话术,这是已知的第一版限制,不是 bug。
|
||||
|
||||
P4 低置信度/未命中
|
||||
→ 固定"无法确认,建议转人工"话术,不得猜测或编造
|
||||
```
|
||||
|
||||
这个安全路由表本身**不依赖底座的 `IntentClassifier`**,因此不需要解决"`handle()` 里读不到 `self._classified_intent`"这个此前发现的底座缺口——本 Agent 完全绕开这个问题,用自己的确定性规则做主要业务判断。底座的自动分类结果仍然会被记录进 `CoreResult.intent`(供审计和统计用),但不驱动业务分支。
|
||||
|
||||
安全路由的关键词/正则规则来源:《一期运行边界基线》§4 的禁止事项分类描述和《一期向量化前流程文档》§3 的场景描述,实现时需要把这些自然语言描述转成具体的匹配规则列表,作为 Agent 内部常量维护(不需要走数据库配置化,因为这是硬编码的合规红线,不是可调业务参数)。
|
||||
|
||||
## 4. 知识命中后的回复生成
|
||||
|
||||
按《向量化前流程文档》的要求,标准答案不应被模型自由改写。结合确认过的决策:
|
||||
|
||||
- **命中 1 条**:直接返回该条 `content_text` 原文,不调用模型。
|
||||
- **命中多条**:允许调用 `self.generate_with_model()` 把这几条标准答案"组织一下语言",但 Prompt 必须明确约束"只能使用给定的几段文字内容,不得添加任何未出现在原文中的新事实、数字、承诺或渠道信息",属于"复述/合并"而非"生成新内容"。
|
||||
- **未命中或全部相似度低于阈值**:走 P4 固定话术,不调用模型编造。
|
||||
|
||||
`source_references` 由 `query_knowledge` 工具调用自动生成(`ToolExecutor` 已有机制),Agent 不需要自己拼引用。
|
||||
|
||||
**`CoreResult.intent` 必须由 `handle()` 显式设置,不能留空等底座自动分类结果回填**:底座会在 `handle()`
|
||||
执行前自动跑一次独立的 LLM 意图分类(结果存在 `self._classified_intent`,`handle()` 内部读不到,也不应该依赖它,
|
||||
见第 3 节)。`BaseAgent._execute_governed()` 的规则是 `result.intent or self._classified_intent`——如果
|
||||
`handle()` 返回的 `CoreResult.intent` 是 `None`,最终写入 `conversation_message.intent` 的会是那个独立分类器的结果,
|
||||
它完全可能和 Agent 本节安全路由实际走的分支对不上(例如安全路由判定为 P1 账户数据问题直接拒答,但自动分类器
|
||||
独立跑出来的结果是 `intent=faq, confidence=0.8`,两者没有关联)。
|
||||
|
||||
因此 `handle()` 必须根据自己在第 3 节安全路由里实际走的分支,显式构造并返回准确的 `IntentResult`:
|
||||
|
||||
```text
|
||||
P0/P1/P2(安全/账户数据/转人工诉求) → intent="transfer_human", confidence=1.0, needs_clarification=False
|
||||
P3 命中知识库 → intent 取 "faq"/"product_inquiry"/"policy_explain" 中实际命中的一个,
|
||||
confidence 用检索相似度或固定较高值
|
||||
P3 闲聊/人设问答 → intent="chitchat", confidence=1.0
|
||||
P4 未命中/低置信度 → intent="transfer_human", confidence=<实际值>, needs_clarification=True
|
||||
```
|
||||
|
||||
这样落库的 `intent` 字段才能真实反映业务分支,供审计、统计和(如果第 6 节采用方案二)闲聊连续计数使用。
|
||||
|
||||
## 5. 转人工
|
||||
|
||||
Agent 只做两件事:① 在回复文本里包含《一期向量化前流程文档》§3 规定的固定联系方式(人工客服 15936583816,工作日 09:00-18:00 等);② 在 `CoreResult` 上设置 `transfer_required=True` 和 `transfer_reason`。
|
||||
|
||||
**联系方式文案的来源需要澄清**:由于第 3 节 P0/P1/P2 分支明确"不查知识库",这几个分支的固定话术(含电话、邮箱、地址等联系方式)
|
||||
**只能是硬编码在 Agent 代码里的常量**,不可能像 P3 分支那样从 `query_knowledge` 动态取——安全关键路径不应该依赖额外的
|
||||
检索调用才能返回正确结果。这意味着联系方式如果未来变更,需要同时改两个地方:QA 知识库源文件(供 P3 检索到的知识内容使用)
|
||||
和 Agent 代码里的常量(供 P0/P1/P2 安全话术使用)。这是刻意的取舍,不是疏漏。
|
||||
|
||||
是否真正调用 `POST /conversations/{session_id}/handover-requests` 创建 `svc_handover_ticket`,由前端根据 `transfer_required` 标记决定是否调用,不属于本 Agent 职责——这与 `AGENTS.md`"Agent 只能提出转人工请求,不能分配、接单、解决或关闭工单"的约束一致,也是本轮讨论中厘清的、胜宇材料与团队大文档表面冲突实际不冲突的点(见子项目 A 文档记录之外,本文档单独说明:大文档设想的"生成完整工单"是前端调用已有接口后的结果,不是 Agent 的新增职责)。
|
||||
|
||||
## 6. 闲聊计数与软引导
|
||||
|
||||
《一期运行边界基线》§3 和《向量化前流程文档》§3 都提到"连续闲聊第 4 条仅做一次自然业务引导"。
|
||||
|
||||
**实现方式需要先说明一个架构约束**:业务 Agent 不允许直接创建 `SQLAlchemy Session`、直接查
|
||||
`ConversationRepository` 或任何 Model(`AGENTS.md`/`docs/14` 明文禁止),第一版设计里"直接查
|
||||
`conversation_message` 最近 3 条"的写法违反了这条规则,已修正如下:
|
||||
|
||||
这是一条 UX 软性提示规则,不是合规红线(不像 P0-P2 那样错了会有实质风险),因此**第一版不追求跨请求的精确计数**,
|
||||
按以下简化方式实现,两种都不需要新增底座能力:
|
||||
|
||||
- 方案一(推荐,第一版采用):不做跨轮次持久计数,只在单次 `handle()` 内根据本次和上一轮用户消息的表面特征做启发式判断,
|
||||
或者干脆先不实现"连续第 4 条"的精确触发,只保证"闲聊类回复本身语气自然、不生硬",把精确计数列为已知的第一版简化项。
|
||||
- 方案二(如果产品认为这条规则必须精确执行):需要底座新增一个只读工具(例如
|
||||
`get_recent_intent_history`,走 `ToolExecutor` 注册,权限和其他工具一致地声明),Agent 通过
|
||||
`self.call_tool(...)` 调用,而不是绕过工具体系直接查库。这属于对底座的小额扩展,需要和底座负责人确认是否本轮一起做。
|
||||
|
||||
本设计文档默认采用方案一,方案二作为备选留给用户决定是否升级范围。
|
||||
|
||||
## 7. 合规治理的兼容性
|
||||
|
||||
`handle()` 产出的 `CoreResult` 仍然会经过底座既有的 `governance.review_output()`(负面词过滤、引用来源校验、手机号/证件号脱敏),不需要额外适配——因为 Agent 返回的是知识库原文或"复述"文本,本身已经是审核过的合规内容,治理层是兜底而非主要防线,两层防线不冲突。
|
||||
|
||||
## 8. 测试计划
|
||||
|
||||
采纳《客服Agent一期向量化前流程文档_v1.md》§5 第五步给出的验收测试清单,逐项覆盖:
|
||||
|
||||
```text
|
||||
公开产品资料、15 点规则、费率、适当性问题 → 正确命中知识库并作答
|
||||
本人持仓、订单状态、投诉进度问题 → 一律固定降级话术,不返回任何数据
|
||||
明确要求人工 → 固定转人工话术 + transfer_required=True
|
||||
验证码/密码泄露、诈骗场景 → 立即安全话术,不继续收集信息
|
||||
投资推荐类问题 → 明确拒绝给出具体建议
|
||||
连续闲聊达到第 4 条 → 触发一次软性业务引导,之后不重复
|
||||
低置信度/无法判断的问题 → 固定"无法确认"话术,不编造
|
||||
```
|
||||
|
||||
另外补充底座接入层面的常规测试(参照 `docs/14`):未授权角色/入口调用拒绝、工具白名单外调用拒绝、模型格式错误时安全路由仍正常工作(因为安全路由不依赖模型)。
|
||||
|
||||
## 9. 范围外事项
|
||||
|
||||
- 真实创建 `svc_handover_ticket` 工单——前端职责。
|
||||
- `fin_product_collection`/`fin_policy_collection` 的实际内容——见子项目 A 文档第 2 节。
|
||||
- 知识库内容的在线编辑/审核——见子项目 A 文档第 2、8 节。
|
||||
- "我的账户"页面本身——独立前端页面和后端专属接口,本 Agent 只做跳转提示,不实现该页面。
|
||||
- 游客版智能客服(公开站点上面向未登录访客的客服功能)——与本 Agent 是完全独立的两套功能,不共用后端实现,见第 2 节范围确认。
|
||||
**已确认后续也要接入本底座**,但这涉及匿名身份认证(`AGENTS.md` 列为高风险区域的 Authentication),
|
||||
需要单独作为子项目 C 走一次完整的 brainstorm 和设计评审,不在本文档展开,也不应该为了预留接口而现在改动
|
||||
`build_request_context`/JWT 相关代码。子项目 C 启动时的关键设计问题:匿名身份怎么签发和校验、新增角色
|
||||
`role=guest` 的权限边界(大概率只能调用 `query_knowledge`,不能召回记忆、不能有任何客户归属数据)、
|
||||
游客会话如何过渡到登录后的正式会话(如果产品要求两者衔接)。
|
||||
@@ -0,0 +1,285 @@
|
||||
# 设计文档:客服知识检索基础设施(子项目 A)
|
||||
|
||||
> 状态:待用户审阅
|
||||
> 日期:2026-09-10
|
||||
> 依赖关系:本文档是子项目 B(客服 Agent 本体)的前置依赖,须先完成本文档再启动 B。
|
||||
> 权威来源优先级:本文档在"检索路径"上完全采纳桌面《15-知识检索接入方案.md》(架构负责人已批准的方案,不重新设计);在"知识内容与安全边界"上采纳桌面《胜宇前期开发资料》文件夹内的
|
||||
> 《客服Agent一期运行边界基线_v1.md》《客服Agent一期向量化前流程文档_v1.md》
|
||||
> 《客服Agent知识库_QA问答对_v5_RAG发布候选版.txt》(v5.8,105 条)。
|
||||
> `智能财富管家系统-业务与技术全景设计说明书.md`、`智能财富管家系统-完整项目流程文档.md`
|
||||
> 两份文档确认为更早期的整体项目背景资料(不同技术栈),仅作背景参考,不作为本设计依据。
|
||||
|
||||
## 1. 背景与目标
|
||||
|
||||
客服 Agent 需要一条"检索知识库回答问题"的链路。现状:
|
||||
|
||||
- `fin_knowledge_meta`/`agent_faq_synonym` 两张表存在于数据库基线,但代码里没有任何 ORM、Repository 或 Service 使用它们。
|
||||
- `KnowledgeReferenceService.resolve()` 是无条件抛异常的占位代码。
|
||||
- `ModelGateway`/`ModelGenerationService` 只有文本生成能力,没有向量生成(embedding)能力。
|
||||
- Milvus 服务本身可连通(健康检查通过),但没有任何集合,也没有任何写入或检索它的代码。
|
||||
- 客服知识内容已经由业务方(胜宇)审核完毕,固化在 `客服Agent知识库_QA问答对_v5_RAG发布候选版.txt`(v5.8,105 条),文件本身声明状态为 `approved_candidate`,可直接作为向量化语料,**不需要在系统里另外实现一套"草稿→审核→发布"的在线知识库管理后台**——这是本轮讨论中被推翻的早期方向,纠正记录见第 8 节。
|
||||
|
||||
**目标**:把这 105 条已审核 QA 结构化导入数据库,建立"生成向量并写入 Milvus"和"检索"两条链路,产出一个公共只读工具 `query_knowledge`,供客服 Agent(子项目 B)调用。
|
||||
|
||||
## 2. 范围
|
||||
|
||||
**做什么**:
|
||||
- `fin_knowledge_meta` 新增 ORM 映射(`milvus_collection` 已存在于基线,**不新增字段、不新增迁移**,见 §3.1)。
|
||||
- `agent_faq_synonym` 新增只读 ORM 映射。
|
||||
- 一次性结构化导入脚本:解析 QA 源文件 → 校验 → 写入两张表。
|
||||
- `ModelGateway`/`ModelGenerationService` 新增 `embed()` 方法(阿里云 DashScope,`text-embedding-v3`,1024 维)。
|
||||
- 只读 Milvus 检索适配器 + 检索服务 `KnowledgeRetrievalService`(三集合路由、配置驱动、MySQL 二次校验、降级兜底)。
|
||||
- 写路径 Milvus 适配器(`upsert`/`delete`,与只读适配器代码隔离)+ 复用现有 `domain_event_outbox`/`OutboxWorker` 机制的向量同步 Worker。
|
||||
- 公共工具 `query_knowledge` 注册进 `bootstrap.py`。
|
||||
- 修复 `KnowledgeReferenceService.resolve()`(HMAC 签名 token)。
|
||||
|
||||
**不做什么(明确排除)**:
|
||||
- 知识内容的在线创建/审核管理页面或接口(本轮只做一次性导入,未来如需运营团队自主维护知识库,是独立后续任务)。
|
||||
- `fin_product_collection`、`fin_policy_collection` 两个集合的实际内容——105 条 QA 目前没有可靠依据区分"产品咨询"和"政策解释"两类,第一版**全部导入 `fin_faq_collection`**,另外两个集合建好但暂不populate,等有明确分类依据的内容时再拆分。
|
||||
- `agent_faq_synonym` 的审核后台——本轮直接把 QA 文件里的"相似问法"写成 `status='approved'`(因为源文件本身已审核),不做同义词的独立审核流程。
|
||||
|
||||
## 3. 数据模型变更
|
||||
|
||||
### 3.1 `fin_knowledge_meta`
|
||||
|
||||
`docs/02` §6.2 已明确要新增 4 个字段:`content_text`、`tags`、`reviewer_id`、`review_status`。
|
||||
|
||||
**`milvus_collection` 的字段状态已经核实完毕:它已存在于不可变基线中,本设计不新增该字段,也不需要新增迁移。**
|
||||
|
||||
核实链路(可复现,不依赖任何一方的"没提到"这类间接证据):
|
||||
|
||||
1. `docs/00-新数据库基线设计.md` §6.2 `fin_knowledge_meta` 字段表(第 871 行):
|
||||
`| milvus_collection | VARCHAR(64) | 非空 | Milvus 集合名 |`——**未标 🆕**,而同表其它 7 个字段
|
||||
(`effective_date`/`expire_date`/`content_text`/`tags`/`reviewer_id`/`review_status` 等)都带 🆕 标记。
|
||||
这个对照本身就是"它是基线自带、不是新增字段"的直接证据。
|
||||
2. `docs/02-数据库建表设计.md` 的 ALTER TABLE 清单没提它,**不构成矛盾**——该清单只列"需要在基线上补充的字段",
|
||||
基线已有字段本就不该出现在里面。此前把"没提到"当成矛盾是误读,此处更正。
|
||||
3. `tools/generate_baseline_sql.py` 从 `docs/00` 的字段表生成 `alembic/baseline_generated.sql`,
|
||||
生成的 DDL 中该列为 `milvus_collection VARCHAR(64) NOT NULL`,并经 `tools/audit_schema.py`
|
||||
作为期望 schema 的一部分参与审计。
|
||||
|
||||
**对实施的影响(与原稿的关键差异)**:
|
||||
|
||||
- 不写新增字段的 Alembic 迁移;只写 ORM 映射。
|
||||
- 该列是 **`NOT NULL` 且无默认值**(不是原稿假设的"可空"),所以 §4 的导入脚本**必须为每一行显式赋值**
|
||||
`milvus_collection`,不能依赖默认值——这一点原本就是设计意图,现在从"设计选择"变为"数据库强约束"。
|
||||
- 实施时仍需对现库做一次 Inspector/`audit_schema.py` 复核,确认现库确实是按这份基线建的表;
|
||||
但这属于**常规环境一致性校验**,不再是"设计上悬而未决的未知项"。
|
||||
|
||||
同表 `version`、`effective_date`、`expire_date` 三个字段的状态沿用同一条核实链:`docs/00` 中
|
||||
`version` 无 🆕 标记(基线自带),`effective_date`/`expire_date` 带 🆕 标记(由后续迁移补充),
|
||||
`alembic/baseline_generated.sql` 中三者齐全,因此本设计同样**不新增这三个字段**。
|
||||
|
||||
**另外提醒**:桌面另有一份《智能客服Agent新增表详细设计.md》(2026-01-08,v1.0),是 `docs/00` 引用但已过期的草稿,
|
||||
其 `agent_negative_word`/`svc_handover_ticket`/`agent_faq_synonym`/`conversation_feedback` 字段定义与代码实际实现
|
||||
(`docs/02` + 现有 ORM/governance.py 查询)不一致,本设计文档不采用,实施时也不应参考该文件。
|
||||
|
||||
### 3.2 ORM 映射(新增文件 `app/model/knowledge.py`)
|
||||
|
||||
```python
|
||||
class KnowledgeMeta(Base):
|
||||
__tablename__ = "fin_knowledge_meta"
|
||||
# 字段对齐 docs/02 §6.2 + 本文档 §3.1,只读字段沿用既有定义,不重复列出全部历史字段
|
||||
|
||||
class FaqSynonym(Base):
|
||||
__tablename__ = "agent_faq_synonym"
|
||||
# 只读映射,字段对齐 docs/02 §7.3
|
||||
```
|
||||
|
||||
具体字段清单在实现阶段对照 `docs/00-新数据库基线设计.md` 和 `docs/02-数据库建表设计.md` 逐字段核对,不在本设计文档重复抄写 DDL。
|
||||
|
||||
## 4. 一次性结构化导入
|
||||
|
||||
### 4.1 源文件字段映射
|
||||
|
||||
按《客服Agent一期向量化前流程文档_v1.md》§4 定义:
|
||||
|
||||
| 源文件字段 | 目标 | 说明 |
|
||||
|---|---|---|
|
||||
| `qa_id`(如 `RAG-PER-001`) | 存入 `KnowledgeMeta.tags` JSON(如 `{"qa_id": "RAG-PER-001"}`),不新增字段 | 保持原始编号,不重新编号 |
|
||||
| `问题` | `KnowledgeMeta.title` | |
|
||||
| `相似问法`(`\|` 分隔) | `FaqSynonym.phrase`(每个变体一行) | 去空、去重 |
|
||||
| `回答` | `KnowledgeMeta.content_text` | 原文照抄,不改写 |
|
||||
| `phase`固定 `phase_1`、`source_version`固定 `v5.8` | `KnowledgeMeta.tags`(JSON) | |
|
||||
| 状态 | `KnowledgeMeta.review_status = 'published'`、`FaqSynonym.status = 'approved'`、`FaqSynonym.source_type = 'import'` | 因为源文件本身已是 `approved_candidate`;`source_type='import'` 是 `docs/02` §7.3 DDL 里已经定义好的合法枚举值 |
|
||||
| 分类 | `KnowledgeMeta.milvus_collection = 'fin_faq_collection'`(全部,本轮不拆分) | 见第 2 节 |
|
||||
| (源文件无对应字段) | `KnowledgeMeta.knowledge_type`、`KnowledgeMeta.status`、`KnowledgeMeta.created_at`、`KnowledgeMeta.updated_at` | **这些列在 DDL 里都是 `NOT NULL` 且无默认值,导入脚本必须显式赋值**,不能依赖数据库默认 |
|
||||
|
||||
**`FaqSynonym` 侧还有三个非空列必须显式赋值,原稿遗漏了**:`docs/02` §7.3 的 `agent_faq_synonym` DDL 里
|
||||
`created_by BIGINT UNSIGNED NOT NULL`,另有可空的 `reviewer_id`/`reviewed_at`。由于本轮是脚本导入而非人工操作,
|
||||
`created_by` 需要一个**明确的"系统导入"账号 ID**,不能凭空填 0 或 NULL:
|
||||
|
||||
- **该列带外键约束**:`CONSTRAINT fk_synonym_created_by FOREIGN KEY (created_by) REFERENCES sys_user(id)`
|
||||
(`reviewer_id` 同样有 `fk_synonym_reviewer`)。所以填的必须是一个**真实存在的 `sys_user.id`**,
|
||||
否则导入会在数据库层直接失败。实施时使用一个已存在的运维/管理员账号 ID,或按项目既有做法新增一个专用系统导入账号;
|
||||
具体取值在实施计划里作为显式前置步骤确认,不在本文档臆断。
|
||||
- `reviewer_id`/`reviewed_at` 建议一并填入同一账号与导入时间——它们可空,但填上可以让"这批同义词是谁在什么时候
|
||||
导入的"可追溯,符合源文件本身已经过审核的事实。
|
||||
- `normalized_phrase` 与 `phrase_hash` 按 `docs/02` §7.3 已有明文约定实现:**`phrase_hash` 由应用对完整
|
||||
`normalized_phrase` 计算 SHA-256**(目的是避免索引仅用 191 前缀时把不同长文本误判为重复),
|
||||
不自行发明算法。去重依据是唯一索引 `uk_faq_synonym (knowledge_id, phrase_hash)`,
|
||||
所以 §4.1 里"相似问法去重"应落在 `normalized_phrase` 归一化之后做。
|
||||
- `status` 有 CHECK 约束 `IN ('pending','approved','disabled','archived')`,本设计用的 `'approved'` 合法。
|
||||
|
||||
### 4.2 导入校验规则(不通过则整体不导入)
|
||||
|
||||
- 记录数必须等于源文件声明的 105 条;
|
||||
- `qa_id` 不重复;
|
||||
- 每条必须有问题、至少一个相似问法、完整答案;
|
||||
- 答案文本与源文件逐字一致(防止转录出错)。
|
||||
|
||||
### 4.3 实现形式
|
||||
|
||||
一次性管理脚本(例如 `tools/import_knowledge_seed.py`),而非常驻的 HTTP 写接口——因为本轮明确不做知识管理后台。脚本可重复执行(先清空本次导入批次再重新写入,或按 `qa_id` upsert),便于源文件升级到 v5.9 等后续版本时重新运行。
|
||||
|
||||
### 4.4 导入后的向量同步
|
||||
|
||||
复用第 5 节的写路径:导入脚本对每条新写入/更新的记录,在同一批次结束后,写入 `domain_event_outbox`(`event_type="knowledge.vector_sync_requested"`),交给 Worker 异步生成向量并写入 Milvus,导入脚本本身不直接调用 Milvus 写入。
|
||||
|
||||
## 5. 写路径(生成向量并同步 Milvus)
|
||||
|
||||
### 5.0 建立 Milvus 集合(两份来源文档都未覆盖的空白,本节补齐)
|
||||
|
||||
《15-知识检索接入方案.md》和本文档早期版本都只把"Milvus 三集合已建"当作前置条件,没有说明谁建、怎么建。
|
||||
本节补上这一步,作为写路径里实际最先执行的动作。
|
||||
|
||||
**集合结构**(`fin_faq_collection`/`fin_product_collection`/`fin_policy_collection` 三个集合结构相同,仅名字不同):
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `knowledge_id` | VARCHAR(64),主键 | 对应 `KnowledgeMeta.id` 的字符串形式,与读路径 `KnowledgeHit.knowledge_id: str` 类型一致 |
|
||||
| `title` | VARCHAR(256) | 对应问题标题 |
|
||||
| `snippet` | VARCHAR(4000) | 对应标准答案全文(这批 QA 答案长度都在几百字以内,4000 字节留足余量) |
|
||||
| `tags` | VARCHAR(512) | 逗号拼接的标签字符串(不用 Milvus JSON 字段类型,避免不同 pymilvus 版本对 JSON 字段支持不一致的风险) |
|
||||
| `version` | VARCHAR(16) | 对应源版本号,如 `v5.8` |
|
||||
| `embedding` | FLOAT_VECTOR(1024) | text-embedding-v3 输出向量 |
|
||||
|
||||
索引:向量字段使用 `AUTOINDEX`,`metric_type="COSINE"`——必须和读路径适配器 `search()` 时用的 `metric_type` 一致,
|
||||
否则检索结果不可信。数据量小(105 条起步),不需要额外调优索引参数。
|
||||
|
||||
**建立方式**:新增一次性脚本 `tools/setup_milvus_knowledge_collections.py`,对三个集合分别执行"若不存在则创建
|
||||
(`has_collection` 判断,幂等)+ 创建向量索引 + `load()` 载入内存"。此脚本与 `alembic upgrade head` 是并列的
|
||||
独立环境初始化步骤,不通过 Alembic 管理(Milvus 不是关系型数据库,没有对应的迁移框架)。每个环境(本地/测试/生产)
|
||||
部署时都需要单独执行一次。
|
||||
|
||||
执行顺序建议:`tools/audit_schema.py`(确认现库与基线一致,不再需要为 `milvus_collection` 做字段核实)→
|
||||
`python tools/setup_milvus_knowledge_collections.py` → `python tools/import_knowledge_seed.py`(第 4 节的一次性导入,
|
||||
导入后触发第 5.3 节的向量同步)。
|
||||
|
||||
### 5.1 Embedding 出口
|
||||
|
||||
采纳《15-知识检索接入方案.md》步骤 1 的设计:给 `ModelGateway` 协议新增 `embed()` 方法,`ModelGenerationService` 新增对应封装方法。模型端点为阿里云 DashScope `text-embedding-v3`(1024 维),通过 `ModelEndpointConfig` 新增一条 `capabilities=["embedding"]` 的记录,密钥走 `secret_ref`。
|
||||
|
||||
**这条 `ModelEndpointConfig` 记录不需要新写代码创建**:平台已有通用管理端资源接口
|
||||
(`app/api/controllers/admin.py` 已注册 `model-endpoints` 资源类型),用管理员账号调用现有接口新增即可。
|
||||
新增时除了 `base_url`/`model_name`/`secret_ref`/`capabilities` 外,`EndpointPayload` 还要求
|
||||
`allowed_data_levels`(数据级别清单,需要和现有其他端点记录的取值约定保持一致,不要凭空发明新值)和
|
||||
`context_window`(正整数,DashScope `text-embedding-v3` 的单次输入 Token 上限,需查官方文档确认具体数值)两个必填字段,
|
||||
实施时不要漏填。
|
||||
|
||||
DashScope OpenAI 兼容接口地址为 `https://dashscope.aliyuncs.com/compatible-mode/v1`,`secret_ref` 格式固定为
|
||||
`env:XXX`(大写字母/数字/下划线),对应 `.env` 里新增一行真实 Key(不提交 git),`.env.example` 只放变量名占位。
|
||||
本次确认可以复用团队已有的 DashScope 账号 Key,不需要新申请,但需要确认该账号的额度/计费方式能覆盖本项目的调用量。
|
||||
|
||||
**补充一处读路径文档未展开的实现细节**:`ModelGenerationService.embed()` 委托给 `self.dispatch.embed(...)`,
|
||||
意味着 `ModelDispatchService`(`app/service/model_gateway.py` 现有类)也需要新增 `embed()` 方法,
|
||||
镜像其 `generate()` 方法已有的"按顺序尝试多个已批准端点、全部失败才报错"的重试逻辑,读路径文档的代码片段里没有
|
||||
单独给出这个方法体,实施时需要照 `generate()` 的模式补上,而不是漏掉。
|
||||
|
||||
**从现有代码读出的三处实现约束(原稿未覆盖,实施时容易踩)**:
|
||||
|
||||
1. **路由必须按能力筛选,否则会把 embedding 请求打到聊天端点上**。`ModelRouterService.select_endpoint()`/
|
||||
`select_with_fallback()` 支持 `required_capability` 参数(`app/service/model_router_service.py` 第 19/34 行),
|
||||
现有 `generate()` 链路依赖它做能力筛选。新增的 embedding 链路解析端点时必须传
|
||||
`required_capability="embedding"`,与新端点记录 `capabilities=["embedding"]` 对应。
|
||||
2. **HTTP 路径是 `/embeddings`,不是复用 `/chat/completions`**。`OpenAICompatibleGateway.generate()` 内部写死
|
||||
`endpoint.base_url.rstrip("/") + "/chat/completions"`(`model_gateway.py` 第 56 行);embedding 需要新增一条
|
||||
请求路径为 `/embeddings`、请求体为 `{"model": ..., "input": ...}`、响应从 `body["data"][0]["embedding"]`
|
||||
取向量的实现。DashScope 的 `compatible-mode/v1` 同时提供这两条路径,所以 `base_url` 可以沿用同一套约定。
|
||||
3. **`DatabaseModelGateway` 也需要一个 `embed()` 镜像**:它现在从 `ModelEndpointConfig` 动态取端点、再构造
|
||||
`OpenAICompatibleGateway`(`model_gateway.py` 第 83-99 行),embedding 要走同一条"从库取配置"的路径,
|
||||
不能另起一套端点解析逻辑。
|
||||
|
||||
`secret_ref` 校验沿用 `EnvironmentSecretResolver.resolve()` 的既有约定:**必须以 `env:` 开头**
|
||||
(`model_gateway.py` 第 27-28 行硬校验),否则直接抛 `RecoverableAgentError`;变量名从环境读取,缺失同样报错。
|
||||
|
||||
### 5.2 写路径 Milvus 适配器(与读路径隔离)
|
||||
|
||||
新文件 `app/infrastructure/milvus_knowledge_writer.py`,只提供:
|
||||
|
||||
```python
|
||||
class MilvusKnowledgeWriter:
|
||||
async def upsert(self, *, collection: str, knowledge_id: str,
|
||||
vector: list[float], fields: dict[str, Any]) -> None: ...
|
||||
async def delete(self, *, collection: str, knowledge_id: str) -> None: ...
|
||||
```
|
||||
|
||||
与《15-知识检索接入方案.md》里"只读"的 `MilvusKnowledgeClient` 是两个不同的类、不同的文件,检索路径永远不持有写权限的客户端实例。
|
||||
|
||||
### 5.3 Outbox Worker Handler
|
||||
|
||||
复用已有的通用 `domain_event_outbox` + `OutboxWorker`(`app/worker/outbox_worker.py`),不新建专用表。新增两个事件类型的 handler:
|
||||
|
||||
- `knowledge.vector_sync_requested`:按 `knowledge_id` 重新从 MySQL 读取最新已发布内容 → 调用 `embed()` → 校验向量维度等于 1024(不等则失败,交给 Outbox 重试机制,不静默降级)→ `MilvusKnowledgeWriter.upsert()`。
|
||||
- `knowledge.vector_delete_requested`:知识下线(`review_status` 转为 `archived`)时触发,调用 `MilvusKnowledgeWriter.delete()`。即使删除失败也不影响安全性——读路径的 MySQL 二次校验(见第 6 节)会挡住已下线内容,这是纵深防御,不是唯一防线。
|
||||
|
||||
失败重试完全复用 `OutboxWorker` 已有的指数退避、5 次上限、死信标记机制,不新写重试逻辑。
|
||||
|
||||
## 6. 读路径(检索)
|
||||
|
||||
完全采纳《15-知识检索接入方案.md》第 3-8 步的设计,不重新设计,仅摘要关键约束供本文档自成一体地被理解:
|
||||
|
||||
- 三集合按意图路由:`faq→fin_faq_collection(top_k=3)`、`product_inquiry→fin_product_collection(top_k=5)`、`policy_explain→fin_policy_collection(top_k=5)`(本轮只有第一个集合有数据)。
|
||||
- 集合白名单强校验,非白名单集合名一律拒绝。
|
||||
- Milvus 命中后必须回查 `fin_knowledge_meta`,确认 `review_status='published' AND status='active'` 且在有效期内,防止已下线知识被检索到。
|
||||
- Milvus 不可用时降级为 MySQL `content_text LIKE` 关键词检索,降级结果标记 `degraded=True`,同样执行"已发布+有效期"过滤。
|
||||
- Embedding 维度与集合定义不一致时**失败关闭**,不得静默返回空结果或错误结果。
|
||||
- 公共只读工具 `query_knowledge` 注册进 `bootstrap.py`,`allowed_roles` 含 `customer`(区别于 `query_financial_data` 不含 customer)。
|
||||
- `KnowledgeReferenceService.resolve()` 改为 HMAC 签名 token 校验 + 真实查询 `fin_knowledge_meta`,返回脱敏元数据、不返回全文。
|
||||
|
||||
## 7. 测试计划
|
||||
|
||||
在《15-知识检索接入方案.md》第 8 步既有测试计划基础上,补充导入相关测试:
|
||||
|
||||
| 类型 | 覆盖 |
|
||||
|---|---|
|
||||
| 单元 | 导入校验:105 条数量、编号唯一、字段完整、答案与源文件一致;破坏其中一项应导致整体导入失败 |
|
||||
| 单元 | `embed()` 返回维度不等于 1024 时失败关闭 |
|
||||
| 集成(真实 MySQL) | 导入后能查到 105 条 `review_status='published'` 记录;`agent_faq_synonym` 条数等于所有相似问法之和 |
|
||||
| 集成 | Outbox handler:向量生成失败时重试,达到上限进入死信,不阻塞已发布记录本身的可读性 |
|
||||
| (沿用读路径文档)| 意图路由集合与 TopK 正确;非法集合名拒绝;降级仍执行有效期过滤;跨已下线内容不可检索 |
|
||||
|
||||
## 8. 本轮讨论中的方向修正记录
|
||||
|
||||
为避免后续协作者困惑,记录一次被推翻的设计方向:讨论初期曾计划新建完整的知识库在线审核后台(草稿→审核→发布状态机、管理端 HTTP 接口、创建人不得审核自己等),对齐 `config_release_service` 的模式。读到胜宇提供的《客服Agent一期向量化前流程文档_v1.md》后确认:一期知识内容已经是审核完毕的静态文件(v5.8,105 条),系统只需一次性结构化导入,不需要在线管理后台。该方向已放弃,本文档第 4 节的"一次性导入脚本"取代了原计划。
|
||||
|
||||
## 9. 开工前必须确认(沿用读路径文档 + 本轮新增)
|
||||
|
||||
1. 可用的 `text-embedding-v3` 端点(`base_url`/`model_name`/`secret_ref`),维度确认为 1024。
|
||||
2. Milvus 三集合的建立已在第 5.0 节纳入本方案范围(`tools/setup_milvus_knowledge_collections.py`),不再是外部前置条件;
|
||||
但需要和胜宇那边确认他是否已经自行建过同名集合,避免重复建或字段结构对不上(例如他本地测试环境可能已建了一份不同字段的
|
||||
`fin_faq_collection`,需要先核实、必要时统一)。
|
||||
3. `pymilvus` 异步客户端可用性确认(决定读写/建集合三类操作的具体实现方式,AsyncMilvusClient 不可用则需用
|
||||
`anyio.to_thread.run_sync` 包装同步客户端)。
|
||||
**已核实(2026-09-10,见第 10 节):目标环境 `pymilvus 2.6.17` 提供 `AsyncMilvusClient`,且
|
||||
`has_collection`/`create_collection`/`create_index`/`load_collection`/`upsert`/`delete`/`search` 全部具备。
|
||||
因此三类操作直接用原生异步客户端实现,`anyio.to_thread` 包装方案**不需要**。**
|
||||
4. 源文件后续升级(如 v5.9)时,重新运行导入脚本的责任人和时机需要约定。
|
||||
5. 导入脚本写入 `agent_faq_synonym.created_by` 所使用的那一个真实 `sys_user.id`(外键约束要求存在),
|
||||
由实施计划显式确认,见 §4.1。
|
||||
|
||||
## 10. 交接后的核实记录(2026-09-10,接手方补充)
|
||||
|
||||
本轮接手时对照代码与文档逐条核实了第 9 节的外部事实,结论如下(**只记录已复核的事实,未复核的保持"待确认"**):
|
||||
|
||||
| # | 事项 | 结论 | 依据 |
|
||||
|---|---|---|---|
|
||||
| 1 | `fin_knowledge_meta.milvus_collection` 是否已存在 | **已存在于基线,无需新增字段与迁移**;且为 `NOT NULL` 无默认值 | `docs/00` §6.2 第 871 行无 🆕 标记(同表其它 7 字段均有);`alembic/baseline_generated.sql` 生成的 DDL 含该列并通过 `tools/audit_schema.py` 审计 |
|
||||
| 2 | `fin_knowledge_meta.version`/`effective_date`/`expire_date` | 均已存在于生成 DDL,无需新增 | 同上 |
|
||||
| 3 | `pymilvus` 异步客户端 | **可用**,原生异步实现,无需线程包装 | 目标环境 `pymilvus 2.6.17` 实测 `AsyncMilvusClient` 及其 7 个所需方法 |
|
||||
| 4 | `agent_faq_synonym` 导入所需的非空列 | `created_by` 非空**且有外键指向 `sys_user(id)`**;`phrase_hash` = SHA-256(`normalized_phrase`);`status` 有 CHECK 约束 | `docs/02` §7.3 DDL 与第 355 行说明 |
|
||||
| 5 | 现库是否与基线一致 | **待确认**——需环境(MySQL)可用后跑 `tools/audit_schema.py`,属常规环境一致性校验 | — |
|
||||
| 6 | Milvus 三个集合是否已被胜宇建过 | **待确认**——需 Milvus 可用后查现有集合名与字段结构 | — |
|
||||
| 7 | DashScope Key 额度/计费覆盖本项目调用量 | **待确认**(用户提供 Key 与额度信息后核实) | — |
|
||||
| 8 | `text-embedding-v3` 端点的 `context_window` 具体取值 | **待确认**——创建 `ModelEndpointConfig` 记录前须查阿里云官方文档确定 | [阿里云 text-embedding 同步接口文档](https://www.alibabacloud.com/help/tc/model-studio/text-embedding-synchronous-api#2) |
|
||||
|
||||
Reference in New Issue
Block a user