偷懒是人类发展的动力,写一个MCP+Skill来代替日志检查

前言

作为一个后端开发,每天上班第一件事情就是打开日志平台,看有没有错误发生。
但这项重要但又枯燥的活,在LLM出现之前是无法交给实习生/初级开发的,因为大概率需要中/高级开发来分析根本原因,这个过程又耗时又费劲,还无法体现工作量。
所以在LLM出现之后,这个工作完全可以"偷懒",来提高工作效率。

公司在live环境使用sumologc作为监控观察平台,非常不巧的是,官方并没有提供mcp来接入Claude code。不过好在sumogloic提供了完整的restful API , 因此只要写一个本地运行的MCP,再组合Skill就可以替代我这部分工作

什么是MCP

MCP(Model Context Protocol 模型上下文协议),是Anthorpic发起的一个开放协议,主要是为了解决LLM应用(Claude,Cursor)如何通过统一的方式访问外部工具,数据与提示模板,而不用为每个LLM应用单独写一套集成。

为什么需要MCP

假如你有3个LLM应用,Claude,自建的agent,某个IDE插件,你想让他们都能访问数据库。
在MCP出现之前,你要为每一个LLM应用单独写一套集成代码

  • Claude Desktop 要连 PostgreSQL → 写一套读 schema、执行查询、把结果转成模型能理解的文本的逻辑
  • 自建Agen要连 PostgreSQL → 上面 PostgreSQL 那套逻辑在你的 Agent 里重写一遍(因为两边的应用架构不同,代码不能直接复用)
  • 某个IDE插件又要重复上面的操作

以此类推,当你所有的LLM应用要同时对接Jira,Git。。。。你要重新对接的成本就是 M×N 一个爆炸的组合

MCP出现之后,它定义了工具怎么描述,参数怎么传,结果怎么返回,于是乎:
每个数据源只需要写一个MCP Server,按照协议暴露它的查询能力,每个LLM也只要实现一个MCP Client ,遵循同一个协议去连接任意MCP Server

工作量从 M×N 骤降。

MCP架构

MCP架构分为三层,底层用JSON-PRC进行通信

  • Host Claude,Cursor
  • Client 协议层链接
  • Server 你写的程序

MCP-Server能做什么

Server能暴露3种东西给LLM应用

  • Tools 工具
    LLM可以调用的函数,比如“查询天气”,“执行SQL”
  • Resources 资源
    模型可以读取的数据,比如文件内容,数据库表结构
  • Prompts 提醒模板
    预先定义好,参数化的提示词模板

传输方式 Transport

两种

  • stdio
    本地进程
  • StreamableHTTP
    远程部署,Web服务常用,取代了早期的SSE

做一个简单的MCP

需引入ModelContextProtocol包

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using ModelContextProtocol.Server;
using System.ComponentModel;

var builder = Host.CreateApplicationBuilder(args);


builder.Services
    .AddMcpServer()               // 注册 MCP Server 相关服务到依赖注入容器
    .WithToolsFromAssembly();     // 自动扫描当前程序集里带 [McpServerToolType] 的类,注册其中的工具

await builder.Build().RunAsync();

//标记这个类里的方法可以被注册为 MCP 工具
[McpServerToolType]
public static class EchoTool
{
    // [McpServerTool] 标记这个方法本身是一个工具,模型可以看到并调用它
    // Description 会被放进工具的 schema 里,直接影响模型判断"要不要调用这个工具"
    [McpServerTool, Description("Echoes the message back, optionally shouting it.")]
    public static string Echo(
        // 参数上的 Description 同样会进 schema,帮助模型正确填参数
        [Description("The text to echo back.")] string message,
        [Description("If true, returns the message in upper case.")] bool shout = false)
    {
        // 参数校验:MCP 工具本质是给模型调用的"外部函数",模型传来的参数不可尽信,
        // 需要像处理外部输入一样做校验
        if (string.IsNullOrWhiteSpace(message))
        {
            throw new ArgumentException("message must not be empty.", nameof(message));
        }

        return shout ? message.ToUpperInvariant() : message;
    }

    [McpServerTool,Description("调用词方法会返回原样")]
    public static string Echo2([Description("需要回传的文本")] string message, [Description("如果为ture,会多余返回一段文字")] bool add = false)
    {
        if (add)
        {
            return message + "哇哇哇哇哇哇";
        }
        return message;
    }

