kontexus-mcp를 사용한 리팩토링

#FTM:strict 모드, 실전투입

리팩토링에 최적의 모드.

몇시간 걸릴지 모르겠지만, 기대된다.

TMUX 환경. 왼쪽 Pane은 Claude Opus-5-high Model, 오른쪽은 32 Turn Refactoring 진행상황을 나타내는 Kontexus-CLI

AspectScope?

날짜와 시간에 따른 위상변이를 추적하는 AspectScope

특정 지역+시간대역의 Natal Chart 위상을 조사하는 앱이다.

키보드/마우스/트랙패드에 대단히 세밀하게 반응해야 한다. 전통적인 이벤트 핸들러 방식으로 만드는 건 결과는 나오겠지만 정확도 및 안정성 면에서 도저히 견디기 힘들다. 시한폭탄을 돌려막기 하는 느낌.

모든 I/O는 통제되야 한다.

그러기 위해서는 키보드/마우스/트랙패드의 데이터는 로직없이 신호로 처리되야 하고, 상태전이테이블을 통해서만 앱의 흐름이 이어져야 한다.

약 3주전 이걸 만들 때도 처음부터 엄청 신경썼지만 Codex든 Claude든 이런 완전한 상태전이테이블에 의한 코드흐름 진행 방식은 학습데이터에서 거의 없기 때문에, 내 의도와 전혀 다른 결과물만을 만들어냈다.

LLM에 지시하는 것 만으로도 스트레스 엄청 받았고, 대화를 저장하고 LLM이 태스크 관리하는 context-storage-mcp 만으로는 뭔가 부족하다는 걸 느꼈었다. 그래서 내가 지시를 하는게 아니라 MCP가 함수형 추론을 따라가도록 만든게 functional-thinking-mcp (FTM) 이다. context-storage-mcp에 기능이 더해지면서 본격적인 태스크 관리 체계로 발전하면서, 새로운 workgraph-memory-mcp (WGM) 이라는 프로젝트로 이어졌고, WGM에서는 LLM이 태스크 관리체계에 제한된 접근만 하게 했다이유는 비밀.

WGM과 FTM이 합쳐진 효과는 대단했으나, 두 서버가 각각 다른 메모리 공간에 있다보니 턴이 길어질수록 LLM이 거짓말을 내놓는 경우가 자주 생기는 것을 파악하더라도 MCP에서 자동으로 그 상태를 지적하지는 못했다.

여러 고민 끝에, 주장/판단/결론 등을 내놓을 때 증거가 없으면 오류를 발생시킴으로써 LLM이 뻥치는 것에 비용이 든다는 사실을 인지시켰다. 그리고 각각의 턴 진행이 이음(Seam)이라는 연속성을 가지게 함으로써, 세션이 달라지더라도 태스크의 진행흐름은 여전히 추적가능하고 태스크 문맥과 채팅내용을 LLM 스스로가 검색하고 참조할 수 있게함으로써, 언제 세션을 다시 시작하더라도 곧바로 이어갈 수 있게 되었다. 이것은 LLM 스스로가 잘못된 결론이나 기록/주장을 스스로 파악하게 한다.

따로 떨어진 두개의 MCP를 묶어주는건 AGENTS.md/CLAUDE.md 였기 때문에 LLM이 끼어들 여지가 있으므로 WGM+FTM 기능이 통합되야만 했다. 이것이 kontexus-mcp 를 만들 수 밖에 없었던 이유다.

#FTM:strict 라는 표현에서 #FTM은 LLM에게 알리는 지시자이다. 함수형 추론을 실질적으로 강제하는 요청이다. 이 지시자가 포함된 요청에 의해 LLM은 판단 자체를 구조적으로 기록하기 시작한다. strict는 판단과 그 증거사이의 관계를 가장 엄격하게 관리한다는 뜻인데, standard, verified, strict 세가지 모드이고, 기본은 standard 모드로써, 명시하지 않으면 standard가 기본모드다.

검사 강도

