Skip to content

技能系统

技能系统允许通过 Markdown 文件定义复杂任务的执行步骤,LLM 按照步骤调用工具完成任务。

概述

什么是技能

技能是对复杂任务的抽象,包含:

  • 触发场景(什么情况下使用)
  • 执行步骤(按什么顺序调用工具)
  • 参数说明(每个工具需要什么参数)
  • 输出格式(结果应该如何呈现)

为什么需要技能

场景没有技能有技能
复杂任务LLM 需要多轮试错按步骤一次完成
参数传递LLM 可能遗漏参数步骤中明确参数来源
结果格式格式不统一模板化输出
可维护性改 Prompt 需重新调优改 Markdown 即可

技能定义

文件位置

技能定义文件放在 resources/skills/ 目录下:

src/main/resources/skills/
├── recommend_manual.md    # 功法推荐
├── recommend_task.md      # 任务推荐
└── ...

文件格式

markdown
# 功法推荐

根据修士的境界、灵根、身份,推荐最适合修炼的功法。

**触发场景**:推荐功法|适合修炼的功法|能买什么功法

## 前置条件

**如果已知修士详细信息**,可直接使用,无需查询。

**如果未知修士信息**,调用工具查询:

调用工具:`query_user_detail`

${tool:query_user_detail}

## 执行步骤

### 步骤1:查询功法列表

调用工具:`page_manuals`

${tool:page_manuals}

**参数建议**
- `realmMin``realmMax`:设为修士当前境界
- `element`:设为修士灵根属性

### 步骤2:查询兑换规则

调用工具:`query_exchange_rules`

${tool:query_exchange_rules}

## 分析阶段

综合以上信息,按以下维度筛选和排序:

### 1. 匹配度评估

| 匹配等级 | 条件 | 说明 |
|------|------|------|
| 完美匹配 | 境界匹配 + 灵根属性匹配 | 最适合修炼 |

## 输出格式

请按以下结构输出推荐结果:

### 🌟 强烈推荐

| 功法名 | 品级 | 属性 | 匹配度 | 价格 |
|-----|----|----|-----|------|

关键元素

1. 标题

第一行的一级标题作为技能名称:

markdown
# 功法推荐

2. 触发场景

**触发场景**: 后面的内容用于匹配用户意图:

markdown
**触发场景**:推荐功法|适合修炼的功法|能买什么功法

多个触发词用 | 分隔。

3. 工具占位符

${tool:工具名} 会被自动替换为工具的详细参数格式:

markdown
${tool:query_user_detail}

替换后:

【工具信息】
名称:query_user_detail
描述:查询修士的详细信息
权限要求:峰主

【参数说明】
{
  "type": "object",
  "properties": {
    "userName": {
      "type": "string",
      "description": "修士的道号或姓名"
    }
  },
  "required": ["userName"]
}

4. 执行步骤

清晰的步骤指导 LLM 如何执行:

markdown
### 步骤1:查询功法列表

调用工具:`page_manuals`

**参数建议**
- `realmMin``realmMax`:设为修士当前境界

技能加载

AgentCapabilityRegistry

技能在应用启动时自动加载:

java
@Component
public class AgentCapabilityRegistry implements SmartInitializingSingleton {

    private static final String SKILLS_LOCATION = "classpath*:skills/*.md";

    private final Map<String, String> skillGuides = new ConcurrentHashMap<>();
    private final Map<String, SkillMeta> skillMetas = new ConcurrentHashMap<>();

    @Override
    public void afterSingletonsInstantiated() {
        registerTools();
        loadSkills();
    }

    private void loadSkills() {
        PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        Resource[] resources = resolver.getResources(SKILLS_LOCATION);
        for (Resource resource : resources) {
            loadSkillFromMarkdown(resource);
        }
    }

    private void loadSkillFromMarkdown(Resource resource) {
        String filename = resource.getFilename();
        String skillId = filename.substring(0, filename.length() - 3); // 去掉 .md
        String content = new String(resource.getInputStream().readAllBytes(), UTF_8);

        // 提取元信息
        SkillMeta meta = extractMeta(skillId, content);
        skillMetas.put(skillId, meta);

        // 替换工具占位符
        String guide = replaceToolPlaceholders(content);
        skillGuides.put(skillId, guide);
    }
}

元信息提取

从 Markdown 中提取技能元信息:

