RAG知识库

zy123
2025-08-19 /  0 评论 /  0 点赞 /  11 阅读 /  5164 字
最近更新于 10-29

Spring AI + Ollama

1. Ollama

  • 定位:本地/容器化的 大模型推理服务引擎
  • 作用:负责加载模型、执行推理、返回结果
  • 支持的模型:Qwen、DeepSeek、nomic-embed-text(embedding 模型)等
  • 交互方式:REST API(默认 http://localhost:11434
ollama run qwen2.5:7b-instruct

2. Spring AI

  • 定位:Spring 官方推出的 AI 接入框架
  • 作用:统一封装对各种 AI 服务的调用逻辑,让开发者更容易接入(OpenAI、Ollama 等)
  • 关键点
    • OllamaChatModel:封装了调用 Ollama 的对话接口
    • OllamaOptions:指定模型、参数(温度、上下文大小等)
    • PgVectorStore:对接向量数据库(Spring AI 提供的默认向量数据库适配器。)
    • TokenTextSplitter:默认的文本切分器 (Spring AI 内置逻辑)

3.pgVectorStore

pgVectorStoreSpring AI 提供的向量存储接口实现,底层基于 PostgreSQL + pgvector 插件。它在项目中的作用:

1)存储向量和元数据

pgVectorStore.accept(splits);

这行代码会:

1.splits 中每个 Documenttext → 调用 embedding 模型 → 生成向量

2.将生成的向量、原始文本、以及文档元数据统一存入数据库表(默认表名:vector_store);

  • embedding:存储生成的向量;
  • content:存储原始文本内容;
  • metadata:以 JSONB 形式存储元数据。

2)检索时自动生成 SQL

List<Document> documents = pgVectorStore.similaritySearch(request);

在执行检索时,pgVectorStore 会自动生成 SQL 查询,

  • 解析查询请求中的文本;

  • 生成查询向量;

  • 自动构造 SQL(结合 pgvector 的 <-> 向量相似度运算符);

  • 按相似度排序返回最相关的文档列表。

进一步地,结合 filterExpression(例如 metadata->>'knowledge' = 'xxx')可在相似度计算的基础上进一步进行条件过滤

配置

ai:
  ollama:
    base-url: http://ollama:11434 #告诉 Spring AI 去 http://ollama:11434 调用模型接口。
    embedding:
      options:
        num-batch: 512  #批处理参数
      model: nomic-embed-text  #指定使用的 embedding 模型
  rag:
    embed: nomic-embed-text

注意,需要提前下载模型:

ollama pull deepseek-r1:1.5b
ollama pull qwen2.5:7b-instruct
ollama pull nomic-embed-text
ollama list
image-20251008204625886

Docker如何开启GPU加速大模型推理

巨坑!

默认是CPU跑大模型,deepseek1.5b勉强能跑动,但速度很慢,一换qwen2.5:7b模型瞬间就跑不动了,这才发现一直在用CPU跑!!

如何排查?

1.查看ollama日志:

load_backend: loaded CPU backend from /usr/lib/ollama/libggml-cpu-alderlake.so

2.本地cmd命令行输入,查看显存占用率。

nvidia-smi

|   0  NVIDIA GeForce RTX 4060 ...  WDDM  |   00000000:01:00.0  On |                  N/A |
| N/A   58C    P0             24W /  110W |    2261MiB /   8188MiB |      6%      Default |

设备状态

  • RTX 4060显卡驱动(576.02)和CUDA 12.9环境正常
  • 当前GPU利用率仅6%GPU-Util列)
  • 显存占用2261MB/8188MB(约27.6%)

可见模型推理的时候压根没有用GPU!!!

解决

参考博客:1-3 Windows Docker Desktop安装与设置docker实现容器GPU加速_windows docker gpu-CSDN博客

1)配置WSL2,打开 PowerShell(以管理员身份运行),执行以下命令:

