On this page
- Unit Tests Were Not Enough
- Golden Cases and a Deterministic Runner
- What Baseline Gating Does
- Why I Left a Known Failure in the Suite
- Designing With the Limits in View
- A Safety Net for the Next Stage
- 한국어 버전
- LLM 에이전트에 평가 프레임워크를 붙인 이유
- 유닛 테스트만으로는 부족했다
- 골든 케이스와 결정적 러너
- 베이스라인 게이팅의 역할
- known-failure를 일부러 남겨둔 이유
- 한계를 알고 쓰는 것도 설계다
- 다음 단계를 위한 안전망
There is a moment when a small change to the Planner or Executor quietly changes how an agent behaves. In a diff, it may not look like much. A condition changed, part of a prompt was cleaned up, or a tool-selection hint became slightly different.
But in an LLM agent, those small changes can silently shift which tool the agent chooses. The user may ask the same thing, like “find my recent files,” but one day the agent starts trying to read a specific file instead of searching the file list. Nothing looks like a major feature change from the outside, but the actual behavior has regressed.
That is hard to sustain through manual review alone. So I added an evaluation framework to the agent. The goal was not grand. I wanted a mechanism that could run a small set of golden cases deterministically whenever I changed the Planner or Executor, then mechanically tell me whether the behavior had gotten worse.
Unit Tests Were Not Enough
Traditional unit tests are good at checking function inputs and outputs. But the core problem in an agent sits one level above a simple return value.
An agent has to interpret the user’s request, choose the right tool, decide the execution order, and pick the next action when something fails. That flow is produced by several components working together. Each individual function can pass while the overall decision is still wrong.
Tool selection is especially subtle. Words like “read,” “find,” “recent,” and “list” can appear together, and the agent has to decide which tool to call. That decision is influenced by prompts, hint tables, the TaskExtractor, and Planner rules at the same time.
What I needed was not just another unit test. I needed an evaluation suite that could feed in cases close to real user requests and verify the plan and tool choices the agent produced.
Golden Cases and a Deterministic Runner
The first version managed golden cases as JSONL. It started with 42 cases, then grew to 62 and eventually 70. Each case contains a user request and the expected behavior. The important part is making the expected decision explicit: which tool should be selected for this request, and what flow counts as success.
I made the runner deterministic. If evaluation results fluctuate every time they run, they cannot tell you whether a regression happened. LLM agents naturally contain nondeterministic pieces, but inside the evaluation harness I wanted the same input to produce the same judgment as much as possible.
The CLI stayed simple. Running evaluations, comparing results, and updating the baseline are separate commands. There is one command to run the suite, one to compare the current result against the existing baseline, and another to update the baseline explicitly.
The important design choice was that baselines do not update automatically. If a test fails and it is too easy to say, “let’s make the current result the new standard,” the framework quickly loses its meaning. Baseline updates require a separate command. A regression stops as a failure, and changing the standard has to be intentional.
What Baseline Gating Does
The heart of the framework is baseline gating. When evaluations run before and after a change, any case that gets worse is treated as a regression. The exit codes are explicit: 0 for success, 1 for regression, 2 for usage errors. That makes it easy to plug into local development, CI, or later automation.
The best part is that it reduces the developer’s cognitive load. When changing agent code, I no longer have to keep asking myself, “did I accidentally break tool selection somewhere?” For the cases we have defined as important, the machine tells me.
Of course, this does not prevent every bug. But in areas like the Planner and Executor, where small changes can create broad behavioral shifts, even one gate like this can make development faster. To move quickly with confidence, you need to know quickly when something broke.
Why I Left a Known Failure in the Suite
There was one interesting decision along the way. The agent had a real bug on requests close to “show me the recent file list.” Instead of choosing the file search tool, it chose the file read tool. The cause was a bad “file” hint that biased tool selection in the wrong direction.
I did not fix it immediately. At that stage, the goal was not to repair that specific bug. The goal was to build the evaluation framework. So I put the case into the suite as a known failure.
That pattern is useful. Even when you cannot fix a bug right away, putting it into the evaluation suite means it does not disappear. Later, when you touch related logic, the case keeps serving as a reference point. A known failure is not a way to ignore failure. It is a way to make failure trackable.
Designing With the Limits in View
The evaluation harness has limits too. For example, the overall status can look successful when it follows a simulated goal path. But at the step level, a failure may still be recorded, such as when access to a sensitive configuration file is rejected during a read-only phase.
In that situation, the truth is in the step-level error code, not just the overall status. When reading evaluation results, “the whole run says success” is not the end of the analysis. You need to know which layer of success you are looking at.
I think it is better to make those limits explicit rather than hide them. An evaluation framework is not a perfect copy of reality. It is a device for repeatedly checking the behaviors we care about. Knowing which parts you can trust, and which parts need separate inspection, is part of the design.
A Safety Net for the Next Stage
This evaluation framework was never the final goal by itself. It was closer to a prerequisite for more dangerous stages like Scheduler, Reflection, and Memory-Write.
Once an agent moves beyond answering questions and starts creating schedules, updating memory, and handling long-term state, the cost of regressions gets higher. A single wrong tool choice can become more than a failed response. It can turn into an incorrect state change.
So I added a small evaluation framework first. It gives us golden cases, deterministic runs, baseline gates, and known failures that remain visible inside the suite.
From the outside, this may look like adding a few tests and CLI commands. But the meaning is bigger than that. It is a decision not to manage agent behavior by intuition alone. If small diffs can quietly change decisions, the system also needs a way to catch those quiet changes.
한국어 버전
LLM 에이전트에 평가 프레임워크를 붙인 이유
Planner와 Executor를 조금 손봤을 뿐인데, 에이전트의 행동이 달라지는 순간이 있다. diff만 보면 별일 아닌 변경처럼 보인다. 조건문 하나가 바뀌었거나, 프롬프트 일부가 정리되었거나, 도구 선택 힌트가 조금 달라졌을 뿐이다.
그런데 LLM 에이전트에서는 이런 작은 변경이 “어떤 도구를 고르는가”를 조용히 바꾼다. 사용자는 똑같이 “최근 파일을 찾아줘”라고 말했는데, 에이전트는 어느 날부터 파일 목록 검색이 아니라 특정 파일 읽기를 시도할 수 있다. 겉으로는 큰 기능 변경이 없었는데도 실제 행동은 회귀한 것이다.
이 문제를 사람이 매번 눈으로 확인하는 방식으로는 오래 버티기 어렵다. 그래서 에이전트에 평가 프레임워크를 붙였다. 목적은 거창하지 않았다. Planner와 Executor를 고칠 때마다 최소한의 골든 케이스를 결정적으로 돌리고, 이전보다 나빠졌는지 기계적으로 확인하는 장치를 만드는 것이었다.
유닛 테스트만으로는 부족했다
일반적인 유닛 테스트는 함수의 입력과 출력을 확인하는 데 좋다. 하지만 에이전트의 핵심 문제는 단순한 함수 반환값보다 한 단계 위에 있다.
예를 들어 사용자의 요청을 해석하고, 필요한 도구를 고르고, 실행 순서를 정하고, 실패했을 때 다음 행동을 선택하는 흐름이 있다. 이 흐름은 여러 컴포넌트가 맞물려 만들어진다. 개별 함수는 통과해도 전체 의사결정은 틀릴 수 있다.
특히 도구 선택은 미묘하다. “읽어줘”, “찾아줘”, “최근”, “목록” 같은 단어들이 섞이면 에이전트는 어떤 도구를 호출해야 할지 판단해야 한다. 이 판단은 프롬프트, 힌트 테이블, TaskExtractor, Planner 규칙의 영향을 동시에 받는다.
그래서 필요한 것은 단순 유닛 테스트가 아니라, 실제 사용자 요청에 가까운 케이스를 넣고 에이전트가 어떤 계획과 도구 선택을 하는지 검증하는 평가 스위트였다.
골든 케이스와 결정적 러너
첫 버전은 골든 케이스를 JSONL로 관리하는 방식으로 만들었다. 처음에는 42개 케이스로 시작했고, 이후 62개, 70개까지 늘렸다. 각 케이스는 사용자의 요청과 기대되는 행동을 담는다. 핵심은 “이 요청에서는 어떤 도구를 골라야 하는가”, “어떤 흐름이 성공으로 간주되는가”를 명시해두는 것이다.
러너는 결정적으로 동작하도록 만들었다. 평가할 때마다 결과가 흔들리면 회귀 여부를 판단할 수 없기 때문이다. LLM 에이전트는 원래 비결정적인 요소를 품고 있지만, 최소한 평가 하네스 안에서는 가능한 한 같은 입력에 같은 판정을 내리도록 고정해야 한다.
CLI도 단순하게 구성했다. 실행, 비교, 베이스라인 갱신을 각각 분리했다. 평가를 돌리는 명령, 현재 결과를 기존 베이스라인과 비교하는 명령, 그리고 베이스라인을 명시적으로 갱신하는 명령을 따로 둔 것이다.
여기서 중요했던 점은 베이스라인이 자동으로 올라가지 않게 하는 것이었다. 테스트가 실패했는데 “현재 결과를 새 기준으로 삼자”가 너무 쉬워지면 평가 프레임워크는 금방 의미를 잃는다. 그래서 베이스라인 갱신은 별도 커맨드로만 가능하게 했다. 회귀가 발생하면 실패로 멈추고, 기준을 바꿀 때는 의도적으로 바꾸도록 했다.
베이스라인 게이팅의 역할
이 프레임워크의 핵심은 베이스라인 게이팅이다. 변경 전후로 평가를 돌렸을 때 기존보다 나빠진 케이스가 있으면 회귀로 판단한다. 성공이면 0, 회귀면 1, 사용법 오류면 2처럼 종료 코드도 명확히 나누었다. 이렇게 해두면 로컬 개발뿐 아니라 이후 CI나 자동화 단계에도 붙이기 쉽다.
좋았던 점은, 이 장치가 개발자의 심리적 부담을 줄여준다는 것이다. 에이전트 코드를 고칠 때마다 “혹시 도구 선택이 어딘가에서 깨졌을까?”를 머릿속으로만 걱정하지 않아도 된다. 최소한 우리가 중요하다고 정의한 케이스들에 대해서는 기계가 알려준다.
물론 이것이 모든 버그를 막아주지는 않는다. 하지만 Planner와 Executor처럼 작은 변경이 넓은 행동 변화를 만들 수 있는 영역에서는, 이런 게이트 하나가 개발 속도를 오히려 높여준다. 마음 놓고 고치려면, 고친 뒤 망가진 곳을 빨리 알 수 있어야 한다.
known-failure를 일부러 남겨둔 이유
재미있는 결정도 있었다. 실제로 “최근 파일 목록을 보여줘”에 가까운 요청에서 에이전트가 파일 검색 도구 대신 파일 읽기 도구를 고르는 버그가 있었다. 원인은 요청 안의 “file” 힌트가 잘못 작동하면서 도구 선택이 한쪽으로 기울어진 것이었다.
그 자리에서 바로 고치지 않았다. 당시 단계의 목표는 수정이 아니라 평가 프레임워크를 세우는 것이었기 때문이다. 대신 이 케이스를 known-failure로 스위트 안에 박아두었다.
이 패턴은 꽤 유용하다. 버그를 당장 고치지 못하더라도, 그것을 평가 스위트 안에 넣어두면 최소한 잊히지 않는다. 이후 관련 로직을 고칠 때 그 케이스가 계속 기준점 역할을 한다. known-failure는 실패를 방치하는 장치가 아니라, 실패를 추적 가능한 상태로 고정하는 장치에 가깝다.
한계를 알고 쓰는 것도 설계다
평가 하네스에도 한계는 있다. 예를 들어 전체 status는 시뮬레이션된 goal 경로를 기준으로 성공처럼 보일 수 있다. 하지만 실제 읽기 전용 단계에서 민감한 설정 파일 접근이 거부되는 경우처럼, 스텝 레벨에서는 실패가 기록될 수 있다.
이 경우 진실은 전체 status보다 스텝 레벨의 error code에 있다. 그래서 평가 결과를 볼 때도 “전체가 성공이라고 나왔으니 끝”이 아니라, 어떤 수준의 성공을 보고 있는지 알아야 한다.
나는 이런 한계를 숨기기보다 명시하는 쪽이 낫다고 본다. 평가 프레임워크는 현실을 완벽히 복제하는 시스템이 아니다. 우리가 중요하게 보는 행동을 반복 가능하게 검증하는 장치다. 어느 층위까지 믿을 수 있고, 어느 부분은 별도로 봐야 하는지 알고 쓰는 것이 설계의 일부다.
다음 단계를 위한 안전망
이 평가 프레임워크는 그 자체가 최종 목표는 아니었다. 이후 Scheduler, Reflection, Memory-Write 같은 더 위험한 단계로 가기 위한 전제 조건에 가까웠다.
에이전트가 단순히 답변하는 수준을 넘어, 스케줄을 만들고, 기억을 갱신하고, 장기적인 상태를 다루기 시작하면 회귀의 비용은 더 커진다. 잘못된 도구 선택 하나가 단순 실패가 아니라 잘못된 상태 변경으로 이어질 수 있다.
그래서 먼저 작은 평가 프레임워크를 붙였다. 골든 케이스를 쌓고, 결정적으로 돌리고, 베이스라인으로 막고, 알려진 실패를 스위트에 남기는 구조를 만들었다.
겉으로 보기에는 테스트 몇 개와 CLI 몇 개를 추가한 작업처럼 보일 수 있다. 하지만 실제 의미는 다르다. 에이전트의 행동을 감으로만 관리하지 않겠다는 선언에 가깝다. 작은 diff가 조용히 의사결정을 바꾸는 시스템이라면, 그 조용한 변화를 잡아내는 장치도 같이 있어야 한다.