产出 docs/NL_develop交付说明-评审意见.md(可直接转给组员),以及一个只读探查工具 tools/probe_knowledge_collections.py(Milvus 集合 schema 与行数)。 核查中发现的硬矛盾,都有可复核的证据: 1. 配置库不是同一个。对方说 active=216、合并前生效版本是 186;我这边实测 active=201, 最近 5 条 id 就是 201/198/197/196/195 —— release.id 自增,同库不可能一边 216 一边 201。 所以他以为已发布的 customer_service:faq=[search_knowledge, query_customer_profile] 在这边并不存在,而他的画像出口复用的正是这个 key ⇒ 合并后被 ToolExecutor 失败关闭。 同理他说的 fund_query_demo:fund_quote 缺失,我这边是存在的。 2. Milvus 也不是同一个。对方说现库字段是 knowledge_id/snippet、无 visibility、行数 106/177/73;我这边实测是 doc_id/content/chapter/section/.../visibility 共 15 个字段、 行数 125/297/214,且与 knowledge_search_service.py 的 OUTPUT_FIELDS 逐字一致。 他这次的字段映射改动(doc_id→knowledge_id)在这边会直接报 field knowledge_id not exist,把客服知识检索整条打挂 —— 比他自述的"关闭 visibility 隔离"严重得多。 建议不是 A/B/C 三选一,而是第四种:运行时探测字段名,两套 schema 都能跑。 3. 解释器不同。他用 .venv(项目里不存在,那是他机器上的 gitignore 目录),约定是 D:\conda\envs\jr_py313。所以"mypy 151→181"跑不到本基线 —— 这边是 138 文件 0 错。 另指出:docs/26-JWT密钥管理与轮换.md 会与已存在的 docs/21-JWT密钥管理与轮换.md 重复, 建议并入 21;驳回删除 docs/04/06/10/13/99。 四项待裁决的答复:同意 agent_type 方案(要求补审计);字段映射改为运行时探测; 驳回删除编号文档;26 并入 21。
80 lines
2.7 KiB
Python
80 lines
2.7 KiB
Python
"""只读探查:Milvus 知识集合的真实 schema(用于评审 NL_develop 的字段映射改动)。
|
||
|
||
只做 describe / query,不写入。结果写 `docs/evidence/knowledge-collections.json`:
|
||
|
||
python tools/probe_knowledge_collections.py
|
||
|
||
要回答的问题(对应交付说明 §4.2):
|
||
1. 现库集合的**实际字段名**是什么 —— 是 `doc_id`/`content`/`visibility` 那套,
|
||
还是 `knowledge_id`/`snippet` 那套?他选的字段映射(A 方案)建立在这个前提上;
|
||
2. 每个集合有多少行;
|
||
3. 有没有 `visibility` 字段 —— 没有的话,检索层的内部资料硬隔离就是关着的。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
from pathlib import Path
|
||
from typing import Any
|
||
|
||
from app.core.config import get_settings
|
||
|
||
OUTPUT = Path("docs/evidence/knowledge-collections.json")
|
||
|
||
|
||
def _client() -> Any:
|
||
from pymilvus import MilvusClient # type: ignore[import-untyped]
|
||
|
||
settings = get_settings()
|
||
return MilvusClient(uri=settings.milvus_uri, token=settings.milvus_token or None)
|
||
|
||
|
||
def collect() -> dict[str, Any]:
|
||
client = _client()
|
||
report: dict[str, Any] = {"collections": []}
|
||
for name in sorted(client.list_collections()):
|
||
try:
|
||
described = client.describe_collection(name)
|
||
fields = [
|
||
{
|
||
"name": str(field.get("name")),
|
||
"type": str(field.get("type")),
|
||
"is_primary": bool(field.get("is_primary", False)),
|
||
}
|
||
for field in described.get("fields", [])
|
||
]
|
||
stats = client.get_collection_stats(name)
|
||
row_count = stats.get("row_count") if isinstance(stats, dict) else None
|
||
except Exception as exc: # 单个集合探测失败不影响其余
|
||
report["collections"].append(
|
||
{"collection": name, "error": f"{type(exc).__name__}: {exc}"}
|
||
)
|
||
continue
|
||
report["collections"].append(
|
||
{
|
||
"collection": name,
|
||
"row_count": row_count,
|
||
"field_names": [field["name"] for field in fields],
|
||
"fields": fields,
|
||
"has_visibility": any(field["name"] == "visibility" for field in fields),
|
||
"looks_like_legacy_schema": any(
|
||
field["name"] in {"doc_id", "chapter", "doc_no"} for field in fields
|
||
),
|
||
}
|
||
)
|
||
return report
|
||
|
||
|
||
def main() -> None:
|
||
report = collect()
|
||
OUTPUT.parent.mkdir(parents=True, exist_ok=True)
|
||
OUTPUT.write_text(
|
||
json.dumps(report, ensure_ascii=False, indent=2, default=str),
|
||
encoding="utf-8",
|
||
)
|
||
print(f"wrote {OUTPUT}")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|