wsl --install -d Ubuntu  # 安装 Linux 发行版
wsl --set-default-version 2  # 设为默认版本
wsl --update  # 更新内核
  • 重启计算机以使更改生效。

2)检查wsl是否安装成功

C:\Users\zhangsan>wsl --list --verbose
  NAME              STATE           VERSION
* Ubuntu            Running         2
  docker-desktop    Running         2

这个命令会列出所有已安装的Linux发行版及其状态。如果看到列出的Linux发行版,说明WSL已成功安装。

3)安装docker desktop默认配置:

image-20250819211426320

4)保险起见也启用一下开启 Windows 的 Hyper-V 虚拟化技术:

搜索“启用或关闭Windows功能”,勾选:Hyper-V 虚拟化技术。

image-20250819211715265

5) 命令提示符输入 nvidia-smi

C:\Users\zhangsan>nvidia-smi
Tue Aug 19 21:18:11 2025
+-----------------------------------------------------------------------------------------+
| NVIDIA-SMI 576.02                 Driver Version: 576.02         CUDA Version: 12.9     |
|-----------------------------------------+------------------------+----------------------+
| GPU  Name                  Driver-Model | Bus-Id          Disp.A | Volatile Uncorr. ECC |
| Fan  Temp   Perf          Pwr:Usage/Cap |           Memory-Usage | GPU-Util  Compute M. |
|                                         |                        |               MIG M. |
|=========================================+========================+======================|
|   0  NVIDIA GeForce RTX 4060 ...  WDDM  |   00000000:01:00.0  On |                  N/A |
| N/A   55C    P4             12W /  129W |    2132MiB /   8188MiB |     33%      Default |
|                                         |                        |                  N/A |
+-----------------------------------------+------------------------+----------------------+

若显示GPU信息(如型号、显存等),则表示支持。

6)Docker Desktop配置**(重要)**

  • 打开 Docker 设置 → Resources → WSL Integration → 启用 Ubuntu 实例。

  • 进入 Docker 设置 → Docker Engine,添加以下配置:

    • {
        "experimental": true,
        "features": {
          "buildkit": true
         },
        "registry-mirrors": ["https://registry.docker-cn.com"]  // 可选镜像加速
      }
      
  • 保存并重启 Docker。

7)验证 GPU 加速

docker run --rm -it --gpus=all nvcr.io/nvidia/k8s/cuda-sample:nbody nbody -gpu -benchmark

NOTE: The CUDA Samples are not meant for performance measurements. Results may vary when GPU Boost is enabled.

> Windowed mode
> Simulation data stored in video memory
> Single precision floating point simulation
> 1 Devices used for simulation
MapSMtoCores for SM 8.9 is undefined.  Default to use 128 Cores/SM
MapSMtoArchName for SM 8.9 is undefined.  Default to use Ampere
GPU Device 0: "Ampere" with compute capability 8.9

> Compute 8.9 CUDA device: [NVIDIA GeForce RTX 4060 Laptop GPU]
24576 bodies, total time for 10 iterations: 17.351 ms
= 348.102 billion interactions per second
= 6962.039 single-precision GFLOP/s at 20 flops per interaction

成功输出应包含 GPU 型号及性能指标 [NVIDIA GeForce RTX 4060 Laptop GPU]

8)Ollama验证:

image-20250819220345141

在容器中也能显示GPU了,说明配置成功了!!!

version: '3.9'
services:
  ollama:
    image: registry.cn-hangzhou.aliyuncs.com/xfg-studio/ollama:0.5.10
    container_name: ollama
    restart: unless-stopped
    ports:
      - "11434:11434"
    volumes:
      - ./ollama:/root/.ollama
    runtime: nvidia
    environment:
      - NVIDIA_VISIBLE_DEVICES=all
      - NVIDIA_DRIVER_CAPABILITIES=all

什么原理?

GPU 驱动仍运行在 Windows 主机上WSL2 作为 Linux 内核子系统,共享访问该 GPU 驱动的底层接口Docker Desktop 在 WSL2 内运行(作为 Linux 虚拟环境), 通过 NVIDIA Container Runtime (nvidia-container-toolkit) 把 GPU 设备挂载进容器;

