agentscope-ai / agentscope-ai/agentscope-java

[Feature]: HarnessAgent 单实例时,如何动态切换模型?好像不支持

Abierto
#2,085 1 comentario 0 reacciones 0 asignados Ver en GitHub
area/harness enhancement
Lenguaje dominante
Java
Estrellas
5.6k
Forks
1.3k
Merge medio
4 d 12 h
PR fusionados (30 d)
77

Descripción

需求场景:
容器部署项目,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 的默认值。

Guía de contribución

Abrir la guía de contribución

Evaluación

Este issue todavía no se ha evaluado.

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.