java
private SkillMeta extractMeta(String skillId, String content) {
    String name = skillId;
    String description = "";
    String trigger = "";

    for (String line : content.split("\n")) {
        line = line.trim();
        if (line.startsWith("# ")) {
            name = line.substring(2).trim();  // 标题作为名称
        } else if (line.startsWith("**触发场景**:")) {
            trigger = line.substring("**触发场景**:".length()).trim();
        } else if (description.isEmpty() && !line.startsWith("#") && !line.isEmpty()) {
            description = line;  // 第一段非空文本作为描述
        }
    }

    return new SkillMeta(skillId, name, description, trigger);
}

工具占位符替换

${tool:xxx} 替换为工具的详细参数格式:

java
private String replaceToolPlaceholders(String content) {
    Matcher matcher = TOOL_PLACEHOLDER.matcher(content);
    StringBuilder result = new StringBuilder();

    while (matcher.find()) {
        String toolName = matcher.group(1);
        String toolSchema = getToolSchema(toolName);
        matcher.appendReplacement(result, Matcher.quoteReplacement(toolSchema));
    }
    matcher.appendTail(result);

    return result.toString();
}

技能使用

LLM 调用流程

System Prompt 中的技能列表

java
private String buildSystemPrompt(AgentContext context, XiuxianUserEntity user) {
    String skillList = capabilityRegistry.getSkillList();
    // ...
}

// getSkillList() 返回:
// - **功法推荐** (recommend_manual): 根据修士的境界、灵根、身份,推荐最适合修炼的功法。
//   触发场景:推荐功法|适合修炼的功法
// - **任务推荐** (recommend_task): 根据修士的境界和身份,推荐适合的任务。
//   触发场景:推荐任务|适合我的任务

最佳实践

1. 明确触发场景

使用多个同义词,提高匹配率:

markdown
**触发场景**:推荐功法|适合修炼的功法|能买什么功法|有什么功法适合

2. 步骤清晰

每个步骤包含:

  • 步骤名称
  • 调用工具
  • 参数建议
  • 预期结果
markdown
### 步骤1:查询修士信息

调用工具:`query_user_detail`

**参数建议**
- `userName`:当前用户名

**预期结果**:获取境界、灵根类型、灵根属性、灵石余额

3. 参数来源明确

说明参数从哪里获取:

markdown
**如果已知修士详细信息**(境界、灵根类型),可直接使用,无需查询。

**如果未知修士信息**,调用工具查询:
- 从步骤1的结果中提取境界、灵根类型

4. 输出格式模板

提供输出格式示例,保证结果一致性:

markdown
## 输出格式

### 🌟 强烈推荐(完美匹配且能购买)

| 功法名 | 品级 | 属性 | 匹配度 | 灵石价格 | 推荐支付方式 |
|-----|----|----|-----|------|--------|

5. 引导用户

在结果末尾添加引导:

markdown
> 💰 **资源不足?** 可以通过以下方式获取:
> - 完成任务获取灵石和贡献点
> - 输入「推荐任务」查看适合你的任务

示例:任务推荐

markdown
# 任务推荐

根据修士的境界、身份、当前状态,推荐最适合的任务。

**触发场景**:推荐任务|适合我的任务|有什么任务|任务推荐

## 前置条件

调用工具:`query_user_detail`

${tool:query_user_detail}

从结果中提取:**境界、身份、状态**

## 执行步骤

### 步骤1:查询任务列表

调用工具:`page_tasks`

${tool:page_tasks}

**参数建议**
- `status`:设为 `1`(进行中的任务)
- `realmMin``realmMax`:设为修士当前境界范围

### 步骤2:查询任务成员

对于感兴趣的任务,调用工具:`query_task_members`

${tool:query_task_members}

## 分析阶段

### 1. 境界匹配

| 任务等级 | 适合境界 |
|------|------|
| S级 | 金丹及以上 |
| A级 | 筑基及以上 |
| B级 | 练气及以上 |

### 2. 状态判断

- 状态为"正常":可接取
- 状态为"闭关中":建议先出关
- 状态为"任务中":建议先完成当前任务

## 输出格式

### 📋 推荐任务

| 任务名 | 等级 | 奖励 | 截止时间 | 匹配度 |
|-----|----|----|------|------|

### 💡 建议

根据您的当前状态,建议...

下一步