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
@@ -1,74 +0,0 @@
from typing import Any
from app.core.errors import ForbiddenAgentError, RecoverableAgentError
from app.core.knowledge_contracts import ALLOWED_KNOWLEDGE_COLLECTIONS
class MilvusKnowledgeClient:
def __init__(self, uri: str, token: str | None = None) -> None:
self._uri = uri
self._token = token
self._client: Any | None = None
async def _ensure_client(self) -> Any:
if self._client is None:
from pymilvus import AsyncMilvusClient # type: ignore[import-untyped]
self._client = AsyncMilvusClient(uri=self._uri, token=self._token)
return self._client
async def search(
self, collection: str, vector: list[float], top_k: int
) -> list[dict[str, Any]]:
if collection not in ALLOWED_KNOWLEDGE_COLLECTIONS:
raise ForbiddenAgentError("未授权的知识集合")
if len(vector) != 1024 or not 1 <= top_k <= 20:
raise RecoverableAgentError("知识检索参数无效")
try:
client = await self._ensure_client()
# Lite 重启后集合默认未加载;远程 Milvus 对重复加载保持幂等。
load_collection = getattr(client, "load_collection", None)
if load_collection is not None:
await load_collection(collection_name=collection)
batches = await client.search(
collection_name=collection,
data=[vector],
limit=top_k,
output_fields=["knowledge_id", "title", "snippet", "tags", "version"],
search_params={"metric_type": "COSINE"},
)
except Exception as exc:
raise RecoverableAgentError("知识检索不可用") from exc
return [
normalized
for batch in batches
for hit in batch
if (normalized := self._normalize_hit(hit)) is not None
]
@staticmethod
def _normalize_hit(hit: Any) -> dict[str, Any] | None:
"""统一 Milvus SDK 的平铺与 entity 包装命中格式。"""
raw = dict(hit)
entity = raw.get("entity")
fields = entity if isinstance(entity, dict) else raw
knowledge_id = fields.get("knowledge_id")
snippet = fields.get("snippet")
score = raw.get("score", raw.get("distance", fields.get("score")))
if (
not isinstance(knowledge_id, str)
or not isinstance(snippet, str)
or not isinstance(score, (int, float))
or isinstance(score, bool)
):
return None
normalized: dict[str, Any] = {
"knowledge_id": knowledge_id,
"snippet": snippet,
"score": float(score),
}
for field in ("title", "tags", "version"):
value = fields.get(field)
if value is not None:
normalized[field] = value
return normalized