最终容器内程序(如 PyTorch、Ollama)能直接使用 GPU 计算。

RAG(检索增强生成)

postgre向量数据库

PostgreSQL也是结构化数据库(关系型数据库)!!!

  • 所有数据都遵守固定的结构(Schema)

  • 每一列(Column)定义了数据类型;

  • 每一行(Row)表示一条记录;

表结构:

@Bean
public PgVectorStore pgVectorStore(JdbcTemplate jdbcTemplate, EmbeddingModel embeddingModel) {
    return PgVectorStore.builder(jdbcTemplate, embeddingModel).build();
}

PgVectorStore 会自动检测并在首次使用时创建所需的向量表,默认表名是 vector_store;默认表结构是固定格式(id, content, metadata, embedding)。

PgAdmin软件下:ai-rag-knowledge -> 架构 -> public -> 表 -> vector_store

image-20250820170837656

id → 每条数据的唯一标识(主键/uuid)。

content → 存放 chunk 的原始文本。

metadata → 存放 JSON 格式的额外信息(文件名、路径、知识库标签等)。

embedding → 存放向量,类型是 vector(N)(N 由 Embedding 模型决定,比如 768)。

所有文件切分出来的 chunk 都存在这一张表里,每条记录就是一个 chunk

表的主要用途

1)相似度检索(Retrieval):用embedding <-> 查询向量 进行向量距离计算,找出与输入文本语义最相近的片段。其中<-> 是 pgvector 的距离运算符

2)结果展示(Content Retrieval):检索后,取出距离最近的若干条记录的 content,作为模型回答时的上下文。

3)溯源/过滤:通过 metadata 字段中的信息(如文件名、标签、知识类别等)进行结果筛选或来源追踪,例如:其中 ->> 从 JSON 对象中取出某个字段的值,并以文本(text)形式返回。

INSERT INTO vector_store (content, metadata) VALUES
('公司年度财报分析', '{"knowledge": "finance", "source": "report.pdf"}'),
('销售策略优化', '{"knowledge": "marketing", "source": "plan.docx"}'),
('财务合规指引', '{"knowledge": "finance", "source": "policy.pdf"}'),
('技术架构设计', '{"knowledge": "engineering", "source": "design.md"}');

现在你想只查出属于 “财务领域(finance)” 的内容,可以这样写:

SELECT id, content, metadata
FROM vector_store
WHERE metadata->>'knowledge' = 'finance';

查询

同一般的SQL语句。

image-20250820171104405

image-20250820171145997

vector_dims(embedding)是内置函数,返回维度大小。

还有 metadataembedding 列显示不下。

A. 索引/入库(Ingestion)

