Stop Letting Your AI Agents Call Each Other Directly
Synchronous agent-to-agent calls recreate every pain of the distributed monolith. The real cost isn' 2026-7-17 15:0:3 Author: hackernoon.com(查看原文) 阅读量:1 收藏

Synchronous agent-to-agent calls recreate every pain of the distributed monolith. The real cost isn't one extra hop of latency — it's brittleness you can't replay and a conversation you can't reconstruct.

The 3 a.m. version of this story.

A multi-agent order-processing workflow. A planner agent calls an enrichment agent, which calls a pricing agent, which calls a fraud agent, and each one waits for the next. It demoed beautifully.

Then, one afternoon, the enrichment agent got a malformed product record, fell into a retry loop against a flaky downstream API, and just... sat there. It didn't crash. It didn't return. It held the connection open and kept thinking.

Everything upstream is blocked behind it. The planner was waiting on enrichment, the queue of incoming orders backed up behind the planner, and within a few minutes, the whole pipeline was wedged. No errors in the dashboard — just throughput quietly going to zero.

The part that actually hurt came after. Someone asked the obvious question: what was the agent doing when it hung? And we had nothing.

No log of the messages between agents. No record of what enrichment was received or what it tried to emit. The conversation happened in RAM, in a stack of blocking HTTP calls, and when we killed the process to recover, the only forensic artifact left was a stack trace.

That's not an AI problem. That's a 2014 microservices problem wearing an LLM costume.

Direct calls rebuild the distributed monolith.

When agent A invokes agent B synchronously and waits, you've made five bad trades at once. I've watched every one of them bite.

Tight coupling. A has to know B's address, B's request shape, B's response shape, and that B exists at all. Add a compliance agent that also wants to see every order, and you're editing the planner to fan out to it. The graph of who-calls-whom becomes the architecture, and it's wired in code.

Cascading failure. B's latency is now A's latency. B's outage is A's outage. In a chain of five agents, your availability is the product of five availabilities, and your tail latency is the sum of five tails. LLM agents are slow and variable by nature — a tool call here, a reasoning loop there — so the tails are fat, and they stack.

Fan-out storms. One incoming event that needs four agents becomes a tree of synchronous calls, each holding a connection open while it waits on its children. Concurrency you can't see and can't bound.

No backpressure. A produces faster than B can consume? In a synchronous mesh, A just blocks, or piles up retries, or times out and loses the work. There's no buffer that says, "Hold these, B will get to them."

No replay, no audit. This is the one that matters most and gets ignored most. The interaction between agents is ephemeral. When something goes wrong, there is no log of the decisions, no way to re-run the exact sequence, and no record for an auditor asking why an order was flagged. The most interesting data in the entire system — what the agents said to each other — is thrown away the instant the call returns.

You've heard all five of these before. They're the reasons we stopped building distributed systems out of blocking RPC a decade ago. Agents didn't repeal that lesson.

Put a log in the middle.

The fix is old and boring, and it works: agents don't call each other. They publish events to a broker, and other agents subscribe to the events they care about.

Agent A doesn't invoke the enrichment agent. It publishes order.enrichment.requested and moves on. The enrichment agent — whenever it's ready, at whatever rate it can sustain — consumes that event, does its work, and publishes order.enrichment.completed. Anyone who cares about enriched orders subscribes. Pricing. Fraud. A brand-new audit agent you add next quarter. A never learns they exist.

The contract between agents collapses from "know B's API, address, and liveness" down to one thing: the event schema. That's the entire coupling surface. Make it a typed, versioned schema, and you've turned a tangle of point-to-point calls into a flat, observable stream.

Veera Ravindra Divi's image-6a0428

Here's the shape of an intent event and the two ends that produce and consume it. Typed, versioned, and — crucially — durable the moment it lands in the log:

# Typed agent "intent" event — the entire coupling surface lives here
event = {
    "event": "order.enrichment.requested",   # topic / type"version": "1.0",                         # schema version, evolve safely"id": "evt-7f3a",                         # idempotency key"ts": "2026-06-29T14:02:11Z",
    "trace_id": "ord-9182",                   # ties the whole conversation together"payload": {"order_id": "9182", "sku": "A-204", "qty": 3},
}

