Solon v3.9.4

示例:应用参考

</> markdown
2026年2月26日 下午9:44:35

以下仅作参考

1、ToolGatewaySkill:动态工具网关技能

解决痛点:当插件库中有成百上千个工具时,一次性全部注入会导致上下文(Token)溢出及模型幻觉。通过网关模式,实现工具的“按需发现”与“延迟加载”。

// 即使后台有 1000 个工具,Agent 启动时也只加载 2 个网关工具
ChatModel model = ChatModel.of(config)
    .defaultSkillAdd(new ToolGatewaySkill().addTool(mcpClient)) 
    .build();

// 模型会先调用 search_tools 发现具体的“发票开具”工具,再通过 call_tool 执行它
model.prompt("帮我开一张 100 元的餐饮发票").call();

ToolGatewaySkill

package org.noear.solon.ai.chat.tool;

import org.noear.solon.Utils;
import org.noear.solon.ai.annotation.ToolMapping;
import org.noear.solon.ai.chat.prompt.Prompt;
import org.noear.solon.ai.chat.skill.AbsSkill;
import org.noear.solon.annotation.Param;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.*;
import java.util.stream.Collectors;

/**
 * 工具网关技能:解决工具过多导致的上下文溢出问题。
 *
 * 支持三阶段模式自动切换:
 * 1. FULL: 数量 <= dynamicThreshold,全量平铺。
 * 2. DYNAMIC: 数量 <= searchThreshold,指令内展示清单。
 * 3. SEARCH: 数量 > searchThreshold,强制搜索。
 *
 * @author noear
 * @since 3.9.5
 */
public class ToolGatewaySkill extends AbsSkill {
    private static final Logger LOG = LoggerFactory.getLogger(ToolGatewaySkill.class);

    private final Map<String, FunctionTool> dynamicTools = new LinkedHashMap<>();
    private int dynamicThreshold = 15; // 超过此值,不再平铺 Schema,进入清单模式
    private int searchThreshold = 50;  // 超过此值,不再展示清单,进入强制搜索模式

    public ToolGatewaySkill dynamicThreshold(int dynamicThreshold) {
        this.dynamicThreshold = dynamicThreshold;
        return this;
    }

    public ToolGatewaySkill searchThreshold(int searchThreshold) {
        this.searchThreshold = searchThreshold;
        return this;
    }

    /**
     * 添加工具
     */
    public ToolGatewaySkill addTool(ToolProvider toolProvider) {
        if (toolProvider != null) {
            for (FunctionTool tool : toolProvider.getTools()) {
                addTool(tool);
            }
        }
        return this;
    }

    /**
     * 添加工具
     */
    public ToolGatewaySkill addTool(FunctionTool tool) {
        if (tool != null) {
            dynamicTools.put(tool.name().toLowerCase(), tool);
        }
        return this;
    }

    @Override
    public String getInstruction(Prompt prompt) {
        if (dynamicTools.isEmpty()) {
            return "#### 工具网关\n当前暂无业务工具。";
        }

        int size = dynamicTools.size();
        StringBuilder sb = new StringBuilder();
        sb.append("#### 业务工具发现规范 (共 ").append(size).append(" 个工具)\n");

        if (size <= dynamicThreshold) {
            // FULL 模式:直接交付给 AI
            sb.append("当前已加载全量业务工具定义,请分析需求并直接调用。");
        } else {
            // 引导 AI 走中转流程
            sb.append("由于业务工具库较多,已开启**动态路由**模式。请严格遵循以下步骤:\n");

            if (size > searchThreshold) {
                // 优化点 1: SEARCH 模式指令,强调必须搜索
                sb.append("- **Step 1 (搜索)**: 业务清单已折叠。请务必先使用 `search_tools` 寻找匹配的工具名。\n");
            } else {
                // 优化点 2: DYNAMIC 模式指令,清单可见,搜索作为辅助
                sb.append("- **Step 1 (锁定)**: 从下方清单确定工具名。如描述模糊,可使用 `search_tools` 进一步搜索。\n");
            }

            sb.append("- **Step 2 (详情)**: 使用 `get_tool_detail` 获取选定工具的参数定义 (JSON Schema)。\n");
            sb.append("- **Step 3 (执行)**: **必须**通过 `call_tool` 执行。禁止直接调用业务工具名。\n\n");

            if (size > searchThreshold) {
                sb.append("> **提示**: 工具量大,建议通过关键词搜索,例如:search_tools('天气')。");
            } else {
                // 展示摘要清单
                sb.append("### 可用业务清单:\n");
                for (FunctionTool tool : dynamicTools.values()) {
                    sb.append("- **").append(tool.name()).append("**: ").append(tool.description()).append("\n");
                }
            }
        }

        return sb.toString();
    }

