Appearance
Spring Boot + Spring AI:Agent 完整落地方案
很多 Spring AI 文章会先从聊天接口开始,ChatClient 一接,接口一通,第一步就算跑完了。 但项目真往下做,很快就会发现难点并不在“能不能回答”,而在“这套能力怎么落到工程里”。
通常会一起冒出来的问题有:
- 对话模型怎么接
- 知识库怎么入库、分片、向量化、召回、重排
- 本地
Tool怎么组织 - 远程能力什么时候走
MCP Skill这一层该不该有,应该怎么抽Agent到底放在哪一层来编排这些能力- 怎么把它做成一个真正能扩展、能排障、能治理的 Spring Boot 项目
这篇就顺着这条线往下拆,把一套更完整的 Spring AI Agent 方案从项目骨架一路讲到能力编排。
1. 先说结论:完整方案通常是 6 层
如果把一个 Spring AI Agent 项目压缩成最常见的结构,通常可以拆成 6 层:
- 对话模型层:负责接模型、组织 Prompt、输出结果
- 知识库层:负责文档入库、检索、召回、重排
- Tool 层:负责把外部动作封成业务语义化工具
- MCP 层:负责接入远程共享工具 / 资源 / Prompt
- Skill 层:负责把一类任务封成稳定能力
- Agent 层:负责围绕目标选择能力并推进执行
🌟 Agent 往往不是“最大、最核心”的那个类,它更像执行主线,负责把模型、知识库、工具和外部能力串起来。
2. 一张图把完整链路串起来
mermaid
flowchart TD
A[用户请求] --> B[Controller]
B --> C[AgentApplicationService]
C --> D{是否只需普通问答}
D -- 是 --> E[ChatService]
D -- 否 --> F{是否需要知识库}
F -- 是 --> G[KnowledgeQaService]
F -- 否 --> H{是否需要动作能力}
G --> H
H -- 是 --> I[Tool / MCP / Skill]
H -- 否 --> J[直接生成结果]
I --> K[Agent 编排执行]
K --> L[整合上下文]
J --> L
L --> M[模型生成最终答案]
M --> N[返回结果与引用/执行摘要]把这张图看清楚,后面的代码会更容易对上:
- 聊天、知识库、工具、Agent 不是并列乱堆的功能点
- 它们更像围绕一次请求逐层展开的能力
Agent负责编排,但并不替代知识库层、工具层和治理层
3. 用一个真实场景统一后面的代码
下面统一用一个场景把代码串起来:
企业技术助手
它需要同时支持:
- 普通技术问答
- 基于内部文档的知识库检索
- 查询订单、查询发布状态等业务 Tool
- 调用远程共享能力,例如文档平台或浏览器类 MCP Tool
- 把“查资料 -> 调工具 -> 汇总结果”串成一次 Agent 执行
用这个场景有两个好处:
- 对话、知识库、工具、
MCP都有自然落点 Skill和Agent也不至于写成空架子
4. 项目结构建议怎么拆
既然目标是项目落地,目录最好直接落到文件级,而不是只停在包名这一层。
一个更接近真实项目的结构,可以拆成这样:
text
src/main/java/com/example/ai
├── AiAgentApplication.java
├── config
│ ├── AiAgentProperties.java
│ ├── AiAgentConfig.java
│ └── McpClientToolConfig.java
├── web
│ ├── AgentController.java
│ ├── AgentRequest.java
│ └── AgentResponse.java
├── chat
│ └── ChatService.java
├── knowledge
│ ├── KnowledgeIngestionService.java
│ ├── KnowledgeQaService.java
│ └── KnowledgeRerankService.java
├── tool
│ ├── OrderTools.java
│ └── client
│ └── OrderClient.java
├── mcp
│ ├── McpCapabilityService.java
│ └── McpToolService.java
├── skill
│ ├── AgentSkill.java
│ ├── KnowledgeSearchSkill.java
│ └── RemoteCapabilitySkill.java
├── agent
│ ├── AgentContext.java
│ └── AgentApplicationService.java
└── governance
└── AgentAuditService.java
src/main/resources
├── application.yml
└── mcp-servers.json各层职责大致是:
chat:模型接入和普通对话knowledge:文档导入、检索、重排、知识问答tool:本地业务工具mcp:远程 MCP 能力接入skill:任务能力封装agent:目标执行和编排主线governance:审计、限流、超时、监控
4.1 把 pom.xml 补齐
如果只是做最小聊天 Demo,依赖不会很多,但要把 Agent 方案真正落起来,模型、向量库、MCP、配置校验这些基础件最好一开始就带上。
xml
<properties>
<java.version>21</java.version>
<spring-ai.version>1.1.5</spring-ai.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>${spring-ai.version}</version>
<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.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</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-vector-store-pgvector</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-client-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>这组依赖分别对应下面几块:
model-openai:负责接对话模型和向量模型vector-store-pgvector:负责把分片后的文档写入PostgreSQL + pgvectormcp-client-webflux:负责把远程MCP Server的 Tool 接进来mcp-server-webmvc:负责把当前应用自己的 Tool / Resource / Prompt 对外暴露lombok:负责收掉配置类、DTO 里重复的样板代码
🌟 如果当前项目只消费远程 MCP,不打算把自己的能力开放成 MCP Server,那 spring-ai-starter-mcp-server-webmvc 可以不加。
4.2 主启动类不能省
很多示例只给 Service,不给启动类。代码看起来不少,但真照着搭时会发现还差一截。
java
package com.example.ai;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
/**
* Spring AI Agent 示例项目启动入口。
*/
@SpringBootApplication
@ConfigurationPropertiesScan
public class AiAgentApplication {
public static void main(String[] args) {
SpringApplication.run(AiAgentApplication.class, args);
}
}这里加 @ConfigurationPropertiesScan,是为了把分片大小、召回条数、系统提示词这类参数统一收进配置类,而不是散在各个 Service 里。
4.3 application.yml 建议一开始就配到位
yaml
server:
port: 8080
spring:
application:
name: spring-ai-agent-demo
datasource:
url: jdbc:postgresql://localhost:5432/ai_agent_demo
username: postgres
password: postgres
ai:
openai:
api-key: ${OPENAI_API_KEY}
chat:
options:
model: gpt-4o-mini
temperature: 0.2
embedding:
options:
model: text-embedding-3-small
vectorstore:
pgvector:
initialize-schema: true
mcp:
client:
type: SYNC
request-timeout: 20s
toolcallback:
enabled: true
stdio:
servers-configuration: classpath:mcp-servers.json
server:
enabled: true
name: enterprise-agent-mcp
version: 1.0.0
instructions: 对外提供知识检索与内部查询能力
app:
ai:
default-system-prompt: 你是一名企业技术助手,回答要简洁、准确、贴近工程。
knowledge-system-prompt: 你是企业知识库助手,必须优先基于资料回答;资料不足时要明确说明。
tool-system-prompt: 你是一名企业助手,只有在确实需要时才调用工具。
chunk-size: 400
chunk-overlap: 80
retrieval-top-k: 10
answer-top-n: 4这份配置可以分成两部分看:
spring.ai.*:交给 Starter 自动装配app.ai.*:交给你自己的项目做业务参数收口
像 chunk-size、retrieval-top-k 这种经常要调的值,更适合放在自己的业务配置前缀下,后面调参也更顺手。
4.4 远程 MCP 连接单独放到 mcp-servers.json
如果远程 MCP Server 走 stdio,配置通常会像这样:
json
{
"mcpServers": {
"doc-platform": {
"command": "java",
"args": [
"-jar",
"/opt/mcp/doc-platform-server.jar"
]
},
"browser-gateway": {
"command": "java",
"args": [
"-jar",
"/opt/mcp/browser-gateway.jar"
]
}
}
}这里没有故意去写某个现成三方 Server 的命令,因为项目里更常见的是:
- 接公司内部文档中心
- 接统一搜索服务
- 接浏览器自动化网关
- 接内部业务查询能力
4.5 把业务参数收进配置类
java
package com.example.ai.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* 项目内 AI 业务配置。
*/
@Data
@ConfigurationProperties(prefix = "app.ai")
public class AiAgentProperties {
private String defaultSystemPrompt;
private String knowledgeSystemPrompt;
private String toolSystemPrompt;
private int chunkSize = 400;
private int chunkOverlap = 80;
private int retrievalTopK = 10;
private int answerTopN = 4;
}4.6 基础配置类也要补上
后面的 Service 如果想真正串起来,至少还需要两个配置类:
- 一个负责切分器等基础 Bean
- 一个负责把远程
MCP Tool适配成ToolCallbackProvider
java
package com.example.ai.config;
import org.springframework.ai.transformer.splitter.TokenTextSplitter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 项目基础配置。
*/
@Configuration
public class AiAgentConfig {
@Bean
public TokenTextSplitter tokenTextSplitter(AiAgentProperties properties) {
return new TokenTextSplitter(
properties.getChunkSize(),
properties.getChunkOverlap(),
20,
12000,
true
);
}
}java
package com.example.ai.config;
import io.modelcontextprotocol.client.McpSyncClient;
import org.springframework.ai.mcp.SyncMcpToolCallbackProvider;
import org.springframework.ai.tool.ToolCallbackProvider;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 把远程 MCP Tool 接进 Spring AI 的工具调用体系。
*/
@Configuration
public class McpClientToolConfig {
@Bean
public ToolCallbackProvider remoteToolProvider(McpSyncClient mcpSyncClient) {
return SyncMcpToolCallbackProvider.builder()
.mcpClients(mcpSyncClient)
.build();
}
}🌟 走到这里,项目的基础骨架才算搭完整:依赖、入口、配置文件、配置类都已经到位。
5. 第一层:对话模型怎么接
5.1 最小配置
yaml
spring:
ai:
openai:
api-key: ${OPENAI_API_KEY}
chat:
options:
model: gpt-4o-mini
vectorstore:
pgvector:
initialize-schema: true这里先关注三件事:
- 模型配置放在配置层
- 不把模型名写死在业务代码里
- 后面知识库向量存储也能继续接进来
5.2 一个统一的对话服务
java
package com.example.ai.chat;
import com.example.ai.config.AiAgentProperties;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
/**
* 负责最基础的模型对话能力。
*/
@Service
public class ChatService {
private final ChatClient chatClient;
private final AiAgentProperties properties;
public ChatService(
ChatClient.Builder chatClientBuilder,
AiAgentProperties properties) {
this.chatClient = chatClientBuilder.build();
this.properties = properties;
}
/**
* 普通同步问答。
*
* @param question 用户问题
* @return 模型回答
*/
public String ask(String question) {
return chatClient.prompt()
.system(properties.getDefaultSystemPrompt())
.user(question)
.call()
.content();
}
/**
* 流式问答。
*
* @param question 用户问题
* @return 增量输出
*/
public Flux<String> stream(String question) {
return chatClient.prompt()
.system(properties.getDefaultSystemPrompt())
.user(question)
.stream()
.content();
}
}这一层的工作很单纯,就是把模型能力稳定接进 Spring Boot。
6. 第二层:知识库怎么落
6.1 知识库层最少要拆成哪几段
一个更稳的知识库层,通常至少要拆成 4 段:
- 文档导入
- 文档切分
- 向量化与索引写入
- 检索、召回、重排、答案生成
6.2 一条入库链路
mermaid
flowchart TD
A[原始文档] --> B[清洗]
B --> C[Chunk 切分]
C --> D[补元数据]
D --> E[Embedding]
E --> F[写入 VectorStore]6.3 文档入库服务
java
package com.example.ai.knowledge;
import com.example.ai.config.AiAgentProperties;
import java.util.List;
import java.util.Map;
import org.springframework.ai.document.Document;
import org.springframework.ai.transformer.splitter.TokenTextSplitter;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.stereotype.Service;
/**
* 负责文档清洗后的入库主线。
*/
@Service
public class KnowledgeIngestionService {
private final VectorStore vectorStore;
private final TokenTextSplitter tokenTextSplitter;
public KnowledgeIngestionService(
VectorStore vectorStore,
TokenTextSplitter tokenTextSplitter) {
this.vectorStore = vectorStore;
this.tokenTextSplitter = tokenTextSplitter;
}
/**
* 导入一篇文档到知识库。
*
* @param sourceId 文档来源标识
* @param title 文档标题
* @param content 清洗后的正文
*/
public void ingest(String sourceId, String title, String content) {
Document source = new Document(content, Map.of(
"sourceId", sourceId,
"title", title,
"docType", "manual",
"category", "knowledge-base"
));
List<Document> chunks = tokenTextSplitter.apply(List.of(source));
for (int i = 0; i < chunks.size(); i++) {
chunks.get(i).getMetadata().put("chunkIndex", i);
}
vectorStore.add(chunks);
}
}6.4 检索、召回、重排服务
java
package com.example.ai.knowledge;
import com.example.ai.config.AiAgentProperties;
import java.util.List;
import java.util.stream.Collectors;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.document.Document;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.stereotype.Service;
/**
* 负责知识库问答主链路。
*/
@Service
public class KnowledgeQaService {
private final VectorStore vectorStore;
private final ChatClient chatClient;
private final KnowledgeRerankService knowledgeRerankService;
private final AiAgentProperties properties;
public KnowledgeQaService(
VectorStore vectorStore,
ChatClient.Builder chatClientBuilder,
KnowledgeRerankService knowledgeRerankService,
AiAgentProperties properties) {
this.vectorStore = vectorStore;
this.chatClient = chatClientBuilder.build();
this.knowledgeRerankService = knowledgeRerankService;
this.properties = properties;
}
/**
* 先召回、再重排、最后生成答案。
*
* @param question 用户问题
* @return 基于知识库的答案
*/
public String answer(String question) {
SearchRequest request = SearchRequest.builder()
.query(question)
.topK(properties.getRetrievalTopK())
.similarityThreshold(0.75d)
.build();
List<Document> recalled = vectorStore.similaritySearch(request);
List<Document> reranked = knowledgeRerankService.rerank(question, recalled);
List<Document> topDocuments = reranked.stream()
.limit(properties.getAnswerTopN())
.toList();
String context = topDocuments.stream()
.map(doc -> "【%s】%s".formatted(doc.getMetadata().get("title"), doc.getText()))
.collect(Collectors.joining("\n\n"));
return chatClient.prompt()
.system(properties.getKnowledgeSystemPrompt())
.user("""
用户问题:
%s
检索资料:
%s
""".formatted(question, context))
.call()
.content();
}
}6.5 一个简单的重排服务
java
package com.example.ai.knowledge;
import java.util.Comparator;
import java.util.List;
import org.springframework.ai.document.Document;
import org.springframework.stereotype.Service;
/**
* 负责对召回结果进行精排。
*/
@Service
public class KnowledgeRerankService {
/**
* 按相关性重新排序。
*
* @param query 用户问题
* @param recalled 召回候选
* @return 精排后的候选
*/
public List<Document> rerank(String query, List<Document> recalled) {
return recalled.stream()
.sorted(Comparator.comparingInt(doc -> estimateScore(query, doc)).reversed())
.toList();
}
private int estimateScore(String query, Document document) {
String text = document.getText();
int score = 0;
if (text.contains(query)) {
score += 20;
}
if (Boolean.TRUE.equals(document.getMetadata().get("isFaq"))) {
score += 5;
}
return score;
}
}6.6 知识库层这时已经覆盖了什么
走到这里,知识库这一层已经把下面几件事串起来了:
- 分片
- 向量化
- 检索
- 召回
- 重排
- 最终回答生成
这时的知识库能力就不只是“接了个向量库”,而是一条完整链路:从切分、入库到召回、重排、生成,主线已经闭环了。
7. 第三层:本地 Tool 怎么组织
7.1 Tool 层最稳的目标
本地 Tool 更适合承接这类动作:
- 查询订单
- 查询发布状态
- 查询服务健康
- 执行某个明确业务动作
不太适合直接暴露给模型的则是:
- 任意 SQL
- 任意 URL
- 任意脚本执行
- 无边界写操作
7.2 一个业务语义化 Tool
java
package com.example.ai.tool;
import com.example.ai.tool.client.OrderClient;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.stereotype.Component;
/**
* 订单查询工具。
*/
@Component
public class OrderTools {
private final OrderClient orderClient;
public OrderTools(OrderClient orderClient) {
this.orderClient = orderClient;
}
/**
* 根据订单号查询订单摘要。
*
* @param orderNo 订单号
* @return 适合返回给模型的订单摘要
*/
@Tool(description = "根据订单号查询订单状态、金额和关键时间点")
public String getOrderSummary(String orderNo) {
return orderClient.queryOrderSummary(orderNo);
}
}7.3 一个聚合 Tool Service
java
package com.example.ai.tool;
import com.example.ai.config.AiAgentProperties;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.stereotype.Service;
/**
* 负责使用本地 Tool 完成外部动作调用。
*/
@Service
public class ToolExecutionService {
private final ChatClient chatClient;
private final OrderTools orderTools;
private final AiAgentProperties properties;
public ToolExecutionService(
ChatClient.Builder chatClientBuilder,
OrderTools orderTools,
AiAgentProperties properties) {
this.chatClient = chatClientBuilder.build();
this.orderTools = orderTools;
this.properties = properties;
}
/**
* 让模型按需调用本地工具。
*
* @param question 用户问题
* @return 最终答案
*/
public String askWithTools(String question) {
return chatClient.prompt()
.system(properties.getToolSystemPrompt())
.user(question)
.tools(orderTools)
.call()
.content();
}
}8. 第四层:远程能力什么时候用 MCP
8.1 MCP 放在哪一层
MCP 更适合出现在“本地 Tool 不够用了,开始要复用远程共享能力”的阶段。
例如:
- 远程文档平台
- 浏览器自动化
- 搜索服务
- 公司统一的 AI 能力平台
8.2 一个更稳的判断
| 场景 | 更适合的做法 |
|---|---|
| 当前应用自己使用 | 本地 Tool |
| 多个宿主都要共享 | MCP |
| 需要同时暴露 Tool / Resource / Prompt | MCP |
8.3 一个 MCP 能力接入服务
java
package com.example.ai.mcp;
import java.util.List;
import org.springframework.ai.tool.ToolCallbackProvider;
import org.springframework.stereotype.Service;
/**
* 负责统一收口远程 MCP 能力。
*/
@Service
public class McpCapabilityService {
private final ToolCallbackProvider remoteToolProvider;
public McpCapabilityService(ToolCallbackProvider remoteToolProvider) {
this.remoteToolProvider = remoteToolProvider;
}
/**
* 返回远程 MCP Tool Provider。
*
* @return 远程工具 provider
*/
public ToolCallbackProvider remoteTools() {
return remoteToolProvider;
}
/**
* 返回远程能力摘要。
*
* @return 工具名称列表
*/
public List<String> listToolNames() {
return List.of(remoteToolProvider.getToolCallbacks()).stream()
.map(callback -> callback.getToolDefinition().name())
.toList();
}
}8.4 远程 MCP Tool 怎么挂给模型
java
package com.example.ai.mcp;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.stereotype.Service;
/**
* 负责基于远程 MCP Tool 回答问题。
*/
@Service
public class McpToolService {
private final ChatClient chatClient;
private final McpCapabilityService mcpCapabilityService;
public McpToolService(
ChatClient.Builder chatClientBuilder,
McpCapabilityService mcpCapabilityService) {
this.chatClient = chatClientBuilder.build();
this.mcpCapabilityService = mcpCapabilityService;
}
/**
* 使用远程 MCP Tool 回答问题。
*
* @param question 用户问题
* @return 最终答案
*/
public String askWithRemoteTools(String question) {
return chatClient.prompt()
.system("你是一名企业助手,必要时可以使用远程共享工具。")
.user(question)
.tools(mcpCapabilityService.remoteTools())
.call()
.content();
}
}9. 第五层:为什么这里还要有 Skill
9.1 Skill 在这里不是协议概念,而是应用内抽象
放在这套方案里,Skill 更适合理解成:
把一类任务需要的 Prompt、知识库、工具和执行规则收成一层能力封装。
它不是单个动作,也不是整个 Agent,而是介于两者之间的一层任务能力封装。
9.2 一个 Skill 接口
java
package com.example.ai.skill;
import com.example.ai.agent.AgentContext;
/**
* AI 任务能力抽象。
*/
public interface AgentSkill {
/**
* 返回 Skill 名称。
*
* @return 名称
*/
String name();
/**
* 判断当前 Skill 是否适合处理这个目标。
*
* @param context Agent 上下文
* @return 是否匹配
*/
boolean supports(AgentContext context);
/**
* 执行当前 Skill。
*
* @param context Agent 上下文
* @return 执行结果
*/
String execute(AgentContext context);
}9.3 一个知识库问答 Skill
java
package com.example.ai.skill;
import com.example.ai.agent.AgentContext;
import com.example.ai.knowledge.KnowledgeQaService;
import org.springframework.stereotype.Component;
/**
* 知识库问答 Skill。
*/
@Component
public class KnowledgeSearchSkill implements AgentSkill {
private final KnowledgeQaService knowledgeQaService;
public KnowledgeSearchSkill(KnowledgeQaService knowledgeQaService) {
this.knowledgeQaService = knowledgeQaService;
}
@Override
public String name() {
return "knowledge-search";
}
@Override
public boolean supports(AgentContext context) {
return context.requiresKnowledge();
}
@Override
public String execute(AgentContext context) {
return knowledgeQaService.answer(context.question());
}
}9.4 一个远程 MCP Skill
java
package com.example.ai.skill;
import com.example.ai.agent.AgentContext;
import com.example.ai.mcp.McpToolService;
import org.springframework.stereotype.Component;
/**
* 远程共享能力 Skill。
*/
@Component
public class RemoteCapabilitySkill implements AgentSkill {
private final McpToolService mcpToolService;
public RemoteCapabilitySkill(McpToolService mcpToolService) {
this.mcpToolService = mcpToolService;
}
@Override
public String name() {
return "remote-capability";
}
@Override
public boolean supports(AgentContext context) {
return context.requiresRemoteCapability();
}
@Override
public String execute(AgentContext context) {
return mcpToolService.askWithRemoteTools(context.question());
}
}9.5 Skill 层解决的是什么问题
如果没有这一层,系统很容易变成:
- 所有判断都堆进 Agent Service
- 所有工具都直接交给模型
- 知识库、Tool、MCP 逻辑互相缠住
Skill 这一层的价值,在于把高频任务收成稳定能力,再交给 Agent 去选择和编排。
10. 第六层:Agent 怎么编排这些能力
10.1 一个最小 Agent 上下文
java
package com.example.ai.agent;
/**
* Agent 执行上下文。
*/
public record AgentContext(
String question,
boolean requiresKnowledge,
boolean requiresRemoteCapability,
boolean requiresAction) {
}10.2 一个完整 Agent 服务
java
package com.example.ai.agent;
import com.example.ai.chat.ChatService;
import com.example.ai.skill.AgentSkill;
import com.example.ai.tool.ToolExecutionService;
import java.util.List;
import org.springframework.stereotype.Service;
/**
* 负责围绕目标选择能力并推进执行。
*/
@Service
public class AgentApplicationService {
private final ChatService chatService;
private final ToolExecutionService toolExecutionService;
private final List<AgentSkill> skills;
public AgentApplicationService(
ChatService chatService,
ToolExecutionService toolExecutionService,
List<AgentSkill> skills) {
this.chatService = chatService;
this.toolExecutionService = toolExecutionService;
this.skills = skills;
}
/**
* 统一处理 Agent 请求。
*
* @param context Agent 上下文
* @return 最终结果
*/
public String handle(AgentContext context) {
for (AgentSkill skill : skills) {
if (skill.supports(context)) {
return skill.execute(context);
}
}
if (context.requiresAction()) {
return toolExecutionService.askWithTools(context.question());
}
return chatService.ask(context.question());
}
}10.3 这时 Agent 真正在做什么
到这里,Agent 这一层已经在做三件事:
- 根据目标判断应该走哪条能力线
- 优先挑合适的 Skill
- 如果没有匹配 Skill,再回退到 Tool 或普通对话
所以这里要把两个点分开看:
Agent不是简单等于“模型 + tools”Agent更像围绕目标组织能力的一层编排服务
11. 一条完整的请求主线
mermaid
sequenceDiagram
participant U as 用户
participant C as AgentController
participant A as AgentApplicationService
participant S as Skill
participant K as KnowledgeQaService
participant T as ToolExecutionService
participant M as McpToolService
participant L as ChatService
U->>C: 提交目标
C->>A: handle(context)
A->>A: 判断能力需求
A->>S: supports(context)
alt 命中知识库 Skill
S->>K: answer(question)
K-->>S: 知识答案
S-->>A: 返回结果
else 命中远程能力 Skill
S->>M: askWithRemoteTools(question)
M-->>S: 远程能力结果
S-->>A: 返回结果
else 需要本地动作
A->>T: askWithTools(question)
T-->>A: 工具结果
else 普通对话
A->>L: ask(question)
L-->>A: 模型回答
end
A-->>C: 最终结果
C-->>U: 返回响应12. 一个统一 Controller
java
package com.example.ai.web;
import com.example.ai.agent.AgentApplicationService;
import com.example.ai.agent.AgentContext;
import jakarta.validation.Valid;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
/**
* Agent 统一入口。
*/
@RestController
public class AgentController {
private final AgentApplicationService agentApplicationService;
public AgentController(AgentApplicationService agentApplicationService) {
this.agentApplicationService = agentApplicationService;
}
@PostMapping("/demo/ai/agent/run")
public AgentResponse run(@Valid @RequestBody AgentRequest request) {
AgentContext context = new AgentContext(
request.question(),
request.requiresKnowledge(),
request.requiresRemoteCapability(),
request.requiresAction()
);
String answer = agentApplicationService.handle(context);
return new AgentResponse(answer, "knowledge/tool/mcp/agent");
}
}12.1 请求对象、返回对象也要补齐
如果没有 AgentRequest、AgentResponse,上面的 Controller 其实还是半截代码。
java
package com.example.ai.web;
import jakarta.validation.constraints.NotBlank;
/**
* Agent 统一请求。
*/
public record AgentRequest(
@NotBlank(message = "question 不能为空")
String question,
boolean requiresKnowledge,
boolean requiresRemoteCapability,
boolean requiresAction) {
}java
package com.example.ai.web;
/**
* Agent 统一返回。
*/
public record AgentResponse(
String answer,
String route) {
}12.2 一次完整调用长什么样
接口起来之后,可以用最简单的请求把主链路跑通:
bash
curl --request POST 'http://localhost:8080/demo/ai/agent/run' \
--header 'Content-Type: application/json' \
--data '{
"question": "帮我查一下订单 A20260818001 的状态,并总结关键节点",
"requiresKnowledge": false,
"requiresRemoteCapability": false,
"requiresAction": true
}'如果想先验证知识库链路,把 requiresKnowledge 打开就行:
bash
curl --request POST 'http://localhost:8080/demo/ai/agent/run' \
--header 'Content-Type: application/json' \
--data '{
"question": "我们系统里订单超时取消的规则是什么?",
"requiresKnowledge": true,
"requiresRemoteCapability": false,
"requiresAction": false
}'12.3 更接近真实项目的启动顺序
如果按这套方案一步步把项目搭起来,建议按这个顺序验证:
- 先确认
PostgreSQL + pgvector能连通 - 再确认
OPENAI_API_KEY已注入环境变量 - 再启动 Spring Boot,看
ChatService能不能返回普通问答 - 然后导入一批文档,验证
KnowledgeIngestionService - 再验证
KnowledgeQaService的检索、召回、重排链路 - 再测本地
Tool - 最后再测远程
MCP和Agent编排
这个顺序比较稳,原因也很直接:
- 向量库问题和模型问题最好分开排
- 本地 Tool 和远程
MCP最好分开排 Agent永远应该是最后一层,不适合作为第一步排障入口
13. 这套方案为什么是“可落地”的
说它“可落地”,核心就在于项目里几层关键能力已经分开了:
- 对话模型层
- 知识库层
- Tool 层
- MCP 层
- Skill 层
- Agent 编排层
后面要继续往下补,路径也比较清楚:
- 想提升知识库效果,就去调分片、召回和重排
- 想增强外部动作,就补 Tool 或 MCP
- 想做高频任务固化,就补 Skill
- 想提升多步执行能力,就增强 Agent 编排
14. 还缺哪些治理能力,才能上线
写到这里,项目通常已经能跑,但离上线还差几类治理问题:
14.1 模型调用治理
- 超时
- 重试
- 降级
- 模型切换
14.2 Tool / MCP 治理
- 权限控制
- 幂等
- 审计
- 高风险动作确认
14.3 知识库治理
- 文档更新重建索引
- 引用来源返回
- 效果评估
- 召回质量观测
14.4 Agent 治理
- 步骤上限
- 成本控制
- 状态记录
- 执行轨迹审计
15. 最容易踩的坑
15.1 把所有事情都塞进一个 AgentService
很快就会出现这几个问题:
- 不好扩展
- 不好调试
- 不好治理
15.2 只有 Tool,没有 Skill
最后系统很容易变成这样:
- 工具很多
- 但没有稳定任务能力
- Agent 编排越来越乱
15.3 只有向量库,没有知识库链路
真正决定效果的,从来不只是 VectorStore,还包括:
- 分片
- 向量化
- 召回
- 重排
- 上下文拼接
15.4 过早把所有远程能力都收进 MCP
更稳的节奏通常是:
- 本地 Tool 先跑通
- 共享能力再抽成
MCP
15.5 把 Agent 理解成“天然会自己工作”
没有边界控制的 Agent,更容易出现:
- 工具乱调
- 成本失控
- 执行不稳定
16. 推荐怎么落这套方案
如果准备从 0 到 1 把这套方案落到真实项目里,更自然的顺序通常是:
- 先接
ChatService - 再补知识库入库和检索链路
- 再补本地 Tool
- 再接远程
MCP - 再把高频任务抽成 Skill
- 最后再把 Agent 编排层补齐
按这个顺序往下推,排障会轻松很多:
- 每一层都能单独验证
- 失败点更容易定位
- 不会一开始就把复杂度堆满
17. 一段更稳的总结
最后可以把这套方案记成一句话:
对话模型负责基础生成,知识库负责外部知识增强,Tool 和 MCP 负责动作能力接入,Skill 负责把高频任务封成稳定能力,而 Agent 则负责围绕目标把这些能力组织起来。真正可落地的关键,不是某一个组件,而是把这些层次拆清楚、串起来,并给每一层留出可治理的边界。