设计文档:重构分析报告功能
概述
重构 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() 重写,渲染四个板块:
- 库存概览板块: 统计卡片(总数量、总金额、库销比)+ 五项构成明细表(在库未锁/在途/计划数/主动调拨在途/自动调拨在途)+ LLM 分析文本
- 销量分析板块: 统计卡片(月均销量、总金额)+ 构成明细表 + LLM 分析文本
- 健康度板块: 统计卡片 + Chart.js 环形图(数量占比 + 金额占比)+ LLM 分析文本
- 补货建议板块: 优先级统计表 + 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)
- 数据库写入和读取一致性
- 前端渲染(手动验证)