Практычныя прытамулі: Агентныя архітектуры — Стаття 6: Коардынацыя калькольніка агентаў
Практычныя прыказкі: Архітектуры на агентах — Старонка 6: Кантролюванне колькісці агентаў: контракты, перакрычанні та слоты для коду для команд, якія викорыстоўваюць гэты патерн.
Наступныя прыміткі паказваюць практычны шлях для розумэння тэмы «Агентныя архітектуры — Стаття 6: Шаблоны кантролю калькі ля мнагах агентах». Акцэнт ставіцца на контракты, пераконтроўкі і месца для падставлення коду, а не на мотывацыйныя аспекты. Калі працуеце на стадзіі агледжэння, спачатку запісайце контракт: неабяжныя вхідныя даны, сігнал успеху і тое, што выканаецца у разы частковага невясковасці. Такі список дапамагае залічваць пазнейшыя змены ў кодзе чыста і прозрачна. Запісвайце час выканання і вартасць токеноў або запытак праза функцыйнае рэзультат. Відразувыя даны пра вартасць запобегаюць неспакойным рахункам, калі процес пераходзіць з дэмаверсіі ў спяльныя среды.
Што вы знайдзеце тут
Этап «Што вы знайдзеце» працюе найкраща, калі яго розглядаць як вимерную паверхню. Запісаўце адна «золатая» транскрыпцыя, адзін прыклад неудачы і запіс пра вярнэнне да поперадньага стану прычым перад расшырэнням масштаба. Зберагачце конфігурацыю паза кодам прыемлена. Файлы сяродавішча, хранальнікі секрэтных дадзеных і флагі функцыйяў должны знаходзіцца ў аднам месцы, куды аператары можаць адбавляць контроль без неабяжнага чытання всіх дадзеных. Зберагачце стан графа ў простам і типаваным формате. Вкладныя блокі маскуюць інфармацыю пра тое, який вузел запісаў канкрэтны поле, і спакоююць працу пасля перарываў.
Чаму адзін агент досягае свайго максімуму
Этап «Чаму адзін агент» работае наяўней, калі яго спрыяваць як вимерную паверхню. Зафіксавайце адна ідеальная транскрыпцыя, адзін прыклад неудачы і запіс пра вярнэнне да пачатковага стану перш чым расширваць масштабы. Дакументавайце як шлях успеху, так і шлях вяснавання разам. Перапрыбуткі, людзкія контрольныя пункты і обработка некоректных паведамленняў є частью продукту, а не наступным этапам дапрацоўкі. Храніце стан графа ў простам і типаваным формате. Вкладзеныя блокі маскуюць інфармацыю пра тое, який вузол запісаў канкрэтны поле, і спакшуюць продовжэнне роботы пасля перерываў.
Таксанамія шаблонаў калькольніках агентаў
Этап «Таксанамія кальмінацыйных агентаў» працуе наякрасцей, калі яго спрыяваць як мерыемую паверхню. Зафіксавайце адна ідеальная транскрыпцыю, адзін прыклад неудачы і запіс пра вярнэнне да пачатковага стану перад расшырэнням масштаба. Валіце маленькія, тэставаныя елементы замест большых скрыптов. Калі якісь крок не выйшае, прычына неудачы павінна вказываць на адную адпаведальнасць, а не на заплутаны ланцюг задач. Рэзультаты роботы графа павінны быць простымі та з адначытаемымі дадзеннямі. Вкладзеныя блокі маскуюць інфармацыю пра тое, який вузел запісаў канкрэтны поле, і спакшуюць продовжэнне роботы пасля перерываў. Этап «Таксанамія кальмінацыйных агентаў» працуе наякрасцей, калі яго спрыяваць як мерыемую паверхню. Зафіксавайце адна ідеальная транскрыпцыю, адзін прыклад неудачы і запіс пра вярнэнне да пачатковага стану перад расшырэнням масштаба. Запісвайце часы виконання та косты токеноў або запытак праза функцыйнае рэзультаты. Відкрытасць костаў з самага пачатку запобегае неспакойным рахункам, калі процес пераходзіць з дэмаверсіі ў спяльныя сераўы.
+--------------------+--------------------------------------------------+------------------+
| Pattern | Structure | Best For |
+--------------------+--------------------------------------------------+------------------+
| Supervisor-Worker | One orchestrator decomposes + delegates | Complex tasks |
| | N workers execute specialized subtasks | needing expert |
| | | decomposition |
+--------------------+--------------------------------------------------+------------------+
| Pipeline | Agent A -> Agent B -> Agent C (sequential) | Transformation |
| | Each processes the previous output | chains, ETL-like |
| | | workflows |
+--------------------+--------------------------------------------------+------------------+
| Parallel Fan-out | Orchestrator sends same/related task to N agents | Research, |
| | Results aggregated into single output | analysis tasks |
| | | that parallelize |
+--------------------+--------------------------------------------------+------------------+
| Debate / Critique | Agent A produces solution, Agent B critiques | High-stakes |
| | Agent C synthesizes or adjudicates | outputs needing |
| | | adversarial QA |
+--------------------+--------------------------------------------------+------------------+
Шаблон 1: Надзірнік-рабочы
Для стадіі «Наглядач-рабочы» патэрна 1 неабяжна ўзначыць вхідныя даны, адпаведальнага за крок і критэрыя завершэння пры перадзеіснаванні коду. Аператары должны магчымаць перзапуск кроку з вядомай точкі контролю, не прабуючы спадарожваць схованы стан. Конфігурацыю трэба захаваць паза кодам прыемліка. Файлы сераўнавальнага сераўса, хранілішчы секрэтных данных і флагі функцыйяў должны знаходзіцца ў аднам месцы, якое аператары можаць пераглядаць, не чытаяўшы весь ланцуг задач. Прызначаць людскія апраўды для рэшэнняеў, якія витрачаюць грошы або зменяюць даны праўай працы. Підключэнняе пад час компілявання не ўзначае повнайшага адпрацоўвання бізнес-логікі.
┌─────────────────────────────┐
│ SUPERVISOR AGENT │
│ (task decomposition + │
│ result synthesis) │
└──────┬───────────┬──────────┘
│ │
┌────────▼──┐ ┌────▼──────────┐ ┌──────────────┐
│ Worker A │ │ Worker B │ │ Worker C │
│ (code │ │ (security │ │ (test │
│ analysis)│ │ review) │ │ coverage) │
└────────────┘ └───────────────┘ └──────────────┘
# harness/multi_agent/supervisor.py
from typing import Literal, TypedDict, Annotated, List
from langgraph.graph import StateGraph, END, START
from langgraph.graph.message import add_messages
from langchain_core.messages import BaseMessage, HumanMessage, SystemMessage
from langchain_aws import ChatBedrock
from pydantic import BaseModel
import boto3
class SupervisorState(TypedDict):
messages: Annotated[List[BaseMessage], add_messages]
task_spec: str
subtasks: List[dict] # decomposed work items
worker_results: dict # keyed by subtask ID
current_worker: str # which worker is active
synthesis_complete: bool
agent_run_id: str
class SubtaskAssignment(BaseModel):
"""Structured output from supervisor decomposition."""
subtask_id: str
worker_type: Literal["code_analyst", "security_reviewer", "test_evaluator"]
description: str
depends_on: List[str] # subtask IDs this depends on
priority: int
SUPERVISOR_SYSTEM_PROMPT = """
You are an orchestrator agent. You do not write code or perform analysis yourself.
Your job is to:
1. Break the task into discrete subtasks
2. Assign each subtask to the correct specialist worker
3. Track dependencies between subtasks
4. Synthesize worker outputs into a coherent final result
Available workers:
- code_analyst: Reads and analyzes code structure, dependencies, patterns
- security_reviewer: Evaluates security implications, checks against CVEs
- test_evaluator: Assesses test coverage, identifies gaps
When decomposing, be specific. A subtask description like "analyze the auth module"
is useful. "analyze the code" is not.
Respond in JSON when asked to decompose. Respond in prose when asked to synthesize.
"""
def build_supervisor(region: str = "us-east-1") -> callable:
bedrock = boto3.client("bedrock-runtime", region_name=region)
# Supervisor uses the heavier model — it's doing strategic reasoning
supervisor_model = ChatBedrock(
client=bedrock,
model_id="anthropic.claude-3-7-sonnet-20250219-v1:0",
model_kwargs={
"temperature": 0.2,
"max_tokens": 8000,
"thinking": {"type": "enabled", "budget_tokens": 5000}
}
)
def supervisor_node(state: SupervisorState) -> SupervisorState:
if not state.get("subtasks"):
# First pass: decompose the task
response = supervisor_model.invoke([
SystemMessage(content=SUPERVISOR_SYSTEM_PROMPT),
HumanMessage(content=f"Decompose this task into subtasks:\n{state['task_spec']}")
])
import json
try:
subtasks = json.loads(response.content)
if isinstance(subtasks, dict) and "subtasks" in subtasks:
subtasks = subtasks["subtasks"]
except json.JSONDecodeError:
subtasks = []
return {**state, "subtasks": subtasks}
# All workers done: synthesize
results_summary = "\n\n".join([
f"=== {worker} ===\n{result}"
for worker, result in state["worker_results"].items()
])
synthesis_prompt = f"""
Original task: {state['task_spec']}
Worker results:
{results_summary}
Synthesize these into a coherent final report. Highlight conflicts between
worker findings and make clear recommendations.
"""
response = supervisor_model.invoke([
SystemMessage(content=SUPERVISOR_SYSTEM_PROMPT),
HumanMessage(content=synthesis_prompt)
])
return {
**state,
"messages": state["messages"] + [response],
"synthesis_complete": True,
}
return supervisor_node
def route_to_worker(state: SupervisorState) -> str:
"""
Routes to the next worker with unfinished subtasks.
Returns END when all subtasks are complete and synthesis is done.
"""
if state.get("synthesis_complete"):
return END
# Find next unfinished subtask whose dependencies are met
completed = set(state.get("worker_results", {}).keys())
for subtask in state.get("subtasks", []):
sid = subtask["subtask_id"]
if sid in completed:
continue
deps = set(subtask.get("depends_on", []))
if deps.issubset(completed):
return subtask["worker_type"] # route to this worker
# All subtasks done, back to supervisor for synthesis
return "supervisor"
# harness/multi_agent/workers.py
from langchain_aws import ChatBedrock
from langchain_core.messages import SystemMessage, HumanMessage
import boto3
CODE_ANALYST_PROMPT = """
You are a code analysis specialist. You receive specific, bounded analysis tasks.
Focus only on what you were asked to analyze. Do not expand scope.
Return structured findings: what you found, confidence level, and specific evidence.
"""
SECURITY_REVIEWER_PROMPT = """
You are a security review specialist. You look for vulnerabilities, insecure patterns,
and CVE-relevant code. Reference specific CWE numbers when applicable.
Return structured findings with severity levels (CRITICAL, HIGH, MEDIUM, LOW).
"""
TEST_EVALUATOR_PROMPT = """
You are a test coverage specialist. You assess test quality and identify gaps.
Focus on: coverage percentage where available, missing edge cases, and untested paths.
Return structured findings with specific test gaps and suggested test cases.
"""
WORKER_PROMPTS = {
"code_analyst": CODE_ANALYST_PROMPT,
"security_reviewer": SECURITY_REVIEWER_PROMPT,
"test_evaluator": TEST_EVALUATOR_PROMPT,
}
def build_worker(worker_type: str, region: str = "us-east-1") -> callable:
bedrock = boto3.client("bedrock-runtime", region_name=region)
# Workers use the faster model — they execute a specific, bounded task
worker_model = ChatBedrock(
client=bedrock,
model_id="anthropic.claude-3-5-sonnet-20241022-v2:0",
model_kwargs={"temperature": 0, "max_tokens": 4000}
)
system_prompt = WORKER_PROMPTS[worker_type]
def worker_node(state: SupervisorState) -> SupervisorState:
# Find the subtask assigned to this worker type
completed = set(state.get("worker_results", {}).keys())
current_subtask = None
for subtask in state["subtasks"]:
if subtask["worker_type"] == worker_type and subtask["subtask_id"] not in completed:
deps = set(subtask.get("depends_on", []))
if deps.issubset(completed):
current_subtask = subtask
break
if not current_subtask:
return state
# Pass relevant prior results as context if this task has dependencies
context = ""
if current_subtask.get("depends_on"):
for dep_id in current_subtask["depends_on"]:
if dep_id in state.get("worker_results", {}):
context += f"\nPrevious analysis ({dep_id}):\n{state['worker_results'][dep_id]}\n"
prompt = f"Task: {current_subtask['description']}"
if context:
prompt = f"Prior context:{context}\n\n{prompt}"
response = worker_model.invoke([
SystemMessage(content=system_prompt),
HumanMessage(content=prompt)
])
updated_results = {**state.get("worker_results", {})}
updated_results[current_subtask["subtask_id"]] = response.content
return {**state, "worker_results": updated_results}
return worker_node
# harness/multi_agent/supervisor_graph.py
from langgraph.graph import StateGraph, END, START
from langgraph.checkpoint.memory import MemorySaver
from harness.multi_agent.supervisor import SupervisorState, build_supervisor, route_to_worker
from harness.multi_agent.workers import build_worker
def build_supervisor_graph(region: str = "us-east-1"):
graph = StateGraph(SupervisorState)
graph.add_node("supervisor", build_supervisor(region))
graph.add_node("code_analyst", build_worker("code_analyst", region))
graph.add_node("security_reviewer", build_worker("security_reviewer", region))
graph.add_node("test_evaluator", build_worker("test_evaluator", region))
graph.add_edge(START, "supervisor")
graph.add_conditional_edges(
"supervisor",
route_to_worker,
{
"code_analyst": "code_analyst",
"security_reviewer": "security_reviewer",
"test_evaluator": "test_evaluator",
END: END,
}
)
# All workers route back to supervisor after completing their subtask
for worker in ["code_analyst", "security_reviewer", "test_evaluator"]:
graph.add_edge(worker, "supervisor")
return graph.compile(checkpointer=MemorySaver())
Патэрн 2: Оркестрацыя пайплайнаў
Для стадіі оркестрацыі Pipeline Orchestration Model 2 неабяжна прадзефінаваць вхідныя даны, адпраўніка крока і критэрыя завершэння пры перадзеўці коду. Аперацыйныя працавнікі должны магчыма было перзапускаць крок з вядомай точкі контролю, не падозрываючы прыхованы стан. Неабяжна задокументаваць як шлях успеху, так і шлях вярнення да нормальнага стану. Перапрыбуткі, людзкія пераказы і обробка некоректных паведамленняў є часткай продукту, а не элементамі пазнейшага дапрацоўкі. Неабяжна застосаваць людзкую апраўдку для тых крокаў, якія выкарыстоўваюць грошы або зменяюць даны ў працэсе виробніцтва. Працэсы, якія выкананы ў часе компіляцыі, не є падставай для стверджэння аб повнасці продукту.
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
│ Agent A │ │ Agent B │ │ Agent C │
│ (extraction) │────>│ (enrichment) │────>│ (validation) │
└───────────────┘ └───────────────┘ └───────────────┘
output A output B output C
becomes input B becomes input C final result
# harness/multi_agent/pipeline.py
from typing import TypedDict, Annotated, List, Optional, Any
from langgraph.graph import StateGraph, END, START
from langchain_core.messages import BaseMessage, HumanMessage, SystemMessage
from langchain_aws import ChatBedrock
import boto3
class PipelineState(TypedDict):
original_input: str
stage_outputs: List[dict] # accumulates each stage's compressed result
current_stage: int
final_output: Optional[str]
agent_run_id: str
def compress_for_handoff(full_output: str, model: ChatBedrock) -> str:
"""
Compresses a stage's full output into a structured handoff summary.
This is the core of pipeline context management — the next agent gets
the substance, not the reasoning trace.
"""
response = model.invoke([
SystemMessage(content="""
Compress the following agent output into a structured handoff summary.
Include: key findings, decisions made, artifacts produced, and what the
next stage needs to know. Discard reasoning traces and intermediate steps.
Target length: 20% of original. Use bullet points for clarity.
"""),
HumanMessage(content=f"Compress this:\n\n{full_output}")
])
return response.content
def build_pipeline_stage(
stage_name: str,
system_prompt: str,
region: str = "us-east-1"
) -> callable:
bedrock = boto3.client("bedrock-runtime", region_name=region)
model = ChatBedrock(
client=bedrock,
model_id="anthropic.claude-3-5-sonnet-20241022-v2:0",
model_kwargs={"temperature": 0, "max_tokens": 6000}
)
# Cheaper model for compression — this is mechanical, not creative
compressor = ChatBedrock(
client=bedrock,
model_id="anthropic.claude-haiku-3-5",
model_kwargs={"temperature": 0, "max_tokens": 2000}
)
def stage_node(state: PipelineState) -> PipelineState:
# Build context from compressed prior stage outputs only
prior_context = ""
for past_stage in state.get("stage_outputs", []):
prior_context += f"\n### {past_stage['stage']} output:\n{past_stage['compressed_output']}\n"
prompt = f"Original task: {state['original_input']}\n"
if prior_context:
prompt += f"\nPrior stage results:\n{prior_context}\n"
prompt += f"\nNow perform your stage: {stage_name}"
response = model.invoke([
SystemMessage(content=system_prompt),
HumanMessage(content=prompt)
])
full_output = response.content
# Compress before storing — next stage won't see raw output
compressed = compress_for_handoff(full_output, compressor)
updated_outputs = list(state.get("stage_outputs", []))
updated_outputs.append({
"stage": stage_name,
"full_output": full_output,
"compressed_output": compressed,
})
return {
**state,
"stage_outputs": updated_outputs,
"current_stage": state.get("current_stage", 0) + 1,
}
return stage_node
Model 3: Паралельны фан-аут / фан-ін
Для стадіі Parallel Fan-out патэрна 3 неабяжна ўзначыць вхідныя даны, адпаведальнага за крок і крэтыяры завершэння пры змены коду. Аперацыйныя працавнікі должны магчымае перайсці на выкананне кроку з вядомага пункта контролю, не спрабоўваючы здагадвацца пра схованы стан. Лепш выбіраць маленькія, тэставаныя елементы замест большых скрыптов. Калі крок не выканаецца, прычына нехарактэрызуе адну конкрэтную адпаведальнасць, а не заплутаны ланцужок задач. Неабяжна ўключыць людзкія апраўленні для тых крокаў, якія выкарыстоўваюць грошы або зміняюць даны праўдзівай працы. Компіляцыйныя наладкі не ўзначаюць павнайстковасці бізнес-процэсаў. Для стадіі Parallel Fan-out патэрна 3 неабяжна ўзначыць вхідныя даны, адпаведальнага за крок і крэтыяры завершэння пры змены коду. Аперацыйныя працавнікі должны магчымае перайсці на выкананне кроку з вядомага пункта контролю, не спрабоўваючы здагадвацца пра схованы стан. Запісвайце час выканання, а таксу токенаў аб запытавань разам з функцыйнальнымі рэзультатамі. Відразлівасць костаў з самага пачатку запобегае неспакойным рахункам, калі процес пераходзіць з дэмаверсіі ў спяльную среду.
Этапы роботы.
┌──────────────────┐
│ ORCHESTRATOR │
│ (task splitter) │
└──┬───┬───┬───┬──┘
│ │ │ │
┌──────────▼┐ ┌▼─┐ ┌▼──┐ ┌▼──────────┐
│ Agent 1 │ │A2│ │A3 │ │ Agent 4 │
│ (region A)│ │ │ │ │ │ (region D)│
└──────────┬┘ └┬─┘ └┬──┘ └┬──────────┘
│ │ │ │
┌──▼───▼────▼─────▼──┐
│ AGGREGATOR │
│ (result merger) │
└────────────────────┘
# harness/multi_agent/fanout.py
import asyncio
from typing import List, TypedDict, Annotated, Optional
from langchain_aws import ChatBedrock
from langchain_core.messages import SystemMessage, HumanMessage
import boto3
class FanoutResult(TypedDict):
agent_id: str
input_slice: str
output: str
success: bool
error: Optional[str]
async def run_agent_async(
agent_id: str,
input_slice: str,
system_prompt: str,
model: ChatBedrock,
) -> FanoutResult:
"""Run a single agent asynchronously."""
try:
response = await model.ainvoke([
SystemMessage(content=system_prompt),
HumanMessage(content=input_slice)
])
return FanoutResult(
agent_id=agent_id,
input_slice=input_slice,
output=response.content,
success=True,
error=None,
)
except Exception as e:
return FanoutResult(
agent_id=agent_id,
input_slice=input_slice,
output="",
success=False,
error=str(e),
)
async def fan_out(
task_slices: List[str],
system_prompt: str,
region: str = "us-east-1",
max_concurrent: int = 5, # don't hammer Bedrock rate limits
) -> List[FanoutResult]:
"""
Runs agents concurrently with a semaphore to cap parallelism.
max_concurrent protects against Bedrock throttling — you
will hit rate limits if you fire 20 concurrent requests.
"""
bedrock = boto3.client("bedrock-runtime", region_name=region)
model = ChatBedrock(
client=bedrock,
model_id="anthropic.claude-3-5-sonnet-20241022-v2:0",
model_kwargs={"temperature": 0, "max_tokens": 4000}
)
semaphore = asyncio.Semaphore(max_concurrent)
async def bounded_run(agent_id, slice_content):
async with semaphore:
return await run_agent_async(agent_id, slice_content, system_prompt, model)
tasks = [
bounded_run(f"agent_{i}", slice_content)
for i, slice_content in enumerate(task_slices)
]
return await asyncio.gather(*tasks)
def aggregate_results(
results: List[FanoutResult],
aggregator_model: ChatBedrock,
aggregation_strategy: str = "synthesize",
) -> str:
"""
Merges parallel agent outputs.
aggregation_strategy options:
- "synthesize": ask a model to merge findings coherently
- "concat": simple concatenation (fast, no model call needed)
- "vote": majority-vote for classification tasks
"""
if aggregation_strategy == "concat":
successful = [r for r in results if r["success"]]
return "\n\n---\n\n".join(r["output"] for r in successful)
failed = [r for r in results if not r["success"]]
successful = [r for r in results if r["success"]]
if failed:
# Log partial failures but don't crash — partial results are usually useful
for f in failed:
print(f"Agent {f['agent_id']} failed: {f['error']}")
outputs_for_synthesis = "\n\n".join([
f"[Agent {r['agent_id']}]:\n{r['output']}"
for r in successful
])
response = aggregator_model.invoke([
SystemMessage(content="""
You are a results aggregator. You receive outputs from multiple parallel agents
that each analyzed a different slice of the same problem. Your job is to:
1. Identify common findings across agents
2. Surface unique findings from individual agents
3. Flag any contradictions between agents and explain which to trust
4. Produce a single coherent output as if one expert had analyzed everything
Do not simply concatenate. Actively synthesize.
"""),
HumanMessage(content=f"Synthesize these {len(successful)} agent outputs:\n\n{outputs_for_synthesis}")
])
return response.content
# Convenience wrapper for synchronous callers
def run_parallel_analysis(
task_slices: List[str],
system_prompt: str,
region: str = "us-east-1",
) -> str:
results = asyncio.run(fan_out(task_slices, system_prompt, region))
bedrock = boto3.client("bedrock-runtime", region_name=region)
aggregator = ChatBedrock(
client=bedrock,
model_id="anthropic.claude-3-7-sonnet-20250219-v1:0",
model_kwargs={"temperature": 0, "max_tokens": 8000}
)
return aggregate_results(results, aggregator)
Шаблон 4: Дыялог/Крітыка
Калі працуеце над этапам дыялогу і крітыкі па шаблоне 4, спачатку запісайце умовы: неабяжлівыя данні, сігнал успеху і тое, што выходзіць у случае частковага нявыпання. Такі список контроля дапамагае заліцвачыць змяны ў кодзе. Зберагаюце настройкі за межамі коду прыемліка. Файлы сэрвеіру, храненнія секретных данных і флагі функцый належыць у аднам месца, якое аператары можаць пераглядаць без неабяжлівага чытання всей структуры. Ставяце контрольныя пункты пасля дорогіх крокаў. Система вярнення не должна занова ставіць плату за той самы вызов LLM, калі аператар перапрыявляе задачу да наступнага вузла.
┌─────────────┐ ┌─────────────┐
│ Proposer │ │ Challenger │
│ (solution │ │ (solution │
│ attempt 1)│ │ attempt 2)│
└──────┬──────┘ └──────┬──────┘
│ │
└──────────┬─────────────┘
▼
┌─────────────────┐
│ ADJUDICATOR │
│ (critique + │
│ synthesis) │
└─────────────────┘
# harness/multi_agent/debate.py
from dataclasses import dataclass
from typing import Optional
from langchain_aws import ChatBedrock
from langchain_core.messages import SystemMessage, HumanMessage
import boto3
@dataclass
class DebateResult:
proposer_solution: str
challenger_solution: str
adjudication: str
final_recommendation: str
agreement_level: str # HIGH | MEDIUM | LOW | CONTRADICTION
PROPOSER_PROMPT = """
You are a solution proposer. Approach the problem carefully and produce your best
solution. Explain your reasoning. Do not hedge excessively — commit to a specific answer.
"""
CHALLENGER_PROMPT = """
You are a solution challenger. You will receive a problem that another agent has
already attempted. Produce your own independent solution WITHOUT seeing their work.
Approach this fresh. Your goal is not to contradict — it's to find the best solution.
"""
ADJUDICATOR_PROMPT = """
You are an adjudicator reviewing two independent solutions to the same problem.
Your job:
1. Identify where the two solutions agree (these are likely correct)
2. Identify where they diverge (these need careful evaluation)
3. For each divergence, evaluate which solution is better and why
4. Produce a final synthesis that takes the best of both
Be direct about contradictions. Do not smooth over genuine disagreements —
surface them clearly so the human reviewer can make a judgment call.
Rate the agreement level: HIGH (minor differences), MEDIUM (some significant
divergences), LOW (fundamentally different approaches), or CONTRADICTION
(mutually exclusive conclusions).
"""
def run_debate(
problem: str,
region: str = "us-east-1",
) -> DebateResult:
bedrock = boto3.client("bedrock-runtime", region_name=region)
heavy_model = ChatBedrock(
client=bedrock,
model_id="anthropic.claude-3-7-sonnet-20250219-v1:0",
model_kwargs={
"temperature": 0.3, # slight temperature for independent solutions
"max_tokens": 6000,
"thinking": {"type": "enabled", "budget_tokens": 4000}
}
)
# Proposer works the problem
proposer_response = heavy_model.invoke([
SystemMessage(content=PROPOSER_PROMPT),
HumanMessage(content=problem)
])
proposer_solution = proposer_response.content
# Challenger works the same problem independently
# Note: Challenger does NOT see Proposer's solution
challenger_response = heavy_model.invoke([
SystemMessage(content=CHALLENGER_PROMPT),
HumanMessage(content=problem)
])
challenger_solution = challenger_response.content
# Adjudicator sees both and synthesizes
adjudicator_response = heavy_model.invoke([
SystemMessage(content=ADJUDICATOR_PROMPT),
HumanMessage(content=f"""
Problem: {problem}
Solution A (Proposer):
{proposer_solution}
Solution B (Challenger):
{challenger_solution}
Adjudicate and synthesize.
""")
])
adjudication = adjudicator_response.content
# Extract agreement level from adjudication
agreement_level = "MEDIUM"
for level in ["CONTRADICTION", "LOW", "HIGH", "MEDIUM"]:
if level in adjudication.upper():
agreement_level = level
break
return DebateResult(
proposer_solution=proposer_solution,
challenger_solution=challenger_solution,
adjudication=adjudication,
final_recommendation=adjudication,
agreement_level=agreement_level,
)
Перадача задач між агентамі і передача контэксту
Калі працуеце над стадзіямі «Перадача межы агентам» і «Контэкст», спачатку запісайце умовы кантракту: неабяжлівыя даны, сигнал успеху і тое, што выканаецца у разе частковага нявыполнення. Такі список контроля дапамагае заліцьварыць пасляэйшныя змены коду. Документавайце як шлях успеху, так і шлях вярнення да нормальнага стану. Перапрыбуткі, людзкія контрольныя пункты і обработка некоректных паведамленняў ёсцю частью продукту, а не пасляэйшным дапрацоўкам. Стварайце контрольныя пункты пасля дорогіх крокаў. Система вярнення не павінна знову ставіць плату за той самы вызов LLM, калі аператар перапрыбуе пазнейшы вузел.
# harness/multi_agent/handoff.py
from dataclasses import dataclass, field
from typing import Any, Optional
from langchain_core.messages import HumanMessage
@dataclass
class AgentHandoff:
"""
Structured context passed between agents.
The split between result and trace is deliberate: downstream agents
need the result, not the full reasoning history. Keeping them separate
lets each agent decide how much context it wants to consume.
"""
source_agent: str
task_completed: str
result_summary: str # compressed, structured result
artifacts: dict = field(default_factory=dict) # files, code, structured data
reasoning_trace: Optional[str] = None # full trace, passed only if downstream needs it
confidence: str = "MEDIUM" # HIGH | MEDIUM | LOW
flags: list = field(default_factory=list) # NEEDS_REVIEW, PARTIAL_RESULT, etc.
def to_context_message(self, include_trace: bool = False) -> HumanMessage:
"""
Converts handoff to a HumanMessage for injection into next agent's context.
include_trace=True only when the downstream agent genuinely needs the reasoning.
"""
content = f"""
[HANDOFF FROM: {self.source_agent}]
Task completed: {self.task_completed}
Confidence: {self.confidence}
Flags: {', '.join(self.flags) if self.flags else 'none'}
Result summary:
{self.result_summary}
"""
if self.artifacts:
content += f"\nArtifacts available:\n"
for key, value in self.artifacts.items():
if isinstance(value, str) and len(value) < 500:
content += f" {key}: {value}\n"
else:
content += f" {key}: [available, {type(value).__name__}]\n"
if include_trace and self.reasoning_trace:
content += f"\nFull reasoning trace:\n{self.reasoning_trace}"
return HumanMessage(content=content)
Зміцнэнне продукту для працы
Калі працуеце над стадзіяй ударожэння працы ў продакшэне, спачатку запісайце «кантракт»: неабяжныя даны, сігнал успеху і тое, што выходзіць у разе частковага абякання. Такі список перакладоў заходзіць пазнейшыя змены коду ад нечыстасці. Валіце маленькія, тэставаныя елементы замест большых скрыптов. Калі якісь крок абякае, абяканне павінна вказваць на адную адпаведальнасць, а не на заплутаны ланцюг задач. Зробіце перапактаванне пасля дорогіх крокаў. Система не павинна зноў нарахоўваць адпаведную плата за вызов LLM, калі аператар перапрыяўляе роботу да наступнага вузла. Калі працуеце над стадзіяй ударожэння працы ў продакшэне, спачатку запісайце «кантракт»: неабяжныя даны, сігнал успеху і тое, што выходзіць у разе частковага абякання. Такі список перакладоў заходзіць пазнейшыя змены коду ад нечыстасці. Запісвайце час выканання і кост токеноў або запытаў праза функцыйнае рэзультат. Відкрытыя даны пра косцы з’являюцца неспадзейаных нарахоўкаў, калі процес пераходзіць з дэмаверсіі ў спяльныя сераўы.
Запобежэнне вялікам косцам
Этап запобежчыцы взрывнаму адрысаванню костаў работае наякрацэ, калі яго спрыяваць як мерымабельную паверхню. Зафіксавайце адны ідеальны прыклад, адзін прыклад неудачы і запіс пра вярнэнне да пачатковага стану перш чым расширваць сферу дзеяння. Зберагаюце настройкі пазырочна ад коду прыемліка. Файлы сераўнавальнага сяродовішча, хранілішчы секрэтных дадзеных і флагі функцыйяў должны знаходзіцца ў аднам месцы, куды аператары можаць адбавляць контроль без неабяжнага чытання всіх дадзеных. Зберагаюце стан графа ў простам і типаваным формате. Вярстакаваныя блокі дадзеных маскуюць інфармацыю пра тое, який вузел запісаў канкрэтны поле, і спакойваюць працу пасля перарываў.
# harness/multi_agent/budget.py
import boto3
import time
from decimal import Decimal
class AgentBudgetGuard:
"""
Tracks cumulative cost and agent count per run.
Hard-stops execution when limits are exceeded.
"""
def __init__(
self,
max_agents_per_run: int = 10,
max_total_tokens: int = 500_000,
table_name: str = "agent-budget-state",
region: str = "us-east-1",
):
self.max_agents = max_agents_per_run
self.max_tokens = max_total_tokens
self.table = boto3.resource("dynamodb", region_name=region).Table(table_name)
def register_agent_spawn(self, run_id: str, agent_id: str) -> bool:
"""
Returns True if spawn is allowed, False if budget exceeded.
Call this before spawning any sub-agent.
"""
response = self.table.update_item(
Key={"run_id": run_id},
UpdateExpression="SET agent_count = if_not_exists(agent_count, :z) + :inc",
ExpressionAttributeValues={":z": 0, ":inc": 1},
ReturnValues="UPDATED_NEW",
)
new_count = int(response["Attributes"]["agent_count"])
if new_count > self.max_agents:
raise AgentBudgetExceededError(
f"Run {run_id} attempted to spawn agent #{new_count}, "
f"but max_agents_per_run is {self.max_agents}. "
f"Either the supervisor is over-decomposing, or there is a spawn loop."
)
return True
def record_token_usage(self, run_id: str, tokens_used: int):
response = self.table.update_item(
Key={"run_id": run_id},
UpdateExpression="SET total_tokens = if_not_exists(total_tokens, :z) + :inc",
ExpressionAttributeValues={":z": 0, ":inc": tokens_used},
ReturnValues="UPDATED_NEW",
)
total = int(response["Attributes"]["total_tokens"])
if total > self.max_tokens:
raise AgentBudgetExceededError(
f"Run {run_id} consumed {total:,} tokens, exceeding limit of {self.max_tokens:,}."
)
class AgentBudgetExceededError(Exception):
pass
Адкрыцья застрэйвання
Этап выявлення застою працюе найэфектыўней, калі яго розглядаць як вимерную плошчу. Запісаце адны ідеальны прыклад роботы, адзін прыклад неудачы і прыметкі па адкатуванню перш чым расширваць сферу дзеяння. Документаваце як шлях успеху, так і шлях вяснавання разам. Перапрыбуткі, людзкія контралі і обработка некоректных паведамленняў є частью продукту, а не наступным етапам дорабкі. Храніце стан графа ў простам і типаваным формате. Вкладзеныя блокі маскуюць, який вузел запісаў якое поле, і спакоююць продовжэнне роботы пасля перерываў.
# harness/multi_agent/deadlock.py
import boto3
import time
from typing import List
class DeadlockDetector:
"""
Tracks the delegation chain per run and detects cycles.
Stored in DynamoDB so it works across parallel agent branches.
"""
def __init__(self, table_name: str = "agent-delegation-chain", region: str = "us-east-1"):
self.table = boto3.resource("dynamodb", region_name=region).Table(table_name)
def record_delegation(self, run_id: str, from_agent: str, to_agent: str):
"""
Records a delegation event and checks for cycles.
Raises DeadlockDetectedError if a cycle is found.
"""
self.table.put_item(Item={
"run_id": run_id,
"delegation_id": f"{from_agent}->{to_agent}-{int(time.time())}",
"from_agent": from_agent,
"to_agent": to_agent,
"timestamp": int(time.time()),
})
chain = self._get_delegation_chain(run_id)
if self._has_cycle(chain):
cycle_description = self._describe_cycle(chain)
raise DeadlockDetectedError(
f"Delegation cycle detected in run {run_id}: {cycle_description}. "
f"Check supervisor decomposition logic for circular dependencies."
)
def _get_delegation_chain(self, run_id: str) -> List[tuple]:
response = self.table.query(
KeyConditionExpression="run_id = :rid",
ExpressionAttributeValues={":rid": run_id}
)
return [(item["from_agent"], item["to_agent"]) for item in response.get("Items", [])]
def _has_cycle(self, chain: List[tuple]) -> bool:
graph = {}
for from_a, to_a in chain:
graph.setdefault(from_a, set()).add(to_a)
visited, rec_stack = set(), set()
def dfs(node):
visited.add(node)
rec_stack.add(node)
for neighbor in graph.get(node, []):
if neighbor not in visited:
if dfs(neighbor): return True
elif neighbor in rec_stack:
return True
rec_stack.discard(node)
return False
return any(dfs(node) for node in graph if node not in visited)
def _describe_cycle(self, chain: List[tuple]) -> str:
return " -> ".join(f"{f}->{t}" for f, t in chain[-5:])
class DeadlockDetectedError(Exception):
pass
Возможнасць спостерэння між агентамі
Этап абсарбавання між-агентнымі дадзеннямі працюе найэфектывней, калі яго спрыяваць як меравальную паверхню. Зберагчыце адны ідеальны транскрыпт, адзін прыклад неудачы і запіс пра вярнэнне да пачатковага стану, перш чым расширваць сферу дзеяння. Валіце маленькія, тэставальныя элементы замест большых скрыптов. Калі якісь крок не выйшае, прычына неудачы павінна вказываць на адную адпаведальнасць, а не на заплутаны ланцюг дзеяння. Рэжым графа павінен быць простым і з адначытаемымі дадзеннямі. Вкладаныя структуры дадзення маскуюць інфармацыю пра тое, канферны ўзоркі які вузел, і спаказваюць работу пасля перерываў. Этап абсарбавання між-агентнымі дадзеннямі працюе найэфектывней, калі яго спрыяваць як меравальную паверхню. Зберагчыце адны ідеальны транскрыпт, адзін прыклад неудачы і запіс пра вярнэнне да пачатковага стану, перш чым расширваць сферу дзеяння. Запісвайце часы выканання і косты токеноў або запытак праза функцыйнальныя рэзултаты. Відразувая візуабельнасць костаў запобегае неспакойным рашчыткам, калі процес пераходзіць з дэмавайшага режыму ў спяльныя сераўеры.
# harness/multi_agent/observability.py
import os
from contextlib import contextmanager
from langsmith import Client
from langsmith.run_trees import RunTree
class MultiAgentTracer:
"""
Maintains a run tree across all agents in a multi-agent system.
Pass the parent_run_id to each agent so their traces nest correctly.
"""
def __init__(self):
self.client = Client()
@contextmanager
def agent_span(self, parent_run_id: str, agent_name: str, inputs: dict):
"""
Context manager for an individual agent's trace span.
Usage:
with tracer.agent_span(parent_run_id, "security_reviewer", {...}) as span:
result = run_security_review(...)
span.end(outputs={"result": result})
"""
run = self.client.create_run(
name=agent_name,
run_type="chain",
inputs=inputs,
parent_run_id=parent_run_id,
)
try:
yield run
except Exception as e:
self.client.update_run(run.id, error=str(e))
raise
finally:
self.client.update_run(run.id, end_time=None) # auto-sets end time
Практычная перакананасць у працэсе вырабоцтва
Для стадіі перагледу рэальнасці працы ў працоўнай средзе неабходна пазначыць вхідныя даны, адпаведальнага за крок і крэтырыя завершэння перад змінайом коду. Аператары должны магчымае пераўстаць выконанне кроку з вядомага пункта контролю, не спрабоўваючы здагадвацца пра схованы стан. Конфігурацыю трэба залічыць параду ад коду прыкладнення. Файлы среды, хранільнікі секрэтных дадзеных і флагі функцыйяў должны знаходзіцца ў аднам месцы, якое аператары можаць пераглядаць, не чытаючы весь ланцуг задач. Пры выконанні дзеяння, якія коштуюць грошы або зменяюць даны працоўнай средзе, неабходна атрыбут затверджэння чалавека. Підключэнне падчас компіляцыі не ўзроўнаўваецца з повнай адпаведнасцю бізнес-процэсам.
Архітектура для адналежнага практыкавання
У стадії архітектуры для паведамлення неабяцо задаць вхідныя даны, адпавядающага за крок адпаведальнага і критэрыі завершэння пры змяне коду. Аперацыйныя працавнікі павінны магчымае запускаць крок з вядомай точкі контролю, не падозрываючы прыхованы стан. Неабяцо задокументаваць як шлях успеху, так і шлях вярнення. Перапрыбуткі, людзкіе перакрыцця і обробка некоректных паведамленняў є часткай продукту, а не яго пазнейшай дапрацоўкі. Павінна быць людзкая затверджэнняе для тых крокоў, якія выкарыстоўваюць грошы або змянююць даны ў працэсе. Прыєднанне элементаў у час компілявання не значыць, што продукт ўжо цэлысны з пункту зору бізнеса.
┌───────────────────────────────────────────┐
│ User Request │
└──────────────────┬────────────────────────┘
│
┌──────────────────▼────────────────────────┐
│ AgentHarness Runtime │
│ (budget guard, deadlock detector, tracer)│
└──────────────────┬────────────────────────┘
│
┌──────────────────▼────────────────────────┐
│ SUPERVISOR / ORCHESTRATOR │
│ Claude 3.7 + extended thinking │
│ Task decomposition + result synthesis │
└────┬──────────────┬──────────────┬────────┘
│ │ │
┌───────────▼──┐ ┌────────▼──┐ ┌───────▼───────┐
│ Worker A │ │ Worker B │ │ Worker C │
│ Claude 3.5 │ │ Claude 3.5 │ │ Claude 3.5 │
│ specialist │ │ specialist │ │ specialist │
└───────┬──────┘ └─────┬─────┘ └──────┬────────┘
│ │ │
┌───────▼───────────────▼────────────────▼────────┐
│ Tool Execution Layer │
│ (auth, retry, circuit breaker from Art.5) │
└───────────────────────┬─────────────────────────┘
│
┌───────────────────────▼──────────────────────────┐
│ AWS Services │
│ Bedrock │ DynamoDB (budget+deadlock+loop+circ) │
│ Secrets Manager │ Knowledge Bases │
└──────────────────────────────────────────────────┘
Observability: LangSmith run trees — full hierarchy per user request
Інфраструктура для паведамлення
Для стадіі Reference Infrastructure Stack неабяжна ўзначыць вхідныя даны, адпаведальнага за крок і крэтырыя завершэння пры змены коду. Аперацыйныя працавнікі павінны магчымае перазапускаць крок з вядомай точкі контролю, не падозрываючы прыхованы стан. Лепш выбіраць маленькія, тэставаныя елементы замест большых скрыптов. Калі крок не выйшоў, прычына нехарактэрыстыкі павінна вказываць на адзін конкрэтны аспект, а не на заплутаны процес. Неабяжна ўключыць людзкія апраўленні для тых крокоў, якія выкарыстоўваюць грошы чы зміняюць даны ў працэсе. Компіляцыйныя налашчэнні не ўзначаюць павнае адпрацоўвання задачі.
+-----------------------------+---------------------+------------------------------+
| Component | Technology | Role |
+-----------------------------+---------------------+------------------------------+
| Orchestration | LangGraph 0.2+ | Multi-agent graph, routing, |
| | | supervisor-worker topology |
+-----------------------------+---------------------+------------------------------+
| Supervisor Model | Claude 3.7 Sonnet | Task decomposition, |
| | (extended thinking) | result synthesis |
+-----------------------------+---------------------+------------------------------+
| Worker Models | Claude 3.5 Sonnet | Specialized execution, |
| | | bounded tasks |
+-----------------------------+---------------------+------------------------------+
| Parallel Execution | asyncio + Bedrock | Concurrent agent runs with |
| | | semaphore-gated concurrency |
+-----------------------------+---------------------+------------------------------+
| Context Compression | Claude Haiku 3.5 | Pipeline stage handoffs, |
| | | summary generation |
+-----------------------------+---------------------+------------------------------+
| Budget Guard | DynamoDB | Agent count + token limits |
| | | per run |
+-----------------------------+---------------------+------------------------------+
| Deadlock Detection | DynamoDB | Delegation cycle detection |
+-----------------------------+---------------------+------------------------------+
| Loop Detection | DynamoDB (Art. 5) | Per-resource edit tracking |
+-----------------------------+---------------------+------------------------------+
| Circuit Breaker State | DynamoDB (Art. 5) | Shared across all agents |
| | | in a run |
+-----------------------------+---------------------+------------------------------+
| Cross-Agent Observability | LangSmith run trees | Full hierarchy per request |
+-----------------------------+---------------------+------------------------------+
| Auth Propagation | CredentialManager | JWT passed to all workers |
| | (Art. 5) | via execution context |
+-----------------------------+---------------------+------------------------------+
| Local Dev Alternative | Ollama + Docker | All patterns testable |
| | Compose | without Bedrock costs |
+-----------------------------+---------------------+------------------------------+
| Infrastructure as Code | Terraform | DynamoDB tables, IAM roles |
+-----------------------------+---------------------+------------------------------+
Зьмены, якія нас чакаюць
Для стадії «Прымечка ўжо», перш чым зменіць код, неабходна визначыць вхідныя даны, адпаведальнага за крок і критэрыя завершэння. Аперацыйныя працавікі должны магчымае запускаць крок з вядомай точкі контролю, не спрабоўваючы здогадвацца пра схованы стан.
Чэк-ліст для аперацый
Для стадіі чэк-ліста для аперацый, перш чым зменіць код, неабходна визначыць вхідныя даны, адпаведальнага за крок і критэрыя завершэння. Аперацыйныя працавікі должны магчымае запускаць крок з вядомай точкі контролю, не спрабоўваючы здагадвацца пра схованы стан.
Спрытваце гэты ўражак як кантракт межа вхіднымі дадзеннямі і пасвярджанымі выходнымі рэзультатамі. Дайце назвы артыфактам, задаце критэрыя успеху і не падтрымвайце тыхню частковую завершэннасць.
Неабходна людская апраўка для тых процесаў, якія витрачаюць грошы або зменяюць данні ў працэсе виробніцтва. Праця ў часе компілявання не адпаведае повнай завершэннасці бізнес-процесаў.
Напісце кароткі посібнік: як зменяць клучы, як апустошыць чергу, як вярнуць стан да пярэдніх налашоўкаў.
Зазначайце час выканання аперацый, а таксу токенаў чы запытаў праза функцыйнае рэзультат. Відкрытая інформацыя пра витраты запобегае неспакойным рахункам, калі працэс пераходзіць з дэмовай среды ў спяльную.
Неабходна людская апраўка для тых процесаў, якія витрачаюць грошы або зменяюць данні ў працэсе виробніцтва. Праця ў часе компілявання не адпаведае повнай завершэннасці бізнес-процесаў.
Перш чым запускать стак, заморозьце версіі, зафіксавце «золаты» транскрыпты для критычнага шляху і паказвце способы абяроны. У спільных сэрвісах неабходны ліміты частоты запытоў, перакананне ў належнасці ресурсаў і чысткі власнік для змены секрэтных даных. Лепш выбіраць простую надзею на надзейнасць, чым хітрыя експерыментальныя дэманстрацыі.
Прыметкі для a0dc7ff1211b: не кладзіце ключы прадаўцаў у репазітарый, задаце ліміт токена на кожную сесію і зберагачыце транскрыпты празаўседліва з фіксатрамі eval, каб пазнейшыя замены модэляў заставаліся пораўнанымі.