这里用了SpringAI提供的:TikaDocumentReaderpgVectorStore

  1. 文档读取器

    • TikaDocumentReader 把上传的 PDF、Word、TXT、PPT 等文件解析成 List<Document>
    • 每个 Document 包含 textmetadata(比如 pagesource)。
    • 相当于是“把二进制文件 → 转成纯文本段落”。
    TikaDocumentReader documentReader = new TikaDocumentReader(resource);
    List<Document> documents = documentReader.get();
    
  2. 清洗过滤

    • 剔除空文本,避免无效数据进入后续流程。
    List<Document> docs = new ArrayList<>(documents);
    docs.removeIf(d -> d.getText() == null || d.getText().trim().isEmpty());
    if (docs.isEmpty()) {
        log.warn("文件内容为空,跳过处理: {}", normalizedPath);
        return;
    }
    
  3. 文档切分器

    • TokenTextSplitter 把每个 Document 再切成 chunk(默认 800 tokens 一段,可配置)。
    • 目的是避免超长文本超过 Embedding 模型的输入限制。
    @Bean
    public TokenTextSplitter tokenTextSplitter() {
        return TokenTextSplitter.builder()
                .withChunkSize(600)          // 每段最多 600 token
                .withMinChunkSizeChars(300)  // 每段至少 300 字符
                .withMinChunkLengthToEmbed(10)
                .withMaxNumChunks(10000)
                .withKeepSeparator(true)
                .build();
    }
    //使用,省略依赖注入
    List<Document> splits = tokenTextSplitter.apply(docs);
    
    • withMinChunkSizeChars(300): 如果某个切分块的文本少于 300 个字符TokenTextSplitter 会避免直接拆分它,而是尝试 合并它与下一个切分块,直到符合字符数要求。
  4. 打元数据

    • 给原始文档和 chunk 都加上 knowledge(ragTag)、pathoriginal_filename 等信息。
    • 方便后续检索时追溯来源。
    docs.forEach(doc -> {
        doc.getMetadata().put("knowledge", ragTag);
        doc.getMetadata().put("path", normalizedPath);
        doc.getMetadata().put("original_filename", originalFilename);
    });
    
    splits.forEach(doc -> {
        doc.getMetadata().put("knowledge", ragTag);
        doc.getMetadata().put("path", normalizedPath);
        doc.getMetadata().put("original_filename", originalFilename);
    });
    
  5. Embedding 模型

    • 每个 chunk 调用 Ollama 的 nomic-embed-text,生成一个固定维度的向量(如 768 维)。
    • 注意:这是对 chunk 整体嵌入,不是对单个 token。
  6. 向量存储

    //配置类
    @Bean
    public PgVectorStore pgVectorStore(JdbcTemplate jdbcTemplate, EmbeddingModel embeddingModel) {
        return PgVectorStore.builder(jdbcTemplate, embeddingModel).build();
    }
    //使用,省略依赖注入
    pgVectorStore.accept(splits);
    
    • pgvector 存储 [embedding 向量 + metadata + 原始文本]
    • 向量维度由 Embedding 模型决定,pgvector 表的维度必须保持一致(如 768/1024/1536)。

B. 检索/回答(Query)

接收用户问题

  • 输入用户的问题文本 message

相似度检索

  • 使用同一个 Embedding 模型(如 nomic-embed-text)将问题转为查询向量。

  • 在向量库中用 余弦相似度 / 内积 搜索相似 chunk。

  • 可附加条件过滤:如 knowledge == 'xxx',只在某个知识库范围内查。

  • 常用参数:

    • topK:取最相似的前 K 个结果(例如 5~8)。
    • minSimilarityScore(可选):过滤低相关度结果。
// 1) 相似度检索(带 ragTag 过滤)
List<Document> documents = pgVectorStore.similaritySearch(
    SearchRequest.builder()
        .query(message)
        .topK(8)
        .filterExpression("knowledge == '" + ragTag + "'")
        .build()
);
List<Document> documents = pgVectorStore.similaritySearch(request);

拼装文档上下文

  • 把检索到的文档片段拼接成系统提示中的 DOCUMENTS 部分。
  • 可以在拼接时附带 metadata(如文件名、页码),方便溯源。
String documentContent = documents.stream()
    .map(doc -> "[来源:" + doc.getMetadata().get("original_filename") + "]\n" + doc.getText())
    .collect(joining("\n\n---\n\n"));

构造提示词(Prompt)

  • 使用 SystemPromptTemplate 注入 DOCUMENTS 内容。
  • System Prompt 应该放在 用户消息之前,确保模型优先遵循规则。
Message ragMessage = new SystemPromptTemplate(SYSTEM_PROMPT)
        .createMessage(Map.of("documents", documentContent));
List<Message> messages = new ArrayList<>();
messages.add(ragMessage);  // 先放系统提示
messages.add(new UserMessage(message));

调用对话模型(流式返回)

return ollamaChatModel.stream(new Prompt(
        messages,
        OllamaOptions.builder().model(model).build()
));

优化

1.优化分词逻辑

image-20250820175639642