    [McpServerToolType]
    public static class MathTool
    {
        // 一个更简单的工具示例:两个整数相加
        [McpServerTool, Description("Adds two integers and returns the sum.")]
        public static int Add(
            [Description("First addend.")] int a,
            [Description("Second addend.")] int b) => a + b;
    }
}

MCP Inspector

再使用官方测试工具,启动并调用这些Tools
mcp-inspector "C:\Users\simpletruss\source\repos\mcp-dotnet-tutorial\Stage1.HelloMcpServer\bin\Debug\net10.0\Stage1.HelloMcpServer.exe"
image

理解为我们代替agent来调用Tools,在实际中,会由agent来调用,这里简化了操作,只验证MCP是否正常运行以及交互。

可以看到,所谓的Tools与调用function并无本质差距,只是换了一种形式而已

Resources + Prompts

作为一个MCP,仅仅有Tools是不够的。

  1. Tools
    Tool是模型主动决定要不要调用的函数
  2. Resources
    是客户端被动拉取的数据,更接近“文件”的概念,而不是“函数”
  3. Prompts
    是预先写好,参数化的提示词模板。

核心区别:谁是"驱动者"

谁决定调用 怎么进到对话里 类比
Tools 模型自己 host 把工具列表转成 LLM API 的 tools 参数,模型在推理过程中自主决定"我要调用 xxx",结果被塞回对话历史 助理随手能用的工具
Resources 人(或 host 应用) host 在 UI 上给用户一个"附加资源"的入口(比如回形针图标),用户手动选中某个资源,host 读取内容后当成普通文本/文件贴进对话 你自己去翻文件柜,挑一份文件贴进聊天
Prompts host 通常做成"斜杠命令"或提示词模板选择器,用户手动选一个、填几个参数,host 把生成的消息插入对话开头 你自己去选一个写好的邮件模板,填几个空

实际效果

纸上得来终觉浅绝知此事要躬行,理论部分结束,现在开始看看实际效果

image

因为代码涉及到公司隐私,就不方便公开了,让AI重写一个也是分分钟的事情,只是提供一种思路,偷懒是人类发展的动力,核心是mcp提供让AI访问的能力,skill告诉AI应该怎么做。

分享自用的skill

点击查看代码
---
name: sumologic-daily-triage
description: Runs the user's daily Sumo Logic error-log triage across their services (fill in your own service names in "巡检范围" below) using the sumologic MCP server's search_logs tool — pulls ERROR-level logs from the last 24 hours for all monitored services in one combined run, groups them by service and error type with counts, and drills into ambiguous errors by TraceId to reconstruct request context. Trigger whenever the user asks to check Sumo Logic / Sumologic, check a specific service for errors, do their daily/morning log check, triage overnight errors, or says things like "看看日志有没有异常", "查一下 <service> 的日志", "今天的 Sumologic 巡检", "有没有报错" — even if they don't name this skill explicitly.
---

# Sumologic 每日日志巡检

## 这个 Skill 做什么

每天巡检下面"巡检范围"里列出的所有服务,过去 24 小时的 ERROR 级别日志:合并成一次查询,按服务、再按错误类型分组统计,对照本地积累的历史记录标出"新增"(有记录以来第一次出现)的错误类型,对信息不足、无法判断根因的错误按 TraceId 深挖调用链上下文,最后产出一份简洁的巡检报告。这是把用户原本每天手动登录 Sumo Logic 平台查看的动作,搬到这里自动跑一遍。

## 巡检范围

**用之前先改这里**——把下面换成你自己要巡检的服务名(跟 Sumo Logic 里 `service` 字段的实际取值对应):
- `<your-service-1>`
- `<your-service-2>`

(如果只巡检一个服务,删掉多余的一行;如果查询字段里 `service` 不是这个名字、或者你们用别的字段区分服务,第一步的 query 也要跟着改)

新加服务进来时,在这里加一行,其他步骤不用改——前提是新服务用的是同一套查询字段。

## 前置条件

需要 `sumologic` MCP server 已经注册在当前会话里,提供 `search_logs` 工具(query, from, to, timeZone, maxMessages)。这个 server 的项目在 `SumologicMcpServer/`(与本 skill 同一个仓库),注册方式见该项目的 README。

**如果发现 `search_logs` 工具不可用**:直接告诉用户需要先注册这个 MCP server(给出 README 里 `claude mcp add` 的命令),不要假装查询过、不要编造巡检结果。

## 流程

### 第一步:一次性拉取所有服务过去 24 小时的错误日志

