design.md 21.1 KB

设计文档:重构分析报告功能

概述

重构 AI 补货建议系统的分析报告功能,将现有的四模块宏观决策报告(整体态势研判/风险预警/采购策略/效果预期)替换为四大数据驱动板块(库存总体概览/销量分析/库存构成健康度/补货建议生成情况)。每个板块包含精确的统计数据和 LLM 生成的分析文本。

核心设计变更:

  • 从"LLM 主导分析"转变为"数据统计 + LLM 辅助分析"模式
  • 使用 LangGraph 动态节点并发生成四个板块的 LLM 分析
  • 前端新增 Chart.js 图表支持库存健康度可视化
  • 数据库表结构完全重建,四个 JSON 字段分别存储各板块数据

架构

整体数据流

graph TD
    A[allocate_budget 节点完成] --> B[generate_analysis_report 节点]
    B --> C[统计计算阶段]
    C --> C1[库存概览统计]
    C --> C2[销量分析统计]
    C --> C3[健康度统计]
    C --> C4[补货建议统计]
    C1 & C2 & C3 & C4 --> D[LangGraph 并发 LLM 分析]
    D --> D1[库存概览 LLM 节点]
    D --> D2[销量分析 LLM 节点]
    D --> D3[健康度 LLM 节点]
    D --> D4[补货建议 LLM 节点]
    D1 & D2 & D3 & D4 --> E[汇总报告]
    E --> F[Result_Writer 写入数据库]
    F --> G[API 返回前端]
    G --> H[Report_UI 渲染]

LangGraph 并发子图设计

在现有工作流 allocate_budget → generate_analysis_report → END 中,generate_analysis_report 节点内部使用一个 LangGraph 子图实现并发:

graph TD
    START[统计计算完成] --> FORK{并发分发}
    FORK --> N1[inventory_overview_llm]
    FORK --> N2[sales_analysis_llm]
    FORK --> N3[inventory_health_llm]
    FORK --> N4[replenishment_summary_llm]
    N1 & N2 & N3 & N4 --> JOIN[汇总合并]
    JOIN --> END_NODE[返回完整报告]

实现方式:使用 langgraph.graph.StateGraph 构建子图,通过 add_node 添加四个并发 LLM 节点,使用 fan-out/fan-in 模式(从 START 分发到四个节点,四个节点汇聚到 END)。每个节点独立调用 LLM,失败不影响其他节点。

组件与接口

1. 统计计算模块 (analysis_report_node.py)

calculate_inventory_overview(part_ratios: list[dict]) -> dict

计算库存总体概览统计数据。有效库存使用 valid_storage_cnt(三项公式:in_stock_unlocked_cnt + on_the_way_cnt + has_plan_cnt)。

输入:PartRatio 字典列表 输出:

{
    "total_valid_storage_cnt": Decimal,      # 有效库存总数量(五项之和)
    "total_valid_storage_amount": Decimal,   # 有效库存总金额(资金占用)
    "total_in_stock_unlocked_cnt": Decimal,  # 在库未锁总数量
    "total_in_stock_unlocked_amount": Decimal,
    "total_on_the_way_cnt": Decimal,         # 在途总数量
    "total_on_the_way_amount": Decimal,
    "total_has_plan_cnt": Decimal,           # 计划数总数量
    "total_has_plan_amount": Decimal,
    "total_transfer_cnt": Decimal,           # 主动调拨在途总数量
    "total_transfer_amount": Decimal,
    "total_gen_transfer_cnt": Decimal,       # 自动调拨在途总数量
    "total_gen_transfer_amount": Decimal,
    "total_avg_sales_cnt": Decimal,          # 月均销量总数量
    "overall_ratio": Decimal,                # 整体库销比
    "part_count": int,                       # 配件总种类数
}

calculate_sales_analysis(part_ratios: list[dict]) -> dict

计算销量分析统计数据。

输入:PartRatio 字典列表 输出:

{
    "total_avg_sales_cnt": Decimal,          # 月均销量总数量
    "total_avg_sales_amount": Decimal,       # 月均销量总金额
    "total_out_stock_cnt": Decimal,          # 90天出库数总量
    "total_storage_locked_cnt": Decimal,     # 未关单已锁总量
    "total_out_stock_ongoing_cnt": Decimal,  # 未关单出库总量
    "total_buy_cnt": int,                    # 订件总量
    "has_sales_part_count": int,             # 有销量配件数
    "no_sales_part_count": int,              # 无销量配件数
}

