Files
tianxuan/docs/故障诊断手册.md
T

8.7 KiB

天璇跨机故障诊断手册

问题描述

同一份项目压缩包、同一份 CSV 数据(SHA256 一致)、同一份 runtime/python/ 运行时, 在不同 Windows 10 x64 机器上出现不同行为:

设备 表现
你的机器 正常运行
第二台 "尝试对str类型求mean"
第三台 "ZZZ不在{XXX,YYY}中" + 字符串相关错误
(可能的更多) 其他奇怪错误

1. 立即诊断流程

步骤 A:收集两台机器的日志

在两台问题机器上各执行一次:

# 1. 清理旧日志
del logs\tianxuan.log 2>nul

# 2. 生成测试数据(确保 seed 一致)
runtime\python\python.exe scripts\gen_complex_test.py --rows 5000 --output data\complex_test.csv

# 3. 运行完整管道
runtime\python\python.exe manage.py run_pipeline data\complex_test.csv --algo hdbscan

# 4. 复制日志到桌面备用
copy logs\tianxuan.log %USERPROFILE%\Desktop\log_机器A.txt

你的机器上也执行一次,生成 log_你的机器.txt

步骤 B:对比日志

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] 就是导致问题的根源。


2. 关键日志标记解读

日志中所有 [TAG] 标记的含义:

标记 对应文件 含义
[LOAD] data_loader.py CSV 加载完成,文件数和预估行数
[LOAD_SCHEMA] data_loader.py 每列的推断类型(最重要!)
[FIND_COL] entity_aggregator.py 关键词匹配结果,显示匹配到的列名和类型
[AGGR_EXPR] entity_aggregator.py 聚合表达式
[CLUSTER_INPUT] tool_registry.py 聚类输入数据形状
[LLM REQUEST] llm_orchestrator.py LLM 请求内容
[LLM RESPONSE] llm_orchestrator.py LLM 响应状态
[TOOL CALL] llm_orchestrator.py LLM 调用的工具
[TOOL RESULT] llm_orchestrator.py 工具执行结果

3. "尝试对str类型求mean" 故障排查

根因树

"尝试对str类型求mean" 报错
├── 调用链: build_aggregation_params → pl.mean(col_name)
│
├── [可能A] 列名关键词匹配到了错误的列
│   ├── 检查 [FIND_COL] keywords=duration matched=xxx
│   ├── 如果 matched 列的类型是 Utf8/String → 匹配错误
│   └── 修复: entity_aggregator.py 中 frozenset→tuple (已改)
│
├── [可能B] CSV 列的实际类型与推断类型不同
│   ├── 检查 [LOAD_SCHEMA] col=duration dtype=xxx
│   ├── 对比两台机器的 dtype 是否一致
│   ├── 如果一致但行为不同 → 可能是 Polars 版本差异
│   └── 验证: runtime\python\python.exe -c "import polars; print(polars.__version__)"
│
├── [可能C] PYTHONUTF8 环境变量未生效
│   ├── 验证: runtime\python\python.exe -c "import sys; print(sys.getdefaultencoding())"
│   ├── 期望输出: utf-8
│   ├── 如果输出 cp936/gbk → PYTHONUTF8 没生效
│   └── 修复: run.bat 中 set PYTHONUTF8=1 (已加)
│
└── [可能D] 聚合表达式中混入了 Utf8 列
    ├── 检查 [FIND_COL] keywords=duration matched=duration dtype=Utf8
    ├── 这意味着 duration 列被 Polars 推断为字符串
    └── 修复: entity_aggregator.py 的 dtype 安全聚合 (已改)

诊断命令

# 1. 检查 Python 默认编码
runtime\python\python.exe -c "import sys; print(f'enc={sys.getdefaultencoding()} fs={sys.getfilesystemencoding()}')"

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

# 3. 检查 CSV 加载后的实际列类型
runtime\python\python.exe -c "
import polars as pl
lf = pl.scan_csv(r'data\complex_test.csv', infer_schema_length=10000)
schema = lf.collect_schema()
for name, dtype in zip(schema.names(), schema.dtypes()):
    print(f'{name}: {dtype}')
"

# 4. 检查关键词匹配结果
runtime\python\python.exe -c "
import sys; sys.path.insert(0, '.')
import os; os.environ['DJANGO_SETTINGS_MODULE']='tianxuan.settings'
import django; django.setup()
from analysis.session_store import SessionStore
from analysis.data_loader import load_csv_directory
from analysis.entity_aggregator import _find_column, _BUILT_AGGR

