chore: 清理违反底座规则的死代码并修正接口文档编号

- 删除生产死代码 app/service/knowledge_tool_service.py 与
  app/infrastructure/milvus_knowledge_adapter.py:后者硬编码 Milvus 字段名,
  违反 AGENTS.md §E,且仅被前者引用;生产检索链路实际走
  knowledge_search_tool -> KnowledgeSearchService -> knowledge_schema 运行时探测。
- 删除上述两模块的单测,以及依赖 legacy 位置参数构造的
  tests/unit/service/test_knowledge_retrieval.py。
- app/service/knowledge_retrieval_service.py 整文件回退底座版本,
  移除 legacy 双构造与重复检索实现。
- docs/05-接口文档.md:客服画像候选改登记为 §8.5,恢复 §8.2 解析知识引用;
  既有 §8.1-§8.4 编号全部保持,修复此前出现两个 8.3 的问题。
- app/model/profile.py:current_customer_id 改为普通可空列映射,与
  alembic/baseline_generated.sql 及真实库一致;原 Computed 声明会让 ORM 把该列
  从 INSERT 中排除,与「必须显式写入」的实际 schema 不符。
- 新增 docs/客服Agent接入底座扩展说明_v1.md,供集成分支评审逐项确认。

验证:pytest tests/unit tests/contract -> 1275 passed, 2 skipped, 0 failed;
ruff check app tests tools alembic 通过;mypy app 通过(244 个源文件)。
This commit is contained in:
张胜宇
2026-09-12 11:15:24 +08:00
parent e85989b344
commit 9aaacc242f
9 changed files with 189 additions and 483 deletions
+22 -20
View File
@@ -616,26 +616,7 @@ Authorization: Bearer <token>
记忆提取没有客户端写接口。`memory.extraction_requested` 由 `complete_run()` 与最终结果在同一事务写入 Outbox,再由 Worker 调用内部 `MemoryService`。更正、遗忘和监管删除属于独立隐私流程,本接口不临时复用 `memory_conflict`。
### 8.2 客服画像候选(Phase 2)
客服 Agent 不读取或直接修改正式画像。已登录用户明确陈述长期偏好、约束或目标时,系统
异步生成 `memory_unit.status='candidate'` 候选;访客不会生成候选。候选不进入客服召回,
必须经过用户确认和管理员审核后才能晋升为 `active`。
```text
GET /api/v1/users/me/memory-candidates
POST /api/v1/users/me/memory-candidates/{candidate_id}/decisions
GET /api/v1/admin/customer-profile-candidates
POST /api/v1/admin/customer-profile-candidates/{candidate_id}/reviews
```
用户确认请求体为 `{ "decision": "confirmed" | "rejected" }`,需要
`memory:candidate:confirm`;确认只将状态改为 `verified`。管理员审核请求体复用
`ReviewPayload`,需要管理员角色和 `memory:candidate:review`;`approved` 会在事务内
处理同键旧记忆冲突并将候选改为 `active`,`rejected` 将其改为 `rejected`。接口只返回
结构化候选值,不返回对话证据摘录、密码、验证码或其他原始敏感内容。
### 8.3 解析知识引用
### 8.2 解析知识引用
```http
GET /api/v1/knowledge-references/{reference_token}
@@ -772,6 +753,27 @@ DELETE /api/v1/knowledge/{knowledge_id}
**不返回 `real_name`、`birth_date`、`mobile_masked`、`trade_account` 等 PII**;
`assessment_expired` 按**当前时间**重算,不采信快照里的历史布尔值。
### 8.5 客服画像候选(Phase 2)
> 编号说明:本节为客服二期新增,**不占用 §8.1–§8.4 既有号段**,以避免破坏 `AGENTS.md`、`docs/09`、`docs/14` 对「§8.3 知识库管理三端点」「§8.4 公共只读工具索引」的既有引用。
客服 Agent 不读取或直接修改正式画像。已登录用户明确陈述长期偏好、约束或目标时,系统
异步生成 `memory_unit.status='candidate'` 候选;访客不会生成候选。候选不进入客服召回,
必须经过用户确认和管理员审核后才能晋升为 `active`。
```text
GET /api/v1/users/me/memory-candidates
POST /api/v1/users/me/memory-candidates/{candidate_id}/decisions
GET /api/v1/admin/customer-profile-candidates
POST /api/v1/admin/customer-profile-candidates/{candidate_id}/reviews
```
用户确认请求体为 `{ "decision": "confirmed" | "rejected" }`,需要
`memory:candidate:confirm`;确认只将状态改为 `verified`。管理员审核请求体复用
`ReviewPayload`,需要管理员角色和 `memory:candidate:review`;`approved` 会在事务内
处理同键旧记忆冲突并将候选改为 `active`,`rejected` 将其改为 `rejected`。接口只返回
结构化候选值,不返回对话证据摘录、密码、验证码或其他原始敏感内容。
## 9. 平台管理面接口
管理面只操作草稿、审核、激活、停用、回滚和归档流程,不提供绕过版本控制的通用 CRUD。所有更新和状态转换都需要 `If-Match`;创建、审核、激活、回滚和停用需要 `Idempotency-Key`。
@@ -0,0 +1,154 @@
# 客服 Agent 接入底座扩展说明(v1)
> 用途:提交 `qyqy_develop`(集成分支)评审时使用。
> 本文档逐项列出「客服 Agent + RAG」能力为接入底座而**触碰的既有文件**、改动性质、合规依据,
> 以及**明确未改动**的底座边界,便于架构师逐条确认。
>
> 依据:`AGENTS.md`、`docs/00`、`docs/01`、`docs/05`、`docs/09`、`docs/14`、`docs/20`。
---
## 一、总体口径
- 本次接入遵循 `AGENTS.md` 规则 7:客服 Agent 继承公共 `BaseAgent`、由 `AgentFactory` 创建,
**不绕过**公共鉴权、记忆、模型路由、工具、合规、审计与事件流程。
- 数据库侧**未做任何基线改动**:没有新增/重命名/删除表,没有改字段类型、可空性或业务含义,
没有新增迁移脚本。
- 底座中所有「既有 Agent 默认行为」均通过**带默认值的声明位**扩展,默认值等于改动前行为,
其他 Agent(`FundQueryDemoAgent`、`RiskAgent`、`PlatformProbeAgent`)路径不受影响。
---
## 二、A 类:纯新增,不影响既有代码
| 文件 | 说明 |
|---|---|
| `app/core/conversation_privacy.py` | 客服会话落库前的凭据最小化(密码/验证码/证件号/银行卡/手机号 → 占位符) |
| `app/api/controllers/visitor_tokens.py`、`app/api/schemas/visitor_tokens.py` | 访客短时令牌签发接口 |
| `app/service/agent/customer_service_routing.py` | 客服确定性意图路由(安全/账户/人工/合规/闲聊/公开知识) |
| `app/service/customer_service_session_memory_service.py` | 客服 Redis 短期会话记忆(30 分钟滑动 TTL、24 小时绝对上限、16 条/约 4096 Token 截断) |
| `app/service/customer_service_handover_context.py`、`customer_service_handover_admin_service.py` | 转人工上下文构造与管理员只读工单服务 |
| `app/service/customer_profile_candidate_service.py` | 画像候选(Phase 2,异步、需用户确认 + 管理员审核) |
| `app/service/knowledge_authority.py`、`knowledge_config.py`、`knowledge_publication_service.py` | 知识权威回查、运行期配置、受控发布工具所依赖的服务 |
| `app/infrastructure/milvus_profile_projection.py`、`neo4j_profile_projection.py` | 画像投影适配器(异步派生写入) |
| `app/worker/memory_sync_outbox_worker.py`、`customer_profile_candidate_worker.py` | 画像投影 Outbox 消费者、候选画像 Worker |
| `app/static/index.html` | 客服联调测试页(`/customer-service-test`) |
| `tools/publish_customer_service_knowledge.py`、`verify_customer_service_phase1.py`、`knowledge_import_preflight.py` | 受控发布、只读门禁、导入预检 |
以上均为新增文件,不改变任何既有模块行为。
---
## 三、B 类:带默认值、默认行为不变的扩展(对齐底座扩展点,建议直接确认)
| 文件 | 改动 | 默认值是否等于原行为 |
|---|---|---|
| `app/core/contracts.py` | `AgentDefinition` 新增 `requires_model_intent_classification=True`、`recalls_customer_memory=True`;`AgentRequestMetadata` 新增 `chitchat_streak`/`clarification_round`/`session_context`;`CoreResult` 新增 `clarification_required` | 是 |
| `app/service/agent/base.py` | `recall_memory()` 在声明关闭或访客时置空;`classify_intent()` 在声明关闭时返回 `None` | 是 |
| `app/service/agent/factory.py` | 仅在 `requires_model_intent_classification` 为真时绑定意图分类器 | 是 |
| `app/core/config.py` | 新增 `visitor_token_ttl_seconds`、`milvus_local_uri`、`knowledge_embedding_endpoint_code`、`knowledge_embedding_timeout_ms` 与 `resolved_milvus_uri` 属性 | 是(新增项均有默认值) |
| `app/core/knowledge_contracts.py` | 新增 `ALLOWED_KNOWLEDGE_COLLECTIONS` 别名(与 `ALLOWED_COLLECTIONS` 恒等);`KnowledgeHit.answer` 可选字段;`KnowledgeSearchResult.hits` 补默认值 | 是(三个 `fin_*_collection` 白名单内容未变) |
| `app/model/knowledge.py` | 新增 `FinKnowledgeMeta = KnowledgeMeta` 别名 | 是 |
| `app/model/memory.py` | `__all__` 与 `ProfileSnapshot` 兼容转出 | 是 |
| `app/service/agent/bootstrap.py` | 新增注册 `query_knowledge` 工具,`handler` 复用 `knowledge_search_tool`(与 `search_knowledge` 同一实现),`required_permission="knowledge:query"` | 是(不新增能力面,实际范围仍由 `config_release` 白名单收口) |
| `app/service/memory_service.py` | `upsert_memory()` 新增 `status` 参数(`active`/`candidate`),`candidate` 不覆盖现有有效记忆 | 是(默认 `active`) |
| `app/service/public_platform_service.py` | 转人工工单 `reason_detail` 落库前做凭据脱敏 | 是(安全收紧) |
| `app/main.py` | 注册 `visitor_tokens_router`、挂载 `/customer-service-test` 静态页 | 是(纯新增装配) |
| `pyproject.toml`、`requirements.txt` | 新增 `milvus-lite>=3.2,<4` | 是(可选本地开发依赖) |
| `.gitignore` | 新增 `.worktrees/`、`data/milvus/` | 是 |
---
## 四、C 类:需要架构师确认的底座语义扩展
### C1. 访客身份(`app/api/dependencies/auth.py` + `app/core/security.py`)
- `security.py`:`JwtAuthenticator.authenticate()` 新增分支——当令牌含 `visitor: true` 声明时,
返回 `roles=("visitor",)`、`permissions=("agent:run", "knowledge:query")`、`data_scope="public"`。
- `auth.py`:`build_request_context()` 对 `visitor` 角色跳过 `IdentityService().resolve()`
(访客不在 `sys_user` 中,无法解析身份)。
**合规说明:**
- `visitor` 角色只可能出自用项目 RS256 私钥签名、且显式带 `visitor: true` 的令牌,普通用户令牌
无法携带该角色;
- 访客权限固定为 `agent:run` + `knowledge:query`,`data_scope="public"`,
**不包含任何账户、持仓、订单、银行卡、投诉进度或画像权限**;
- 该分支只影响访客令牌,非访客令牌仍走完整 `IdentityService` 身份解析。
**请求确认:** 是否接受在底座统一鉴权链路中增加这一受控访客身份;如需要,我们可补一份
`docs/` 说明或在 `docs/05` 登记该令牌类型。
### C2. 客服 Agent 的角色与安全路由(`app/service/agent/implementations/customer_service.py`)
- `allowed_roles` 由 `("customer",)` 扩为 `("visitor", "customer")`;
- `handle()` 首部插入确定性安全路由 `route_message()`(安全提示、合规拒答、账户入口、人工转接优先于检索);
- 新增 `recalls_customer_memory=False`(客服不隐式召回长期/画像记忆);
- 新增 `chitchat_streak == 4` 的一次性业务引导;
- 业务口径:`COMPANY` 由 `南方科技` 改为 `奶龙基金责任有限公司`。
**请求确认:** 角色扩容与行为契约变更(安全路由前置、闲聊计数);品牌名属业务口径调整。
### C3. 画像快照 ORM 声明清理与 `current_customer_id` 映射更正
三处相关改动(**属于修既有缺陷,不是改设计**):
1. `app/model/risk_questionnaire.py`:原有内联 `ProfileSnapshot` 声明与 `app/model/profile.py`
的同表声明**重复**,两个 declarative 类映射同一张表会让 SQLAlchemy 直接拒绝导入。
现改为从 `app.model.profile` 转出规范类,保留对既有调用方的兼容导出。
2. `app/model/profile.py`:把 `current_customer_id` 映射为**普通可空列**。
3. 依据:该列在**真实库中是普通可空列**(`EXTRA=''`、`GENERATION_EXPRESSION=''`,已实测),
`alembic/baseline_generated.sql` 的建表语句也没有 `GENERATED` 子句,
`tools/seed_profile_demo.py` 明确写着「**不是生成列**,必须显式写入」。
因此**不能**声明 `Computed(...)`——那会让 SQLAlchemy 把它从 INSERT 中排除,反而永远写不进去。
4. **附带发现(建议底座侧修正):** `docs/00-新数据库基线设计.md` 第 783 行把该列描述为
「生成列」,与 `docs/02`/`alembic` 基线 DDL 及真实库不一致。本次未改动 `docs/00`,
仅在此登记,请底座侧决定以哪一侧为准。
**请求确认:** 上述三点是否按「修缺陷」接受。
### C4. Milvus 双地址与部署约束(`app/core/config.py` + `app/service/health_service.py`)
- 新增 `milvus_local_uri`(默认空串)与 `resolved_milvus_uri = milvus_local_uri or milvus_uri`;
- `health_service.py` 的健康检查改用 `resolved_milvus_uri`。
**约束(请一并确认并写入环境规范):**
- 团队/生产环境的 `.env` **必须留空 `MILVUS_LOCAL_URI`**,否则健康检查与部分检索链路会指向
本地 Milvus Lite 文件,出现「健康检查正常、实际查的是另一个库」的隐性偏差;
- `.env` 中默认值为空串,**默认行为与改动前完全一致**。
---
## 五、D 类:本次已从 ZSY 侧移除、不进入集成分支的内容
为避免把违反底座规则的内容带进集成分支,以下内容已在本分支删除:
| 内容 | 原因 |
|---|---|
| `app/service/knowledge_tool_service.py` | **生产死代码**(全仓唯一引用是它自己的单测)。生产路径是 `bootstrap.get_knowledge_search_service()` → `KnowledgeSearchService` |
| `app/infrastructure/milvus_knowledge_adapter.py` | **违反 `AGENTS.md` §E**:硬编码 `knowledge_id`/`snippet`/`tags` 等 Milvus 字段名,会打挂字段名不同的环境。且仅被上面那个死模块引用 |
| `tests/unit/service/test_knowledge_tool_service.py`、`tests/unit/infrastructure/test_milvus_knowledge_adapter.py` | 上述两模块的单测 |
| `tests/unit/service/test_knowledge_retrieval.py` | 依赖已移除的 legacy 位置参数构造 |
| `app/service/knowledge_retrieval_service.py` 的 legacy 双构造与 `_legacy_search` | 已整文件回退为底座版本,消除「同一服务两套构造语义 + 两套检索实现」的技术债 |
检索仍统一走底座既有链路:`ToolExecutor` → `knowledge_search_tool` → `KnowledgeSearchService`
→ `app/core/knowledge_schema.py` 的 `detect_schema()` **运行时字段探测**,无任何硬编码字段名。
---
## 六、E 类:明确未改动的底座边界
- `docs/00`、`docs/02` 的表结构与字段语义:**未改动**;
- `alembic/` 迁移脚本:**未改动**;
- Milvus 字段名:**无硬编码**,全部走运行时探测;
- `docs/05-接口文档.md` 的既有章节编号:**§8.1–§8.4 全部保持原编号**,客服二期新增的
「客服画像候选」登记为 **§8.5**(不占用既有号段),另有 §9.7 追加;原有交叉引用
「§8.3 知识库管理三端点」「§8.4 公共只读工具索引」仍然成立;
- 其他业务 Agent、场外/推广/风控域:**未改动**行为契约。
---
## 七、F 类:需要业务方在环境侧确认的既有约束(非本次引入)
1. `config_release` 是**环境数据、不随代码合并**:换环境需重新发布客服工具白名单。
2. Milvus 集合 schema 因环境而异,检索层已运行时探测,**任何新增代码都不得硬编码字段名**。
3. 本地 Milvus Lite 仅用于本地开发;团队环境应使用受管 Milvus 并留空 `MILVUS_LOCAL_URI`。