calculate_inventory_health(part_ratios: list[dict]) -> dict

计算库存构成健康度统计数据。

输入:PartRatio 字典列表 输出:

{
    "shortage": {"count": int, "amount": Decimal, "count_pct": float, "amount_pct": float},
    "stagnant": {"count": int, "amount": Decimal, "count_pct": float, "amount_pct": float},
    "low_freq": {"count": int, "amount": Decimal, "count_pct": float, "amount_pct": float},
    "normal":   {"count": int, "amount": Decimal, "count_pct": float, "amount_pct": float},
    "total_count": int,
    "total_amount": Decimal,
}

calculate_replenishment_summary(part_results: list) -> dict

计算补货建议生成情况统计数据。

输入:part_results 列表(配件汇总结果) 输出:

{
    "urgent":    {"count": int, "amount": Decimal},  # 急需补货 (priority=1)
    "suggested": {"count": int, "amount": Decimal},  # 建议补货 (priority=2)
    "optional":  {"count": int, "amount": Decimal},  # 可选补货 (priority=3)
    "total_count": int,
    "total_amount": Decimal,
}

2. LLM 分析节点

四个独立的 LLM 分析函数,每个接收对应板块的统计数据,调用 LLM 生成分析文本:

  • llm_analyze_inventory_overview(stats: dict) -> str — 返回 JSON 字符串
  • llm_analyze_sales(stats: dict) -> str — 返回 JSON 字符串
  • llm_analyze_inventory_health(stats: dict) -> str — 返回 JSON 字符串
  • llm_analyze_replenishment_summary(stats: dict) -> str — 返回 JSON 字符串

每个函数加载对应的提示词模板(统一放在 prompts/analysis_report.md 中,按板块分段),填充统计数据,调用 LLM,解析 JSON 响应。

3. 提示词文件

拆分为四个独立提示词文件,每个板块一个,确保分析专业且有实际决策价值:

prompts/report_inventory_overview.md — 库存概览分析

角色:资深汽车配件库存管理专家。输入统计数据后,要求 LLM 分析:

  • 资金占用评估:总库存金额是否合理,各构成部分(在库未锁/在途/计划数/主动调拨在途/自动调拨在途)的资金分配比例是否健康
  • 库销比诊断:当前整体库销比处于什么水平(3 严重积压),对比行业经验给出判断
  • 库存结构建议:基于五项构成的比例,给出具体的库存结构优化方向

输出 JSON 格式:

{
    "capital_assessment": {
        "total_evaluation": "总资金占用评估",
        "structure_ratio": "各构成部分的资金比例分析",
        "risk_level": "high/medium/low"
    },
    "ratio_diagnosis": {
        "level": "不足/合理/偏高/严重积压",
        "analysis": "库销比具体分析",
        "benchmark": "行业参考值对比"
    },
    "recommendations": ["具体建议1", "具体建议2"]
}

prompts/report_sales_analysis.md — 销量分析

角色:汽车配件销售数据分析师。输入统计数据后,要求 LLM 分析:

  • 销量构成解读:90天出库数占比最大说明正常销售为主,未关单已锁/出库占比高说明有大量待处理订单,订件占比高说明客户预订需求旺盛
  • 销售活跃度:有销量 vs 无销量配件的比例反映 SKU 活跃度,无销量占比过高说明 SKU 管理需要优化
  • 需求趋势判断:基于各组成部分的比例关系,判断当前需求是稳定、上升还是下降趋势

输出 JSON 格式:

{
    "composition_analysis": {
        "main_driver": "主要销量来源分析",
        "pending_orders_impact": "未关单对销量的影响",
        "booking_trend": "订件趋势分析"
    },
    "activity_assessment": {
        "active_ratio": "活跃SKU占比评估",
        "optimization_suggestion": "SKU优化建议"
    },
    "demand_trend": {
        "direction": "上升/稳定/下降",
        "evidence": "判断依据",
        "forecast": "短期需求预测"
    }
}

prompts/report_inventory_health.md — 库存健康度分析

角色:汽车配件库存健康度诊断专家。输入统计数据后,要求 LLM 分析:

  • 健康度评分:基于正常件占比给出整体健康度评分(正常件>70%为健康,50-70%为亚健康,<50%为不健康)
  • 问题诊断:呆滞件占比高说明采购决策需要优化,缺货件占比高说明补货不及时,低频件占比高说明 SKU 精简空间大
  • 资金释放机会:呆滞件和低频件占用的资金可以通过促销、退货等方式释放,给出具体金额估算
  • 改善优先级:按影响程度排序,给出最应优先处理的问题类型

