由于不怎么喜欢ds,一直到opencode订阅限时免费用v4 flash才试着蹬了一会儿,没想到我(果然)是大怨种。
给定同一份代码库(python+fastapi后台,vue3前台)和同一份需求,分别让deepseek v4 flash(思考等级high)和minimax m2.7,两个规模和年代相近的模型生成计划。然后让gemini3.1pro做裁判。
我来当裁判的话,m2.7也是逊于v4flash的,minimax刷分嫌疑很重,有些跑分榜超过gemini3flash很多但是实际体验上差了一截。而ds v4 flash已经非常接近互有胜负了。ds的架构思维比较好,在阅读代码的时候也会体现出来,对没有文档的旧代码库理解更透彻(甚至略微强于gemini3flash)。
提示词
以上是两个ai针对一样的代码和一样的需求“xxx”提出的两份修改计划,你来裁判哪个比较好,提出详尽的细节代码不作为加分项,侧重考虑宏观层面。
例子1
背景:后来加了一个基于谷歌adk的ai agent功能,独立于原有的普通聊天机制和配置,没有从原数据库(带配置界面)中读取模型配置。
需求:将后台agent的大模型配置,包括模型,url,apikey,改成从现有数据库配置项读取
结果:ds胜出
好的,当数据库配置读取失败时直接报错。让我提出详细的实施计划:
—
## 实施计划
### 1. 修改 `agent.py`
**文件**: `D:\sub\MedAutoML\code\backend\app\agent\agent.py`
#### 1.1 添加新的导入和辅助函数
# 添加在 imports 区域后,约第19行
from config import Config
from app.models.settings import SystemSettings
from sqlalchemy import select
# provider 对应的 base_url 映射
PROVIDER_BASE_URLS = {
'ali': 'https://dashscope.aliyuncs.com/compatible-mode/v1',
'openai': 'https://api.openai.com/v1',
}
async def _get_llm_config_from_db(session: AsyncSession) -> dict:
"""从数据库读取 LLM 配置"""
query = select(SystemSettings).filter_by(setting_key='llm_config')
result = await session.execute(query)
setting = result.scalars().first()
if not setting or not setting.setting_value:
raise ValueError("数据库中未找到 LLM 配置 (llm_config)")
return setting.setting_value
def _get_base_url_by_provider(provider: str) -> str:
"""根据 provider 返回 base_url"""
base_url = PROVIDER_BASE_URLS.get(provider)
if not base_url:
raise ValueError(f"不支持的 LLM provider: {provider}")
return base_url
#### 1.2 创建异步工厂函数
# 约在 _build_litellm_model() 位置 (line 33)
async def _create_litellm_model_from_db(session: AsyncSession):
"""从数据库配置创建 LiteLLM 模型实例"""
config = await _get_llm_config_from_db(session)
model_name = config.get('model', 'qwen-plus')
provider = config.get('provider', 'ali')
api_key = config.get('apiKey')
if not api_key:
raise ValueError("LLM 配置中缺少 apiKey")
# 设置环境变量供 LiteLLM 使用
os.environ['OPENAI_API_KEY'] = api_key
# 获取 base_url
base_url = _get_base_url_by_provider(provider)
# LiteLLM 模型名称格式: provider/model_id
# 根据 llm_service.py 的 MODEL_CONFIG,model_name='qwen-plus' 对应 model_id='qwen3.5-plus'
model_id = _get_model_id_from_name(model_name) # 需要添加这个辅助函数
litellm_model_name = f"openai/{model_id}"
return LiteLlm(model=litellm_model_name, base_url=base_url)
#### 1.3 添加 model_id 映射函数
# MODEL_CONFIG 中的 model_id 映射
MODEL_ID_MAP = {
'qwen-plus': 'qwen3.5-plus',
'qwen-max': 'qwen3-max',
'qwen-turbo': 'qwen3.5-flash',
}
def _get_model_id_from_name(model_name: str) -> str:
"""获取 LiteLLM 使用的 model_id"""
return MODEL_ID_MAP.get(model_name, model_name)
#### 1.4 修改 AgentSessionManager
**修改**: `AgentSessionManager.__init__` 和添加新方法
class AgentSessionManager:
def __init__(self):
self._session_service: Optional[InMemorySessionService] = None
self._runners: Dict[str, Runner] = {}
self._agent: Optional[LlmAgent] = None # 懒加载
async def _get_or_create_agent(self, session: AsyncSession) -> LlmAgent:
"""获取或创建 Agent 实例(懒加载)"""
if self._agent is None:
litellm_model = await _create_litellm_model_from_db(session)
tools = get_all_tools()
self._agent = LlmAgent(
name="MedAutoMLAgent",
model=litellm_model,
description="MedAutoML智能助手 - 帮助医生和研究人员构建临床预测模型",
instruction=_load_instruction(),
tools=tools,
output_key="agent_output",
)
return self._agent
def get_runner(self, user_id: str) -> Runner:
# 需要改为异步,或者使用全局 agent
# 方案:预先创建 agent,但需要数据库session
...
**关键决策点**:
– `AgentSessionManager.get_runner()` 是同步方法,但创建 agent 需要异步数据库操作
– **方案**: 在 `get_agent_session_manager()` 调用时预先创建 agent(使用应用级 session)
#### 1.5 修改全局初始化
# 约在 line 230 后
_agent_session_manager: Optional[AgentSessionManager] = None
_medautoml_agent: Optional[LlmAgent] = None
async def _init_agent_with_db_config():
"""初始化 Agent(使用数据库配置)- 应用启动时调用"""
global _medautoml_agent
from app import get_db
from sqlalchemy import select
# 获取数据库 session
async for session in get_db():
litellm_model = await _create_litellm_model_from_db(session)
tools = get_all_tools()
_medautoml_agent = LlmAgent(
name="MedAutoMLAgent",
model=litellm_model,
description="MedAutoML智能助手",
instruction=_load_instruction(),
tools=tools,
output_key="agent_output",
)
break # 只获取一次 session
def get_agent() -> LlmAgent:
"""获取 MedAutoMLAgent 实例"""
if _medautoml_agent is None:
raise RuntimeError("Agent 未初始化,请先调用 init_agent_with_db_config()")
return _medautoml_agent
### 2. 需要协调的修改
由于 `create_medautoml_agent` 需要在应用启动时调用,需要在 `run.py` 中添加初始化调用。
**文件**: `D:\sub\MedAutoML\code\backend\run.py` (如果存在) 或 `app/__init__.py`
—
## 实现步骤
1. **修改 `agent.py`**:
– 添加 imports、辅助函数、`MODEL_ID_MAP`
– 创建 `_create_litellm_model_from_db()` 异步工厂函数
– 添加 `_init_agent_with_db_config()` 初始化函数
– 修改 `get_agent_session_manager()` 使用懒加载 agent
2. **添加启动调用** (如 `run.py` 或 `app/__init__.py`):
– 在应用启动时调用 `_init_agent_with_db_config()`
3. **移除/更新 `config.py`**:
– 考虑是否移除 `DEFAULT_AGENT_MODEL` 和 `AGENT_API_BASE`(如果不再需要作为 fallback)
—
Now I have a complete understanding of the codebase. Here’s my analysis and plan:
—
## Analysis
**Current state:**
– `agent/agent.py` creates `_litellm_model` and `MedAutoMLAgent` **at module import time** using hardcoded `Config.DEFAULT_AGENT_MODEL` and `Config.AGENT_API_BASE`
– The DB already has a `SystemSettings` table with `setting_key=’llm_config’` storing `{apiKey, model, provider, enabled}` (see `models/settings.py` and `routes/settings.py`)
– `llm_service.py` already reads from this DB config — but the ADK agent does not
– The DB config has no `base_url` field, which the agent needs for `LiteLlm`
**Key challenge:** The agent is created at import time before the DB is available. Need lazy initialization.
—
## Implementation Plan
### 1. `code/backend/app/agent/agent.py` — Main changes
– **Remove** module-level `_litellm_model` and `MedAutoMLAgent` creation
– **Add** lazy init: `get_agent()` reads from DB on first call, falls back to env vars / `Config`
– **Add** a sync SQLAlchemy engine to read the DB (needed because agent runs in thread pool via `asyncio.to_thread`)
– **Add** `reload_agent_config()` public function for settings route to trigger a reload
– **Update** `AgentSessionManager.get_runner()` to use `get_agent()`
– Construct the LiteLLM model name as `openai/{config.model}`, derive `base_url` from config or provider
– Pass `api_key` to `LiteLlm` (currently not passed at all)
### 2. `code/backend/app/routes/settings.py` — Minor changes
– Add `base_url` to `DEFAULT_LLM_CONFIG`
– Handle `base_url` field in `PUT /api/settings/llm`
– Call `reload_agent_config()` after updating LLM config
### 3. `code/backend/app/agent/__init__.py` — No change
### 4. `code/backend/config.py` — No change (kept as fallback)
—
Would you like me to proceed with this plan? One question: for the `base_url` field in the DB config, should I add it to the settings API route as described, or would you prefer to derive it from the existing `provider` field in the agent code (e.g., `ali` → DashScope URL, `openai` → OpenAI URL)?
从宏观架构和系统设计的角度来看,实施计划 2(Plan 2)显著优于实施计划 1(Plan 1)。
虽然计划 1 提供了非常详尽的代码片段,但它在架构设计上存在几个致命的硬伤。计划 2 则更加敏锐地抓住了动态配置系统的核心痛点。
以下是针对这两个计划在宏观层面的详细对比裁判分析:
1. 配置的热更新能力 (Hot Reloading) 🏆 计划 2 胜出
-
计划 1 的缺陷:它选择在应用启动时(
run.py)进行全局初始化读取数据库。这意味着如果用户在前端页面修改了 API Key 或更换了模型,系统必须重启才能生效。这完全违背了将配置移入数据库(动态化)的初衷。 -
计划 2 的优势:它不仅考虑到了懒加载(Lazy Init),还精准地提出在
routes/settings.py(设置 API 路由)中增加reload_agent_config()机制。这保证了用户修改配置后,Agent 实例能够实时刷新,实现真正的动态配置。
2. 全局状态污染与安全性 🏆 计划 2 胜出
-
计划 1 的缺陷:为了给 LiteLLM 传递鉴权信息,计划 1 直接使用了
os.environ['OPENAI_API_KEY'] = api_key。这是一种非常危险的反模式(Anti-pattern)。它不仅污染了全局环境变量,而且在多并发、多租户或多模型切换的场景下会引发严重的竞态条件(Race Condition)。 -
计划 2 的优势:明确指出需要将
api_key作为参数直接传递给LiteLlm实例,做到实例级别的状态隔离,安全且优雅。
3. 系统扩展性与硬编码 (Extensibility) 🏆 计划 2 胜出
-
计划 1 的缺陷:为了解决
base_url和model_id的问题,计划 1 在代码里硬编码了PROVIDER_BASE_URLS和MODEL_ID_MAP这两个字典。如果未来想要接入一个新的大模型(例如通义千问的新版本,或者百川、智谱等),依然需要修改代码并重新发布。 -
计划 2 的优势:它敏锐地发现现有的数据库模型中缺少
base_url字段,并建议修改 Settings 相关的 API,让base_url也成为数据库和前端管理的字段。这种“配置数据化”的思路扩展性远好于硬编码。
4. 运行上下文与并发模型匹配 🏆 计划 2 胜出
-
计划 1 的缺陷:
AgentSessionManager.get_runner()是一个同步方法,而数据库读取是异步的。计划 1 的解决方案是“在启动时用全局变量强行初始化”,这种做法耦合度过高。 -
计划 2 的优势:准确识别出 Agent 实际上是在线程池(
asyncio.to_thread)中运行的。因此,它建议在懒加载时使用同步的 SQLAlchemy Engine 去读取数据库。这在宏观上规避了在同步代码块中强行调用异步方法的事件循环(Event Loop)地狱。
裁判总结
计划 1 是典型的“顺向思维脚本式”写法,能跑通,但埋下了全局变量污染、无法热更新、扩展性差等技术债务。
计划 2 展现了高级工程师的系统性思维,考虑到了配置的生命周期、线程安全、实例隔离以及数据的完整性。
建议采用实施计划 2 作为重构的指导方针。