Nicksxs's Blog

What hurts more, the pain of hard work or the pain of regret?

鉴于用upic配置r2存储的图床失败后,目前的方式退化了一下,直接使用rclone作为临时替代
但是也才了个小坑,因为rclone在做copy的时候是不管bucket是否存在的,直接会调用 CreateBucket 命令
同时我们的accessKey一般是只有对bucket内部文件的读写权限,所以会导致一个比较尴尬的问题
直接用copy就是报

1
2025/05/11 21:16:04 NOTICE: Failed to copy: failed to prepare upload: operation error S3: CreateBucket, https response error StatusCode: 403, RequestID: , HostID: , api error AccessDenied: Access Denied

这个403的错误,结果谷歌搜索了下
可以通过这个选项进行屏蔽

1
--s3-no-check-bucket

这样我就能用

1
./rclone --s3-no-check-bucket copy ~/Downloads/r2/test_for_rclone.png r2_new:blog

来上传了,只是这里相比uPic麻烦了两步,第一截图后需要对文件进行命名,不然就是qq截图或者微信截图的文件名,
第二上传后需要自己拼图片的链接,当然希望是能够使用uPic的,但目前的问题是uPic似乎对于s3协议的支持不那么好
噢,对了,我用的是直接基于开源代码的自己打包版本,个人感觉还有点不太习惯从免费用成付费软件,主要是它是开源的
打算空了再折腾打包试试,或者自己调试下,实在不行就付费了

在大模型的演进过程中,mcp是个对于使用者非常有用的一个协议或者说工具,Model Context Protocol (MCP) 是一种专为大型语言模型和 AI 系统设计的通信协议框架,它解决了 AI 交互中的一个核心问题:如何有效地管理、传递和控制上下文信息。打个比方,如果把 AI 模型比作一个智能助手,那么 MCP 就是确保这个助手能够”记住”之前的对话、理解当前问题的背景,并按照特定规则进行回应的通信机制。
MCP 的工作原理
基本结构
MCP 的基本结构可以分为三个主要部分:

  1. 上下文容器 (Context Container):存储对话历史、系统指令和用户背景等信息
  2. 控制参数 (Control Parameters):调节模型行为的设置,如温度、最大输出长度等
  3. 消息体 (Message Body):当前需要处理的输入内容
    ┌─────────────────────────────────┐
    │ MCP 请求/响应结构 │

├─────────────────────────────────┤
│ │
│ ┌─────────────────────────┐ │
│ │ 上下文容器 │ │
│ │ ┌─────────────────┐ │ │
│ │ │ 对话历史 │ │ │
│ │ └─────────────────┘ │ │
│ │ ┌─────────────────┐ │ │
│ │ │ 系统指令 │ │ │
│ │ └─────────────────┘ │ │
│ │ ┌─────────────────┐ │ │
│ │ │ 用户背景 │ │ │
│ │ └─────────────────┘ │ │
│ └─────────────────────────┘ │
│ │
│ ┌─────────────────────────┐ │
│ │ 控制参数 │ │
│ └─────────────────────────┘ │
│ │
│ ┌─────────────────────────┐ │
│ │ 消息体 │ │
│ └─────────────────────────┘ │
│ │
└─────────────────────────────────┘

实操

首先基于start.spring.io创建一个springboot应用,需要springboot的版本3.3+和jdk17+
然后添加maven依赖

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-server</artifactId>
<version>1.0.0-M8</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>6.1.12</version>
</dependency>

我们就可以实现一个mcp的demo server

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
package com.nicksxs.mcp_demo;

import com.jayway.jsonpath.JsonPath;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestClient;

import java.util.List;
import java.util.Map;

