跳到正文
DEV Community· flisk·· 2 小時前AI 評分62

多語言 Agent 系統應以本機迴圈作為架構核心

The Local Loop Is the Architecture

AI 導讀

文章主張,多語言 Agent 系統應把本機迴圈視為架構本身,並由專用閘道集中持有憑證及執行受限的工具操作。閘道以 attempt id 防止重試造成重複寫入,Agent 之間則透過事件總線溝通,避免把對話和工具呼叫混為一個通道。

正文

We spent a decade teaching systems to cross a network. We are about to spend the next one teaching them not to.

An agent is not a service with a prompt bolted on. It is a runtime that can decide, retry, and call a tool whose effect outlives the call. Put two of those runtimes in different languages — a legal graph in Python, a commercial node in TypeScript — and the interesting design problem is no longer which model you picked. It is which process is allowed to touch the ledger, and how the other processes are prevented from growing their own path to it.

That boundary will not appear in production. By then each team will have a client, a secret, and a story about retries. It has to appear on the laptop, while both agents are still being rewritten every afternoon. The local loop is not a staging copy of the architecture. For this class of system, it is the architecture.

Fragmentation moved

The old fragmentation was operational. Compose files, port maps, a README that lied about the order of docker compose up. Painful, and solvable, because the processes were passive. A worker did not invent a new call while you were looking at a trace.

Agent fragmentation is semantic. The Python node and the TypeScript node can both "check the budget" and mean different things: one reads a view, one posts a reservation, one retries the post because its HTTP client timed out after the database had already committed. They share no type, no credential policy, and no definition of an attempt. Language is the visible difference. Tool ownership is the defect.

A heterogeneous pair makes this obvious.

flowchart LR
  UI[BFF] --> Legal[Legal node\nPython]
  UI --> Commercial[Commercial node\nTypeScript]
  Legal --> ERP[(Ledger)]
  Commercial --> ERP

Two arrows into the ledger is already a distributed system. The model vendors will not design it for you. Neither will a framework that only knows one language. Mastra does not owe LangGraph a session model. LangGraph does not owe your Next.js app a type. The thing they can share is a protocol at the edge of the privileged resource, and a loop that runs all three processes where a human can still watch a retry happen.

A boundary with a name

Call the privileged process a Local Loop Gateway. Agents never receive a connection string. They receive a tool list. The gateway is the only component that can turn a tool call into a query, and the only component that is allowed to remember that a given attempt already happened.

flowchart TB
  UI[BFF\nmints a session]
  subgraph loop [Local loop]
    direction TB
    Legal[Legal node]
    Commercial[Commercial node]
    Bus[Bus]
    GW[Loop gateway\nMCP]
    Legal <-->|events| Bus
    Commercial <-->|events| Bus
    Legal -->|tools| GW
    Commercial -->|tools| GW
  end
  UI --> Legal
  UI --> Commercial
  GW -->|one credential| ERP[(Ledger)]

MCP is the dialect. It is not the control. A schema that says departmentId: string does not stop a second write. What stops the second write is a process that refuses to be a tunnel: closed arguments, named operations, an attempt id stored beside the row.

server.setRequestHandler(ListToolsRequestSchema, async () => ({
  tools: [
    {
      name: "check_department_budget",
      description: "Read the remaining budget for one department.",
      inputSchema: {
        type: "object",
        properties: {
          departmentId: { type: "string", minLength: 1, maxLength: 64 },
        },
        required: ["departmentId"],
        additionalProperties: false,
      },
    },
  ],
}));

The frontier move is not "expose the ERP over MCP." That is how you launder a database into every agent. The move is to publish capabilities, and to make the capability smaller than the fear. check_department_budget can be backed by a view, a procedure, or a fixture. The agent cannot tell, and should not be able to. When the fixture and the procedure return the same shape, you have a boundary. When they do not, you have a demo.

Three properties fall out of putting the tool in a process that is not an agent.

  • Credentials live in one environment. Adding a third language does not add a third secret.
  • Argument policy runs before the query is built. Rate limits and sanitization are gateway code, not a paragraph in each agent's system prompt.
  • The attempt id is a write-side fact. A retry is a cache hit against the ledger, not a second mutation.

