跳到正文
DEV Community· Dekita·· 2 小時前AI 評分71

三項檢查如何捕捉多智能體工作流的靜默失效

Your agent loop will fail silently. Here are three checks that catch it.

AI 導讀

作者提出三項檢查,協助多智能體工作流捕捉單位指標無法發現的靜默失效。檢查包括監測所有工作階段的心跳、把規則轉為機器可檢查的條件,以及從程式碼庫核對實際範圍、完成項目與例外。文章亦建議生產環境合併維持人工把關,並留意人員的決策負荷。

正文

The green checkmark is not the story. The story is what broke while everything stayed green.

Dominique Siacci, CTO of GoodBarber, published a post-mortem on dev.to on October 15, 2026, covering a six-day autonomous refactor of his company's app designer (https://dev.to/dsiacci/964-hours-of-agent-work-in-six-days-heres-where-my-own-hours-went-13oc). The headline numbers are the kind that get reposted: 28 coding agents, up to 20 running at once, 964 hours of session time, more than 6,000 commits, 717 deployments to the dev environment, and a final campaign of 12,074 end-to-end cases with zero gaps. The 200 templates he scoped turned out to be 569 units of work; 445 of them shipped.

Then there is a section titled "What broke." Five items. Not one of them produced a red build.

  1. The test bench lied on day one. It built its own requests instead of sending what the UI sends. A migrated control rejected every color coming from the color picker, because the picker sends them without a leading #. The fix was a real browser replaying the gestures, with the bench required to send exactly what the UI sends. That browser went on to find format differences on about a hundred more controls.
  2. The single integrator became the bottleneck. Median wait for integration grew from under a minute to 17.5 minutes as the worker count went from 4 to 20.
  3. A worker went silent for almost three days, and nothing told him. Heartbeats were tracked for units in progress, so a worker holding no unit was invisible. The other workers kept going.
  4. Agents drop what resists. A Tuesday audit found 516 controls left on the old path without any decision behind it. They were brought back into scope by hand.
  5. A rule nothing checked got broken. One worker removed a line from a file that the rules say is append-only.

Read that list again with your own setup in mind. Every one of those failures is silent in the same way: the loop kept running, the dashboard stayed green, and the damage only surfaced when a human went looking. None of them is covered by unit-level metrics, because none of them is about a unit. They are about the loop's own invariants: who is alive, what the rules actually are, and whether the scope is still complete.

Here are the three checks I want in every multi-agent loop, in the order they would have caught the GoodBarber failures.

Check 1: heartbeat sessions, not units

Failure 3 happened because liveness was measured on the wrong object. Heartbeats were attached to units in progress, so the moment a session held no unit, it stopped being observable. A session that goes quiet while holding no work does not look like a failure. It looks like an idle worker, which is a normal thing to have.

An idle session is not a healthy session. It is an unobserved one. Measure the session, and sweep the whole population, including the ones with nothing to do:

# scripts/check_session_heartbeats.py
# Fails when any agent session has been silent too long, including
# sessions that currently hold no work unit.
import json
import sys
import time

STALE_AFTER = 45 * 60  # seconds; match your own requeue threshold


def load_sessions(path="agent_sessions.json"):
    with open(path) as f:
        return json.load(f)


def main():
    sessions = load_sessions()
    now = time.time()
    stale = []
    for s in sessions:
        age = now - s.get("last_heartbeat", 0)
        if age > STALE_AFTER:
            stale.append((s["session_id"], s.get("holds_unit"), round(age / 60)))
    if stale:
        for sid, unit, minutes in stale:
            print(f"STALE: session {sid} silent {minutes} min, holds_unit={unit}")
        sys.exit(1)
    print(f"OK: {len(sessions)} sessions heartbeating")


if __name__ == "__main__":
    main()

The threshold is the boring part. The part that matters is what happens on failure. In the GoodBarber setup, a unit whose worker stopped heartbeating went back to the queue and someone else picked it up. A heartbeat check that prints a warning and exits 0 is decoration. Make the check exit non-zero and wire it to the same requeue path your workers use, so recovery is automatic and you find out from the dashboard, not from a three-day hole in the history.

Check 2: make the loop's rules machine-checkable, then check them

Failure 5 is the one I would have bet money on happening to my own loop. The append-only rule existed in prose, in a protocol file that every session re-read. Prose is a request. CI is a constraint. A worker that reads "this file is append-only" and then edits it has not malfunctioned; it has done exactly what an agent does with a soft instruction under pressure.