    @Override
    public Collection<FunctionTool> getTools(Prompt prompt) {
        if (dynamicTools.size() <= dynamicThreshold) {
            return dynamicTools.values();
        } else {
            return this.tools;
        }
    }

    @ToolMapping(name = "search_tools", description = "在海量工具库中通过关键词模糊搜索工具名和描述")
    public Object searchTools(@Param("keyword") String keyword) {
        if (Utils.isEmpty(keyword)) return "错误:搜索关键词不能为空。";

        String k = keyword.toLowerCase().trim();
        List<Map<String, String>> results = dynamicTools.values().stream()
                .filter(t -> t.name().toLowerCase().contains(k) || t.description().toLowerCase().contains(k))
                .limit(10)
                .map(this::mapToolBrief)
                .collect(Collectors.toList());

        if (results.isEmpty()) {
            return "未找到与关键词 '" + keyword + "' 相关的业务工具。\n" +
                    "建议:尝试更通用的词汇(如用'天气'代替'下雨'),或确认功能是否超出目前支持范围。";
        }

        return results;
    }

    @ToolMapping(name = "get_tool_detail", description = "获取指定业务工具的完整参数 Schema")
    public String getToolDetail(@Param("tool_name") String name) {
        if (Utils.isEmpty(name)) return "错误:tool_name 不能为空";

        FunctionTool tool = dynamicTools.get(name.trim().toLowerCase());
        if (tool != null) {
            return "### 工具详情: " + tool.name() + "\n" +
                    "- 功能描述: " + tool.description() + "\n" +
                    "- 参数架构 (JSON Schema): \n```json\n" + tool.inputSchema() + "\n```";
        }

        return "错误:未找到工具 '" + name + "',请检查名称拼写是否正确。";
    }

    @ToolMapping(name = "call_tool", description = "代理执行特定的业务工具")
    public ToolResult callTool(@Param("tool_name") String name,
                               @Param("tool_args") Map<String, Object> args) {
        if (Utils.isEmpty(name)) {
            return ToolResult.success("错误:tool_name 不能为空");
        }

        FunctionTool tool = dynamicTools.get(name.trim().toLowerCase());

        if (tool == null) {
            return ToolResult.success("错误:未找到工具 '" + name + "'");
        }

        try {
            return tool.call(args);
        } catch (Throwable e) {
            LOG.error("Tool gateway execution failed: {}", name, e);
            return ToolResult.success("执行异常: " +
                    (e.getMessage() != null ? e.getMessage() : e.getClass().getSimpleName()));
        }
    }

    private Map<String, String> mapToolBrief(FunctionTool t) {
        Map<String, String> map = new HashMap<>();
        map.put("tool_name", t.name());
        map.put("tool_description", t.description());
        return map;
    }
}

2、OrderIntentSkill:意图感知的订单技能

解决痛点:通过 isSupported 实现“精准空降”。只有在用户聊到订单相关话题时,对应的指令和工具才会进入上下文,极大提高了非相关对话(如闲聊)时的响应速度和准确度。

ChatModel model = ChatModel.of(config)
    .defaultSkillAdd(new OrderIntentSkill())
    .build();

// 场景 A:闲聊。isSupported 返回 false,模型不会看到订单工具,也不会被订单指令干扰。
model.prompt("今天天气不错").call();

// 场景 B:业务。isSupported 返回 true,模型瞬间获得订单处理专家能力。
model.prompt("我昨天的订单到哪了?").call();

OrderIntentSkill

/**
 * 意图感知的订单技能
 */
public class OrderIntentSkill extends AbsSkill {
    // 定义该技能关心的意图关键词
    private static final List<String> INTENT_KEYWORDS = Arrays.asList("订单", "买过", "物流", "发货", "退款");

