---
name: midea-recall-diagnose
description: 用于排查 sit/uat/prod 环境下 `/rag-recall/api/search/keyword` 未召回目标 doc/faq 的问题。支持两种输入：1) 完整请求（headers+body；若 `headers.appId` 缺失但 `body.appId` 存在，可回填）；2) requestId+targetId。统一走“回放 -> ELK -> ES -> 代码最小核对”，禁止 broad search 和冲突口径。
---

# Recall 排查（索引路由版）

## 0. 优先级与硬规则（必须遵守）

- **规则优先级**：`SKILL.md` > `references/*.md`。冲突时只按本文件执行。
- **接口约束**：`keyword` 回放只能用终端 `curl -X POST`，禁止浏览器地址栏访问。
- **取证范围**：本技能只用 `ELK + ES`，禁止调用 `/rag-recall/api/search/trace/recordInfo`。
- **执行通道约束（强制）**：除 `keyword` 回放外，`ELK` 取证一律使用 `python3 scripts/elk_api_query.py`（Kibana API）；`ES` 取证一律使用 `python3 scripts/es_proxy_query.py`（中立云控制台代理 API）。禁止 Playwright 页面查询 ELK/ES，禁止 `curl` 直连 ES，也禁止手写 shell 请求中立云代理接口。
- **完整请求强制回放**：拿到 `headers + body` 后，必须先回放并获取 fresh `requestId`，再查 ELK/ES。
- **回放后 requestId-first**：第一条 ELK 查询必须包含 `requestId + TRACE_TARGET_ES + targetId`。
- **首条 KQL 精确匹配（强制）**：首条查询中 `requestId/targetId/TRACE_TARGET_ES` 必须完整精确匹配，禁止 `*` 通配（如 `replay_*`）。
- **禁止 broad search**：回放成功后，禁止先用 `targetId` 单独扫 3 天日志再逐步收敛。
- **ELK 门禁（强制）**：任何 ELK 查询执行前，必须先通过 `python3 scripts/elk_guard.py ... --kql '<KQL>'` 校验；校验失败禁止继续查 ELK。
- **ELK 网络约束（强制）**：`elk_api_query.py` 必须忽略本地代理环境变量（`HTTP_PROXY/HTTPS_PROXY/ALL_PROXY` 等）并直连 ELK；禁止走本地代理。
- **时间窗规则**：回放后先查 `回放时间点 ±15 分钟`；无结果再扩到 `now-3d~now`。
- **TRACE 触发条件（已核对）**：`TRACE_TARGET_ES` 只会在 `traceTargetIds` 非空时打印；若原始请求 `traceTargetIds=[]`，原 `requestId` 很可能查不到该类日志，必须回放并注入 `targetIds`。
- **TRACE 日志格式（已核对）**：真实生产日志中，`phase=request` 会携带 `requestDsl=...`，`phase=response` 会携带 `isError/tookMs/returnedHitCount/totalHitCount`；`targetUrl` 形如 `GET /<index或逗号分隔索引> [cluster=N] (<desc>)`。样例见 `references/trace-target-es-format.md`。
- **回放头回填规则（已核对）**：若 `headers.appId` 缺失但 `request.body.appId` 存在，可回填为回放请求头；`appChannel` 同理。除这两个已核对字段外，其他关键鉴权头不得猜。
- **字段规则**：优先以 ELK `requestDsl` 实际字段为准；字段不明确再查 ES `_mapping`。
- **ES 路由规则（强制）**：进入 ES 前优先从 ELK `targetUrl` 中的 `[cluster=...]` 直接解析集群；若没有集群标识，再从 `requestDsl` / `targetUrl` 提取实际索引名做路由；禁止固定地址直查。
- **ES 路由消歧规则（强制）**：若日志已带 `[cluster=...]`，不得再要求用户补 `sourceSystem`；只有在无集群标识且 `requestDsl` 命中共享索引导致多集群歧义时，才可用 `sourceSystem` 辅助消歧；若仍不能唯一定位，必须中止，禁止 fallback。
- **ES 代理接口规则（已核对）**：中立云 ES 控制台支持通过 `POST /Elasticsearch/2024-01-11/dataRetrievales/requestEs` 代理 `_count/_search/_mapping` 等请求；必须通过 `scripts/es_proxy_query.py` 读取本地浏览器 cookie 并发起 API 请求，不启动 Playwright。可加 `--check-url-templates` 先用 `GET /Elasticsearch/2024-01-11/dataRetrievales/urlTemplates?cinsId=<id>` 校验代理接口可用性。
- **阶段顺序来源（强制）**：优先用运行时 `CHAIN_NAME` 提取真实阶段顺序；拿不到则动态读取关键链路代码（`SearchLiteFlowService + LiteFlowConstants`）；都失败才回退默认顺序。
- **阶段顺序门禁（强制）**：首次丢失阶段必须按当前链路顺序判定，未验证前序文本召回证据时，禁止直接判定向量阶段丢失。
- **首次丢失口径（强制）**：first-loss 只能由 `phase=response hit=false` 判定；仅凭下游 `phase=request` 缺失不得判该下游阶段丢失。
- **下游 request 缺失口径（强制）**：若已确认上游阶段 `response hit=false`，下游无 `phase=request` 只允许表述为“下游未触发/未发起 request（由上游丢失导致）”，禁止表述为“下游阶段丢失”。
- **描述禁令（强制）**：若向量阶段没有 `phase=response hit=false` 证据，禁止输出“向量召回没了/丢了”；必要时只能写“向量阶段 request 未触发（上游 first-loss 在 XXX）”。
- **首次丢失校验（强制）**：输出结论前必须通过 `python3 scripts/first_loss_guard.py` 校验。
- **代码后置**：默认先完成回放/ELK/ES 定位，输出前再做最小代码核对。
- **最小代码集**：只读与“首次丢失阶段”直接相关的 `2~4` 个文件，禁止全量扫代码。
- **targetIds 上限**：最多 10 个，超出直接拒绝。
- **缺参处理**：缺少可复用 `appId`（优先 `headers.appId`，其次 `body.appId`）或其他关键鉴权头时，不得猜测，必须要求补齐。