None of these are model features. They are what you have left when the model is allowed to call something that matters.

Two channels, not one

Agents that share a ledger still have to negotiate with each other. The failure mode is to make that negotiation another HTTP call, because HTTP is what the laptop already has.

Do not. A tool call and a conversation are different topologies, and collapsing them is how ordering bugs become invisible.

Channel Carries Must not carry
MCP, into the gateway A named operation on a privileged resource "Ask the other agent"
Bus, between agents Facts about what an agent decided A connection string, a retry of a write

The BFF mints x-session-id once per user task. That id is for routing and traces. It is not an idempotency key. A session contains many writes. An attempt contains one. If the commercial node times out on reserve_budget and sends the same attempt id again, the gateway returns the first reservation. If it sends the session id instead, the gateway cannot tell a retry from a second offer, and your ledger grows a twin.

Events stay dull on purpose.

legal.checkpoint.recorded  { session, attempt, departmentId }
commercial.offer.proposed  { session, eventId, terms }

Dull events are how you keep a Python graph and a TypeScript workflow from becoming each other's clients. The moment the commercial node imports a URL for the legal node, you have coupled two hot-reload cycles and called it an architecture.

The loop has to be generated

This pattern dies if the loop is a hand-maintained Compose file. Someone will add a port "just for debugging," point an agent at the database, and the boundary will exist only in the diagram.

The declaration has to be the source, and it has to be local. Flisk is a local control plane for that declaration: one service.json per service, Docker and Tilt generated from it, traffic on *.localhost, no deploy step hiding behind the same command. The legal node, the commercial node, the broker, and the MCP gateway come up as one stack because each file names its dependencies, not because a human remembered an order.

{
  "schemaVersion": 1,
  "appName": "agent-legal",
  "appType": "backend",
  "command": "python agent.py",
  "port": 4001,
  "healthCheckPath": "/health",
  "dependsOn": ["secure-gateway", "event-broker"]
}
flisk doctor
flisk up enterprise-procurement

doctor is part of the architecture. A loop that starts half-up teaches agents to retry against absence, and then you encode that retry as if it were a business rule. Fail before Tilt starts, or you will debug a race that was a missing binary.

Live update is the other half. A change to an evaluation step should appear in the running graph without destroying the session you were tracing. A change to the declaration should not. Routes and dependencies are topology. Topology is regenerated on the next up, not patched in a container that has already forgotten why it is allowed to listen.

There is a second Flisk, at flisk.ai, that manages tags. It is a different system. The one this pattern needs is the CLI that refuses to deploy. That refusal matters. A tool that can both reshape your local boundary and ship it will eventually ship the debug port.

What you are actually testing

A local multi-agent stack is not impressive because five containers started. It is impressive if you can force the failure the production incident will be.

  1. Call a write tool. Kill the client after the gateway has committed, before the agent has seen the response.
  2. Repeat the call with the same attempt id.
  3. Read the ledger. One row, or the pattern is fiction.

Hot reload does not perform that test. Tilt updating a Python file only proves the file was mounted. The test is the duplicate, and the duplicate has to lose.

The same discipline applies to the tool list. If a coding assistant can drive the loop — Flisk can expose the stack over MCP, the reverse direction of an mcp resource — it must not be able to widen a tool because widening was faster than editing the schema. Generated files are not a scratchpad. The assistant proposes a change to service.json. The next up is the merge.

The shift

Monoliths were a packaging decision. Microservices were a team decision. Heterogeneous agents are a permission decision: which runtime may cause an effect, and which runtimes may only request one.

You can postpone that decision until the first double charge. Or you can put a small, closed tool list in a process that holds the only credential, run the disagreeing languages beside it, and watch an attempt id collide with itself before a customer does.

The model will keep changing. The language of each node will keep changing. The boundary should not. Build the loop so that replacing an agent is a service.json and a command, and replacing the ledger is an implementation detail behind a name the agent was never allowed to see.

來源:DEV Community · dev.to