# AI / Skill / MCP 排障

按现象排查 AI 功能：助手不回答或半路中断、Skill 没被用上或装不上、MCP 客户端连不上——每条都能从产品日志核对。

最后校对于 2026-10-10

先看现象，再给做法。这页上的每一条都能从产品自己的日志和报错里核对—— 不用去猜模型当时在想什么。

## 哪一半坏了[​](#哪一半坏了 "哪一半坏了的直接链接")

会坏的是两件互不相干的事，坏法也不一样：

* **模型那一半**——助手、`/a2a`、各处的 AI 按钮。需要 LLM 配置。 现象是报错卡片、回答慢或者回答空；
* **数据那一半**——`/mcp`，以及每个回答背后的那些工具。完全不需要模型。 现象是 HTTP 状态码。

一个快速的分界动作：问助手一句\*「你能做什么？」\*。答得上来，说明模型那一半没问题， 毛病在工具；弹报错卡片，就从模型这边查起。

## 助手不回答[​](#助手不回答 "助手不回答的直接链接")

| 你看到的                         | 原因                                                                 |
| -------------------------------- | -------------------------------------------------------------------- |
| 卡片：**当前环境没有大模型配置** | 没有任何一条配置同时是「启用」**且**「默认」。两个开关，同一行       |
| 回答吐到一半停了，说请求失败     | 一般是模型自己超时。把那条配置高级设置里的\*\*超时时间（秒）\*\*调大 |
| 等很久什么都没有                 | 夜莺所在机器根本够不着那个地址——见下一段                             |

单独验证模型这条链路：编辑那条配置，点**测试连接**，它会拿表单里的值真发一次请求。 失败弹窗会说清是哪一类：

| 类型             | 该改什么                                                                             |
| ---------------- | ------------------------------------------------------------------------------------ |
| 鉴权             | Key 不对，或者首尾带了空格。Anthropic 的 Key 以 `sk-ant-` 开头，OpenAI 以 `sk-` 开头 |
| 端点不存在       | 地址。填到版本根这一级——`https://api.openai.com/v1`——不是对话路径，也不是光一个域名  |
| 被限流           | 提供商那边的额度。去他们控制台看用量                                                 |
| 回复里没有内容   | 通常是纯思考模型，预算全花在思考上了。把**最大 Tokens** 调大，或者换一个非思考模型   |
| 看着像网络问题的 | 夜莺所在机器到不了那个地址。在那台机器上验一下，需要就填**代理地址**                 |