## 1. 输入模式

### A. 完整请求模式（优先）

- 输入：`env + targetType + targetIds + request.headers + request.body`
- 行为：必须先回放，再进入 ELK/ES。

### B. requestId 模式

- 输入：`env + targetType + targetIds + requestId`
- 行为：直接 ELK-first；证据不足时要求补全完整请求并执行回放。

## 2. 30 秒流程卡（固定顺序）

1. 规范化输入并校验 JSON。
2. 回放请求（fresh `requestId` + 注入 `traceTargetIds`）。
3. 先用 `scripts/elk_guard.py` 生成并校验 KQL，再用 `requestId + TRACE_TARGET_ES + targetId` 查 ELK。
4. 仅当首次丢失在召回阶段时进入 ES 做三步快检。
5. 输出前做最小代码核对，给出代码证据。

## 3. 标准流程（可执行）

### 3.1 完整请求模式

1. 规范化输入：

```bash
cat >/tmp/diag_input.json <<'JSON'
<input-json>
JSON
jq -e . /tmp/diag_input.json >/dev/null
python3 scripts/prepare_diagnosis.py --input /tmp/diag_input.json
```

2. 回放前处理：
- 将 `body.requestId` 替换为 fresh 值（`原ID_replay_<ts>` 或 `uuidgen`）。
- 将 `targetIds` 合并到 `body.traceTargetIds`。
- 若 `headers.appId` 缺失但 `body.appId` 存在，可用 `body.appId` 回填请求头；`appChannel` 同理。

3. 执行回放：

```bash
curl -X POST '<base_url>/rag-recall/api/search/keyword' \
  -H 'Content-Type: application/json' \
  -H 'appId: <appId>' \
  -H 'appChannel: <appChannel>' \
  -d '<body-with-fresh-requestId-and-traceTargetIds>'
```

4. 回放成功判定：
- 响应中拿到 replay `requestId`。
- 记录最小摘要：`requestId`、总命中数、错误信息。

5. ELK 阶段定位：
- 查询必须包含：`requestId + targetId + TRACE_TARGET_ES`（可加 `link_id=requestId`）。
- 查询前必须执行门禁：

```bash
# 生成推荐 KQL
python3 scripts/elk_guard.py \
  --request-id '<replayRequestId>' \
  --target-id '<targetId>' \
  --mode first \
  --emit-template

# 校验你将要执行的 KQL；失败则停止，不得继续
python3 scripts/elk_guard.py \
  --request-id '<replayRequestId>' \
  --target-id '<targetId>' \
  --mode first \
  --kql '<your-kql>'
```

- 首条 KQL 必须直接采用 `--emit-template` 输出，不允许“因为太长”而删减到 requestId-only / targetId-only。
- 时间窗先用 15 分钟，再扩 3 天。
- 执行方式：仅允许 `scripts/elk_api_query.py` 走 ELK API（自动复用浏览器会话 cookie）；禁止 Playwright 页面查询 ELK。
- ELK API 查询示例（先首条三元组，再按需 cmp/hit=false）：

