Fail the Build When Your Voice Agent Gets Worse

작성자

카테고리:

← 피드로
DEV Community · Dima Statz · 2026-07-11 개발(SW)

In this series we’ve turned a raw call recording into a structured CallReport
(post 1) and looked at how to extract signals cheaply enough to run on every
call (post 2). Now the payoff: using those signals to stop regressions
before they ship.

A voice agent’s behavior drifts. You change a prompt, swap a model, pick a new TTS
voice — and the agent gets subtly slower to respond, colder in tone, or starts
skipping a required disclosure. None of that shows up in a normal test suite,
because the regression lives in the audio. So let’s put the audio in the test
suite.

The idea: golden recordings as test fixtures

Treat a small set of representative call recordings as fixtures. On every change,
analyze them and assert on the report. If a prompt change pushes a number past a
threshold, the build goes red — same as any other failing test.

import audiotrace
import pytest

# A few representative calls checked into the repo (or pulled from storage).
GOLDEN_CALLS = [
    "tests/calls/happy_path.wav",
    "tests/calls/frustrated_customer.wav",
    "tests/calls/compliance_heavy.wav",
]


@pytest.mark.parametrize("path", GOLDEN_CALLS)
def test_call_quality_does_not_regress(path):
    report = audiotrace.analyze(path, num_speakers=2)

    # Latency: the agent must stay responsive.
    assert report.latency.total_ms < 6000, "agent got too slow"

    # Quality: overall score must stay healthy.
    assert report.quality.overall_score >= 0.80

    # The agent shouldn't be talking over the caller.
    assert report.quality.interruptions <= 2


def test_required_disclosure_present():
    report = audiotrace.analyze("tests/calls/compliance_heavy.wav")
    # Compliance flags surface missing/again-required disclosures.
    assert "missing_disclosure" not in report.events.compliance_flags


def test_agent_does_not_frustrate_callers():
    report = audiotrace.analyze("tests/calls/happy_path.wav")
    assert report.sentiment.caller_frustration is False
    assert report.sentiment.overall >= 0.0  # net-neutral-or-better tone

Enter fullscreen mode Exit fullscreen mode

Because analyze() runs locally with no API calls, this works in CI with no
secrets and no network — the recordings and the open models are all you need.

Catching drift, not just hard failures

Absolute thresholds catch cliffs. To catch drift, compare against a baseline you
commit alongside the code:

import json
import audiotrace

def snapshot(path):
    r = audiotrace.analyze(path, num_speakers=2)
    return {
        "pace_wpm": r.quality.speaking_pace_wpm,
        "overall": r.quality.overall_score,
        "latency_ms": r.latency.total_ms,
        "sentiment": r.sentiment.overall,
    }

def test_no_drift_from_baseline():
    baseline = json.load(open("tests/baseline.json"))
    current = snapshot("tests/calls/happy_path.wav")

    # Latency may not grow more than 15% vs. the committed baseline.
    assert current["latency_ms"] <= baseline["latency_ms"] * 1.15
    # Tone may not drop more than 0.1 absolute.
    assert current["sentiment"] >= baseline["sentiment"] - 0.1

Enter fullscreen mode Exit fullscreen mode

When you intentionally improve the agent, you regenerate baseline.json and
commit it — the same workflow as snapshot testing.

Emit it as OpenTelemetry spans

CI catches regressions before they ship; observability catches what happens in
production. The CallReport maps cleanly onto OpenTelemetry, so voice-call
signals sit right next to the rest of your traces:

from opentelemetry import trace
import audiotrace

tracer = trace.get_tracer("audiotrace")

def trace_call(path: str):
    report = audiotrace.analyze(path)
    with tracer.start_as_current_span("voice_call") as span:
        span.set_attribute("call.duration_ms", report.media.duration_ms)
        span.set_attribute("call.quality_score", report.quality.overall_score)
        span.set_attribute("call.caller_frustrated", report.sentiment.caller_frustration)
        span.set_attribute("call.cost_usd", report.cost.total_usd)
        span.set_attribute("call.outcome", report.events.outcome)

        # The latency waterfall becomes child spans (STT, LLM, TTS, ...).
        for stage in report.latency.waterfall:
            child = tracer.start_span(stage.name, start_time=stage.start_ms)
            child.end()
    return report

Enter fullscreen mode Exit fullscreen mode

Hang it off your LangChain / LangSmith traces

If you already trace your agent’s reasoning in LangSmith, AudioTrace fills in the
half it can’t see — what actually reached the caller’s ear. Attach the report to
the run as metadata so the audio signals live next to the token-level trace:

from langsmith import Client
import audiotrace

client = Client()

def attach_audio_signals(run_id: str, recording: str):
    report = audiotrace.analyze(recording)
    client.update_run(
        run_id,
        extra={
            "audio": {
                "quality_score": report.quality.overall_score,
                "caller_frustration": report.sentiment.caller_frustration,
                "speaking_pace_wpm": report.quality.speaking_pace_wpm,
                "drop_off": report.events.drop_off,
                "total_cost_usd": report.cost.total_usd,
            }
        },
    )

Enter fullscreen mode Exit fullscreen mode

Now a single LangSmith run shows both what the model thought and how the call
sounded — and the same signals that flag a bad call in production are the
examples you feed back in to fine-tune the next, better agent.

Wrapping the series

Three ideas, one thread:

  1. A voice call is a rich artifact your token-level tooling can’t read — so turn it into a typed CallReport.
  2. Split the work by measure vs. estimate, and don’t reach for a big model when a cheap measurement will do.
  3. Put those signals where they pay off: red builds on regressions and spans/traces in production.

A lot of progress in AI isn’t a new model — it’s packaging hard-won engineering
into something others can pip install. That’s what AudioTrace is trying to be
for voice agents.

pip install audiotrace

Enter fullscreen mode Exit fullscreen mode

⭐ Repo: github.com/dimastatz/audiotrace
it’s early, and provider integrations + richer compliance checks are exactly where
contributions help most.

Keep building!

원문에서 계속 ↗

코멘트

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다