输出 JSON 格式:

{
    "health_score": {
        "score": "健康/亚健康/不健康",
        "normal_ratio_evaluation": "正常件占比评估"
    },
    "problem_diagnosis": {
        "stagnant_analysis": "呆滞件问题分析及原因",
        "shortage_analysis": "缺货件问题分析及影响",
        "low_freq_analysis": "低频件问题分析及建议"
    },
    "capital_release": {
        "stagnant_releasable": "呆滞件可释放资金估算",
        "low_freq_releasable": "低频件可释放资金估算",
        "action_plan": "资金释放行动方案"
    },
    "priority_actions": ["最优先处理事项1", "最优先处理事项2"]
}

prompts/report_replenishment_summary.md — 补货建议分析

角色:汽车配件采购策略顾问。输入统计数据后,要求 LLM 分析:

  • 紧迫度评估:急需补货占比反映当前缺货风险程度,急需占比>30%说明库存管理存在较大问题
  • 资金分配建议:基于各优先级的金额分布,给出资金分配的先后顺序和比例建议
  • 执行节奏建议:急需补货应立即执行,建议补货可在1-2周内完成,可选补货可根据资金情况灵活安排
  • 风险提示:如果可选补货金额占比过高,提示可能存在过度补货风险

输出 JSON 格式:

{
    "urgency_assessment": {
        "urgent_ratio_evaluation": "急需补货占比评估",
        "risk_level": "high/medium/low",
        "immediate_action_needed": true/false
    },
    "budget_allocation": {
        "recommended_order": "建议资金分配顺序",
        "urgent_budget": "急需补货建议预算",
        "suggested_budget": "建议补货建议预算",
        "optional_budget": "可选补货建议预算"
    },
    "execution_plan": {
        "urgent_timeline": "急需补货执行时间建议",
        "suggested_timeline": "建议补货执行时间建议",
        "optional_timeline": "可选补货执行时间建议"
    },
    "risk_warnings": ["风险提示1", "风险提示2"]
}

4. API 接口 (tasks.py)

更新 GET /api/tasks/{task_no}/analysis-report 端点:

响应模型 AnalysisReportResponse

class AnalysisReportResponse(BaseModel):
    id: int
    task_no: str
    group_id: int
    dealer_grouping_id: int
    dealer_grouping_name: Optional[str]
    report_type: str

    inventory_overview: Optional[Dict[str, Any]]    # 库存概览(统计+分析)
    sales_analysis: Optional[Dict[str, Any]]        # 销量分析(统计+分析)
    inventory_health: Optional[Dict[str, Any]]      # 健康度(统计+分析+图表数据)
    replenishment_summary: Optional[Dict[str, Any]] # 补货建议(统计+分析)

    llm_provider: Optional[str]
    llm_model: Optional[str]
    llm_tokens: int
    execution_time_ms: int
    statistics_date: Optional[str]
    create_time: Optional[str]

5. 前端渲染 (app.js)

renderReportTab() 重写,渲染四个板块:

  1. 库存概览板块: 统计卡片(总数量、总金额、库销比)+ 五项构成明细表(在库未锁/在途/计划数/主动调拨在途/自动调拨在途)+ LLM 分析文本
  2. 销量分析板块: 统计卡片(月均销量、总金额)+ 构成明细表 + LLM 分析文本
  3. 健康度板块: 统计卡片 + Chart.js 环形图(数量占比 + 金额占比)+ LLM 分析文本
  4. 补货建议板块: 优先级统计表 + LLM 分析文本

6. Chart.js 集成

ui/index.html 中通过 CDN 引入 Chart.js:

<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.min.js"></script>

健康度板块使用两个环形图(Doughnut Chart):

  • 数量占比图:缺货/呆滞/低频/正常 四类的数量百分比
  • 金额占比图:缺货/呆滞/低频/正常 四类的金额百分比

数据模型

数据库表 (ai_analysis_report)