@Service
public class WeatherService {

private final RestClient restClient;

public WeatherService() {
this.restClient = RestClient.builder()
.baseUrl("https://api.weather.gov")
.defaultHeader("Accept", "application/geo+json")
.defaultHeader("User-Agent", "WeatherApiClient/1.0 (your@email.com)")
.build();
}

@Tool(description = "Get weather forecast for a specific latitude/longitude")
public String getWeatherForecastByLocation(
double latitude, // Latitude coordinate
double longitude // Longitude coordinate
) {
// 首先获取点位信息
String pointsResponse = restClient.get()
.uri("/points/{lat},{lon}", latitude, longitude)
.retrieve()
.body(String.class);

// 从点位响应中提取预报URL
String forecastUrl = JsonPath.read(pointsResponse, "$.properties.forecast");

// 获取天气预报
String forecast = restClient.get()
.uri(forecastUrl)
.retrieve()
.body(String.class);

// 从预报中提取第一个周期的详细信息
String detailedForecast = JsonPath.read(forecast, "$.properties.periods[0].detailedForecast");
String temperature = JsonPath.read(forecast, "$.properties.periods[0].temperature").toString();
String temperatureUnit = JsonPath.read(forecast, "$.properties.periods[0].temperatureUnit");
String windSpeed = JsonPath.read(forecast, "$.properties.periods[0].windSpeed");
String windDirection = JsonPath.read(forecast, "$.properties.periods[0].windDirection");

// 构建返回信息
return String.format("Temperature: %s°%s\nWind: %s %s\nForecast: %s",
temperature, temperatureUnit, windSpeed, windDirection, detailedForecast);
// Returns detailed forecast including:
// - Temperature and unit
// - Wind speed and direction
// - Detailed forecast description
}

@Tool(description = "Get weather alerts for a US state")
public String getAlerts(@ToolParam(description = "Two-letter US state code (e.g. CA, NY)") String state) {

// 获取指定州的天气警报
String alertsResponse = restClient.get()
.uri("/alerts/active/area/{state}", state)
.retrieve()
.body(String.class);

// 检查是否有警报
List<Map<String, Object>> features = JsonPath.read(alertsResponse, "$.features");
if (features.isEmpty()) {
return "当前没有活动警报。";
}

// 构建警报信息
StringBuilder alertInfo = new StringBuilder();
for (Map<String, Object> feature : features) {
String event = JsonPath.read(feature, "$.properties.event");
String area = JsonPath.read(feature, "$.properties.areaDesc");
String severity = JsonPath.read(feature, "$.properties.severity");
String description = JsonPath.read(feature, "$.properties.description");
String instruction = JsonPath.read(feature, "$.properties.instruction");

alertInfo.append("警报类型: ").append(event).append("\n");
alertInfo.append("影响区域: ").append(area).append("\n");
alertInfo.append("严重程度: ").append(severity).append("\n");
alertInfo.append("描述: ").append(description).append("\n");
if (instruction != null) {
alertInfo.append("安全指示: ").append(instruction).append("\n");
}
alertInfo.append("\n---\n\n");
}

return alertInfo.toString();
}

// ......
}

通过 api.weather.gov 来请求天气服务,给出结果
然后通过一个客户端来访问请求下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public static void main(String[] args) {
var stdioParams = ServerParameters.builder("java")
.args("-jar", "/Users/shixuesen/Downloads/mcp-demo/target/mcp-demo-0.0.1-SNAPSHOT.jar")
.build();

var stdioTransport = new StdioClientTransport(stdioParams);

var mcpClient = McpClient.sync(stdioTransport).build();

mcpClient.initialize();

// McpSchema.ListToolsResult toolsList = mcpClient.listTools();

McpSchema.CallToolResult weather = mcpClient.callTool(
new McpSchema.CallToolRequest("getWeatherForecastByLocation",
Map.of("latitude", "47.6062", "longitude", "-122.3321")));
System.out.println(weather.content());

McpSchema.CallToolResult alert = mcpClient.callTool(
new McpSchema.CallToolRequest("getAlerts", Map.of("state", "NY")));

mcpClient.closeGracefully();
}

