feat: add governed knowledge lifecycle
This commit is contained in:
387
backend/apps/knowledge/distillation.py
Normal file
387
backend/apps/knowledge/distillation.py
Normal file
@@ -0,0 +1,387 @@
|
||||
"""从可信已解决工单生成受治理的知识、排查方案和解决方案草稿。"""
|
||||
|
||||
from django.db import transaction
|
||||
from django.utils import timezone
|
||||
|
||||
from apps.agents.models import AgentDefinition, AgentRun, AgentVersion
|
||||
from apps.agents.services import queue_agent_run
|
||||
from apps.audit.models import OperationAuditLog
|
||||
from apps.knowledge.governance import ensure_governed_asset
|
||||
from apps.knowledge.models import (
|
||||
GovernedAssetType,
|
||||
KnowledgeBase,
|
||||
KnowledgeItem,
|
||||
TicketKnowledgeDistillation,
|
||||
TroubleshootingPlan,
|
||||
)
|
||||
from apps.solutions.models import Solution
|
||||
from apps.tickets.models import Ticket, TicketActorSource
|
||||
from apps.tickets.support_analysis import (
|
||||
RETRIEVAL_POLICY_VERSION,
|
||||
retrieve_support_references,
|
||||
)
|
||||
|
||||
KNOWLEDGE_DISTILLATION_AGENT_CODE = "knowledge-distillation"
|
||||
DISTILLATION_OUTPUT_FIELDS = {
|
||||
"should_distill",
|
||||
"reason",
|
||||
"assessment",
|
||||
"citations",
|
||||
"knowledge_draft",
|
||||
"troubleshooting_plan_draft",
|
||||
"solution_draft",
|
||||
}
|
||||
ASSESSMENT_FIELDS = {
|
||||
"novelty_score",
|
||||
"reuse_value_score",
|
||||
"completeness_score",
|
||||
"duplicate_risk_score",
|
||||
}
|
||||
|
||||
|
||||
class KnowledgeDistillationError(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
def validate_knowledge_distillation_contract(version: AgentVersion) -> None:
|
||||
last_step = version.steps.select_related("skill_version").order_by("-order").first()
|
||||
if last_step is None:
|
||||
raise KnowledgeDistillationError("知识蒸馏 Agent 没有可执行步骤")
|
||||
output_schema = last_step.skill_version.output_schema
|
||||
if not isinstance(output_schema, dict) or output_schema.get("type") != "object":
|
||||
raise KnowledgeDistillationError("知识蒸馏 Agent 的最终输出必须是对象")
|
||||
missing = sorted(DISTILLATION_OUTPUT_FIELDS - set(output_schema.get("required", [])))
|
||||
if missing:
|
||||
raise KnowledgeDistillationError(
|
||||
f"知识蒸馏 Agent 最终输出契约缺少字段:{', '.join(missing)}"
|
||||
)
|
||||
|
||||
|
||||
def _require_nonempty_fields(payload: dict, fields: set[str], label: str) -> None:
|
||||
if not isinstance(payload, dict):
|
||||
raise KnowledgeDistillationError(f"{label}必须是对象")
|
||||
missing = sorted(field for field in fields if not str(payload.get(field) or "").strip())
|
||||
if missing:
|
||||
raise KnowledgeDistillationError(f"{label}缺少字段:{', '.join(missing)}")
|
||||
|
||||
|
||||
def validate_knowledge_distillation_output(run: AgentRun, output) -> None:
|
||||
if run.definition.code != KNOWLEDGE_DISTILLATION_AGENT_CODE:
|
||||
return
|
||||
if not isinstance(output, dict):
|
||||
raise KnowledgeDistillationError("知识蒸馏结果必须是对象")
|
||||
missing = sorted(DISTILLATION_OUTPUT_FIELDS - set(output))
|
||||
if missing:
|
||||
raise KnowledgeDistillationError(f"知识蒸馏结果缺少字段:{', '.join(missing)}")
|
||||
try:
|
||||
distillation = run.ticket_knowledge_distillation
|
||||
except TicketKnowledgeDistillation.DoesNotExist as error:
|
||||
raise KnowledgeDistillationError("知识蒸馏运行未关联真实工单") from error
|
||||
citations = output.get("citations")
|
||||
valid_ranks = {item.get("rank") for item in distillation.references}
|
||||
if not isinstance(citations, list) or any(
|
||||
not isinstance(rank, int) or rank not in valid_ranks for rank in citations
|
||||
):
|
||||
raise KnowledgeDistillationError("知识蒸馏结果包含无效引用")
|
||||
assessment = output.get("assessment")
|
||||
if not isinstance(assessment, dict) or set(assessment) != ASSESSMENT_FIELDS:
|
||||
raise KnowledgeDistillationError("知识价值评分字段不完整")
|
||||
if any(
|
||||
isinstance(value, bool)
|
||||
or not isinstance(value, (int, float))
|
||||
or not 0 <= float(value) <= 1
|
||||
for value in assessment.values()
|
||||
):
|
||||
raise KnowledgeDistillationError("知识价值评分必须在 0 到 1 之间")
|
||||
if output.get("should_distill") is not True:
|
||||
return
|
||||
_require_nonempty_fields(
|
||||
output.get("knowledge_draft"),
|
||||
{"title", "problem", "solution"},
|
||||
"知识草稿",
|
||||
)
|
||||
_require_nonempty_fields(
|
||||
output.get("troubleshooting_plan_draft"),
|
||||
{"title", "problem_pattern", "troubleshooting_steps"},
|
||||
"排查方案草稿",
|
||||
)
|
||||
solution_draft = output.get("solution_draft")
|
||||
if not isinstance(solution_draft, dict):
|
||||
raise KnowledgeDistillationError("解决方案草稿必须是对象")
|
||||
if solution_draft.get("create") is True:
|
||||
_require_nonempty_fields(
|
||||
solution_draft,
|
||||
{"title", "problem_signature", "root_cause", "solution"},
|
||||
"解决方案草稿",
|
||||
)
|
||||
|
||||
|
||||
def _ticket_snapshot(ticket: Ticket) -> dict:
|
||||
resolution = ticket.resolution
|
||||
return {
|
||||
"id": ticket.id,
|
||||
"ticket_no": ticket.ticket_no,
|
||||
"title": ticket.title,
|
||||
"description": ticket.description,
|
||||
"problem_performance": ticket.problem_performance,
|
||||
"category_code": ticket.category.code if ticket.category_id else "",
|
||||
"related_system": ticket.related_system,
|
||||
"environment": ticket.environment,
|
||||
"product_name": ticket.product_name,
|
||||
"module_name": ticket.module_name,
|
||||
"attempted_actions": ticket.attempted_actions,
|
||||
"error_information": ticket.error_information,
|
||||
"resolution_type": resolution.resolution_type,
|
||||
"root_cause": resolution.root_cause,
|
||||
"solution": resolution.solution_text,
|
||||
}
|
||||
|
||||
|
||||
@transaction.atomic
|
||||
def start_ticket_knowledge_distillation(
|
||||
ticket: Ticket,
|
||||
*,
|
||||
created_by=None,
|
||||
) -> TicketKnowledgeDistillation:
|
||||
"""从可信解决结果启动幂等蒸馏运行,并冻结工单输入快照。"""
|
||||
|
||||
ticket = (
|
||||
Ticket.objects.select_for_update()
|
||||
.select_related("category", "resolution")
|
||||
.get(id=ticket.id)
|
||||
)
|
||||
if ticket.archived_at is not None:
|
||||
raise KnowledgeDistillationError("已归档工单不能启动知识蒸馏")
|
||||
if ticket.status not in {"resolved", "closed"}:
|
||||
raise KnowledgeDistillationError("只有已解决或已关闭工单可以沉淀知识")
|
||||
if not hasattr(ticket, "resolution"):
|
||||
raise KnowledgeDistillationError("工单缺少解决结论")
|
||||
active = (
|
||||
TicketKnowledgeDistillation.objects.select_related("run")
|
||||
.filter(ticket=ticket, run__status__in=["queued", "running"])
|
||||
.first()
|
||||
)
|
||||
if active is not None:
|
||||
return active
|
||||
completed = (
|
||||
TicketKnowledgeDistillation.objects.select_related("run")
|
||||
.filter(ticket=ticket, run__status="succeeded")
|
||||
.first()
|
||||
)
|
||||
if completed is not None:
|
||||
if completed.processed_at is None:
|
||||
process_knowledge_distillation_result(completed.run)
|
||||
completed.refresh_from_db()
|
||||
return completed
|
||||
if not KnowledgeBase.objects.filter(enabled=True).exists():
|
||||
raise KnowledgeDistillationError("尚未配置启用的知识库")
|
||||
definition = AgentDefinition.objects.select_related(
|
||||
"active_version__model_deployment__provider"
|
||||
).filter(code=KNOWLEDGE_DISTILLATION_AGENT_CODE).first()
|
||||
if definition is None or definition.active_version_id is None:
|
||||
raise KnowledgeDistillationError("尚未发布知识蒸馏 Agent")
|
||||
version = definition.active_version
|
||||
validate_knowledge_distillation_contract(version)
|
||||
query, references = retrieve_support_references(ticket)
|
||||
input_payload = {
|
||||
"ticket": _ticket_snapshot(ticket),
|
||||
"existing_matches": references,
|
||||
}
|
||||
run = queue_agent_run(
|
||||
version,
|
||||
input_payload,
|
||||
created_by=created_by,
|
||||
config_snapshot_extra={
|
||||
"business_object": {"type": "ticket", "id": ticket.id},
|
||||
"retrieval": {
|
||||
"policy_version": RETRIEVAL_POLICY_VERSION,
|
||||
"allowed_statuses": {
|
||||
"knowledge": ["published"],
|
||||
"solution": ["published"],
|
||||
"historical_ticket": ["resolved", "closed"],
|
||||
},
|
||||
"reference_count": len(references),
|
||||
},
|
||||
"governance": {
|
||||
"generated_status": "draft",
|
||||
"self_review_allowed": False,
|
||||
"publish_requires_review": True,
|
||||
},
|
||||
},
|
||||
defer_until_commit=True,
|
||||
)
|
||||
distillation = TicketKnowledgeDistillation.objects.create(
|
||||
ticket=ticket,
|
||||
run=run,
|
||||
references=references,
|
||||
retrieval_policy=run.config_snapshot["retrieval"],
|
||||
created_by=created_by,
|
||||
)
|
||||
ticket.events.create(
|
||||
event_type="knowledge_distillation_started",
|
||||
to_value={
|
||||
"run_public_id": str(run.public_id),
|
||||
"reference_count": len(references),
|
||||
},
|
||||
operator=created_by,
|
||||
source=TicketActorSource.HUMAN,
|
||||
agent_run_id=run.id,
|
||||
)
|
||||
OperationAuditLog.objects.create(
|
||||
user=created_by,
|
||||
module="knowledge",
|
||||
action="knowledge_distillation_started",
|
||||
target_type="ticket",
|
||||
target_id=str(ticket.id),
|
||||
request_snapshot={
|
||||
"agent_run_public_id": str(run.public_id),
|
||||
"reference_count": len(references),
|
||||
},
|
||||
result="success",
|
||||
)
|
||||
return distillation
|
||||
|
||||
|
||||
def _value(payload: dict, field: str, fallback: str = "") -> str:
|
||||
return str(payload.get(field) or fallback).strip()
|
||||
|
||||
|
||||
@transaction.atomic
|
||||
def process_knowledge_distillation_result(
|
||||
run: AgentRun,
|
||||
) -> TicketKnowledgeDistillation | None:
|
||||
"""校验 Agent 产物并生成待独立审核的受治理资产草稿。"""
|
||||
|
||||
if run.definition.code != KNOWLEDGE_DISTILLATION_AGENT_CODE or run.status != "succeeded":
|
||||
return None
|
||||
distillation = (
|
||||
TicketKnowledgeDistillation.objects.select_for_update()
|
||||
.select_related("ticket__category", "ticket__resolution", "run__created_by")
|
||||
.get(run=run)
|
||||
)
|
||||
if distillation.processed_at is not None:
|
||||
return distillation
|
||||
output = run.final_output if isinstance(run.final_output, dict) else {}
|
||||
validate_knowledge_distillation_output(run, output)
|
||||
distillation.should_distill = output.get("should_distill") is True
|
||||
distillation.reason = _value(output, "reason")
|
||||
distillation.assessment = output.get("assessment", {})
|
||||
if distillation.should_distill:
|
||||
knowledge_base = KnowledgeBase.objects.filter(enabled=True).order_by("id").first()
|
||||
if knowledge_base is None:
|
||||
raise KnowledgeDistillationError("尚未配置启用的知识库")
|
||||
ticket = distillation.ticket
|
||||
category_code = ticket.category.code if ticket.category_id else ""
|
||||
knowledge_payload = output["knowledge_draft"]
|
||||
knowledge_item = KnowledgeItem.objects.create(
|
||||
knowledge_base=knowledge_base,
|
||||
title=_value(knowledge_payload, "title"),
|
||||
problem=_value(knowledge_payload, "problem"),
|
||||
applicable_scope=_value(knowledge_payload, "applicable_scope"),
|
||||
category_code=category_code,
|
||||
related_system=ticket.related_system,
|
||||
environment_scope=ticket.environment,
|
||||
error_code=_value(knowledge_payload, "error_code"),
|
||||
root_cause=_value(knowledge_payload, "root_cause"),
|
||||
troubleshooting_steps=_value(knowledge_payload, "troubleshooting_steps"),
|
||||
solution=_value(knowledge_payload, "solution"),
|
||||
risk_note=_value(knowledge_payload, "risk_note"),
|
||||
status="draft",
|
||||
source_type="agent",
|
||||
source_ticket_id=ticket.id,
|
||||
created_by=run.created_by,
|
||||
)
|
||||
ensure_governed_asset(
|
||||
GovernedAssetType.KNOWLEDGE_ITEM,
|
||||
knowledge_item,
|
||||
actor=run.created_by,
|
||||
)
|
||||
plan_payload = output["troubleshooting_plan_draft"]
|
||||
troubleshooting_plan = TroubleshootingPlan.objects.create(
|
||||
knowledge_base=knowledge_base,
|
||||
title=_value(plan_payload, "title"),
|
||||
problem_pattern=_value(plan_payload, "problem_pattern"),
|
||||
applicable_scope=_value(plan_payload, "applicable_scope"),
|
||||
category_code=category_code,
|
||||
related_system=ticket.related_system,
|
||||
environment_scope=ticket.environment,
|
||||
decision_rules=_value(plan_payload, "decision_rules"),
|
||||
troubleshooting_steps=_value(plan_payload, "troubleshooting_steps"),
|
||||
risk_note=_value(plan_payload, "risk_note"),
|
||||
status="draft",
|
||||
source_ticket=ticket,
|
||||
created_by=run.created_by,
|
||||
)
|
||||
ensure_governed_asset(
|
||||
GovernedAssetType.TROUBLESHOOTING_PLAN,
|
||||
troubleshooting_plan,
|
||||
actor=run.created_by,
|
||||
)
|
||||
solution = None
|
||||
solution_payload = output["solution_draft"]
|
||||
if solution_payload.get("create") is True:
|
||||
solution = Solution.objects.create(
|
||||
title=_value(solution_payload, "title"),
|
||||
problem_signature=_value(solution_payload, "problem_signature"),
|
||||
related_system=ticket.related_system,
|
||||
environment_scope=ticket.environment,
|
||||
category=ticket.category,
|
||||
root_cause=_value(solution_payload, "root_cause"),
|
||||
solution=_value(solution_payload, "solution"),
|
||||
risk_note=_value(solution_payload, "risk_note"),
|
||||
status="draft",
|
||||
allow_auto_resolve=False,
|
||||
requires_human_action=True,
|
||||
created_by=run.created_by,
|
||||
)
|
||||
ensure_governed_asset(
|
||||
GovernedAssetType.SOLUTION,
|
||||
solution,
|
||||
actor=run.created_by,
|
||||
)
|
||||
distillation.knowledge_item = knowledge_item
|
||||
distillation.troubleshooting_plan = troubleshooting_plan
|
||||
distillation.solution = solution
|
||||
distillation.processed_at = timezone.now()
|
||||
distillation.save(
|
||||
update_fields=[
|
||||
"should_distill",
|
||||
"reason",
|
||||
"assessment",
|
||||
"knowledge_item",
|
||||
"troubleshooting_plan",
|
||||
"solution",
|
||||
"processed_at",
|
||||
]
|
||||
)
|
||||
distillation.ticket.events.create(
|
||||
event_type="knowledge_distillation_completed",
|
||||
to_value={
|
||||
"should_distill": distillation.should_distill,
|
||||
"knowledge_item_id": distillation.knowledge_item_id,
|
||||
"troubleshooting_plan_id": distillation.troubleshooting_plan_id,
|
||||
"solution_id": distillation.solution_id,
|
||||
},
|
||||
operator=run.created_by,
|
||||
source=TicketActorSource.AGENT,
|
||||
agent_run_id=run.id,
|
||||
metadata={"assessment": distillation.assessment},
|
||||
)
|
||||
OperationAuditLog.objects.create(
|
||||
user=run.created_by,
|
||||
module="knowledge",
|
||||
action="knowledge_distillation_completed",
|
||||
target_type="ticket",
|
||||
target_id=str(distillation.ticket_id),
|
||||
request_snapshot={
|
||||
"agent_run_public_id": str(run.public_id),
|
||||
"should_distill": distillation.should_distill,
|
||||
"generated_asset_ids": {
|
||||
"knowledge_item": distillation.knowledge_item_id,
|
||||
"troubleshooting_plan": distillation.troubleshooting_plan_id,
|
||||
"solution": distillation.solution_id,
|
||||
},
|
||||
},
|
||||
result="success",
|
||||
)
|
||||
return distillation
|
||||
Reference in New Issue
Block a user