Foundry 구동을 위한 로컬 LLM 기반의 Function Calling: Fuzz, Read, Repeat
요약
로컬 LLM(Qwen2.5-Coder)을 활용하여 Foundry 프레임워크 기반의 퍼즈 테스트를 자동화하는 에이전트 루프 구축 방법을 소개합니다. 도구 세트를 최소화하고 출력값을 절단(truncation)하는 설계 전략을 통해 작은 모델의 한계를 극복합니다.
핵심 포인트
- Qwen2.5-Coder와 Ollama를 활용한 로컬 에이전트 구현
- 도구 세트 최소화 및 단일 파일 작성 방식을 통한 모델 혼란 방지
- 컨텍스트 과부하 방지를 위한 테스트 실행 결과의 공격적 절단(Truncation)
- 로컬 환경에서 데이터 보안을 유지하며 자동화된 보안 테스트 수행
처음으로 로컬 모델이 감독 없이 Foundry를 구동하게 했을 때, 그 모델은 퍼즈 테스트 (fuzz test)를 수정하기 위해 테스트 함수의 이름을 바꾸는 데 무려 11번의 턴을 소비했습니다. 로직을 변경한 것이 아니라, 이름을 바꾼 것이었습니다. testFuzz_withdraw, 그다음은 test_fuzz_withdraw, 그다음은 testWithdrawFuzz로 바꾸며, 매번 테스트 스위트 (suite)를 실행하고 새로운 낙관주의를 품은 채 동일한 컴파일러 에러를 읽었습니다.
에이전트 루프 (agent loop)에서 작은 로컬 모델이 할 수 있는 것과 할 수 없는 것을 받아들인 후, 그 실험은 여전히 제 워크플로우에서 가장 유용한 도구 중 하나가 되었습니다. 아이디어는 간단합니다. qwen2.5-coder에게 세 가지 도구(forge 테스트 실행, 컨트랙트 파일 읽기, 퍼즈 테스트 작성)를 주고 대상 컨트랙트 (target contract)를 지정하는 것입니다. 모델은 코드를 읽고, 속성 테스트 (property tests)를 작성하고, 이를 실행하며, 실패 내용을 읽고, 반복합니다. 제대로 작동할 때는 제가 결국에는 찾아냈을 깨진 불변량 (broken invariants)을 찾아내 주지만, 제가 커피를 마시는 동안 모든 작업이 이루어지며 모든 데이터는 제 기기에 머물러 있습니다.
세 가지 도구
Ollama는 채팅 API를 통해 함수 호출 (function calling)을 지원합니다. JSON 스키마 (JSON schema)로 도구를 설명하면 모델이 tool_calls를 반환하고, 사용자가 이를 실행하여 결과를 다시 전달하는 방식입니다. 도구 세트는 최소한으로 유지하세요. 추가되는 도구가 많아질수록 7b 모델이 혼란을 겪을 가능성도 높아집니다.
const tools = [
{
type: "function",
...
여기에 숨겨진 두 가지 설계 결정이 있습니다. 첫째, write_fuzz_test는 경로를 받지 않습니다. 모델은 항상 test/Fuzz.t.sol이라는 단 하나의 파일을 전체 내용으로 작성합니다. 작은 모델에게 파일 경로를 선택하게 하거나 디프 (diffs)를 생성하게 하는 것은 contracts/test/../test/Fuzz.sol 같은 경로를 만들거나 존재하지 않는 라인에 패치 (patches)를 시도하게 만드는 지름길입니다. 둘째, run_forge_test는 인자를 전혀 받지 않으므로 모델이 임의로 플래그 (flags)를 만들어낼 수 없습니다.
실행기 (executors)는 얇은 래퍼 (wrappers)입니다. 유일하게 흥미로운 것은 테스트 러너 (test runner)인데, 이는 출력을 공격적으로 잘라냅니다 (truncates).
import { execFileSync } from "node:child_process";
function runForgeTest(): string {
...
절단(Truncation)은 매우 중요합니다. 실패한 퍼즈 테스트(fuzz test)에 대한 forge trace는 엄청난 크기가 될 수 있으며, 4만 토큰에 달하는 trace를 작은 모델의 컨텍스트(context)에 쏟아붓는 것은 모델에게 정보를 제공하는 것이 아니라 모델을 '뇌사(lobotomize)' 상태로 만들기 때문입니다. 저는 Foundry가 반례(counterexample)와 요약(summary)을 배치하는 부분인 뒷부분(tail)만 남겨둡니다.
루프 (The loop)
const messages: Message[] = [
{ role: "system", content: SYSTEM_PROMPT },
{ role: "user", content: `Target contract: src/${target}. Read it, identify invariants, write fuzz tests, run them, iterate until they compile and either pass or reveal a real counterexample.` },
...
시스템 프롬프트(system prompt)는 반복적인 실험(iteration)이 가장 많이 이루어진 부분입니다. 그중 자리를 잡은 부분들은 다음과 같습니다:
You write Foundry fuzz tests for one target contract.
Rules:
- ALWAYS read the target contract before writing any test.
...
마지막 줄은 중요하며 직관에 어긋납니다. 모델의 본능은 테스트가 통과할 때까지 단언(assertion)을 약화시켜 실패하는 테스트를 "수정"하려 한다는 점인데, 이는 보안 도구가 해야 할 일과 정반대되는 행동입니다. 정당한 반례(counterexample)가 발견되는 것이 승리 조건임을 명시적으로 알려주면, 이러한 본능적 반응을 대부분(항상 그렇지는 않지만) 억제할 수 있습니다. 저는 spectr-ai를 구축할 때도 동일한 실패 모드를 겪었습니다. 작은 모델들은 오류를 없애도록 훈련되어 있으며, 보안 작업에서는 그 오류가 곧 결과물인 경우가 많기 때문입니다.
모델이 막히는 지점과 이를 제한하는 방법
작은 모델들은 긴 에이전트 루프(agent loop)에서 맥락을 놓칩니다. 약 8회에서 12회 정도의 턴(turn)이 지나면, qwen2.5-coder:7b는 시스템 프롬프트의 제약 사항을 잊어버리거나, 이미 읽었던 파일을 다시 읽거나, 동일한 테스트의 고장 난 두 버전 사이를 반복하는 현상이 나타납니다. 1.5b 모델은 더 심각하여, 잘못된 인자(argument)가 포함된 도구 호출(tool call)을 내뱉기 전까지 4번째 턴을 넘기는 경우가 드뭅니다. 이는 프롬프트로 해결할 수 있는 버그가 아니라, 제한된 컨텍스트 처리 능력과 적은 파라미터 수(parameter counts)가 실무에서 나타나는 실제 모습입니다.
그래서 저는 모델과 싸우는 대신 루프를 제한하기로 했습니다.
강제 턴 제한 (Hard turn cap). MAX_TURNS = 12. 이 횟수를 넘어가면 test/Fuzz.t.sol에 있는 내용을 그대로 출력으로 간주하고 제가 직접 개입합니다.
루프 탐지 (Loop detection). 저는 각 write_fuzz_test 페이로드(payload)를 해싱합니다. 동일한 해시가 두 번 나타나면 모델이 순환(cycling)하고 있다는 의미이며, 이때 하네스(harness)는 이를 알리는 사용자 메시지를 한 번 주입합니다. 두 번째 반복이 발생하면 실행을 종료합니다.
컴파일 우선 게이팅 (Compile-first gating). 가장 흔한 데스 스파이럴(death spiral)은 컴파일러 오류(잘못된 import 경로, 잘못된 pragma 등)를 쫓는 것입니다. 하네스는 파일에 이미 검증된 스켈레톤(skeleton, 올바른 import 및 setUp 내 타겟의 배포된 인스턴스 포함)을 미리 심어두어, 모델이 컴파일이 가능한 상태에서 시작하여 속성(properties)을 채워 넣기만 하면 되도록 합니다. 이 단 한 번의 변화만으로 유용한 결과로 끝나는 실행 횟수가 대략 두 배로 늘어났습니다.
하나의 타겟, 하나의 파일. 저는 모델에게 프로토콜 전체를 퍼징(fuzz)하라고 요구하지 않습니다. 하나의 컨트랙트, 때로는 하나의 함수만을 퍼징하라고 요구합니다. 좁은 범위(Small scope) 설정은 집중된 속성 테스트(property test)와 11번째 턴에서 함수 이름을 바꾸는 행위 사이의 차이를 만듭니다.
이러한 제한 사항 덕분에, 제 컴퓨터(WSL2, Ollama, 7b 모델)에서 실행 시 몇 분 정도가 소요되며 대부분의 경우 유용하게 끝납니다. 즉, 제가 수정하여 보관할 수 있는 속성 테스트를 컴파일하거나, 진정한 반례(counterexample)를 찾아냅니다. 모델은 코드가 무엇을
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기