AgentScope-Java 2.0 如何接入模型供应商:从 DashScope 切换到 DeepSeek
AgentScope-Java 2.0 如何接入模型供应商:从 DashScope 切换到 DeepSeek
在 AgentScope-Java 的 agentscope-builder 示例里,BuilderConfig 默认从 application.yml 读取 DashScope 的 API Key、模型名和流式开关,然后创建一个 DashScopeChatModel Bean。
如果想把它换成 DeepSeek,第一反应通常很直接:把 builder.dashscope 改成 builder.deepseek,再把 DashScopeChatModel 换成一个 OpenAI 兼容客户端。
这当然能作为修改的起点,但它还没有触及 AgentScope-Java 2.0 模型接入的真正边界。
我的判断是:AgentScope-Java 的 provider-neutral 不是“所有供应商都一样”,而是应用层不用理解每家 SDK,适配层仍然忠实处理每家协议差异。
从 DashScope 切换到 DeepSeek,表面只涉及一个配置类,背后实际会经过依赖模块、模型注册中心、Provider、创建上下文和 Formatter。把这条链路看清楚,以后再接 GLM、Kimi、MiniMax 或公司内部模型网关,就不需要为每家供应商重新设计 Agent 层。

先看这个示例现在是怎么接 DashScope 的
当前 agentscope-builder 在三个位置绑定了 DashScope。
第一处是 Maven 依赖:
io.agentscope
agentscope-extensions-model-dashscope
第二处是 application.yml:
builder:
dashscope:
api-key: ${DASHSCOPE_API_KEY:}
model-name: ${BUILDER_MODEL_NAME:${CLAW_MODEL_NAME:qwen-max}}
stream: true
第三处是 BuilderConfig。它通过 @Value 读取配置,在 API Key 非空并且 Spring 容器里没有其他 Model Bean 时,创建 DashScopeChatModel:
@Bean
@ConditionalOnMissingBean(Model.class)
@ConditionalOnExpression(
"'${builder.dashscope.api-key:${claw.dashscope.api-key:}}' != ''")
public Model dashscopeModel() {
return DashScopeChatModel.builder()
.apiKey(dashscopeApiKey)
.modelName(dashscopeModelName)
.stream(dashscopeStream)
.build();
}
后面的 BuilderBootstrap 并不知道这个 Bean 来自 DashScope。它只注入 Optional,存在就传给所有 Agent,不存在就让应用在无模型状态下启动。
这已经体现了第一层抽象:Agent 和 BuilderBootstrap 依赖的是 Model,不是具体供应商。
但如果只看到这一层,很容易得出一个过于简单的结论:换供应商,就是再创建一个不同的 Model 实现。
替换成 DeepSeek,最少要改三个位置
DeepSeek 在 AgentScope-Java 里没有独立的 model artifact。它和 GLM、Kimi、MiniMax 一样,放在 OpenAI 模型扩展中,通过专用 Provider 和 Formatter 提供一等适配。
因此,第一步不是寻找一个 agentscope-extensions-model-deepseek,而是把依赖换成:
io.agentscope
agentscope-extensions-model-openai
这一步解决的是 classpath。只有 OpenAI 扩展进入 classpath,Java ServiceLoader 才能发现 DeepSeekModelProvider。
第二步,把配置改成 DeepSeek:
builder:
deepseek:
api-key: ${DEEPSEEK_API_KEY:}
model-name: ${BUILDER_MODEL_NAME:${CLAW_MODEL_NAME:deepseek-chat}}
# 留空时使用 Provider 内置的 https://api.deepseek.com
base-url: ${DEEPSEEK_BASE_URL:}
stream: true
第三步,不直接在配置类里拼 OpenAIChatModel,而是把配置转成 ModelCreationContext,再交给 ModelRegistry:
import io.agentscope.core.model.ModelCreationContext;
import io.agentscope.core.model.ModelRegistry;
@Value("${builder.deepseek.api-key:${claw.deepseek.api-key:}}")
private String deepseekApiKey;
@Value("${builder.deepseek.model-name:${claw.deepseek.model-name:deepseek-chat}}")
private String deepseekModelName;
@Value("${builder.deepseek.base-url:${claw.deepseek.base-url:}}")
private String deepseekBaseUrl;
@Value("${builder.deepseek.stream:${claw.deepseek.stream:true}}")
private boolean deepseekStream;
@Bean
@ConditionalOnMissingBean(Model.class)
@ConditionalOnExpression(
"'${builder.deepseek.api-key:${claw.deepseek.api-key:}}' != ''")
public Model deepseekModel() {
if (deepseekApiKey == null || deepseekApiKey.isBlank()) {
throw new IllegalStateException(
"builder.deepseek.api-key must not be blank");
}
String configuredModel =
deepseekModelName == null ? "" : deepseekModelName.trim();
if (configuredModel.isEmpty()) {
throw new IllegalStateException(
"builder.deepseek.model-name must not be blank");
}
String modelId =
configuredModel.startsWith("deepseek:")
? configuredModel
: "deepseek:" + configuredModel;
ModelCreationContext context =
ModelCreationContext.builder()
.apiKey(deepseekApiKey)
.baseUrl(deepseekBaseUrl)
.stream(deepseekStream)
.build();
return ModelRegistry.resolve(modelId, context);
}
如果使用需要显式开启 thinking 的模型,可以继续在 Context 中增加:
.enableThinking(true)
builderBootstrap() 不需要改变装配逻辑,因为它仍然只接收 Optional。需要同步修改的只是无模型时的提示,把 builder.dashscope.api-key 换成 builder.deepseek.api-key。
项目里几个 Spring 上下文测试还显式设置了 builder.dashscope.api-key=。完整替换时最好一起改成 builder.deepseek.api-key=,避免测试配置和实际 Bean 条件脱节。
到这里,代码改动已经结束。但这套写法真正值得解释的,不是它少写了多少行,而是 ModelRegistry.resolve() 接手以后发生了什么。