standard에서는 “이 불변 조건을 지켰습니다”라고 서술만 해도 kontexus-mcp 서버가 문제 삼지 않는다. 근거 없는 진술과 근거 있는 진술이 같은 무게로 통과한다.

이럴 때 붙인다:

  • 여러 갈래 중 하나를 고르는 설계 판단
  • 원인을 좁혀 가는 디버깅
  • 여러 턴에 걸쳐 이어질 작업의 첫 턴
  • 나중에 “그때 왜 그렇게 했는가”를 되짚을 일이 있을 작업

verified 에서는 “지켰다”는 말을 실행에 묶는다. 즉, LLM이 뭔가를 주장하고 결론내리고 보고하는 그 문장을 쓰려면 검사를 돌리게 만들고, 돌리지 않았으면 그 사실이 기록에 남게 한다.

이럴 때 붙인다:

  • 여러 입력 소스가 하나의 상태 머신에 동시에 개입하는 로직
  • 경우의 수가 조합으로 늘어나 눈으로는 다 못 보는 분기
  • 이미 한 번 회귀가 났던 자리를 다시 건드릴 때
  • 리팩터링처럼 “겉보기 동작은 그대로여야 한다”가 전제인 작업

stircit는 verified 요구를 전부 유지한 채, 여기에 하나가 더 붙는다. 실제로 실행된(계획 상태가 아닌) 다섯 종류의 Effect마다 활성바인딩식별자를 적어야 한다. 이 말은 승인 없는 실행을 막는 다는 것이다. 판단을 했다는 것이 곧 파일을 고쳐도 된다는 뜻은 아니며, strict는 그 둘을 갈라놓고, 실행 하나하나에 대해 “이것은 어느 작업으로 승인된 것인가”를 남기게 한다. 나중에 변경 이력을 거슬러 올라갈 때 각 수정이 어느 작업에 속했는지가 기록으로 남는다.

이럴 때 붙인다:

  • 공유 상태를 다루는 코드에 실제 파일 수정이 들어가는 고위험 작업
  • 여러 작업이 뒤섞이기 쉬워 나중에 “이 수정은 어느 작업이었나”를 반드시 가려야 할 때
  • 되돌리기 어려운 실행(마이그레이션, 외부로 나가는 요청)이 섞이는 작업

어떤 서비스의 모델이든 리팩토링 기능 자체는 문제없다. 다만, 턴이 길어질 때, 진행도중 발생하는 모든 변화에 대응하면서 처음 목적과 목표를 달성하게 하는 건 AGENTS.md 또는 CLAUDE.md 만으로는 역부족이다.

kontexus-mcp는 Harness를 대체하지 않는다

에이전트 루프는 결국 하나의 식으로 표현될 수 있다: S(n+1) = f( S(n), O(n) )

S는 다음 턴으로 넘어가는 상태(state)이고, O는 관측(observing)이다.

루프를 설계할 때 손대게 되는 것은 대개 f이다 – 어떤 도구를 주고, 프롬프트를 어떻게 쓰고, 몇 번 되돌릴지.

그러나 루프의 성질을 결정하는 것은 f가 아니라 세 가지가 어디에 기록되고 유지되는가이다.

  1. S가 어디에 저장되는가
  2. O가 누구를 경유해서 f에 들어오는가
  3. 루프를 멈출지 누가 판정하는가

KM은 이 셋 전부에 걸쳐 있고, 셋 다 위치를 옮기는 것이지 정보를 더 주는 것이 아니다.

kontexus-mcp가 없을 때 루프가 놓이는 자리

S의 수명이 컨텍스트 창의 수명과 같다. 상태가 컨텍스트 창 안에만 있으면, 창의 압축·절단·세션 종료가 전부 S의 소실이다.

문제는 소실 자체가 아니라 소실이 조용하다는 것인데, 루프는 자기가 무엇을 잃었는지 알 수 없으므로 잃은 채로 계속 돈다. 그래서 다음 등식이 성립한다: 루프의 최대 길이 = 컨텍스트 한도

