码农戏码

新生代农民工的自我修养

0%

arthas 为例,探索一下Spring AI 中 MCP Client端通信机制的实现。

Client调用流程

MCP 客户端调用服务器工具的流程如下:

1、建立连接 :与 MCP 服务器搭建通信链路。

2、查询工具 :获取服务器上所有外部工具的数量信息。

3、生成列表 :将查询到的外部工具整理成列表,并融入当前对话场景。

4、调用工具 :通过 Function calling 技术调用所需的外部工具。

sequenceDiagram
    participant Client as MCP客户端 (如AI应用)
    participant Server as MCP服务端

    Note over Client,Server: 1. 发现阶段
    Client->>Server: 请求 resources/list
    Server-->>Client: 返回资源元信息列表
(URI, 名称, 描述, MIME类型等) Note over Client,Server: 2. 读取阶段 Client->>Server: 请求 resources/read (指定URI) Server-->>Client: 返回资源实际内容 Note over Client,Server: 可选:实时更新 Client->>Server: resources/subscribe (订阅特定URI) Server-->>Client: notifications/resources/updated (内容变更时推送)

通信协议

MCP 中 Client 与 Server 间使用 JSON-RPC 2.0 作为通信消息格式。

JSON-RPC 2.0是一种基于JSONJavaScript Object Notation)的远程过程调用(RPC)协议。

它是一种轻量级的、无状态的、跨语言的通信协议,常用于客户端与服务端之间的交互。

JSON-RPC 是 RPC(远程过程调用)的一种具体实现,RPC 是一种通信范式,其核心目标是屏蔽网络细节,使远程调用如同本地调用般简单,并可基于多种底层网络协议(如 TCP/HTTP)实现。常见的 RPC 框架有 gRPC、Dubbo 和 Thrift。

Request

1
2
3
4
5
6
{
"jsonrpc": "2.0",
"method": "方法名",
"params": {"参数名": "值"} | ["值 1", "值 2"], // 对象或数组
"id": "唯一ID" // 可选(通知请求可省略)
}

jsonrpc : 必须为 "2.0"

method: 调用的方法名(字符串)。

params: 参数(可省略),支持对象(命名参数)或数组(位置参数)。

id: 请求标识符。

Response

成功

1
2
3
4
5
{
"jsonrpc": "2.0",
"result": "返回值",
"id": "对应请求 ID"
}

失败

1
2
3
4
5
6
7
8
9
{
"jsonrpc": "2.0",
"error": {
"code": -32601,
"message": "Method not found",
"data": "额外错误信息" // 可选
},
"id": "对应请求 ID" // 若请求无 ID,则为 null
}

Spring AI中相应源码