这个就是实现了mcp的简单示例,几个问题,要注意java的多版本管理,我这边主要是用jdk1.8,切成 17需要对应改maven的配置等

上次简单介绍了openmanus的使用,但是它究竟是怎么个原理还是一知半解的,如果想要能比较深入的理解,最直接粗暴的就是阅读源码了,然而对于很多人包括我来说阅读源码不是件简单的事情,有时候会陷入局部细节,不得要领
正好这次我发现了有个理解项目的神器,这次不加双引号是因为这个真的好
比如我们就拿openmanus举例,首先python的语法就没那么熟,以及对整体结构的理解
那么我们就可以打开 openmanus 的仓库地址
https://github.com/mannaandpoem/OpenManus
然后把 github 替换成 deepwiki,
页面变成了这样

左边是结构大纲,中间我们可以看到项目的主体介绍,包括有核心的架构图,agent的继承关系,tool的生态系统,LLM的集成

这里就展示了核心架构,通过这个方式我们如果想对一个项目有个初始的认识,就变得非常简单,因为很多项目的当前的代码都是非常复杂的,没有足够的时间精力是没办法一下子学习到项目的整体结构,因为除非我们是要真正投入到一个项目的开发贡献中,我们大概率都是从了解整体的概况,再分模块的去学习,而这个正是这个deepwiki做得非常牛的地方,相当于给每个项目都加了一个更详细具体的wiki,更牛的还有我们可以通过对话的形式进行提问题,比如我们想自己开发个工具,让openmanus集成进去进行调用

它就给出了一个非常详尽的回答,
首先

  1. 创建自定义工具类,创建一个基于 BaseTool 的新工具类
  2. 将工具添加到Manus代理,修改Manus类的available_tools定义,将您的工具添加到默认工具列表
  3. 使用MCP协议集成远程工具(可选)
  4. 工具执行流程
    一旦您的工具被集成,Manus代理会在执行过程中使用它:
  • 代理的think()方法会向LLM发送请求,包含所有可用工具的信息
  • LLM会决定使用哪个工具(包括您的Excel工具)
  • 代理的act()方法会执行工具调用
  • 您的工具的execute()方法会被调用,执行Excel函数并返回结果
  • 结果会被添加到代理的记忆中,用于后续决策
    这样子就让一个项目的理解跟上手变得非常简单,甚至比如我们想要参与这个项目的开源贡献,也能借助这个 deepwiki 来让我们能快速上手。
    如果对这个结果不满意还可以开启deep research,能让大模型通过深度思考来给出更加合理的答案,这个deepwiki是目前为止我觉得大模型对程序员最有效的一个工具了。

前阵子一个manus在目前的所谓人工智能圈子里甚至普通人视野里都很火了,宣称是什么中国的下一个deepseek时刻,首先deepseek是经过了v1,v2等一系列版本的迭代之后,并且一直是在技术上非常花功夫的,有种宝剑锋从磨砺出的感觉,而这个manus听着更像是个蹭热度的
这不没出多久有个openmanus宣称用了三小时做了个开源的复刻版,那么我们就来简单体验下,从概念上来说吧,有点类似于做了个mcp的规划和整合调用
我个人理解好像没有到改变世界的程度
首先呢我们先来安装下
可以使用conda,也可以使用uv,以conda举例,先建个环境

1
2
conda create -n open_manus python=3.12
conda activate open_manus

然后来clone下代码仓库

1
2
git clone https://github.com/mannaandpoem/OpenManus.git
cd OpenManus

接着再安装下依赖

1
pip install -r requirements.txt

这里可以借助下源替换加速,临时使用可以这样子 pip install -i https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple some-package , 这里需要安装蛮久的,可能也说明了这是个依赖于很多现成库的工具

第二阶段配置