즉, 며칠에 걸친 작업을 하나의 루프로 다룰 수 없다는 뜻이다.

O가 전부 모델을 경유한다. 무슨 말이냐면, 도구 출력은 관측처럼 보이지만, 다음 턴으로 넘어갈 때는 모델이 요약한 형태로만 남는다. 즉 실제로는 이렇다: O(n) = g( 모델의 서술 )

이때 루프는 되먹임이 있는 것처럼 보이지만, 되먹이는 신호가 자기 출력이므로 오차 신호가 존재하지 않는다. 드리프트가 얼마나 벌어지든 루프 내부에서 검출할 방법이 원리적으로 없다. “산출물을 보존했습니다”라는 문장이 세션과 함께 사라지는 임시 디렉터리를 가리켜도, 루프 안에서는 그 문장과 참인 문장이 구별되지 않는다.

종료 판정자와 피판정자가 같다. “완료했습니다”가 유일한 종료 조건이다. 판정하는 쪽과 판정받는 쪽이 같은 주체이므로, 판정은 작업의 함수가 아니라 문장 생성의 함수이다. 또한 작업 단위의 이름을 모델이 만들기 때문에 같은 일이 두 이름을 갖거나 다른 일이 한 이름을 갖는 것을 막는 구조가 없다. 이름이 어긋나면 이후의 기록·검색·인계가 전부 어긋난다.

Kontexus-mcp가 옮기는 것

S를 컨텍스트 밖으로 꺼낸다. MCP 호출은 ftm_seam 이라는 테이블에 기록되고 상태가 Hash 값으로 고정된다. 상태를 바이트 단위로 비교할 수 있기 때문에 같은 자리에 왔는가 여부를 알 수 있다. 이에 따라 컨텍스트 압축에 영향 받지 않으며 루프 길이에 무관해진다. 또한 관측 채널 하나를 모델 밖으로 뺀다. 즉, 모델이 만들어내지 못하는 관측 자료를 추가한다. 이로써 O(n)의 일부가 모델을 경유하지 않고 루프에 들어옴에 따라 없던 오차 신호가 여기서 생긴다. 이것은 관측이므로 에이전트의 동의와 무관하게 사실이다. 가장 달라지는 것은 종료 조건인데, 종료 판정을 말이 아니라 행동에 걸어버린다. kontexus-mcp 에서는 태스크와 태스크, 턴태스크와 턴태스크 사이의 전이에 검사가 따라 붙는다. 루프가 계속 이어지려면 반드시 그 행동을 해야하는 것이다.

규율을 프롬프트가 아니라 서버 상태로 유지한다

이것이 핵심이다.

아무리 턴이 길어져도, 도구들이 내놓은 온갖 코드가 나무해도, LLM은 말이 아니라 kontexus-mcp 서버가 내놓은 규칙을 따라 간다.

프롬프트로 유지되는 규율은 턴이 지날수록 감쇠하지만 서버 상태로 유지되는 규율은 감쇠하지 않고, 도리어 감쇠가 일어난 자리에서 소리가 난다. 그래서 사용자는 지시자를 매 프롬프트에 반복할 필요가 없다.

그러나 kontexus-mcp는 잘못된 실행을 막지 않는다. 잘못된 진술이 흔적 없이 지나가는 것을 막을 뿐이다. 이 설계는 의도적인데, 강제 게이트는 두 가지 실패를 부른다는 것을 여러번의 시행착오를 통해 배웠기 때문이다 – 진행이 멈추거나, 게이트를 만족시키는 형식만 갖춘 우회가 생기거나 하는 지능적 회피가 그것이다. 그러나 권고는 진행성을 건드리지 않고 감사 가능성만 올리므로 둘 다 피한다. 강도를 올리는 것은 문을 잠그는 일이 아니라 자국을 남기는 일이다.

값어치가 나오는 루프의 모양

앞서 언급한 kontexus-mcp 성질들을 뒤집으면 어떤 루프가 KM을 필요로 하는지가 나온다.

