내 친구의 Rust 코드를 읽는 가짜 면접관을 만들다
요약
이 글은 코드를 읽고 기술 면접을 진행하는 모의 인터뷰 도구 'grill' 개발 과정을 소개합니다. 이 도구는 오픈 모델과 로컬 환경에서 실행되며, 단순한 교과서적 질문 대신 실제 시니어 레벨 면접처럼 코드 스니펫 기반으로 심층적인 질의응답 및 평가를 수행할 수 있게 설계되었습니다.
핵심 포인트
- grill은 코드를 읽어 기술 면접을 진행하는 모의 도구입니다.
- 오픈 모델과 로컬 환경에서 실행되어 네트워크 연결 없이 사용 가능합니다.
- 단순한 질문 나열이 아닌, 코드 스니펫 기반의 심층적인 질의응답에 초점을 맞춥니다.
- 모델에게는 한 번에 하나의 작은 작업만 수행하도록 설계하여 안정성을 높였습니다.
이 글은 Hacktoberfest Weekend Challenge: Build for a Friend에 제출하는 내용입니다.
내가 만든 것
grill은 코드를 읽는 가짜 기술 면접관입니다. 터미널에서, 오픈 모델 위에서, 자신의 노트북에서 실행됩니다.
저는 친구 Akash를 위해 이것을 만들었습니다. 그는 지난 1년 동안 Rust를 작성하고 사이드 프로젝트로 TypeScript를 배포했으며, 정규직 역할을 찾고 있습니다. 그가 가장 두려워하는 라운드는 Rust와 JavaScript 심층 면접입니다.
대부분의 면접 준비 도구는 같은 교과서적인 질문을 합니다: 클로저(closure)란 무엇인가, 소유권(ownership)을 설명하라. 실제 시니어 레벨 면접은 그렇게 진행되지 않습니다. 그들은
세션은 다음과 같이 진행됩니다. 코드와 위치는 실제 스캐너 출력이며, 플레이스홀더 줄 대신 본인이 직접 질문, 답변 및 등급을 기입한 내용을 붙여넣으세요:
── 1 of 5 ──
AkashJana18/miccli src/audio.rs:62 shared-state
62 │ pub fn start_capture(&self) -> Result<AudioStream> {
...
Akash가 말한 것
[추가: Akash의 반응, 그의 말로]
코드
GitHub logo yashksaini-coder / grill

grill
코드를 읽은 모의 면접관입니다. 네트워크 연결 없이, 로컬 노트북에서 오픈 모델로 실행됩니다.
대부분의 면접 준비는 교과서적인 질문들을 다룹니다. 하지만 실제 시니어 레벨 인터뷰에서는
- **Gemma**는 Google의 open-weight 모델이며,
localhost에서 Ollama을 통해 서비스됩니다. - **Mastra**는 두 개의 에이전트와 도구를 위한 오픈소스 TypeScript 프레임워크입니다.
설계 규칙: 모델은 한 번에 작은 작업 하나만 수행한다
노트북에서 실행되는 4B 모델은 인터뷰를 계획하거나, 루브릭(rubric)을 머릿속에 유지하거나, 다섯 턴 전에 무엇을 질문했는지 기억할 수 없습니다. 그래서 저는 그렇게 요청하지 않았습니다. 단계의 순서는 일반 TypeScript에 존재합니다. 모델에게는 오직 세 가지 중 하나만 수행하도록 요청됩니다: 질문 작성, 후속 질문 작성, 또는 교환 내용 채점.
이러한 호출들 각각에는 실제 코드 스니펫이 다시 전달됩니다. 모델은 코드를 기억할 필요가 없으므로, 상상할 여지가 적습니다.
1단계: 질문할 가치가 있는 라인 찾기
스캐너는 총 38개의 패턴 테이블로 구성되어 있으며, Rust용 17개와 JavaScript 및 TypeScript용 21개가 있습니다. 각 패턴은 주제(topic), 가중치(weight), 그리고 좋은 인터뷰어가 취할 수 있는 관점(angle)을 가지고 있습니다:
rust("lock", "concurrency", /\.(lock|read|write)\(\)\s*(\.\await|\.unwrap\(\)|\?)/, 4,
"가드가 얼마나 오래 살아남는지, await를 거쳐 유지되는지, 그리고 오염되거나 경쟁하는 잠금(poisoned or contended lock)이 무엇을 하는지에 대한 내용"),
...
한 라인에서 정규 표현식(regex)에 걸렸다고 해서 질문이 되는 것은 아닙니다. 따라서 모든 히트(hit)마다 스캐너는 해당 코드를 감싸는 함수까지 이동하여, 중괄호(braces)를 일치시켜 끝을 찾고 전체 내용을 구성합니다. 가중치는 함수별로 합산되며, 겹치는 범위(overlapping frames)는 가장 강력한 것으로 대체되고, 각 저장소(repository)는 주제에 걸쳐 라운드 로빈 방식으로 최고의 40개 지점을 기여하여, .unwrap()으로 가득 찬 코드베이스가 인터뷰 전체를 에러 핸들링으로 만들지 않도록 합니다.
이것은 파서(parser)가 아니라 정규 표현식과 중괄호 카운팅입니다. 흥미로운 코드를 찾아낼 뿐, 이해하지는 못합니다. 이해하는 것은 모델의 임무입니다.
Stage 2: Gemma에서 작동하는 두 개의 Mastra 에이전트
Mastra는 OpenAI와 호환되는 엔드포인트를 통해 Ollama에 연결되므로, 제공자 SDK나 API 키가 필요 없습니다:
const model = { providerId: "local", modelId: config.modelId, url: config.url, apiKey: "local" };
this.interviewer = new Agent({ id: "interviewer", name: "interviewer", instructions: INTERVIEWER, model });
...
**면접관(interviewer)**은 정확히 하나의 질문을 하도록 지시받습니다. 이 질문은 코드 스니펫에서 식별자를 이름 짓게 하여 지원자가 코드를 읽었다는 것을 알 수 있게 해야 하며, 절대 답변하거나 힌트를 주어서는 안 됩니다.
**평가자(grader)**는 별도의 프롬프트를 가진 분리된 에이전트입니다. 질문을 작성하지 않았기 때문에 답변의 질에 대해 이해관계가 없습니다. 이 에이전트는 JSON을 반환합니다:
{"score": 2, "verdict": "one sentence", "missed": ["specific point"], "better": "the answer you wanted"}
소형 모델들은 JSON을 산문과 코드 울타리(code fences)로 감쌉니다. 따라서 답변을 신뢰하는 대신, grill은 첫 번째 균형 잡힌 {...}를 추출하고 zod으로 검증하며, 이 과정이 실패하면 모델에게 자체의 잘못된 답변을 보여주고 한 번 더 질문합니다.
또한 옵트인(opt-in) read_source 도구(grill start --tools)가 있습니다. 이것은 면접관이 스니펫을 넘어 호출자나 타입 정의를 볼 수 있게 합니다. 이 기능은 저장소에 국한됩니다. 즉, 체크아웃 외부로 해결되는 경로는 거부됩니다. 이 기능은 도구 호출(tool calling) 기능을 가진 모델이 필요하므로 기본적으로 비활성화되어 있습니다.
Stage 3: 약점 지도 (weak-spot map)
모든 라운드는 ~/.grill/sessions 아래에 JSON으로 저장됩니다. 다음 세션은 가중치 기반 드로우(weighted draw)를 통해 지점을 선택합니다:
export function priority(spot: Hotspot, seen: Set<string>, stats: TopicStat[]): number {
const stat = stats.find((s) => s.topic === spot.topic);
const weakness = stat ? (4 - stat.mean) / 4 : 0.5;
...
수명(lifetimes)에서 점수를 1점 받으면, 그 수명 관련 지점이 다시 나옵니다. unsafe에서 4점을 받으면, 그 주제는 사라집니다. 이미 질문된 지점은 드물지만 금지되지는 않습니다. 왜냐하면 같은 것을 두 번 질문하는 것이 그것을 배웠는지 알아내는 방법이기 때문입니다.
LLM 없이 LLM 앱 테스트하기
테스트 스위트가 GPU를 필요로 하는 것은 원치 않았습니다. 테스트는 임의의 로컬 포트에서 가짜 OpenAI 호환 서버를 시작하고 실제 Mastra 에이전트를 그곳으로 연결합니다. 이를 통해 전체 과정(JSON 재시도, :skip 및 :quit, 경로 이스케이프 거절을 포함한 도구 호출)을 커버할 수 있습니다. 모델 없이 18개의 테스트가 몇 초 만에 완료됩니다.
Open Innovation이 중요한 이유?
이 프로젝트에서 이것은 선택 사항이 아닙니다. 폐쇄형 API는 Akash에게 네 가지 구체적인 방식으로 더 나쁜 도구가 되었을 것입니다.
그의 서툰 대답은 여전히 그의 것이에요. 면접 연습은 틀릴 의향이 있을 때만 효과가 있습니다. grill은 그가 하는 모든 약한 답변을 설계상 저장합니다. 이 파일은 그의 디스크에 ~/.grill에 위치합니다. 호스팅된 API를 사용한다면, 그가 모르는 모든 것의 기록이 다른 사람의 서버에 있게 됩니다.
개인 코드가 작동해요. grill ingest는 로컬 경로를 가져옵니다. NDA 하에 있는 업무 코드, 어디에도 푸시할 수 없는 저장소, 보여줄 준비가 되지 않은 사이드 프로젝트: 이 모든 것을 기기를 떠나지 않고 면접 볼 수 있습니다. 모델이 다운로드되면, 로컬 경로 세션은 네트워크 연결이 전혀 필요 없습니다.
연습은 무료라서 많이 할 수 있어요. 질문당 3번의 모델 호출, 세션당 5개의 질문, 필요한 만큼 많은 세션입니다. 이는 제가 장려하고 싶은 행동과 정확히 일치하는 청구서가 되는 측정형 API에서는 불가능합니다.
면접관을 바꿀 수 있습니다. 루브릭은 src/interview/mastra-brain.ts에 있는 문자열입니다. 만약 그가 언어 지식보다는 시스템 설계에 더 신경 쓰는 회사에 대비하고 있다면, 프롬프트를 수정할 수 있습니다. 더 좋은 노트북을 얻게 된다면, GRILL_MODEL은 환경 변수 하나로 더 큰 모델로 교체됩니다. Ollama가 적합하지 않다면, GRILL_URL은 llama.cpp나 LM Studio를 가리킵니다. 이 도구의 어떤 부분도 특정 공급업체에 묶여 있지 않습니다.
오픈 웨이트(Open weights)는 그것을 개인적이고 무료로 만들었습니다. 오픈 프레임워크(open framework)는 그것을 변경 가능하게 만들었습니다. 그는 세 가지 모두가 필요합니다.
- 스캐너는 의도적인
unwrap()과 부주의한unwrap()을 구분할 수 없습니다. 후보군만 제시합니다. 일부는 평범합니다. - 질문의 질은 모델에 달려 있습니다. 더 큰 로컬 모델일수록 날카로운 질문을 합니다.
- 점수는 판정이 아니라 학습 보조 자료입니다. 숫자가 아닌 '놓친' 목록을 읽으세요.
수상 부문 (Prize Categories)
- Gemma의 최적 활용(Best Use of Gemma). Gemma는 Ollama를 통해 로컬에서 실행되며, 질문하기, 압박하기, 채점하기 세 가지 작업을 모두 수행합니다. 고정된 오케스트레이션과 호출당 하나의 작은 작업으로 구성된 전체 설계는 작은 오픈 가중치 모델만으로 충분함을 보여주기 위해 존재합니다.
- Mastra의 최적 활용(Best Use of Mastra). 오픈 모델 위에 두 개의 Mastra 에이전트와, 제공업체 SDK가 없는 로컬 OpenAI 호환 엔드포인트에 연결된 격리된
read_source도구를 사용했습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기