devto 2026-07-20 원문 보기 ↗
When an agent can see 80 tools at once, the model does not get smarter. It gets noisier. Token bills climb, tool choice drifts, and a simple “check order status” prompt suddenly carries half of your company’s OpenAPI surface.
Solon AI (v4.0.3) answers that with the Gateway Talent trio in solon-ai-talent-gateway:
| Talent | Source of tools | Unit of management |
|---|---|---|
OpenApiGatewayTalent |
OpenAPI / Swagger docs | one API source |
ToolGatewayTalent |
local / MCP FunctionTools |
one tool, runtime add/remove |
McpGatewayTalent |
MCP client connections | one MCP server, plus allow/deny lists |
All three share the same four-stage adaptive discovery model. Small catalogs stay fully expanded. Large catalogs fold into summary → name list → search, so the LLM only pays for what it needs.
This article sticks to official APIs from article/1353, article/1389, article/1335, and article/1293.
In Solon AI, a Tool is an atomic function. A Talent packages tools with instruction, activation, and SOP-style constraints.
Gateways need that packaging:
That is why you hang gateways with defaultTalentAdd(...) (or request-scoped talentAdd), not by dumping every OpenAPI operation into defaultToolAdd.
<dependency>
<groupId>org.noear</groupId>
<artifactId>solon-ai-talent-gateway</artifactId>
</dependency>
MCP features also need solon-ai-mcp. OpenAPI parsing pulls in swagger-parser (v2 and v3); exclude it only if you never load docs.
| Stage | Trigger (defaults) | What the model sees | Proxy tools exposed |
|---|---|---|---|
| FULL | count <= dynamicThreshold (8) |
full tool schemas | original tools (OpenAPI collapses to call_api) |
| SUMMARY |
8 < count <= listThreshold (30 OpenAPI / 40 others) |
name + description (+ endpoint) |
get_*_detail + call_*
|
| LIST |
listThreshold < count <= searchThreshold (100) |
grouped names only |
search_* + get_*_detail + call_*
|
| SEARCH | count > 100
|
no catalog dump | forced search path: search_* → detail → call |
Design rule from the docs: when the catalog is small, embed full schemas in the instruction so the model can call in one step; when it grows, fold information and force progressive discovery.
Shared knobs:
| Method | Default | Notes |
|---|---|---|
dynamicThreshold(n) |
8 | FULL ceiling |
listThreshold(n) |
30 (OpenAPI) / 40 (others) | SUMMARY ceiling |
searchThreshold(n) |
100 | LIST ceiling |
retryConfig(maxRetries) |
3 | shared |
retryConfig(maxRetries, retryDelayMs) |
3, 1000 | McpGatewayTalent delay form |
maxContextLength(n) |
8000 | OpenApiGatewayTalent response truncate |
Use this when tools already exist as OpenAPI / Swagger documents (remote http:// or local classpath:).
Capabilities called out officially:
$ref expansion and circular-ref markersmaxContextLength)allowedTools / disallowedTools
ApiAuthenticator (bearer / apiKey / custom)@Deprecated operations
import org.noear.solon.ai.agent.react.ReActAgent;
import org.noear.solon.ai.talents.gateway.OpenApiGatewayTalent;
import org.noear.solon.ai.talents.gateway.openapi.ApiAuthenticator;
import org.noear.solon.ai.talents.gateway.openapi.ApiSource;
import java.time.Duration;
import java.util.Arrays;
OpenApiGatewayTalent apiTalent = new OpenApiGatewayTalent()
.dynamicThreshold(5)
.listThreshold(30)
.searchThreshold(100)
.maxContextLength(10_000)
.defaultTimeout(Duration.ofSeconds(30))
.defaultAuthenticator(ApiAuthenticator.bearer("your-token"));
// simple multi-source mount
apiTalent.addApi("http://order-service:8081/v3/api-docs", "http://order-service:8081");
apiTalent.addApi("classpath:swagger/user-api.json", "http://user-service:8082");
// fine-grained source
ApiSource pay = new ApiSource();
pay.setDocUrl("http://pay-service:8083/v3/api-docs");
pay.setApiBaseUrl("http://pay-service:8083");
pay.setAllowedTools(Arrays.asList("createPayment", "queryPayment"));
pay.setAuthenticator(ApiAuthenticator.bearer("pay-service-token"));
pay.setTimeout(Duration.ofSeconds(60));
apiTalent.addApi(pay);
ReActAgent agent = ReActAgent.of(chatModel)
.role("Senior business assistant")
.instruction("Prefer the provided business APIs. Do not invent path or body fields.")
.defaultTalentAdd(apiTalent)
.build();
// runtime lifecycle
apiTalent.removeApi("http://order-service:8081/v3/api-docs");
apiTalent.refreshApi("http://pay-service:8083/v3/api-docs");
apiTalent.refreshApiAll();
Built-in OpenAPI proxy tools by stage:
| Tool | Modes | Role |
|---|---|---|
call_api |
all | execute REST call (path/query/body/multipart) |
get_api_detail |
SUMMARY / LIST / SEARCH | full parameter schema |
search_apis |
LIST / SEARCH | multi-keyword AND search |
Auth priority: source authenticator > defaultAuthenticator.
Runtime permission edits happen on the ApiSourceClient copy, then refreshApi(...):
var client = apiTalent.getApiSource("http://pay-service:8083/v3/api-docs");
client.setAllowedTools(Arrays.asList("createPayment"));
client.getDisallowedTools().add("queryRefund");
apiTalent.refreshApi("http://pay-service:8083/v3/api-docs");
Use this when tools live in code — AbsToolProvider, single FunctionTool, or already-fetched MCP tools — and you need runtime, imperative add/remove at tool granularity.
import org.noear.solon.ai.talents.gateway.ToolGatewayTalent;
ToolGatewayTalent toolGateway = new ToolGatewayTalent()
.dynamicThreshold(8)
.listThreshold(40)
.searchThreshold(100);
toolGateway.addTool("Finance", financeToolProvider);
toolGateway.addTool("HR", hrToolProvider);
toolGateway.addTool("General", singleFunctionTool);
ReActAgent agent = ReActAgent.of(chatModel)
.role("Enterprise ops assistant")
.defaultTalentAdd(toolGateway)
.build();
Built-in proxy tools:
| Tool | Modes | Role |
|---|---|---|
call_tool |
SUMMARY / LIST / SEARCH | proxy execute by tool_name + tool_args
|
get_tool_detail |
SUMMARY / LIST / SEARCH | full JSON Schema |
search_tools |
LIST / SEARCH | keyword search |
Important FULL-mode note from the docs: original business tools are exposed directly to the LLM; the proxy trio appears only after the catalog leaves FULL.
Use this when tools arrive as MCP services and you prefer managing connections (with optional allow/deny lists) over hand-picking every tool at runtime.
import org.noear.solon.ai.mcp.client.McpServerParameters;
import org.noear.solon.ai.talents.gateway.McpGatewayTalent;
import java.util.Arrays;
McpGatewayTalent mcpGateway = new McpGatewayTalent()
.dynamicThreshold(8)
.listThreshold(40)
.searchThreshold(100)
.retryConfig(3, 1000);
mcpGateway.addMcpServer("weather", new McpServerParameters().then(e -> {
e.setTransport("stdio");
e.setCommand("npx");
e.addArgVar("-y");
e.addArgVar("@modelcontextprotocol/server-weather");
}));
mcpGateway.addMcpServer("database", new McpServerParameters().then(e -> {
e.setTransport("stdio");
e.setCommand("npx");
e.setArgs(Arrays.asList("-y", "@modelcontextprotocol/server-database"));
e.setAllowedTools(Arrays.asList("query", "list_tables"));
e.setDisallowedTools(Arrays.asList("drop_table", "execute_raw"));
}));
// alternative: pass an existing McpClientProvider
// mcpGateway.addMcpServer("database", provider);
ReActAgent agent = ReActAgent.of(chatModel)
.role("Ops agent with remote MCP tools")
.defaultTalentAdd(mcpGateway)
.build();
mcpGateway.removeMcpServer("weather"); // remove + close connection
mcpGateway.refreshMcpServer("database"); // after allow/deny changes
mcpGateway.refreshMcpServerAll();
Official lifecycle details worth keeping:
removeMcpServer closes the underlying connectionMcpClientProvider.setEnabled(false) only drops the tool index; the provider stays alive until re-enabled + refreshProxy tool names match ToolGatewayTalent: call_tool / get_tool_detail / search_tools.
Pick by ingress shape and mutation unit:
| If your tools… | Prefer |
|---|---|
| are OpenAPI / Swagger documents | OpenApiGatewayTalent |
are code-side FunctionTools you add/remove at runtime |
ToolGatewayTalent |
| come from MCP servers you want to attach as wholes | McpGatewayTalent |
Common mistake: “I need per-tool MCP control, so I must use ToolGatewayTalent.”
Not required. McpGatewayTalent already supports tool-level exposure through allowedTools / disallowedTools on McpServerParameters. After changing lists, call refreshMcpServer(...).
Real split:
You can also hang more than one gateway Talent on the same agent when sources differ.
{name} — gateway URL-encodes replacements; leftover {xxx} fails loudly.ApiSource.setEnabled(false) / provider setEnabled(false)) stay registered for management but leave the tool index until re-enabled + refresh.dynamicThreshold earlier if FULL schemas already dominate context; raise it only when the catalog is tiny and high-precision one-shot calls matter.Stay on AbsToolProvider + @ToolMapping + defaultToolAdd while the set is small and always relevant.
Move to a Gateway Talent when any of these becomes true:
Gateways do not replace ReAct planning, HITL, or session memory. They solve a narrower, painful problem: keep tool surfaces usable as they scale.
solon-ai-talent-gateway: https://solon.noear.org/article/1389
talents field: https://solon.noear.org/article/1293
Solon version used for this write-up: v4.0.3.