这块比较复杂,要切的恰到好处...切的不大不小。

2.更新向量嵌入模型

MCP服务

简介 - 模型上下文协议

MCP 是“模型与外部世界沟通的桥梁”。它让模型不仅能聊天,还能调用本地文件系统、数据库、网络 API 等服务,实现“有工具可用的 AI”。

基础介绍

角色 说明 举例
🖥️ MCP Server(服务端) 提供“工具能力”,比如操作文件系统、调用系统命令、执行数据库查询等。 @modelcontextprotocol/server-filesystemmcp-server-computer.jar
🤖 MCP Client(客户端) 嵌入在应用(你的 Spring Boot)中,让模型能访问这些工具。 spring-ai-mcp-client-webflux-spring-boot-starter 实现

快速使用

1)引入依赖(POM)

<dependency>
      <groupId>org.springframework.ai</groupId>
      <artifactId>spring-ai-mcp-server-spring-boot-starter</artifactId>
</dependency>

<dependency>
      <groupId>org.springframework.ai</groupId>
      <artifactId>spring-ai-mcp-client-webflux-spring-boot-starter</artifactId>
</dependency>

2)配置 MCP 服务(Server)

创建配置文件:resources/config/mcp-servers-config.json

{
  "mcpServers": {
    "filesystem": {
      "command": "npx.cmd",
      "args": [
        "-y",
        "@modelcontextprotocol/server-filesystem",
        "D:/folder/MCP-test",
        "D:/folder/MCP-test"
      ]
    },
    "mcp-server-computer": {
      "command": "java",
      "args": [
        "-Dspring.ai.mcp.server.stdio=true",
        "-jar",
        "D:/folder/study/apache-maven-3.8.4/mvn_repo/edu/whut/mcp/mcp-server-computer/1.0.0/mcp-server-computer-1.0.0.jar"
      ]
    }
  }
}

filesystem:定义了一个 MCP 文件系统服务

  • 通过 npx 启动;
  • 调用包 @modelcontextprotocol/server-filesystem
  • 并将本地路径 D:/folder/MCP-test 暴露给模型访问。

mcp-server-computer:定义了一个自定义的 Java MCP 服务

  • 使用 java -jar 启动;
  • 通过标准输入输出(stdio)与主程序通信。

Spring配置文件(application.yml)配置 MCP 服务加载路径

spring:
  ai:
    mcp:
      servers-config: classpath:config/mcp-servers-config.json

3)安装并启用 Filesystem MCP 服务

需要提前下载该文件服务,官网: https://www.npmjs.com/package/@modelcontextprotocol/server-filesystem (创建、移动目录,读取、写入文件...)

  • 先装 Node.js,配置环境变量

    node -v
    npm -v
    
  • 安装 MCP 文件系统服务

    npm install -g @modelcontextprotocol/server-filesystem
    

4)配置客户端

Spring AI 通过 ChatClient 调用大模型,如果你使用多个模型(如 Ollama、OpenAI),可以通过不同的 ChatModel Bean 实例来区分。

@Bean
public OllamaChatModel ollamaChatModel(OllamaApi ollamaApi) {
    return OllamaChatModel.builder()
            .ollamaApi(ollamaApi)
            .defaultOptions(OllamaOptions.builder().model("qwen2.5:7b-instruct").build())
            .build();
}

@Bean
public ChatClient.Builder chatClientBuilder(OllamaChatModel ollamaChatModel) {
    return new DefaultChatClientBuilder(
            ollamaChatModel,
            ObservationRegistry.NOOP,
            (ChatClientObservationConvention) null
    );
}

注意,ollamaChatModel()是基础模型,创建与Ollama服务交互的底层Chat模型;chatClientBuilder()客户端构建器,提供高级功能封装(如工具调用)

5)模型调用示例

ollamaChatModel 是连接本地 Ollama 服务的客户端; 具体使用哪个模型(deepseek-r1:1.5bqwen2.5:7b-instruct 等)是在调用时通过 OllamaOptions 决定的:

ChatResponse response = chatClientBuilder
        .defaultOptions(OllamaOptions.builder().model("qwen2.5:7b-instruct").build())
        .build()
        .prompt("你好,介绍一下你自己")
        .call();

6)测试 MCP 调用

查看当前可用工具

@GetMapping("/tools")
public Object listTools() {
    return Arrays.stream(tools.getToolCallbacks())
            .map(cb -> Map.of(
                    "name", cb.getName(),
                    "description", cb.getDescription()
            ))
            .toList();
}

image-20250820101236265

自动调用工具(含 MCP)

@GetMapping("/test-workflow")
public String testWorkflow(@RequestParam String question) {
    var chatClient = chatClientBuilder
            .defaultOptions(OllamaOptions.builder().model("qwen2.5:7b-instruct").build())
            .build();
    return chatClient.prompt(question).tools(tools).call().chatResponse().toString();
}
image-20250820101525612

当模型发现某个问题需要使用外部工具(如文件读写、计算服务)时,会自动调用注册在 MCP 中的工具进行操作。

有时候大模型不会去调用Tools,可能是模型能力不够。

注意,docker部署后,这个路径还没改!!!自然查不到可用的工具,因为不是在本地windows中了。

自定义MCP服务

目录结构

mcp-server-demo/
  ├─ src/
  │  └─ main/
  │     ├─ java/…/DemoApplication.java
  │     ├─ java/…/NoteService.java      // 你的工具:@Tool 方法
  │     └─ resources/application.yml     // 仅需几行
  └─ pom.xml

POM 依赖

<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>org.springframework.ai</groupId>
      <artifactId>spring-ai-bom</artifactId>
      <version>1.0.0-M6</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>
  </dependencies>
</dependencyManagement>

<dependencies>
  <dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-mcp-server-spring-boot-starter</artifactId>
  </dependency>
  <dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
  </dependency>
</dependencies>

关键配置

spring:
  application:
    name: mcp-server-demo
  ai:
    mcp:
      server:
        name: ${spring.application.name}
        version: 1.0.0
# 让它安静点,避免把 MCP JSON 混进正常日志
logging:
  level:
    org.springframework.ai.mcp: INFO
  pattern:
    console:
server:
  main:
    banner-mode: off
  # 非 Web 进程(可选)
  # web-application-type: none

入口 & 工具

DemoApplication.java

@SpringBootApplication
public class DemoApplication {
  public static void main(String[] args) {
    SpringApplication.run(DemoApplication.class, args);
  }
}

启动配置。

NoteService.java

@Service
public class NoteService {

  @Tool(description = "把输入转成大写并附带字符数")
  public Map<String, Object> toUpper(
      @ToolParam(name = "text", description = "要转换的文本") String text) {

    if (text == null) text = "";
    return Map.of(
        "upper", text.toUpperCase(),
        "length", text.length()
    );
  }
}

注意,你的服务方法必须配置 @Tool(description = "获取电脑配置")

多一个注解,就向外多提供一个工具!!!

生成Jar包

image-20251010100847256

在客户端项目中引用

{
  "mcpServers": {
    "mcp-server-demo": {
      "command": "java",
      "args": [
        "-Dspring.ai.mcp.server.stdio=true",
        "-jar",
        "C:/path/to/target/mcp-server-demo-1.0.0.jar"
      ]
    }
  }
}

注意上一步生成的jar包要放到这个"C:/path/to/target/mcp-server-demo-1.0.0.jar"中。

底层调用逻辑

一、启动客户端

假设你运行的客户端配置里包含:

"mcpServers": {
  "filesystem": {...},
  "mcp-server-computer": {...}
}

当客户端启动时,会做以下几步:

1)客户端启动子进程

客户端发现你声明了两个 MCP 服务器,于是准备分别启动它们。

npx.cmd -y @modelcontextprotocol/server-filesystem D:/folder/MCP-test D:/folder/MCP-test

