Files
tianxuan/docs/工作流.md
T

819 lines
27 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 天璇 (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` | counthex 对数统计) |
| `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 合并为一个实体节点。
支持 /240-255)和 /28240-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` 获取跨机问题深度排查指南。