L2 高等篇:MCP 协议 + Spring AI + Agent 编排
》本篇目标:理解 MCP 协议细节,能用 Java 写生产级 MCP Server,
》把 Spring 业务系统接入 Spring AI,用 LangChain4j 做客户端,
》掌握 3 种主流 Agent 编排模式。
5. MCP 协议深度解读
5.1 协议基础:JSON-RPC 2.0
MCP 跑在 JSON-RPC 2.0 之上(MCP-1)。一个 MCP 消息长这样:
请求(Request):
json
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "add",
"arguments": {"a": 3, "b": 5}
}
}
响应(Response):
json
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"content": [{"type": "text", "text": "3 + 5 = 8"}],
"isError": false
}
}
通知(Notification,无响应):
json
{
"jsonrpc": "2.0",
"method": "notifications/progress",
"params": {"token": "abc", "value": 50}
}
错误(Error Response):
json
{
"jsonrpc": "2.0",
"id": 1,
"error": {
"code": -32602,
"message": "Invalid params",
"data": {"field": "a", "reason": "must be integer"}
}
}
标准错误码(与 JSON-RPC 一致):
| Code | 含义 | 何时用 |
|---|---|---|
| -32700 | Parse error | JSON 解析失败 |
| -32600 | Invalid Request | 不是合法 JSON-RPC |
| -32601 | Method not found | 方法名不存在 |
| -32602 | Invalid params | 参数不合法 |
| -32603 | Internal error | 服务器内部错误 |
| -32000 ~ -32099 | Server error | 服务端自定义错误 |
5.2 协议生命周期
Client Server
| |
|--- initialize (capabilities) ----->| <- 1. 握手,交换能力
|<-- initialize result --------------|
|--- initialized (notification) ---->| <- 2. 客户端确认
| |
|--- tools/list -------------------->| <- 3. 正常业务
|<-- tools (list) -------------------|
|--- resources/read ---------------->|
|<-- resource content ---------------|
|--- tools/call -------------------->|
|<-- tool result --------------------|
| |
| anytime |
|--- notifications/... ------------->| <- 4. 异步通知
|--- ping -------------------------->| <- 5. 心跳
|<-- pong ---------------------------|
| |
|--- shutdown ---------------------->| <- 6. 关闭
关键点:
initialize是强制的,必须先于其他调用initialized是 notification(无 id)- 所有
list/call/read都需要 request/response notifications/*是单向通知ping用于心跳和健康检查
5.3 Capability 协商
Client 能力:
json
{
"capabilities": {
"sampling": {}, // 客户端支持 LLM 采样
"roots": { // 客户端能提供文件系统根
"listChanged": true
},
"experimental": {"feature-x": {}}
}
}
Server 能力:
json
{
"capabilities": {
"tools": {"listChanged": true}, // 暴露工具
"resources": {"subscribe": true}, // 暴露资源
"prompts": {"listChanged": false}, // 暴露提示模板
"logging": {}, // 接受日志
"completions": {}, // 接受补全
"experimental": {"feature-y": {}}
}
}
Client 不知道的 Capability 不要调 。比如 Server 没声明 tools: {},Client 调 tools/list 会返回 Method not found.
5.4 3 种传输方式(Transport)
A. Stdio(本地子进程,最常用)
+-------------------+
| Client App |
| |
| stdin -> +-------------+
| | MCP Server | (java/python/node 进程)
| stdout <- \--------------+
+-------------------+
- 适合:本地工具,本地数据,开发环境
- 优点:零网络配置,安全(沙箱)
- 缺点:不能跨主机
- 典型场景:读本地文件,查本地数据库,执行本地脚本
B. Streamable HTTP(2025 新标准,推荐远程用)
Client Server
| POST /mcp (request) |
|------------------------------------->|
| HTTP 200 + SSE stream (response) |
|<-------------------------------------|
| |
| GET /mcp (SSE 通道,保持长连接) |
|<=====================================|
| SSE event: server request | <- 服务端可以主动发请求(如 sampling)
| |
- 适合:远程工具,SaaS 化,跨主机
- 优点:双向通信,HTTP 友好(防火墙友好)
- 缺点:需要部署 HTTP 服务器
- 注意:已替代旧的 HTTP+SSE 传输
C. SSE(旧标准,已废弃)
- 2025 之前的方案:
GET /sse拿服务端事件,POST /messages发请求 - 新部署请直接用 Streamable HTTP
选型决策表
| 场景 | 推荐 |
|---|---|
| 本地工具(读文件,查 DB) | Stdio |
| 远程 SaaS API 包装 | Streamable HTTP |
| 高频双向通信 | Streamable HTTP |
| 一次性命令行调用 | Stdio |
| Web 应用内嵌 | Streamable HTTP |
| 移动端 | Streamable HTTP |
5.5 Resources:可读取的数据
Resource 标识(URI):
file:///home/user/doc.txt
db://postgres/customers
https://api.example.com/v1/users
声明 Resource:
json
{
"uri": "file:///etc/hosts",
"name": "Hosts File",
"description": "系统 hosts 文件",
"mimeType": "text/plain"
}
读取:
json
// Request
{"method": "resources/read", "params": {"uri": "file:///etc/hosts"}}
// Response
{
"contents": [{
"uri": "file:///etc/hosts",
"mimeType": "text/plain",
"text": "127.0.0.1 localhost\n..."
}]
}
订阅(Server 主动推送变化):
json
// 客户端订阅
{"method": "resources/subscribe", "params": {"uri": "file:///var/log/app.log"}}
// 服务端推送
{"method": "notifications/resources/updated", "params": {"uri": "file:///var/log/app.log"}}
5.6 Tools:可调用的函数
声明 Tool:
json
{
"name": "search_docs",
"description": "Search internal documentation",
"inputSchema": {
"type": "object",
"properties": {
"query": {"type": "string", "description": "搜索关键词"},
"max_results": {"type": "integer", "default": 5, "minimum": 1, "maximum": 50}
},
"required": ["query"]
}
}
调用:
json
// Request
{
"method": "tools/call",
"params": {
"name": "search_docs",
"arguments": {"query": "Spring AI MCP", "max_results": 3}
}
}
// Response
{
"content": [
{"type": "text", "text": "Found 3 results:\n1. ..."}
],
"isError": false
}
Tool 结果类型:
text:纯文本image:base64 编码图片audio:base64 编码音频resource:内嵌 resource
关键设计原则:
- Tool 名字要清晰(模型靠 name + description 选工具)
- Description 要说明何时用 / 何时不用(减少模型选错)
- 参数尽量少(>10 个参数模型选不对)
- 返回内容要简洁(token 是钱)
- 错误要明确 (返回
isError: true+ 错误信息)
5.7 Prompts:可复用的提示模板
json
{
"name": "code-review",
"description": "Code review with security focus",
"arguments": [
{"name": "language", "description": "编程语言", "required": true},
{"name": "code", "description": "代码片段", "required": true}
]
}
获取渲染后的 Prompt:
json
// Request
{"method": "prompts/get", "params": {"name": "code-review", "arguments": {...}}}
// Response
{
"description": "Code review with security focus",
"messages": [
{
"role": "user",
"content": {
"type": "text",
"text": "请用 ${language} 安全最佳实践审查以下代码:\n\n${code}\n\n关注:注入,XSS,权限..."
}
}
]
}
5.8 Sampling:让 Server 调 Client 的 LLM
这是 MCP 的杀手级特性 -- Server 可以反向调用 Client 的 LLM.
Server: "我需要 LLM 帮用户总结这条日志"
-> 发 sampling/createMessage 请求给 Client
Client: 调用自家 LLM,把结果返回
Server: 把结果嵌入 tool response
典型场景:
- 工具调用后,Server 想"美化"一下结果给用户
- Server 想要 LLM 决策下一步该调哪个子工具
- Server 想让 LLM 帮用户生成查询 SQL
声明(Server):
json
{"capabilities": {"sampling": {}}}
Request:
json
{
"method": "sampling/createMessage",
"params": {
"messages": [{"role": "user", "content": {"type": "text", "text": "总结这条日志"}}],
"modelPreferences": {"hints": [{"name": "fast"}]},
"maxTokens": 100
}
}
5.9 Roots:客户端能提供的文件系统根
声明:
json
{"method": "roots/list"}
// Response
{
"roots": [
{"uri": "file:///home/user/projects", "name": "Projects"}
]
}
用途:Server 知道客户端授权访问哪些目录。
6。用 Java MCP SDK 写一个完整 Server
6.1 项目结构
mcp-fs-server/
+--- pom.xml
\--- src/main/java/com/example/mcpfs/
+--- McpFsServer.java # 启动入口
+--- FsTools.java # 工具实现
\--- FsResources.java # 资源实现
6.2 pom.xml
xml
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>mcp-fs-server</artifactId>
<version>1.0.0</version>
<properties>
<maven.compiler.source>21</maven.compiler.source>
<mcp.version>0.10.0</mcp.version>
</properties>
<dependencies>
<dependency>
<groupId>io.modelcontextprotocol</groupId>
<artifactId>mcp-server</artifactId>
<version>${mcp.version}</version>
</dependency>
<dependency>
<groupId>io.modelcontextprotocol</groupId>
<artifactId>mcp-json-jackson2</artifactId>
<version>${mcp.version}</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>2.0.13</version>
</dependency>
</dependencies>
</project>
6.3 FsTools.java
java
package com.example.mcpfs;
import io.modelcontextprotocol.server.McpServerFeatures;
import io.modelcontextprotocol.spec.McpSchema.*;
import java.nio.file.*;
import java.util.*;
public class FsTools {
public static McpServerFeatures.SyncToolSpecification readFileTool(Path allowedRoot) {
return McpServerFeatures.SyncToolSpecification.builder()
.tool(Tool.builder()
.name("read_file")
.description("读取指定路径的文件内容.路径必须在允许的根目录内.")
.inputSchema(Map.of(
"type", "object",
"properties", Map.of(
"path", Map.of("type", "string", "description", "相对路径")
),
"required", List.of("path")
))
.build())
.callHandler((exchange, args) -> {
try {
String rel = (String) args.get("path");
Path abs = allowedRoot.resolve(rel).normalize();
if (!abs.startsWith(allowedRoot)) {
return new CallToolResult("错误:路径越界", true);
}
String content = Files.readString(abs);
return new CallToolResult(content, false);
} catch (Exception e) {
return new CallToolResult("读取失败: " + e.getMessage(), true);
}
})
.build();
}
public static McpServerFeatures.SyncToolSpecification listDirTool(Path allowedRoot) {
return McpServerFeatures.SyncToolSpecification.builder()
.tool(Tool.builder()
.name("list_dir")
.description("列出目录内容")
.inputSchema(Map.of(
"type", "object",
"properties", Map.of(
"path", Map.of("type", "string", "default", ".")
)
))
.build())
.callHandler((exchange, args) -> {
try {
String rel = (String) args.getOrDefault("path", ".");
Path abs = allowedRoot.resolve(rel).normalize();
if (!abs.startsWith(allowedRoot)) {
return new CallToolResult("错误:路径越界", true);
}
StringBuilder sb = new StringBuilder();
try (var stream = Files.list(abs)) {
stream.sorted().forEach(p ->
sb.append(p.getFileName()).append("\n"));
}
return new CallToolResult(sb.toString(), false);
} catch (Exception e) {
return new CallToolResult("列出失败: " + e.getMessage(), true);
}
})
.build();
}
}
6.4 McpFsServer.java
java
package com.example.mcpfs;
import io.modelcontextprotocol.server.McpServer;
import io.modelcontextprotocol.server.McpSyncServer;
import io.modelcontextprotocol.server.transport.StdioServerTransportProvider;
import io.modelcontextprotocol.spec.McpSchema.*;
import java.nio.file.Path;
import java.nio.file.Paths;
public class McpFsServer {
public static void main(String[] args) {
// 从环境变量获取允许的根目录,默认 ~/mcp-workspace
String rootEnv = System.getenv().getOrDefault("MCP_FS_ROOT",
System.getProperty("user.home") + "/mcp-workspace");
Path allowedRoot = Paths.get(rootEnv).toAbsolutePath().normalize();
var transport = new StdioServerTransportProvider();
McpSyncServer server = McpServer.sync(transport)
.serverInfo("fs-server", "1.0.0")
.capabilities(ServerCapabilities.builder()
.tools(true)
.resources(true)
.build())
.build();
server.addTool(FsTools.readFileTool(allowedRoot));
server.addTool(FsTools.listDirTool(allowedRoot));
// 资源:暴露根目录本身
server.addResource(Resource.builder()
.uri("file://" + allowedRoot)
.name("Workspace Root")
.description("允许访问的工作目录根")
.mimeType("inode/directory")
.build());
// 阻塞主线程
Thread.currentThread().join();
}
}
6.5 打包 + 测试
bash
mvn clean package
# 配置 Hermes:
# ~/.hermes/config.yaml
# mcp:
# servers:
# - name: fs
# command: ["java", "-jar", "/path/to/mcp-fs-server-1.0.0.jar"]
# env: {MCP_FS_ROOT: "/home/user/projects"}
# transport: stdio
7. Spring AI 完整集成
7.1 依赖
xml
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>1.0.0-M6</version> <!-- 截至 2025 末最新 -->
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-client</artifactId>
</dependency>
</dependencies>
7.2 配置 application.yml
yaml
spring:
ai:
openai:
api-key: ${OPENAI_API_KEY}
base-url: https://api.deepseek.com
chat:
options:
model: deepseek-chat
temperature: 0.7
mcp:
# 客户端:连接外部 MCP Server
client:
enabled: true
stdio:
servers:
- name: fs
command: java
args: ["-jar", "/opt/mcp/fs-server.jar"]
env: {MCP_FS_ROOT: "/data/workspace"}
# 服务端:把本应用的能力暴露为 MCP
server:
enabled: true
stdio: true # 启动时通过 stdio 提供
name: my-spring-ai-server
version: 1.0.0
7.3 注解式 Tool 开发
java
@Component
public class DatabaseTools {
@Autowired
private CustomerRepository repo;
@Tool(description = "根据客户 ID 查询客户信息")
public Customer getCustomer(
@ToolParam(description = "客户 ID,整数") Long id) {
return repo.findById(id).orElse(null);
}
@Tool(description = "搜索名字包含关键字的客户")
public List<Customer> searchByName(
@ToolParam(description = "搜索关键字") String keyword) {
return repo.findByNameContaining(keyword);
}
@Tool(description = "创建新客户.返回新客户的 ID.")
public Long createCustomer(
@ToolParam(description = "客户姓名") String name,
@ToolParam(description = "邮箱") String email) {
Customer c = new Customer(name, email);
return repo.save(c).getId();
}
}
7.4 把 Service 暴露为 MCP Server
java
@SpringBootApplication
public class MyApp {
public static void main(String[] args) {
SpringApplication.run(MyApp.class, args);
}
}
// 只要 @Tool 注解的方法在 Spring 容器里,
// spring-ai-starter-mcp-server 会自动注册它们
7.5 完整 Agent 路由
java
@RestController
public class AgentController {
private final ChatClient chatClient;
public AgentController(ChatClient.Builder b,
List<ToolCallback> localTools, // 注解 @Tool
ToolCallbackProvider mcpTools) { // MCP 服务器
this.chatClient = b
.defaultTools(mcpTools) // MCP 工具
.defaultToolCallbacks(localTools) // 本地工具
.defaultSystem("你是一个 helpful 助手." +
"可以查客户信息,读写文件.")
.build();
}
@PostMapping("/chat")
public String chat(@RequestBody ChatRequest req) {
return chatClient.prompt()
.user(req.message())
.call()
.content();
}
}
8. LangChain4j 集成:MCP 客户端
8.1 Maven 依赖
xml
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-mcp</artifactId>
<version>1.0.0-beta1</version>
</dependency>
8.2 加载 MCP 工具
java
import dev.langchain4j.mcp.McpToolProvider;
import dev.langchain4j.mcp.client.transport.stdio.StdioMcpTransport;
import java.util.List;
public class AgentWithMcp {
public static void main(String[] args) {
// 启动 MCP server 子进程
var transport = new StdioMcpTransport.Builder()
.command(List.of("java", "-jar", "/path/to/mcp-fs-server.jar"))
.environment(Map.of("MCP_FS_ROOT", "/data/workspace"))
.build();
// 把 MCP 工具转成 LangChain4j 的 ToolProvider
var toolProvider = McpToolProvider.builder()
.mcpServers(List.of(transport))
.build();
// 构建 LLM
var model = OpenAiChatModel.builder()
.apiKey(System.getenv("OPENAI_API_KEY"))
.baseUrl("https://api.deepseek.com")
.modelName("deepseek-chat")
.build();
// 构建 Agent
var agent = AiServices.builder(Agent.class)
.chatModel(model)
.toolProvider(toolProvider)
.build();
String response = agent.chat("列出 /data/workspace 下的所有文件");
System.out.println(response);
}
interface Agent {
String chat(String message);
}
}
8.3 4 种 Transport 对应 API
| Transport | Java 类 | 说明 |
|---|---|---|
| stdio | StdioMcpTransport |
本地子进程 |
| HTTP/SSE(旧) | HttpMcpTransport |
已废弃 |
| Streamable HTTP | StreamableHttpMcpTransport |
新标准 |
| WebSocket | WebSocketMcpTransport |
LangChain4j 特有扩展 |
8.4 Docker stdio Transport
LangChain4j 特有,可以跑容器化的 MCP Server:
java
var transport = new StdioMcpTransport.Builder()
.command(List.of("docker", "run", "-i", "--rm",
"-v", "/data:/data",
"my-mcp-fs:latest"))
.build();
9. Agent 设计模式
9.1 ReAct(Reason + Act)
最经典也最稳的模式:
User: 深圳明天会下雨吗?
Iteration 1:
Thought: 我需要查深圳明天的天气
Action: weather_lookup(city="深圳", day="tomorrow")
Observation: {rain_prob: 75%, temp: 18-24}
Iteration 2:
Thought: 75% 概率下雨,应该建议带伞
Final Answer: 深圳明天 75% 概率下雨,18-24°C,建议带伞.
Spring AI 写法(默认就是 ReAct):
java
ChatClient chat = chatClientBuilder
.defaultTools(weatherTool, calendarTool)
.build();
9.2 Plan-and-Execute
适合:复杂多步任务,需要先规划。
User: 帮我调研竞品 A,B,C,写一份对比报告
Plan:
Step 1: 搜索 A 的最近新闻
Step 2: 搜索 B 的最近新闻
Step 3: 搜索 C 的最近新闻
Step 4: 总结成对比表
Step 5: 写报告
Execute Step 1 -> ... -> Execute Step 5
Spring AI 1.0 写法 (用 agentic-patterns):
java
// Spring AI 1.0 引入了 PlanningAgent
PlanningAgent agent = PlanningAgent.builder()
.chatClient(chatClient)
.planner(planner)
.executor(executor)
.build();
9.3 Reflection(自我反思)
适合:需要高质量输出的任务(写作,代码).
Iteration 1:
生成: "深圳适合创业..."
Reflection: "这段话太笼统,缺少数据支撑"
Iteration 2:
生成: "根据 2025 报告,深圳..."
Spring AI 手工实现:
java
public String reflectiveWrite(String topic) {
String draft = chat("写一篇关于 " + topic + " 的短文");
String critique = chat("请批判性评价以下短文,指出 3 个改进点:\n" + draft);
String revision = chat("根据以下批评改进短文:\n" + critique);
return revision;
}
9.4 Multi-Agent(多 Agent 协作)
CrewAI / AutoGen 风格:
Researcher -> 找资料
v
Writer -> 写文章
v
Reviewer -> 审核
v
Publisher -> 发布
Spring AI 1.0 实验特性 (@Agent 注解):
java
@Agent
class WriterAgent {
public String write(String topic) { ... }
}
@Agent
class ReviewerAgent {
public String review(String text) { ... }
}
@Workflow
class PublishFlow {
public String publish(String topic) {
String draft = writerAgent.write(topic);
String reviewed = reviewerAgent.review(draft);
return reviewed;
}
}
9.5 模式选型决策表
| 任务类型 | 推荐模式 | 原因 |
|---|---|---|
| 单步问答 | 简单 LLM 调用 | ReAct 太重 |
| 简单工具调用 | ReAct | 经典稳定 |
| 复杂多步任务 | Plan-and-Execute | 可观测,可中断 |
| 写作/创作 | Reflection | 提高质量 |
| 角色扮演/团队 | Multi-Agent | 分工明确 |
| 高频实时数据 | ReAct + 缓存 | 低延迟优先 |
10. Hermes Agent 实战
深入理解 SKILL 是怎么被加载的。
10.1 启动流程
~/.hermes/config.yaml
v
加载内置 Skills (~/.hermes/skills/built-in/)
v
加载用户 Skills (~/.hermes/skills/)
v
加载 MCP Servers (stdio/HTTP)
v
合并工具集 (built-in + skills + mcp)
v
Agent Ready
10.2 SKILL 加载机制
核心代码(Hermes gateway):
python
def load_skill(skill_dir: Path) -> Skill:
content = (skill_dir / "SKILL.md").read_text()
frontmatter, body = parse_frontmatter(content)
return Skill(
name=frontmatter["name"],
description=frontmatter["description"],
body=body,
path=skill_dir
)
# 启动时:
all_skills = []
for d in Path("~/.hermes/skills").glob("*/SKILL.md"):
all_skills.append(load_skill(d))
# 注入到 system prompt
system = build_system_prompt(all_skills)
关键点:
- 所有 skill 的
name+description都会进 system prompt - 模型看到用户问题后,自己决定调哪个 skill 的步骤
- 这就是为什么 description 要写"Use when ..."
10.3 MCP 自动注册
python
# ~/.hermes/config.yaml
mcp:
servers:
- name: github
command: ["npx", "-y", "@modelcontextprotocol/server-github"]
env: {GITHUB_TOKEN: "..."}
# 启动时:
for server in config.mcp.servers:
client = McpClient.connect(server)
tools = client.list_tools() # 获取所有 tools
for tool in tools:
register_global_tool(tool) # 注册到 agent
结果 :GitHub MCP 的 create_issue,list_repos 等工具直接出现在 Agent 工具列表,模型直接调用。
10.4 写一个生产级 SKILL
反面教材(description 太模糊):
yaml
description: "Some useful tools"
-》模型不知道何时用
正面教材:
yaml
name: code-review
description: "Use when the user asks to review, audit, or critique code. Triggers on phrases like 'review this code', 'check for bugs', 'is this secure', 'explain this function'. Don't use for: writing new code, formatting, or simple questions."
SKILL 大小限制(Hermes 实际约束):
- description: <= 1024 字符
- 全文: <= 100,000 字符
- 实际建议 8-15K 字符(再大就拆 references/)
10.5 SKILL 的进阶组织
~/.hermes/skills/
\--- production-incident/
+--- SKILL.md # 主入口
+--- references/
| +--- runbook-mysql.md
| +--- runbook-redis.md
| \--- runbook-k8s.md
+--- scripts/
| +--- collect_logs.sh
| \--- parse_stacktrace.py
\--- templates/
\--- incident-report.md
在 SKILL.md 里引用:
markdown
## 步骤
1. 运行 `bash scripts/collect_logs.sh`
2. 查阅 [MySQL 故障手册](references/runbook-mysql.md)
3. 用 [事故报告模板](templates/incident-report.md) 输出
11. L2 小结
到这一步你已经能:
- 读懂 MCP 协议的每个字段
- 用 Java MCP SDK 写生产级 Server
- 用 Spring AI Boot Starter 集成现有 Spring 系统
- 用 LangChain4j 加载 MCP 工具
- 根据任务选合适的 Agent 模式
- 写一个真正好用的 SKILL
L3 我们进入商用:成本,性能,可观测性,安全,选型。