feat: add governed knowledge lifecycle
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
from apps.knowledge.models import KnowledgeBase, KnowledgeChunk, KnowledgeItem
|
||||
from apps.knowledge.services import retrieve_knowledge
|
||||
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
|
||||
|
||||
|
||||
@@ -51,3 +53,154 @@ def test_published_solution_can_be_indexed_for_retrieval(db):
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user