一次 resolve,背后走过五层边界
这段代码:
ModelRegistry.resolve("deepseek:deepseek-chat", context);
实际会走过下面这条链路:
agentscope-extensions-model-openai 进入 classpath
↓
ModelRegistry 解析 deepseek:deepseek-chat
↓
ServiceLoader 发现 DeepSeekModelProvider
↓
Provider 消费 ModelCreationContext
↓
创建 OpenAIChatModel,并装配 DeepSeekFormatter
ModelRegistry 先检查有没有同名注册实例,再检查缓存,然后匹配用户注册的 ModelFactory,最后才查找 SPI Provider。用户工厂优先于内置 Provider,所以应用以后可以把相同模型 id 接到企业网关、租户路由器或者测试替身,而不必改 Agent 代码。
DeepSeekModelProvider 匹配 deepseek:.+。创建模型时,它会:
- 优先读取
ModelCreationContext中的 API Key,否则回退到DEEPSEEK_API_KEY; - 去掉模型 id 前面的
deepseek:; - 在没有覆盖时使用
https://api.deepseek.com; - 根据 Context 设置 stream 和 thinking;
- 创建共用的
OpenAIChatModel; - 注入
DeepSeekFormatter; - 默认关闭 native structured output,并查询模型上下文窗口。
所以,ModelRegistry 不是一个字符串版 Builder。它解决的是模型如何被发现、用户配置如何进入供应商适配层,以及应用如何在不依赖具体 Builder 的情况下创建模型。

