Conversation
rootdeng
commented
Aug 4, 2026
- 容器与底层维度(LlmStatisticsDecorator 及 LlmStatisticsScheduler):可观测每一发底层的首 Token 耗时、非流式调用时耗及完成时局,从 LLM 客户端层面上排查接口延迟。
- 用户请求维度(ChatController):可观测用户在会话气泡中发送消息后到全响应落盘的整体历时。通过比较两者,您能够在发生响应卡顿时,非常快速且准确地辨别是底层大模型接口发生阻塞还是由 MateClaw 本地业务逻辑层(数据库访问、上下文整合或内存调度等) 引起的耗时。
Specifies the architecture, data models, decorator, and scheduler timing metrics. Co-Authored-By: Claude <noreply@anthropic.com>
… capacity limits Co-Authored-By: Claude <noreply@anthropic.com>
…ncy tracking Co-Authored-By: Claude <noreply@anthropic.com>
This aggregates min, max, average, median, p90, and p99 metrics. Reverted JVM-compile lombok overrides in pom.xml to preserve production JDK 21 safety. Co-Authored-By: Claude <noreply@anthropic.com>
This completes decorator wrapping for all chat models built by the factory. Co-Authored-By: Claude <noreply@anthropic.com>
Measures overall request-level latency for both synchronous and SSE streaming dialogues. Co-Authored-By: Claude <noreply@anthropic.com>
mateaix
left a comment
There was a problem hiding this comment.
感谢 PR!方向是有价值的 —— 能区分「大模型接口慢」和「本地业务逻辑慢」确实是目前缺的一块观测能力。
我把分支拉下来实测了一遍,但目前还不能合,下面按严重程度列一下。
🔴 阻塞项
1. 编译不通过
[ERROR] ChatController.java:[728,78] cannot find symbol
symbol: variable streamStart
location: class vip.mate.channel.web.ChatController
long streamStart 加在了 ChatController.java:250,那里是 approval 恢复流 的 sseExecutor.execute(...) lambda 内部;而使用它的日志加在 728 行,属于 主聊天流 的 lambda(起始于 570 行)。两个 lambda 作用域不互通。
实际效果是:真正想统计的主流程没有起始时间戳,approval 分支多了个没人用的局部变量。麻烦把 streamStart 挪到 570 行那个 lambda 的开头,并在本地跑一次 mvn -q compile 确认。
2. 会破坏 SummarizingNode 的 Anthropic 分支
ProviderChatModelFactory.buildFor() 现在无条件把所有 ChatModel 包一层装饰器。但 SummarizingNode.java:206 依赖具体类型判断:
if (chatModel instanceof AnthropicChatModel) { // 包装之后永远是 false
opts = AnthropicChatOptions.builder()
.thinking(ThinkingType.DISABLED, 0).build();
} else {
opts = OpenAiChatOptions.builder().build(); // 拿 OpenAI options 去打 Anthropic
}调用链是 AgentGraphBuilder:569 → buildRuntimeChatModel() → buildFor(),产物在 AgentGraphBuilder:985 传给 SummarizingNode,所以这条分支必然被打断。该方法上方的注释写得很明确:不显式关掉 thinking 的话会继承 thinkingLevel=high,「浪费 100+ 秒」—— 这个 PR 会把它重新引入。
解决方式二选一:
- 装饰器提供
unwrap()/ 实现某个可穿透的接口,并把SummarizingNode的判断改成穿透后再instanceof; - 或者干脆不用装饰器(见下面「关于实现路径」)。
3. 被取消的流一条都不会被记录
装饰器只挂了 doOnComplete 和 doOnError,这两个回调在 cancel 时都不触发。而 NodeStreamingChatHelper 有四处会主动 subscription.dispose():
- 用户点击 stop
- thinking-only 软上限触发
- 内容重复检测触发
- 10 分钟总超时
也就是说,「最该被观测的慢请求 / 卡死请求」恰好全部不会进入统计,p99 会被系统性低估 —— 这跟这个功能本身的目的是矛盾的。建议改用 doFinally,并在 metric 里带上终止原因(complete / error / cancel)。
🟡 数据质量与性能
4. 成功和失败混在一起算分位数
LlmRequestMetric 没有 success 字段,call() 的 catch 分支和成功分支记录的是同一种指标。一次 20ms 就返回的 401 会和正常调用一起进 p50/p99,把延迟数据拉偏。建议加字段并在报表里分开统计。
5. ConcurrentLinkedQueue.size() 是 O(n)
if (requestMetrics.size() >= MAX_CAPACITY) { ... }ConcurrentLinkedQueue.size() 需要遍历整个队列。这段在每次 LLM 调用时都会执行,队列满时就是每次遍历 10000 个节点。另外 check-then-poll 不是原子操作,并发下队列会超过上限。
建议配 AtomicInteger 计数,或者换成带锁的 ArrayDeque。
6. 队列满之后每次调用都打一条 WARN
实测输出:
WARN LlmStatisticsCollector -- [LlmStatisticsCollector] Capacity limit (10000) reached. Evicted oldest metric from: ...
WARN LlmStatisticsCollector -- [LlmStatisticsCollector] Capacity limit (10000) reached. Evicted oldest metric from: ...
(每插入一条就一行)
日调用量超过 1 万次的实例,日志会被这条 WARN 淹没。淘汰是设计内的正常行为,不该用 WARN;要提示的话,降到 DEBUG 或者做成「首次触发只打一次」。
7. 跨天重置有重复逻辑和竞态
reportStatistics() 的 cron 0 */15 * * * ? 在 00:00:00 也会触发,和 midnightReset() 的 0 0 0 * * ? 撞在同一秒,执行顺序不确定。如果 midnightReset() 先跑,collector.reset() 会先清空缓冲区,随后 reportStatistics() 里当天的最终报表就变成空的,静默丢失。
这两套机制是重复的,留一套即可。
🟡 工程约定
8. 缺少开关和配置项
@Scheduled 是硬编码的,没有任何 mate.* 配置项可以关闭或调整间隔。本项目的约定是「可调项放 application.yml,Java 字段默认值只作兜底」。
另外空闲实例每天会往日志里写 96 行 === LLM Performance stats: No requests recorded today yet ===,对桌面版用户是纯噪音。建议:
mate:
llm:
statistics:
enabled: false # 默认关闭
report-interval: 15m
max-buffer-size: 10000并在无数据时直接跳过输出。
9. 内联全限定类名
LlmStatisticsScheduler 里的 java.util.Objects::nonNull 需要改成顶部 import java.util.Objects; + Objects::nonNull。项目规范要求所有类型引用用简单名 + import,不写内联 FQN。
10. 新增类没有任何注释
本仓库的类基本都有 Javadoc 说明「为什么这么做」。新增的四个类目前一行注释都没有,尤其是装饰器和调度器这类会影响全局链路的组件,建议补上设计意图。
11. 文档文件
docs/superpowers/specs/2026-08-04-llm-statistics-design.md 把四个类的完整源码又抄了一遍(356 行)。代码一改文档就腐烂,而且 docs/ 目录是面向用户的文档站,不放内部设计稿。建议直接删掉,把设计意图浓缩进类的 Javadoc。
关于实现路径
有个更根本的问题想和你确认一下:这套东西和现有能力有相当大的重叠。
AnthropicChatModelBuilder/OpenAiCompatibleChatModelBuilder/ClaudeCodeChatModelBuilder都已经注入了 Micrometer 的ObservationRegistry,Spring AI 原生就会发gen_ai.client.operation观测事件,里面自带耗时。ChatStreamTracker和NodeStreamingChatHelper里已经有firstTokenReceived的追踪逻辑(目前用于心跳间隔切换)。
所以更省事、也更不容易碰坏现有链路的做法可能是写一个 ChatModelObservationHandler,接到已有的 ObservationRegistry 上 —— 这样天然规避掉上面的第 2 点(不用包装 ChatModel)和第 3 点(observation 的生命周期覆盖 cancel),指标也能顺带对接 actuator,而不是只能翻日志。
如果有非用装饰器不可的理由(比如需要拿到 Spring AI observation 里没有的字段),也说一下,我们可以在这个方案上继续推进。
测试
把编译错误临时注释掉之后,5 个测试是全绿的。不过覆盖面还比较浅:
LlmStatisticsSchedulerTest只断言了「跑完不抛异常」,getPercentile()的边界(空集合、单元素、n=2 时 p50 取下界)都没验证;- 跨天重置、并发写入、cancel 场景都没有用例。
上面第 3、4、7 点都属于「有测试就能提前发现」的类型,建议补一下。
再次感谢,这个方向值得做,主要是实现细节需要再打磨一轮。上面第 1~3 点是必须解决的,其余可以按优先级逐步来。