把"巡检范围"里的服务用 `OR` 合并成一次 `search_logs` 调用,而不是每个服务单独查一次——省查询次数,而且合并查询里每条日志本身还是带着 `service` 字段的,不会丢失服务归属信息:

- `query`: `(service="<your-service-1>" OR service="<your-service-2>") _loglevel=ERROR`("巡检范围"加了新服务,这里的 `OR` 列表也要跟着加;`_loglevel`/`service` 这两个字段名要换成你自己 Sumo Logic 环境里实际的字段名,不一定叫这个)
- `from`: 24 小时前(不传则工具默认就是这个,可以直接不传)
- `to`: 现在(同上,可以不传)
- `timeZone`: 用户没特别说明就用 `UTC`;如果用户提到自己所在时区,用那个
- `maxMessages`: 先用默认值查一次。如果返回的 `totalMessageCount` 明显大于 `returnedMessages`(说明有截断),如实告诉用户实际总数是多少,问要不要调大 `maxMessages` 重新查,**不要悄悄按默认值截断却不提**。多服务合并查询更容易触发截断,这一步更要留意。

如果用户明确只想查其中一个服务(比如"查一下 <your-service-2> 的日志"),就把 query 里的 `OR` 去掉,只查那一个,不用把所有服务都拉一遍。

### 第二步:读取本地"已知错误类型"历史记录

路径:`.claude/skills/sumologic-daily-triage/known-errors.json`(相对项目根目录,跟这个 skill 文件同一个文件夹)。这是本机本地积累的历史,不在 git 仓库里,每台机器/每个人的记录是独立的。

用 Read 工具读它。**文件不存在很正常**——说明是这台机器第一次跑这个 skill,当成空的 `{}` 处理就行,不用报错、不用告诉用户"文件缺失"。

文件结构:
```json
{
  "<service名>": {
    "<错误signature>": { "firstSeen": "<ISO时间>", "lastSeen": "<ISO时间>", "occurrences": <累计次数> }
  }
}
```

### 第三步:按服务、按错误类型分组,标出新增的,并把结果写回历史记录

不要把原始 JSON 甩给用户看。先按 `service` 字段分组,每个服务内部再按错误类型分组统计。**分组用的 signature 要按固定规则提取,不要每次都自由发挥**——不然同一个错误今天叫法和明天叫法不一致,新增识别就废了:

- signature = 异常类型名(如果日志里能看出明确的异常类名)+ 精简后的消息模板
- 把消息里每次都会变的具体值去掉,只留下"能代表这是哪一类错误"的部分——比如订单号、GUID、具体数字、时间戳这些要剥离;"同一种异常、不同订单触发的"应该归成同一个 signature,不要因为订单号不同就分成两条

每组统计:
- 错误摘要(异常类型/关键信息)
- 出现次数
- 首次出现时间、末次出现时间(这次巡检窗口内的,不是历史上的)
- 是否属于"新增"——这个 signature 不在第二步读到的、该 service 对应的历史记录里,就标 🆕

每个服务内部按出现次数从高到低排序展示,但**新增的错误优先往上放**,即使次数不多——新出现的东西通常比"老毛病又犯了一次"更值得先看。

**分组统计完、报告写完之后**,把这次看到的 signature 写回 `known-errors.json`(用 Edit 或 Write 工具):
- 这次是新增的(历史里没有)→ 新加一条,`firstSeen`/`lastSeen` 都设成这次巡检的时间,`occurrences` 设成这次的出现次数
- 历史里已经有的 → `lastSeen` 更新成这次巡检的时间,`occurrences` 累加这次的出现次数

**如果这次巡检是这台机器第一次跑(第二步读到的文件不存在/是空的)**,今天所有错误都会显示 🆕——这是符合预期的正常现象,报告里提一句"本机首次运行,历史记录为空,以下错误均为本机首次记录",不要让用户误以为突然爆发了一堆新问题。

### 第四步:对信息不足的错误,按 TraceId 深挖上下文

单条错误日志经常信息不够(比如只有一行异常类名,看不出触发链路)。这种情况下:

1. 从这条日志的字段里取出 `TraceId`(如果你的日志schema里不叫这个名字,换成你自己的关联ID字段)
2. 再调用一次 `search_logs`:
   - `query`: `TraceId="<该值>"`(**不加** `_loglevel` 过滤,要看这次请求完整的日志,不只是错误那一条)
   - `from`/`to`:缩小到这条错误发生时间前后即可,不必仍用 24 小时整个窗口