```bash
python3 scripts/elk_api_query.py \
  --env prod \
  --request-id '<replayRequestId>' \
  --target-id '<targetId>' \
  --mode first \
  --time-window 'now-15m~now'
```
- 先从 ELK 提取该次请求的 `CHAIN_NAME` 阶段顺序，再按顺序找首个 `phase=response hit=false` 的 `cmpId`。
- 对每个下游阶段，`phase=request` 仅用于说明“是否触发”，不可替代 `phase=response` 作为 first-loss 证据。
- `DOC` 目标输出时，必须同时给出：
  - `FIRST_LOSS`（按完整链路顺序）
  - `DOC_PATH_FIRST_LOSS`（仅 DOC 主线：`full_range_meta_filter -> full_range_docTxtRecall -> recall_doc_vector_v3_filter -> doc_item_vector_retrieval_batch_es -> full_range_rerank`）
- 首次丢失结论前必须跑阶段门禁（示例）：

```bash
python3 scripts/first_loss_guard.py \
  --target-type DOC \
  --chain-line 'CHAIN_NAME[_FULL_RANGE_SEARCH_WITH_LLM_] full_range_meta_filter[...]==>full_range_docTxtRecall[...]==>doc_item_vector_retrieval_batch_es[...]==>full_range_rerank[...]' \
  --events '[{"cmpId":"full_range_docTxtRecall","phase":"response","hit":true},{"cmpId":"doc_item_vector_retrieval_batch_es","phase":"response","hit":false}]' \
  --assert-first-loss doc_item_vector_retrieval_batch_es
```

- 若没有 `CHAIN_NAME`，直接让脚本从代码提取链路顺序：

```bash
python3 scripts/first_loss_guard.py \
  --target-type DOC \
  --repo-root '<rag-recall-root>' \
  --chain-id '_FULL_RANGE_SEARCH_WITH_LLM_' \
  --events '<events-json>'
```

- `--chain-order` 仅用于调试覆盖，不作为常规输入。
- 若 `first_loss_guard.py` 返回 `BLOCKED/FAIL`，禁止输出“向量阶段首次丢失”。

6. ES 验证（仅召回阶段进入）：
- 先解析 ES 控制台路由（示例）：

```bash
python3 scripts/prepare_diagnosis.py \
  --input /tmp/diag_input.json \
  --config references/env-config.local.yaml \
  --request-dsl '<requestDsl-or-raw-elk-line>' \
  --source-system '<sourceSystem-if-needed>' | jq '.esConsoleRoute'
```

- 若脚本报 `requestDsl index route is ambiguous`：先检查是否命中了共享 FAQ 索引；必要时补一个 `sourceSystem` 做消歧。
- 若脚本报 `unable to resolve ES console route` 或 `sourceSystem ... has no ES cluster mapping`：立即中止并补齐有效的 `requestDsl/sourceSystem` 证据。
- `Q1` 原始 `requestDsl` 复跑。
- `Q2` 目标存在性（DOC 用 `doc_id`；FAQ 用 `knowledge_base_id`）。
- `Q3` 保留 filter + 去掉 text must（仅文本阶段需要）。
- 执行方式：仅允许 `scripts/es_proxy_query.py` 走中立云控制台代理 API（自动复用浏览器 cookie），禁止 Playwright 页面操作。
- 代理调用固定格式：`POST /Elasticsearch/2024-01-11/dataRetrievales/requestEs`，请求体包含 `cinsId`、`path`、`method`、`body`；其中 `body` 必须是传给 ES 的 JSON 字符串，而不是对象。
- 原始 `requestDsl` 复跑示例：

```bash
python3 scripts/es_proxy_query.py \
  --env prod \
  --request-dsl '<requestDsl-or-raw-elk-line>' \
  --path '/<index>/_search' \
  --method POST \
  --body '<requestDsl-json-body>'
```

- 目标存在性示例（DOC）：

```bash
python3 scripts/es_proxy_query.py \
  --env prod \
  --request-dsl '<requestDsl-or-raw-elk-line>' \
  --path '/<index>/_count' \
  --method POST \
  --body '{"query":{"term":{"doc_id":"<targetId>"}}}'
```

- 仍然禁止 `curl` 直连 ES；若浏览器 cookie 失效或代理接口返回未登录/无权限，先中止并要求用户在浏览器重新登录控制台后重跑脚本，不得 fallback 到 Playwright 页面点击。
- 若 gateway 鉴权 cookie 挂在父域，可给 `scripts/es_proxy_query.py` 增加 `--cookie-domain midea.com`；仍不得绕过脚本手写请求。

