article · 2026-07-03

모호한 말 한마디는 어떻게 정확한 실행이 되는가

내가 만든 user-utterance-grounding의 실제 구현을 근거로, keyword 매칭과 margin 기반 확신(confidence), 의도(intent) 분류, scope 기반 정렬(grounding)이 모호한 발화를 정확한 실행으로 좁혀가는 과정을 정리한다.

"리서치 돌려줘"라는 말에는 프로젝트 이름이 없다. 그런데도 시스템은 대개 어느 레포로 가야 할지 알아낸다. 내가 만든 UUG(user-utterance-grounding)를 다시 들여다보고서야, 그 "대개"가 어떻게 만들어지는지 정리가 됐다. 점수 하나로 끝내지 않는다. keyword 매칭으로 후보를 찾고, 1위와 2위 후보의 점수 차이(margin)로 확신을 검증하고, 애매하면 되묻고, intent와 scope로 범위를 좁힌 뒤에야 정렬(grounding)해서 넘긴다.


1. 동점이 문제였다

테스트 스위트를 돌려보고 나서야 알았다. UUG가 틀리는 지점은 거의 다 한 곳에 몰려 있었다. 1위 후보와 2위 후보가 똑같은 점수를 받는, 그러니까 동점이다.

"리서치 돌려줘", "정리하자" 같은 발화에는 프로젝트명이 없다. UUG는 발화 속 키워드를 미리 등록해둔 규칙과 대조해서 후보를 찾는다. "리서치"라는 단어가 있으면 run-research 후보에 점수가 붙고, "정리"가 있으면 tidy-organize 후보에 점수가 붙는 식이다.

점수 하나만 보고 무조건 1위를 고르는 방식이었다면, 동점 상황에서 시스템은 매번 둘 중 하나를 억지로 찍었을 거고, 그 판단은 절반은 틀렸을 거다. 실제 테스트 스위트도 이걸 확인해준다. 틀린 판정은 거의 다 동점에서 나온다.

2. margin: 점수 차이가 확신이다

확신(confidence)이라는 말을 종종 쓰지만, UUG 안에는 그런 이름의 필드가 없다. 대신 margin이 있다. margin은 1위 후보와 2위 후보의 점수 차이다. 점수가 아무리 높아도 2위와 거의 차이가 없으면 margin은 0에 가깝고, 그건 시스템이 그 판단에 자신이 없다는 뜻이다.

UUG는 이 margin에 기준선을 두고 판정한다. 사용자 intent는 margin이 1 미만이면 애매함(unclear)으로 분류해 되묻는다. 반면 도메인 intent는 점수가 조금이라도 높은 쪽을 그대로 확정한다. 이미 실행 대상이 좁게 정해져 있어서 되물을 필요가 적기 때문이다.

1절에서 본 동점 문제가 여기서 풀린다. 동점은 곧 margin 0이니, 사용자 intent라면 자동으로 애매함으로 분류돼 되묻기(HITL)로 넘어간다. 추측해서 실행하는 대신, 확신이 없다는 걸 시스템 스스로 인정하는 셈이다.

3. intent: 발화가 원하는 것의 이름

margin을 통과하면 intent가 하나로 정해진다. 의도(intent)는 발화가 실제로 원하는 게 뭔지를 정리한 이름이다. UUG는 RDF Turtle 형식으로 다섯 가지를 등록해뒀다. 프로젝트 작업 시작(work-on-project), 지식베이스 추가(add-to-knowledge), 리서치 실행(run-research), 메모리 기록(record-memory), 구조 정리(tidy-organize).

각 intent는 다섯 가지 정보를 갖는다. 무슨 종류의 동작인지(verb_concept: 작업·생성·조회·수정), 무엇을 대상으로 하는지(target_concept), 얼마나 좁은 범위에서 실행되는지(scope), 이 intent를 트리거하는 키워드 목록(trigger_keywords), 그리고 발화에 빠진 정보를 어떻게 채울지 정해둔 슬롯 규칙(slot_specs)이다. "리서치", "조사해줘"라는 키워드가 run-research의 trigger_keywords에 등록돼 있는 식이다.

4. scope: 얼마나 좁게 실행할 것인가

다섯 가지 정보 중 scope가 가장 낯설다. scope는 이 intent가 건드리는 범위, 그러니까 후보를 찾을 때 얼마나 좁게 봐야 하는지를 정해둔 값이다. 스키마에는 네 단계가 선언돼 있다. meta(사용자 개인 차원), repository(레포 하나), workflow(레포 안의 워크플로 하나), workflow.executionRail(이미 실행 중인 워크플로의 특정 실행 경로 하나). 뒤로 갈수록 범위가 좁아진다.

실제로 등록된 다섯 intent 중 넷, work-on-project·add-to-knowledge·run-research·tidy-organize는 전부 repository 스코프다. 어느 레포에서 벌어지는 일인지가 핵심이라는 뜻이다. 예외는 record-memory 하나뿐이다. "기록해줘", "메모해줘"는 특정 레포가 아니라 사용자 자신에 관한 일이라 meta 스코프로 등록돼 있다. workflow와 workflow.executionRail은 스키마에 선언만 돼 있을 뿐, 지금 등록된 다섯 intent 중 이 값을 쓰는 건 아직 하나도 없다. 실행 중인 워크플로의 한 지점을 다시 실행하라는 명령처럼 앞으로 더 좁은 통제가 필요해지면 쓰라고 미리 비워둔 자리다.

