Skip to content

Spring Boot + Spring AI:Agent 完整落地方案 ​

很多 Spring AI 文章会先从聊天接口开始,ChatClient 一接,接口一通,第一步就算跑完了。 但项目真往下做,很快就会发现难点并不在“能不能回答”,而在“这套能力怎么落到工程里”。

通常会一起冒出来的问题有:

  1. 对话模型怎么接
  2. 知识库怎么入库、分片、向量化、召回、重排
  3. 本地 Tool 怎么组织
  4. 远程能力什么时候走 MCP
  5. Skill 这一层该不该有,应该怎么抽
  6. Agent 到底放在哪一层来编排这些能力
  7. 怎么把它做成一个真正能扩展、能排障、能治理的 Spring Boot 项目

这篇就顺着这条线往下拆,把一套更完整的 Spring AI Agent 方案从项目骨架一路讲到能力编排。

1. 先说结论:完整方案通常是 6 层 ​

如果把一个 Spring AI Agent 项目压缩成最常见的结构,通常可以拆成 6 层:

  1. 对话模型层:负责接模型、组织 Prompt、输出结果
  2. 知识库层:负责文档入库、检索、召回、重排
  3. Tool 层:负责把外部动作封成业务语义化工具
  4. MCP 层:负责接入远程共享工具 / 资源 / Prompt
  5. Skill 层:负责把一类任务封成稳定能力
  6. 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[返回结果与引用/执行摘要]

把这张图看清楚,后面的代码会更容易对上:

  1. 聊天、知识库、工具、Agent 不是并列乱堆的功能点
  2. 它们更像围绕一次请求逐层展开的能力
  3. Agent 负责编排,但并不替代知识库层、工具层和治理层

3. 用一个真实场景统一后面的代码 ​

下面统一用一个场景把代码串起来:

企业技术助手

它需要同时支持:

  1. 普通技术问答
  2. 基于内部文档的知识库检索
  3. 查询订单、查询发布状态等业务 Tool
  4. 调用远程共享能力,例如文档平台或浏览器类 MCP Tool
  5. 把“查资料 -> 调工具 -> 汇总结果”串成一次 Agent 执行

用这个场景有两个好处:

  1. 对话、知识库、工具、MCP 都有自然落点
  2. 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

各层职责大致是:

  1. chat:模型接入和普通对话
  2. knowledge:文档导入、检索、重排、知识问答
  3. tool:本地业务工具
  4. mcp:远程 MCP 能力接入
  5. skill:任务能力封装
  6. agent:目标执行和编排主线
  7. 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>

这组依赖分别对应下面几块:

  1. model-openai:负责接对话模型和向量模型
  2. vector-store-pgvector:负责把分片后的文档写入 PostgreSQL + pgvector
  3. mcp-client-webflux:负责把远程 MCP Server 的 Tool 接进来
  4. mcp-server-webmvc:负责把当前应用自己的 Tool / Resource / Prompt 对外暴露
  5. 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

这份配置可以分成两部分看:

  1. spring.ai.*:交给 Starter 自动装配
  2. 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 的命令,因为项目里更常见的是:

  1. 接公司内部文档中心
  2. 接统一搜索服务
  3. 接浏览器自动化网关
  4. 接内部业务查询能力

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 如果想真正串起来,至少还需要两个配置类:

  1. 一个负责切分器等基础 Bean
  2. 一个负责把远程 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

这里先关注三件事:

  1. 模型配置放在配置层
  2. 不把模型名写死在业务代码里
  3. 后面知识库向量存储也能继续接进来

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 段:

  1. 文档导入
  2. 文档切分
  3. 向量化与索引写入
  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 知识库层这时已经覆盖了什么 ​

走到这里,知识库这一层已经把下面几件事串起来了:

  1. 分片
  2. 向量化
  3. 检索
  4. 召回
  5. 重排
  6. 最终回答生成

这时的知识库能力就不只是“接了个向量库”,而是一条完整链路:从切分、入库到召回、重排、生成,主线已经闭环了。

7. 第三层:本地 Tool 怎么组织 ​

7.1 Tool 层最稳的目标 ​

本地 Tool 更适合承接这类动作:

  1. 查询订单
  2. 查询发布状态
  3. 查询服务健康
  4. 执行某个明确业务动作

不太适合直接暴露给模型的则是:

  1. 任意 SQL
  2. 任意 URL
  3. 任意脚本执行
  4. 无边界写操作

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 不够用了,开始要复用远程共享能力”的阶段。

例如:

  1. 远程文档平台
  2. 浏览器自动化
  3. 搜索服务
  4. 公司统一的 AI 能力平台

