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. SkillAgent 也不至于写成空架子

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-sizeretrieval-top-k 这种经常要调的值,更适合放在自己的业务配置前缀下,后面调参也更顺手。

4.4 远程 MCP 连接单独放到 mcp-servers.json

如果远程 MCP Serverstdio,配置通常会像这样:

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 请求对象、返回对象也要补齐

如果没有 AgentRequestAgentResponse,上面的 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. 最后再测远程 MCPAgent 编排

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

  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 构建的个人技术笔记。