配置其实也是常规的,依赖于大模型,那么要不就是自己部署提供api,要不就是去火山引擎或者其他大模型api提供商搞个api(免费额度用完要自己付费的)

1
cp config/config.example.toml config/config.toml

这一步其实跟之前使用chatbox连接火山的配置类似
因为像国外很多都还是openai的接口服务,这边就需要改用成国内可用的

1
2
3
4
5
6
[llm]
model = "deepseek-r1-250120" # The LLM model to use
base_url = "https://ark.cn-beijing.volces.com/api/v3" # API endpoint URL
api_key = "xxxxxxxxxxxxxxxxxx" # Your API key
max_tokens = 8192 # Maximum number of tokens in the response
temperature = 0.0

模型可以用deepseek-r1,不过要注意是什么时间版本的,否则也会访问不到
然后我们就可以运行

1
python main.py

来运行openmanus,我们简单问个问题

1
明天杭州的天气怎么样,给出个穿衣指南

看下结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
python main.py
INFO [browser_use] BrowserUse logging setup complete with level info
INFO [root] Anonymized telemetry enabled. See https://docs.browser-use.com/development/telemetry for more information.
Enter your prompt: 帮我看下明天杭州的天气,并且生成穿衣指南
2025-04-20 20:40:14.721 | WARNING | __main__:main:16 - Processing your request...
2025-04-20 20:40:14.722 | INFO | app.agent.base:run:140 - Executing step 1/20
2025-04-20 20:40:33.488 | INFO | app.llm:update_token_count:250 - Token usage: Input=2165, Completion=658, Cumulative Input=2165, Cumulative Completion=658, Total=2823, Cumulative Total=2823
2025-04-20 20:40:33.489 | INFO | app.agent.toolcall:think:81 - ✨ Manus's thoughts:


2025-04-20 20:40:33.489 | INFO | app.agent.toolcall:think:82 - 🛠️ Manus selected 1 tools to use
2025-04-20 20:40:33.489 | INFO | app.agent.toolcall:think:86 - 🧰 Tools being prepared: ['browser_use']
2025-04-20 20:40:33.489 | INFO | app.agent.toolcall:think:89 - 🔧 Tool arguments: {
"action": "web_search",
"query": "杭州明天天气预报"
}
2025-04-20 20:40:33.490 | INFO | app.agent.toolcall:execute_tool:180 - 🔧 Activating tool: 'browser_use'...
ERROR [browser] Failed to initialize Playwright browser: BrowserType.launch: Executable doesn't exist at /Users/username/Library/Caches/ms-playwright/chromium-1161/chrome-mac/Chromium.app/Contents/MacOS/Chromium
╔════════════════════════════════════════════════════════════╗
║ Looks like Playwright was just installed or updated. ║
║ Please run the following command to download new browsers: ║
║ ║
║ playwright install ║
║ ║
║ <3 Playwright Team ║
╚════════════════════════════════════════════════════════════╝
2025-04-20 20:40:34.896 | INFO | app.agent.toolcall:act:150 - 🎯 Tool 'browser_use' completed its mission! Result: Observed output of cmd `browser_use` executed:
Error: Browser action 'web_search' failed: BrowserType.launch: Executable doesn't exist at /Users/username/Library/Caches/ms-playwright/chromium-1161/chrome-mac/Chromium.app/Contents/MacOS/Chromium
╔════════════════════════════════════════════════════════════╗
║ Looks like Playwright was just installed or updated. ║
║ Please run the following command to download new browsers: ║
║ ║
║ playwright install ║
║ ║
║ <3 Playwright Team ║
╚════════════════════════════════════════════════════════════╝
2025-04-20 20:40:34.896 | INFO | app.agent.base:run:140 - Executing step 2/20
ERROR [browser] Failed to initialize Playwright browser: BrowserType.launch: Executable doesn't exist at /Users/username/Library/Caches/ms-playwright/chromium-1161/chrome-mac/Chromium.app/Contents/MacOS/Chromium
╔════════════════════════════════════════════════════════════╗
║ Looks like Playwright was just installed or updated. ║
║ Please run the following command to download new browsers: ║
║ ║
║ playwright install ║
║ ║
║ <3 Playwright Team ║
╚════════════════════════════════════════════════════════════╝
WARNING [browser] Page load failed, continuing...
ERROR [browser] Failed to initialize Playwright browser: BrowserType.launch: Executable doesn't exist at /Users/username/Library/Caches/ms-playwright/chromium-1161/chrome-mac/Chromium.app/Contents/MacOS/Chromium
╔════════════════════════════════════════════════════════════╗
║ Looks like Playwright was just installed or updated. ║
║ Please run the following command to download new browsers: ║
║ ║
║ playwright install ║
║ ║
║ <3 Playwright Team ║
╚════════════════════════════════════════════════════════════╝

