# 设计文档:重构分析报告功能 ## 概述 重构 AI 补货建议系统的分析报告功能,将现有的四模块宏观决策报告(整体态势研判/风险预警/采购策略/效果预期)替换为四大数据驱动板块(库存总体概览/销量分析/库存构成健康度/补货建议生成情况)。每个板块包含精确的统计数据和 LLM 生成的分析文本。 核心设计变更: - 从"LLM 主导分析"转变为"数据统计 + LLM 辅助分析"模式 - 使用 LangGraph 动态节点并发生成四个板块的 LLM 分析 - 前端新增 Chart.js 图表支持库存健康度可视化 - 数据库表结构完全重建,四个 JSON 字段分别存储各板块数据 ## 架构 ### 整体数据流 ```mermaid 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 子图实现并发: ```mermaid 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 字典列表 输出: ```python { "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 字典列表 输出: ```python { "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 字典列表 输出: ```python { "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 列表(配件汇总结果) 输出: ```python { "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 分析: - **资金占用评估**:总库存金额是否合理,各构成部分(在库未锁/在途/计划数/主动调拨在途/自动调拨在途)的资金分配比例是否健康 - **库销比诊断**:当前整体库销比处于什么水平(<1 库存不足,1-2 合理,2-3 偏高,>3 严重积压),对比行业经验给出判断 - **库存结构建议**:基于五项构成的比例,给出具体的库存结构优化方向 输出 JSON 格式: ```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 格式: ```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 格式: ```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 格式: ```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`: ```python 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: ```html ``` 健康度板块使用两个环形图(Doughnut Chart): - 数量占比图:缺货/呆滞/低频/正常 四类的数量百分比 - 金额占比图:缺货/呆滞/低频/正常 四类的金额百分比 ## 数据模型 ### 数据库表 (ai_analysis_report) ```sql 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) ```python @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 分析文本)两部分: ```python # 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) - 数据库写入和读取一致性 - 前端渲染(手动验证)