D17 批 2:算法接缝(OCC payload 通道 + hints 透传到分模评分)—— 闭环通
让批 1 沉淀的老师傅经验 hints 真接入分模方案生成:
processing_service 拉同指纹 hints 装进 OCC worker payload,
planner 透传到 candidate_generator(axis 优先级加成)和
scheme_scorer(score_breakdown 新字段 + total_score 加成),
写入即消费闭环通。
变更内容:
- src/moldinsight/core/parting_candidate_generator.py
generate_candidates(..., hints=None):_build_axis_metrics 末尾按 hints
加成(priority_score += weight × 20 上限;sample_count ≥ 2 + weight ≥ 0.5
→ method 标签升级 "human_experience_primary")
- src/moldinsight/core/parting_scheme_scorer.py
score_schemes(schemes, *, hints=None) keyword-only:_score_scheme 新增
human_hint_bonus 字段(weight × 12 上限;sample_count < 2 时 ×0.5 折半
防信号不足过度影响);_compute_human_hint_bonus 静态方法解析 axis
(parting.axis → axis → Z);bonus 纳入 total_score
- src/moldinsight/core/multi_scheme_planner.py
generate_plan(..., hints=None):透传 hints 到 candidate_generator 与
scheme_scorer;global_summary.applied_hints 注入返回供前端 ResultView
渲染经验角标
- src/moldinsight/services/processing_service.py
_step_generate_cavity 加 db_session 形参;调用
experience_feedback_service.resolve_for_process_params 拿同指纹 hints,
装进 run_occ payload 顶层 experience_hints;解析失败回退空 list
不阻塞主流程(旧任务不因 receives 闭包退化)
- src/moldinsight/core/occ_worker.py
_op_generate_cavity:payload.get("experience_hints") or {} 透传给
planner.generate_plan(..., hints=...);普通 dict 跨进程 pickle 安全
(满足 occ_worker.py:7-8 硬规则)
- tests/test_experience_feedback_algorithm.py(new)11 例:
- candidate_generator 3 例(无 hints 默认 / hints 加成 / sample_count < 2 不升级)
- scheme_scorer 4 例(无 hints 无 bonus / bonus 加成 / sample_count 折半 /
weight=0 不加成)
- multi_scheme_planner 2 例 OCC-gated(透传 / applied_hints 默认空)
- processing_service 2 例 OCC-gated(payload 含 experience_hints /
解析失败回退空 list)
设计取舍:
- keyword-only hints:避免与位置参数混淆
- weight 仅正向上有效:max(0, (adopted-rejected)/total),老师傅拒绝
的不扣分老算法,只让采纳的加分
- signal-noise 控制:sample_count < 2 时 bonus ×0.5,但 priority_score
仍加成(候选方向仍偏向,避免完全无信号)
- graceful degradation:hints 解析失败回退空 list,主流程继续
- docs/STATUS.md 顶部加 2026-09-23 批 2 日志条目
- docs/TECH_DEBT.md D17 追加批 2 已完成描述 + 缩减剩余工作(仅剩批 3 / 4)
测试基线:192 passed, 13 skipped(净增 7 通过 + 4 OCC-gated skip)。
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -30,6 +30,7 @@ class MultiSchemeMoldPlanner:
|
||||
is_foam_material: bool = False,
|
||||
max_schemes: int = 3,
|
||||
process_params: Optional[Dict[str, Any]] = None,
|
||||
hints: Optional[Dict[str, Dict[str, Any]]] = None,
|
||||
) -> Dict[str, Any]:
|
||||
generator = mold_generator_registry.get_by_type("aluminum_foam" if is_foam_material else "injection")
|
||||
generator.set_material(material["name"])
|
||||
@@ -37,10 +38,12 @@ class MultiSchemeMoldPlanner:
|
||||
|
||||
analysis = generator.analyze_product_geometry(shape)
|
||||
analysis["axis_normal_stats"] = self._collect_axis_normal_stats(generator, shape)
|
||||
# D17 Human-in-Loop 闭环:把老师傅经验 hints 注入候选方向生成
|
||||
candidates = self.candidate_generator.generate_candidates(
|
||||
analysis=analysis,
|
||||
is_foam_material=is_foam_material,
|
||||
max_candidates=max_schemes,
|
||||
hints=hints,
|
||||
)
|
||||
|
||||
schemes = []
|
||||
@@ -62,7 +65,8 @@ class MultiSchemeMoldPlanner:
|
||||
if not schemes:
|
||||
raise ValueError("未能生成任何可用分模方案")
|
||||
|
||||
scored_schemes = self.scheme_scorer.score_schemes(schemes)[:max_schemes]
|
||||
# D17:hints 透传到评分器,权重轴方向评分加成
|
||||
scored_schemes = self.scheme_scorer.score_schemes(schemes, hints=hints)[:max_schemes]
|
||||
export_shapes = {}
|
||||
for idx, scheme in enumerate(scored_schemes, start=1):
|
||||
scheme["raw_scheme_id"] = scheme.get("scheme_id")
|
||||
@@ -80,6 +84,7 @@ class MultiSchemeMoldPlanner:
|
||||
"global_summary": {
|
||||
"scheme_count": len(scored_schemes),
|
||||
"recommended_reason": best_scheme.get("summary", ""),
|
||||
"applied_hints": hints or {}, # D17:给前端展示"本次应用了哪几条经验"
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -121,6 +121,9 @@ def _op_generate_cavity(payload):
|
||||
plan_result 里携带的 _export_shapes(TopoDS 对象)无法跨进程,子进程直接
|
||||
经 CADExporter 落盘为持久化 STEP,返回文件 manifest——与旧 _persist_step_exports
|
||||
产物结构一致,主进程原样存入 export_artifacts。
|
||||
|
||||
D17 Human-in-Loop:payload 顶层 experience_hints 透传给 planner.generate_plan
|
||||
让同指纹历史老师傅反馈影响本次分模评分。payload 普通 dict 透传,pickle 安全。
|
||||
"""
|
||||
parser = _cached("parser", _get_parser)
|
||||
planner = _cached("planner", _get_planner)
|
||||
@@ -130,6 +133,7 @@ def _op_generate_cavity(payload):
|
||||
material=payload["material"],
|
||||
is_foam_material=payload.get("is_foam_material", False),
|
||||
process_params=payload.get("process_params"),
|
||||
hints=payload.get("experience_hints") or {},
|
||||
)
|
||||
export_shapes = plan_result.pop("_export_shapes", {}) or {}
|
||||
export_manifest = _persist_export_shapes(payload, export_shapes)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from typing import Dict, Any, List
|
||||
from typing import Dict, Any, List, Optional
|
||||
|
||||
|
||||
class PartingCandidateGenerator:
|
||||
@@ -15,10 +15,31 @@ class PartingCandidateGenerator:
|
||||
analysis: Dict[str, Any],
|
||||
is_foam_material: bool = False,
|
||||
max_candidates: int = 3,
|
||||
hints: Optional[Dict[str, Dict[str, Any]]] = None,
|
||||
) -> List[Dict[str, Any]]:
|
||||
bbox_dims = analysis.get("bounding_box", {}).get("dimensions", [0, 0, 0])
|
||||
|
||||
axis_metrics = self._build_axis_metrics(bbox_dims, analysis, is_foam_material)
|
||||
|
||||
# D17 Human-in-Loop 闭环:老师傅采纳多的 axis 优先级加成。
|
||||
# hints 结构:{axis: {"weight": 0.0-1.0, "sample_count": int, ...}};
|
||||
# 由 ExperienceFeedbackService.list_hints_for_task 聚合后产出。
|
||||
if hints:
|
||||
for axis_metric in axis_metrics:
|
||||
axis = axis_metric["axis"]
|
||||
hint = hints.get(axis)
|
||||
if not hint:
|
||||
continue
|
||||
weight = float(hint.get("weight", 0.0))
|
||||
sample_count = int(hint.get("sample_count", 0))
|
||||
# 上限 +20 分(weight=1.0 时);weight 仅正值,不"扣分"老算法。
|
||||
axis_metric["priority_score"] = axis_metric["priority_score"] + weight * 20.0
|
||||
# sample_count 足够 + weight 强信号 → method 标签升级为"经验驱动"
|
||||
if sample_count >= 2 and weight >= 0.5:
|
||||
axis_metric["method"] = "human_experience_primary"
|
||||
axis_metric["human_hint_weight"] = weight
|
||||
axis_metric["human_hint_sample_count"] = sample_count
|
||||
|
||||
axis_order = [item["axis"] for item in sorted(
|
||||
axis_metrics,
|
||||
key=lambda item: item["priority_score"],
|
||||
|
||||
@@ -5,10 +5,15 @@ import re
|
||||
class PartingSchemeScorer:
|
||||
"""对候选分模方案打分并排序。"""
|
||||
|
||||
def score_schemes(self, schemes: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
||||
def score_schemes(
|
||||
self,
|
||||
schemes: List[Dict[str, Any]],
|
||||
*,
|
||||
hints: Optional[Dict[str, Dict[str, Any]]] = None,
|
||||
) -> List[Dict[str, Any]]:
|
||||
scored = []
|
||||
for scheme in schemes:
|
||||
score_breakdown = self._score_scheme(scheme)
|
||||
score_breakdown = self._score_scheme(scheme, hints=hints)
|
||||
undercut_priority_bonus = self._build_undercut_priority_bonus(scheme, score_breakdown)
|
||||
total_score = round(
|
||||
score_breakdown["manufacturability"] * 0.25
|
||||
@@ -16,6 +21,7 @@ class PartingSchemeScorer:
|
||||
+ score_breakdown["parting_quality"] * 0.15
|
||||
+ score_breakdown["machining_cost"] * 0.15
|
||||
+ score_breakdown["risk"] * 0.10
|
||||
+ score_breakdown.get("human_hint_bonus", 0.0)
|
||||
+ undercut_priority_bonus,
|
||||
2,
|
||||
)
|
||||
@@ -44,7 +50,12 @@ class PartingSchemeScorer:
|
||||
scheme["title"] = "推荐方案" if rank == 1 else f"备选方案 {rank}"
|
||||
return scored
|
||||
|
||||
def _score_scheme(self, scheme: Dict[str, Any]) -> Dict[str, float]:
|
||||
def _score_scheme(
|
||||
self,
|
||||
scheme: Dict[str, Any],
|
||||
*,
|
||||
hints: Optional[Dict[str, Dict[str, Any]]] = None,
|
||||
) -> Dict[str, float]:
|
||||
cavity_data = scheme.get("cavity_data", {})
|
||||
key_info = scheme.get("key_info", {})
|
||||
candidate_priority = float(scheme.get("priority_score", 60.0))
|
||||
@@ -125,14 +136,48 @@ class PartingSchemeScorer:
|
||||
risk_base += 4.0
|
||||
risk = max(35.0, risk_base)
|
||||
|
||||
human_hint_bonus = PartingSchemeScorer._compute_human_hint_bonus(scheme, hints)
|
||||
|
||||
return {
|
||||
"manufacturability": round(manufacturability, 2),
|
||||
"undercut_complexity": round(undercut_complexity, 2),
|
||||
"parting_quality": round(parting_quality, 2),
|
||||
"machining_cost": round(machining_cost, 2),
|
||||
"risk": round(risk, 2),
|
||||
"human_hint_bonus": human_hint_bonus,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _compute_human_hint_bonus(
|
||||
scheme: Dict[str, Any],
|
||||
hints: Optional[Dict[str, Dict[str, Any]]],
|
||||
) -> float:
|
||||
"""D17 Human-in-Loop:老师傅经验加权(写入即消费)。
|
||||
|
||||
设计要点:
|
||||
- weight ∈ [0, 1] 由 history aggregation 算(adopted-rejected)/ total;仅正值
|
||||
- bonus 上限 +12(与 undercut_priority_bonus 同量级),避免单条反馈过权重
|
||||
- sample_count < 2 时 bonus × 0.5(信号不足折半)
|
||||
- axis 解析优先级:scheme.parting.axis → scheme.axis → 默认 Z
|
||||
"""
|
||||
if not hints:
|
||||
return 0.0
|
||||
parting = scheme.get("parting", {}) if isinstance(scheme.get("parting"), dict) else {}
|
||||
axis = (
|
||||
parting.get("axis")
|
||||
or scheme.get("axis")
|
||||
or "Z"
|
||||
)
|
||||
hint = hints.get(axis) or {}
|
||||
weight = float(hint.get("weight", 0.0))
|
||||
if weight <= 0:
|
||||
return 0.0
|
||||
sample_count = int(hint.get("sample_count", 0))
|
||||
bonus = weight * 12.0
|
||||
if sample_count < 2:
|
||||
bonus *= 0.5
|
||||
return round(bonus, 2)
|
||||
|
||||
@staticmethod
|
||||
def _build_undercut_priority_bonus(
|
||||
scheme: Dict[str, Any],
|
||||
|
||||
@@ -223,8 +223,13 @@ class ProcessingService:
|
||||
|
||||
stage_started = time.perf_counter()
|
||||
plan_result, export_artifacts = await self._step_generate_cavity(
|
||||
file_path, selected_material, is_foam_material, process_params,
|
||||
task_id, timeout=timeout_seconds,
|
||||
db_session,
|
||||
file_path,
|
||||
selected_material,
|
||||
is_foam_material,
|
||||
process_params,
|
||||
task_id,
|
||||
timeout=timeout_seconds,
|
||||
)
|
||||
stage_timings["generate_cavity"] = round(time.perf_counter() - stage_started, 3)
|
||||
# 方案 B:各方案形状的持久化 STEP 已由子进程导出并返回 manifest(export_artifacts),
|
||||
@@ -529,14 +534,46 @@ class ProcessingService:
|
||||
return mesh_result
|
||||
|
||||
async def _step_generate_cavity(
|
||||
self, file_path: str, selected_material: dict, is_foam_material: bool,
|
||||
process_params: Dict[str, Any], task_id: str, timeout: float = 600,
|
||||
self,
|
||||
db_session: AsyncSession,
|
||||
file_path: str,
|
||||
selected_material: dict,
|
||||
is_foam_material: bool,
|
||||
process_params: Dict[str, Any],
|
||||
task_id: str,
|
||||
timeout: float = 600,
|
||||
) -> Tuple[Dict[str, Any], Optional[Dict[str, Any]]]:
|
||||
"""生成多方案分模结果(方案 B:子进程内完成分模 + 方案形状 STEP 导出)。
|
||||
|
||||
D8:型腔是任务的核心产出,生成失败必须让任务 failed——
|
||||
异常直接向编排层传播。返回 (plan_result, export_manifest)。
|
||||
|
||||
D17 Human-in-Loop 闭环:解析同指纹历史 hints(list of {scheme_axis, weight, sample_count, ...}),
|
||||
装进 OCC worker payload,让子进程内的 planner/candidate_generator/scheme_scorer 加成。
|
||||
hints 解析失败不阻塞主流程(logger.warning 后视为空),保证已有任务不退化。
|
||||
"""
|
||||
# D17:拉取同指纹老师傅经验(写入即消费)
|
||||
experience_hints: List[Dict[str, Any]] = []
|
||||
try:
|
||||
from moldinsight.services.experience_feedback_service import (
|
||||
experience_feedback_service,
|
||||
)
|
||||
experience_hints = await experience_feedback_service.resolve_for_process_params(
|
||||
session=db_session,
|
||||
task_id=task_id,
|
||||
process_params=process_params or {},
|
||||
)
|
||||
if experience_hints:
|
||||
logger.info(
|
||||
f"D17 Human-in-Loop:注入 {len(experience_hints)} 条经验"
|
||||
f"到 task={task_id} 的分模方案"
|
||||
)
|
||||
except Exception as hints_err:
|
||||
logger.warning(
|
||||
f"D17 hints 解析失败,回退到无 hints 模式: {hints_err}"
|
||||
)
|
||||
experience_hints = []
|
||||
|
||||
result = await self.run_occ(
|
||||
"generate_cavity",
|
||||
{
|
||||
@@ -546,6 +583,7 @@ class ProcessingService:
|
||||
"is_foam_material": is_foam_material,
|
||||
"process_params": process_params,
|
||||
"export_out_dir": os.path.abspath(self.cad_exporter.output_dir),
|
||||
"experience_hints": experience_hints, # D17 payload 通道
|
||||
},
|
||||
timeout=timeout,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user