feat(api): ADR-030 Phase 5 持續學習迴圈
從執行結果中學習,持續優化決策: 1. learning_service.py - 持續學習服務 - process_execution_result(): 處理執行結果 - process_human_feedback(): 處理人工反饋 - 自動調整信任度 (成功+1 / 失敗歸零) - 更新 Playbook 統計 - 成功案例自動萃取 Playbook 2. approval_execution.py - 整合學習觸發 - 執行成功後觸發學習 - 執行失敗後觸發學習 - _trigger_learning(): 非阻塞呼叫學習服務 學習流程: 執行完成 → LearningService.process_execution_result() ├─ 成功: TrustEngine +1 分 + Playbook 統計更新 └─ 失敗: TrustEngine 歸零 + 記錄失敗原因 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -151,6 +151,15 @@ class ApprovalExecutionService:
|
||||
asyncio.create_task(
|
||||
self._trigger_playbook_extraction(approval)
|
||||
)
|
||||
|
||||
# ADR-030 Phase 5: 觸發學習服務 (fire-and-forget)
|
||||
asyncio.create_task(
|
||||
self._trigger_learning(
|
||||
approval=approval,
|
||||
success=True,
|
||||
duration_seconds=result.duration_ms / 1000 if result.duration_ms else 0,
|
||||
)
|
||||
)
|
||||
else:
|
||||
logger.error(
|
||||
"background_execution_failed",
|
||||
@@ -187,6 +196,57 @@ class ApprovalExecutionService:
|
||||
)
|
||||
)
|
||||
|
||||
# ADR-030 Phase 5: 觸發學習服務 (失敗案例)
|
||||
asyncio.create_task(
|
||||
self._trigger_learning(
|
||||
approval=approval,
|
||||
success=False,
|
||||
error_message=result.error,
|
||||
duration_seconds=result.duration_ms / 1000 if result.duration_ms else 0,
|
||||
)
|
||||
)
|
||||
|
||||
async def _trigger_learning(
|
||||
self,
|
||||
approval: ApprovalRequest,
|
||||
success: bool,
|
||||
duration_seconds: float = 0,
|
||||
error_message: str | None = None,
|
||||
) -> None:
|
||||
"""
|
||||
ADR-030 Phase 5: 觸發學習服務
|
||||
|
||||
處理執行結果,調整信任度和 Playbook 統計
|
||||
"""
|
||||
try:
|
||||
from src.services.learning_service import (
|
||||
ExecutionResult,
|
||||
get_learning_service,
|
||||
)
|
||||
|
||||
learning = get_learning_service()
|
||||
result = ExecutionResult(
|
||||
approval_id=str(approval.id),
|
||||
incident_id=approval.incident_id or "",
|
||||
action=approval.action,
|
||||
success=success,
|
||||
error_message=error_message,
|
||||
duration_seconds=duration_seconds,
|
||||
)
|
||||
|
||||
await learning.process_execution_result(
|
||||
approval=approval,
|
||||
result=result,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
# 學習失敗不影響主流程
|
||||
logger.warning(
|
||||
"learning_trigger_failed",
|
||||
approval_id=str(approval.id),
|
||||
error=str(e),
|
||||
)
|
||||
|
||||
async def _send_execution_notification(
|
||||
self,
|
||||
approval: ApprovalRequest,
|
||||
|
||||
438
apps/api/src/services/learning_service.py
Normal file
438
apps/api/src/services/learning_service.py
Normal file
@@ -0,0 +1,438 @@
|
||||
"""
|
||||
Learning Service - Phase 5 持續學習迴圈
|
||||
======================================
|
||||
ADR-030: 智能自動修復系統
|
||||
|
||||
從執行結果中學習,持續優化決策:
|
||||
1. 更新 Playbook 統計 (成功率/執行次數)
|
||||
2. 調整信任度 (成功 +分 / 失敗 -分)
|
||||
3. 萃取新 Playbook (成功案例自動萃取)
|
||||
4. 處理人工反饋 (有效性評分)
|
||||
|
||||
設計原則:
|
||||
- 非同步執行,不阻塞主流程
|
||||
- 失敗容忍,學習失敗不影響執行結果
|
||||
- 完整審計追蹤
|
||||
|
||||
版本: v1.0
|
||||
建立: 2026-03-26 (台北時區)
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import UTC, datetime
|
||||
from enum import Enum
|
||||
from typing import Any
|
||||
|
||||
import structlog
|
||||
|
||||
from src.models.approval import ApprovalRequest
|
||||
from src.models.incident import Incident, IncidentStatus
|
||||
from src.services.trust_engine import get_trust_manager
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Constants
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class FeedbackType(str, Enum):
|
||||
"""反饋類型"""
|
||||
|
||||
EXECUTION_SUCCESS = "execution_success" # 執行成功
|
||||
EXECUTION_FAILURE = "execution_failure" # 執行失敗
|
||||
HUMAN_APPROVE = "human_approve" # 人工批准
|
||||
HUMAN_REJECT = "human_reject" # 人工拒絕
|
||||
HUMAN_OVERRIDE = "human_override" # 人工覆蓋 AI 決策
|
||||
EFFECTIVENESS_RATING = "effectiveness_rating" # 有效性評分
|
||||
|
||||
|
||||
# 信任度調整參數
|
||||
TRUST_SUCCESS_BOOST = 1 # 成功 +1 分
|
||||
TRUST_FAILURE_PENALTY = 2 # 失敗 -2 分 (或歸零)
|
||||
TRUST_HUMAN_REJECT_PENALTY = 1 # 人工拒絕 -1 分
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Data Models
|
||||
# =============================================================================
|
||||
|
||||
|
||||
@dataclass
|
||||
class ExecutionResult:
|
||||
"""執行結果"""
|
||||
|
||||
approval_id: str
|
||||
incident_id: str
|
||||
action: str
|
||||
success: bool
|
||||
error_message: str | None = None
|
||||
duration_seconds: float = 0.0
|
||||
executed_at: datetime = field(default_factory=lambda: datetime.now(UTC))
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"approval_id": self.approval_id,
|
||||
"incident_id": self.incident_id,
|
||||
"action": self.action,
|
||||
"success": self.success,
|
||||
"error_message": self.error_message,
|
||||
"duration_seconds": self.duration_seconds,
|
||||
"executed_at": self.executed_at.isoformat(),
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class FeedbackRequest:
|
||||
"""人工反饋請求"""
|
||||
|
||||
incident_id: str
|
||||
feedback_type: FeedbackType
|
||||
effectiveness_score: int | None = None # 1-5 分
|
||||
learning_notes: str | None = None # 學習筆記
|
||||
submitted_by: str | None = None
|
||||
submitted_at: datetime = field(default_factory=lambda: datetime.now(UTC))
|
||||
|
||||
|
||||
@dataclass
|
||||
class LearningRecord:
|
||||
"""學習記錄"""
|
||||
|
||||
incident_id: str
|
||||
feedback_type: FeedbackType
|
||||
action_pattern: str
|
||||
trust_before: int
|
||||
trust_after: int
|
||||
playbook_updated: bool = False
|
||||
new_playbook_id: str | None = None
|
||||
learned_at: datetime = field(default_factory=lambda: datetime.now(UTC))
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"incident_id": self.incident_id,
|
||||
"feedback_type": self.feedback_type.value,
|
||||
"action_pattern": self.action_pattern,
|
||||
"trust_before": self.trust_before,
|
||||
"trust_after": self.trust_after,
|
||||
"playbook_updated": self.playbook_updated,
|
||||
"new_playbook_id": self.new_playbook_id,
|
||||
"learned_at": self.learned_at.isoformat(),
|
||||
}
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Learning Service
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class LearningService:
|
||||
"""
|
||||
持續學習服務
|
||||
|
||||
職責:
|
||||
1. 處理執行結果 → 更新 Playbook + 信任度
|
||||
2. 處理人工反饋 → 調整 Playbook 有效性
|
||||
3. 萃取新 Playbook (成功案例)
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self._trust_manager = get_trust_manager()
|
||||
|
||||
async def process_execution_result(
|
||||
self,
|
||||
approval: ApprovalRequest,
|
||||
result: ExecutionResult,
|
||||
) -> LearningRecord:
|
||||
"""
|
||||
處理執行結果,觸發學習
|
||||
|
||||
Args:
|
||||
approval: 原始審批請求
|
||||
result: 執行結果
|
||||
|
||||
Returns:
|
||||
LearningRecord: 學習記錄
|
||||
"""
|
||||
action_pattern = self._extract_action_pattern(approval.action)
|
||||
|
||||
# 取得當前信任分數
|
||||
trust_record = self._trust_manager.get_trust_record(action_pattern)
|
||||
trust_before = trust_record.score if trust_record else 0
|
||||
|
||||
# 1. 調整信任度
|
||||
if result.success:
|
||||
# 成功: 記錄批准 (信任分數 +1)
|
||||
self._trust_manager.record_approval(
|
||||
action_pattern=action_pattern,
|
||||
user_role="system",
|
||||
user_id="auto_learning",
|
||||
)
|
||||
feedback_type = FeedbackType.EXECUTION_SUCCESS
|
||||
else:
|
||||
# 失敗: 記錄拒絕 (信任分數歸零)
|
||||
self._trust_manager.record_rejection(
|
||||
action_pattern=action_pattern,
|
||||
user_role="system",
|
||||
user_id="auto_learning",
|
||||
reason=result.error_message,
|
||||
)
|
||||
feedback_type = FeedbackType.EXECUTION_FAILURE
|
||||
|
||||
# 取得更新後的信任分數
|
||||
trust_record = self._trust_manager.get_trust_record(action_pattern)
|
||||
trust_after = trust_record.score if trust_record else 0
|
||||
|
||||
# 2. 更新 Playbook 統計 (如果有匹配)
|
||||
playbook_updated = False
|
||||
if hasattr(approval, "matched_playbook_id") and approval.matched_playbook_id:
|
||||
try:
|
||||
await self._update_playbook_stats(
|
||||
playbook_id=approval.matched_playbook_id,
|
||||
success=result.success,
|
||||
)
|
||||
playbook_updated = True
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
"playbook_stats_update_failed",
|
||||
playbook_id=approval.matched_playbook_id,
|
||||
error=str(e),
|
||||
)
|
||||
|
||||
# 3. 嘗試萃取新 Playbook (成功且無匹配 Playbook)
|
||||
new_playbook_id = None
|
||||
if result.success and not getattr(approval, "matched_playbook_id", None):
|
||||
try:
|
||||
new_playbook_id = await self._try_extract_playbook(
|
||||
incident_id=result.incident_id,
|
||||
action=approval.action,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
"playbook_extraction_failed",
|
||||
incident_id=result.incident_id,
|
||||
error=str(e),
|
||||
)
|
||||
|
||||
# 建立學習記錄
|
||||
record = LearningRecord(
|
||||
incident_id=result.incident_id,
|
||||
feedback_type=feedback_type,
|
||||
action_pattern=action_pattern,
|
||||
trust_before=trust_before,
|
||||
trust_after=trust_after,
|
||||
playbook_updated=playbook_updated,
|
||||
new_playbook_id=new_playbook_id,
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"learning_completed",
|
||||
incident_id=result.incident_id,
|
||||
success=result.success,
|
||||
trust_change=f"{trust_before} → {trust_after}",
|
||||
playbook_updated=playbook_updated,
|
||||
new_playbook=new_playbook_id,
|
||||
)
|
||||
|
||||
return record
|
||||
|
||||
async def process_human_feedback(
|
||||
self,
|
||||
feedback: FeedbackRequest,
|
||||
) -> LearningRecord:
|
||||
"""
|
||||
處理人工反饋
|
||||
|
||||
Args:
|
||||
feedback: 反饋請求
|
||||
|
||||
Returns:
|
||||
LearningRecord: 學習記錄
|
||||
"""
|
||||
# 從 incident 取得 action pattern (需查詢)
|
||||
action_pattern = f"incident:{feedback.incident_id}"
|
||||
|
||||
trust_record = self._trust_manager.get_trust_record(action_pattern)
|
||||
trust_before = trust_record.score if trust_record else 0
|
||||
|
||||
playbook_updated = False
|
||||
|
||||
if feedback.feedback_type == FeedbackType.HUMAN_APPROVE:
|
||||
# 人工批准: 信任 +1
|
||||
self._trust_manager.record_approval(
|
||||
action_pattern=action_pattern,
|
||||
user_role="human",
|
||||
user_id=feedback.submitted_by,
|
||||
)
|
||||
|
||||
elif feedback.feedback_type == FeedbackType.HUMAN_REJECT:
|
||||
# 人工拒絕: 信任歸零
|
||||
self._trust_manager.record_rejection(
|
||||
action_pattern=action_pattern,
|
||||
user_role="human",
|
||||
user_id=feedback.submitted_by,
|
||||
reason="Human rejected",
|
||||
)
|
||||
|
||||
elif feedback.feedback_type == FeedbackType.EFFECTIVENESS_RATING:
|
||||
# 有效性評分
|
||||
if feedback.effectiveness_score is not None:
|
||||
if feedback.effectiveness_score >= 4:
|
||||
# 高評分: 增加信任
|
||||
self._trust_manager.record_approval(
|
||||
action_pattern=action_pattern,
|
||||
user_role="feedback",
|
||||
user_id=feedback.submitted_by,
|
||||
)
|
||||
playbook_updated = await self._promote_playbook(feedback.incident_id)
|
||||
elif feedback.effectiveness_score <= 2:
|
||||
# 低評分: 降低信任
|
||||
self._trust_manager.record_rejection(
|
||||
action_pattern=action_pattern,
|
||||
user_role="feedback",
|
||||
user_id=feedback.submitted_by,
|
||||
reason=f"Low effectiveness score: {feedback.effectiveness_score}",
|
||||
)
|
||||
playbook_updated = await self._demote_playbook(feedback.incident_id)
|
||||
|
||||
trust_record = self._trust_manager.get_trust_record(action_pattern)
|
||||
trust_after = trust_record.score if trust_record else 0
|
||||
|
||||
record = LearningRecord(
|
||||
incident_id=feedback.incident_id,
|
||||
feedback_type=feedback.feedback_type,
|
||||
action_pattern=action_pattern,
|
||||
trust_before=trust_before,
|
||||
trust_after=trust_after,
|
||||
playbook_updated=playbook_updated,
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"human_feedback_processed",
|
||||
incident_id=feedback.incident_id,
|
||||
feedback_type=feedback.feedback_type.value,
|
||||
effectiveness_score=feedback.effectiveness_score,
|
||||
trust_change=f"{trust_before} → {trust_after}",
|
||||
)
|
||||
|
||||
return record
|
||||
|
||||
# =========================================================================
|
||||
# Private Methods
|
||||
# =========================================================================
|
||||
|
||||
def _extract_action_pattern(self, action: str) -> str:
|
||||
"""從 action 字串提取 pattern"""
|
||||
if not action:
|
||||
return "unknown"
|
||||
|
||||
parts = action.split()
|
||||
if len(parts) < 3:
|
||||
return "unknown"
|
||||
|
||||
verb = parts[1] if len(parts) > 1 else "unknown"
|
||||
resource_part = parts[2] if len(parts) > 2 else ""
|
||||
|
||||
if "/" in resource_part:
|
||||
resource_name = resource_part.split("/")[-1]
|
||||
else:
|
||||
resource_name = resource_part
|
||||
|
||||
# 移除 pod hash suffix
|
||||
resource_parts = resource_name.split("-")
|
||||
if len(resource_parts) >= 3:
|
||||
resource_name = "-".join(resource_parts[:-2]) + "-*"
|
||||
|
||||
return f"{verb}:{resource_name}"
|
||||
|
||||
async def _update_playbook_stats(
|
||||
self,
|
||||
playbook_id: str,
|
||||
success: bool,
|
||||
) -> None:
|
||||
"""更新 Playbook 統計"""
|
||||
try:
|
||||
from src.services.playbook_service import get_playbook_service
|
||||
|
||||
service = get_playbook_service()
|
||||
await service.record_execution(playbook_id, success)
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
"playbook_stats_update_error",
|
||||
playbook_id=playbook_id,
|
||||
error=str(e),
|
||||
)
|
||||
|
||||
async def _try_extract_playbook(
|
||||
self,
|
||||
incident_id: str,
|
||||
action: str,
|
||||
) -> str | None:
|
||||
"""嘗試從成功案例萃取 Playbook"""
|
||||
try:
|
||||
from src.repositories.incident_repository import get_incident_repository
|
||||
from src.services.playbook_service import get_playbook_service
|
||||
|
||||
# 取得 Incident
|
||||
repo = get_incident_repository()
|
||||
incident = await repo.get_by_id(incident_id)
|
||||
|
||||
if not incident:
|
||||
return None
|
||||
|
||||
# 確認狀態為 RESOLVED
|
||||
if incident.status not in [IncidentStatus.RESOLVED, IncidentStatus.CLOSED]:
|
||||
return None
|
||||
|
||||
# 萃取 Playbook
|
||||
service = get_playbook_service()
|
||||
playbook = await service.extract_from_incident(
|
||||
incident=incident,
|
||||
auto_approve=False, # 需人工審核
|
||||
)
|
||||
|
||||
if playbook:
|
||||
logger.info(
|
||||
"playbook_auto_extracted",
|
||||
incident_id=incident_id,
|
||||
playbook_id=playbook.playbook_id,
|
||||
)
|
||||
return playbook.playbook_id
|
||||
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
"playbook_extraction_error",
|
||||
incident_id=incident_id,
|
||||
error=str(e),
|
||||
)
|
||||
return None
|
||||
|
||||
async def _promote_playbook(self, incident_id: str) -> bool:
|
||||
"""提升 Playbook 信心度 (高評分)"""
|
||||
# TODO: 實作 Playbook 信心度提升邏輯
|
||||
logger.debug("playbook_promoted", incident_id=incident_id)
|
||||
return True
|
||||
|
||||
async def _demote_playbook(self, incident_id: str) -> bool:
|
||||
"""降低 Playbook 信心度 (低評分)"""
|
||||
# TODO: 實作 Playbook 信心度降低邏輯
|
||||
logger.debug("playbook_demoted", incident_id=incident_id)
|
||||
return True
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Singleton
|
||||
# =============================================================================
|
||||
|
||||
_learning_service: LearningService | None = None
|
||||
|
||||
|
||||
def get_learning_service() -> LearningService:
|
||||
"""取得學習服務 singleton"""
|
||||
global _learning_service
|
||||
if _learning_service is None:
|
||||
_learning_service = LearningService()
|
||||
return _learning_service
|
||||
Reference in New Issue
Block a user