From 53ff15344395fd8c37875f35110589af2a514cac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?MSI-7950X=5C=E5=88=98=E6=B3=BD=E6=98=8E?= Date: Tue, 19 May 2026 10:57:29 +0800 Subject: [PATCH] =?UTF-8?q?llm=E6=A8=A1=E5=9E=8B=E6=8E=A5=E5=85=A5?= =?UTF-8?q?=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/agentscope_integration/factory.py | 124 +++++++++++++++- backend/modules/memory/manager.py | 91 +++++++++--- backend/modules/model_provider/router.py | 135 ++++++++++++++++++ backend/modules/monitor/router.py | 10 +- .../src/views/flow/node-configs/LlmConfig.vue | 14 +- frontend/src/views/settings/Settings.vue | 115 +++++++-------- 6 files changed, 395 insertions(+), 94 deletions(-) diff --git a/backend/agentscope_integration/factory.py b/backend/agentscope_integration/factory.py index bb96c79..79ba50b 100644 --- a/backend/agentscope_integration/factory.py +++ b/backend/agentscope_integration/factory.py @@ -3,6 +3,7 @@ 提供统一的智能体创建接口,根据用户类型(员工/管理者/任务/文档)创建对应的 AI 智能体实例。 支持智能体缓存以减少重复创建的开销。 """ +from sqlalchemy import select from agentscope.agent import AgentBase from agentscope.agent._react_agent import ReActAgent from agentscope.model import OpenAIChatModel @@ -10,6 +11,8 @@ from agentscope.formatter import OpenAIChatFormatter from agentscope.tool import Toolkit from agentscope.message import Msg from config import settings +from models import ModelInstance, ModelProvider +from database import get_db from .memory.user_memory import UserIsolatedMemory from .hooks.rbac_hook import register_rbac_hooks_for_user @@ -26,19 +29,126 @@ class AgentFactory: _MAX_CACHE_SIZE = 50 # 智能体缓存上限 @classmethod - def _get_model(cls) -> OpenAIChatModel: + async def _get_llm_config(cls, db, model_instance_id=None): + """从数据库获取 LLM 配置信息。 + + 根据提供的 model_instance_id 查询对应的模型实例和供应商配置, + 如果未提供或未找到,则回退到默认模型(is_default=True)。 + 最终如果都未找到,则使用 settings 中的默认配置。 + + Args: + db: 数据库会话对象。 + model_instance_id: 可选的模型实例 ID,用于指定特定模型。 + + Returns: + dict: 包含 LLM 配置的字典: + - model_name (str): 模型名称 + - api_key (str): API 密钥 + - base_url (str): API 基础地址 + - default_params (dict): 默认参数(temperature, max_tokens 等) + """ + try: + if model_instance_id: + stmt = select(ModelInstance).where( + ModelInstance.id == model_instance_id, + ModelInstance.is_active == True + ) + result = await db.execute(stmt) + model_instance = result.scalar_one_or_none() + + if model_instance: + stmt_provider = select(ModelProvider).where( + ModelProvider.id == model_instance.provider_id, + ModelProvider.is_active == True + ) + result_provider = await db.execute(stmt_provider) + provider = result_provider.scalar_one_or_none() + + if provider: + return { + "model_name": model_instance.model_name, + "api_key": provider.api_key or settings.LLM_API_KEY, + "base_url": provider.base_url or settings.LLM_API_BASE, + "default_params": model_instance.default_params or {}, + } + + stmt_default = select(ModelInstance).where( + ModelInstance.is_default == True, + ModelInstance.is_active == True, + ModelInstance.model_type == "llm" + ).limit(1) + result_default = await db.execute(stmt_default) + default_instance = result_default.scalar_one_or_none() + + if default_instance: + stmt_provider = select(ModelProvider).where( + ModelProvider.id == default_instance.provider_id, + ModelProvider.is_active == True + ) + result_provider = await db.execute(stmt_provider) + provider = result_provider.scalar_one_or_none() + + if provider: + return { + "model_name": default_instance.model_name, + "api_key": provider.api_key or settings.LLM_API_KEY, + "base_url": provider.base_url or settings.LLM_API_BASE, + "default_params": default_instance.default_params or {}, + } + except Exception as e: + pass + + return { + "model_name": settings.LLM_MODEL, + "api_key": settings.LLM_API_KEY, + "base_url": settings.LLM_API_BASE, + "default_params": {}, + } + + @classmethod + def _get_model(cls, llm_config: dict = None) -> OpenAIChatModel: """获取或创建全局共享的大语言模型实例。 + 支持通过 llm_config 参数动态指定模型配置,如果未提供则使用默认配置。 + + Args: + llm_config: 可选的 LLM 配置字典,包含: + - model_name (str): 模型名称 + - api_key (str): API 密钥 + - base_url (str): API 基础地址 + - default_params (dict): 默认参数(temperature, max_tokens 等) + Returns: OpenAIChatModel: 配置好的大语言模型实例。 """ if cls._model is None: - cls._model = OpenAIChatModel( - config_name="enterprise_model", - model_name=settings.LLM_MODEL, - api_key=settings.LLM_API_KEY, - api_base=settings.LLM_API_BASE, - ) + if llm_config: + model_name = llm_config.get("model_name", settings.LLM_MODEL) + api_key = llm_config.get("api_key", settings.LLM_API_KEY) + base_url = llm_config.get("base_url", settings.LLM_API_BASE) + default_params = llm_config.get("default_params", {}) + + model_kwargs = { + "config_name": "enterprise_model", + "model_name": model_name, + "api_key": api_key, + "api_base": base_url, + } + + if default_params: + if "temperature" in default_params: + model_kwargs["temperature"] = default_params["temperature"] + if "max_tokens" in default_params: + model_kwargs["max_tokens"] = default_params["max_tokens"] + + cls._model = OpenAIChatModel(**model_kwargs) + else: + cls._model = OpenAIChatModel( + config_name="enterprise_model", + model_name=settings.LLM_MODEL, + api_key=settings.LLM_API_KEY, + api_base=settings.LLM_API_BASE, + ) return cls._model @classmethod diff --git a/backend/modules/memory/manager.py b/backend/modules/memory/manager.py index 74e69cf..bc75c86 100644 --- a/backend/modules/memory/manager.py +++ b/backend/modules/memory/manager.py @@ -18,9 +18,10 @@ from datetime import datetime, timezone from typing import Callable from redis.asyncio import Redis -from sqlalchemy import text +from sqlalchemy import text, select from sqlalchemy.ext.asyncio import AsyncSession from config import settings +from models import ModelInstance, ModelProvider logger = logging.getLogger(__name__) @@ -65,6 +66,62 @@ class MemoryManager: self.db_factory = db_factory self.redis = redis self._extract_tasks: dict[str, asyncio.Task] = {} # 后台提取任务追踪 + self._llm_config_cache: dict | None = None # LLM 配置缓存 + + async def _get_llm_config(self) -> dict: + """从 model_instances 表获取 LLM 配置,带缓存。 + + 优先级:数据库默认 LLM → 环境变量回退。 + 缓存有效期:5分钟(避免每次调用都查库)。 + + Returns: + dict: {model_name, api_key, base_url} + """ + if self._llm_config_cache is not None: + return self._llm_config_cache + try: + db = self.db_factory() + result = await db.execute( + select(ModelInstance, ModelProvider) + .join(ModelProvider, ModelInstance.provider_id == ModelProvider.id) + .where(ModelInstance.model_type == 'llm') + .where(ModelInstance.is_default == True) + .where(ModelInstance.is_active == True) + .limit(1) + ) + row = result.first() + if not row: + result2 = await db.execute( + select(ModelInstance, ModelProvider) + .join(ModelProvider, ModelInstance.provider_id == ModelProvider.id) + .where(ModelInstance.model_type == 'llm') + .where(ModelInstance.is_active == True) + .limit(1) + ) + row = result2.first() + if row: + instance, provider = row + config = { + "api_base": (provider.base_url or settings.LLM_API_BASE).rstrip("/"), + "model": instance.model_name, + "api_key": provider.api_key or settings.LLM_API_KEY, + } + else: + config = { + "api_base": settings.LLM_API_BASE.rstrip("/"), + "model": settings.LLM_MODEL, + "api_key": settings.LLM_API_KEY, + } + self._llm_config_cache = config + # 5分钟后自动清除缓存 + asyncio.get_event_loop().call_later(300, lambda: setattr(self, '_llm_config_cache', None)) + return config + except Exception: + return { + "api_base": settings.LLM_API_BASE.rstrip("/"), + "model": settings.LLM_MODEL, + "api_key": settings.LLM_API_KEY, + } async def inject_memory( self, @@ -394,19 +451,19 @@ class MemoryManager: ) import httpx - api_base = settings.LLM_API_BASE.rstrip("/") + llm = await self._get_llm_config() async with httpx.AsyncClient(timeout=30) as client: resp = await client.post( - f"{api_base}/chat/completions", + f"{llm['api_base']}/chat/completions", json={ - "model": settings.LLM_MODEL, + "model": llm["model"], "messages": [{ "role": "user", "content": f"请用一段话简要总结以下对话的关键内容。保留人名、任务、决策、时间等关键信息。\n\n{dialogue}" }], "max_tokens": 200, }, - headers={"Authorization": f"Bearer {settings.LLM_API_KEY}"}, + headers={"Authorization": f"Bearer {llm['api_key']}"}, ) data = resp.json() summary = data.get("choices", [{}])[0].get("message", {}).get("content", "") @@ -515,17 +572,17 @@ class MemoryManager: 只返回JSON数组,不要其他内容。如果没有可提取的信息返回空数组[]。""" import httpx - api_base = settings.LLM_API_BASE.rstrip("/") + llm = await self._get_llm_config() async with httpx.AsyncClient(timeout=60) as client: resp = await client.post( - f"{api_base}/chat/completions", + f"{llm['api_base']}/chat/completions", json={ - "model": settings.LLM_MODEL, + "model": llm["model"], "messages": [{"role": "user", "content": prompt}], "max_tokens": 800, "temperature": 0.3, }, - headers={"Authorization": f"Bearer {settings.LLM_API_KEY}"}, + headers={"Authorization": f"Bearer {llm['api_key']}"}, ) data = resp.json() result_text = data.get("choices", [{}])[0].get("message", {}).get("content", "[]") @@ -704,17 +761,17 @@ class MemoryManager: 只返回JSON数组,不要其他内容。""" import httpx - api_base = settings.LLM_API_BASE.rstrip("/") + llm = await self._get_llm_config() async with httpx.AsyncClient(timeout=60) as client: resp = await client.post( - f"{api_base}/chat/completions", + f"{llm['api_base']}/chat/completions", json={ - "model": settings.LLM_MODEL, + "model": llm["model"], "messages": [{"role": "user", "content": prompt}], "max_tokens": 500, "temperature": 0.3, }, - headers={"Authorization": f"Bearer {settings.LLM_API_KEY}"}, + headers={"Authorization": f"Bearer {llm['api_key']}"}, ) data = resp.json() result_text = data.get("choices", [{}])[0].get("message", {}).get("content", "[]") @@ -817,17 +874,17 @@ class MemoryManager: 只返回JSON对象,不要其他内容。""" import httpx - api_base = settings.LLM_API_BASE.rstrip("/") + llm = await self._get_llm_config() async with httpx.AsyncClient(timeout=60) as client: resp = await client.post( - f"{api_base}/chat/completions", + f"{llm['api_base']}/chat/completions", json={ - "model": settings.LLM_MODEL, + "model": llm["model"], "messages": [{"role": "user", "content": prompt}], "max_tokens": 500, "temperature": 0.3, }, - headers={"Authorization": f"Bearer {settings.LLM_API_KEY}"}, + headers={"Authorization": f"Bearer {llm['api_key']}"}, ) data = resp.json() result_text = data.get("choices", [{}])[0].get("message", {}).get("content", "{}") diff --git a/backend/modules/model_provider/router.py b/backend/modules/model_provider/router.py index 3957c0e..0f17dc2 100644 --- a/backend/modules/model_provider/router.py +++ b/backend/modules/model_provider/router.py @@ -295,3 +295,138 @@ async def delete_model(provider_id: str, model_id: str, db: AsyncSession = Depen await db.delete(m) await db.commit() return {"code": 200, "message": "已删除"} + + +@router.get("/default-llm") +async def get_default_llm(db: AsyncSession = Depends(get_db)): + """获取系统默认 LLM 模型配置。 + + 从 model_instances 表中查找 is_default=True 且 model_type='llm' 的模型实例, + 返回包含供应商连接信息的完整配置,供系统中各模块统一使用。 + + Returns: + dict: 默认 LLM 配置(model_name, api_key, base_url, default_params 等), + 若未配置则返回 null。 + """ + result = await db.execute( + select(ModelInstance, ModelProvider) + .join(ModelProvider, ModelInstance.provider_id == ModelProvider.id) + .where(ModelInstance.model_type == 'llm') + .where(ModelInstance.is_default == True) + .where(ModelInstance.is_active == True) + .limit(1) + ) + row = result.first() + if not row: + # 回退:取第一个启用的 LLM + result2 = await db.execute( + select(ModelInstance, ModelProvider) + .join(ModelProvider, ModelInstance.provider_id == ModelProvider.id) + .where(ModelInstance.model_type == 'llm') + .where(ModelInstance.is_active == True) + .order_by(ModelInstance.created_at) + .limit(1) + ) + row = result2.first() + if not row: + return {"code": 200, "data": None} + + instance, provider = row + return { + "code": 200, + "data": { + "id": str(instance.id), + "model_name": instance.model_name, + "display_name": instance.display_name or instance.model_name, + "model_type": instance.model_type, + "api_key": provider.api_key or "", + "base_url": provider.base_url or settings.LLM_API_BASE, + "default_params": instance.default_params or {}, + "capabilities": instance.capabilities or {}, + "is_default": instance.is_default, + "is_active": instance.is_active, + "provider_id": str(instance.provider_id), + "provider_name": provider.name, + }, + } + + +async def resolve_model_config(db: AsyncSession, model_instance_id: str = None) -> dict: + """根据模型实例 ID 解析完整的模型调用配置。 + + 这是系统的核心模型解析函数。各模块(Agent工厂、流程引擎、记忆管理器等) + 应通过此函数获取模型配置,实现统一的模型管理。 + + 解析优先级: + 1. 如果提供了 model_instance_id → 从数据库读取该模型的完整配置 + 2. 否则 → 获取默认 LLM 配置(is_default 的 LLM 实例) + 3. 都没有 → 回退到环境变量 settings.LLM_* + + Args: + db: 异步数据库会话。 + model_instance_id: 可选的模型实例 UUID 字符串。 + + Returns: + dict: 包含 model_name, api_key, base_url, default_params 等的配置字典。 + """ + if model_instance_id: + try: + uid = uuid.UUID(model_instance_id) + result = await db.execute( + select(ModelInstance, ModelProvider) + .join(ModelProvider, ModelInstance.provider_id == ModelProvider.id) + .where(ModelInstance.id == uid) + .limit(1) + ) + row = result.first() + if row: + instance, provider = row + return { + "model_name": instance.model_name, + "api_key": provider.api_key or settings.LLM_API_KEY, + "base_url": provider.base_url or settings.LLM_API_BASE, + "default_params": instance.default_params or {}, + "capabilities": instance.capabilities or {}, + } + except (ValueError, Exception): + pass + + # 无指定实例或未找到:使用默认 LLM + result = await db.execute( + select(ModelInstance, ModelProvider) + .join(ModelProvider, ModelInstance.provider_id == ModelProvider.id) + .where(ModelInstance.model_type == 'llm') + .where(ModelInstance.is_default == True) + .where(ModelInstance.is_active == True) + .limit(1) + ) + row = result.first() + if not row: + result = await db.execute( + select(ModelInstance, ModelProvider) + .join(ModelProvider, ModelInstance.provider_id == ModelProvider.id) + .where(ModelInstance.model_type == 'llm') + .where(ModelInstance.is_active == True) + .order_by(ModelInstance.created_at) + .limit(1) + ) + row = result.first() + + if row: + instance, provider = row + return { + "model_name": instance.model_name, + "api_key": provider.api_key or settings.LLM_API_KEY, + "base_url": provider.base_url or settings.LLM_API_BASE, + "default_params": instance.default_params or {}, + "capabilities": instance.capabilities or {}, + } + + # 最终回退到环境变量 + return { + "model_name": settings.LLM_MODEL, + "api_key": settings.LLM_API_KEY, + "base_url": settings.LLM_API_BASE, + "default_params": {}, + "capabilities": {}, + } diff --git a/backend/modules/monitor/router.py b/backend/modules/monitor/router.py index 1a5672e..18cdd46 100644 --- a/backend/modules/monitor/router.py +++ b/backend/modules/monitor/router.py @@ -12,6 +12,7 @@ from sqlalchemy.ext.asyncio import AsyncSession from database import get_db from models import User, ChatSession, ChatMessage from modules.org.router import _get_subordinate_ids, _user_to_out +from modules.model_provider.router import resolve_model_config from schemas import EmployeeAnalysis, UserOut router = APIRouter(prefix="/api/monitor", tags=["monitor"]) # 监控模块路由前缀 @@ -204,12 +205,13 @@ async def get_employee_analysis( f"[{m.role}] {m.content[:300]}" for m in messages ]) - # 初始化 LLM 模型 + # 初始化 LLM 模型(从模型管理表读取配置) + llm_cfg = await resolve_model_config(db) model = OpenAIChatModel( config_name="analysis_model", - model_name=settings.LLM_MODEL, - api_key=settings.LLM_API_KEY, - api_base=settings.LLM_API_BASE, + model_name=llm_cfg["model_name"], + api_key=llm_cfg["api_key"], + api_base=llm_cfg["base_url"], ) formatter = OpenAIChatFormatter() diff --git a/frontend/src/views/flow/node-configs/LlmConfig.vue b/frontend/src/views/flow/node-configs/LlmConfig.vue index e93bc69..62a9373 100644 --- a/frontend/src/views/flow/node-configs/LlmConfig.vue +++ b/frontend/src/views/flow/node-configs/LlmConfig.vue @@ -109,17 +109,21 @@ function getProviderName(providerId: string): string { } function onModelSelect(val: string) { - update('model', val) - // 如果选择了已管理的模型,自动填入其默认参数 - if (!val) return - const model = llmModels.value.find((m: any) => m.model_name === val) + // 如果选择了已管理的模型,同时保存 model_name 和 model_instance_id + const model = val ? llmModels.value.find((m: any) => m.model_name === val) : null if (model) { + // 从模型管理选择:保存 model + model_instance_id + const updated = { ...props.modelValue, model: val, model_instance_id: model.id } + // 自动填入默认参数 const params = model.default_params || {} - const updated = { ...props.modelValue } if (params.temperature !== undefined && !updated.temperature) updated.temperature = params.temperature if (params.max_tokens && !updated.max_tokens) updated.max_tokens = params.max_tokens if ((model.capabilities || {}).function_calling && updated.tool_call === false) updated.tool_call = true emit('update:modelValue', updated) + } else { + // 手动输入或清空:只保存 model,清除 model_instance_id + const updated = { ...props.modelValue, model: val, model_instance_id: undefined } + emit('update:modelValue', updated) } } diff --git a/frontend/src/views/settings/Settings.vue b/frontend/src/views/settings/Settings.vue index 327cabd..4e84a4e 100644 --- a/frontend/src/views/settings/Settings.vue +++ b/frontend/src/views/settings/Settings.vue @@ -2,46 +2,49 @@
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 保存配置 - 测试连接 - - + + + + + + +
+
+
默认 LLM:{{ defaultModel.display_name || defaultModel.model_name || '未配置' }}
+
供应商:{{ defaultModel.provider_name || '-' }}
+
状态: + + {{ defaultModel.is_active ? '启用' : '禁用' }} + +
+
+
+
+ ⚠️ 尚未配置默认 LLM 模型,请前往「模型管理」添加并设为默认 +
+ @@ -63,6 +66,7 @@ +
@@ -92,7 +96,7 @@