8.2 一个更稳的判断 ​

场景更适合的做法
当前应用自己使用本地 Tool
多个宿主都要共享MCP
需要同时暴露 Tool / Resource / PromptMCP

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 层解决的是什么问题 ​

如果没有这一层,系统很容易变成:

  1. 所有判断都堆进 Agent Service
  2. 所有工具都直接交给模型
  3. 知识库、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 这一层已经在做三件事:

  1. 根据目标判断应该走哪条能力线
  2. 优先挑合适的 Skill
  3. 如果没有匹配 Skill,再回退到 Tool 或普通对话

所以这里要把两个点分开看:

  1. Agent 不是简单等于“模型 + tools”
  2. 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 更接近真实项目的启动顺序 ​

如果按这套方案一步步把项目搭起来,建议按这个顺序验证:

  1. 先确认 PostgreSQL + pgvector 能连通
  2. 再确认 OPENAI_API_KEY 已注入环境变量
  3. 再启动 Spring Boot,看 ChatService 能不能返回普通问答
  4. 然后导入一批文档,验证 KnowledgeIngestionService
  5. 再验证 KnowledgeQaService 的检索、召回、重排链路
  6. 再测本地 Tool
  7. 最后再测远程 MCP 和 Agent 编排

这个顺序比较稳,原因也很直接:

  1. 向量库问题和模型问题最好分开排
  2. 本地 Tool 和远程 MCP 最好分开排
  3. Agent 永远应该是最后一层,不适合作为第一步排障入口

13. 这套方案为什么是“可落地”的 ​

说它“可落地”,核心就在于项目里几层关键能力已经分开了:

  1. 对话模型层
  2. 知识库层
  3. Tool 层
  4. MCP 层
  5. Skill 层
  6. Agent 编排层

后面要继续往下补,路径也比较清楚:

  1. 想提升知识库效果,就去调分片、召回和重排
  2. 想增强外部动作,就补 Tool 或 MCP
  3. 想做高频任务固化,就补 Skill
  4. 想提升多步执行能力,就增强 Agent 编排

14. 还缺哪些治理能力,才能上线 ​

写到这里,项目通常已经能跑,但离上线还差几类治理问题:

14.1 模型调用治理 ​

  1. 超时
  2. 重试
  3. 降级
  4. 模型切换

14.2 Tool / MCP 治理 ​

  1. 权限控制
  2. 幂等
  3. 审计
  4. 高风险动作确认

14.3 知识库治理 ​

  1. 文档更新重建索引
  2. 引用来源返回
  3. 效果评估
  4. 召回质量观测

14.4 Agent 治理 ​

  1. 步骤上限
  2. 成本控制
  3. 状态记录
  4. 执行轨迹审计

15. 最容易踩的坑 ​

15.1 把所有事情都塞进一个 AgentService ​

很快就会出现这几个问题:

  1. 不好扩展
  2. 不好调试
  3. 不好治理

15.2 只有 Tool,没有 Skill ​

最后系统很容易变成这样:

  1. 工具很多
  2. 但没有稳定任务能力
  3. Agent 编排越来越乱

15.3 只有向量库,没有知识库链路 ​

真正决定效果的,从来不只是 VectorStore,还包括:

  1. 分片
  2. 向量化
  3. 召回
  4. 重排
  5. 上下文拼接

15.4 过早把所有远程能力都收进 MCP ​

更稳的节奏通常是:

  1. 本地 Tool 先跑通
  2. 共享能力再抽成 MCP

15.5 把 Agent 理解成“天然会自己工作” ​

没有边界控制的 Agent,更容易出现:

  1. 工具乱调
  2. 成本失控
  3. 执行不稳定

16. 推荐怎么落这套方案 ​

如果准备从 0 到 1 把这套方案落到真实项目里,更自然的顺序通常是:

  1. 先接 ChatService
  2. 再补知识库入库和检索链路
  3. 再补本地 Tool
  4. 再接远程 MCP
  5. 再把高频任务抽成 Skill
  6. 最后再把 Agent 编排层补齐

按这个顺序往下推,排障会轻松很多:

  1. 每一层都能单独验证
  2. 失败点更容易定位
  3. 不会一开始就把复杂度堆满

17. 一段更稳的总结 ​

最后可以把这套方案记成一句话:

对话模型负责基础生成,知识库负责外部知识增强,Tool 和 MCP 负责动作能力接入,Skill 负责把高频任务封成稳定能力,而 Agent 则负责围绕目标把这些能力组织起来。真正可落地的关键,不是某一个组件,而是把这些层次拆清楚、串起来,并给每一层留出可治理的边界。

基于 VitePress 构建的个人技术笔记。