逐字段的说明在[配置大模型提供方](https://n9e.github.io/zh/docs/ai/llm-provider.md)。

## 回答不对、很空，或者半路停了[​](#回答不对很空或者半路停了 "回答不对、很空，或者半路停了的直接链接")

**「达到最大迭代次数」。** 工具调用链撞到预算了，默认 25 次。 要么把问题问窄一点，要么在覆盖这类问题的那个 Skill 的 frontmatter 里调大 `max_iterations`。

**你看得到的数据它看不到。** 它跑在提问账号的权限上，所以这是权限问题不是 AI 问题： 去查那个账号的角色，以及它所在团队有哪些业务组。模型见 [权限继承与 RBAC](https://n9e.github.io/zh/docs/ai/permissions.md)。

**它编字段名、编指标名。** 把对象和时间窗点名，并且让它去查而不是去回忆： *「读一下 42 号规则的定义再告诉我」*。文档检索工具存在的意义就是让版本相关的名字 从文档里来——出网到 `flashcat.cloud` 被挡掉时，这个工具会降级，这个毛病会更明显。

**对话一长回答就被截断。** 把 LLM 配置里的**上下文长度**填成模型真实的窗口大小； 一次带多少历史是按它算的，留空就按一个保守的固定预算走。

## Skill 没被用上[​](#skill-没被用上 "Skill 没被用上的直接链接")

启用一个 Skill 不等于它会被加载。命中判断看的是**描述**，所以：

1. 先看开关——关掉的 Skill 挂着 `OFF` 标签，永远不被考虑；
2. 看可见范围。标成*仅管理团队可见*的 Skill，对团队之外的人是不存在的， 模型也不知道它存在；
3. **用用户会打出来的话重写描述**，把他们会粘贴进来的告警名和报错串都放进去。 绝大多数情况下就是这条；
4. 找一找近似重复。几个描述差不多的 Skill 会同时命中、同时吃上下文，合并掉。

写法和例子在[安装、管理与编写 Skills](https://n9e.github.io/zh/docs/ai/skills.md)。

## Skill 装不上[​](#skill-装不上 "Skill 装不上的直接链接")

| 报错                                             | 怎么办                                                                                       |
| ------------------------------------------------ | -------------------------------------------------------------------------------------------- |
| `SKILL.md not found in archive root`             | 你把上一级目录打包进去了。多包一层会自动拆，多包两层不会                                     |
| frontmatter 里 `name` 不能为空                   | 文件要以 `---` 开头、YAML 要能解析、`name` 要有值。三条都要满足                              |
| `archive size exceeds 10MB limit`                | 上传上限。删点附带文件，或者改用 Git 安装                                                    |
| `git_url must be an http or https URL`           | 不支持 ssh 地址，用 https 的 clone 地址                                                      |
| `git_token is required when git_auth_type=token` | 要么填 token，要么把认证方式换回不认证                                                       |
| 私有仓库装不上                                   | Token 是用 `[HTTP.RSA]` 的密钥加密存的，那对密钥要先配好。需要用户名的凭据写成 `用户名:令牌` |
| 拨启用开关时提示要先设管理团队                   | 先打开**修改**，把管理团队设上                                                               |
| 删除按钮是灰的                                   | 先把 Skill 停用。内置 Skill 永远删不掉                                                       |

## Skill 脚本跑不起来[​](#skill-脚本跑不起来 "Skill 脚本跑不起来的直接链接")

脚本执行有两道互不相干的门，报错会告诉你撞的是哪一道。

**「没有可运行的脚本」。** 运行器按 `main.py`、`main.sh`、顶层唯一的 `.py` 或 `.sh` 这个顺序找。多于一个就得你自己指定。

**执行被拒绝。** 你设了 `RequireIsolation = true`，而这台机器搭不出真沙箱。 这是配置在按预期工作。要么在 Linux 上提供一份 python-base 根文件系统， 要么接受这台机器不跑 Skill 脚本。

**跑起来了，但启动日志里有 `SKILL EXECUTION RUNNING WITHOUT ISOLATION (unsafe-exec)`。** 脚本是直接在夜莺所在主机上跑的，而且没有网络。就这么放着之前， 先读一遍[安装、管理与编写 Skills](https://n9e.github.io/zh/docs/ai/skills.md)里的安全那一节。

每次执行都会打 `sandbox audit: exec_id=... engine=... network=... exit_code=...`， 非零退出码就在这行里。

## MCP 客户端连不上[​](#mcp-客户端连不上 "MCP 客户端连不上的直接链接")

| 现象                                              | 原因和做法                                                                                                                  |
| ------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
| **401**，响应体是纯文本 `unauthorized`            | Token 不对或者已被删。如果所有 Token 都不行，那是 `[HTTP.TokenAuth]` 关着——启动日志里有 `[A2A] HTTP.TokenAuth.Enable=false` |
| **405**，带 `Allow: POST`                         | 客户端发的是 `GET /mcp`。无状态模式没有独立 SSE 流，客户端要按 Streamable HTTP 配，不是旧的 SSE 传输                        |
| **415** 或 **400**                                | 缺请求头。`Content-Type: application/json` 和 `Accept: application/json, text/event-stream` 两个都要                        |
| **403** 且提到 host                               | 反向代理没设 `proxy_set_header Host`，触发了 SDK 自带的 DNS rebinding 防护                                                  |
| 代理返回 404，或者本该是 JSON 的地方回了一段 HTML | 请求根本没到 `n9e`。`/mcp` 在根路径，不在 `/api/n9e` 下面，只转发 `/api/n9e/*` 的代理不会把它放过去——加一段 `location /mcp` |
| 连上大约 60 秒后断开                              | nginx 的默认值。加 `proxy_buffering off`，并把 `proxy_read_timeout`、`proxy_send_timeout` 放到一小时                        |

一份满足上面所有条件的代理配置在[启用 MCP 端点](https://n9e.github.io/zh/docs/ai/mcp-endpoint.md)。 想把客户端从嫌疑里排除掉，就用那页的 curl 复现一遍。

## 连上了，但工具不对[​](#连上了但工具不对 "连上了，但工具不对的直接链接")

| 现象                     | 原因                                                                                                                                                            |
| ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 一个工具都没有           | `MCPToolsets` 里的名字全被丢掉了。启动日志里每个坏名字对应一条 `[MCP] ignoring unknown toolset`——一份全是错别字的白名单换来的是零个工具，它不会回退成「全放开」 |
| 工具比预期少             | `MCPToolsets` 收得比你以为的窄，或者写工具没开。只读是 42 个，全开是 74 个                                                                                      |
| 明明开了写工具却没有     | `MCPEnableWriteTools = true` 要重启才生效。之后 `tools/list` 应该返回 74 个                                                                                     |
| 工具在，但每次调用都被拒 | Token 主人没有对应权限点，或者他所在团队够不着被点名的那个业务组。让它调一次 `list_busi_groups`，和那个账号在界面上看到的对比                                   |
| 出现了你不想要的工具     | 收窄 `MCPToolsets`。没有按单个工具开关的机制                                                                                                                    |

## A2A 调用卡住或者没返回[​](#a2a-调用卡住或者没返回 "A2A 调用卡住或者没返回的直接链接")

`/a2a` 驱动的就是内置助手，所以**它需要一份能用的 LLM 配置**—— 前两节的内容在这里要先适用一遍。

| 现象                                   | 原因                                                                                         |
| -------------------------------------- | -------------------------------------------------------------------------------------------- |
| 请求挂了几十秒                         | 正常。模型在思考、在调工具；服务端每 30 秒发一次状态心跳，防止网关掐连接                     |
| 网关还是把它掐了                       | 把 `proxy_read_timeout` 调大、`proxy_buffering` 关掉——网关自己的超时是 60 秒的话，心跳救不了 |
| 明明跑完了，`tasks/get` 却说任务不存在 | 任务状态在 Redis 里，TTL 24 小时。对话本身还在数据库里                                       |
| Agent Card 公布的是个内网地址          | `BaseURL` 留空了，是从请求头猜出来的。显式写上                                               |

更多在 [A2A 端点](https://n9e.github.io/zh/docs/ai/a2a.md)。

## 下一步[​](#下一步 "下一步的直接链接")

* 端点本身的配置：[启用 MCP 端点](https://n9e.github.io/zh/docs/ai/mcp-endpoint.md)
* 认证失败的细节：[MCP 认证为什么失败？](https://n9e.github.io/zh/docs/troubleshooting/mcp-auth.md)
* 工具缺失的细节：[MCP 工具为什么缺失或被拒绝？](https://n9e.github.io/zh/docs/troubleshooting/mcp-tools-missing.md)