    @Override
    public String name() {
        return "order_manager";
    }

    @Override
    public String description() {
        return "处理所有与订单、物流相关的业务请求";
    }

    /**
     * 核心:准入检查
     * 只有当用户提问包含相关关键词时,此技能才会被激活
     */
    @Override
    public boolean isSupported(Prompt prompt) {
        String content = prompt.getUserContent();
        if (content == null) return false;

        // 简单的关键词匹配(生产环境可以换成正则或微型分类模型)
        return INTENT_KEYWORDS.stream().anyMatch(content::contains);
    }

    @Override
    public String getInstruction(Prompt prompt) {
        return "1. 查询订单前,请确认用户是否提供了订单号或手机号。\n" +
                "2. 如果物流状态显示为'已签收',主动询问用户对商品的满意度。";
    }

    @ToolMapping(description = "根据订单号查询详细的物流信息")
    public String getOrderLogistics(@Param("orderId") String orderId) {
        return "订单 " + orderId + " 正在上海分拣中心处理中...";
    }
}

3、TechnicalSupportSkill:多级决策与 RAG 结合

解决痛点:展示了复杂的 SOP(标准作业程序) 控制。Skill 可以根据 Prompt 内容动态调整指令优先级,并利用工具元数据(Meta)触发安全确认,实现 RAG(检索增强生成)与人工干预的闭环。

ChatModel model = ChatModel.of(config)
    .defaultSkillAdd(new TechnicalSupportSkill())
    .build();

// 即使知识库没搜到,模型也会根据 SOP 引导,在最终转人工前执行确认逻辑
model.prompt("Solon AI 如何配置拦截器?知识库搜不到。").call();

TechnicalSupportSkill

/**
 * 技术支持技能:展示多级决策与 RAG 结合
 */
public class TechnicalSupportSkill extends AbsSkill implements Skill {
    public TechnicalSupportSkill() {
        super();
        this.metadata().category("support").tags("rag", "helpdesk").sensitive(false);
    }

    @Override
    public String name() {
        return "tech_support";
    }

    @Override
    public String description() {
        return "多级技术支持与知识库检索";
    }

    @Override
    public String getInstruction(Prompt prompt) {
        String userMessage = prompt.getUserContent();

        // 如果用户直接贴了代码或错误栈,调整 SOP 优先级
        if (userMessage.contains("StackOverflow") || userMessage.contains("Exception")) {
            return "检测到具体异常,请跳过基础搜索,优先调用 'search_online_docs' 查找最新补丁。";
        }

        return "处理技术咨询时必须遵循以下 SOP:\n" +
                "1. 优先调用 'search_knowledge_base' 获取标准答案。\n" +
                "2. 如果知识库无法解决或信息过时,调用 'search_online_docs' 获取最新文档。\n" +
                "3. 只有当前两步都无法给出确切方案,且用户问题属于紧急故障时,才允许调用 'create_human_ticket'。";
    }

    @ToolMapping(description = "检索内部私有知识库 (已核实的标准操作手册)")
    public String searchKnowledgeBase(@Param("query") String query) {
        // 模拟 RAG 检索
        if (query.contains("安装")) {
            return "知识库命中:请运行 'npm install solon-ai' 并配置 API Key。";
        }
        return "知识库未命中相关词条。";
    }

    @ToolMapping(description = "检索实时在线开发文档 (最新 API 变更和社区讨论)")
    public String searchOnlineDocs(@Param("url_path") String path) {
        // 模拟爬虫或文档 API
        return "在线文档显示:3.8.4 版本后,Skill 接口新增了 injectInstruction 方法。";
    }

    @ToolMapping(
            description = "创建人工支持工单",
            // 增加确认话术和操作等级
            meta = "{'danger':3, 'confirm_msg':'确定要转接到人工座席吗?可能会产生额外费用。'}"
    )
    public String createHumanTicket(@Param("issue") String issue, @Param("urgency") String urgency) {
        // 标记为敏感/破坏性操作,会触发你在 ReActSystemPrompt 里的安全约束
        return "工单已提交成功,工单号:TICK-" + System.currentTimeMillis();
    }
}