首页 / 文章

Bedrock与LangGraph上的多智能体协调:四种模式

在 AWS Bedrock 上使用 LangGraph 的监督-工人、流水线、扇出与辩论模式,以及任务交接、预算监控、追踪和生产故障模式。

发布者:REACTAPP.TOP阅读约 22 分钟5114 词

来源: Agentic Architectures — Article 6: Multi-Agent Orchestration Patterns

当单个智能体遇到瓶颈时

人们普遍认为,只要为智能体配备完善的工具包,它就能处理任何任务。然而后来出现了一个项目,要求在单次运行中完成对庞大代码库的扫描、与安全建议清单的比对、修复方案的起草,以及针对现有测试对修复方案的有效性验证。这样的任务量让智能体的处理能力不堪重负。它要么陷入代码的某个复杂部分,忘记之前二十万个标记处获取的安全建议;要么在分析完成后,预算早已耗尽,无法再制定有效的补救措施。

无论采用更完善的上下文注入、更智能的摘要生成,还是更严格的工具范围控制,现有的框架技巧都无法解决这个问题。该任务的范畴过于广泛,单个智能体无法同时兼顾所有方面。某些问题与提示词设计毫无关系,需要多个智能体协同处理。在 AWS Bedrock 上使用 LangGraph 时,有四种编排模式被用于实际应用中,同时还有相应的防护措施防止系统出错。单智能体框架的相关技术依然适用:在工具边界使用 JWT 认证,电路断路器和循环检测机制也适用于多智能体场景且未被替代。

在此过程中需要解答的问题包括:

  • 何时单个智能体已不足以完成任务?
  • 哪种编排模式适合特定问题?
  • 如何让上下文在智能体之间传递而不耗尽令牌预算?
  • 为何在实际应用中会出现演示环境中从未出现的问题?

三种限制

上下文长度限制是最明显的约束,但并非唯一的限制。单智能体设计最终会因三个独立原因而失败。

上下文耗尽是最受关注的问题。在需要处理大量工具输出、中间推理结果及对话历史的长任务中,即便20万token的上下文窗口也会被填满。智能体会开始忘记之前获取的信息,且随着窗口逐渐充满,其推理能力也会下降。

专业化与泛化的问题则更为微妙。被要求样样精通的智能体最终会在所有方面都表现平平。那些需要同时编写Python代码、评估安全风险并判断测试覆盖率的智能体,实际上要在三种不同的认知模式间切换。相比试图同时完成这些任务的单一智能体,由安全专家来审查代码生成智能体的输出效果会更好。

并行化是往往被低估的性能提升手段。某些任务彼此独立,可以同时执行,若在单个智能体中依次处理,则会浪费性能。

LangGraph在这方面能提供帮助,因为它对多智能体拓扑结构有完善的支持。图结构能够自然地模拟协调模式,这是顺序式框架无法做到的。

四种模式一览

这四种模式并非互斥,实际生产系统常常会将它们结合使用。下表列出了每种模式的结构及其最佳应用场景:

+--------------------+--------------------------------------------------+------------------+
| 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   |
+--------------------+--------------------------------------------------+------------------+

应根据任务的结构来选择,而非哪种模式听起来最令人印象深刻。当简单的监督者-执行者结构能更快更省钱时,团队仍会选择辩论模式。

监督者-执行者

这是最常用的模式。监督者节点会将复杂的任务分解为子任务,分配给专门的执行者,收集他们完成的结果并整合成最终成果。其职责在于管理,具体操作则交由他人完成。

          ┌─────────────────────────────┐
          │       SUPERVISOR AGENT      │
          │  (task decomposition +      │
          │   result synthesis)         │
          └──────┬───────────┬──────────┘
                 │           │
        ┌────────▼──┐   ┌────▼──────────┐  ┌──────────────┐
        │  Worker A  │  │   Worker B    │  │   Worker C   │
        │  (code     │  │  (security    │  │  (test       │
        │   analysis)│  │   review)     │  │   coverage)  │
        └────────────┘  └───────────────┘  └──────────────┘

LangGraph通过一个监督节点来实现这一功能,该节点会根据任务的当前进展将消息发送到相应的worker子图。每个worker都是独立编译的图结构,能够拥有自己的工具、验证流程和错误处理机制。

# 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

随后,所有内容都会在 LangGraph 状态图中连接起来,并附加一个内存检查指针:

# 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())

LangGraph监督员教程以更详细的方式介绍了相同的方法。

处理流程

处理流程本质上是顺序进行的。Agent A生成输出,Agent B对其进行重构,Agent C则对其进行评估,每个角色都有明确界定的职责。图表展示了从数据提取、再到信息丰富、最后是验证的流程:

  ┌───────────────┐     ┌───────────────┐     ┌───────────────┐
  │   Agent A     │     │   Agent B     │     │   Agent C     │
  │  (extraction) │────>│ (enrichment)  │────>│ (validation)  │
  └───────────────┘     └───────────────┘     └───────────────┘
       output A               output B               output C
    becomes input B        becomes input C         final result

压缩上下文是最大的难题。到了第三个智能体时,它需要承载来自智能体A和B的全部消息历史,这意味着数千个智能体C根本用不上的推理标记。它需要的是智能体A的工作结果,而非达成该结果的过程轨迹。

# 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

不过压缩是有代价的,那就是保真度。信息会丢失。对于某些流程来说这并无大碍。但在Agent C必须完全遵循Agent A的推理逻辑的情况下,就需要仔细考虑哪些内容需要压缩,哪些内容要完整保留。

并行扩展

