如果文章对你有帮助,请点个"关注"
版本 :v1.0
日期 :2026-04-10
作者:阿财
目录
1. 功能概述
1.1 什么是热切换
Agent 配置热切换:在不重启服务的情况下,动态更新 Agent 的配置(如切换大模型供应商),新配置立即生效。
1.2 解决的问题
传统方式:
修改配置 → 保存 → 重启服务 → 等待启动完成 → 测试
- 需要重启服务
- 中断正在进行的对话
- 用户体验差
热切换方式:
修改配置 → 保存 → 自动重新加载 → 立即生效
- 无需重启服务
- 不中断对话
- 用户体验好
1.3 使用场景
| 场景 | 说明 |
|---|---|
| 切换供应商 | 火山方舟 → 阿里百炼 |
| 修改 API Key | Key 过期或轮换 |
| 更换模型 | qwen3.5-plus → deepseek-v3.2 |
| 测试对比 | 快速对比不同模型的效果 |
2. 核心原理
2.1 架构图
┌─────────────────────────────────────────────────────────┐
│ 前端(PySide6) │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ AgentDialog(Agent 编辑对话框) │ │
│ │ │ │
│ │ 1. 用户修改配置 │ │
│ │ 2. 保存到数据库 │ │
│ │ 3. 调用 POST /api/reload-agent │ │
│ │ 4. 显示"已保存"提示 │ │
│ └──────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
↓ HTTP POST
┌─────────────────────────────────────────────────────────┐
│ 后端(Node.js + Express) │
│ 监听:3002 端口 │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ /api/reload-agent │ │
│ │ │ │
│ │ 1. 从数据库读取新配置 │ │
│ │ 2. 更新 agentConfigs Map │ │
│ │ 3. 更新 assistant 实例配置 │ │
│ │ 4. 返回成功响应 │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ SimpleAIAssistant │ │
│ │ │ │
│ │ apiKey, apiUrl, modelName │ │
│ │ ↓ 被 reloadAgent 更新 │ │
│ │ 下次对话使用新配置 │ │
│ └──────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
↓ SQLite
┌─────────────────────────────────────────────────────────┐
│ 数据库(system.db) │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ agents │ │ models │ │ providers │ │
│ │ │ │ │ │ │ │
│ │ name │ │ name │ │ base_url │ │
│ │ model │ │ provider_id │ │ api_key │ │
│ │ provider │ │ contextWindow│ │ │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
└─────────────────────────────────────────────────────────┘
2.2 数据流
1. 用户修改 Agent 配置
↓
2. 点击"保存"按钮
↓
3. 前端保存到数据库
↓
4. 前端调用 POST /api/reload-agent
↓
5. 后端 reloadAgent 函数
├─ 从数据库读取新配置
├─ 更新 agentConfigs Map
└─ 更新 assistant 实例配置 ← 关键!
↓
6. 返回成功响应
↓
7. 前端显示"已保存"提示
↓
8. 下次对话使用新配置
2.3 关键设计
设计 1:更新 assistant 实例配置
javascript
// server.js
async function reloadAgent(agentName) {
const agent = await configManager.getAgentByName(agentName);
const model = await configManager.getModelByName(agent.model);
// 如果是当前默认 Agent,同时更新 assistant 实例
if (agentName === 'main') {
assistant.apiKey = model.api_key; // ← 更新 API Key
assistant.apiUrl = model.apiUrl; // ← 更新 API URL
assistant.modelName = model.name; // ← 更新模型名称
assistant.contextWindow = model.contextWindow;
assistant.maxTokens = model.maxTokens;
console.log(`assistant 实例配置已更新`);
}
}
为什么需要更新 assistant 实例?
因为 assistant.js 在启动时加载配置:
javascript
// assistant.js
async init() {
const model = await configManager.getDefaultModel();
this.apiKey = model.api_key; // ← 启动时加载
this.apiUrl = model.apiUrl; // ← 启动时加载
this.modelName = model.name; // ← 启动时加载
}
如果不更新实例配置,下次对话仍然使用旧配置!
设计 2:下次对话重新构建 runtime
javascript
// assistant.js
async chat(userMessage) {
// 每次对话都会重新构建 runtime
const runtimeText = getFullRuntimeInfo({
workspace: this.workspace,
model: this.modelName, // ← 使用新模型名称
default_model: this.modelName, // ← 使用新模型名称
...
});
// 构建提示词
const prompt = await buildSystemPrompt(memoryResults) + runtimeText;
// 调用 API
const response = await fetch(this.apiUrl, ...); // ← 使用新 API URL
}
3. 后端实现
3.1 添加 configManager 导入
文件 :src/server.js
位置:第 13 行附近
javascript
import db from './db.js';
import configManager from './core/config-manager.js'; // ← 添加这行
3.2 添加热切换函数
文件 :src/server.js
位置 :第 128 行(await assistant.init(); 之后)
javascript
// 热切换功能
const agentConfigs = new Map();
/**
* 重新加载 Agent 配置
*/
async function reloadAgent(agentName) {
console.log(`重新加载 Agent: ${agentName}`);
const agent = await configManager.getAgentByName(agentName);
const model = await configManager.getModelByName(agent.model);
agentConfigs.set(agentName, {
name: agent.name,
nickname: agent.nickname,
apiKey: model.api_key,
apiUrl: model.apiUrl,
modelName: model.name
});
// 如果是当前默认 Agent,同时更新 assistant 实例
if (agentName === 'main') {
assistant.apiKey = model.api_key;
assistant.apiUrl = model.apiUrl;
assistant.modelName = model.name;
assistant.contextWindow = model.contextWindow;
assistant.maxTokens = model.maxTokens;
console.log(`assistant 实例配置已更新`);
}
console.log(`Agent ${agentName} 配置已更新`);
console.log(` API URL: ${model.apiUrl}`);
console.log(` API Key: ${model.api_key.substring(0, 8)}...`);
}
/**
* 刷新所有 Agent
*/
async function refreshAllAgents() {
console.log('刷新所有 Agent 配置...');
configManager.refreshCache();
const agents = await configManager.getAgents();
for (const agent of agents) {
await reloadAgent(agent.name);
}
console.log('所有 Agent 配置已刷新');
}
// 初始化默认 Agent
await reloadAgent('main');
3.3 添加 API 接口
文件 :src/server.js
位置 :第 221 行左右(http.createServer 内部,// SSE 端点:/api/events 之前)
javascript
// ===== 热切换 API 接口 =====
// 重新加载 Agent: POST /api/reload-agent
if (url.pathname === '/api/reload-agent' && req.method === 'POST') {
try {
const body = await new Promise((resolve) => {
let data = '';
req.on('data', chunk => { data += chunk; });
req.on('end', () => resolve(JSON.parse(data)));
});
const { agentName } = body;
if (!agentName) {
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ success: false, error: 'agentName 是必填参数' }));
return;
}
await reloadAgent(agentName);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ success: true, message: `Agent ${agentName} 配置已重新加载` }));
return;
} catch (e) {
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ success: false, error: e.message }));
return;
}
}
// 刷新所有 Agent: POST /api/refresh-all
if (url.pathname === '/api/refresh-all' && req.method === 'POST') {
try {
await refreshAllAgents();
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ success: true, message: '所有 Agent 配置已刷新' }));
return;
} catch (e) {
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ success: false, error: e.message }));
return;
}
}
// 获取 Agent 列表:GET /api/agents
if (url.pathname === '/api/agents' && req.method === 'GET') {
try {
const agents = await configManager.getAgents();
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ success: true, agents }));
return;
} catch (e) {
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ success: false, error: e.message }));
return;
}
}
// SSE 端点:/api/events
4. 前端实现
4.1 修改 agent_dialog.py
文件 :desktop_client/agent_dialog.py
位置 :save_agent 方法
python
def save_agent(self):
"""保存 Agent 并应用更改"""
name = self.name_input.text().strip()
# ... 其他代码 ...
try:
cursor = config_manager.cursor
conn = config_manager.conn
if self.agent_name:
cursor.execute('''UPDATE agents SET ... WHERE name=?''', (...))
else:
cursor.execute('''INSERT INTO agents (...) VALUES (...)''', (...))
conn.commit()
# 通知后端重新加载配置(热切换)
print(f'[save_agent] 调用后端 API: /api/reload-agent')
try:
import requests
print(f'[save_agent] POST http://localhost:3002/api/reload-agent')
print(f'[save_agent] JSON: {{"agentName": "{name}"}}')
response = requests.post(
'http://localhost:3002/api/reload-agent',
json={'agentName': name},
timeout=5
)
print(f'[save_agent] 响应状态码:{response.status_code}')
print(f'[save_agent] 响应内容:{response.text}')
if response.json().get('success'):
print(f'[save_agent] 热切换成功')
else:
print(f'[save_agent] 热切换失败:{response.json().get("error")}')
QMessageBox.warning(
self, '警告',
'热切换失败:' + response.json().get('error', '未知错误')
)
except Exception as e:
print(f'[save_agent] API 调用失败:{e}')
QMessageBox.warning(
self, '提示',
'配置已保存,但热切换失败\n需要重启服务才能生效'
)
# 关闭对话框(无论成功还是失败都关闭)
self.accept()
# 创建工作区目录(仅新建 Agent 时)
if not self.agent_name:
base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
workspace_path = os.path.join(base_dir, 'workspace', name)
os.makedirs(workspace_path, exist_ok=True)
print(f'[save_agent] 已创建工作区:{workspace_path}')
except Exception as e:
print(f'[save_agent] 错误:{e}')
if 'UNIQUE constraint failed' in str(e):
QMessageBox.critical(self, '错误', f'Agent {name} 已存在')
else:
QMessageBox.critical(self, '错误', f'保存失败:{e}')
4.2 修改 config_dialog.py
文件 :desktop_client/config_dialog.py
位置 :save_agent 方法
python
def save_agent(self):
"""保存 Agent 并应用更改"""
name = self.agent_name.text().strip()
provider = self.agent_provider.currentText()
model = self.agent_model.currentData()
if not name or not provider:
QMessageBox.warning(self, '警告', '名称和供应商必填')
return
try:
cursor = config_manager.cursor
cursor.execute('SELECT id FROM agents WHERE name = ?', (name,))
if cursor.fetchone():
cursor.execute('''UPDATE agents SET ... WHERE name=?''', (...))
else:
cursor.execute('''INSERT INTO agents (...) VALUES (...)''', (...))
config_manager.conn.commit()
# 通知后端重新加载配置(热切换)
print(f'[save_agent] 调用后端 API: /api/reload-agent')
try:
import requests
print(f'[save_agent] POST http://localhost:3002/api/reload-agent')
print(f'[save_agent] JSON: {{"agentName": "{name}"}}')
response = requests.post(
'http://localhost:3002/api/reload-agent',
json={'agentName': name},
timeout=5
)
print(f'[save_agent] 响应状态码:{response.status_code}')
print(f'[save_agent] 响应内容:{response.text}')
if response.json().get('success'):
print(f'[save_agent] 热切换成功')
# 不显示成功提示
else:
print(f'[save_agent] 热切换失败:{response.json().get("error")}')
QMessageBox.warning(
self, '警告',
'热切换失败:' + response.json().get('error', '未知错误')
)
except Exception as e:
print(f'[save_agent] API 调用失败:{e}')
# 不阻止保存,只显示警告
QMessageBox.warning(
self, '提示',
'配置已保存,但热切换失败\n需要重启服务才能生效'
)
config_manager.refresh_cache()
self.load_data()
self.config_changed.emit()
except Exception as e:
QMessageBox.critical(self, '错误', str(e))
5. 测试验证
5.1 重启后端服务
bash
# 停止当前服务(Ctrl+C)
# 重新启动
node src/server.js
预期输出:
重新加载 Agent: main
Agent main 配置已更新
API URL: https://ark.cn-beijing.volces.com/api/coding/v3/chat/completions
API Key: c867c7bb...
assistant 实例配置已更新
5.2 测试 API 接口
测试 1:获取 Agent 列表
bash
curl http://localhost:3002/api/agents
预期输出:
json
{
"success": true,
"agents": [
{"id": 1, "name": "main", "nickname": "阿财", ...},
...
]
}
测试 2:重新加载 Agent
bash
curl -X POST http://localhost:3002/api/reload-agent \
-H "Content-Type: application/json" \
-d "{\"agentName\":\"main\"}"
预期输出:
json
{
"success": true,
"message": "Agent main 配置已重新加载"
}
测试 3:刷新所有 Agent
bash
curl -X POST http://localhost:3002/api/refresh-all
预期输出:
json
{
"success": true,
"message": "所有 Agent 配置已刷新"
}
5.3 测试前端热切换
步骤:
- 打开桌面客户端
- 配置管理 → Agent 管理
- 双击 main Agent
- 修改大模型(如火山方舟 → 阿里百炼)
- 点击"保存"
- 等待 2-3 秒
- 对话框自动关闭
查看日志:
客户端日志:
[save_agent] 调用后端 API: /api/reload-agent
[save_agent] POST http://localhost:3002/api/reload-agent
[save_agent] JSON: {"agentName": "main"}
[save_agent] 响应状态码:200
[save_agent] 响应内容:{"success":true,"message":"..."}
[save_agent] 热切换成功
服务器日志:
重新加载 Agent: main
Agent main 配置已更新
API URL: https://dashscope.aliyuncs.com/...
API Key: sk-...
assistant 实例配置已更新
测试 4:验证新配置生效
发送一条消息,查看日志:
apiurl: https://dashscope.aliyuncs.com/... ← 新 URL
apikey: sk-... ← 新 Key
model: qwen3.5-plus ← 新模型
C. 常见问题
Q1: 热切换会影响正在进行的对话吗?
A: 不会,只影响下次对话
Q2: 需要重启服务吗?
A: 不需要,热切换的核心就是无需重启
Q3: 切换后 runtime 会更新吗?
A: 会,下次对话时重新构建 runtime
Q4: 可以批量切换所有 Agent 吗?
A: 可以,调用 /api/refresh-all 接口
Q5: 切换失败会影响保存吗?
A: 不会,配置已保存到数据库,热切换失败不影响保存
如果文章对你有帮助,请点个"关注"