속 빈 강정 — 함수는 있는데 왜 안 불렸나
AI 에이전트 생태계 구축기· 4 / 10
에이전트 시스템을 굴리다 보면 “간판 기능이 잘 동작한다”는 보고와 “그 기능이 실제로 불린다”는 사실이 늘 같은 말은 아니라는 걸 알게 된다. 이번에 뜯어본 프로젝트가 정확히 그 간극을 보여줬다.
복리 지능이라는 엔진
이 프로젝트는 두 원리로 가치를 설명한다. 원리1은 모든 산출물이 Library로 환류돼 시간이 갈수록 똑똑해진다는 복리 지능, 원리2는 세션은 여럿이어도 뇌·정체성은 하나라는 유기적 연결이다.
이 두 원리의 엔진이 “검증된 지식의 복리 축적”인데, 그 축적을 담보하는 자가학습·검증 루프가 실제로는 배선되지 않았다(교훈 저장 0건). 즉 간판 원리가 실동작하지 않는다.
이 한 문장이 결론을 미리 말한다.
자가학습 3층·검증 3종 — 기획서 상의 설계
기획서 단계에서 자가학습은 3층, 검증은 3종으로 설계됐다.
- A층(명시 교훈): “교훈:” 접두어를 감지해 그대로 저장한다.
- C층(자동 감지): 지크의 교정 말투를 감지해 후보를 제안하고, 승인 시 저장한다.
- 승격(SL-02): 같은 교정이 K회 반복되면 잠정 교훈을 규칙으로 승격한다. 한 번의 우연을 규칙으로 굳히는 과잉일반화를 막기 위한 장치다.
- 메타루프(SL-04): 검출율 추세를 보고 합격선을 자동 조정한다.
- 검증 A(작업자 자신 검토)·B(별도 세션 게이트,
source_facts대조)·C(백그라운드 Lint·비용 감시) 3종에 더해, **루프(VF-06)**가 작업자↔검증자 라운드를 돌리다 합격선 미달이면 재작업시킨다.
여기까지는 기획서고, 문제는 코드로 내려갔을 때다.
함수 3개 해부 — 코드는 멀쩡한데 호출처가 없다
승격·메타루프·검증루프를 구현한 세 함수를 각각 열어봤다. 셋 다 결정론이고 설계 의도가 코드에 정확히 반영돼 있다는 점에서는 흠잡을 데가 없다.
| 함수 | 기능 | 어디까지 지어졌나 | 호출처 |
|---|---|---|---|
promote.mjs(SL-02) | 같은 실수가 3회 이상 쌓이면 승격 후보로 뽑는다 | 임계값 판정 로직 완결 | sl02-measure.mjs 1건 |
learning-meta.mjs(SL-04) | 검출율 시계열의 추세(기울기)를 보고 합격선을 0.05씩 조정한다 | 회귀 계산·상하한 클램프까지 완결 | sl04-measure.mjs 1건 |
verify-loop.mjs(VF-06) | 작업자↔검증자를 최대 3라운드 반복하고 초과 시 에스컬레이션한다 | 무한루프 방지까지 갖춘 완결 로직 | vf06-measure.mjs 1건 |
세 함수 모두 호출처가 자기 자신을 측정하는 스크립트 하나뿐이다. 특히 learning-meta.mjs는 파일 맨 위 주석이 스스로 한계를 실토해 놓았다.
실 추세 판정엔 운영 누적 시계열이 필요 — 여기선 메커니즘(기울기·조정 규칙)을 결정론으로 제공하고, 실 시계열 정량은 운영 누적 후로 미룬다(test-spec SL-04: 불확실 — 정량 없으면 정성 강등).
즉, 이 함수가 먹을 데이터를 쌓을 운영 루프 자체가 아직 없다는 걸 작성 시점부터 코드가 알고 있었다. verify-loop.mjs도 마찬가지다. 파일 주석은 작업자·검증자로 “실제=작업자 query·verify 에이전트, 측정=스텁”이 주입된다고 명시한다 — 측정에서는 진짜 작업자·검증자가 아니라 스텁이 대신 들어간다는 뜻이다.
grep 전수조사 — 못을 박다
세 함수가 각각 measure 파일에서만 불린다는 걸 확인으로 못박기 위해 런타임 경로 전체를 뒤졌다. server.ts·drop-core.mjs·telegram-listener·terminal-adapter·backstage 잡 어디에서도 이 세 함수를 import하지 않는다. 직접 재확인한 import 구문도 각 measure 파일에 한 줄씩뿐이다 — 이 세 줄이 세 함수가 시스템에서 불리는 유일한 자리다.
같은 계열의 다른 껍데기도 있다. verify-roi.mjs(과검증 ROI 게이팅)·lint-links.mjs(서재 링크 Lint) 둘 다 자율 잡에 미배선이다.
비유하자면 이 함수들은 벤치 위에서 시동까지 걸어본 엔진이다. 회전수도 소음도 정상이지만, 그 엔진이 구동축에 연결돼 있지 않으면 아무리 잘 돌아도 차는 1밀리미터도 움직이지 않는다. 측정은 “엔진이 도는지”만 봤지 “바퀴로 연결됐는지”는 애초에 보지 않았다.
배선은 됐는데 데이터가 없는 층
대비할 게 하나 더 있다. 승격·메타루프·검증루프처럼 아예 안 불리는 게 아니라, 배선은 됐는데 실사용이 0인 층도 있다.
- **자가학습 기본(A 명시 교훈·C 자동 감지)**은 실제로 배선돼 있다.
server.ts의 교정·교훈 감지·저장 로직부터drop-core.mjs의loadLessons()(비서 프롬프트 주입)까지 코드 경로가 이어진다. - 그런데
memory/lessons/에 저장된 교훈은 0건이다(README만 있다). 배선됐지만 한 번도 실사용되지 않았다. - 더 심각한 정합 구멍도 있다. 교훈 캡처는 옛
server.ts(앱 채널 전용)에만 있고, 앱·텔레그램·터미널 3채널을 통합한 채널중립 엔진drop-core.mjs는loadLessons()(주입)만 부른다.
게다가 캡처는 옛 server.ts(앱 채널)에만 있고 채널 중립 엔진 drop-core.mjs는 주입만 한다 → 텔레그램·터미널로 던진 교정은 교훈으로 저장조차 안 된다. 3채널 통합 때 자가학습 캡처가 따라오지 못한 정합 구멍.
왜 이렇게 됐나
소스가 스스로 밝힌 구조적 원인은 셋이다.
- design-now 원칙의 그림자: “설계·자리·트리거는 지금, 구현만 단계적으로”라는 원칙 자체는 옳았다. 그러나 실제로는 함수 껍데기를 짓고 측정을 통과시킨 지점까지만 하고 그걸 “완료”로 보고했다.
- 측정이 배선을 안 봄: test-spec은 함수의 출력만 검증했고, “파이프라인에 꽂혀 실사용되는가”는 애초에 검증 항목에 없었다.
- 단기 집중 작업의 밀도: 검증·자가학습·비용·보안·개발팀 관련 태스크 5개가 2026-07-03 하루에 몰렸다. 각 함수를 짓고 측정을 통과시켰지만 런타임 배선 확인은 통째로 빠졌다.
건질 것과 버릴 것
정직한 재분류가 먼저였다. 실제로 도는 것과 껍데기를 표로 갈라 “건질 것 지도”를 만들었는데, 자가학습 심화·검증 루프·개발팀 모드·보안 스캐너 껍데기 계열은 버림이었다. 대신 이식 가치가 있는 건 코드가 아니라 패턴과 지식이다.
- 패턴: 결정론 하드체크와 소프트 판정을 분리하는 설계.
- 발상: “입구만 다르고 처리는 하나”라는 채널중립 엔진 아이디어.
- 지식: 실 API 스펙(엔드포인트·인증·필드).
- 교훈: SDK·launchd 운영 중 얻은 구체적 교훈들.
착지
잘 설계된 결정론 함수라도 — 승격 임계값 3, 선형회귀 기울기, 상한 3라운드 재시도 루프 — 파이프라인에 단 한 줄의 import가 빠지면 그 함수는 시스템 안에서 존재하지 않는 것과 실질적으로 같다. 코드 품질과 시스템 동작은 서로 다른 축이다.
간판 기능일수록 검증이 허술해지기 쉽다는 것도 이번에 확인한 사실이다. 화려한 이름을 붙인 기능일수록, 그 이름값이 “이미 다 됐다”는 착시를 강화한다.