Files
ai-ticket-platform/backend/apps/knowledge/tests/test_retrieval.py
2026-07-27 19:01:19 +08:00

207 lines
7.1 KiB
Python

from apps.knowledge.models import KnowledgeBase, KnowledgeChunk, KnowledgeItem
from apps.knowledge.services import rebuild_item_chunks, retrieve_knowledge
from apps.modelhub.crypto import encrypt_secret
from apps.modelhub.models import ModelDeployment, ModelProvider
from apps.solutions.models import Solution
def test_retrieve_knowledge_prefers_relevant_chunk(db):
base = KnowledgeBase.objects.create(name="采集知识库", scope="数据采集")
item = KnowledgeItem.objects.create(
knowledge_base=base,
title="税局采集进度",
problem="采集进度不更新",
solution="查询 Python 日志,确认税局状态。",
status="published",
)
KnowledgeChunk.objects.create(
knowledge_base=base,
item=item,
chunk_index=0,
content="税局采集进度不更新时,需要查询 Python 日志。",
embedding=[0.1, 0.2],
metadata={"source_type": "knowledge_item", "title": item.title},
)
KnowledgeChunk.objects.create(
knowledge_base=base,
chunk_index=1,
content="登录失败通常需要检查账号密码。",
embedding=[0.9, 0.1],
metadata={"source_type": "document", "title": "登录问题"},
)
results = retrieve_knowledge("税局采集进度怎么查询", limit=1)
assert len(results) == 1
assert results[0]["title"] == "税局采集进度"
assert "Python 日志" in results[0]["content"]
def test_published_solution_can_be_indexed_for_retrieval(db):
solution = Solution.objects.create(
title="税局不稳定采集处理中",
problem_signature="采集进度长时间不更新",
root_cause="税局服务不稳定",
solution="回复客户税局不稳定,任务仍在采集中。",
status="published",
)
from apps.knowledge.services import index_published_solution
chunks = index_published_solution(solution)
assert len(chunks) == 1
assert chunks[0].metadata["source_type"] == "solution"
assert chunks[0].metadata["solution_id"] == solution.id
def test_hybrid_retrieval_uses_configured_embedding_and_exposes_score_audit(
db,
monkeypatch,
):
provider = ModelProvider.objects.create(
name="向量渠道",
base_url="https://models.example.com/v1",
api_key_encrypted=encrypt_secret("secret"),
)
deployment = ModelDeployment.objects.create(
provider=provider,
display_name="知识向量模型",
model_name="embedding-model",
capability="embedding",
)
vector = [0.0] * 1536
vector[0] = 1.0
calls = []
def request_embeddings(selected, contents, dimensions):
calls.append((selected.id, contents, dimensions))
return [vector[:] for _ in contents]
monkeypatch.setattr(
"apps.knowledge.embeddings._request_embeddings",
request_embeddings,
)
base = KnowledgeBase.objects.create(name="正式知识库")
item = KnowledgeItem.objects.create(
knowledge_base=base,
title="订单超时",
problem="订单接口返回 TIMEOUT-504",
solution="检查下游连接池",
status="published",
)
chunks = rebuild_item_chunks(item)
results = retrieve_knowledge("订单 TIMEOUT-504", limit=1, published_only=True)
assert chunks[0].embedding_model == "embedding-model"
assert chunks[0].embedding_vector is not None
assert results[0]["metadata"]["retrieval"]["query_embedding_model"] == "embedding-model"
assert results[0]["metadata"]["retrieval"]["query_embedding_deployment_id"] == deployment.id
assert results[0]["metadata"]["retrieval"]["query_embedding_fallback"] is False
assert results[0]["metadata"]["retrieval"]["channels"] == [
"portable_text",
"portable_vector",
]
assert calls[0][2] == 1536
def test_configured_rerank_model_controls_final_order_and_is_audited(db, monkeypatch):
provider = ModelProvider.objects.create(
name="重排渠道",
base_url="https://models.example.com/v1",
api_key_encrypted=encrypt_secret("secret"),
)
deployment = ModelDeployment.objects.create(
provider=provider,
display_name="知识重排模型",
model_name="rerank-model",
capability="rerank",
)
base = KnowledgeBase.objects.create(name="重排知识库")
first = KnowledgeItem.objects.create(
knowledge_base=base,
title="订单超时基础方案",
problem="订单 TIMEOUT-504",
solution="检查连接池",
status="published",
)
preferred = KnowledgeItem.objects.create(
knowledge_base=base,
title="订单超时权威方案",
problem="订单 TIMEOUT-504",
solution="按权威步骤恢复连接池",
status="published",
)
rebuild_item_chunks(first)
rebuild_item_chunks(preferred)
def rerank(selected, query, candidates):
assert selected.id == deployment.id
return {
item["id"]: 1.0 if item["title"] == preferred.title else 0.0
for item in candidates
}
monkeypatch.setattr("apps.knowledge.reranking._request_rerank", rerank)
results = retrieve_knowledge("订单 TIMEOUT-504", limit=2, published_only=True)
assert results[0]["metadata"]["item_id"] == preferred.id
audit = results[0]["metadata"]["retrieval"]["rerank"]
assert audit["status"] == "succeeded"
assert audit["model_name"] == "rerank-model"
assert audit["deployment_id"] == deployment.id
assert audit["model_weight"] == 0.6
def test_configured_rerank_failure_falls_back_to_weighted_order_and_is_audited(
db,
monkeypatch,
):
provider = ModelProvider.objects.create(
name="异常重排渠道",
base_url="https://models.example.com/v1",
api_key_encrypted=encrypt_secret("secret"),
)
deployment = ModelDeployment.objects.create(
provider=provider,
display_name="异常知识重排模型",
model_name="broken-rerank-model",
capability="rerank",
)
base = KnowledgeBase.objects.create(name="重排降级知识库")
relevant = KnowledgeItem.objects.create(
knowledge_base=base,
title="订单 TIMEOUT-504 处理",
problem="订单 TIMEOUT-504",
solution="检查连接池",
status="published",
)
unrelated = KnowledgeItem.objects.create(
knowledge_base=base,
title="账号登录失败",
problem="账号密码错误",
solution="重置密码",
status="published",
)
rebuild_item_chunks(relevant)
rebuild_item_chunks(unrelated)
def failed_rerank(*_args, **_kwargs):
raise TimeoutError("rerank unavailable")
monkeypatch.setattr(
"apps.knowledge.reranking._request_rerank",
failed_rerank,
)
results = retrieve_knowledge("订单 TIMEOUT-504", limit=2, published_only=True)
assert results[0]["metadata"]["item_id"] == relevant.id
audit = results[0]["metadata"]["retrieval"]["rerank"]
assert audit["status"] == "failed_fallback"
assert audit["model_name"] == "broken-rerank-model"
assert audit["deployment_id"] == deployment.id
assert audit["model_weight"] == 0.0