1
2
3
4
5
6
public record JSONRPCRequest( // @formatter:off  
@JsonProperty("jsonrpc") String jsonrpc,
@JsonProperty("method") String method,
@JsonProperty("id") Object id,
@JsonProperty("params") Object params) implements JSONRPCMessage {
}
1
2
3
4
5
public record JSONRPCResponse( // @formatter:off  
@JsonProperty("jsonrpc") String jsonrpc,
@JsonProperty("id") Object id,
@JsonProperty("result") Object result,
@JsonProperty("error") JSONRPCError error) implements JSONRPCMessage {

🚀 三种通信方式

MCP(模型上下文协议)主要支持 3 种通信方式,每种方式适用于不同的场景:

方式 通信协议 适用场景 优势 限制
1. STDIO 标准输入/输出 本地进程通信 简单、低延迟、无需网络 仅限本机、需管理进程生命周期
2. SSE Server-Sent Events 远程服务器通信 支持跨网络、自动重连 单向流(服务器→客户端)
3. HTTP JSON-RPC over HTTP 远程调用、负载均衡 双向请求-响应、可扩展 需处理网络延迟

建立连接

根据MCP的3 种通信方式,Spring AI 中如何选择?

1
2
3
4
5
6
7
8
9
10
11

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


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

创建MCPTransport

StreamableHttpWebFluxTransportAutoConfiguration
创建WebClientStreamableHttpTransport

创建MCPClient

根据配置的MCP Client 类型,创建相应类型的MCPClient

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* Client types supported by the MCP client.
*/
public enum ClientType {

/**
* Synchronous (McpSyncClient) client
*/
SYNC,

/**
* Asynchronous (McpAsyncClient) client
*/
ASYNC

}

连接建立成功后,会发送一个method为initialize的初始化的消息

1
2
3
Sending message for method initialize

Sending message JSONRPCRequest[jsonrpc=2.0, method=initialize, id=faf05893-0, params=InitializeRequest[protocolVersion=2025-06-18, capabilities=ClientCapabilities[experimental=null, roots=null, sampling=null, elicitation=null], clientInfo=Implementation[name=north-agent - arthas-mcp, title=arthas-mcp, version=1.0.0], meta=null]]

查询工具

建立完连接后,会发送 tools/list 请求

1
Sending message JSONRPCRequest[jsonrpc=2.0, method=tools/list, id=5fc929e0-1, params=PaginatedRequest[cursor=null, meta=null]]

会获取到完整的工具列表

生成列表

在Spring AI体系里面,MCP Client 获取的工具列表,会抽象成 ToolCallback

1
2
3
4
SyncMcpToolCallbackProvider mcpProvider = mcpToolCallbackProvider.getIfAvailable();  
List<ToolCallback> arthasCallbacks = mcpProvider == null
? List.of()
: List.of(mcpProvider.getToolCallbacks());

调用工具

比如让查询一下JVM信息,会调用 jvm 命令,让服务端发送 tools/call 命令

1
Sending message JSONRPCRequest[jsonrpc=2.0, method=tools/call, id=5fc929e0-2, params=CallToolRequest[name=jvm, arguments={}, meta={user_id=user-001, _AGENT_=老}]]

鉴于当前人人都因AI引起的焦虑,自己想不明白时,出去走走,向别人请教,也许别人不能直接解答困惑,但可能被启发一下

所以上周去参与了一下【腾讯云架构师沙龙】,向优秀的同行学习一下

第一位演讲嘉宾沈欣老师就带来了一些思考。

软件是什么?软件开发是什么样?

我们都是战术的勤奋者,但战略上的确缺少一些思考。

不过对潘志伟老师的演进主题《AI时代研发新范式的演进与实战》更加感兴趣

他讲了Vibe Coding规模落地,个人实践到团队变革。因为这部分有切身感受。

Vibe Coding已经快一年了,从迷茫到兴奋,再到苦恼,再回到平常心。

有成功有失败,未来可能真如沈欣老师所讲达到真正软件开发皇冠顶上的明珠

一年前,当时Vibe Coding声音还没有现如今火爆的时候,当时CTO就已经在全公司推广,所有人必须拥抱AI,每个人都要成为300倍效率工程师

当时我也有很多不理解的地方,有几次我们聊到晚上11点。很有收获,主要围绕AI

1、为什么出来再创业,AI绝对是未来,现在就是好的契机,之前他就在公司试点AI,有些成果,但还不满意,时机不等人,希望这次能成功落地

他其实就是希望从
需求分析 -> 架构设计 -> 代码落地 -> 代码审查 -> 发布部署 这一系列流程,通过AI加持,快速输出产品

用现在更流行的话叫Spec-Driven Development,虽然当时还没有这名字

2、为什么不重产品需求质量,却总嫌弃开发太慢?
这是我当时特别不理解的地方,我想应该是对AI理解不深入。今年的一篇blog写明白了他的想法[[编程Agent如何重塑工程、产品与设计]]

当时我幼稚地认为他是不是没管理过完整的产品研发团队,为什么总打压研发,却对产品不完整的PRD却不理不管。甚至让我们不要看产品PRD了,都听AI的,AI给出的产品方案直接采用。

后来让他请教,他的思路跟[[编程Agent如何重塑工程、产品与设计]]是一样的
以前编程很难很重,所以对PRD要层层把控,把产品PRD这一环做得很重,这样编程减小返工

但现在AI,生成代码很便捷,不应该跟过去思路一样,把不成熟的PRD快速实现POC版本,加快迭代,而不是瀑布式开发。

3、资金的绝对支持,公司开通比较贵的AugmentCode,它有强大的上下文处理,适合大型项目。

刚开始好些人排斥AI还不用它,后来一个月要800美元开销都不太够。

当时还没有现在这么多的编程Agent,AugmentCode有个问题,思考时间比较长,输出比较慢。

他就让大家自己选AI工具,公司都可以报销

回想起23年的时候,当时那公司要大家去尝试AI,召集了一些架构师和资深开发学习,为公司业务提效。
当时GitHub Copilot是10美元一个月,公司都不愿意出钱。
加班学习AI,自己出时间出钱,最后成果还得给公司。真是学习华为狼性文化的典范

有些人真的就是不一样
“花一秒钟就看透事物本质的人,和花一辈子都看不清事物本质的人,注定是截然不同的命运。”
CTO绝对是战略性人才

回到团队落地Vibe Coding

潘老师讲的几件事,启发了我

1、以前绩效有一个手段,按照代码量排名

虽然这个手段我也反对,因为同样一个问题,初级者写了50行才解决,但高手抽象后可能10行 甚至一行就解决了。

但从宏观角度,这算是一个量化指标

那到了AI,代码量指标就得淘汰了。

用什么替代呢?

可能是token消耗量,也可以是与AI交流的次数。

一个人一天只问了AI一个问题,另一个人跟AI对话了10个问题。
这应该比较明显吧,谁在思考谁在摸鱼

2、减少差异化,统一工具

这我们以前好像做得不好,每个人都可以寻找自己适合的AI

Coding可以统一,但架构设计还真是有些差距

灵活的地方就得灵活应对,每个AI还真有自己的灵魂

像我之前做架构设计,发现deepseek比较自我坚持,跟他说错误的设计,他不会同意

但有些AI,像个马屁精,你说什么就是一顿夸,情绪价值满满的,但可能是相当不合理的设计

不过这也可能是管理者的通病,为了减少管理成本

3、提示词也是资产,需要像知识库一样沉淀

我们现在交付的还是原代码,未来可能交付的就是提示词

不仅要沉淀,还要拿出来评比,再推广

不管有什么工具,都要接上MCP,把提示词上报。

公司最后把每个人的提示词再让AI去评分,不用人的参与,人怕得罪人,AI不怕。

给我个人启发是什么?

也想回头复盘一下自己的提示词是否优秀,统计一下自己的token消耗

现在主要使用claude code 和 codex,需要来回切换,虽然平台提供了统计token功能,但两者还得计算总额

想了个办法,自己实现一个mcp server,自己把提示词上报,正好也统计一下token

不过在github上发现了一个现成的软件

codemate:https://github.com/loocor/codmate

对于token消耗一目了然,还能回放对话

让自己保持一种竞技状态,有种追求的精神

AI加剧焦虑,也希望能加速人进化速度

继续排盘软件开发,最近把格局的能力也加上。

直接让AI自己去分析一下什么是格局,以及怎么取

最后的效果是这样的:

看着AI真是啥都知道,先不论对错,说是的有模有样

一句话结论

格局以月令为核心,透干为优先,清纯为贵,成败看全局。

也就是说:

  1. 先看月令是什么
  2. 再看月令所藏之神有没有透出天干
  3. 再看这个“格神”有没有根、有无帮扶、是否被破坏
  4. 最后再看全局能不能成“局”,再谈用神喜忌。

一、先分清“格”和“局”

这两个词经常连在一起说,但其实不是一回事:

  • :偏重“月令主什么”,也就是命局的主导十神从哪里立出来;
  • :偏重“全盘有没有配合成势”,也就是这个格能不能成立、能不能发挥。

可以粗略理解为:

  • = 立题目
  • = 看这道题有没有做成

所以程序上、分析上都建议按这个顺序:

先取格,再判局,最后定用神。


二、主流子平法的取格顺序

1. 以日主为参照,看月令藏干对应什么十神

格局不是脱离日主单独看的。

比如:

  • 甲日主 来说,辛是正官、庚是七杀、己是正财、戊是偏财;
  • 丙日主 来说,戊是食神、己是伤官、癸是正官、壬是七杀。

所以“格”到底是什么,必须先把月令藏干换算成日主的十神


2. 先看“月令所藏之神是否透干”

这是取格的核心。

常见原则

  • 月令本气透出,最容易立格;
  • 月令中气、余气透出,如果本气不透、而透出的那个神又得令得势,也可以立格;
  • 月令多神同透,不能见一个就立一个,要看谁更当令、谁更清纯、谁更能主事。

口诀化理解

月令是根,透干是门。

月令里有这个东西,只是“藏着”;
透到天干,才算“出来做事”。

所以传统上常说:

有令有透,方能真立格。


3. 透者为先,本气为重

如果月令里有多个藏干,优先级通常是:

  1. 本气透干
  2. 中气/余气透干,但明显得势
  3. 都不透,则回到月令本气或当令之气

例如辰、戌、丑、未这类“杂气月”,最容易出现多个候选。
这时不能只看“月支里有什么”,还要看:

  • 谁透出来了;
  • 谁正在得令;
  • 谁在全局中有根;
  • 谁没有被严重冲克。

4. 月令不透时,仍以月令主气为主

有些八字月令之神完全不透,但也不能因此说“没有格”。

这时常见处理是:

  • 先按月令本气取格
  • 若月令是杂气、且出生日期正好落在分金用事阶段,则可参考司权之神做细化判断;
  • 但一般不建议完全抛开月令主气,只按别处旺衰硬立格。

这点和仓库里已有的《月令分金》文档可以配合看。


三、月令分金在“取格”里怎么用

这个项目里已经整理了“月令分金(人元司令分野)”,这对精细取格很有帮助。

但要注意:

  • 月令主气,仍然是定格的大前提;
  • 司权用事,更像是对月令内部轻重的细分;
  • 它适合做“候选格排序”和“杂气月细化”,不一定是完全取代月令本气。

实务上可以这样理解

简法

  • 不细分出生在本月第几天;
  • 直接按月支本气取格;
  • 适合快速排盘和基础分析。

细法

  • 结合节气后第几天,确认当前是余气、中气还是本气在司权;
  • 特别适合寅、辰、巳、未、申、戌、亥、丑这些多藏干月份;
  • 可用于解释“为什么同月不同日,取格重点略有不同”。

一个例子

假设 丙日主生辰月

  • 辰中藏 戊、乙、癸
  • 对丙来说,戊是食神,乙是正印,癸是正官;
  • 如果戊透出,可优先看食神格
  • 如果癸透出且官星清纯,也可能按官格看;
  • 如果都不透,而出生又落在辰月后段、戊土司权明显,则更偏向按食神格/杂气食神去理解。

所以分金的价值,在于帮助判断:

月令里面,到底是谁在“当前主事”。


[[使用ExecutorServiceMetrics对线程池进行监控]]

为了实现这个目标,就让AI进行代码生成,生成得还不错,还自己加戏做了一些额外工作

自定义了一些tag,看着应该有用

1
2
3
4
5
6
private Iterable<Tag> buildTags(String poolType) {  
return Tags.of(
"thread.pool.source", "ThreadPoolUtil",
"thread.pool.type", poolType
);
}
1
2
Iterable<Tag> tags = buildTags(poolType);  
new ExecutorServiceMetrics(executor, poolName, tags).bindTo(meterRegistry);

/actuator/prometheus 中应该能持续看到类似指标:

1
2
3
4
5
executor_active_threads{...,name="Test_xxx",thread_pool_source="ThreadPoolUtil",thread_pool_type="Test"} 1.0
executor_pool_core_threads{...,name="Test_xxx",thread_pool_source="ThreadPoolUtil",thread_pool_type="Test"} 25.0
executor_pool_max_threads{...,name="Test_xxx",thread_pool_source="ThreadPoolUtil",thread_pool_type="Test"} 25.0
executor_queued_tasks{...,name="Test_xxx",thread_pool_source="ThreadPoolUtil",thread_pool_type="Test"} 0.0
executor_completed_tasks_total{...,name="Test_xxx",thread_pool_source="ThreadPoolUtil",thread_pool_type="Test"} 0.0

理想很美好,现实却打脸。在/actuator/prometheus一直没有看到收集到的指标

让AI再分析一下,给了几个猜测

结合当前代码实现,最常见原因通常不是“完全没绑定”,而是以下几类问题:

  1. 查询的指标名不对
  2. 线程池生命周期太短,抓取时已经结束
  3. 实际线程池没有走 ThreadPoolUtil.createFixedThreadPool(...)
  4. binder 没有真正初始化成功

看着第2点,有点道理。防止他在不停地怀疑1、3、4,他不相信人类,只能让他自己干了,让他自己写测试代码进行测试。


分析一通后,又开始猜测了

最可能根因:同名 executor.* 指标的 label 集不一致,Prometheus 无法合并

Prometheus 对同一个 metric family 有一个重要要求:

  • 同名指标的所有时间序列,label key 集合必须一致

也就是说,如果同一个指标名下有两组数据:

第一组

1
executor_active_threads{application="north",name="csmThreadPoolTaskExecutor"} 1

第二组

1
2
3
4
5
6
executor_active_threads{
application="north",
name="Test_xxx",
thread_pool_source="ThreadPoolUtil",
thread_pool_type="Test"
} 0

那么这两组数据的 label key 并不一致:

第一组 label key

  • application
  • name

第二组 label key

  • application
  • name
  • thread_pool_source
  • thread_pool_type

这会导致同一个 executor_active_threads family 出现 label schema 冲突


为什么项目里很可能已经存在另一组 executor.* 指标

Spring Boot 2.7 自带:

  • TaskExecutorMetricsAutoConfiguration

它会自动为 Spring 容器里的 ThreadPoolTaskExecutor / ThreadPoolTaskScheduler 绑定 ExecutorServiceMetrics

本项目里已经存在多个 ThreadPoolTaskExecutor Bean,

Spring Boot 的自动绑定方式相当于:

1
new ExecutorServiceMetrics(threadPoolExecutor, beanName, Collections.emptyList()).bindTo(registry);

也就是说,这批自动注册的 executor.* 指标通常只有:

  • name
  • 再加全局 common tags(如 application

它们没有

  • thread.pool.source
  • thread.pool.type

自己拉屎,自己再吃

推荐修复方案

针对这个问题,推荐优先使用以下方案。

方案一:给 ThreadPoolUtil 指标加独立 metric prefix(最推荐)

不要继续使用默认的:

  • executor.*

而是改成带前缀的独立指标族,例如:

  • threadpoolutil.executor.*
  • north.threadpool.executor.*

代码上可以改为:

1
new ExecutorServiceMetrics(executor, poolName, "threadpoolutil", tags).bindTo(meterRegistry);

这样 Micrometer 原始名会变成:

  • threadpoolutil.executor.active
  • threadpoolutil.executor.completed

在 Prometheus 中会变成类似:

  • threadpoolutil_executor_active_threads
  • threadpoolutil_executor_completed_tasks_total

这样它就不会和 Spring Boot 自动注册的:

  • executor_active_threads

发生 metric family 冲突。

这是最推荐方案,因为它:

  • 保留了 thread.pool.source
  • 保留了 thread.pool.type
  • 避免和已有 executor 指标同名冲突

方案二:去掉额外 tag,仅保留与默认 executor 指标一致的标签集

也可以把自定义 tags 去掉,改成只保留默认的:

  • name

这样就能和 Spring Boot 自动线程池指标共用同一个 family。

但缺点是:

  • 无法区分这些指标是否来自 ThreadPoolUtil
  • 无法直接按 thread_pool_type 做筛选

所以一般不如方案一。

当前处理结果

本次已经按方案二落地:

  • ThreadPoolExecutorMetricsBinder 不再为 ThreadPoolUtilexecutor.* 指标附加:
    • thread.pool.source
    • thread.pool.type
  • 改为只使用 ExecutorServiceMetrics 的默认标签集

这样做的直接效果是:

  1. ThreadPoolUtil 创建的线程池指标可以和 Spring Boot 自动注册的 executor.* 指标使用同一套 label key
  2. /actuator/prometheus 中不再因为 label schema 不一致导致 family 冲突
  3. 查询时应主要通过 name="Test_xxx" 来定位对应线程池

也就是说,修复后 Prometheus 中应按下面方式检索:

1
curl -s http://localhost:8088/actuator/prometheus | grep 'name="Test_'

而不是再按:

1
thread_pool_type="Test"

因为方案二落地后,这两个额外标签已经被移除。


方案三:统一全项目所有 executor 指标的 label 结构

理论上也可以让所有 executor.* 指标都带同样的 label keys。

但这意味着:

  • 要覆盖 Spring Boot 自动注册逻辑
  • 或自己接管所有线程池指标绑定

实现复杂度高,不适合作为当前问题的首选方案。


最后选择了方案二,把label key统一。还真成功了。

这次使用的 gpt-5.4 high 模型,感觉就差一口气。就不让一开始就规避这个低级错误吗?

是不是故意烧点token

最近想对线程池增加一下监控,先从https://grafana.com/grafana/dashboards/ 找到一个关于Spring Boot Executors的Grafana面板

尽然发现,已经有部分线程池被监控到了,记得以前都得使用ExecutorServiceMetrics包装一下,原来在Springboot 2.6.0版本提供了TaskExecutorMetricsAutoConfiguration,可以自动给线程池加上metrics

TaskExecutorMetricsAutoConfiguration

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@AutoConfiguration(after = { MetricsAutoConfiguration.class, SimpleMetricsExportAutoConfiguration.class,  
TaskExecutionAutoConfiguration.class, TaskSchedulingAutoConfiguration.class })
@ConditionalOnClass(ExecutorServiceMetrics.class)
@ConditionalOnBean({ Executor.class, MeterRegistry.class })
public class TaskExecutorMetricsAutoConfiguration {

@Autowired
public void bindTaskExecutorsToRegistry(Map<String, Executor> executors, MeterRegistry registry) {
executors.forEach((beanName, executor) -> {
if (executor instanceof ThreadPoolTaskExecutor) {
monitor(registry, safeGetThreadPoolExecutor((ThreadPoolTaskExecutor) executor), beanName);
}
else if (executor instanceof ThreadPoolTaskScheduler) {
monitor(registry, safeGetThreadPoolExecutor((ThreadPoolTaskScheduler) executor), beanName);
}
});
}

看源码,原理跟之前手动操作一样遍历executors,然后执行monitor方法,而monitor方法则是创建ExecutorServiceMetrics然后绑定到meterRegistry

ExecutorServiceMetrics

ExecutorServiceMetrics 是 Micrometer 中专门用于监控 Java 线程池(ExecutorService)的工具。它可以帮助你实时了解线程池的运行状态,比如活跃线程数、队列积压、任务执行耗时等,这些指标对排查性能问题和优化线程池配置很有价值。

核心的使用方法有两种:直接包装(推荐)构造器绑定

📊 暴露的监控指标

指标名称会根据你的线程池类型(ThreadPoolExecutorForkJoinPool)略有不同:

指标名称 类型 描述
executor.active Gauge 当前正在执行任务的活跃线程数
executor.queued Gauge 队列中等待执行的任务数量
executor.completed FunctionCounter 线程池已完成的任务总数
executor.pool.size Gauge 线程池中当前的线程总数
executor Timer 记录任务执行的耗时分布(P99、P95等)
executor.idle Timer 记录任务在队列中的等待耗时
executor.queue.remaining Gauge 队列剩余容量(ThreadPoolExecutor 特有)
executor.steals FunctionCounter 工作窃取总数(ForkJoinPool 特有)

🛠️ 使用步骤

1. 添加依赖

确保你的项目中引入了 Micrometer 的核心依赖。

1
2
3
4
5
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-core</artifactId>
<version>最新版本</version>
</dependency>

2. 方法一:直接包装(monitor 方法)

这是最常用的方式。monitor 方法会返回一个被 Micrometer 增强过的 ExecutorService 代理对象。你只需要像平常一样用它提交任务即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.binder.jvm.ExecutorServiceMetrics;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ExecutorMonitorDemo {

private ExecutorService monitoredExecutor;

public ExecutorMonitorDemo(MeterRegistry registry) {
// 1. 创建原始的线程池
ExecutorService originalExecutor = Executors.newFixedThreadPool(10);

// 2. 用 Monitor 方法进行包装
// 参数说明:MeterRegistry, 原始Executor, 线程池名称(用于标签), 可选的额外标签
this.monitoredExecutor = ExecutorServiceMetrics
.monitor(registry, originalExecutor, "my-service-pool");

// 3. 使用被包装过的线程池执行任务
monitoredExecutor.submit(() -> {
System.out.println("Task is being monitored!");
});
}
}

3. 方法二:构造器绑定(bindTo 方法)

这种方式只注册指标,不返回代理对象。适用于你不想改变现有线程池引用的情况。

1
2
3
4
5
6
7
8
9
10
11
12
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.binder.jvm.ExecutorServiceMetrics;
import io.micrometer.core.instrument.Tags;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;

// 假设你已有一个 threadPool 实例
ThreadPoolExecutor threadPool = (ThreadPoolExecutor) Executors.newFixedThreadPool(5);

// 创建 ExecutorServiceMetrics 实例,并绑定到 MeterRegistry
new ExecutorServiceMetrics(threadPool, "my-pool-name", Tags.empty())
.bindTo(registry);

对于Agent的价值,一直抱有怀疑态度,甚至认为未来会不会像大前端一样

别看现在火爆得不行,其实再走到下一个节点,就如前端一样,在裁员批次里面是第一梯队

因为它只是个连接器,一端是大脑LLM,另一端是承载业务的平台或专业软件,它自己本身对业务没有任何承载。

或者说它只是LLM的一个外延,也许未来会有其它的形式

之前想到的一个价值,就类似客服一样,可以帮助客户或者实施更快捷地接入产品,使用产品。

像一个垂直类专业软件,需要花费很多的资源去培训客户,但如果有了agnet,通过自然语言直接对话,都不需要去学习软件的使用,就能产出相应的结果

有点类似后端程序,可能都不清楚产品前端界面是什么样,开发一个个接口逻辑就行了。

但还不够具象。

在看到arthas也出品了agent,这感觉具象化了,对于arthas 我是实实在在的使用客户。

以前你需要去学习arthas的各种命令,会安装一下IDEA 的插件,直接能复制生成出相应完整命令行,再去执行。

但有了agent 那就更方便了,什么都不需要,把碰到的问题直接使用自然语言抛给agent,它能帮你分析,帮你执行命令。相当方便

本来打算学习一下arthas agent,结果agent也没对外开源,只是阿里内部使用。

不过对外开源了mcp server,自己搭agent

https://arthas.aliyun.com/doc/mcp-server.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
mcp:  
client:
enabled: true
type: sync
name: north-agent
version: 1.0.0
request-timeout: 15s
toolcallback:
enabled: true
streamable-http:
connections:
arthas-mcp:
url: http://localhost:8563
endpoint: /mcp

直接创建一个SKILL.md

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
---
name: arthas-diagnosis
description: 用于 Arthas MCP 诊断任务,包括 JVM 信息查看、线程分析、类加载器排查、方法跟踪、性能采样和文件结果读取。
---

# Arthas 诊断技能

这是面向运行时诊断与排障的专用技能。

## 什么时候使用

- 用户明确提到 `Arthas`、`JVM`、`线程`、`堆`、`类加载器`、`trace`、`watch`、`profiler`
- 用户要做运行时排障、性能分析、线程阻塞分析、类加载问题诊断
- 用户希望读取 Arthas 导出的诊断结果文件

## 核心原则

- 这是诊断域技能,不要和 Lot / CPE 业务操作混用
- 当前环境中的诊断能力来自 **arthas-mcp 已注册工具**,不要假设存在本地 Arthas CLI、`arthas-boot.jar`、JVM attach 权限或 shell 执行能力
- 只能调用当前 skill 已挂载的 MCP 工具;不要编造不存在的工具名,例如 `run_arthas_command`
- 不要读取或引用不存在的示例文件;若用户没有提供真实文件路径,不要假设如 `dashboard_sample_output.txt` 之类的本地样例文件存在
- 先做只读诊断,再做可能影响运行时状态的动作
- 如果用户目标不明确,先澄清要排查的是:线程、内存、类、方法调用、还是性能
- 优先使用风险更低、范围更小的工具

## 可直接使用的工具类型

本技能应直接使用 arthas-mcp 暴露出来的工具,例如:

- 概览:`dashboard`、`jvm`、`memory`、`thread`
- 类诊断:`sc`、`sm`、`classloader`、`jad`
- 方法诊断:`stack`、`trace`、`watch`、`monitor`、`tt`
- 性能分析:`profiler`
- 文件读取:`viewfile`

效果还可以,后面分析一下arthas相关源码,里面的实现很是精巧,有意思!

最近在给排盘软件增加一下神煞的功能

首先神煞命中分成两个太极点:年和日

  • 年取:以年柱为太极点去查
  • 日取:以日柱为太极点去查

然后基本流程是:
1、先确认神煞的查法和作用
2、把介绍、查法和作用都整理输出到文档
3、再根据文档里面的逻辑变成代码逻辑
4、再确认是否展示到UI
5、又增加一项,点击神煞时,会弹出神煞介绍

刚开始手动完成了 三丘五墓、桃花

后来想起这是一套标准动作,以后增加新的神煞时,都需要这么跟AI交互。

这不就是skill吗? 所以就让codex直接创建一个扩展神煞的skill

按这个套路,创建一个 skill,方便后面增加其他的神煞

完成之后,会创建相应的SKILL.md

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
---
name: extend-mingli-shensha
description: Add or update shensha logic in the mingli repo, including rule documentation, utility functions, explanation/method text, and UI display. Use when Codex needs to add a new 神煞, adjust an existing 神煞’s取法 or作用, wire it into src/utils/shensha.ts, update docs/shensha-*.md and docs/shensha-overview.md, or connect it to the App.tsx/App.css display and popup flow.
---

# Extend Mingli Shensha

Implement new shensha in this repo with the same pattern already used for 驿马、桃花、羊刃、禄、天乙贵人、三丘五墓.

## Quick workflow

1. Read `docs/shensha-overview.md` first.
2. If the target shensha already has a dedicated doc, read `docs/shensha-*.md`; otherwise create one before coding.
3. Implement or update logic in `src/utils/shensha.ts`.
4. If the shensha should appear in the UI, wire it into `src/App.tsx` and `src/App.css`.
5. If the shensha needs click-to-explain support, add explanation and method text in `src/utils/shensha.ts`.
6. Run `npm run build` before finishing.

这个skill就叫 extend-mingli-shensha

用这个skill,试一下增加文昌神煞

extend-mingli-shensha to add 文昌的规则、文档和展示

中途不再需要跟AI不停地交互,完整按标准动作完成了整个神煞的整理和开发工作

最后展示的效果也还不错

最近在项目中引入skill

把之前需要在系统提示词中注明的规则,全面挪移到相应skill的 SKILL.md 说明文件中

不过AI迭代太快,Spring Ai Alibaba 文档都来不及更新,里面的bug也不少。

在本地功能测试没有问题,发布到服务器,发现Skill not found: Skill not found: lot-query

1
2
3
4
5
6
7
8

INFO 1 --- [oundedElastic-1] c.a.c.ai.graph.agent.node.AgentLlmNode : [ThreadId 07f735f0-fd80-49b8-b4e5-7e90cfc2eb35] Agent ??? reasoning round 0 streaming output: [ToolCall[id=call_03da1a646a1a410bb82cf6, type=function, name=read_skill, arguments={"skill_name": "lot-query"}]]
2026-03-17T03:01:57.046330027Z 2026-03-17T11:01:57.046+08:00 INFO 1 --- [oundedElastic-1] c.a.c.ai.graph.agent.node.AgentToolNode : [ThreadId 07f735f0-fd80-49b8-b4e5-7e90cfc2eb35] Agent ??? acting with 1 tools.
2026-03-17T03:01:57.048982723Z 2026-03-17T11:01:57.048+08:00 INFO 1 --- [oundedElastic-1] c.a.c.ai.graph.agent.node.AgentToolNode : [ThreadId 07f735f0-fd80-49b8-b4e5-7e90cfc2eb35] Agent ??? acting, executing tool read_skill.
2026-03-17T03:01:57.050245886Z 2026-03-17T11:01:57.050+08:00 WARN 1 --- [oundedElastic-1] c.a.c.a.g.a.hook.skills.ReadSkillTool : **Skill not found: Skill not found: lot-query**
2026-03-17T03:01:57.051826871Z 2026-03-17T11:01:57.051+08:00 INFO 1 --- [oundedElastic-1] c.a.c.ai.graph.agent.node.AgentToolNode : [ThreadId 07f735f0-fd80-49b8-b4e5-7e90cfc2eb35] Agent ??? acting, tool read_skill finished
2026-03-17T03:01:57.053390410Z 2026-03-17T11:01:57.052+08:00 INFO 1 --- [oundedElastic-1] c.a.c.ai.graph.agent.node.AgentToolNode : [ThreadId 07f735f0-fd80-49b8-b4e5-7e90cfc2eb35] Agent ??? acting returned: ToolResponseMessage{responses=[ToolResponse[id=call_03da1a646a1a410bb82cf6, name=read_skill, responseData="Error: Skill not found: lot-query"]], messageType=TOOL, metadata={messageType=TOOL}}

原来是框架里面的ClasspathSkillRegistry 在Spring Boot fat jar 场景中有bug,会加载不到SKILL.md 文件

  1. 打包后的 jar 中已经确认包含 skill 文件,例如:
    • BOOT-INF/classes/skills/cpe-operation/SKILL.md
    • BOOT-INF/classes/skills/lot-operation/SKILL.md
    • BOOT-INF/classes/skills/lot-query/SKILL.md
    • BOOT-INF/classes/skills/true-north-router/SKILL.md
  2. 但应用启动日志显示:
    • TrueNorth skill registry loaded 0 skills.
    • TrueNorth skill registry is empty at startup.
  3. 随后模型调用 read_skill("lot-query") 时,得到:
    • Skill not found: lot-query

使用AI分析了一下源码,现在有AI 学习效率提升不少,以前还要手动翻源码,画流程图,清楚调用关系。

现在先让AI分析一下源码,输出各种结构图,快速梳理出框架主干

再针对性提问题,让他提出源码位置,细节一目了然

把AI分析结果提交个issue: https://github.com/alibaba/spring-ai-alibaba/issues/4426

再让AI重新实现一个SkillRegistry,解决这个问题

项目中有一个参数cpeModel ,是固定的枚举项,希望在大模型询问参数时,给出固定的选项,在
Tool 参数上,描述一下:

1
@ToolParam(description = "必须让用户确认选择cpeModel,可选值只能是CPE2;CPE6;CPE6CF9;CPE15;CPE15CF4;CPE19其中一个") @NotNull String cpeModel

这样就达到了效果,从没有出现幻觉现象。

最近在项目中引入skill,系统提示词大幅减小,却出现了幻觉

模型输出类似:

  • CD65_CPE_Standard
  • CD65_CPE_Advanced
  • CD65_CPE_Quality
  • CPEv2
  • CPEv3
  • CPE-AI

从当前项目代码和 skill 契约来看,这些都不是正式允许值,因此属于幻觉式扩展

这就奇怪,现在的Tool定义上也有,而且在SKILL.md文件中也特意说明

1
2
3
4
5
6
7
8
9
10
11
12
如果用户尚未提供 `cpeModel`,你必须只展示正式允许值列表,并要求用户从中选择。
禁止补充任何额外示例、行业常见名、产品名、历史名、猜测值,禁止使用“常见模型包括”“例如”“或者其他模型名称”这类会诱导扩展的表达。

固定回答格式:

请从以下正式 cpeModel 中选择一个:
- CPE2
- CPE6
- CPE6CF9
- CPE15
- CPE15CF4
- CPE19

虽然这样写了,但有时还是会幻觉,特地还写了一个tool,强制输出枚举项

在SKILL.md里面写上当需要枚举参数确认时,调用这个tool来确认

可过两天测试又正常了,不再需要这个参数确认的tool

最近又有一个新词叫:Harness Engineering

Prompt Engineering管的是「说什么」,Context Engineering管的是「知道什么」,Harness Engineering管的是「在什么环境里做事」

技术上来说,Harness 是包裹在 AI Agent 外面的那层基础设施,负责管理:

  • 工具调用(Tool Use)

  • 上下文记忆(Context / Memory)

  • 错误重试(Retry)

  • 人工审批节点(Human-in-the-loop)

  • 架构约束和代码规范检查

主要就是来应对模型的幻觉和执行的漂移

模型的幻觉
指模型生成内容时,编造出看似合理但事实上错误、无依据的信息。

  • 根源:大模型本质是概率生成器,它在“续写”时没有内置的事实核查机制;训练数据中的偏差、知识截止后的新事实、过度自信的生成都会导致幻觉。

  • 表现:在事实性问答、代码生成、逻辑推理中常见,尤其在模型不确定时反而会“自信地胡说”。

执行的漂移
指在复杂任务(尤其是多步、长周期、与环境交互的任务)中,模型逐渐偏离原始目标、违反约束或进入无效状态。

  • 根源:自回归误差累积、缺乏全局规划、对中间反馈的误判。

  • 表现:例如Agent在调用API时循环调用、智能体在完成多步任务时忘记最初指令、模型输出逐渐超出安全边界等。

最近笔记转换到了Obsidian,所以想开发了一个Obsidian插件

震求很简单,把Obsidian里面的笔记一键发布到公众号,在这个里面有个小的需求,需要把mermaid图转换成图片自动上传微信

类似hexo中的hexo-filter-mermaid-diagrams插件

hexo-filter-mermaid-diagrams 是一个用于 Hexo 的 Mermaid 插件。

它的核心思路不是“在构建时把 Mermaid 转成最终图片”,而是:

在页面中注入 Mermaid 脚本,等博客页面加载后,由浏览器再去渲染 Mermaid 图表。

但公众号里面不能这处理

微信公众号复制场景有个关键限制:

  • 粘贴到公众号编辑器后,不能依赖额外脚本继续执行 Mermaid

比如一个流程图:

Mermaid 自定义样式渲染修复

问题:Mermaid 图表中使用 style 指令自定义节点颜色(如 style B fill:#e1f5fe)时,导出到 PNG 后节点显示为黑色背景,丢失自定义填充色。

根本原因

  • Mermaid 将自定义样式写入 SVG 元素的 inline style 属性
  • Canvg 在光栅化时无法正确处理 inline style 的 CSS 优先级
  • <style> 块中的默认主题规则(如 .node rect { fill: #ECECFF; })覆盖了自定义样式

解决方案

  1. 保留 <style> 块(默认节点依赖它提供填充色和边框)
  2. 提取 inline style 中的 SVG 属性(fill、stroke 等)
  3. 为包含自定义样式的元素添加唯一 CSS 类
  4. 将提取的属性以 !important 规则追加到 <style> 块末尾
  5. 同时设置 SVG presentation attributes 作为兜底

这样既保证默认节点的样式完整,又通过 CSS 优先级确保自定义样式正确渲染。

使用 Claude Opus 4.6 模型

使用gpt-5.3-codex 和 gpt-5.4 模型

这么一个需求,模型知错、认错的功夫杠杠的,但就是改不了错

模型切了好几个,token花费不少,可最终问题还是没有完美解决