java -Dspring.ai.mcp.server.stdio=true -jar D:/folder/.../mcp-server-computer-1.0.0.jar

这两个命令都会启动新的独立进程,“挂在”客户端进程下面,通过标准输入/输出通信。

2)客户端 → 服务端 “握手”(initialize)

客户端通过每个子进程的 stdin 写入 MCP 初始化请求:

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "initialize",
  "params": {
    "clientInfo": {"name": "MyClient", "version": "1.0"},
    "capabilities": {}
  }
}

3)服务端 → 客户端 回应握手结果

你的 mcp-server-computer.jar在内部收到这个 JSON-RPC 请求后,返回:

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "serverInfo": {"name": "mcp-server-computer", "version": "1.0.0"},
    "capabilities": {"tools": true}
  }
}

客户端读取这条 JSON(从 stdout),于是握手成功

4)客户端请求工具清单

紧接着客户端发:

{ 
	"jsonrpc": "2.0", 
	"id": 2, 
    "method": "tools/list", 
	"params": {} 
}

服务端返回:

{
  "jsonrpc": "2.0",
  "id": 2,
  "result": {
    "tools": [
      { "name": "queryConfig", "description": "获取电脑配置", ... }
    ]
  }
}

客户端现在就知道:“这个 MCP Server 提供一个叫 queryConfig 的工具”。

二、用户发起请求

1)用户发起问题“请读取这台电脑的配置”

2)客户端构造 Chat 请求

将用户消息 + 系统消息 + 工具的 JSON Schema(名称、入参、描述)一起发给大模型。

请求里不仅包含用户输入,还要告诉模型——“你可以调用这些工具,这些工具的参数长这样。”

在 Spring AI 中,这一步通常由 ChatClient 或底层 ChatModel 完成

@GetMapping("/test-workflow")
public String testWorkflow(@RequestParam String question) {
    log.info("Workflow测试请求接收 - 问题: {}", question);
    var chatClient = chatClientBuilder
            .defaultOptions(OllamaOptions.builder().model("qwen2.5:7b-instruct").build())
            .build();

    ChatResponse response = chatClient
            .prompt(question)
            .system("你必须使用提供的工具完成任务,禁止凭空回答;若没有调用任何工具,不要返回答案。")
            .tools(tools)
            .call()
            .chatResponse();
    log.info("工具调用情况: {}", tools.getToolCallbacks());
    log.info("模型响应: {}", response);
    return response.toString();
}

实际发送给模型的底层 payload 类似这样:

{
  "model": "qwen2.5:7b-instruct",
  "messages": [
    {
      "role": "system",
      "content": "你必须使用提供的工具完成任务..."
    },
    {
      "role": "user",
      "content": "在 D:/folder/MCP-test 下创建配置.txt..."
    }
  ],
  "tools": [
    {
      "type": "function",
      "function": {
        "name": "queryConfig",
        "description": "获取电脑配置(操作系统、用户信息、Java环境等)",
        "parameters": {
          "type": "object",
          "properties": {
            "computer": {
              "type": "string",
              "description": "电脑名称,可为空"
            }
          }
        }
      }
    },
    {
      "type": "function",
      "function": {
        "name": "write_file",
        "description": "创建一个文件并写入内容",
        "parameters": {
          "type": "object",
          "properties": {
            "path": {"type": "string"},
            "content": {"type": "string"}
          }
        }
      }
    }
  ],
  "tool_choice": "auto"
}

3)模型分析任务 → 决定调用哪个工具

大模型在接收到 prompt 后,会“思考”:

“要回答这个问题,我需要外部信息吗?” “有一个叫 queryConfig 的工具能获取电脑配置,那我就调用它。”

现代指令模型(包括 Qwen、GPT-4o、Claude、Llama)都经过一种专门的微调(finetune)