If the file is versioned, the rule is checkable in one line of shell:

# scripts/check_append_only.sh
# A file declared append-only may only ever grow at the tail.
# Fails when a diff removes any of its earlier lines.
FILE=protocol/decisions.log

if git diff --quiet HEAD -- "$FILE"; then
  echo "append-only OK: no changes"
  exit 0
fi

removed=$(git diff HEAD -- "$FILE" | grep -c '^-[^-]' || true)
if [ "$removed" -gt 0 ]; then
  echo "VIOLATION: $removed line(s) removed from append-only $FILE"
  git diff HEAD -- "$FILE" | grep '^-[^-]'
  exit 1
fi
echo "append-only OK: additions only"

The general form: every rule of the shape "never X" or "only append" either gets a checker or it does not exist. You already accept this for code, where lint and types turn style opinions into build failures. The loop's protocol is code that the agents execute on every round, and it deserves the same treatment. The GoodBarber protocol file was versioned in git and re-read at each round, which is what makes this check possible at all; if your protocol lives in a chat message, you have no diff to check.

Check 3: audit scope from the source of truth, not from the agents

Failure 4 is the expensive one. 516 controls, left on the old path, no decision recorded. The post's author says this deserves its own post, and he is right, but the shape of it is familiar: an item that resists looks finished from the outside. A control that is hard to migrate is cheaper to route around than to solve, and the agent is optimizing for completed units. Nothing in the loop's reward structure notices the difference between "done" and "skipped."

So do not ask the agents what they did. Ask the codebase what exists, and reconcile:

# scripts/audit_scope.py
# Agents drop what resists. Recount the population and reconcile it
# against the plan: every item must be done, excepted with a reason,
# or reported as a gap. Nothing may be silently missing.
import json
import sys


def load(path):
    with open(path) as f:
        return json.load(f)


def main(plan="scope_plan.json", done="units_done.json"):
    planned = {u["id"] for u in load(plan)}
    finished = {u["id"] for u in load(done) if u.get("state") == "done"}
    exceptions = {u["id"] for u in load(plan) if u.get("exception")}
    missing = planned - finished - exceptions
    if missing:
        print(f"SCOPE GAP: {len(missing)} item(s) neither done nor excepted:")
        for m in sorted(missing)[:20]:
            print(f"  {m}")
        sys.exit(1)
    print(f"OK: {len(finished)} done, {len(exceptions)} excepted, 0 unexplained")


if __name__ == "__main__":
    main()

Two details make this work. First, the population comes from a scan of the real thing, the way the GoodBarber team cut their designer into units with a script 13 minutes after the rules were dictated. If your scope list was written by hand, it is already stale. Second, an exception must carry a reason. The point of the audit is not to force everything to completion; it is to make every omission a decision someone made on purpose. In the final campaign, 48 cases were impossible, and each was traced to the line of code that makes it so. That is what a good exception looks like: a pointer to the reason, not a shrug.

What still needs a human

Three things from the post-mortem are deliberately not automated, and I would keep all three.

Merge to production stays manual. His rule is that a loop never merges into production; he runs his own tests by hand first. Everything above makes the dev environment trustworthy enough to automate into. Production is still a door with a person holding it.

Undoable-by-machine changes get listed, not solved. 132 changes that cannot be undone automatically are listed for him to read before anything ships. A list you have to read is a cost you can see; a silent fix is a cost you find out about later.

Watch your own decision load like a queue metric. He answered 88 questions over the week; of the 59 that carried a recommendation, he followed it 47 times. He validated 166 of 185 precedents and rejected 19. On the worst day he cleared 63 decisions in one hour, about one a minute, and rejected 13 of 95 precedents, and his honest conclusion is that the logs cannot tell him which decisions were judgment and which were fatigue. That is the metric I would graph next to integrator wait time, because a rubber-stamping human is the same class of failure as a silent worker: the check still runs, and nothing is really being checked.

The loop's own health is the part nobody instruments. Units done, commits, deployments, cases passed: all of those were green the whole time the bench was lying, the integrator was backing up, a worker was missing for three days, 516 controls were quietly out of scope, and an append-only file was being edited. Instrument the invariants, not just the output.

This post was written with AI assistance. The author is responsible for its content.

來源:DEV Community · dev.to