其思路很简单:将任务拆分,同时运行多个智能体,再将结果合并。但实际上,大多数实现方案都在合并环节出现问题。

                    ┌──────────────────┐
                    │   ORCHESTRATOR   │
                    │  (task splitter) │
                    └──┬───┬───┬───┬──┘
                       │   │   │   │
            ┌──────────▼┐ ┌▼─┐ ┌▼──┐ ┌▼──────────┐
            │  Agent 1  │ │A2│ │A3 │ │  Agent 4  │
            │ (region A)│ │  │ │   │ │ (region D)│
            └──────────┬┘ └┬─┘ └┬──┘ └┬──────────┘
                       │   │    │     │
                    ┌──▼───▼────▼─────▼──┐
                    │     AGGREGATOR     │
                    │  (result merger)   │
                    └────────────────────┘

Python中的并发功能来自asyncio。在Bedrock上实现这一点很简单,因为每个并发的智能体调用都相当于独立的API请求。

# 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)

成本问题需要坦诚面对。并行处理会使Bedrock的消耗呈线性增长:执行同一任务的四个独立代理所需成本大约是一个代理的四倍。对于真正独立的任务来说,这通常是可接受的;但当任务可以顺序执行时,这种方式就显得不妥了。有些团队选择并行处理只是因为觉得这样更高效,而非问题本身确实需要如此。LangGraph关于并行节点执行的指南中介绍了相关的分支机制。

争议点

两个代理独立处理同一个问题,然后第三个代理比较它们的输出并生成综合结果。这种方法成本高昂且速度缓慢,因此只有在对质量要求远高于速度的场景下才有价值:高风险的决策、对安全性要求极高的代码、法律文件审核等。

  ┌─────────────┐          ┌─────────────┐
  │  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,
    )

辩论展现了单智能体评估无法实现的功能。当两个独立推理的模型得出相同结论时,其置信度会有更坚实的依据;而当它们出现分歧时,这种分歧本身就是一个信号,表明存在需要人类判断的真正歧义。

在智能体之间传递上下文

无论采用何种模式,要在智能体间传递信息都需要谨慎考虑,因为单智能体的上下文窗口问题会以新的形式出现。对于两个智能体而言,直接传递完整的消息历史是可行的,但随着链条变长这种方法就会失效。通过将结果与推理轨迹分开来处理的结构化传递对象则更为有效:

# 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)

生产故障模式

多智能体系统会引发单一智能体所没有的故障,其中一些故障较为隐蔽。

成本不断上升。当智能体能够生成子智能体时,支出会迅速增加。如果监管机制过于激进地将一个用户请求分解为20个处理单元,就必须设置严格的限制:

# 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

循环委派。这种情况发生的概率比人们想象的要低,但一旦出现后果将十分严重。主管将任务委派给员工A,员工A决定再将任务转委派出去,于是主管再次进行委派。LangGraph的图结构通常能防止这种情况发生,但动态路由机制却使其成为可能。

# 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

碎片化的追踪信息。当一个请求涉及五个代理时,需要的是整个图结构的统一视图,而非五个相互独立的追踪记录。LangSmith通过运行树实现了这一点,每个子代理的运行都是其父运行的子节点,从而可以遍历完整的层级结构。

# 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

值得铭记的教训

多智能体系统里,复杂性不再令人兴奋,反而需要谨慎管理。

调试难度大幅增加。单个智能体运行失败时只会留下一条可分析的痕迹,而主管-工作员系统出错则可能需要对比十二条不同痕迹。在考虑拓扑结构之前,应先确保系统的可观测性。如果没有全面了解所有智能体的运行状况,本该几分钟就能解决的问题可能会耗费数天时间。

成本随智能体数量增加而上升。“推理三明治”模式会在每个智能体上重复应用,因此在基于扩展思维模型的五智能体主管-工作员架构中,单个请求就可能会消耗大量令牌预算。从第一天起就应该设定严格的预算限制。上述的预算监控机制并非可选,它能够将可控的成本模型与意外的费用支出区分开来。

部分失败是正常现象。单个智能体可能成功也可能失败。在多智能体运行中,可能会出现六个工作智能体中有五个完成任务而另一个失败的情况,因此必须事先明确部分成功的定义。如果安全检测过程中某个智能体失效,它应该退回已处理的结果还是完全失败?产品负责人应当明确做出决策,因为这是业务层面的问题,不应由默认行为来决定。

模型升级会变得复杂。各种工具都是针对特定模型设计的,而智能体可能运行在不同的模型上,因此升级往往意味着需要重新调整多个系统提示和验证流程。为每种类型的智能体准备相应的测试套件,才是能够放心进行升级的唯一方法。

协调器是唯一的故障点。在主管-工人模式中,如果主管出现故障,所有工作都会停止。生产设计应保留主管的状态,以便在运行中断后能够继续。LangGraph的检查点功能可以实现这一点,但需要手动进行配置。

整个系统

从用户请求开始,经过主管、工人、任务传递以及可观测性功能,完整的架构如下所示:

                  ┌───────────────────────────────────────────┐
                  │            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

此处总结了各组件、其背后的技术以及各自的功能:

+-----------------------------+---------------------+------------------------------+
| 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   |
+-----------------------------+---------------------+------------------------------+

尚缺什么

随着智能体系统的规模扩大,一个问题不断出现,上文也提到过但未明确指出。管道压缩、任务交接摘要以及无法回忆先前运行记录的监督器,这些问题都源于智能体会遗忘这一事实。每次运行都是从零开始。周二解决了某个复杂问题的监督器,在周三遇到类似任务时将一无所知;而处理过棘手边缘情况的智能体也仅能记住当前这一次的解决方案。能够跨多次运行保留信息的记忆机制是接下来需要构建的部分。它分为不同层级(工作级、情景级和语义级),在AWS上每个层级都有各自的实现方式。