最近,继mcp后,skill又大火。
什么是skill
一、CLI+skill代替mcp
背景
MCP带来的痛点:
-
配置门槛高:用户需要在 Cursor 里手动配置 MCP Server 的 JSON(路径、参数),换台电脑又得来一遍。
-
连接不稳定:MCP Server 通过 stdio 通信,Cursor 偶尔会丢失连接,需要重启。
-
工具数量爆炸:当工具超过 40-50 个,AI 的工具选择准确率会下降。MCP 把所有工具一次性暴露给 AI,没有上下文筛选。
-
调试黑盒:出了问题很难定位 ------ 是 MCP 协议的问题?是 Server 崩了?是 Cursor 没正确传参?
-
Ask 模式不可用:Cursor 的 Ask 模式不支持 MCP 工具,只有 Agent/Plan/Debug 模式才能用。
前提
我们注意到:AI 编码助手天生就会执行 shell 命令。无论是 Cursor、Claude Code 还是 Windsurf,它们都有一个强大的 Shell 工具。如果我们把 MCP 工具包装成 CLI 命令,AI 就能直接在终端里执行,不需要任何 MCP 配置。
而要让 cursor 知道 该怎么用这些 CLI 命令,我们需要一种机制把"使用说明"注入到 AI 的上下文里。这就可以使用 Skills。
1、demo
下面对之前写好的一个mcp server进行改造,改成CLI模式
知识体系------MCP(四)demo(1)自定义mcp server(http模式&stdio模式)的demo-CSDN博客
security-ai-mcp-demo/
├── pom.xml
├── README.md
└── src/
└── main/
├── java/com/demo/mcp/
│ ├── SecurityAiMcpApplication.java
│ ├── auth/
│ │ └── AppKeyService.java
│ ├── backend/
│ │ └── BackendApiClient.java
│ └── cli/
│ └── CliRunner.java
└── resources/
├── application.properties
└── logback-spring.xml
(1)pom
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.demo</groupId>
<artifactId>security-ai-mcp-demo</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>security-ai-mcp-demo</name>
<description>CLI for security-ai-demo /v1 APIs (user/school/role CRUD) with app_key auth</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.4</version>
<relativePath/>
</parent>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
(2)配置文件和启动类
# CLI 模å¼ï¼ä¸å¯å¨ Webï¼æ¥å¿èµ° stderr
spring.main.web-application-type=none
spring.main.banner-mode=off
backend.base-url=http://localhost:6666/securityAIDemo
backend.user-account=admin
backend.password=123
mcp.app-key=demo-key-001
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- CLI:stdout 留给命令输出,日志输出到 stderr -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<target>System.err</target>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="CONSOLE"/>
</root>
</configuration>
package com.demo.mcp;
import org.springframework.boot.Banner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class SecurityAiMcpApplication {
public static void main(String[] args) {
if (args.length < 1 || !"cli".equals(args[0])) {
System.err.println("Usage: java -jar security-ai-mcp-demo-<version>.jar cli <resource> <action> [options]");
System.err.println(" resource: user | school | role");
System.err.println(" action: list | get | save | update | delete");
System.err.println(" Set SECURITY_AI_APP_KEY or use --app-key=KEY for auth.");
System.exit(1);
}
SpringApplication app = new SpringApplication(SecurityAiMcpApplication.class);
app.setBannerMode(Banner.Mode.OFF);
app.setAdditionalProfiles("cli");
app.run(args);
}
}
(3)cli解析
package com.demo.mcp.cli;
import com.demo.mcp.auth.AppKeyService;
import com.demo.mcp.backend.BackendApiClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* CLI 模式入口:当以 <code>cli <resource> <action> [options]</code> 启动时运行,
* 调用 Backend API 并将结果输出到 stdout,供 Cursor Agent 通过 skill+shell 使用。
* <p>
* 用法示例:
* <pre>
* java -jar app.jar cli user list --pageNum=1 --pageSize=5 [--account=xxx]
* java -jar app.jar cli user get --id=1
* java -jar app.jar cli user save --body="{\"account\":\"a\",\"name\":\"n\"}"
* java -jar app.jar cli user update --body="{\"id\":1,...}"
* java -jar app.jar cli user delete --id=1
* java -jar app.jar cli school list|get|save|update|delete (同上,get/delete 用 --id,save/update 用 --body)
* java -jar app.jar cli role list|get|save|update|delete
* </pre>
* AppKey(优先级从高到低):--app-key 参数、环境变量 SECURITY_AI_APP_KEY、本地文件 ~/.security-ai-cli(或 %USERPROFILE%\.security-ai-cli)。
*/
@Component
@Profile("cli")
public class CliRunner implements ApplicationRunner {
private static final Logger log = LoggerFactory.getLogger(CliRunner.class);
/** 本地 appKey 文件:用户主目录下的 .security-ai-cli,内容为一行 key 或 SECURITY_AI_APP_KEY=xxx */
private static final String APP_KEY_FILE = ".security-ai-cli";
private final AppKeyService appKeyService;
private final BackendApiClient backend;
public CliRunner(AppKeyService appKeyService, BackendApiClient backend) {
this.appKeyService = appKeyService;
this.backend = backend;
}
@Override
public void run(ApplicationArguments args) throws Exception {
String[] raw = args.getSourceArgs();
if (raw.length < 3 || !"cli".equals(raw[0])) {
printUsage();
exit(1);
return;
}
String resource = raw[1].toLowerCase();
String action = raw[2].toLowerCase();
Map<String, String> opts = parseOptions(raw, 3);
String appKey = opts.get("app-key");
if (appKey == null || appKey.isBlank()) {
appKey = System.getenv("SECURITY_AI_APP_KEY");
}
if (appKey == null || appKey.isBlank()) {
appKey = readAppKeyFromFile();
}
if (!appKeyService.validate(appKey)) {
err("Invalid or missing appKey. Use one of: --app-key=KEY, env SECURITY_AI_APP_KEY, or local file ~/.security-ai-cli (must match mcp.app-key in config).");
exit(1);
return;
}
try {
String result = dispatch(resource, action, opts);
if (result != null) {
System.out.println(result);
}
exit(0);
} catch (IllegalArgumentException e) {
err(e.getMessage());
printUsage();
exit(1);
} catch (Exception e) {
log.error("CLI error", e);
err("Error: " + e.getMessage());
exit(1);
}
}
private Map<String, String> parseOptions(String[] raw, int start) {
Map<String, String> m = new HashMap<>();
for (int i = start; i < raw.length; i++) {
String s = raw[i];
if (s.startsWith("--")) {
String keyVal = s.substring(2);
int eq = keyVal.indexOf('=');
if (eq > 0) {
m.put(keyVal.substring(0, eq).trim(), keyVal.substring(eq + 1).trim());
} else if (i + 1 < raw.length && !raw[i + 1].startsWith("--")) {
m.put(keyVal.trim(), raw[++i].trim());
} else {
m.put(keyVal.trim(), "");
}
}
}
return m;
}
/** 从用户主目录的 .security-ai-cli 读取 appKey;支持单行 key 或 SECURITY_AI_APP_KEY=xxx */
private String readAppKeyFromFile() {
String home = System.getProperty("user.home");
if (home == null || home.isBlank()) return null;
Path path = Paths.get(home, APP_KEY_FILE);
if (!Files.isRegularFile(path)) return null;
try {
List<String> lines = Files.readAllLines(path);
for (String line : lines) {
String s = line.trim();
if (s.isEmpty() || s.startsWith("#")) continue;
if (s.contains("=")) {
int eq = s.indexOf('=');
String key = s.substring(0, eq).trim();
if ("SECURITY_AI_APP_KEY".equals(key) || "appKey".equals(key) || "app-key".equals(key)) {
String val = s.substring(eq + 1).trim();
if (!val.isEmpty()) return val;
}
} else {
return s;
}
}
} catch (Exception e) {
log.debug("Failed to read appKey from {}: {}", path, e.getMessage());
}
return null;
}
private String dispatch(String resource, String action, Map<String, String> opts) {
switch (resource) {
case "user" -> {
return dispatchUser(action, opts);
}
case "school" -> {
return dispatchSchool(action, opts);
}
case "role" -> {
return dispatchRole(action, opts);
}
default -> throw new IllegalArgumentException("Unknown resource: " + resource + ". Use: user, school, role.");
}
}
private String dispatchUser(String action, Map<String, String> opts) {
return switch (action) {
case "list" -> {
String pageNum = opts.getOrDefault("pagenum", opts.getOrDefault("pageNum", "1"));
String pageSize = opts.getOrDefault("pagesize", opts.getOrDefault("pageSize", "5"));
String account = opts.get("account");
Map<String, String> params = new HashMap<>(Map.of("pageNum", pageNum, "pageSize", pageSize));
if (account != null && !account.isBlank()) params.put("account", account);
yield backend.get("/user/list", params);
}
case "get" -> requireId(opts, id -> backend.get("/user/" + id, null));
case "save" -> requireBody(opts, body -> backend.post("/user", body));
case "update" -> requireBody(opts, body -> backend.put("/user", body));
case "delete" -> requireId(opts, id -> backend.delete("/user/" + id));
default -> throw new IllegalArgumentException("Unknown action: " + action + ". Use: list, get, save, update, delete.");
};
}
private String dispatchSchool(String action, Map<String, String> opts) {
return switch (action) {
case "list" -> {
String pageNum = opts.getOrDefault("pagenum", opts.getOrDefault("pageNum", "1"));
String pageSize = opts.getOrDefault("pagesize", opts.getOrDefault("pageSize", "5"));
yield backend.get("/school/list", Map.of("pageNum", pageNum, "pageSize", pageSize));
}
case "get" -> requireId(opts, id -> backend.get("/school/" + id, null));
case "save" -> requireBody(opts, body -> backend.post("/school", body));
case "update" -> requireBody(opts, body -> backend.put("/school", body));
case "delete" -> requireId(opts, id -> backend.delete("/school/" + id));
default -> throw new IllegalArgumentException("Unknown action: " + action + ". Use: list, get, save, update, delete.");
};
}
private String dispatchRole(String action, Map<String, String> opts) {
return switch (action) {
case "list" -> {
String pageNum = opts.getOrDefault("pagenum", opts.getOrDefault("pageNum", "1"));
String pageSize = opts.getOrDefault("pagesize", opts.getOrDefault("pageSize", "5"));
yield backend.get("/role/list", Map.of("pageNum", pageNum, "pageSize", pageSize));
}
case "get" -> requireId(opts, id -> backend.get("/role/" + id, null));
case "save" -> requireBody(opts, body -> backend.post("/role", body));
case "update" -> requireBody(opts, body -> backend.put("/role", body));
case "delete" -> requireId(opts, id -> backend.delete("/role/" + id));
default -> throw new IllegalArgumentException("Unknown action: " + action + ". Use: list, get, save, update, delete.");
};
}
private String requireId(Map<String, String> opts, java.util.function.Function<String, String> fn) {
String id = opts.get("id");
if (id == null || id.isBlank()) throw new IllegalArgumentException("Missing required option: --id=...");
return fn.apply(id.trim());
}
private String requireBody(Map<String, String> opts, java.util.function.Function<String, String> fn) {
String body = opts.get("body");
if (body == null || body.isBlank()) throw new IllegalArgumentException("Missing required option: --body='...' (JSON string)");
return fn.apply(body.trim());
}
private void printUsage() {
String u = """
Usage: java -jar security-ai-mcp-demo-<version>.jar cli <resource> <action> [options]
Resources: user, school, role
Actions: list, get, save, update, delete
Options (global): --app-key=KEY or env SECURITY_AI_APP_KEY or file ~/.security-ai-cli
List: --pageNum=1 --pageSize=5 [user list only: --account=xxx]
Get/Delete: --id=1
Save/Update: --body='{"key":"value"}' (JSON string)
Examples:
cli user list --pageNum=1 --pageSize=10
cli user get --id=1
cli user save --body="{\\"account\\":\\"u1\\",\\"name\\":\\"User 1\\"}"
cli school list
cli role get --id=1
""";
err(u);
}
private void err(String msg) {
System.err.println(msg);
}
private void exit(int code) {
System.exit(code);
}
}
(4)接口调用
package com.demo.mcp.backend;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.*;
import org.springframework.stereotype.Component;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.HttpStatusCodeException;
import org.springframework.web.client.RestTemplate;
import java.util.Map;
/**
* 调用 security-ai-demo 的 /v1 接口。使用配置的服务账号登录获取 JWT,并在请求头中携带 token。
* 收到 401 时清除 token 并重试一次(应对 token 过期);403/4xx 时抛出带 body 的异常便于排查。
*/
@Component
public class BackendApiClient {
@Value("${backend.base-url}")
private String baseUrl;
@Value("${backend.user-account}")
private String userAccount;
@Value("${backend.password}")
private String password;
private final RestTemplate restTemplate;
private final ObjectMapper objectMapper;
private volatile String cachedToken;
public BackendApiClient(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
this.restTemplate = new RestTemplate();
}
private void clearToken() {
cachedToken = null;
}
private String getToken() {
if (cachedToken != null) {
return cachedToken;
}
synchronized (this) {
if (cachedToken != null) return cachedToken;
MultiValueMap<String, String> form = new LinkedMultiValueMap<>();
form.add("userAccount", userAccount);
form.add("passWord", password);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
HttpEntity<MultiValueMap<String, String>> req = new HttpEntity<>(form, headers);
String loginUrl = baseUrl + "/login";
ResponseEntity<String> res = restTemplate.exchange(loginUrl, HttpMethod.POST, req, String.class);
if (res.getStatusCode().is2xxSuccessful() && res.getBody() != null) {
try {
JsonNode root = objectMapper.readTree(res.getBody());
JsonNode data = root.path("data");
if (!data.isMissingNode() && data.isTextual()) {
cachedToken = data.asText();
return cachedToken;
}
} catch (Exception e) {
throw new RuntimeException("Failed to parse login response: " + e.getMessage());
}
}
throw new RuntimeException("Backend login failed: " + res.getStatusCode());
}
}
private HttpHeaders authHeaders() {
HttpHeaders h = new HttpHeaders();
h.set("token", getToken());
h.setContentType(MediaType.APPLICATION_JSON);
return h;
}
private static String messageFrom(HttpStatusCodeException e) {
String body = e.getResponseBodyAsString();
if (body != null && !body.isBlank()) return body;
return "[no body]";
}
private static RuntimeException toRuntime(HttpStatusCodeException e) {
int code = e.getStatusCode().value();
String msg = "Backend returned " + code + ": " + messageFrom(e);
if (code == 403) msg = "Backend 403 Forbidden (token 可能过期或权限不足). " + messageFrom(e);
if (code == 401) msg = "Backend 401 Unauthorized (登录失效). " + messageFrom(e);
return new RuntimeException(msg, e);
}
public String get(String path, Map<String, String> queryParams) {
return getImpl(path, queryParams, 0);
}
private String getImpl(String path, Map<String, String> queryParams, int retryCount) {
String url = baseUrl + path;
if (queryParams != null && !queryParams.isEmpty()) {
StringBuilder sb = new StringBuilder(url);
sb.append('?');
queryParams.forEach((k, v) -> {
if (v != null) sb.append(k).append('=').append(v).append('&');
});
url = sb.substring(0, sb.length() - 1);
}
try {
ResponseEntity<String> res = restTemplate.exchange(url, HttpMethod.GET, new HttpEntity<>(authHeaders()), String.class);
return res.getBody();
} catch (HttpStatusCodeException e) {
int code = e.getStatusCode().value();
if ((code == 401 || code == 403) && retryCount == 0) {
clearToken();
return getImpl(path, queryParams, 1);
}
throw toRuntime(e);
}
}
public String post(String path, String jsonBody) {
return postImpl(path, jsonBody, 0);
}
private String postImpl(String path, String jsonBody, int retryCount) {
String url = baseUrl + path;
HttpEntity<String> entity = new HttpEntity<>(jsonBody != null ? jsonBody : "{}", authHeaders());
try {
ResponseEntity<String> res = restTemplate.exchange(url, HttpMethod.POST, entity, String.class);
return res.getBody();
} catch (HttpStatusCodeException e) {
int code = e.getStatusCode().value();
if ((code == 401 || code == 403) && retryCount == 0) {
clearToken();
return postImpl(path, jsonBody, 1);
}
throw toRuntime(e);
}
}
public String put(String path, String jsonBody) {
return putImpl(path, jsonBody, 0);
}
private String putImpl(String path, String jsonBody, int retryCount) {
String url = baseUrl + path;
HttpEntity<String> entity = new HttpEntity<>(jsonBody != null ? jsonBody : "{}", authHeaders());
try {
ResponseEntity<String> res = restTemplate.exchange(url, HttpMethod.PUT, entity, String.class);
return res.getBody();
} catch (HttpStatusCodeException e) {
int code = e.getStatusCode().value();
if ((code == 401 || code == 403) && retryCount == 0) {
clearToken();
return putImpl(path, jsonBody, 1);
}
throw toRuntime(e);
}
}
public String delete(String path) {
return deleteImpl(path, 0);
}
private String deleteImpl(String path, int retryCount) {
String url = baseUrl + path;
try {
ResponseEntity<String> res = restTemplate.exchange(url, HttpMethod.DELETE, new HttpEntity<>(authHeaders()), String.class);
return res.getBody();
} catch (HttpStatusCodeException e) {
int code = e.getStatusCode().value();
if ((code == 401 || code == 403) && retryCount == 0) {
clearToken();
return deleteImpl(path, 1);
}
throw toRuntime(e);
}
}
}
package com.demo.mcp.auth;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
/**
* 校验入参 appKey 与配置文件中的 mcp.app-key 是否一致。
*/
@Component
public class AppKeyService {
@Value("${mcp.app-key:}")
private String appKey;
public boolean validate(String key) {
return appKey != null && !appKey.isBlank()
&& appKey.equals(key != null ? key.trim() : "");
}
}
(5) 测试前先配置好本地鉴权的key:
内容为:
demo-key-001
2、CLI 命令测试
java -jar c:\mydemo\security-ai-mcp-demo\target\security-ai-mcp-demo-1.0-SNAPSHOT.jar cli user list --pageNum=1 --pageSize=5
c:\mydemo\security-ai-mcp-demo\scripts\security-ai-cli.bat school get --id=1