5. 정렬(grounding): 슬롯을 채우고 타깃에 넘긴다

intent와 scope까지 정해지면 마지막 단계가 정렬(grounding)이다. 정렬은 이 intent를 실제로 실행할 수 있는 형태로 완성하는 작업이다. 발화에 빠진 정보, 예를 들어 어느 프로젝트인지가 명시되지 않았으면 세션 맥락에서 채우고, 그것도 없으면 도메인별 기본값을 쓴다. 그렇게 채운 슬롯과 절대경로(레포별 machine.yaml의 anchor로 해석한 경로)를 묶어 intent_id, verb, slots를 반환한다.

여기서 결과가 향하는 곳은 타깃 프로젝트의 실행 창구(CLI)다. UUG는 이 CLI를 subprocess로 그대로 호출하고, 실제로 슬롯을 검증하고 실행하는 일은 전부 타깃 프로젝트(MSO 등) 쪽에서 일어난다. 방향은 한쪽으로만 난다. MSO는 UUG를 몰라도 정상 작동하고, UUG가 뭘 넘기든 MSO 입장에선 자기 CLI로 들어오는 요청 하나일 뿐이다.

6. 그 사이, 지식은 project에서 user로 승격된다

지금까지는 "무엇을 실행할지"를 다루는 intent 얘기였다. 그런데 UUG가 다루는 것 중에는 실행이 아니라 저장에 관한 것도 있다. record-memory intent가 그렇다.

README.md: "User-scope, cross-project(global 레이어). MSO가 프로젝트 단위라면 UUG는 사용자/전-프로젝트 레이어다."

여기서 말하는 스코프는 4절의 intent scope(meta/repository/workflow/workflow.executionRail)와는 다른 축이다. 이건 저장소가 project 것인지 user 것인지를 가르는 스코프다. UUG는 user 스코프를 갖는다. 레포 하나에 묶이지 않고 여러 프로젝트를 가로지르는 사용자 맥락(UC/UP/UF)이다. 반대로 work-memory는 레포 하나에 묶인 project 스코프다. 둘은 별개의 저장소지만 연결돼 있다. 같은 판단이 여러 프로젝트에서 반복 관측되면, project 스코프의 work-memory에서 user 스코프로 승격된다. 이때 내용을 복사하지 않고 derived-from 관계로 이어, 원래 어디서 나온 판단이었는지(provenance)는 남긴다.

7. 지금 어디까지 됐나

지금 "리서치 돌려줘"라고 말하면 keyword 매칭부터 margin 판정, intent 확정, 정렬까지 실제로 동작한다. 반면 예전부터 여기저기 흩어져 있던 개인 메모리 데이터를 이 구조로 옮기는 일은 아직 손대지 못했다.

  • 완료: grounding 파이프라인(keyword 매칭, margin 기반 임계값 판정, slot-fill, HITL 되묻기)과 위치 인덱스/리졸버(ug ground / resolve / doctor / list)는 동작한다. pytest 검증 통과.
  • 완료: UUG에서 타깃 프로젝트로의 배선은 subprocess CLI 호출로 실측 확인됐다.
  • 부분: uug-user-memory 엔진(UC/UP/UF)은 스키마와 승격(derived-from) 구조까지 완성됐지만, 기존에 흩어져 있던 개인 메모리 데이터를 실제로 옮기는 마이그레이션은 아직이다.
  • 부분: uug-pattern-analytics는 아직 intent 반복 횟수를 세는 수준이고, 그 카운트를 user-pattern 후보로 승격하는 로직은 더 붙어야 한다.
  • 미해결: 키워드 매칭이 실패했을 때 LLM으로 복구하는 상위 티어는 아직 붙지 않았다. 지금은 키워드 매칭 실패가 곧 grounding 실패다.

8. 정리

전체 흐름을 한 장으로 정리하면 이렇다.

다시 처음 질문으로 돌아가면, "리서치 돌려줘"라는 말에 프로젝트명이 없어도 시스템이 알아서 가는 이유는 간단하다. 점수 하나로 끝내지 않고 margin으로 확신을 재확인하고, 애매하면 되묻고, intent와 scope로 범위를 좁힌 뒤에야 정렬해서 넘긴다. 그리고 그 결과가 실행이 아니라 지식이라면, 프로젝트 하나에 갇히지 않고 반복되는 만큼 user 스코프로 승격된다.

동점을 무시하지 않는 것, 그게 이 파이프라인 전체의 시작이었다.


출처: user-utterance-grounding 레포의 README.md, skills/uug-grounding/scripts/ug.py, skills/uug-grounding/src/lookup.py, skills/uug-grounding/instances/user_intents.ttl, skills/uug-grounding/references/schemas/nlu_intent.yaml, skills/uug-grounding/tests/test_lookup.py, skills/uug-user-memory/schema.yaml