根据这个返回可以看到它做了些啥,主要是规划步骤和选择调用的工具,相对来说没有特别的,playwright我就不去搞了,浏览器那套

arthas是阿里开源的一个非常好用的java诊断工具,提供了很多很好用的命令,这里讲一个最近使用到的
就是将arthas挂载上我们的springboot应用,然后调用其中的方法,这样能够在如果没加日志已经看不到函数返回时更方便的排查问题
首先举个例子,我们有个Controller
它的一个query方法是这样的

1
2
3
4
5
@RequestMapping(value = "/query", method = RequestMethod.GET)
@ResponseBody
public String query() {
return demoService.queryName("1");
}

而在这个demoService中它的实现是这样

1
2
3
4
5
6
7
public String queryName(String no) {
if ("1".equals(no)) {
return "no1";
} else {
return "no2";
}
}

假如现在Controller这的这个方法有点问题,那么我想确认下是不是demoService这个方法的实现有问题,或者说确定下它的返回值是否符合预期
那么我们就可以在应用启动后,运行arthas,找到这个应用的进程,进行挂载
然后执行

1
vmtool --action getInstances --className com.nicksxs.spbdemo.service.DemoServiceImpl --express 'instances[0].queryName("1")'

先介绍下这个vmtool命令
主要来说 vmtool 可以利用JVMTI接口,实现查询内存对象,强制 GC 等功能。
例如官方示例里的,我想把内存里的string对象捞一些出来看看存的是啥

1
vmtool --action getInstances --className java.lang.String --limit 10

就可以这样,首先这个action就是指定要做的操作,支持的action 还包括

1
2
forceGc
interruptThread

等,那么对于 getInstances 就是从内存里捞出这个类的对象,然后是后面一部分
--express 就是执行表达式,这里的表达式,
instances[0].queryName("1") 其中 instances 就是前面从内存中获取的对象数组,因为这些是对象的非静态方法,那就需要从其中取一个来执行我们的方法
另外假如我们的场景里需要对比如返回结果做个json序列化
我们可以这样

1
vmtool --action getInstances --className com.nicksxs.spbdemo.service.DemoServiceImpl --express '@com.alibaba.fastjson.JSON@toJSONString(instances[0].queryName("1"))'

这里为什么类开头跟方法开头要用 @, 是因为对于类和静态方法的调用规则是这样,还有如果代码比较多,有可能默认的类加载器中没有加载这个JSON类,那么就需要在参数中加上指定的classloader,
可以用sc命令来查找我们的目标类的类加载器,一般来说如果目标类是我们核心业务的,大概率也会有JSON这个类

1
sc -d com.nicksxs.spbdemo.service.DemoServiceImpl

然后在上面命令中加上sc结果中的 classLoaderHash 的值,

1
vmtool --action getInstances -c 18b4aac2 --className com.nicksxs.spbdemo.service.DemoServiceImpl --express '@com.alibaba.fastjson.JSON@toJSONString(instances[0].queryName("1"))'

这样就能正常执行了

0%