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