当输入里包含 "tools""parameters""function" 这样的字段时, 模型学会:

  • 识别哪些任务需要调用工具;
  • 生成标准 JSON 格式的 tool_calls
  • 不胡编乱造工具名;
  • 遵守 schema 中参数名的拼写和结构。

于是它返回结构化响应(OpenAI function-calling 格式):

{
  "id": "chatcmpl-01",
  "object": "chat.completion",
  "choices": [
    {
      "index": 0,
      "message": {
        "role": "assistant",
        "tool_calls": [
          {
            "id": "call_1",
            "type": "function",
            "function": {
              "name": "queryConfig",
              "arguments": "{\"computer\": \"DESKTOP-01\"}"
            }
          }
        ]
      }
    }
  ]
}

4)客户端执行工具调用(MCP 调用链)

Spring AI 的 DefaultToolCallingManager 会检测到(框架自动实现):

if (response.hasToolCalls()) {
    executeToolCalls(response.getToolCalls());
}

客户端解析出模型请求调用的工具名 queryConfig,然后通过 MCP 协议向对应的 MCP Server(你的 mcp-server-computer)发送调用请求(通过 stdin):

{
  "jsonrpc": "2.0",
  "id": 3,
  "method": "tools/call",
  "params": {
    "name": "queryConfig",
    "arguments": { "computer": "DESKTOP-01" }
  }
}

注意,默认的工具名就是函数名!!!

你的 MCP Server(Spring Boot 进程)接收后,由框架自动路由到 @Tool 方法:

@Service
public class ComputerService {

    @Tool(description = "获取电脑配置")
    public ComputerFunctionResponse queryConfig(
        @ToolParam(name = "computer", description = "电脑名称") String computer
    ) {
        log.info("获取电脑配置信息: {}", computer);
        // 读取系统属性 + systeminfo 等命令
        ...
        return response; // 返回 JSON 对象
    }
}

6)客户端将结果反馈回模型(第二轮)

客户端收到结果后,会把它作为新的上下文输入发回模型:

{
  "role": "tool",
  "tool_call_id": "call_1",
  "name": "queryConfig",
  "content": "{\"osName\": \"Windows 11\", ... }"
}

模型在这时再进行一轮“总结性思考”:

“我得到了电脑配置结果,可以组织成自然语言回答。”

最终返回:

{
  "role": "assistant",
  "content": "你的电脑运行的是 Windows 11,架构为 x64,用户名为 Jerry。"
}
sequenceDiagram
    participant User
    participant Client
    participant Model
    participant MCPServer as MCP Server

    User->>Client: "请读取这台电脑的配置"
    Client->>Model: 封装 Prompt + Tool Schema<br/>发送请求
    Model-->>Client: 返回 tool_calls: queryConfig(...)
    Client->>MCPServer: 通过 MCP stdin 发送<br/>tools/call 请求
    MCPServer-->>Client: 执行 Java @Tool 方法<br/>返回 JSON 结果
    Client->>Model: 将工具结果再发给模型
    Model-->>Client: 生成自然语言回答
    Client-->>User: 输出最终文本

如果有长的调用链

当模型需要调用多个工具(例如:create_directoryqueryConfigwrite_file), Spring AI 会自动进入一个「循环式调用流程」,直到模型自己不再请求调用工具为止。

框架就会自动:

1.发现模型要调用工具;

2.执行工具;

3.把结果回灌给模型;

4.模型如果再要求调用下一个工具,再执行;

5.一直到模型返回纯文本。

while (true) {
    ChatResponse response = model.call(currentPrompt);

    if (response.hasToolCalls()) {
        // 1. 模型请求调用工具
        for (ToolCall call : response.getToolCalls()) {
            Object result = executeTool(call.name, call.arguments);
            // 2. 把结果封装成“tool_result”消息
            currentPrompt.addToolResult(call.id, call.name, result);
        }
    } else {
        // 没有工具调用 = 模型给出最终自然语言
        return response;
    }
}
© 版权声明
THE END
喜欢就支持一下吧
点赞 0 分享 收藏
评论 抢沙发
取消