24 KiB
设计文档:客服知识检索基础设施(子项目 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 的字段状态已经核实完毕:它已存在于不可变基线中,本设计不新增该字段,也不需要新增迁移。
核实链路(可复现,不依赖任何一方的"没提到"这类间接证据):
docs/00-新数据库基线设计.md§6.2fin_knowledge_meta字段表(第 871 行):| milvus_collection | VARCHAR(64) | 非空 | Milvus 集合名 |——未标 🆕,而同表其它 7 个字段 (effective_date/expire_date/content_text/tags/reviewer_id/review_status等)都带 🆕 标记。 这个对照本身就是"它是基线自带、不是新增字段"的直接证据。docs/02-数据库建表设计.md的 ALTER TABLE 清单没提它,不构成矛盾——该清单只列"需要在基线上补充的字段", 基线已有字段本就不该出现在里面。此前把"没提到"当成矛盾是误读,此处更正。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)
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() 的模式补上,而不是漏掉。
从现有代码读出的三处实现约束(原稿未覆盖,实施时容易踩):
- 路由必须按能力筛选,否则会把 embedding 请求打到聊天端点上。
ModelRouterService.select_endpoint()/select_with_fallback()支持required_capability参数(app/service/model_router_service.py第 19/34 行), 现有generate()链路依赖它做能力筛选。新增的 embedding 链路解析端点时必须传required_capability="embedding",与新端点记录capabilities=["embedding"]对应。 - 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可以沿用同一套约定。 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,只提供:
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. 开工前必须确认(沿用读路径文档 + 本轮新增)
- 可用的
text-embedding-v3端点(base_url/model_name/secret_ref),维度确认为 1024。 - Milvus 三集合的建立已在第 5.0 节纳入本方案范围(
tools/setup_milvus_knowledge_collections.py),不再是外部前置条件; 但需要和胜宇那边确认他是否已经自行建过同名集合,避免重复建或字段结构对不上(例如他本地测试环境可能已建了一份不同字段的fin_faq_collection,需要先核实、必要时统一)。 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包装方案不需要**。**- 源文件后续升级(如 v5.9)时,重新运行导入脚本的责任人和时机需要约定。
- 导入脚本写入
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 同步接口文档 |