如果key错误:


3、cursor调用
(1)先配置skill

如我New了两个skill,分别对应上述的user tool和school tool:

① school的skill
---
name: security-ai-cli-school
description: >
School CRUD for security-ai-demo via CLI.
Use when the user asks about schools, school list, get/add/update/delete school in security-ai-demo.
Triggers on: 学校列表、查学校、分页查学校、根据 ID 查学校、新增学校、更新学校、删除学校、security-ai 学校、demo 学校.
version: 1.0.0
---
# Security AI Demo --- School CLI
Jar: `SECURITY_AI_CLI_JAR` (env, absolute path) or `c:\mydemo\security-ai-mcp-demo\target\security-ai-mcp-demo-1.0-SNAPSHOT.jar` when workspace is the demo project.
Auth: `SECURITY_AI_APP_KEY` or `--app-key=...`.
```bash
java -jar "%SECURITY_AI_CLI_JAR%" cli school <action> [options]
```
PowerShell: `$env:SECURITY_AI_CLI_JAR`. Bash: `"$SECURITY_AI_CLI_JAR"`.
---
## Quick Reference
| action | Required | Optional |
|--------|----------|----------|
| list | --- | --pageNum=1 --pageSize=5 |
| get | --id= | --- |
| delete | --id= | --- |
| save | --body='{"name":"..."}' | --- |
| update | --body='{"id":1,"name":"..."}' | --- |
---
## Examples
```bash
java -jar "%SECURITY_AI_CLI_JAR%" cli school list --pageNum=1 --pageSize=5
java -jar "%SECURITY_AI_CLI_JAR%" cli school get --id=1
java -jar "%SECURITY_AI_CLI_JAR%" cli school delete --id=1
java -jar "%SECURITY_AI_CLI_JAR%" cli school save --body="{\"name\":\"School A\"}"
java -jar "%SECURITY_AI_CLI_JAR%" cli school update --body="{\"id\":1,\"name\":\"School A Updated\"}"
```
②user的skill
---
name: security-ai-cli-user
description: >
User CRUD for security-ai-demo via CLI.
Use when the user asks about users, user list, get/add/update/delete user in security-ai-demo.
Triggers on: 用户列表、查用户、按 ID/account 查用户、新增用户、更新用户、删除用户、security-ai 用户、demo 用户.
version: 1.0.0
---
# Security AI Demo --- User CLI
Jar: `SECURITY_AI_CLI_JAR` (env, absolute path) or `c:\mydemo\security-ai-mcp-demo\target\security-ai-mcp-demo-1.0-SNAPSHOT.jar` when workspace is the demo project.
Auth: `SECURITY_AI_APP_KEY` or `--app-key=...`.
```bash
java -jar "%SECURITY_AI_CLI_JAR%" cli user <action> [options]
```
PowerShell: `$env:SECURITY_AI_CLI_JAR`. Bash: `"$SECURITY_AI_CLI_JAR"`.
---
## Quick Reference
| action | Required | Optional |
|--------|----------|----------|
| list | --- | --pageNum=1 --pageSize=5 --account=xxx |
| get | --id= | --- |
| delete | --id= | --- |
| save | --body='{"account":"","name":""...}' | --- |
| update | --body='{"id":1,...}' | --- |
---
## Examples
```bash
java -jar "%SECURITY_AI_CLI_JAR%" cli user list --pageNum=1 --pageSize=10
java -jar "%SECURITY_AI_CLI_JAR%" cli user list --account=admin
java -jar "%SECURITY_AI_CLI_JAR%" cli user get --id=1
java -jar "%SECURITY_AI_CLI_JAR%" cli user delete --id=1
java -jar "%SECURITY_AI_CLI_JAR%" cli user save --body="{\"account\":\"u1\",\"name\":\"User One\"}"
java -jar "%SECURITY_AI_CLI_JAR%" cli user update --body="{\"id\":1,\"account\":\"u1\",\"name\":\"Updated\"}"
```
(2)对话测试
可以看到,调用了对应的skill,执行了cli

