From 77a899e24ca360f6aa940b6702ddb80d06875148 Mon Sep 17 00:00:00 2001 From: Nelson <1475262689@qq.com> Date: Thu, 30 Jul 2026 14:36:09 +0800 Subject: [PATCH] fix: align agent no-hit handling --- .../src/components/AgentManagementView.vue | 2 +- .../app/services/agent_debug_service.py | 6 +- .../backend/app/services/model_service.py | 2 +- .../app/services/model_stream_service.py | 26 ++------ .../tests/test_agent_runtime_config.py | 60 +++++++++++++++++++ 5 files changed, 71 insertions(+), 25 deletions(-) diff --git a/ai_knowledge_base_v2/apps/admin-web/src/components/AgentManagementView.vue b/ai_knowledge_base_v2/apps/admin-web/src/components/AgentManagementView.vue index 766eeeb..9552500 100644 --- a/ai_knowledge_base_v2/apps/admin-web/src/components/AgentManagementView.vue +++ b/ai_knowledge_base_v2/apps/admin-web/src/components/AgentManagementView.vue @@ -74,7 +74,7 @@ const selectedModelName = computed(() => { return model?.displayName || model?.modelName || "未选择模型"; }); const selectedKnowledgeSummary = computed(() => { - if (!agentForm.knowledgeIds.length) return "未选择知识库"; + if (!agentForm.knowledgeIds.length) return "默认使用全部正式开放知识库"; if (agentForm.knowledgeIds.length === 1) { return knowledge.value.find((item) => item.id === agentForm.knowledgeIds[0])?.name || "1 个知识库"; } diff --git a/ai_knowledge_base_v2/apps/backend/app/services/agent_debug_service.py b/ai_knowledge_base_v2/apps/backend/app/services/agent_debug_service.py index 31c2cb1..b823702 100644 --- a/ai_knowledge_base_v2/apps/backend/app/services/agent_debug_service.py +++ b/ai_knowledge_base_v2/apps/backend/app/services/agent_debug_service.py @@ -23,12 +23,14 @@ class AgentDebugService: SimpleNamespace(id=index + 1, role=item.role, content=item.content) for index, item in enumerate(payload.history) ] + preview_knowledge_ids = payload.knowledgeIds or None + version_overrides = payload.knowledgeVersions or None rag_result = await KnowledgeAgentService.build_result( db, question=payload.question, history=history, - version_overrides=payload.knowledgeVersions, - preview_knowledge_ids=payload.knowledgeIds, + version_overrides=version_overrides, + preview_knowledge_ids=preview_knowledge_ids, prompt_override=payload.promptContent, response_depth=payload.responseDepth, ) diff --git a/ai_knowledge_base_v2/apps/backend/app/services/model_service.py b/ai_knowledge_base_v2/apps/backend/app/services/model_service.py index 5a409bc..0853224 100644 --- a/ai_knowledge_base_v2/apps/backend/app/services/model_service.py +++ b/ai_knowledge_base_v2/apps/backend/app/services/model_service.py @@ -38,7 +38,7 @@ class ModelClientService: if model is None: raise ExternalServiceError("未启用可用模型,请先在模型管理中启用一个模型。", provider="model") model_name = model.model_name - answer = _call_configured_model(model, rag_result, allow_no_hit=rag_result.allow_general_knowledge) + answer = _call_configured_model(model, rag_result, allow_no_hit=True) return ModelCompletion( answer=answer, model_id=model.id if model is not None else None, diff --git a/ai_knowledge_base_v2/apps/backend/app/services/model_stream_service.py b/ai_knowledge_base_v2/apps/backend/app/services/model_stream_service.py index 927b412..4f3c18d 100644 --- a/ai_knowledge_base_v2/apps/backend/app/services/model_stream_service.py +++ b/ai_knowledge_base_v2/apps/backend/app/services/model_stream_service.py @@ -30,7 +30,7 @@ from app.services.model_service import ( _system_and_turn_messages, _system_config_bool, ) -from app.services.rag_service import NO_HIT_ANSWER, RagResult +from app.services.rag_service import RagResult @dataclass(frozen=True) @@ -64,14 +64,6 @@ class ModelStreamService: chunks=_display_chunks(model, _mock_answer(rag_result)), ) - if not rag_result.is_hit and not rag_result.allow_general_knowledge: - return StreamingModelResponse( - model_id=model.id if model is not None else None, - model_name=model.model_name if model is not None else "no-hit", - input_token=_rough_token_count(rag_result.prompt), - chunks=_display_chunks(model, NO_HIT_ANSWER), - ) - if model is None: raise ExternalServiceError("未启用可用模型,请先在模型管理中启用一个模型。", provider="model") if not (model.api_url or model.base_url) or not model.api_key: @@ -98,14 +90,6 @@ class ModelStreamService: chunks=_async_display_chunks(model, _mock_answer(rag_result)), ) - if not rag_result.is_hit and not rag_result.allow_general_knowledge: - return AsyncStreamingModelResponse( - model_id=model.id if model is not None else None, - model_name=model.model_name if model is not None else "no-hit", - input_token=_rough_token_count(rag_result.prompt), - chunks=_async_display_chunks(model, NO_HIT_ANSWER), - ) - if model is None: raise ExternalServiceError("未启用可用模型,请先在模型管理中启用一个模型。", provider="model") if not (model.api_url or model.base_url) or not model.api_key: @@ -147,19 +131,19 @@ def _get_enabled_model(db: Session) -> ModelConfig | None: def _stream_configured_model(model: ModelConfig, rag_result: RagResult) -> Iterator[str]: api_type = model.api_type or "openai_compatible" if model.stream_enabled != 1: - return iter((_call_configured_model(model, rag_result, allow_no_hit=rag_result.allow_general_knowledge),)) + return iter((_call_configured_model(model, rag_result, allow_no_hit=True),)) if api_type == "anthropic_messages": return _stream_anthropic_messages(model, rag_result) if api_type == "openai_compatible": return _stream_openai_compatible_model(model, rag_result) - return iter((_call_configured_model(model, rag_result, allow_no_hit=rag_result.allow_general_knowledge),)) + return iter((_call_configured_model(model, rag_result, allow_no_hit=True),)) async def _stream_configured_model_async(model: ModelConfig, rag_result: RagResult) -> AsyncIterator[str]: api_type = model.api_type or "openai_compatible" if model.stream_enabled != 1: answer = await asyncio.to_thread( - _call_configured_model, model, rag_result, allow_no_hit=rag_result.allow_general_knowledge + _call_configured_model, model, rag_result, allow_no_hit=True ) yield answer return @@ -175,7 +159,7 @@ async def _stream_configured_model_async(model: ModelConfig, rag_result: RagResu return answer = await asyncio.to_thread( - _call_configured_model, model, rag_result, allow_no_hit=rag_result.allow_general_knowledge + _call_configured_model, model, rag_result, allow_no_hit=True ) yield answer diff --git a/ai_knowledge_base_v2/apps/backend/tests/test_agent_runtime_config.py b/ai_knowledge_base_v2/apps/backend/tests/test_agent_runtime_config.py index ac678ec..0070fd6 100644 --- a/ai_knowledge_base_v2/apps/backend/tests/test_agent_runtime_config.py +++ b/ai_knowledge_base_v2/apps/backend/tests/test_agent_runtime_config.py @@ -16,6 +16,7 @@ from app.schemas.admin import AgentDebugRequest, AgentRuntimeConfigSaveRequest from app.services.agent_debug_service import AgentDebugService from app.services.model_stream_service import ( AsyncStreamingModelResponse, + ModelStreamService, _openai_stream_payload, _stream_configured_model_async, ) @@ -194,12 +195,71 @@ def test_debug_preview_passes_conversation_history_and_replaces_saved_prompt(): kwargs = build_result.await_args.kwargs assert [item.content for item in kwargs["history"]] == ["最开始的问题", "第一次回答"] + assert kwargs["preview_knowledge_ids"] is None + assert kwargs["version_overrides"] is None assert kwargs["prompt_override"] == "调试主提示词" assert kwargs["response_depth"] == 35 assert result.messages == rag_result.messages assert result.prompt == "调试提示词渲染结果" +def test_agent_debug_empty_knowledge_selection_uses_formal_open_catalog(): + payload = AgentDebugRequest( + promptContent="调试主提示词", + modelId=1, + knowledgeIds=[], + knowledgeVersions={}, + question="测试问题", + ) + rag_result = RagResult( + question="测试问题", + knowledge_scopes=[], + chunks=[], + prompt="测试", + allow_general_knowledge=True, + ) + + with _database() as db: + build_result = AsyncMock(return_value=rag_result) + with patch("app.services.agent_debug_service.KnowledgeAgentService.build_result", build_result): + asyncio.run(AgentDebugService.build_result(db, payload)) + + kwargs = build_result.await_args.kwargs + assert kwargs["preview_knowledge_ids"] is None + assert kwargs["version_overrides"] is None + + +def test_user_stream_no_hit_still_calls_model_for_cautious_answer(): + async def configured_chunks(_model, _rag_result): + yield "我暂时没有可靠课程依据,但可以先帮你整理需要确认的点。" + + async def collect(): + with _database() as db: + model = _model() + db.add_all([ + model, + SystemConfig(config_key="mock_model_enabled", config_value="false"), + ]) + db.commit() + rag_result = RagResult( + question="课程里的特殊练习怎么做?", + knowledge_scopes=[], + chunks=[], + prompt="本轮没有可靠的正式知识章节。", + allow_general_knowledge=False, + ) + with patch( + "app.services.model_stream_service._stream_configured_model_async", + configured_chunks, + ): + response = ModelStreamService.stream_async(db, rag_result) + return [chunk async for chunk in response.chunks] + + chunks = asyncio.run(collect()) + + assert chunks == ["我暂时没有可靠课程依据,但可以先帮你整理需要确认的点。"] + + def test_prompt_includes_response_depth_instruction(): with _database() as db: db.add(SystemConfig(config_key="agent_response_depth", config_value="20"))