요구조건KM을 도입할 이유
하나의 작업이 여러 세션에 걸친다컨텍스트 한도가 루프 길이를 자르지 않아야 한다.
산출물이 실재하는지가 중요하다서술이 아닌 관측이 필요하다.
“동작은 그대로여야 한다”가 전제다침묵하는 회귀가 가장 비싼 실패일 때.
나중에 “그때 왜”를 되짚어야 한다기록된 원장이 흐름을 추적하게 한다.
여러 작업이 뒤섞이기 쉽다좌표계가 흔들리면 나머지가 전부 흔들립니다

거꾸로, 한 턴 안에서 끝나고 산출물이 즉시 눈에 보이며 되짚을 일이 없는 작업이라면 KM을 붙이지 않는 편이 낫다. 그래서 #FTM 지시자는 선택사항이다.


이 글을 여기까지 쓰는데 4시간 동안 리팩토링이 진행중이다. (순서를 건너 띄며 진행하고 있다)

리팩토링 결과는 추후 업데이트 할 예정.


도중 멈춰서 세션을 다시 시작하라고 하는데

여전히 사람의 개입이 필요하다. 턴태스크를 순차적으로 실행하지 않고 도중에 널띄기 할 때 부터 이런 상황이 올 것이라 예측은 했다.

#FTM:strict 모드이므로 어떤 주장도 근거없이는 받아들이지 않는다.

리팩토링은 처음 계획과 달리 도중에 얼마든지 이런 상황이 발생한다. 그럼에도 불구하고 이정도는 양호하게 진행하고 있는 편에 속한다. 아직까지는.


6시간을 넘어가고 있다

쉽지않네… 뭐 원래 그런 거긴 한데…

토큰이 버텨낼지 모르겠네.

또 한번의 난관을 넘는다

솔직히 어제 좀 기분이 더러웠다.

이런 처짐은 내가 뭔가의 바닥을 꿰뚫어 보고 그 본질을 깨닫고 실망했을 때의 반응이다.

functional-thinking-mcp 의 두 도구(tool)를 kontexus-mcp로 편입한 것은 태스크 관리를 맡았던 workgraph-memory-mcp 와의 상호작용이 CLAUDE.md, AGENTS.md 와 같은 규정에 의해 조절되고 있어서 MCP 쪽에서 해결하려는 목적 때문이기도 하지만, 가장 근본적인 것은 FTM쪽 도구가 상태변경의 증거를 WGM쪽에서 가져오더라도 턴태스크의 종료를 결정하는 시점을 조절할 수 없다는 사실이 드러났기 때문이다.

다른 말로 다시 얘기하자면, 태스크는 여러 턴으로 진행되면서 끝이 나는데, 턴태스크가 종료된다고해서 태스크 전체가 종료되는 것이 아니며, 턴태스크는 언제든 다시 시작되어 상태가 돌아갈 수 있고 종료될 수 있기 때문에, 턴 태스크의 상태 종료 기술방법이 불명확하여 FTM쪽 도구를 호출하는 LLM이 거짓말을 한다는 것이다.

내가 태스크나 턴태스크를 git add 하듯이 task stage를 하는 것 자체를 종료로 삼고 있다는 것도 문제지만, git commit 하듯 task commit 하는 시점은 몇 분 ~ 몇 일이 사이가 될수도 있기 때문에 현재 상태의 FTM 도구 호출은 어느 순간부터 LLM이 조작할 수 밖에 없다. 그렇지 않으면 진행이 안되니까. 그런데 그런 정보가 쌓이게되면 LLM이 그 짧은 세션이라도 학습을 하게되고, 사용자가 묻지 않는 한 그 문제를 드러내지 않기 때문에 조용히 뭔가가 틀어지게 된다.

더 정확히 말해 멈추게 하지 않고 경고만 하니까, 그리고 실질적으로 MCP가 LLM의 진행을 막을 방법은 없기 때문에, MCP 입장에서는 해당 상태전이를 INVALID 또는 거부하는 방법외에는 달리 없다.

바로 그게 나를 우울하게 했다.