3. 把追查到的日志链路,总结成"这次请求发生了什么"的简短描述

**不是每条错误都要深挖**——只对看起来信息不足、有代表性、或出现次数较多的错误做这一步,逐条深挖会导致查询量爆炸、体验很慢。**新增的错误优先深挖**,哪怕次数还不多,因为它是新出现的,最需要知道"到底发生了什么";老错误如果已经深挖过、原因明确,不用每天重复挖。

### 第五步:输出报告

大致按这个结构组织(不是必须逐字照抄,但要包含这些信息)——每个有错误的服务一个小节,没错误的服务在开头一句话带过就行,不用给空服务单独铺开一整节:

```
# 每日日志巡检 — <日期>
时间范围: <from> ~ <to>
巡检范围: <your-service-1>, <your-service-2>
错误总数: <N>(按服务: <your-service-1> N1, <your-service-2> N2)
新增错误类型: <M> 个(本机历史记录里首次出现,见下方标 🆕 的条目)

## <your-service-1>
### 按类型汇总
1. 🆕 <错误类型/消息摘要> — N 次(首次 HH:mm,末次 HH:mm)
2. <错误类型/消息摘要> — N 次(首次 HH:mm,末次 HH:mm)
3. ...

### 深挖详情
#### <某条被深挖的错误>
TraceId: <值>
追查结果: <简述这次请求发生了什么,根因是什么>

## <your-service-2>
(同样的结构;如果这个服务没有错误,直接写"过去24小时无 ERROR 日志",不用展开)

## 结论
<每个服务分别说明看起来正常还是需要关注,以及为什么这么判断;新增错误类型单独提一句,不要被淹没在整体结论里>
```

结论部分是判断性的,不是套一个写死的阈值——目前没有约定"每天多少条 ERROR 才算异常",所以按错误的性质、数量变化、是否是新出现的错误类型来综合判断,并把判断依据写清楚,而不是简单甩一个数字让用户自己猜。多服务合并巡检时,各服务的结论要分开给,不要因为其中一个服务有问题就笼统说"需要关注"糊弄过去。

如果过去 24 小时所有服务都没有 ERROR 日志,直接说"过去24小时所有服务无 ERROR 日志,一切正常",不需要走后面的分组/深挖步骤。

如果 `search_logs` 返回了 `error` 字段(说明查询本身失败,比如认证失败、查询语法错误),把这个错误原样告诉用户并停下来,不要假装查询成功、不要编造日志内容。

## 重要说明——这套规则是按具体使用者的实际习惯写的,不是通用规则

- `_loglevel`、`TraceId`、`service` 这几个字段名,以及"巡检范围"里列出的服务,都要换成**你自己** Sumo Logic 环境里已验证过的用法,不是 Sumo Logic 的通用标准字段名。如果你巡检的多个服务用的字段命名不一样,需要分开处理,不要想当然套用同一个 query 模板。
- 目前没有定义"这个错误可以忽略"的噪音名单,所以第三步汇总时要把所有 ERROR 级别结果都列出来,不要自己臆断哪些不重要就悄悄过滤掉。
- 目前没有定义"多少条算异常"的量化阈值,第五步的结论靠判断,不是靠数字比大小。
- 如果以后查询字段变了、加了新的 service 要巡检、或者有了明确的忽略名单/阈值,应该回来更新这个 skill 文件("巡检范围"那节加一行就行),而不是让 Claude 每次都靠猜或者沿用过时的规则。
- "新增错误"现在是对照本机 `known-errors.json` 这份本地历史记录判断的,是真正意义上的"这台机器有记录以来第一次出现",不是简单跟前一天比。但这份历史是**本机本地的**——换电脑、或者别人第一次在自己机器上跑,历史都是从零开始,不会共享你这边积累的记录,这是设计上刻意的(`known-errors.json` 不提交进 git)。
- signature 的提取规则依赖 Claude 每次判断的一致性,不是代码层面强制保证的确定性 key——如果发现同一个错误反复被误判成"新增"(说明 signature 提取不稳定),回来看第三步的 signature 规则要不要写得更具体、更机械化,减少每次判断的随意发挥空间。

有一点需要注意,skill的精髓是按需加载,渐进式披露,不要试图用一个skill来解决所有问题。
所以我通常是把排错的个人经验与心得根据业务模块来定义不同的skill,来提高AI的效率(省点Token)。

posted @ 2026-09-24 14:33  叫我安不理  阅读(100)  评论(0)    收藏  举报