### 3.2 requestId 模式

1. 先按 `requestId + targetId + TRACE_TARGET_ES` 生成并校验 KQL，再用 `scripts/elk_api_query.py --mode first` 查 ELK。
2. 若 15 分钟无结果，扩到 `now-3d~now`。
3. 若原始请求 `traceTargetIds=[]` 或扩窗后仍无有效证据，判定“未完成带 trace 的复现”，要求补全完整请求并回放。
4. 首次丢失在召回阶段时，再进入 ES 三步快检。

## 4. 代码核对（按需触发，输出前必须）

触发条件（任一满足）：
- 已定位首次丢失阶段，准备输出根因。
- ELK/ES 证据冲突或无法解释。
- 用户明确要求查看实现细节。

最小必读文件（按场景选 2~4 个）：
- 入口与参数约束：
  - `api/src/main/java/com/midea/jr/robot/rag/recall/api/web/controller/SearchController.java`
- TRACE 语义：
  - `infrastructure/src/main/java/com/midea/jr/robot/rag/recall/infrastructure/aspect/EsQueryTraceAspect.java`
  - `common/src/main/java/com/midea/jr/robot/rag/recall/common/utils/TraceTargetScanUtils.java`
- cmpId 映射：
  - `common/src/main/java/com/midea/jr/robot/rag/recall/common/constant/LiteFlowConstants.java`
- 召回实现：
  - DOC：`domain/src/main/java/com/midea/jr/robot/rag/recall/domain/search/cmp/fullrange/FullRangeDocTxtRecallCmp.java`
  - DOC 向量：`domain/src/main/java/com/midea/jr/robot/rag/recall/domain/search/cmp/fullrange/RecallDocItemVectorBatchEsCmp.java`
  - FAQ：`domain/src/main/java/com/midea/jr/robot/rag/recall/domain/search/cmp/fullrange/FullRangeFaqTxtRecallCmp.java`

## 5. 根因判定最小集

- `phase=response hit=false`：该阶段未命中目标的最高优先级证据。
- `下游 phase=request 缺失`：只表示该阶段可能未触发，必须依附上游 first-loss 解释，不能单独当作“该阶段丢失”。
- `目标存在性=0`：索引缺数据/发布未生效/索引路由不覆盖。
- `目标存在性>0 且 原DSL=0`：文本匹配或过滤条件问题。
- `原DSL>0 但最终未返回`：排序/阈值/TopN 问题。
- 若已确认丢在 `full_range_rerank` 或之后：停止 ES 深挖，按 rerank/准出问题交付。

## 6. 输出模板（固定）

1. 目标首次丢失阶段（`cmpId`）
2. 简要原因
3. ELK 关键证据
4. 阶段审计（文本/向量/重排各阶段 `response hit=true|false|unknown`；可补充 `request triggered|not_triggered`，但不得用其判 first-loss）
5. 代码证据（至少 2 条，`文件:行号`）
6. 若在召回阶段：ES 证据（`total/returned/rank/score`）
7. 下一步动作（仅当前阶段相关）

## 7. 违规恢复协议（必须）

- 若出现任一违规（如 `targetId-only`、`requestId-only`、缺 `TRACE_TARGET_ES`、先 broad search），必须立即中止当前路径。
- 若出现 `requestId` 通配/截断（如 `replay_*`）或首条 KQL 被降级，按违规处理并立即中止。
- 若出现 Playwright 页面查询 ELK/ES、或未通过 `elk_guard.py` 就直接执行 ELK API 查询，按违规处理并立即中止。
- 若出现 `curl` 直连 ES、或手写 shell 调用中立云 `requestEs` 代理接口（非 `scripts/es_proxy_query.py`），按违规处理并立即中止。
- 若出现“未验证文本阶段就判向量阶段”的情况，按违规处理。
- 先输出一行：`BLOCKED_BY_GUARD: <违规原因>`。
- 然后从“ELK 阶段定位”重跑：先 `elk_guard.py` 校验通过，再继续。

## 8. 资源

- `scripts/prepare_diagnosis.py`
- `scripts/elk_guard.py`
- `scripts/elk_api_query.py`
- `scripts/es_proxy_query.py`
- `scripts/first_loss_guard.py`
- `references/quick-runbook.md`
- `references/trace-target-es-format.md`
- `references/env-config.example.yaml`
- `references/env-config.local.yaml`