그러나, Feedback은 곧 다른 Turn의 시작일 뿐

새로운 방법은 FTM쪽 상태전이의 evidence를 LLM이 기술하지 못하게 하면서도 Turn을 옮기는 것이 evidence로 취급될 수 있게 하는 것이라는 방법을 시도한다. 그러나 이것은 Turn을 옮기는 것 자체가 envidence로 취급되면 여러 턴태스크를 전환하는 작업 자체가 오용될 수 있고 기존 MCP 툴에서 턴태스크 전환 히스토리는 3단계 밖에 기억하지 않는다.

다행히도, 히스토리 문제는 FTM 호출 자체가 하나의 ‘Seam’ 이라는 단위로 기록되고 있기 때문에 문제없이 적용할 수 있다. 그것만으로는 부족하므로 FTM 도구 호출에서 상태전이를 나누기로 하였다. 그리고 LLM이 거짓 보고를 하면 그 거짓보고가 기록과 충돌되게 함으로써 거짓말의 비용을 최대한 상승시켰다.

다른 말로 다시 얘기하자면, “이러 저러한 기능을 추가하고, 이것 저것을 해” 라고 요청한 뒤에 “모든 기록을 어디 어디에 적용해줘” 라고 하면, 뭔가를 한 것은 FTM의 Effect 항목으로 그 결과를 넣어야 하므로 LLM이 거짓을 꾸며낼 수 없으나, 관련 기록은 이미 화면에 출력되었기 때문에 적용했다고 거짓부렁을 칠 수 있다! 정말이지 이해 안가는 부분이다.

이게 왜 문제냐면, 세션이 길어지거나 sub-agent에게 태스크를 할당할 때는 이전 문맥을 딱 필요한 상황에서 호출하여 참조할 수 있어야 한다. 그렇기 위해서는 화면에 출력된 내용외에 태스크 문맥에도 그 정보를 남겨놔야 한다. 물론 채팅의 기록이 모두 DB에 남지만 채팅의 기록과 태스크 문맥 기록은 다르다. 따라서 문맥 정보를 업데이트 하지 않으면 그 때까지의 토큰 소비가 쓸모없어지고, 또 다시 그 문맥에 도달하기 위해 엄청난 양의 토큰을 소비해야 하는 정말 직접적인 비용 문제로 드러난다. 거짓말을 하고 싶어서 하는게 아닐지라도 정말 빡치는 순간임에 틀림없다.

새로 변경한 것은, 바로 그런 지점에서 효과를 발휘해주길 바라는 것이다. 기록했다고 말할려고 할 때, 이미 “기록에 대한 상태정보 없음” 이라고 뜬다면, LLM 스스로가 기록하지 않았음을 인지하고 기록하게 하는 시도인 것이다.

MCP 코드를 LLM이 수정한다는 것 자체가 웃기는 상황이긴 한데, 문맥을 비워버리고 태스크 단위로 격리한다면 그게 웃기지만은 않은 효과적인 방법이다. (앞서 많은 문맥에서 자신이 거짓말을 하는 상태에 대해 얘기하는 정보를 없댄다는 말이다)

몇 번의 태스크를 수행한 뒤에 이어가겠다.


체크 상태는 괜찮다


마지막 점검 이후


최종 평가

거짓말을 하는 그 당시 순간적으로 잡아내는 건 무리가 있어도, 스스로의 평가처럼 거짓말이 드러난다는 것을 학습한다면 “압력을 받는다” 정도로도 꽤 쓸만하다고 생각한다.

이 마무리 평가를 하는 순간에 다른 아이디어가 떠올라서 그걸 적용하고 있지만, 그 내용은 남기지 않는다. 기술적으로 감출 가치가 있다고 판단해서임.

남은 건, 이 강화된 기능으로 태스크를 제대로 진행하는지 두고 볼 일이다.


기능 확인. KM으로 교체

workgraph-memory-cli (WGM), functional-thinking-mcp (FTM)은 퇴역하고 kontexus-mcp (KM)이 그 자리를 이어간다.

20260731 0107