agentscope-ai / agentscope-ai/agentscope-java
[Feature]: HarnessAgent 单实例时,如何动态切换模型?好像不支持
- 主要语言
- Java
- 星标
- 5.6k
- 派生
- 1.3k
- 平均合并
- 4 天 12 小时
- 30 天内合并 PR
- 77
描述
需求场景:
容器部署项目,HarnessAgent 启动时就配置好了默认模型,单实例。但是我的前端是允许用户手动切换模型的,后端却找不到可以动态切换模型的接口。是否需要支持下呢?
自己也问了AI,AI查看的框架源码也没有找到,最后给出的方案是通过反射强制修改模型变量来切换。
AI分析如下:
让我先查看当前项目的配置和服务层代码,以及框架中模型切换的相关机制。
让我再看一下框架源码中 `ReActAgent` 的 model 字段和 `HarnessAgent` 的结构,确认反射方案的具体细节。
Grep 工具遇到了问题,让我直接查看源码文件中的关键方法。
现在我已经完全理解了项目的动态模型切换机制。让我再确认一下 `CompactionMiddleware` 是否也持有 model 引用(压缩时需要用模型做摘要)。
现在我有了完整的理解。以下是这个项目中 HarnessAgent 单例模式下动态切换模型的完整机制。
## 核心问题
`HarnessAgent` 是单例 Bean,内部持有的 [ReActAgent](file:///d:/Java/agentscope-java/agentscope-core/src/main/java/io/agentscope/core/ReActAgent.java#L222) 的 `model` 字段是 `private final`:
```java
// ReActAgent.java:222
private final Model model;
```
而 [HarnessAgent](file:///d:/Java/agentscope-java/agentscope-harness/src/main/java/io/agentscope/harness/agent/HarnessAgent.java#L156) 中的 delegate 也是 `final`:
```java
// HarnessAgent.java:156
private final ReActAgent delegate;
```
框架没有提供 setter 方法,因此**运行时切换 model 需要用反射**。
## 项目已有的实现方案
项目已经完整实现了动态模型切换,链路如下:
```
前端 POST /api/model/switch
→ ChatController.switchModel()
→ AgentChatService.switchModel() [反射替换 model 字段]
→ AgentScopeConfig.buildModel() [按 provider 构建新 Model]
```
### 1. 模型构建 — [AgentScopeConfig.buildModel()](file:///d:/Java/Project2/agentScopeDemo/src/main/java/com/example/myagentdemo/config/AgentScopeConfig.java#L163-L219)
公共方法,Bean 初始化和运行时切换共用。根据 `provider:modelName` 格式分支构建对应的 `ChatModel`:
```java
public Model buildModel(String modelId, String apiKey, String baseUrl) {
String provider = modelId.split(":")[0];
String modelName = modelId.contains(":") ? modelId.substring(...) : modelId;
return switch (provider) {
case "deepseek" -> OpenAIChatModel.builder()...formatter(new DeepSeekFormatter())...build();
case "dashscope" -> DashScopeChatModel.builder()...build();
case "openai" -> OpenAIChatModel.builder()...formatter(new OpenAIChatFormatter())...build();
case "zhipu" -> OpenAIChatModel.builder()...formatter(new OpenAIChatFormatter())...build();
default -> throw ...;
};
}
```
### 2. 反射替换 — [AgentChatService.switchModel()](file:///d:/Java/Project2/agentScopeDemo/src/main/java/com/example/myagentdemo/service/AgentChatService.java#L347-L376)
核心步骤:
```java
public ModelSwitchResult switchModel(String modelId, String apiKey, String baseUrl) {
// 1. apiKey 为空时,根据 provider 读取默认配置
String resolvedApiKey = apiKey;
String resolvedBaseUrl = baseUrl;
if (resolvedApiKey == null || resolvedApiKey.isBlank()) {
String provider = modelId.split(":")[0];
resolvedApiKey = agentScopeConfig.getApiKeyForProvider(provider);
resolvedBaseUrl = agentScopeConfig.getBaseUrlForProvider(provider);
}
// 2. 构建新 Model 实例
Model newModel = agentScopeConfig.buildModel(modelId, resolvedApiKey, resolvedBaseUrl);
// 3. 反射替换 ReActAgent 的 final model 字段
ReActAgent delegate = harnessAgent.getDelegate();
java.lang.reflect.Field modelField = delegate.getClass().getDeclaredField("model");
modelField.setAccessible(true);
modelField.set(delegate, newModel);
// 4. 更新 volatile 状态变量
currentModelId = modelId; // volatile String,保证可见性
}
```
### 3. 状态追踪 — `volatile currentModelId`
```java
// AgentChatService.java:45
private volatile String currentModelId;
```
用 `volatile` 保证多线程可见性,通过 [getCurrentModelId()](file:///d:/Java/Project2/agentScopeDemo/src/main/java/com/example/myagentdemo/service/AgentChatService.java#L334-L336) 供 REST 接口查询。
### 4. REST 端点 — [ChatController](file:///d:/Java/Project2/agentScopeDemo/src/main/java/com/example/myagentdemo/controller/ChatController.java#L218-L258)
| 端点 | 方法 | 功能 |
|------|------|------|
| `POST /api/model/switch` | switchModel | 切换模型,body: `{modelId, apiKey, baseUrl}` |
| `GET /api/model/current` | getCurrentModel | 查询当前模型ID |
## 架构关系图
```mermaid
graph TD
subgraph 单例 Bean
HA[HarnessAgent 单例]
HA -->|getDelegate| RA[ReActAgent]
RA -->|final model 字段| M1[Model 实例]
HA -->|compactionHook| CM[CompactionMiddleware]
CM -->|final model 字段| M1
end
subgraph 切换流程
REQ[POST /api/model/switch] --> SVC[AgentChatService.switchModel]
SVC -->|buildModel| CFG[AgentScopeConfig.buildModel]
CFG -->|反射| NM[新 Model 实例]
SVC -->|反射 set modelField| RA
SVC -->|volatile 写| CID[currentModelId]
end
style NM fill:#90EE90
```
## 已知限制
**`CompactionMiddleware` 持有独立的 model 引用。**
[CompactionMiddleware.java:62](file:///d:/Java/agentscope-java/agentscope-harness/src/main/java/io/agentscope/harness/agent/middleware/CompactionMiddleware.java#L62) 中也持有 `private final Model model`,这个引用在 `HarnessAgent.builder().build()` 时固定,反射只替换了 `ReActAgent.model`,**不会更新 `CompactionMiddleware.model`**。
| 场景 | 使用的 model | 是否更新 |
|------|-------------|----------|
| 正常对话/推理 | `ReActAgent.model` | ✅ 反射替换后生效 |
| 手动压缩 | `harnessAgent.getModel()` → `delegate.getModel()` | ✅ 读取新 model |
| **自动压缩** | `CompactionMiddleware.model` | ❌ 仍用旧 model |
如果需要自动压缩也用新模型,需要额外反射替换 `CompactionMiddleware` 的 model 字段:
```java
// 在 switchModel() 中补充:
CompactionMiddleware cm = harnessAgent.getCompactionHook();
if (cm != null) {
Field cmModelField = cm.getClass().getDeclaredField("model");
cmModelField.setAccessible(true);
cmModelField.set(cm, newModel);
}
```
## 完整调用示例
```bash
# 切换到 DeepSeek
curl -X POST http://localhost:8080/api/model/switch \
-H "Content-Type: application/json" \
-d '{"modelId":"deepseek:deepseek-v4-pro","apiKey":"","baseUrl":""}'
# 切换到智谱
curl -X POST http://localhost:8080/api/model/switch \
-H "Content-Type: application/json" \
-d '{"modelId":"zhipu:glm-5v-turbo","apiKey":"","baseUrl":""}'
# 查询当前模型
curl http://localhost:8080/api/model/current
```
当 `apiKey` 和 `baseUrl` 为空时,会自动回退到 `application.properties` 中配置的对应 provider 的默认值。
贡献指南
评估
这个 Issue 还没有评估数据。