diff --git a/src/services/llm_service.py b/src/services/llm_service.py index 032d024..d842f8f 100644 --- a/src/services/llm_service.py +++ b/src/services/llm_service.py @@ -26,13 +26,44 @@ _DESIGN_REPORT_SYSTEM = """你是一位资深注塑模具设计工程师,拥 要求: 1. 使用中文 -2. 按 "问题摘要 → 关键风险 → 分模方案推荐 → 制造可行性 → 修改建议" 结构组织 +2. 按 "关键问题 → 工艺参数建议 → 改进建议" 结构组织 3. 技术术语准确(如:锁模力、投影面积、分型面、滑块、斜顶、拔模角、缩痕、熔接痕) -4. 每个建议标注优先级(高/中/低)和预计工时 -5. 报告末尾给出一个总体评分(1-10分) -6. 如果数据不足以判断某项,明确标注"数据不足,需人工确认" +4. 每个问题标注优先级(high / medium / low) +5. 如果数据不足以判断某项,明确标注"数据不足,需人工确认" -直接输出 Markdown 格式报告,不要输出 JSON。""" +严格输出 JSON,不要输出其他内容。JSON 格式: +{ + "title": "模具设计评审报告", + "overview": "一段 1-2 句话的整体概述", + "sections": [ + { + "heading": "关键问题", + "type": "issues", + "items": [ + {"level": "high", "content": "拔模角不足,建议增加到 2° 以上"}, + {"level": "medium", "content": "壁厚偏差较大,可能产生缩痕"} + ] + }, + { + "heading": "工艺参数建议", + "type": "params_table", + "headers": ["参数", "推荐值", "说明"], + "rows": [ + ["锁模力", "150 吨", "基于投影面积计算"], + ["注塑温度", "230°C", "ABS 材料推荐值"] + ] + }, + { + "heading": "改进建议", + "type": "recommendations", + "items": [ + "建议将主流道直径从 4mm 增加到 6mm", + "建议在所有垂直面增加 1-2° 拔模角" + ] + } + ], + "overall_score": 7.5 +}""" _DESIGN_REPORT_USER = """请根据以下模具分析数据生成评审报告: @@ -62,7 +93,12 @@ _DESIGN_REPORT_USER = """请根据以下模具分析数据生成评审报告: - 收缩率:{shrinkage_rate} ## 原始设计建议 -{recommendations}""" +{recommendations} + +请生成 JSON 格式评审报告。issues 部分不要超过 8 条,每条内容简洁在一行内; +params_table 至少要包含锁模力、成型周期、模仁材料、推荐型腔数 4 行; +如果某项数据标记为"自动计算"或"自动选择",请在说明中注明"需人工确认"; +overall_score 范围 1-10。""" _SIDE_ACTION_ANALYSIS_SYSTEM = """你是一位资深注塑模具结构工程师。 请根据提供的 STP 分析结果,判断当前产品是否需要倒扣/抽芯机构,并输出标准化结论。 @@ -166,17 +202,25 @@ class LLMService: self, analysis_result: Dict[str, Any], detailed_cavity_json: Optional[Dict[str, Any]] = None, - ) -> Optional[str]: - """生成模具设计评审报告 (Markdown)""" + ) -> Optional[Dict[str, Any]]: + """生成模具设计评审报告 (结构化 JSON)""" if not self._enabled: return None try: prompt = self._build_design_report_prompt(analysis_result, detailed_cavity_json) - response = await self._chat(_DESIGN_REPORT_SYSTEM, prompt, self._max_tokens) - if response: - logger.info("LLM 设计报告生成成功 (%d 字符)", len(response)) - return response + response = await self._chat( + _DESIGN_REPORT_SYSTEM, + prompt, + self._max_tokens, + expect_json=True, + ) + if not response: + return None + result = self._parse_json_response(response) + if result: + logger.info("LLM 设计报告生成成功 (%d sections)", len(result.get("sections", []))) + return result except Exception as e: logger.warning("LLM 设计报告生成失败(不影响主流程): %s", e) return None @@ -214,10 +258,14 @@ class LLMService: @staticmethod def compose_llm_report( - design_report: Optional[str], + design_report: Optional[Dict[str, Any]], side_action_analysis: Optional[Dict[str, Any]], ) -> Optional[str]: - """将结构化倒扣分析打包进既有 llm_report 字段,避免改动外部协议。""" + """将结构化报告和倒扣分析打包进 llm_report 字段,避免改动外部协议。 + + 设计报告以 / 包裹的 JSON 嵌入, + 倒扣分析以 / 包裹的 JSON 嵌入。 + """ sections: List[str] = [] if side_action_analysis: payload = json.dumps(side_action_analysis, ensure_ascii=False) @@ -226,8 +274,13 @@ class LLMService: f"{payload}\n" "" ) - if design_report and design_report.strip(): - sections.append(design_report.strip()) + if design_report: + payload = json.dumps(design_report, ensure_ascii=False) + sections.append( + "\n" + f"{payload}\n" + "" + ) merged = "\n\n".join(sections).strip() return merged or None diff --git a/static/vue-app.js b/static/vue-app.js index 16af0be..8ab56d0 100644 --- a/static/vue-app.js +++ b/static/vue-app.js @@ -1526,6 +1526,17 @@ const ResultView = { return null; } }; + const parseEmbeddedDesignReport = (report) => { + const source = String(report || ''); + const match = source.match(/\s*([\s\S]*?)\s*/); + if (!match) return null; + try { + return JSON.parse(match[1]); + } catch (error) { + console.warn('LLM 设计报告解析失败:', error); + return null; + } + }; const stripEmbeddedSideActionAi = (report) => String(report || '') .replace(/\s*[\s\S]*?\s*/, '') .trim(); @@ -1625,9 +1636,21 @@ const ResultView = { 'ai' ) || fallbackSideActionAiAdvice.value; }); - const cleanedLlmReport = computed(() => stripEmbeddedSideActionAi(state.task?.llm_report || '')); - const hasVisibleLlmReport = computed(() => Boolean(cleanedLlmReport.value)); - const llmReportHtml = computed(() => renderMarkdownToHtml(cleanedLlmReport.value)); + const designReport = computed(() => + parseEmbeddedDesignReport(state.task?.llm_report || '') + ); + const hasVisibleDesignReport = computed(() => + designReport.value && (designReport.value.sections?.length > 0 || designReport.value.overview) + ); + const designReportSections = computed(() => { + const sections = designReport.value?.sections || []; + return sections.filter(s => { + if (s.type === 'issues') return s.items?.length > 0; + if (s.type === 'params_table') return s.rows?.length > 0; + if (s.type === 'recommendations') return s.items?.length > 0; + return true; + }); + }); const stageTimingEntries = computed(() => { const timings = state.task?.stage_timings || {}; return Object.entries(timings) @@ -1907,7 +1930,9 @@ const ResultView = { sideActionAiAdvice, hasVisibleLlmReport, dfmLevelSummary, - llmReportHtml, + designReport, + hasVisibleDesignReport, + designReportSections, stageTimingEntries, getWallThicknessSummary, countFeaturesByTypes, @@ -2006,10 +2031,10 @@ const ResultView = {
| {{ h }} | +
|---|
| {{ cell }} | +