store = SessionStore(); store.drop_all()
lf, schema, rc, fc, mem = load_csv_directory(r'data\complex_test.csv')
for kw_name, kw_set in [
    ('bytes_sent',_BYTES_SENT_KEYWORDS),('duration',_DURATION_KEYWORDS),
    ('dst_ip',_DST_IP_KEYWORDS),('cipher',_CIPHER_KEYWORDS)]:
    col = _find_column(schema, kw_set)
    dtype = schema.get(col, 'N/A') if col else 'N/A'
    print(f'{kw_name:15s} -> {str(col):25s} dtype={dtype}')
" 2>&1

4. "ZZZ不在{XXX,YYY}中" 故障排查

根因

这个错误来自 Polars 的 str.contains()is_in() 操作。当比较字符串时, 两边的编码不一致导致匹配失败。

诊断

# 检查 Python 编码
runtime\python\python.exe -c "
import sys; print(f'default={sys.getdefaultencoding()}')
print(f'filesystem={sys.getfilesystemencoding()}')
print(f'stdin={sys.stdin.encoding}')
print(f'stdout={sys.stdout.encoding}')
"

# 检查 CSV 文件的实际编码(在 Notepad++ 或 VS Code 中查看右下角编码提示)
# 期望: UTF-8 (无 BOM)

修复尝试

# 修复 1: 确认 run.bat 包含 PYTHONUTF8=1
findstr PYTHONUTF8 run.bat
# 应输出: set PYTHONUTF8=1

# 修复 2: 手动设置编码后再运行
set PYTHONUTF8=1
runtime\python\python.exe manage.py run_pipeline data\complex_test.csv

5. 跨机环境一致性检查清单

每台机器上执行以下命令,对比输出:

echo === 1. Python 版本 ===
runtime\python\python.exe --version

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

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

echo === 4. numpy 版本 ===
runtime\python\python.exe -c "import numpy; print(numpy.__version__)"

echo === 5. sklearn 版本 ===
runtime\python\python.exe -c "import sklearn; print(sklearn.__version__)"

echo === 6. 系统区域 ===
systeminfo | findstr "区域"

6. 应急修复方案

如果 diagnose_compare.py 显示的第一个差异是 [LOAD_SCHEMA] 列类型不同:

# 强制所有列读为字符串 → 让 Polars 自己推断
runtime\python\python.exe -c "
import polars as pl
lf = pl.scan_csv(r'data\complex_test.csv', infer_schema_length=100000)
schema = lf.collect_schema()
# 打印所有列的推断类型
for n, d in zip(schema.names(), schema.dtypes()):
    print(f'{n}: {d}')
"

如果差异在 [FIND_COL] 关键词匹配不同:

# 手动指定实体列后运行
runtime\python\python.exe manage.py run_pipeline data\complex_test.csv --entity-col src_ip

如果完全无法解决:

# 重建运行时(在开发机上有网络的机器上)
# 1. 删除旧的 runtime
rmdir /s runtime

# 2. 重新运行构建脚本
scripts\build_runtime.bat

# 3. 重新压缩分发

7. 已知已修复的问题清单

问题编号 修复内容 涉及文件 验证方式
W4-#1 frozensettuple 消除哈希随机性 entity_aggregator.py 查看文件确认无 frozenset
W4-#2 PYTHONUTF8=1 强制 UTF-8 编码 run.bat findstr PYTHONUTF8 run.bat
W4-#3 sorted(keywords) 确定性关键词迭代 entity_aggregator.py 查看 _find_column 函数
W4-#4 结构化 [TAG] 日志 多文件 运行后查看 logs/tianxuan.log
DB-#1 sync_to_async 解决异步 ORM 问题 tool_registry.py 管道完成后特征写入 DB
DB-#2 value_counts() 列名兼容 data_profiler.py profile_data 工具正常返回
DB-#3 numpy corrcoef 替代 pandas data_profiler.py 无 pandas 时也能跑

8. 归档与提交诊断结果

# 收集诊断信息到单个文件,发给开发者
(
  echo === 天璇故障诊断报告 ===
  date /t
  time /t
  echo.
  echo === 环境信息 ===
  runtime\python\python.exe --version
  runtime\python\python.exe -c "import sys; print(f'enc={sys.getdefaultencoding()}')"
  runtime\python\python.exe -c "import polars; print(f'polars={polars.__version__}')"
  echo.
  echo === 日志最后 50  ===
  type logs\tianxuan.log 2>nul
) > 诊断报告.txt