Практические заметки: Агентные архитектуры — Статья 13: Паттерны с участием человека
Пошаговое руководство по практическим заметкам: агентные архитектуры — Статья 13: Паттерны с участием человека: контракты, проверки и готовые блоки кода для команд, использующих этот паттерн.
В следующих примечаниях описывается практический подход к решению задач, связанных с «Агентными архитектурами — Статья 13: Паттерны с участием человека». Основное внимание уделяется контрактам, проверкам и местам для вставки кода, а не мотивационным аспектам. На этапе обзора сначала запишите условия контракта: необходимые входные данные, сигнал о успешном выполнении и действия при частичной неудаче. Такой список поможет сохранять честность при последующих изменениях кода. Рассматривайте этот этап как контракт между входными данными и проверенными выходными результатами. Дайте названия элементам, определите критерии успеха и не допускайте безответственного частичного выполнения задач.
Что вы здесь найдете
Этап «Что вы найдете» работает наилучшим образом, если рассматривать его как измеримую поверхность. Соберите один идеальный пример работы, один случай сбоя и записку о возврате к предыдущему состоянию перед расширением объема работ. Записывайте временные показатели, а также стоимость токенов или запросов рядом с функциональными результатами. Отображение стоимости на раннем этапе предотвращает неожиданные счета при переходе от демо-среды к общедоступным средам. Сохраняйте структуру графа простой и типизированной; вложенные структуры скрывают информацию о том, какой узел заполнил тот или иной поле, и могут нарушить возобновление работы после прерываний.
Где провести границу
Метод «Где рисовать сцену» работает наилучшим образом, когда его рассматривают как измеримую поверхность. Соберите один пример успешного выполнения, один случай сбоя и запись о возврате к предыдущему состоянию перед расширением объёма работ. Храните конфигурацию вне кода приложения. Файлы среды, хранилища секретов и флаги функций должны находиться в одном месте, чтобы операторы могли их проверять, не читая весь граф. Сохраняйте состояние графа простым и типизированным. Вложенные структуры данных скрывают информацию о том, какой узел записал какое поле, и приводят к нарушению возобновления работы после перерывов.
+--------------------+------------------------+------------------------+
| | Low Error Cost | High Error Cost |
+--------------------+------------------------+------------------------+
| Reversible | AUTONOMOUS | OVERSIGHT SAMPLING |
| | (agent acts freely) | (act, log, review some)|
+--------------------+------------------------+------------------------+
| Irreversible | APPROVAL FOR NOVEL | APPROVAL GATE |
| | (approve first time, | (always require human |
| | then autonomous) | approval before act) |
+--------------------+------------------------+------------------------+
# harness/hitl/action_policy.py
from enum import Enum
from dataclasses import dataclass
from typing import Optional, Callable
class ApprovalPolicy(Enum):
AUTONOMOUS = "autonomous" # act freely
OVERSIGHT_SAMPLING = "sampling" # act, log, review a sample
APPROVAL_FOR_NOVEL = "approval_novel" # approve first occurrence, then auto
APPROVAL_GATE = "approval_gate" # always require approval
DUAL_APPROVAL = "dual_approval" # require two approvers
@dataclass
class ActionPolicy:
action_name: str
policy: ApprovalPolicy
reason: str
approver_role: Optional[str] = None # required role to approve
timeout_seconds: int = 3600
timeout_action: str = "reject" # "reject" | "escalate" | "proceed"
# Example policy configuration for a customer support agent
SUPPORT_AGENT_POLICIES = {
"read_customer_record": ActionPolicy(
action_name="read_customer_record",
policy=ApprovalPolicy.AUTONOMOUS,
reason="Read-only, reversible, low risk",
),
"draft_response": ActionPolicy(
action_name="draft_response",
policy=ApprovalPolicy.OVERSIGHT_SAMPLING,
reason="Reversible but customer-facing quality matters",
),
"send_customer_email": ActionPolicy(
action_name="send_customer_email",
policy=ApprovalPolicy.APPROVAL_GATE,
reason="Irreversible, customer-facing, reputation risk",
approver_role="support_agent",
timeout_seconds=1800,
timeout_action="reject",
),
"issue_refund": ActionPolicy(
action_name="issue_refund",
policy=ApprovalPolicy.DUAL_APPROVAL,
reason="Financial impact, irreversible",
approver_role="support_manager",
timeout_seconds=7200,
timeout_action="escalate",
),
"delete_account": ActionPolicy(
action_name="delete_account",
policy=ApprovalPolicy.DUAL_APPROVAL,
reason="Catastrophic and irreversible",
approver_role="senior_manager",
timeout_seconds=86400,
timeout_action="reject",
),
}
class ActionPolicyEngine:
"""
Determines whether an action requires human approval and how.
Consulted at the tool execution layer before any action runs.
"""
def __init__(self, policies: dict):
self.policies = policies
def get_policy(self, action_name: str) -> ActionPolicy:
return self.policies.get(
action_name,
# Default to approval gate for unknown actions: fail safe
ActionPolicy(
action_name=action_name,
policy=ApprovalPolicy.APPROVAL_GATE,
reason="Unknown action, defaulting to approval required",
)
)
def requires_approval(self, action_name: str, is_novel: bool = False) -> bool:
policy = self.get_policy(action_name)
if policy.policy == ApprovalPolicy.AUTONOMOUS:
return False
if policy.policy == ApprovalPolicy.OVERSIGHT_SAMPLING:
return False # acts first, reviewed after
if policy.policy == ApprovalPolicy.APPROVAL_FOR_NOVEL:
return is_novel
return True # APPROVAL_GATE and DUAL_APPROVAL always require approval
Шаблон 1: Блокировки на утверждение
Этап «Ворота одобрения» по шаблону 1 работает наилучшим образом, когда его рассматривают как измеримую структуру. Соберите один идеальный пример работы, один случай сбоя и записку о возврате к предыдущему состоянию перед расширением объёма работ. Документируйте как успешный, так и восстановительный пути выполнения. Повторные попытки, проверки со стороны человека и обработка неработоспособных сообщений являются частью продукта, а не последующими доработками. Сохраняйте простую и типизированную структуру графа. Вложенные элементы скрывают информацию о том, какой узел заполнил тот или иной поле, и могут нарушить возобновление работы после прерываний. Этап «Ворота одобрения» по шаблону 1 работает наилучшим образом, когда его рассматривают как измеримую структуру. Соберите один идеальный пример работы, один случай сбоя и записку о возврате к предыдущему состоянию перед расширением объёма работ. Рассматривайте этот этап как контракт между входными данными и проверенными выходными результатами. Дайте названия соответствующим элементам, определите критерии успеха и не допускайте молчаливого частичного завершения работы.
# harness/hitl/approval_gate.py
import boto3
import time
import uuid
import json
from typing import Optional, Literal
from dataclasses import dataclass
from langgraph.types import interrupt, Command
@dataclass
class ApprovalRequest:
request_id: str
run_id: str
action_name: str
action_args: dict
context_summary: str
requested_at: float
approver_role: str
status: str # PENDING | APPROVED | REJECTED | TIMEOUT
decided_by: Optional[str] = None
decided_at: Optional[float] = None
decision_note: Optional[str] = None
class ApprovalGateManager:
"""
Manages approval requests using LangGraph interrupts for pause/resume
and DynamoDB for durable request state.
"""
def __init__(
self,
table_name: str = "agent-approval-requests",
region: str = "us-east-1",
):
dynamodb = boto3.resource("dynamodb", region_name=region)
self.table = dynamodb.Table(table_name)
self.sns = boto3.client("sns", region_name=region)
def create_approval_request(
self,
run_id: str,
action_name: str,
action_args: dict,
context_summary: str,
approver_role: str,
notification_topic_arn: Optional[str] = None,
) -> ApprovalRequest:
"""
Creates a pending approval request and notifies approvers.
"""
request = ApprovalRequest(
request_id=str(uuid.uuid4()),
run_id=run_id,
action_name=action_name,
action_args=action_args,
context_summary=context_summary,
requested_at=time.time(),
approver_role=approver_role,
status="PENDING",
)
self.table.put_item(Item={
"request_id": request.request_id,
"run_id": request.run_id,
"action_name": request.action_name,
"action_args": json.dumps(request.action_args),
"context_summary": request.context_summary,
"requested_at": int(request.requested_at),
"approver_role": request.approver_role,
"status": "PENDING",
})
# Notify approvers
if notification_topic_arn:
self.sns.publish(
TopicArn=notification_topic_arn,
Subject=f"Approval needed: {action_name}",
Message=json.dumps({
"request_id": request.request_id,
"action": action_name,
"context": context_summary,
"approver_role": approver_role,
}),
)
return request
def record_decision(
self,
request_id: str,
decision: Literal["APPROVED", "REJECTED"],
decided_by: str,
decision_note: Optional[str] = None,
) -> bool:
"""
Records a human's approval decision.
Called by the approval interface when a human responds.
"""
self.table.update_item(
Key={"request_id": request_id},
UpdateExpression=(
"SET #s = :status, decided_by = :by, "
"decided_at = :at, decision_note = :note"
),
ExpressionAttributeNames={"#s": "status"},
ExpressionAttributeValues={
":status": decision,
":by": decided_by,
":at": int(time.time()),
":note": decision_note or "",
},
# Only allow decision on PENDING requests: prevents double-decision
ConditionExpression="#s = :pending",
ExpressionAttributeValues2={":pending": "PENDING"} if False else None,
)
return True
def get_decision(self, request_id: str) -> Optional[str]:
"""Polls the current decision status for a request."""
response = self.table.get_item(Key={"request_id": request_id})
item = response.get("Item")
return item.get("status") if item else None
# The LangGraph node that implements the approval gate
def approval_gate_node(state: dict) -> dict:
"""
A LangGraph node that pauses execution and waits for human approval.
Uses interrupt() to suspend the graph. The graph state is persisted
and can be resumed when the approval decision arrives.
"""
pending_action = state.get("pending_action")
if not pending_action:
return state
# interrupt() pauses the graph and surfaces this data to the caller.
# The caller (your application) presents it to a human and resumes
# the graph with the decision.
decision = interrupt({
"type": "approval_required",
"action": pending_action["name"],
"args": pending_action["args"],
"context": state.get("context_summary", ""),
"run_id": state.get("agent_run_id"),
})
# When resumed, decision contains the human's response
if decision.get("approved"):
return {
**state,
"action_approved": True,
"approved_by": decision.get("approver"),
}
else:
return {
**state,
"action_approved": False,
"rejection_reason": decision.get("reason", "Rejected by human reviewer"),
}
Шаблон 2: Эскалация
На этапе эскалации шаблона 2 необходимо заранее определить входные данные, ответственного за выполнение шага и критерии завершения перед изменением кода. Операторы должны иметь возможность перезапустить шаг с известной точки контроля, не догадываясь о скрытом состоянии. Рядом с функциональными результатами следует записывать время выполнения и стоимость токенов или запросов. Отображение затрат на раннем этапе предотвращает неожиданные счета при переходе с демо-среды в общедоступные среды. Для операций, связанных с тратой денег или изменением производственных данных, требуется утверждение человека. Настройка на этапе компиляции не гарантирует полноты решения с точки зрения бизнес-процессов.
# harness/hitl/escalation.py
import boto3
import time
import json
from typing import Optional
from dataclasses import dataclass
from langchain_aws import ChatBedrock
from langchain_core.messages import SystemMessage, HumanMessage
ESCALATION_TRIGGERS = {
"low_confidence": "Agent confidence in its solution is below threshold",
"conflicting_information": "Retrieved information contradicts itself",
"policy_ambiguity": "The correct action is genuinely ambiguous under policy",
"high_stakes_uncertainty": "High-stakes decision with insufficient certainty",
"repeated_failure": "Agent has failed the same task multiple times",
"explicit_user_request": "User asked to speak with a human",
}
@dataclass
class EscalationEvent:
escalation_id: str
run_id: str
trigger: str
agent_context: str
agent_attempted_solution: Optional[str]
confidence: float
escalated_at: float
assigned_to: Optional[str] = None
resolution: Optional[str] = None
class EscalationManager:
"""
Handles cases where the agent should hand off to a human.
Distinct from approval gates: escalation is triggered by the agent
recognizing its own limitations.
"""
def __init__(
self,
table_name: str = "agent-escalations",
region: str = "us-east-1",
):
dynamodb = boto3.resource("dynamodb", region_name=region)
self.table = dynamodb.Table(table_name)
bedrock = boto3.client("bedrock-runtime", region_name=region)
self.confidence_model = ChatBedrock(
client=bedrock,
model_id="anthropic.claude-haiku-4-5",
model_kwargs={"temperature": 0, "max_tokens": 256},
)
def should_escalate(
self,
task: str,
proposed_solution: str,
attempts: int,
) -> tuple:
"""
Assesses whether the agent should escalate to a human.
Returns (should_escalate, trigger, confidence).
"""
# Repeated failure is a deterministic trigger
if attempts >= 3:
return True, "repeated_failure", 0.0
# Ask the model to self-assess confidence
response = self.confidence_model.invoke([
SystemMessage(content="""
Assess your confidence in a proposed solution. Be honest about uncertainty.
Consider: Is the information sufficient? Is the answer unambiguous?
Are there conflicting considerations? Is this high-stakes?
Return JSON:
{
"confidence": 0.0 to 1.0,
"should_escalate": true | false,
"trigger": "low_confidence | conflicting_information | policy_ambiguity | high_stakes_uncertainty | none"
}
"""),
HumanMessage(content=f"Task: {task}\n\nProposed solution: {proposed_solution}")
])
try:
assessment = json.loads(response.content)
return (
assessment.get("should_escalate", False),
assessment.get("trigger", "low_confidence"),
assessment.get("confidence", 0.5),
)
except json.JSONDecodeError:
# If we cannot assess confidence, escalate to be safe
return True, "low_confidence", 0.0
def create_escalation(
self,
run_id: str,
trigger: str,
agent_context: str,
attempted_solution: Optional[str],
confidence: float,
notification_topic_arn: Optional[str] = None,
) -> EscalationEvent:
import uuid
event = EscalationEvent(
escalation_id=str(uuid.uuid4()),
run_id=run_id,
trigger=trigger,
agent_context=agent_context,
agent_attempted_solution=attempted_solution,
confidence=confidence,
escalated_at=time.time(),
)
self.table.put_item(Item={
"escalation_id": event.escalation_id,
"run_id": event.run_id,
"trigger": event.trigger,
"agent_context": event.agent_context[:2000],
"attempted_solution": (attempted_solution or "")[:2000],
"confidence": str(confidence),
"escalated_at": int(event.escalated_at),
"status": "OPEN",
})
if notification_topic_arn:
boto3.client("sns").publish(
TopicArn=notification_topic_arn,
Subject=f"Agent escalation: {trigger}",
Message=event.agent_context[:1000],
)
return event
Шаблон 3: совместная редактирование
На этапе совместной редактирования по шаблону 3 необходимо заранее определить входные данные, ответственного за выполнение шага и критерии завершения перед изменением кода. Операторы должны иметь возможность перезапустить шаг с известной точки контроля, не догадываясь о скрытом состоянии. Конфигурацию следует хранить отдельно от кода приложения. Файлы среды, хранилища секретов и флаги функций должны находиться в одном месте, чтобы операторы могли их проверять, не читая весь код. Установите человеческое утверждение для операций, связанных с тратой денег или изменением производственных данных. Подключение на этапе компиляции не гарантирует полноты обработки бизнес-логики.
# harness/hitl/collaborative.py
import boto3
import time
import json
from typing import Optional
from dataclasses import dataclass
@dataclass
class CollaborativeEdit:
edit_id: str
run_id: str
original_draft: str
human_edited: str
edit_distance: float # how much was changed
edit_categories: list # "tone", "factual", "structure", "detail"
edited_by: str
edited_at: float
class CollaborativeEditManager:
"""
Manages the draft-edit-finalize flow where a human refines agent output.
Captures edits as learning signals for future improvement.
"""
def __init__(
self,
table_name: str = "agent-collaborative-edits",
region: str = "us-east-1",
):
dynamodb = boto3.resource("dynamodb", region_name=region)
self.table = dynamodb.Table(table_name)
def record_edit(
self,
run_id: str,
original_draft: str,
human_edited: str,
edited_by: str,
edit_categories: list = None,
) -> CollaborativeEdit:
"""
Records a human edit to agent output.
The edit distance and categories become learning signals.
"""
import uuid
edit_distance = self._compute_edit_distance(original_draft, human_edited)
edit = CollaborativeEdit(
edit_id=str(uuid.uuid4()),
run_id=run_id,
original_draft=original_draft,
human_edited=human_edited,
edit_distance=edit_distance,
edit_categories=edit_categories or [],
edited_by=edited_by,
edited_at=time.time(),
)
self.table.put_item(Item={
"edit_id": edit.edit_id,
"run_id": edit.run_id,
"original_draft": original_draft[:5000],
"human_edited": human_edited[:5000],
"edit_distance": str(edit_distance),
"edit_categories": edit.edit_categories,
"edited_by": edited_by,
"edited_at": int(edit.edited_at),
})
return edit
def analyze_edit_patterns(
self,
run_ids: list = None,
min_edits: int = 20,
) -> dict:
"""
Analyzes accumulated edits to find systematic patterns.
If humans consistently edit for the same reason, the agent's
system prompt or procedure should be updated.
"""
response = self.table.scan(Limit=200)
edits = response.get("Items", [])
if len(edits) < min_edits:
return {"sufficient_data": False, "edit_count": len(edits)}
# Aggregate edit categories
category_counts = {}
total_distance = 0.0
for edit in edits:
for cat in edit.get("edit_categories", []):
category_counts[cat] = category_counts.get(cat, 0) + 1
total_distance += float(edit.get("edit_distance", 0))
avg_distance = total_distance / len(edits)
dominant_category = max(category_counts, key=category_counts.get) if category_counts else None
return {
"sufficient_data": True,
"edit_count": len(edits),
"average_edit_distance": round(avg_distance, 3),
"category_distribution": category_counts,
"dominant_edit_category": dominant_category,
"recommendation": self._recommendation(dominant_category, avg_distance),
}
def _recommendation(self, dominant_category: Optional[str], avg_distance: float) -> str:
if avg_distance < 0.1:
return "Edits are minor. Agent output quality is high."
if dominant_category == "tone":
return "Humans frequently adjust tone. Update system prompt with tone guidance."
if dominant_category == "factual":
return "Humans frequently correct facts. Review agent's information sources."
if dominant_category == "structure":
return "Humans frequently restructure. Add output format guidance to system prompt."
return "Review edit patterns manually for systematic improvements."
def _compute_edit_distance(self, a: str, b: str) -> float:
"""Normalized Levenshtein distance, 0 (identical) to 1 (completely different)."""
if not a and not b:
return 0.0
# Simple word-level distance for efficiency on long text
a_words, b_words = a.split(), b.split()
max_len = max(len(a_words), len(b_words))
if max_len == 0:
return 0.0
# Count matching words in order (simplified)
common = len(set(a_words) & set(b_words))
return 1.0 - (common / max_len)
Шаблон 4: Отборочный надзор
На этапе выборки для надзора по шаблону 4 необходимо определить входные данные, ответственного за выполнение шага и критерии завершения перед внесением изменений в код. Операторы должны иметь возможность перезапустить шаг с известной точки контроля, не догадываясь о скрытом состоянии. Необходимо задокументировать как успешный, так и восстановительный сценарии работы. Повторные попытки, проверки человеком и обработка неработоспособных сообщений являются частью продукта, а не этапом последующей доработки. Внедрять утверждение человеком для операций, связанных с тратой денег или изменением производственных данных. Настройки во время компиляции не заменяют полноты бизнес-логики. На этапе выборки для надзора по шаблону 4 необходимо определить входные данные, ответственного за выполнение шага и критерии завершения перед внесением изменений в код. Операторы должны иметь возможность перезапустить шаг с известной точки контроля, не догадываясь о скрытом состоянии. Рассматривайте этот этап как контракт между входными данными и проверенными выходными результатами. Дайте названия соответствующим элементам, определите критерии успеха и не допускайте безответственного частичного завершения работы.
# harness/hitl/oversight_sampling.py
import boto3
import random
import time
import json
from typing import Optional
class OversightSampler:
"""
Routes a sample of autonomous actions to human review after execution.
Does not block the action. Catches quality drift and systematic errors.
Feeds into the Article 8 evaluation pipeline.
"""
def __init__(
self,
base_sample_rate: float = 0.05,
table_name: str = "agent-oversight-samples",
region: str = "us-east-1",
):
self.base_sample_rate = base_sample_rate
dynamodb = boto3.resource("dynamodb", region_name=region)
self.table = dynamodb.Table(table_name)
def should_sample(
self,
action_name: str,
agent_confidence: float = 1.0,
is_novel: bool = False,
) -> bool:
"""
Decides whether this action should be sampled for review.
Samples more aggressively for low-confidence and novel actions.
"""
rate = self.base_sample_rate
# Increase sampling for low confidence
if agent_confidence < 0.7:
rate = min(rate * 3, 1.0)
# Always sample novel actions
if is_novel:
return True
return random.random() < rate
def record_for_review(
self,
run_id: str,
action_name: str,
action_args: dict,
action_result: str,
agent_confidence: float,
):
"""Queues an executed action for human review."""
import uuid
self.table.put_item(Item={
"sample_id": str(uuid.uuid4()),
"run_id": run_id,
"action_name": action_name,
"action_args": json.dumps(action_args)[:2000],
"action_result": action_result[:2000],
"agent_confidence": str(agent_confidence),
"sampled_at": int(time.time()),
"review_status": "PENDING",
})
def record_review_outcome(
self,
sample_id: str,
was_correct: bool,
reviewer: str,
notes: Optional[str] = None,
):
"""
Records the human's assessment of a sampled action.
A pattern of incorrect actions should trigger a policy review.
"""
self.table.update_item(
Key={"sample_id": sample_id},
UpdateExpression=(
"SET review_status = :s, was_correct = :c, "
"reviewed_by = :r, review_notes = :n, reviewed_at = :at"
),
ExpressionAttributeValues={
":s": "REVIEWED",
":c": was_correct,
":r": reviewer,
":n": notes or "",
":at": int(time.time()),
}
)
def get_error_rate(self, action_name: str, days: int = 7) -> dict:
"""
Computes the error rate for a sampled action over a window.
If this exceeds threshold, the action's policy should be tightened.
"""
cutoff = int(time.time()) - (days * 86400)
response = self.table.scan(
FilterExpression="action_name = :a AND sampled_at > :c AND review_status = :s",
ExpressionAttributeValues={
":a": action_name,
":c": cutoff,
":s": "REVIEWED",
}
)
reviews = response.get("Items", [])
if not reviews:
return {"sufficient_data": False}
incorrect = sum(1 for r in reviews if not r.get("was_correct", True))
error_rate = incorrect / len(reviews)
return {
"sufficient_data": len(reviews) >= 10,
"sample_size": len(reviews),
"error_rate": round(error_rate, 3),
"recommendation": (
"TIGHTEN_POLICY" if error_rate > 0.1 else "MAINTAIN"
),
}
Обработка таймаутов и альтернативных решений
При работе над этапом обработки таймаутов и альтернативных решений сначала запишите условия работы: необходимые входные данные, сигнал о успешном выполнении и действия при частичной неудаче. Такой список поможет избежать ошибок при последующих изменениях кода. Записывайте время выполнения операций, а также стоимость токенов или запросов рядом с результатами их работы. Отслеживание затрат на раннем этапе предотвращает неожиданные счета при переходе от демо-среды к общедоступным средам. Выполняйте проверки после дорогостоящих операций. Функция возобновления работы не должна снова взимать плату за один и тот же вызов большой языковой модели, если оператор попытается выполнить последующий шаг.
# harness/hitl/timeout_handler.py
import boto3
import time
from typing import Literal
class ApprovalTimeoutHandler:
"""
Handles approval requests that exceed their timeout.
The timeout action is policy-defined per action type.
Runs as a scheduled Lambda, checking for expired pending requests.
"""
def __init__(
self,
approval_table: str = "agent-approval-requests",
region: str = "us-east-1",
):
dynamodb = boto3.resource("dynamodb", region_name=region)
self.table = dynamodb.Table(approval_table)
self.sns = boto3.client("sns", region_name=region)
def process_expired_requests(self, policies: dict) -> dict:
"""
Scans for pending requests past their timeout and applies
the policy-defined timeout action.
"""
now = int(time.time())
response = self.table.scan(
FilterExpression="#s = :pending",
ExpressionAttributeNames={"#s": "status"},
ExpressionAttributeValues={":pending": "PENDING"},
)
results = {"rejected": 0, "escalated": 0, "proceeded": 0}
for item in response.get("Items", []):
requested_at = int(item.get("requested_at", now))
action_name = item.get("action_name", "")
policy = policies.get(action_name)
if not policy:
continue
age = now - requested_at
if age < policy.timeout_seconds:
continue # not expired yet
timeout_action = policy.timeout_action
if timeout_action == "reject":
self._apply_timeout(item["request_id"], "REJECTED",
"Timed out without approval")
results["rejected"] += 1
elif timeout_action == "escalate":
self._escalate_expired(item)
results["escalated"] += 1
elif timeout_action == "proceed":
# Only for low-risk actions where the gate is advisory
self._apply_timeout(item["request_id"], "APPROVED",
"Auto-approved after timeout per policy")
results["proceeded"] += 1
return results
def _apply_timeout(self, request_id: str, status: str, note: str):
self.table.update_item(
Key={"request_id": request_id},
UpdateExpression="SET #s = :status, decision_note = :note, decided_at = :at",
ExpressionAttributeNames={"#s": "status"},
ExpressionAttributeValues={
":status": status,
":note": note,
":at": int(time.time()),
}
)
def _escalate_expired(self, item: dict):
# Notify a higher tier and extend the deadline
self.table.update_item(
Key={"request_id": item["request_id"]},
UpdateExpression="SET escalated = :t, escalated_at = :at",
ExpressionAttributeValues={":t": True, ":at": int(time.time())},
)
Хроника аудита
При работе на этапе аудита следов действий сначала запишите условия контракта: необходимые входные данные, сигнал о успешном выполнении и действия при частичной неудаче. Такой чек-лист помогает сохранять честность последующих изменений в коде. Храните конфигурацию отдельно от кода приложения. Файлы среды, хранилища секретов и флаги функций должны находиться в одном месте, чтобы операторы могли их аудитировать, не читая весь код. Создавайте контрольные точки после дорогостоящих операций. Функция возобновления работы не должна снова взимать плату за один и тот же вызов большой языковой модели, когда оператор пытается выполнить следующий шаг.
# harness/hitl/audit.py
import boto3
import time
import json
import hashlib
from typing import Optional
class HITLAuditLog:
"""
Immutable audit trail for all human-in-the-loop decisions.
Uses a hash chain so tampering is detectable.
Writes to DynamoDB with a separate append-only access pattern.
"""
def __init__(
self,
table_name: str = "agent-hitl-audit",
region: str = "us-east-1",
):
dynamodb = boto3.resource("dynamodb", region_name=region)
self.table = dynamodb.Table(table_name)
def record_decision(
self,
run_id: str,
decision_type: str, # "approval" | "rejection" | "escalation" | "edit"
action_name: str,
decided_by: str,
decision_details: dict,
previous_hash: Optional[str] = None,
) -> str:
"""
Records a decision in the audit log with a hash chain.
Returns the hash of this entry for chaining the next one.
"""
timestamp = int(time.time())
entry = {
"run_id": run_id,
"decision_type": decision_type,
"action_name": action_name,
"decided_by": decided_by,
"decision_details": json.dumps(decision_details),
"timestamp": timestamp,
"previous_hash": previous_hash or "genesis",
}
# Compute hash of this entry chained to the previous
entry_content = json.dumps(entry, sort_keys=True)
entry_hash = hashlib.sha256(entry_content.encode()).hexdigest()
self.table.put_item(Item={
"audit_id": f"{run_id}#{timestamp}#{entry_hash[:8]}",
**entry,
"entry_hash": entry_hash,
})
return entry_hash
def verify_chain(self, run_id: str) -> bool:
"""
Verifies the hash chain for a run's audit entries.
Returns True if the chain is intact, False if tampering is detected.
"""
response = self.table.query(
KeyConditionExpression="run_id = :rid",
ExpressionAttributeValues={":rid": run_id},
ScanIndexForward=True,
)
entries = sorted(response.get("Items", []), key=lambda x: x["timestamp"])
previous_hash = "genesis"
for entry in entries:
if entry.get("previous_hash") != previous_hash:
return False
# Recompute and verify
check_entry = {
k: entry[k] for k in
["run_id", "decision_type", "action_name", "decided_by",
"decision_details", "timestamp", "previous_hash"]
}
recomputed = hashlib.sha256(
json.dumps(check_entry, sort_keys=True).encode()
).hexdigest()
if recomputed != entry.get("entry_hash"):
return False
previous_hash = entry["entry_hash"]
return True
Проверка реальности в производственных условиях
При работе над этапом проверки реальности в производственных условиях сначала запишите условия работы системы: необходимые входные данные, сигнал о успешном выполнении и действия при частичной неудаче. Такой список поможет сохранять честность при последующих изменениях кода. Задокументируйте как успешный, так и восстановительный сценарии работы. Повторные попытки, проверки со стороны оператора и обработка некорректных сообщений являются частью продукта, а не элементами последующей доработки. Выполняйте контрольные точки после дорогостоящих операций. Механизм возобновления работы не должен повторно взимать плату за один и тот же вызов большой языковой модели, если оператор пытается выполнить следующий этап заново. При работе над этапом проверки реальности в производственных условиях сначала запишите условия работы системы: необходимые входные данные, сигнал о успешном выполнении и действия при частичной неудаче. Такой список поможет сохранять честность при последующих изменениях кода. Рассматривайте этот этап как контракт между входными данными и проверенными результатами. Дайте названия всем элементам, определите критерии успеха и не допускайте безответственного частичного выполнения задач.
Архитектура для справки
Этап разработки эталонной архитектуры работает наилучшим образом, когда его рассматривают как измеримую основу. Соберите один идеальный пример работы, один случай сбоя и записку о возврате к предыдущему состоянию перед расширением объёма работ. Записывайте временные показатели, а также стоимость токенов или запросов рядом с функциональными результатами. Отслеживание затрат на раннем этапе предотвращает неожиданные счёты при переходе от демо-среды к общедоступным средам. Сохраняйте структуру графа простой и типизированной; вложенные структуры скрывают информацию о том, какой узел заполнил тот или иной поле, и могут нарушить возобновление работы после прерываний.
Agent reaches an action
|
v
+---------------------------+
| ActionPolicyEngine |
| Look up action policy |
+---------------------------+
|
+-----+-----+-----------+-----------+
| | | | |
v v v v v
AUTONO SAMP APPROVAL APPROVAL DUAL
MOUS LING FOR NOVEL GATE APPROVAL
| | | | |
| | v v v
| | +--------------------------------+
| | | ApprovalGateManager |
| | | - create request |
| | | - LangGraph interrupt() |
| | | - notify approvers (SNS) |
| | | - persist state (DynamoDB) |
| | +--------------------------------+
| | |
| | Human decides via interface
| | |
| | +-----+-----+
| | | |
| | APPROVED REJECTED / TIMEOUT
| | | |
v v v v
+--------------------------------+
| Execute or Abort Action |
+--------------------------------+
|
v
+--------------------------------+
| HITLAuditLog (hash chain) |
| Immutable decision record |
+--------------------------------+
|
v
+--------------------------------+
| Feed to Article 8 eval + |
| collaborative edit learning |
+--------------------------------+
Стек эталонной инфраструктуры
Этап стека инфраструктуры для справочных целей работает наилучшим образом, когда его рассматривают как измеримую структуру. Соберите один эталонный протокол, один пример сбоя и запись о возврате к предыдущему состоянию перед расширением объема работ. Храните конфигурацию отдельно от кода приложения. Файлы среды, хранилища секретов и флаги функций должны находиться в одном месте, чтобы операторы могли их проверять, не читая весь стек данных. Сохраняйте состояние стека простым и типизированным. Вложенные структуры данных маскируют информацию о том, какой узел заполнил тот или иной поле, и приводят к нарушению возобновления работы после перерывов.
+-----------------------------+---------------------+------------------------------+
| Component | Technology | Role |
+-----------------------------+---------------------+------------------------------+
| Action Policy Engine | Custom | Per-action approval routing |
| | | based on risk profile |
+-----------------------------+---------------------+------------------------------+
| Pause / Resume | LangGraph interrupt | Durable suspend while |
| | + checkpointing | waiting for human |
+-----------------------------+---------------------+------------------------------+
| Approval Requests | DynamoDB | Pending request state, |
| | | decision records |
+-----------------------------+---------------------+------------------------------+
| Notifications | SNS | Alert approvers when a |
| | | decision is needed |
+-----------------------------+---------------------+------------------------------+
| Escalation | Custom + Haiku | Agent self-assessment of |
| | confidence model | when to hand off to human |
+-----------------------------+---------------------+------------------------------+
| Collaborative Edits | DynamoDB | Draft-edit-finalize flow, |
| | | edit pattern learning |
+-----------------------------+---------------------+------------------------------+
| Oversight Sampling | Custom + DynamoDB | Post-hoc review of a sample |
| | | of autonomous actions |
+-----------------------------+---------------------+------------------------------+
| Timeout Handling | Scheduled Lambda | Policy-defined action on |
| | | expired approval requests |
+-----------------------------+---------------------+------------------------------+
| Audit Trail | DynamoDB hash chain | Immutable, tamper-evident |
| | | record of all decisions |
+-----------------------------+---------------------+------------------------------+
| Auth for Approvers | CredentialManager | Role-based approval |
| | (Article 5) | authorization |
+-----------------------------+---------------------+------------------------------+
| Learning Loop | Article 8 eval | Edits and reviews feed the |
| | pipeline | improvement pipeline |
+-----------------------------+---------------------+------------------------------+
| Local Dev Alternative | Docker Compose + | Full HITL flow testable |
| | mock approver UI | without AWS |
+-----------------------------+---------------------+------------------------------+
Чек-лист операционной деятельности
На этапе чек-листа операционной деятельности необходимо заранее определить входные данные, ответственного за выполнение шага и критерии завершения перед внесением изменений в код. Операторы должны иметь возможность повторно выполнить шаг, исходя из известной точки контроля, без необходимости угадывать скрытое состояние системы.
Лучше использовать небольшие, тестируемые единицы вместо обширных скриптов. Когда какой-то шаг терпит неудачу, ошибка должна указывать на конкретную ответственность, а не на запутанную цепочку операций.
Внедряйте утверждение человека на этапах, связанных с тратой денег или изменением производственных данных. Подключение кода на этапе компиляции не гарантирует полноты решения бизнес-задач.
Напишите краткий руководство: как обновлять ключи, как опустошать очередь, как откатывать последнюю загрузку данных.
Рассматривайте этот этап как контракт между входными данными и проверенными выходными результатами. Укажите названия элементов, определите критерии успеха и не допускайте молчаливого частичного выполнения задач.
Внедряйте утверждение человека на этапах, связанных с тратой денег или изменением производственных данных. Подключение кода на этапе компиляции не гарантирует полноты решения бизнес-задач.
Перед внедрением данной стек-технологии необходимо заморозить версии, сохранить эталонный отчет для критической части работы и уточнить шаги возврата к предыдущему состоянию. В совместных средах требуются ограничения на скорость работы, проверки принадлежности пользователя и четко определенный ответственный за обновление секретов. Лучше выбирать надежность, даже если она кажется менее привлекательной, чем красивые одноразовые демонстрации.
Примечание для c9f5fabd2c2d: не храните ключи поставщика в репозитории, установите лимит токенов на одну сессию и сохраняйте отчеты рядом с фиксами для оценки, чтобы последующие замены моделей можно было сравнивать.