DROP TABLE IF EXISTS ai_analysis_report;
CREATE TABLE ai_analysis_report (
    id                   BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
    task_no              VARCHAR(32) NOT NULL COMMENT '任务编号',
    group_id             BIGINT NOT NULL COMMENT '集团ID',
    dealer_grouping_id   BIGINT NOT NULL COMMENT '商家组合ID',
    dealer_grouping_name VARCHAR(128) COMMENT '商家组合名称',
    brand_grouping_id    BIGINT COMMENT '品牌组合ID',
    report_type          VARCHAR(32) DEFAULT 'replenishment' COMMENT '报告类型',

    -- 四大板块 (JSON 结构化存储,每个字段包含 stats + llm_analysis)
    inventory_overview    JSON COMMENT '库存总体概览(统计数据+LLM分析)',
    sales_analysis        JSON COMMENT '销量分析(统计数据+LLM分析)',
    inventory_health      JSON COMMENT '库存构成健康度(统计数据+图表数据+LLM分析)',
    replenishment_summary JSON COMMENT '补货建议生成情况(统计数据+LLM分析)',

    -- LLM 元数据
    llm_provider         VARCHAR(32) COMMENT 'LLM提供商',
    llm_model            VARCHAR(64) COMMENT 'LLM模型名称',
    llm_tokens           INT DEFAULT 0 COMMENT 'LLM Token消耗',
    execution_time_ms    INT DEFAULT 0 COMMENT '执行耗时(毫秒)',

    statistics_date      VARCHAR(16) COMMENT '统计日期',
    create_time          DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',

    INDEX idx_task_no (task_no),
    INDEX idx_group_date (group_id, statistics_date),
    INDEX idx_dealer_grouping (dealer_grouping_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='AI补货建议分析报告表-重构版';

Python 数据模型 (AnalysisReport)

@dataclass
class AnalysisReport:
    task_no: str
    group_id: int
    dealer_grouping_id: int

    id: Optional[int] = None
    dealer_grouping_name: Optional[str] = None
    brand_grouping_id: Optional[int] = None
    report_type: str = "replenishment"

    # 四大板块
    inventory_overview: Optional[Dict[str, Any]] = None
    sales_analysis: Optional[Dict[str, Any]] = None
    inventory_health: Optional[Dict[str, Any]] = None
    replenishment_summary: Optional[Dict[str, Any]] = None

    # LLM 元数据
    llm_provider: str = ""
    llm_model: str = ""
    llm_tokens: int = 0
    execution_time_ms: int = 0

    statistics_date: str = ""
    create_time: Optional[datetime] = None

每个板块的 JSON 数据结构

每个板块的 JSON 包含 stats(统计数据)和 llm_analysis(LLM 分析文本)两部分:

# inventory_overview 示例
{
    "stats": {
        "total_valid_storage_cnt": 12500,
        "total_valid_storage_amount": 3250000.00,
        "total_in_stock_unlocked_cnt": 8000,
        "total_in_stock_unlocked_amount": 2080000.00,
        "total_on_the_way_cnt": 2500,
        "total_on_the_way_amount": 650000.00,
        "total_has_plan_cnt": 1000,
        "total_has_plan_amount": 260000.00,
        "total_transfer_cnt": 600,
        "total_transfer_amount": 156000.00,
        "total_gen_transfer_cnt": 400,
        "total_gen_transfer_amount": 104000.00,
        "total_avg_sales_cnt": 5000,
        "overall_ratio": 2.5,
        "part_count": 800
    },
    "llm_analysis": { ... }  # LLM 返回的 JSON 分析对象
}

# inventory_health 示例(额外包含 chart_data)
{
    "stats": {
        "shortage": {"count": 50, "amount": 125000, "count_pct": 6.25, "amount_pct": 3.85},
        "stagnant": {"count": 120, "amount": 480000, "count_pct": 15.0, "amount_pct": 14.77},
        "low_freq": {"count": 200, "amount": 300000, "count_pct": 25.0, "amount_pct": 9.23},
        "normal":   {"count": 430, "amount": 2345000, "count_pct": 53.75, "amount_pct": 72.15},
        "total_count": 800,
        "total_amount": 3250000
    },
    "chart_data": {
        "labels": ["缺货件", "呆滞件", "低频件", "正常件"],
        "count_values": [50, 120, 200, 430],
        "amount_values": [125000, 480000, 300000, 2345000]
    },
    "llm_analysis": { ... }
}

正确性属性

正确性属性是系统在所有合法执行中都应保持为真的特征或行为——本质上是关于系统应该做什么的形式化陈述。属性是人类可读规范与机器可验证正确性保证之间的桥梁。

Property 1: 库存概览统计一致性

对于任意 PartRatio 字典列表,calculate_inventory_overview 的输出应满足:

  • total_valid_storage_cnt = 所有配件 (in_stock_unlocked_cnt + on_the_way_cnt + has_plan_cnt) 之和
  • total_valid_storage_amount = 所有配件 (in_stock_unlocked_cnt + on_the_way_cnt + has_plan_cnt) × cost_price 之和
  • total_in_stock_unlocked_cnt + total_on_the_way_cnt + total_has_plan_cnt = total_valid_storage_cnt(构成不变量)
  • total_avg_sales_cnt > 0 时,overall_ratio = total_valid_storage_cnt / total_avg_sales_cnt

Validates: Requirements 2.1, 2.2, 2.3

Property 2: 销量分析统计一致性

对于任意 PartRatio 字典列表,calculate_sales_analysis 的输出应满足:

  • total_avg_sales_cnt = 所有配件 (out_stock_cnt + storage_locked_cnt + out_stock_ongoing_cnt + buy_cnt) / 3 之和
  • (total_out_stock_cnt + total_storage_locked_cnt + total_out_stock_ongoing_cnt + total_buy_cnt) / 3 = total_avg_sales_cnt(构成不变量)
  • has_sales_part_count + no_sales_part_count = 总配件数

Validates: Requirements 3.1, 3.2, 3.4

Property 3: 健康度分类完备性与一致性

对于任意 PartRatio 字典列表,calculate_inventory_health 的输出应满足:

  • shortage.count + stagnant.count + low_freq.count + normal.count = total_count(分类完备)
  • shortage.amount + stagnant.amount + low_freq.amount + normal.amount = total_amount(金额守恒)
  • 每种类型的 count_pct = count / total_count × 100
  • 所有 count_pct 之和 ≈ 100.0(浮点精度容差内)

Validates: Requirements 4.1, 4.2

Property 4: 补货建议统计一致性

对于任意 part_results 列表,calculate_replenishment_summary 的输出应满足:

  • urgent.count + suggested.count + optional.count = total_count
  • urgent.amount + suggested.amount + optional.amount = total_amount

Validates: Requirements 5.1, 5.2

Property 5: 报告数据模型序列化 round-trip

对于任意 合法的 AnalysisReport 对象,调用 to_dict() 后再用返回的字典构造新的 AnalysisReport 对象,两个对象的核心字段应相等。

Validates: Requirements 10.2, 10.3

错误处理

错误场景 处理方式
LLM 单板块调用失败 该板块 llm_analysis 置为 {"error": "错误信息"},其他板块正常生成
LLM 返回非法 JSON 记录原始响应到日志,llm_analysis 置为 {"error": "JSON解析失败", "raw": "原始文本前200字符"}
PartRatio 列表为空 所有统计值为 0,LLM 分析说明无数据
part_results 列表为空 补货建议板块统计值为 0,LLM 分析说明无补货建议
数据库写入失败 记录错误日志,不中断主工作流,返回包含错误信息的报告
提示词文件缺失 抛出 FileNotFoundError,由上层节点捕获并记录

测试策略

属性测试 (Property-Based Testing)

使用 hypothesis 库进行属性测试,每个属性至少运行 100 次迭代。

  • Property 1: 生成随机 PartRatio 字典列表(随机数量、随机字段值),验证库存概览统计的不变量
    • Tag: Feature: refactor-analysis-report, Property 1: 库存概览统计一致性
  • Property 2: 生成随机 PartRatio 字典列表,验证销量分析统计的不变量
    • Tag: Feature: refactor-analysis-report, Property 2: 销量分析统计一致性
  • Property 3: 生成随机 PartRatio 字典列表,验证健康度分类的完备性和一致性
    • Tag: Feature: refactor-analysis-report, Property 3: 健康度分类完备性与一致性
  • Property 4: 生成随机 part_results 列表(随机优先级和金额),验证补货建议统计的一致性
    • Tag: Feature: refactor-analysis-report, Property 4: 补货建议统计一致性
  • Property 5: 生成随机 AnalysisReport 对象,验证 to_dict round-trip
    • Tag: Feature: refactor-analysis-report, Property 5: 报告数据模型序列化 round-trip

单元测试

  • 边界情况:空列表输入、月均销量为零、所有配件同一类型
  • LLM 响应解析:合法 JSON、非法 JSON、空响应
  • API 端点:有报告数据、无报告数据

集成测试

  • 完整报告生成流程(mock LLM)
  • 数据库写入和读取一致性
  • 前端渲染(手动验证)