# Producer: the planner agent doesn't call anyone — it publishes and moves on
broker.publish("order.enrichment.requested", event)

# Consumer: the enrichment agent subscribes, works at its own pace, emits a result
@broker.subscribe("order.enrichment.requested")
def on_request(evt):
    enriched = enrich(evt["payload"])        # may be slow; nobody is blocked on it
    broker.publish("order.enrichment.completed",
                   {**evt, "event": "order.enrichment.completed",
                    "payload": enriched})

Twelve lines of mechanism. The planner has no idea who enriches the order or whether that consumer is even online right now. It was published with intent. The log holds it. That's the whole trick.

What the log buys you that a call never will.

Durability. The event is on disk before any agent touches it. The enrichment agent can be down for an hour; the work waits in the log instead of evaporating into a timeout. When it comes back, it picks up where it left off.

Replay. This is the feature people underrate until the first incident. A bad deploy to the pricing agent? Reset its consumer offset and re-run yesterday's events through the fixed code. Debugging a weird decision? Pull the exact event sequence by trace_id and watch the conversation happen again, deterministically. You cannot replay an HTTP call you didn't record. You can always replay a log.

Audit for free. The thing you were going to be forced to build for compliance — a record of every decision every agent made — is just the log. It already exists. Every intent, every result, timestamped and ordered. When someone asks why order 9182 got flagged, you don't reconstruct it from memory. You read it.

Backpressure. A producing faster than B can consume is no longer a failure. It's just lag — a number you can watch climb and scale against. The buffer is the point.

Loose coupling. Add an agent by adding a subscriber. Remove one by removing a subscriber. Swap an agent's implementation entirely, and nobody upstream notices, because they were only ever talking to the topic. The graph stops being load-bearing code and becomes routing config.

This is the same bet I've made building resilient event-driven B2B systems for years: the message bus isn't overhead, it's the part that survives. Agents are just a new kind of producer and consumer on a pattern that has already earned its keep.

"But you added a network hop and a broker to run."

Yes. And it's worth it, and here's the argument I'll actually defend.

The objection is that you've traded a direct call for publish-then-consume, adding latency and a broker to operate. True on both counts. But look at what you're optimizing. The latency of one extra hop is single-digit milliseconds. The cost of the direct-call design is a workflow that wedges when one agent loops, loses work on every timeout, and leaves you blind during the exact incident where you most need to see.

Agent workflows are not latency-bound. They're bound by LLM inference and tool calls measured in seconds. Shaving a broker hop off a path that already spends three seconds in a reasoning loop is a rounding error. You're guarding the wrong number.

The real cost in an agent system was never milliseconds. It's the brittleness and the blindness — the inability to know what happened and the tendency to fall over all at once. An event you can replay is worth more than a millisecond you saved.

The honest tradeoffs, because they're real: you now run a broker (Kafka, NATS, SQS, Redis Streams — pick your weight class). You design for eventual consistency and idempotent consumers, which is why that id field is in the schema. And request/response patterns — "I need this answer right now to continue" — get modeled as a correlated request event plus a response event, which feels like more work until the first time you replay one. For genuinely synchronous, low-latency, two-party exchanges, a direct call is still fine. The mesh is the problem, not the existence of any call.

Takeaways

  • A synchronous agent mesh is a distributed monolith. Tight coupling, cascading failure, fan-out storms, no replay. You've seen this movie; don't film the remake with agents.
  • The coupling surface should be a schema, not an API. Versioned, typed events mean you add and swap agents without rewiring the ones already running.
  • Replay and audit aren't features you build later. With a log in the middle, they fall out for free, and they're exactly what you'll beg for during an incident.
  • You're optimizing the wrong number. The extra hop is milliseconds on a path that spends seconds in inference. The thing that actually breaks is coupling, not latency.
  • Keep direct calls for the rare two-party synchronous case. Kill the mesh. Let the broker hold the conversation.

The next time you're tempted to wire agent A straight into agent B, ask one question: when this hangs at 3 a.m., what will I have to read? If the answer is a stack trace, you built it wrong. Put a log in the middle, and let the agents leave a paper trail.


文章来源: https://hackernoon.com/stop-letting-your-ai-agents-call-each-other-directly?source=rss
如有侵权请联系:admin#unsafe.sh