兼容 OpenAI,为什么还需要 DeepSeekFormatter
常见理解是:只要供应商兼容 OpenAI API,改一下 baseUrl 和 API Key 就接完了。
源码给出的答案正好相反:OpenAI、DeepSeek、GLM、Kimi、MiniMax 虽共用 OpenAIChatModel 和 HTTP 调用骨架,却分别注册 ModelProvider,并使用专用 Formatter。原因不是框架过度设计,而是它们在 thinking 参数、tool_choice、reasoning content、结构化输出和上下文窗口上并不等价。
以 DeepSeek 为例,思考模型不只是多返回一段文字。历史消息中哪些 reasoning_content 可以继续发送,哪些应该移除,工具调用所在分段是否需要保留推理内容,都会影响下一轮请求是否合法。
如果直接创建一个普通 OpenAIChatModel,只把地址改成 DeepSeek,基础文本对话可能可以返回结果,但进入多轮 thinking 和工具调用后,错误才会暴露出来。
其他兼容供应商也有类似问题:
- GLM 的
response_format只支持json_object,不能当作完整的json_schema; - GLM 对
tool_choice的支持范围更窄,部分选择需要降级; - Kimi 的部分模型在 thinking 开启时不能强制指定工具,否则可能返回 HTTP 400;
- Kimi 不接受通用的
thinking_budget,需要根据模型使用thinking或reasoning_effort; - MiniMax 的 thinking 类型使用
adaptive,而 DeepSeek 和 GLM 使用enabled。
这些差异都发生在“HTTP 请求已经能够发出去”之后。返回 200 只能证明传输打通,不能证明模型已经适合 Agent 的多轮工具调用和结构化输出。
Formatter 才是 AgentScope-Java 里真正承接供应商语义差异的层。
5 个模型 artifact,为什么能解析 9 个 provider id
当前源码有 5 个主要模型 artifact:
| artifact | 主要模型实现 |
|---|---|
agentscope-extensions-model-openai |
OpenAIChatModel |
agentscope-extensions-model-anthropic |
AnthropicChatModel |
agentscope-extensions-model-dashscope |
DashScopeChatModel |
agentscope-extensions-model-gemini |
GeminiChatModel |
agentscope-extensions-model-ollama |
OllamaChatModel |
但通过 SPI 可以解析 9 个 provider id:
openai
deepseek
glm
kimi
minimax
anthropic
dashscope
gemini
ollama
原因就在 OpenAI artifact。它的 SPI 服务文件一次注册了 OpenAI、DeepSeek、GLM、Kimi、MiniMax 五个 Provider。它们共用模型调用骨架,各自保留 Provider 和 Formatter。
这比“每家供应商复制一套完整客户端”和“所有兼容供应商只换地址”更接近真实边界:能共用的传输代码继续共用,不能共用的协议语义单独处理。
ModelCreationContext 解决的是动态接入,不只是配置搬家
在 agentscope-builder 这个例子里,ModelCreationContext 只是把 Spring 配置传给 Provider。它的价值看起来还不明显,因为整个应用只有一个全局 Model Bean。
到了多租户平台,情况会改变。同一个 deepseek:deepseek-chat,不同租户可能使用不同 API Key、代理地址、stream 策略和 thinking 配置。插件系统也可能在运行时才知道该加载哪个模型。
ModelCreationContext 为这类场景提供了供应商中立的创建输入:
- 标准字段承载 API Key、base URL、endpoint path、stream、thinking 和缓存策略;
option(key, value)传供应商约定的扩展值;component(type, value)传 Formatter、Transport、代理和 GenerateOptions 等复杂组件。
core 不需要知道这些复杂组件属于哪家供应商,真正消费它们的是扩展模块。
这里还有一个容易忽略的边界:带非空 Context 的解析默认不缓存。
这不是单纯的性能选择。它避免租户 A 的 API Key 和端点创建出的 Model 被租户 B 复用。如果显式开启缓存,又同时传了 options 或 components,框架会要求提供 cacheId,而不是猜测一个供应商对象能不能安全参与哈希。
对于多 Agent 协作平台,模型配置因此不应该只保存 baseUrl + apiKey + modelName。至少还要考虑 provider id、凭证引用、thinking、能力声明、Formatter / Transport 扩展和租户级缓存身份。
Spring Boot 配置和 ModelRegistry 是两条入口
还需要分清一件事:agentscope-builder 当前是在自己的 BuilderConfig 中创建 Model Bean,并没有直接使用模型 starter 的自动配置。
AgentScope-Java 当前为 OpenAI、DashScope、Anthropic、Gemini、Ollama 提供了对应的 Spring Boot starter。它们根据 agentscope.model.provider 和供应商属性创建 Model Bean,核心 starter 再用这个 Bean 组装 ReActAgent。
这条 Spring Boot 路径与 .model("deepseek:...") 的 SPI 字符串解析不是同一个入口。
在当前示例中,保留手写 BuilderConfig 的好处是继续沿用 builder.* 配置,并且能明确控制 Bean 的创建条件。改成 ModelRegistry + ModelCreationContext 后,又可以复用 DeepSeek 的内置 Provider 和 Formatter。
如果直接使用 OpenAI starter,把 base-url 指向 DeepSeek,也不是绝对不行。但只要涉及 thinking、工具调用或结构化输出,就必须确认 Builder 最终装配的是 DeepSeekFormatter。否则只是网络端点兼容,语义适配仍然缺失。
重新理解“接入一个模型供应商”
- 原有想法:接入供应商,就是实现统一
Model接口,或给 OpenAI 客户端换一个地址。 - 问题所在:这种理解只覆盖了“能发请求”,没有覆盖模型如何被发现、凭证如何注入、消息和工具如何转换、响应如何解析,以及能力边界如何声明。
- 真正原因:一个可运行的模型接入至少横跨 classpath、注册发现、配置上下文、协议语义和能力测试五个边界。
- 行动方向:使用内置供应商时先选对 artifact 和 provider id;接 OpenAI-compatible 厂商时先检查专用 Provider / Formatter;开发新供应商时按 SPI、Model、Formatter、Transport、测试五层补齐,而不是只写一个 Builder。
回到最开始的 agentscope-builder 示例,正确的修改不复杂:换成 OpenAI model artifact,把配置改成 DeepSeek,再让 ModelRegistry 使用 ModelCreationContext 创建模型。
真正重要的是,这样修改以后,Agent 层仍然只依赖 Model,DeepSeek 的凭证、地址和协议差异都停留在模型接入层。将来换 GLM、Kimi,或者把所有模型请求接到内部网关时,BuilderBootstrap、HarnessAgent 和多 Agent 编排都不需要跟着重写。
如果只是做一个固定供应商、短生命周期的 Agent,直接显式创建 ChatModel 已经够用。
但如果你想尝试搭建 Java 版多 Agent 协作平台,可以去了解一下 AgentScope-Java 2.0。除了 HarnessAgent、Workspace、权限和状态恢复,也值得看它如何把模型发现、租户配置和供应商语义拆开。模型层边界稳定以后,多 Agent 编排才不会被某一家供应商绑死。
更多推荐



所有评论(0)