From 8e6da99dcb35353afc4d1f9aea7ff590cb02f69d Mon Sep 17 00:00:00 2001 From: Nelson <1475262689@qq.com> Date: Mon, 3 Aug 2026 17:42:17 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=8F=90=E5=8D=87=E5=B9=B6=E5=8F=91?= =?UTF-8?q?=E5=AE=B9=E9=87=8F=E9=BB=98=E8=AE=A4=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../apps/admin-web/src/App.vue | 6 ++ .../admin-web/src/config/systemSettings.ts | 26 ++++-- .../apps/admin-web/src/styles.css | 8 ++ .../versions/0028_chat_concurrency_default.py | 84 +++++++++++++++++++ .../apps/backend/app/core/config.py | 6 +- .../tests/test_production_readiness.py | 9 ++ 6 files changed, 130 insertions(+), 9 deletions(-) create mode 100644 ai_knowledge_base_v2/apps/backend/alembic/versions/0028_chat_concurrency_default.py diff --git a/ai_knowledge_base_v2/apps/admin-web/src/App.vue b/ai_knowledge_base_v2/apps/admin-web/src/App.vue index 43ca050..1b90f9d 100644 --- a/ai_knowledge_base_v2/apps/admin-web/src/App.vue +++ b/ai_knowledge_base_v2/apps/admin-web/src/App.vue @@ -1067,6 +1067,12 @@ async function clearFeishuCache() { v-model="systemSettingValues[setting.key]" /> {{ setting.description }} + + {{ setting.valueHint(systemSettingValues[setting.key]) }} + diff --git a/ai_knowledge_base_v2/apps/admin-web/src/config/systemSettings.ts b/ai_knowledge_base_v2/apps/admin-web/src/config/systemSettings.ts index 5a92434..9c1958c 100644 --- a/ai_knowledge_base_v2/apps/admin-web/src/config/systemSettings.ts +++ b/ai_knowledge_base_v2/apps/admin-web/src/config/systemSettings.ts @@ -16,6 +16,7 @@ export interface SystemSettingDefinition { placeholder?: string; options?: SystemSettingOption[]; description: string; + valueHint?: (value: SystemSettingValue) => string; } export interface SystemSettingSection { @@ -181,28 +182,29 @@ export const systemSettingSections: SystemSettingSection[] = [ key: "chat_max_active_requests", label: "问答最大并发数", type: "number", - defaultValue: 2, + defaultValue: 30, min: 1, max: 1000, - description: "同一后端进程内同时进入飞书和模型生成链路的最大请求数。", + description: "用户正式问答的全局执行上限;Redis 可用时由所有后端实例共享,超过后进入排队。", + valueHint: concurrentCapacityHint, }, { key: "chat_max_queue_size", label: "问答最大排队数", type: "number", - defaultValue: 20, + defaultValue: 90, min: 0, max: 10000, - description: "超过最大并发后允许等待的请求数量,超过后直接提示稍后再试。", + description: "超过最大并发后允许等待的请求数量。默认按并发数的 3 倍预留,避免课程结束后的集中提问被直接拒绝。", }, { key: "chat_queue_timeout_seconds", label: "问答排队超时(秒)", type: "number", - defaultValue: 60, + defaultValue: 90, min: 1, max: 3600, - description: "请求在排队中超过该时间后自动结束并提示用户稍后再试。", + description: "请求在排队中超过该时间后自动结束。默认 90 秒可覆盖 30 并发下约三轮请求释放。", }, { key: "chat_active_lease_seconds", @@ -292,4 +294,16 @@ export const systemSettingSections: SystemSettingSection[] = [ }, ]; +function concurrentCapacityHint(value: SystemSettingValue) { + const concurrency = Math.max(1, Number(value) || 1); + const lower = roundDownToTen((concurrency * 120) / 25 / 1.4); + const upper = roundDownToTen((concurrency * 120) / 25 / 1.2); + const capacity = lower === upper ? `约 ${lower} 人` : `约 ${lower}–${upper} 人`; + return `容量估算:按单次问答约 25 秒并预留 20%–40% 波动,当前 ${concurrency} 并发可承接 2 分钟内${capacity}集中提问。实际能力还受模型服务商额度和回答时长影响。`; +} + +function roundDownToTen(value: number) { + return Math.max(1, Math.floor(value / 10) * 10); +} + export const systemSettingDefinitions = systemSettingSections.flatMap((section) => section.settings); diff --git a/ai_knowledge_base_v2/apps/admin-web/src/styles.css b/ai_knowledge_base_v2/apps/admin-web/src/styles.css index 829aa09..056ddf5 100644 --- a/ai_knowledge_base_v2/apps/admin-web/src/styles.css +++ b/ai_knowledge_base_v2/apps/admin-web/src/styles.css @@ -1725,6 +1725,14 @@ textarea { line-height: 1.5; } +.setting-field > .setting-value-hint { + padding: 9px 11px; + border: 1px solid #cde6dc; + border-radius: 8px; + background: #f3faf7; + color: #246b52; +} + .setting-field .el-input-number, .setting-field .el-input, .setting-field .el-select { diff --git a/ai_knowledge_base_v2/apps/backend/alembic/versions/0028_chat_concurrency_default.py b/ai_knowledge_base_v2/apps/backend/alembic/versions/0028_chat_concurrency_default.py new file mode 100644 index 0000000..2060d86 --- /dev/null +++ b/ai_knowledge_base_v2/apps/backend/alembic/versions/0028_chat_concurrency_default.py @@ -0,0 +1,84 @@ +"""raise the default chat concurrency to thirty + +Revision ID: 0028_chat_concurrency +Revises: 0027_content_generation +""" + +from __future__ import annotations + +from alembic import op +import sqlalchemy as sa + + +revision = "0028_chat_concurrency" +down_revision = "0027_content_generation" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + config = sa.table( + "sys_system_config", + sa.column("config_key", sa.String(length=100)), + sa.column("config_value", sa.Text()), + sa.column("description", sa.String(length=255)), + ) + connection = op.get_bind() + _set_default( + connection, + config, + key="chat_max_active_requests", + old_default="2", + new_default="30", + description="用户正式问答的全局执行上限;Redis 可用时由所有后端实例共享,超过后进入排队。", + ) + _set_default( + connection, + config, + key="chat_max_queue_size", + old_default="20", + new_default="90", + description="超过最大并发后允许等待的请求数量。默认按并发数的 3 倍预留。", + ) + _set_default( + connection, + config, + key="chat_queue_timeout_seconds", + old_default="60", + new_default="90", + description="请求在排队中超过该时间后自动结束。默认可覆盖约三轮请求释放。", + ) + + +def downgrade() -> None: + # 运行时配置可能已经被管理员调整,降级代码版本时不覆盖现场值。 + pass + + +def _set_default( + connection, + config, + *, + key: str, + old_default: str, + new_default: str, + description: str, +) -> None: + current = connection.execute( + sa.select(config.c.config_value).where(config.c.config_key == key) + ).scalar_one_or_none() + if current is None: + connection.execute( + config.insert().values( + config_key=key, + config_value=new_default, + description=description, + ) + ) + return + if str(current).strip() == old_default: + connection.execute( + config.update() + .where(config.c.config_key == key) + .values(config_value=new_default, description=description) + ) diff --git a/ai_knowledge_base_v2/apps/backend/app/core/config.py b/ai_knowledge_base_v2/apps/backend/app/core/config.py index 5fd8425..8c5f4d4 100644 --- a/ai_knowledge_base_v2/apps/backend/app/core/config.py +++ b/ai_knowledge_base_v2/apps/backend/app/core/config.py @@ -59,9 +59,9 @@ class Settings(BaseSettings): default_daily_chat_limit: int = 100 default_user_name_prefix: str = "用户" - chat_max_active_requests: int = 2 - chat_max_queue_size: int = 20 - chat_queue_timeout_seconds: int = 60 + chat_max_active_requests: int = 30 + chat_max_queue_size: int = 90 + chat_queue_timeout_seconds: int = 90 chat_active_lease_seconds: int = 900 periodic_report_worker_enabled: bool = True periodic_report_poll_seconds: int = 5 diff --git a/ai_knowledge_base_v2/apps/backend/tests/test_production_readiness.py b/ai_knowledge_base_v2/apps/backend/tests/test_production_readiness.py index 3b4bbc9..b8fd344 100644 --- a/ai_knowledge_base_v2/apps/backend/tests/test_production_readiness.py +++ b/ai_knowledge_base_v2/apps/backend/tests/test_production_readiness.py @@ -27,6 +27,7 @@ from app.services.security_state_service import SecurityStateService from app.services.auth_service import AuthService from app.services.model_service import ModelClientService from app.core.observability import RequestObservabilityMiddleware +from app.core.config import Settings @pytest.fixture(autouse=True) @@ -43,6 +44,14 @@ def test_secret_is_encrypted_and_masked(): assert SecretService.masked(encrypted) == MASKED_SECRET +def test_production_chat_capacity_defaults_cover_expected_burst(): + settings = Settings(_env_file=None) + + assert settings.chat_max_active_requests == 30 + assert settings.chat_max_queue_size == 90 + assert settings.chat_queue_timeout_seconds == 90 + + def test_migration_revision_ids_fit_default_alembic_version_column(): versions_dir = Path(__file__).parents[1] / "alembic" / "versions" revision_pattern = re.compile(r'^revision\s*=\s*["\']([^"\']+)', re.MULTILINE)