# 天璇 (TianXuan) 分析工作流

> 从原始 TLS 流 CSV 到实体画像、聚类结果、区分特征的完整流水线文档。

---

## 目录

1. [数据准备](#1-数据准备)
2. [数据加载与配置](#2-数据加载与配置)
3. [数据校验](#3-数据校验)
4. [数据清洗](#4-数据清洗)
5. [实体识别](#5-实体识别)
6. [类型感知聚合](#6-类型感知聚合)
7. [聚类分析](#7-聚类分析)
8. [特征提取](#8-特征提取)
9. [可视化](#9-可视化)
10. [LLM 集成](#10-llm-集成)
11. [跨机部署](#11-跨机部署)

---

## 1. 数据准备

### 1.1 CSV 格式要求

天璇接收以 **CSV 格式**存放的 TLS 流记录文件。每行代表一条网络流。

| 要求 | 说明 |
|------|------|
| **分隔符** | 逗号 `,`（默认），支持自定义分隔符 |
| **文件扩展名** | `.csv` |
| **多文件** | 支持 glob 通配符、`**/*.csv` 递归扫描、自动合并 |
| **压缩包** | `.zip` 文件自动解压（通过上传界面） |
| **文件数量** | 无硬性限制（合并时使用 Polars `diagonal_relaxed` 模式） |

### 1.2 编码建议

| 编码 | BOM 标记 | 自动检测 |
|------|---------|---------|
| **UTF-8**（推荐） | 无 / `EF BB BF` | ✅ 默认 |
| UTF-8 with BOM | `EF BB BF` | ✅ 自动检测 |
| UTF-16LE | `FF FE` | ✅ 自动检测 |
| UTF-16BE | `FE FF` | ✅ 自动检测 |
| Latin-1 | 无 | ✅ 手动指定 |

> **⚠️ 生产建议**：始终使用 **UTF-8 无 BOM** 编码。跨机器部署时强烈建议通过 `set PYTHONUTF8=1` 强制 Python 使用 UTF-8 模式（`run.bat` 已包含该设置）。

### 1.3 推荐列

系统基于列名关键词自动识别以下维度：

| 维度 | 推荐列名（不区分大小写） |
|------|------------------------|
| **源 IP** | `src_ip`、`source_ip`、`src_addr` |
| **目的 IP** | `dst_ip`、`dest_ip`、`destination_ip`、`dst_addr`、`ip_dst` |
| **字节数（上行）** | `bytes_sent`、`src_bytes`、`bytes_out`、`up_bytes`、`upload` |
| **字节数（下行）** | `bytes_rev`、`bytes_received`、`dst_bytes`、`down_bytes`、`download` |
| **持续时间** | `duration`、`dur`、`elapsed`、`time_delta`、`flow_duration` |
| **包数** | `packets`、`pkts`、`packet_count`、`total_packets` |
| **TLS 版本** | `tls_version`、`version`、`ssl_version`、`tlsver` |
| **加密套件** | `cipher_suite`、`cipher`、`tls_cipher`、`ssl_cipher` |
| **SNI** | `sni`、`server_name`、`tls_sni` |
| **协议** | `proto`、`protocol`、`l4_proto`、`ip_proto` |
| **目的端口** | `dst_port`、`destination_port`、`dest_port`、`port_dst` |
| **目标 URL** | `dst_url`、`url`、`uri` |
| **时间戳** | `timestamp`、`ts`、`time`、`datetime`、`first_seen` |
| **纬度** | `lat`、`latitude`、`latitud`、`y` |
| **经度** | `lon`、`lng`、`longitude`、`longitud`、`x` |

> 列名通过下划线分词匹配关键词（如 `flow_duration` 匹配 `duration`）。匹配算法见 `analysis/entity_aggregator.py` 中的 `_find_column()`。

### 1.4 命名规范

- **不要求**列名在所有文件中完全一致（默认 `schema_strict=false`）
- 不同名的列自动取并集，缺失列填 `null`
- 建议团队内统一列名，以获得最佳的自动识别效果

---

## 2. 数据加载与配置

### 2.1 加载入口

**主函数**：`analysis/data_loader.py::load_csv_directory()`

```python
lf, schema, row_count, file_count, memory_mb = load_csv_directory(
    glob_pattern="data/*.csv",
    encoding="utf-8",
    delimiter=",",
    schema_strict=False,
    recursive=False,
)
```

**MCP 工具**（通过 `tool_registry.py` 暴露）：

```
load_data(csv_glob="data/*.csv", schema_strict=False, recursive=False)
```

**CLI 管道**（一键运行）：

```bash
runtime\python\python.exe manage.py run_pipeline data/*.csv
```

### 2.2 列类型配置

在 `config/config.yaml` 中通过 `columns` 映射手动指定列类型。配置优先级高于自动推断：

```yaml
columns:
  src_ip: ipv4          # 强制识别为 IPv4
  dst_ip: ipv4
  bytes_sent: int       # 强制为整数类型
  bytes_rev: int
  duration: float       # 强制为浮点类型
  tls_version: enum     # 枚举类型（低基数分类变量）
  cipher_suite: hex     # 十六进制数据
  dst_url: url          # URL 字符串
  lat: lat_lon          # 地理纬度
  lon: lat_lon          # 地理经度
  timestamp: timestamp  # 时间戳
```

支持的类型字符串及映射：

| 配置值 | `DataType` | 聚合方式 |
|--------|-----------|---------|
| `int` | `INT` | mean / sum |
| `float` | `FLOAT` | mean / sum |
| `enum` | `ENUM` | mode（众数） |
| `hex` | `HEX` | count（hex 对数统计） |
| `url` | `URL` | domain（提取域名） |
| `ipv4` | `IPv4` | subnet（/24 网络） |
| `lat_lon` | `LAT_LON` | mean |
| `timestamp` | `TIMESTAMP` | min / max |
| `bool_enum` | `BOOL_ENUM` | mode |
| `string` | `STRING` | count |

### 2.3 Schema 容错

| 模式 | 行为 | 适用场景 |
|------|------|---------|
| `schema_strict: true` | 列名不一致时报 `ValueError` | 测试/质量门禁 |
| `schema_strict: false`（默认） | 自动并集合并，缺失列填 null | 日常使用 |

**建议**：生产环境建议 `schema_strict: true`，提前发现列名漂移。

### 2.4 GeoIP 配置

`config.yaml` 中的 GeoIP 配置段控制自动经纬度填充和旧列丢弃：

```yaml
data:
  geoip:
    enabled: true
    ip_columns: ["src_ip", "dst_ip"]          # 需查询的 IP 列
    drop_columns: ["src_latitude", "src_longitude",  # 加载后丢弃的旧列
                   "dst_latitude", "dst_longitude"]
```

GeoIP 数据文件：`data/geoip_data.txt`，内嵌约 23 个主要城市 IP 段（200 KB 二进制查询格式）。

查询接口：

```python
from analysis.geoip import lookup
result = lookup("8.8.8.8")  # → {lat: 37.386, lon: -122.083, city: "Mountain View", country: "US"}
```

> ⚠️ **旧列丢弃**：如果 CSV 本身已包含 `src_latitude`/`src_longitude` 等列（来自旧有系统的 GeoIP 结果），系统在加载后自动丢弃这些列，避免与新的 GeoIP 查询冲突。

---

## 3. 数据校验

### 3.1 校验入口

**主函数**：`analysis/data_validator.py::validate()`

```python
from analysis.data_validator import validate, report

# 返回 JSON 报告
validation_result = validate("dataset_id", strict=False)

# 返回格式化字符串报告
print(report("dataset_id"))
```

校验项通过 `[VALIDATE]` 日志标记输出。

### 3.2 校验项说明

| 校验项 | 检测内容 | 判定标准 |
|--------|---------|---------|
| **列类型矛盾** | schema 推断类型 vs classify_column 结果 | `INT↔FLOAT`、`ENUM↔BOOL_ENUM` 为良性差异；STRING 类型包容一切 |
| **缺失率** | 每列 `null_count / total_rows` | `>50%` → **高风险**；`>30%` → 警告 |
| **异常值** | 数值列 Z-score > 5 的比例 | `>5%` 触发警告 |
| **枚举分布** | ENUM/BOOL_ENUM 列的值频率 | 标记空白、`+`、`-` 等特殊符号 |
| **IPv4 有效性** | IP 列每个值的合法性 | 无效 IP 比例 `>50%` → **高风险** |

### 3.3 校验报告解读

JSON 报告结构：

```json
{
  "valid": true,              // 无高风险项时为 true
  "dataset_id": "ds_abc123",
  "total_rows": 5000,
  "total_columns": 24,
  "columns": {
    "src_ip": {
      "dtype": "String",
      "inferred_type": "IPv4",
      "null_rate": 0.02,
      "warnings": []
    },
    "duration": {
      "dtype": "Float64",
      "inferred_type": "FLOAT",
      "null_rate": 0.55,
      "z_score_gt_5_ratio": 0.03,
      "warnings": ["null rate 55.0% exceeds 50% (high risk)"]
    }
  },
  "warnings": [...],
  "risks": ["Column 'duration': null rate 55.0% exceeds 50% (high risk)"]
}
```

### 3.4 处理建议

| 发现问题 | 建议处理 |
|---------|---------|
| 缺失率 > 50% | 检查数据源是否漏采集；考虑在预处理阶段 `fillna` 或 `drop` |
| Z-score > 5 比例过高 | 检查是否有异常流量（DDoS、扫描等） |
| 无效 IPv4 比例高 | 检查列名是否错误（非 IP 列被匹配为 IPv4） |
| 枚举值异常（空白/`+`/`-`） | 这些值会在清洗阶段被规范化（见第 4 节） |

---

## 4. 数据清洗

数据清洗在 `load_csv_directory()` 中自动执行，分三个阶段。

### 4.1 旧经纬度丢弃

加载完成后，立即丢弃 GeoIP 配置中指定的旧列：

```python
# 默认丢弃列
drop_columns = ["src_latitude", "src_longitude", "dst_latitude", "dst_longitude"]
# 只丢弃 schema 中实际存在的列
merged_lf = merged_lf.drop(existing_drop)
```

日志标记：`[CLEAN] dropped columns: [...]`

### 4.2 BOOL_ENUM 标准化

对 `classify_schema()` 标记为 `DataType.BOOL_ENUM` 的字符串列，执行以下变换：

| 原始值 | 标准化后 |
|--------|---------|
| `""`（空字符串） | `null` |
| `"+"` | `"True"` |
| `"-"` | `"False"` |
| `"t"`, `"T"`, `"true"`, `"True"` | 保持不变 |
| `"f"`, `"F"`, `"false"`, `"False"` | 保持不变 |

> 原生 Boolean 类型的列跳过此步骤（已经是标准格式）。

日志标记：`[CLEAN] BOOL_ENUM columns normalised: [...]`

### 4.3 HEX 预处理

对标记为 `DataType.HEX` 的列（如 cipher_suite），添加 `{col}_hex_pairs_count` 辅助列：

```python
# 计算每个值中空格分隔的 hex 对数量
pl.col("cipher_suite").str.split(" ").list.len().alias("cipher_suite_hex_pairs_count")
```

该计数列在后续聚合中以 `avg_hex_pairs_count` 参与聚类特征。

日志标记：`[CLEAN] HEX columns added hex_pairs_count: [...]`

### 4.4 通用数值清洗

对于任何未被分类为 IPv4/URL/HEX/ENUM/LAT_LON 的字符串列，系统自动尝试转为 Float64：

| 原始值 | 清洗后 |
|--------|--------|
| `"123.45"` | `123.45` |
| `"+"` / `"-"` | `null` |
| 空白／空字符串 | `null` |
| `"N/A"` / `"null"` / `"None"` | `null` |
| 其他不可解析字符串 | `null` |

这防止了"mean on str column"崩溃（当字符串列因数据伪影未能被早期类型检测捕获时），
无论后续数据中出现何种新伪影。

函数：`analysis/entity_aggregator.py` → `_coerce_to_float()`

日志标记：`[CLEAN] numeric coercion applied to: [...]`

---

## 5. 实体识别

### 5.1 自动检测原理

**主函数**：`analysis/entity_detector.py::detect_entity_column()`

采用三维打分算法，对所有 String/Categorical 列评分：

| 维度 | 分值 | 说明 |
|------|------|------|
| **名称匹配** | +3.0 | 列名（归一化后）精确匹配关键词或 `_` 分隔匹配 |
| **唯一值比率** | 0~10 | `unique_ratio = unique_count / non_null_count`，在 `[0.01, 0.50]` 区间内得分最高 |
| **字符串类型** | +2.0 | `Utf8` / `String` / `Categorical` 类型加分 |
| **高唯一性惩罚** | -5.0 | `unique_ratio > 0.50` 时扣分（可能是随机 ID） |
| **高空值惩罚** | ×0.1 | null 率 > 50% 时分数严重降低 |

关键词集（`ENTITY_KEYWORDS`）：

```
src_ip, dst_ip, source_ip, destination_ip, ip,
sni, server_name, host, domain, user, client,
username, hostname, mac, email, session_id,
uid, id, src_addr, dst_addr
```

### 5.2 多列复合键

**支持场景**：当需要按 `(src_ip, dst_ip)` 或 `(src_ip, sni)` 等多列组合作为实体标识时。

**MCP 工具参数**：

```python
build_entity_profiles(
    dataset_id="ds_...",
    entity_columns=["src_ip", "dst_ip"],  # 多列复合键
    auto_detect=false
)
```

**CLI 管道**目前仅支持单列实体键。

### 5.3 IP 掩码聚类

在聚合阶段中，对 `DataType.IPv4` 类型的列自动生成 `/24` 子网信息：

```python
# entity_aggregator.py 中：
subnet_expr = (
    pl.col(ip_col).str.split('.').list.slice(0, 3).list.join('.') + pl.lit('.0/24')
)
# → "192.168.1.0/24"
```

生成的 `unique_24_networks` 在聚类时作为特征输入，反映实体的网络分布广度。

---

## 6. 类型感知聚合

### 6.1 聚合原理

**主函数**：`analysis/entity_aggregator.py::aggregate_by_entity()`

1. 扫描 schema 通过关键词匹配找到可用列
2. 通过 `type_classifier` 获取每列的 `DataType` 分类
3. 按数据类型选择对应的聚合函数

### 6.2 数据类型→聚合函数映射

| `DataType` | 聚合方式 | 特征命名 | 代码位置 |
|-----------|---------|---------|---------|
| **INT** (整数) | `mean()` 均值 | `total_bytes_sent`, `total_packets` | `build_aggregation_params()` |
| **FLOAT** (浮点) | `mean()` 均值 | `avg_duration`, `avg_latitude` | `build_aggregation_params()` |
| **ENUM** (枚举) | `mode().first()` 众数 | `tls_version_mode`, `tcp_flag_modes` | `build_aggregation_params()` |
| **HEX** (十六进制) | `str.extract_all().list.len().mean()` 统计 hex 对数量 | `avg_hex_pairs_count` | `build_aggregation_params()` |
| **URL** (链接) | `str.extract(r'https?://([^/]+)').n_unique()` 统计唯一域名 | `unique_domains` | `build_aggregation_params()` |
| **IPv4** (地址) | `str.split('.').list.slice(0,3).join('.') + '.0/24'.n_unique()` 子网多样性 | `unique_24_networks` | `build_aggregation_params()` |

### 6.3 聚合特征分类

所有生成的聚合特征分为 6 大类：

| 类别 | 特征 | 说明 |
|------|------|------|
| **流统计** | `flow_count`、`total_bytes_sent`、`total_bytes_rev`、`avg_duration`、`total_packets` | 实体流量基本量 |
| **目标多样性** | `unique_dst_ips`、`unique_dst_ports`、`unique_dst_networks` | 目的地址分布广度 |
| **TLS 特征** | `unique_ciphers`、`tls_version_mode`、`unique_versions`、`sni_count` | 加密和证书多样性 |
| **协议特征** | `unique_protocols`、`tcp_flag_modes` | 协议和标志分布 |
| **地理位置** | `avg_latitude`、`avg_longitude` | 经纬度均值 |
| **时间模式** | `first_seen`、`last_seen`、`active_hours_count` | 活动时间段 |

### 6.4 类型安全防范

build_aggregation_params 在聚合前检查列的实际 Polars dtype：

```python
# 在聚合非数值列前 cast
if schema.get(col_name) in ('Utf8', 'String'):
    pl.col(col_name).cast(pl.Float64)
```

这解决了跨机"mean on str"错误（当同一数据在不同机器上被 Polars 推断为不同 dtypes 时）。

---

## 7. 聚类分析

### 7.1 算法选择

| 算法 | 适用场景 | 优点 | 缺点 |
|------|---------|------|------|
| **HDBSCAN**（默认） | 未知簇数、存在噪声点 | 自动确定簇数；噪声点处理；无需预设 k | 对 `min_cluster_size` 敏感；大数据集计算慢 |
| **KMeans** | 已知簇数、球形分布 | 速度快；可预设 k | 需指定 n_clusters；对离群点敏感 |

### 7.2 参数建议

**HDBSCAN 参数**：

| 参数 | 默认值 | 说明 | 调整建议 |
|------|--------|------|---------|
| `min_cluster_size` | 5 | 最小簇样本数 | 数据量大时增大（10~20）；数据少时自动降级为 `max(len//2, 2)` |
| `min_samples` | None | 核心点邻域 | 默认与 min_cluster_size 相同；增大产生更保守的聚类 |
| `metric` | euclidean | 距离度量 | 高维特征时考虑 `cosine` |
| `cluster_selection_epsilon` | 0.0 | 簇选择阈值 | 越大簇越少 |

**KMeans 参数**：

| 参数 | 默认值 | 说明 |
|------|--------|------|
| `n_clusters` | 3 | 簇数 |
| `random_state` | 42 | 随机种子（保证可复现性） |

### 7.3 零样本降级

当样本数过少或全为缺失值时，系统自动降级：

```python
if len(data) < 3:
    return {"n_clusters": 0, "n_noise": 0, "quality_metrics": {"note": "too few samples"}}
```

同样，全部为 NaN 的列在聚类前自动丢弃。

### 7.4 预处理步骤

聚类执行前的自动预处理流程：

1. **列过滤**：只保留请求的特征列
2. **全 NaN 列丢弃**：无用列自动移除
3. **NaN 填充**：数值 NaN 用该列均值填充
4. **StandardScaler**：所有特征标准化（零均值、单位方差）
5. **min_cluster_size 自适应**：不超过样本数的 50%

### 7.5 质量评估

| 指标 | 范围 | 说明 |
|------|------|------|
| **Silhouette Score** | `[-1, 1]` | >0.5 表示良好聚类；>0.7 表示优秀 |
| **Davies-Bouldin Index** | `[0, +∞)` | 越低越好（簇内紧凑、簇间分离） |
| **Calinski-Harabasz Score** | `[0, +∞)` | 越高越好 |
| **Noise Ratio** | `[0, 1]` | HDBSCAN 特有的噪声点比例 |

### 7.6 CLI 运行示例

```bash
# 完整管道（HDBSCAN 默认）
runtime\python\python.exe manage.py run_pipeline data/complex_test.csv

# 指定 KMeans
runtime\python\python.exe manage.py run_pipeline data/*.csv --algo kmeans

# 手动指定实体列
runtime\python\python.exe manage.py run_pipeline data/*.csv --entity-col src_ip
```

---

## 8. 子网实体聚合

config.yaml中配置:
```yaml
entity:
  subnet_masks: [24, 28]   # 子网掩码列表
  ip_columns: ["src_ip", "dst_ip"]  # 需要聚合的IP列
```

启用后，同一子网（如 10.0.1.0/24）下的所有 IP 合并为一个实体节点。
支持 /24（0-255）和 /28（240-255, 16个IP分组）两种掩码。

`entity_aggregator.ip_to_subnet()` 负责 IP 到 CIDR 子网前缀的转换，
`apply_subnet_aggregation()` 在 group_by 前对 IP 列应用子网掩码。

```python
# 示例: IP → /24 子网
ip_to_subnet("192.168.1.42", 24)  # → "192.168.1.0/24"

# 示例: IP → /28 子网
ip_to_subnet("192.168.1.42", 28)  # → "192.168.1.32/28"
```

---

## 8. 特征提取

### 8.1 方法对比

**主函数**：`analysis/tool_registry.py` → `_handle_extract_features()`

| 方法 | 公式 | 适用场景 |
|------|------|---------|
| **Z-Score**（默认） | `(μ_cluster - μ_global) / σ_global` | 簇中心 vs 全局均值的偏离程度 |
| **ANOVA** | `|μ_cluster - μ_global| / (σ_cluster + σ_global)` | 考虑簇内方差，更保守 |

### 8.2 区分特征解读

返回的每个特征包含：

```json
{
  "feature_name": "total_bytes_sent",
  "cluster_label": 0,
  "mean": 1234567.89,          // 该簇均值
  "std": 234567.89,            // 该簇标准差
  "global_mean": 987654.32,    // 全局均值
  "global_std": 765432.10,     // 全局标准差
  "distinguishing_score": 2.34, // Z-Score 或 ANOVA 分数
  "distinguishing_method": "zscore"
}
```

**解读方法**：

| 分数范围 | 含义 |
|---------|------|
| \|Z\| < 1.0 | 无明显区分力 |
| 1.0 ≤ \|Z\| < 2.0 | 中等区分力 |
| \|Z\| ≥ 2.0 | **强区分特征**（在 Web 界面中以颜色高亮） |

### 8.3 地理分布可视化

如果聚合特征包含 `avg_latitude` 和 `avg_longitude`，聚类概览页会同时显示：

- **PCA 散点图**：实体在二维主成分空间的分布，颜色区分聚类
- **地理分布图**：实体在地理坐标上的分布

地理位置在 Web 界面中通过 Canvas 2D API 原生绘制（无外部 JS 依赖）：

```python
# views.py 中提取逻辑
lat = _extract_lat(feature_json.get('avg_latitude'))
lon = _extract_lon(feature_json.get('avg_longitude'))
# 过滤无效值：纬度 -90~90，经度 -180~180
```

---

## 9. 可视化

### 9.1 PCA 散点图
路径: `/clusters/<run_id>/`
使用纯 Canvas 2D 绘制的散点图，展示实体在二维主成分空间中的分布。
点颜色按聚类标签区分，鼠标悬停显示实体名称。
依赖: `run_pipeline` 步骤 6 自动计算 PCA-2D 坐标并写入 `EntityProfile` 表。

### 9.2 3D 地球流量弧线
路径: `/globe/`
使用 Three.js (603KB, 全离线) 绘制的三维地球，显示源 IP 到目标 IP 的 TLS 流量弧线。
- 颜色: TLSv1.3 (蓝) / TLSv1.2 (粉) / 其他 (青)
- 弧线高度根据距离自动调整
- 鼠标拖拽旋转地球
- 地球贴图: 1.4MB (离线内嵌)
- **多数据源叠加**: 页面复选框选择多个运行，弧线叠加在同一地球上
- **经纬度来源**: 优先使用数据中`src_latitude/src_longitude/dst_latitude/dst_longitude`列，无则回退GeoIP查询
- GeoIP: 从 `data/geoip_city.csv` 查询 IP 对应经纬度
- 滚轮缩放: 鼠标滚轮控制相机距离(3-30)
- 经纬线: 10度间隔的白色半透明辅助线
- 国境线: world_borders.js 10国精简轮廓，含台湾和南海九段线示意
- **背面剔除**: `updatePulses` 中使用 `localToWorld` 替代 `applyQuaternion`，确保弧线脉冲只在面向相机的一侧显示，不穿透地球
- **脉冲动画**: 弧线上移动的贝塞尔曲线脉冲光点，`arcGroup.localToWorld(midPt)` 计算世界坐标，`midPt.dot(camDir) < -0.08` 判段正面可见性
- 多数据选择: 复选框选择多个运行叠加弧线
- 经纬度来源: 优先使用数据列 src_latitude/src_longitude 等，无则GeoIP回退

### 9.3 聚类概览页
路径: `/clusters/<run_id>/`
同时显示 PCA 散点图和每簇的 Top 区分特征。

## 10. LLM 集成

### 9.1 API 配置

通过 `config/config.yaml` 或 Web 界面「配置」页面设置：

```yaml
llm:
  enabled: true              # 启用 LLM 功能
  base_url: "https://api.openai.com/v1"  # OpenAI 兼容 API 地址
  api_key: "sk-..."          # API Key
  model: "deepseek-v4-flash" # 模型名称
  max_tokens: 2048
  temperature: 0.1
```

支持的 LLM 服务：
- OpenAI GPT-4 / GPT-3.5
- Azure OpenAI Service
- 本地部署的 Ollama / vLLM / LM Studio
- 其他 OpenAI 兼容 API

### 9.2 32B 模型优化

系统针对 32B 等较大模型的推理特性做了以下优化（`tianxuan/llm_orchestrator.py`）：

| 优化项 | 实现 | 说明 |
|--------|------|------|
| **Step Guidance** | 每步告诉模型下一步做什么 | 减少 32B 模型在复杂推理中的发散 |
| **工具结果截断** | 截断至 1200 chars | 避免上下文窗口溢出 |
| **超时** | 90s | 32B 推理更慢，比默认超时长 |
| **最大步数** | 15 步 | 限制过长的工具调用链 |
| **工具描述** | 一行简洁描述 | 减少冗长描述对 token 的消耗 |

### 9.3 LLM 分析流程

自动化编排调用链：

```
Step 1: profile_data("ds_...")         → 了解数据集的列统计
Step 2: build_entity_profiles("ds_...") → 实体聚合
Step 3: run_clustering("entity_...")    → 聚类分析
Step 4: extract_features("entity_...")  → 特征提取
Step 5: 总结分析结果
```

### 9.4 日志链解读

LLM 交互日志以 `[LLM]` 和 `[TOOL]` 标记输出到 `logs/tianxuan.log`：

```
[LLM] model=deepseek-v4-flash           # 模型名
[LLM] latency_ms=3245                    # LLM 响应延迟
[LLM REQUEST] ...                        # 请求内容（缩减后）
[LLM RESPONSE] ...                       # 响应状态
[TOOL] profile_data                      # LLM 调用的工具
[TOOL] build_entity_profiles
[TOOL] run_clustering
[TOOL] extract_features
```

### 9.5 故障排查

| 问题 | 常见原因 | 排查方法 |
|------|---------|---------|
| 400 错误 | API 地址错误 / Key 无效 | 配置页「测试连通性」 |
| 超时 | 32B 推理慢 / 网络延迟 | 检查 `latency_ms`；增大 `max_tokens` |
| 工具调用失败 | LLM 返回格式错误 | 检查 `[TOOL]` 日志看哪个工具调用失败 |
| 分析结果为空 | 实体检测失败 | 检查实体列是否正确，尝试手动指定 |

**连通性测试**：

```bash
# CLI 测试
runtime\python\python.exe scripts\test_llm_full.py

# Web 测试
# 导航到「配置」页面 → 填写 LLM 配置 → 点击「测试连通性」
```

---

## 11. 跨机部署

### 10.1 运行时隔离验证

**脚本**：`scripts/verify_runtime.py`

在不同机器上运行该脚本，输出应完全一致：

```bash
# 每台机器各自执行
runtime\python\python.exe scripts\verify_runtime.py
```

验证内容：

| 检查项 | 期望值 |
|--------|--------|
| Python 版本 | 3.12.x |
| ENABLE_USER_SITE | False（隔离） |
| django | 4.2.x |
| polars | 1.42.x |
| sklearn | 1.5.x |
| numpy | 1.26.x |
| defaultencoding | utf-8 |
| filesystemencoding | utf-8 |

### 10.2 diagnose_compare 使用

**脚本**：`scripts/diagnose_compare.py`

对比两台机器的运行日志，定位第一个差异点：

```bash
# 每台机器运行管道，生成日志
del logs\tianxuan.log
runtime\python\python.exe manage.py run_pipeline data\complex_test.csv
copy logs\tianxuan.log log_机器A.txt

# 在开发机上对比
runtime\python\python.exe scripts\diagnose_compare.py log_开发机.txt log_机器A.txt
```

输出示例：

```
[DIFF] 首个差异在行 123:
--- 机器A ---
[LOAD_SCHEMA] col=duration dtype=Utf8 sample=1.2s,0.5s,3.1s
--- 机器B ---
[LOAD_SCHEMA] col=duration dtype=Float64 sample=1.2,0.5,3.1
```

首个 `[DIFF]` 行就是问题根源。

### 10.3 关键日志标记

| 标记 | 对应文件 | 含义 |
|------|---------|------|
| `[LOAD]` | `data_loader.py` | CSV 加载完成 |
| `[LOAD_SCHEMA]` | `data_loader.py` | 每列的推断类型（最重要！） |
| `[CLEAN]` | `data_loader.py` | 清洗操作记录 |
| `[VALIDATE]` | `data_validator.py` | 数据校验结果 |
| `[FIND_COL]` | `entity_aggregator.py` | 关键词匹配结果 |
| `[CLUSTER_INPUT]` | `tool_registry.py` | 聚类输入形状 |
| `[GEOIP]` | `geoip.py` | GeoIP 数据加载 |
| `[LLM]` | `llm_orchestrator.py` | LLM 调用状态 |
| `[TOOL]` | `llm_orchestrator.py` | LLM 调用的工具 |
| `[DB_SAVE_ERROR]` | `tool_registry.py` | 数据库持久化错误 |

### 10.4 跨机一致性检查清单

当两台机器表现不一致时，逐项排查：

```bash
# 1. Python 版本
runtime\python\python.exe --version

# 2. 编码设置
runtime\python\python.exe -c "
import sys; print(f'defaultenc={sys.getdefaultencoding()}')
print(f'fsenc={sys.getfilesystemencoding()}')
print(f'utf8_mode={sys.flags.utf8_mode}')
"

# 3. Polars 版本
runtime\python\python.exe -c "import polars; print(polars.__version__)"

# 4. 依赖版本
runtime\python\python.exe -c "import numpy; print(numpy.__version__)"
runtime\python\python.exe -c "import sklearn; print(sklearn.__version__)"
```

### 10.5 故障诊断流程

```
出现跨机差异
    │
    ├─ Step 1: 收集两台机器的 logs/tianxuan.log
    │
    ├─ Step 2: 运行 diagnose_compare.py 对比日志
    │         └─ 找到首个 [DIFF]
    │
    ├─ Step 3: 判断差异类型
    │   ├─ [LOAD_SCHEMA] 列类型不同
    │   │   └─ 检查 Polars 版本一致、PYTHONUTF8 生效
    │   │
    │   ├─ [FIND_COL] 关键词匹配不同
    │   │   └─ 确认 frozenset→tuple 修复已应用（消除哈希随机性）
    │   │
    │   └─ [CLUSTER_INPUT] 聚类结果不同
    │       └─ 检查 random_state=42、StandardScaler 一致性
    │
    ├─ Step 4: 应急修复
    │   ├─ 手动指定实体列：--entity-col src_ip
    │   ├─ 强制编码：set PYTHONUTF8=1
    │   └─ 使用 tuple 替代 frozenset（已在 entity_aggregator.py 中修复）
    │
    └─ Step 5: 归档诊断信息
        (  echo === 天璇故障诊断报告 ===
           date /t & time /t
           runtime\python\python.exe --version
           type logs\tianxuan.log
        ) > 诊断报告_%COMPUTERNAME%.txt
```

### 10.6 已知跨机问题（已修复）

| 问题 | 根因 | 修复 |
|------|------|------|
| "mean on str" 错误 | frozenset 哈希随机性 | frozenset → tuple 确定性迭代 |
| "ZZZ不在{XXX,YYY}中" 错误 | Python 编码不一致 | `PYTHONUTF8=1` 强制 UTF-8 |
| DB 特征不保存 | Django ORM 在 async 上下文中的限制 | `sync_to_async` 包装 |
| 图表不显示 | Chart.js 从未下载 | 改用纯 Canvas 2D API |

---

> **更多技术细节**：请参阅 `docs/故障诊断手册.md` 获取跨机问题深度排查指南。
